Initial commit
This commit is contained in:
8
skills/codebase-auditor/scripts/analyzers/__init__.py
Normal file
8
skills/codebase-auditor/scripts/analyzers/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
||||
"""
|
||||
Analyzer modules for codebase auditing.
|
||||
|
||||
Each analyzer implements an analyze(codebase_path, metadata) function
|
||||
that returns a list of findings.
|
||||
"""
|
||||
|
||||
__version__ = '1.0.0'
|
||||
411
skills/codebase-auditor/scripts/analyzers/code_quality.py
Normal file
411
skills/codebase-auditor/scripts/analyzers/code_quality.py
Normal file
@@ -0,0 +1,411 @@
|
||||
"""
|
||||
Code Quality Analyzer
|
||||
|
||||
Analyzes code for:
|
||||
- Cyclomatic complexity
|
||||
- Code duplication
|
||||
- Code smells
|
||||
- File/function length
|
||||
- Language-specific issues (TypeScript/JavaScript)
|
||||
"""
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Dict, List
|
||||
|
||||
|
||||
def analyze(codebase_path: Path, metadata: Dict) -> List[Dict]:
|
||||
"""
|
||||
Analyze codebase for code quality issues.
|
||||
|
||||
Args:
|
||||
codebase_path: Path to codebase
|
||||
metadata: Project metadata from discovery phase
|
||||
|
||||
Returns:
|
||||
List of findings with severity, location, and remediation info
|
||||
"""
|
||||
findings = []
|
||||
|
||||
# Determine which languages to analyze
|
||||
tech_stack = metadata.get('tech_stack', {})
|
||||
|
||||
if tech_stack.get('javascript') or tech_stack.get('typescript'):
|
||||
findings.extend(analyze_javascript_typescript(codebase_path))
|
||||
|
||||
if tech_stack.get('python'):
|
||||
findings.extend(analyze_python(codebase_path))
|
||||
|
||||
# General analysis (language-agnostic)
|
||||
findings.extend(analyze_file_sizes(codebase_path))
|
||||
findings.extend(analyze_dead_code(codebase_path, tech_stack))
|
||||
|
||||
return findings
|
||||
|
||||
|
||||
def analyze_javascript_typescript(codebase_path: Path) -> List[Dict]:
|
||||
"""Analyze JavaScript/TypeScript specific quality issues."""
|
||||
findings = []
|
||||
extensions = {'.js', '.jsx', '.ts', '.tsx'}
|
||||
exclude_dirs = {'node_modules', '.git', 'dist', 'build', '.next', 'coverage'}
|
||||
|
||||
for file_path in codebase_path.rglob('*'):
|
||||
if (file_path.suffix in extensions and
|
||||
not any(excluded in file_path.parts for excluded in exclude_dirs)):
|
||||
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
|
||||
content = f.read()
|
||||
lines = content.split('\n')
|
||||
|
||||
# Check for TypeScript 'any' type
|
||||
if file_path.suffix in {'.ts', '.tsx'}:
|
||||
findings.extend(check_any_usage(file_path, content, lines))
|
||||
|
||||
# Check for 'var' keyword
|
||||
findings.extend(check_var_usage(file_path, content, lines))
|
||||
|
||||
# Check for console.log statements
|
||||
findings.extend(check_console_log(file_path, content, lines))
|
||||
|
||||
# Check for loose equality
|
||||
findings.extend(check_loose_equality(file_path, content, lines))
|
||||
|
||||
# Check cyclomatic complexity (simplified)
|
||||
findings.extend(check_complexity(file_path, content, lines))
|
||||
|
||||
# Check function length
|
||||
findings.extend(check_function_length(file_path, content, lines))
|
||||
|
||||
except Exception as e:
|
||||
# Skip files that can't be read
|
||||
pass
|
||||
|
||||
return findings
|
||||
|
||||
|
||||
def check_any_usage(file_path: Path, content: str, lines: List[str]) -> List[Dict]:
|
||||
"""Check for TypeScript 'any' type usage."""
|
||||
findings = []
|
||||
|
||||
# Pattern to match 'any' type (excluding comments)
|
||||
any_pattern = re.compile(r':\s*any\b|<any>|Array<any>|\bany\[\]')
|
||||
|
||||
for line_num, line in enumerate(lines, start=1):
|
||||
# Skip comments
|
||||
if line.strip().startswith('//') or line.strip().startswith('/*') or line.strip().startswith('*'):
|
||||
continue
|
||||
|
||||
if any_pattern.search(line):
|
||||
findings.append({
|
||||
'severity': 'medium',
|
||||
'category': 'code_quality',
|
||||
'subcategory': 'typescript_strict_mode',
|
||||
'title': "Use of 'any' type violates TypeScript strict mode",
|
||||
'description': f"Found 'any' type on line {line_num}",
|
||||
'file': str(file_path.relative_to(file_path.parents[len(file_path.parts) - file_path.parts.index('annex') - 2])),
|
||||
'line': line_num,
|
||||
'code_snippet': line.strip(),
|
||||
'impact': 'Reduces type safety and defeats the purpose of TypeScript',
|
||||
'remediation': 'Replace "any" with specific types or use "unknown" with type guards',
|
||||
'effort': 'low',
|
||||
})
|
||||
|
||||
return findings
|
||||
|
||||
|
||||
def check_var_usage(file_path: Path, content: str, lines: List[str]) -> List[Dict]:
|
||||
"""Check for 'var' keyword usage."""
|
||||
findings = []
|
||||
|
||||
var_pattern = re.compile(r'\bvar\s+\w+')
|
||||
|
||||
for line_num, line in enumerate(lines, start=1):
|
||||
if line.strip().startswith('//') or line.strip().startswith('/*'):
|
||||
continue
|
||||
|
||||
if var_pattern.search(line):
|
||||
findings.append({
|
||||
'severity': 'low',
|
||||
'category': 'code_quality',
|
||||
'subcategory': 'modern_javascript',
|
||||
'title': "Use of 'var' keyword is deprecated",
|
||||
'description': f"Found 'var' keyword on line {line_num}",
|
||||
'file': str(file_path.relative_to(file_path.parents[len(file_path.parts) - file_path.parts.index('annex') - 2])),
|
||||
'line': line_num,
|
||||
'code_snippet': line.strip(),
|
||||
'impact': 'Function-scoped variables can lead to bugs; block-scoped (let/const) is preferred',
|
||||
'remediation': "Replace 'var' with 'const' (for values that don't change) or 'let' (for values that change)",
|
||||
'effort': 'low',
|
||||
})
|
||||
|
||||
return findings
|
||||
|
||||
|
||||
def check_console_log(file_path: Path, content: str, lines: List[str]) -> List[Dict]:
|
||||
"""Check for console.log statements in production code."""
|
||||
findings = []
|
||||
|
||||
# Skip if it's in a test file
|
||||
if 'test' in file_path.name or 'spec' in file_path.name or '__tests__' in str(file_path):
|
||||
return findings
|
||||
|
||||
console_pattern = re.compile(r'\bconsole\.(log|debug|info|warn|error)\(')
|
||||
|
||||
for line_num, line in enumerate(lines, start=1):
|
||||
if line.strip().startswith('//'):
|
||||
continue
|
||||
|
||||
if console_pattern.search(line):
|
||||
findings.append({
|
||||
'severity': 'medium',
|
||||
'category': 'code_quality',
|
||||
'subcategory': 'production_code',
|
||||
'title': 'Console statement in production code',
|
||||
'description': f"Found console statement on line {line_num}",
|
||||
'file': str(file_path.relative_to(file_path.parents[len(file_path.parts) - file_path.parts.index('annex') - 2])),
|
||||
'line': line_num,
|
||||
'code_snippet': line.strip(),
|
||||
'impact': 'Console statements should not be in production code; use proper logging',
|
||||
'remediation': 'Remove console statement or replace with proper logging framework',
|
||||
'effort': 'low',
|
||||
})
|
||||
|
||||
return findings
|
||||
|
||||
|
||||
def check_loose_equality(file_path: Path, content: str, lines: List[str]) -> List[Dict]:
|
||||
"""Check for loose equality operators (== instead of ===)."""
|
||||
findings = []
|
||||
|
||||
loose_eq_pattern = re.compile(r'[^!<>]==[^=]|[^!<>]!=[^=]')
|
||||
|
||||
for line_num, line in enumerate(lines, start=1):
|
||||
if line.strip().startswith('//') or line.strip().startswith('/*'):
|
||||
continue
|
||||
|
||||
if loose_eq_pattern.search(line):
|
||||
findings.append({
|
||||
'severity': 'low',
|
||||
'category': 'code_quality',
|
||||
'subcategory': 'code_smell',
|
||||
'title': 'Loose equality operator used',
|
||||
'description': f"Found '==' or '!=' on line {line_num}, should use '===' or '!=='",
|
||||
'file': str(file_path.relative_to(file_path.parents[len(file_path.parts) - file_path.parts.index('annex') - 2])),
|
||||
'line': line_num,
|
||||
'code_snippet': line.strip(),
|
||||
'impact': 'Loose equality can lead to unexpected type coercion bugs',
|
||||
'remediation': "Replace '==' with '===' and '!=' with '!=='",
|
||||
'effort': 'low',
|
||||
})
|
||||
|
||||
return findings
|
||||
|
||||
|
||||
def check_complexity(file_path: Path, content: str, lines: List[str]) -> List[Dict]:
|
||||
"""
|
||||
Check cyclomatic complexity (simplified).
|
||||
|
||||
Counts decision points: if, else, while, for, case, catch, &&, ||, ?
|
||||
"""
|
||||
findings = []
|
||||
|
||||
# Find function declarations
|
||||
func_pattern = re.compile(r'(function\s+\w+|const\s+\w+\s*=\s*\([^)]*\)\s*=>|\w+\s*\([^)]*\)\s*{)')
|
||||
|
||||
current_function = None
|
||||
current_function_line = 0
|
||||
brace_depth = 0
|
||||
complexity = 0
|
||||
|
||||
for line_num, line in enumerate(lines, start=1):
|
||||
stripped = line.strip()
|
||||
|
||||
# Track braces to find function boundaries
|
||||
brace_depth += stripped.count('{') - stripped.count('}')
|
||||
|
||||
# New function started
|
||||
if func_pattern.search(line) and brace_depth >= 1:
|
||||
# Save previous function if exists
|
||||
if current_function and complexity > 10:
|
||||
severity = 'critical' if complexity > 20 else 'high' if complexity > 15 else 'medium'
|
||||
findings.append({
|
||||
'severity': severity,
|
||||
'category': 'code_quality',
|
||||
'subcategory': 'complexity',
|
||||
'title': f'High cyclomatic complexity ({complexity})',
|
||||
'description': f'Function has complexity of {complexity}',
|
||||
'file': str(file_path.relative_to(file_path.parents[len(file_path.parts) - file_path.parts.index('annex') - 2])),
|
||||
'line': current_function_line,
|
||||
'code_snippet': current_function,
|
||||
'impact': 'High complexity makes code difficult to understand, test, and maintain',
|
||||
'remediation': 'Refactor into smaller functions, extract complex conditions',
|
||||
'effort': 'medium' if complexity < 20 else 'high',
|
||||
})
|
||||
|
||||
# Start new function
|
||||
current_function = stripped
|
||||
current_function_line = line_num
|
||||
complexity = 1 # Base complexity
|
||||
|
||||
# Count complexity contributors
|
||||
if current_function:
|
||||
complexity += stripped.count('if ')
|
||||
complexity += stripped.count('else if')
|
||||
complexity += stripped.count('while ')
|
||||
complexity += stripped.count('for ')
|
||||
complexity += stripped.count('case ')
|
||||
complexity += stripped.count('catch ')
|
||||
complexity += stripped.count('&&')
|
||||
complexity += stripped.count('||')
|
||||
complexity += stripped.count('?')
|
||||
|
||||
return findings
|
||||
|
||||
|
||||
def check_function_length(file_path: Path, content: str, lines: List[str]) -> List[Dict]:
|
||||
"""Check for overly long functions."""
|
||||
findings = []
|
||||
|
||||
func_pattern = re.compile(r'(function\s+\w+|const\s+\w+\s*=\s*\([^)]*\)\s*=>|\w+\s*\([^)]*\)\s*{)')
|
||||
|
||||
current_function = None
|
||||
current_function_line = 0
|
||||
function_lines = 0
|
||||
brace_depth = 0
|
||||
|
||||
for line_num, line in enumerate(lines, start=1):
|
||||
stripped = line.strip()
|
||||
|
||||
if func_pattern.search(line):
|
||||
# Check previous function
|
||||
if current_function and function_lines > 50:
|
||||
severity = 'high' if function_lines > 100 else 'medium'
|
||||
findings.append({
|
||||
'severity': severity,
|
||||
'category': 'code_quality',
|
||||
'subcategory': 'function_length',
|
||||
'title': f'Long function ({function_lines} lines)',
|
||||
'description': f'Function is {function_lines} lines long (recommended: < 50)',
|
||||
'file': str(file_path.relative_to(file_path.parents[len(file_path.parts) - file_path.parts.index('annex') - 2])),
|
||||
'line': current_function_line,
|
||||
'code_snippet': current_function,
|
||||
'impact': 'Long functions are harder to understand, test, and maintain',
|
||||
'remediation': 'Extract smaller functions for distinct responsibilities',
|
||||
'effort': 'medium',
|
||||
})
|
||||
|
||||
current_function = stripped
|
||||
current_function_line = line_num
|
||||
function_lines = 0
|
||||
brace_depth = 0
|
||||
|
||||
if current_function:
|
||||
function_lines += 1
|
||||
brace_depth += stripped.count('{') - stripped.count('}')
|
||||
|
||||
if brace_depth == 0 and function_lines > 1:
|
||||
# Function ended
|
||||
current_function = None
|
||||
|
||||
return findings
|
||||
|
||||
|
||||
def analyze_python(codebase_path: Path) -> List[Dict]:
|
||||
"""Analyze Python-specific quality issues."""
|
||||
findings = []
|
||||
# Python analysis to be implemented
|
||||
# Would check: PEP 8 violations, complexity, type hints, etc.
|
||||
return findings
|
||||
|
||||
|
||||
def analyze_file_sizes(codebase_path: Path) -> List[Dict]:
|
||||
"""Check for overly large files."""
|
||||
findings = []
|
||||
exclude_dirs = {'node_modules', '.git', 'dist', 'build', '__pycache__'}
|
||||
code_extensions = {'.js', '.jsx', '.ts', '.tsx', '.py', '.java', '.go', '.rs'}
|
||||
|
||||
for file_path in codebase_path.rglob('*'):
|
||||
if (file_path.is_file() and
|
||||
file_path.suffix in code_extensions and
|
||||
not any(excluded in file_path.parts for excluded in exclude_dirs)):
|
||||
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
|
||||
lines = len(f.readlines())
|
||||
|
||||
if lines > 500:
|
||||
severity = 'high' if lines > 1000 else 'medium'
|
||||
findings.append({
|
||||
'severity': severity,
|
||||
'category': 'code_quality',
|
||||
'subcategory': 'file_length',
|
||||
'title': f'Large file ({lines} lines)',
|
||||
'description': f'File has {lines} lines (recommended: < 500)',
|
||||
'file': str(file_path.relative_to(file_path.parents[len(file_path.parts) - file_path.parts.index('annex') - 2])),
|
||||
'line': 1,
|
||||
'code_snippet': None,
|
||||
'impact': 'Large files are difficult to navigate and understand',
|
||||
'remediation': 'Split into multiple smaller, focused modules',
|
||||
'effort': 'high',
|
||||
})
|
||||
except:
|
||||
pass
|
||||
|
||||
return findings
|
||||
|
||||
|
||||
def analyze_dead_code(codebase_path: Path, tech_stack: Dict) -> List[Dict]:
|
||||
"""Detect potential dead code (commented-out code blocks)."""
|
||||
findings = []
|
||||
exclude_dirs = {'node_modules', '.git', 'dist', 'build'}
|
||||
|
||||
extensions = set()
|
||||
if tech_stack.get('javascript') or tech_stack.get('typescript'):
|
||||
extensions.update({'.js', '.jsx', '.ts', '.tsx'})
|
||||
if tech_stack.get('python'):
|
||||
extensions.add('.py')
|
||||
|
||||
for file_path in codebase_path.rglob('*'):
|
||||
if (file_path.suffix in extensions and
|
||||
not any(excluded in file_path.parts for excluded in exclude_dirs)):
|
||||
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
|
||||
lines = f.readlines()
|
||||
|
||||
# Count consecutive commented lines with code-like content
|
||||
comment_block_size = 0
|
||||
block_start_line = 0
|
||||
|
||||
for line_num, line in enumerate(lines, start=1):
|
||||
stripped = line.strip()
|
||||
|
||||
# Check if line is commented code
|
||||
if (stripped.startswith('//') and
|
||||
any(keyword in stripped for keyword in ['function', 'const', 'let', 'var', 'if', 'for', 'while', '{', '}', ';'])):
|
||||
if comment_block_size == 0:
|
||||
block_start_line = line_num
|
||||
comment_block_size += 1
|
||||
else:
|
||||
# End of comment block
|
||||
if comment_block_size >= 5: # 5+ lines of commented code
|
||||
findings.append({
|
||||
'severity': 'low',
|
||||
'category': 'code_quality',
|
||||
'subcategory': 'dead_code',
|
||||
'title': f'Commented-out code block ({comment_block_size} lines)',
|
||||
'description': f'Found {comment_block_size} lines of commented code',
|
||||
'file': str(file_path.relative_to(file_path.parents[len(file_path.parts) - file_path.parts.index('annex') - 2])),
|
||||
'line': block_start_line,
|
||||
'code_snippet': None,
|
||||
'impact': 'Commented code clutters codebase and reduces readability',
|
||||
'remediation': 'Remove commented code (it\'s in version control if needed)',
|
||||
'effort': 'low',
|
||||
})
|
||||
comment_block_size = 0
|
||||
|
||||
except:
|
||||
pass
|
||||
|
||||
return findings
|
||||
31
skills/codebase-auditor/scripts/analyzers/dependencies.py
Normal file
31
skills/codebase-auditor/scripts/analyzers/dependencies.py
Normal file
@@ -0,0 +1,31 @@
|
||||
"""
|
||||
Dependencies Analyzer
|
||||
|
||||
Analyzes:
|
||||
- Outdated dependencies
|
||||
- Vulnerable dependencies
|
||||
- License compliance
|
||||
- Dependency health
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Dict, List
|
||||
|
||||
|
||||
def analyze(codebase_path: Path, metadata: Dict) -> List[Dict]:
|
||||
"""
|
||||
Analyze dependencies for issues.
|
||||
|
||||
Args:
|
||||
codebase_path: Path to codebase
|
||||
metadata: Project metadata
|
||||
|
||||
Returns:
|
||||
List of dependency-related findings
|
||||
"""
|
||||
findings = []
|
||||
|
||||
# Placeholder implementation
|
||||
# In production, this would integrate with npm audit, pip-audit, etc.
|
||||
|
||||
return findings
|
||||
30
skills/codebase-auditor/scripts/analyzers/performance.py
Normal file
30
skills/codebase-auditor/scripts/analyzers/performance.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""
|
||||
Performance Analyzer
|
||||
|
||||
Analyzes:
|
||||
- Bundle sizes
|
||||
- Build times
|
||||
- Runtime performance indicators
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Dict, List
|
||||
|
||||
|
||||
def analyze(codebase_path: Path, metadata: Dict) -> List[Dict]:
|
||||
"""
|
||||
Analyze performance issues.
|
||||
|
||||
Args:
|
||||
codebase_path: Path to codebase
|
||||
metadata: Project metadata
|
||||
|
||||
Returns:
|
||||
List of performance-related findings
|
||||
"""
|
||||
findings = []
|
||||
|
||||
# Placeholder implementation
|
||||
# In production, this would analyze bundle sizes, check build configs, etc.
|
||||
|
||||
return findings
|
||||
235
skills/codebase-auditor/scripts/analyzers/security_scan.py
Normal file
235
skills/codebase-auditor/scripts/analyzers/security_scan.py
Normal file
@@ -0,0 +1,235 @@
|
||||
"""
|
||||
Security Scanner
|
||||
|
||||
Analyzes codebase for:
|
||||
- Secrets in code (API keys, tokens, passwords)
|
||||
- Dependency vulnerabilities
|
||||
- Common security anti-patterns
|
||||
- OWASP Top 10 issues
|
||||
"""
|
||||
|
||||
import re
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Dict, List
|
||||
|
||||
|
||||
# Common patterns for secrets
|
||||
SECRET_PATTERNS = {
|
||||
'api_key': re.compile(r'(api[_-]?key|apikey)\s*[=:]\s*["\']([a-zA-Z0-9_-]{20,})["\']', re.IGNORECASE),
|
||||
'aws_key': re.compile(r'AKIA[0-9A-Z]{16}'),
|
||||
'generic_secret': re.compile(r'(secret|password|passwd|pwd)\s*[=:]\s*["\']([^"\'\s]{8,})["\']', re.IGNORECASE),
|
||||
'private_key': re.compile(r'-----BEGIN (RSA |)PRIVATE KEY-----'),
|
||||
'jwt': re.compile(r'eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+'),
|
||||
'github_token': re.compile(r'gh[pousr]_[A-Za-z0-9_]{36}'),
|
||||
'slack_token': re.compile(r'xox[baprs]-[0-9]{10,12}-[0-9]{10,12}-[a-zA-Z0-9]{24,32}'),
|
||||
}
|
||||
|
||||
|
||||
def analyze(codebase_path: Path, metadata: Dict) -> List[Dict]:
|
||||
"""
|
||||
Analyze codebase for security issues.
|
||||
|
||||
Args:
|
||||
codebase_path: Path to codebase
|
||||
metadata: Project metadata from discovery phase
|
||||
|
||||
Returns:
|
||||
List of security findings
|
||||
"""
|
||||
findings = []
|
||||
|
||||
# Scan for secrets
|
||||
findings.extend(scan_for_secrets(codebase_path))
|
||||
|
||||
# Scan dependencies for vulnerabilities
|
||||
if metadata.get('tech_stack', {}).get('javascript'):
|
||||
findings.extend(scan_npm_dependencies(codebase_path))
|
||||
|
||||
# Check for common security anti-patterns
|
||||
findings.extend(scan_security_antipatterns(codebase_path, metadata))
|
||||
|
||||
return findings
|
||||
|
||||
|
||||
def scan_for_secrets(codebase_path: Path) -> List[Dict]:
|
||||
"""Scan for hardcoded secrets in code."""
|
||||
findings = []
|
||||
exclude_dirs = {'node_modules', '.git', 'dist', 'build', '__pycache__', '.venv', 'venv'}
|
||||
exclude_files = {'.env.example', 'package-lock.json', 'yarn.lock'}
|
||||
|
||||
# File extensions to scan
|
||||
code_extensions = {'.js', '.jsx', '.ts', '.tsx', '.py', '.java', '.go', '.rb', '.php', '.yml', '.yaml', '.json', '.env'}
|
||||
|
||||
for file_path in codebase_path.rglob('*'):
|
||||
if (file_path.is_file() and
|
||||
file_path.suffix in code_extensions and
|
||||
file_path.name not in exclude_files and
|
||||
not any(excluded in file_path.parts for excluded in exclude_dirs)):
|
||||
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
|
||||
content = f.read()
|
||||
lines = content.split('\n')
|
||||
|
||||
for pattern_name, pattern in SECRET_PATTERNS.items():
|
||||
matches = pattern.finditer(content)
|
||||
|
||||
for match in matches:
|
||||
# Find line number
|
||||
line_num = content[:match.start()].count('\n') + 1
|
||||
|
||||
# Skip if it's clearly a placeholder or example
|
||||
matched_text = match.group(0)
|
||||
if is_placeholder(matched_text):
|
||||
continue
|
||||
|
||||
findings.append({
|
||||
'severity': 'critical',
|
||||
'category': 'security',
|
||||
'subcategory': 'secrets',
|
||||
'title': f'Potential {pattern_name.replace("_", " ")} found in code',
|
||||
'description': f'Found potential secret on line {line_num}',
|
||||
'file': str(file_path.relative_to(codebase_path)),
|
||||
'line': line_num,
|
||||
'code_snippet': lines[line_num - 1].strip() if line_num <= len(lines) else '',
|
||||
'impact': 'Exposed secrets can lead to unauthorized access and data breaches',
|
||||
'remediation': 'Remove secret from code and use environment variables or secret management tools',
|
||||
'effort': 'low',
|
||||
})
|
||||
|
||||
except:
|
||||
pass
|
||||
|
||||
return findings
|
||||
|
||||
|
||||
def is_placeholder(text: str) -> bool:
|
||||
"""Check if a potential secret is actually a placeholder."""
|
||||
placeholders = [
|
||||
'your_api_key', 'your_secret', 'example', 'placeholder', 'test',
|
||||
'dummy', 'sample', 'xxx', '000', 'abc123', 'changeme', 'replace_me',
|
||||
'my_api_key', 'your_key_here', 'insert_key_here'
|
||||
]
|
||||
|
||||
text_lower = text.lower()
|
||||
return any(placeholder in text_lower for placeholder in placeholders)
|
||||
|
||||
|
||||
def scan_npm_dependencies(codebase_path: Path) -> List[Dict]:
|
||||
"""Scan npm dependencies for known vulnerabilities."""
|
||||
findings = []
|
||||
|
||||
package_json = codebase_path / 'package.json'
|
||||
if not package_json.exists():
|
||||
return findings
|
||||
|
||||
try:
|
||||
with open(package_json, 'r') as f:
|
||||
pkg = json.load(f)
|
||||
|
||||
deps = {**pkg.get('dependencies', {}), **pkg.get('devDependencies', {})}
|
||||
|
||||
# Check for commonly vulnerable packages (simplified - in production use npm audit)
|
||||
vulnerable_packages = {
|
||||
'lodash': ('< 4.17.21', 'Prototype pollution vulnerability'),
|
||||
'axios': ('< 0.21.1', 'SSRF vulnerability'),
|
||||
'node-fetch': ('< 2.6.7', 'Information exposure vulnerability'),
|
||||
}
|
||||
|
||||
for pkg_name, (vulnerable_version, description) in vulnerable_packages.items():
|
||||
if pkg_name in deps:
|
||||
findings.append({
|
||||
'severity': 'high',
|
||||
'category': 'security',
|
||||
'subcategory': 'dependencies',
|
||||
'title': f'Potentially vulnerable dependency: {pkg_name}',
|
||||
'description': f'{description} (version: {deps[pkg_name]})',
|
||||
'file': 'package.json',
|
||||
'line': None,
|
||||
'code_snippet': f'"{pkg_name}": "{deps[pkg_name]}"',
|
||||
'impact': 'Vulnerable dependencies can be exploited by attackers',
|
||||
'remediation': f'Update {pkg_name} to version {vulnerable_version.replace("< ", ">= ")} or later',
|
||||
'effort': 'low',
|
||||
})
|
||||
|
||||
except:
|
||||
pass
|
||||
|
||||
return findings
|
||||
|
||||
|
||||
def scan_security_antipatterns(codebase_path: Path, metadata: Dict) -> List[Dict]:
|
||||
"""Scan for common security anti-patterns."""
|
||||
findings = []
|
||||
|
||||
if metadata.get('tech_stack', {}).get('javascript') or metadata.get('tech_stack', {}).get('typescript'):
|
||||
findings.extend(scan_js_security_issues(codebase_path))
|
||||
|
||||
return findings
|
||||
|
||||
|
||||
def scan_js_security_issues(codebase_path: Path) -> List[Dict]:
|
||||
"""Scan JavaScript/TypeScript for security anti-patterns."""
|
||||
findings = []
|
||||
extensions = {'.js', '.jsx', '.ts', '.tsx'}
|
||||
exclude_dirs = {'node_modules', '.git', 'dist', 'build'}
|
||||
|
||||
# Dangerous patterns
|
||||
patterns = {
|
||||
'eval': (
|
||||
re.compile(r'\beval\s*\('),
|
||||
'Use of eval() is dangerous',
|
||||
'eval() can execute arbitrary code and is a security risk',
|
||||
'Refactor to avoid eval(), use safer alternatives like Function constructor with specific scope'
|
||||
),
|
||||
'dangerouslySetInnerHTML': (
|
||||
re.compile(r'dangerouslySetInnerHTML'),
|
||||
'Use of dangerouslySetInnerHTML without sanitization',
|
||||
'Can lead to XSS attacks if not properly sanitized',
|
||||
'Sanitize HTML content or use safer alternatives'
|
||||
),
|
||||
'innerHTML': (
|
||||
re.compile(r'\.innerHTML\s*='),
|
||||
'Direct assignment to innerHTML',
|
||||
'Can lead to XSS attacks if content is not sanitized',
|
||||
'Use textContent for text or sanitize HTML before assigning'
|
||||
),
|
||||
'document.write': (
|
||||
re.compile(r'document\.write\s*\('),
|
||||
'Use of document.write()',
|
||||
'Can be exploited for XSS and causes page reflow',
|
||||
'Use DOM manipulation methods instead'
|
||||
),
|
||||
}
|
||||
|
||||
for file_path in codebase_path.rglob('*'):
|
||||
if (file_path.suffix in extensions and
|
||||
not any(excluded in file_path.parts for excluded in exclude_dirs)):
|
||||
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
|
||||
content = f.read()
|
||||
lines = content.split('\n')
|
||||
|
||||
for pattern_name, (pattern, title, impact, remediation) in patterns.items():
|
||||
for line_num, line in enumerate(lines, start=1):
|
||||
if pattern.search(line):
|
||||
findings.append({
|
||||
'severity': 'high',
|
||||
'category': 'security',
|
||||
'subcategory': 'code_security',
|
||||
'title': title,
|
||||
'description': f'Found on line {line_num}',
|
||||
'file': str(file_path.relative_to(codebase_path)),
|
||||
'line': line_num,
|
||||
'code_snippet': line.strip(),
|
||||
'impact': impact,
|
||||
'remediation': remediation,
|
||||
'effort': 'medium',
|
||||
})
|
||||
|
||||
except:
|
||||
pass
|
||||
|
||||
return findings
|
||||
76
skills/codebase-auditor/scripts/analyzers/technical_debt.py
Normal file
76
skills/codebase-auditor/scripts/analyzers/technical_debt.py
Normal file
@@ -0,0 +1,76 @@
|
||||
"""
|
||||
Technical Debt Calculator
|
||||
|
||||
Calculates:
|
||||
- SQALE rating (A-E)
|
||||
- Remediation effort estimates
|
||||
- Debt categorization
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Dict, List
|
||||
|
||||
|
||||
def analyze(codebase_path: Path, metadata: Dict) -> List[Dict]:
|
||||
"""
|
||||
Calculate technical debt metrics.
|
||||
|
||||
Args:
|
||||
codebase_path: Path to codebase
|
||||
metadata: Project metadata
|
||||
|
||||
Returns:
|
||||
List of technical debt findings
|
||||
"""
|
||||
findings = []
|
||||
|
||||
# Placeholder implementation
|
||||
# In production, this would calculate SQALE rating based on all findings
|
||||
|
||||
return findings
|
||||
|
||||
|
||||
def calculate_sqale_rating(all_findings: List[Dict], total_loc: int) -> str:
|
||||
"""
|
||||
Calculate SQALE rating (A-E) based on findings.
|
||||
|
||||
Args:
|
||||
all_findings: All findings from all analyzers
|
||||
total_loc: Total lines of code
|
||||
|
||||
Returns:
|
||||
SQALE rating (A, B, C, D, or E)
|
||||
"""
|
||||
# Estimate remediation time in hours
|
||||
severity_hours = {
|
||||
'critical': 8,
|
||||
'high': 4,
|
||||
'medium': 2,
|
||||
'low': 0.5
|
||||
}
|
||||
|
||||
total_remediation_hours = sum(
|
||||
severity_hours.get(finding.get('severity', 'low'), 0.5)
|
||||
for finding in all_findings
|
||||
)
|
||||
|
||||
# Estimate development time (1 hour per 50 LOC is conservative)
|
||||
development_hours = total_loc / 50
|
||||
|
||||
# Calculate debt ratio
|
||||
if development_hours == 0:
|
||||
debt_ratio = 0
|
||||
else:
|
||||
debt_ratio = (total_remediation_hours / development_hours) * 100
|
||||
|
||||
# Assign SQALE rating
|
||||
if debt_ratio <= 5:
|
||||
return 'A'
|
||||
elif debt_ratio <= 10:
|
||||
return 'B'
|
||||
elif debt_ratio <= 20:
|
||||
return 'C'
|
||||
elif debt_ratio <= 50:
|
||||
return 'D'
|
||||
else:
|
||||
return 'E'
|
||||
184
skills/codebase-auditor/scripts/analyzers/test_coverage.py
Normal file
184
skills/codebase-auditor/scripts/analyzers/test_coverage.py
Normal file
@@ -0,0 +1,184 @@
|
||||
"""
|
||||
Test Coverage Analyzer
|
||||
|
||||
Analyzes:
|
||||
- Test coverage percentage
|
||||
- Testing Trophy distribution
|
||||
- Test quality
|
||||
- Untested critical paths
|
||||
"""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Dict, List
|
||||
|
||||
|
||||
def analyze(codebase_path: Path, metadata: Dict) -> List[Dict]:
|
||||
"""
|
||||
Analyze test coverage and quality.
|
||||
|
||||
Args:
|
||||
codebase_path: Path to codebase
|
||||
metadata: Project metadata
|
||||
|
||||
Returns:
|
||||
List of testing-related findings
|
||||
"""
|
||||
findings = []
|
||||
|
||||
# Check for test files existence
|
||||
test_stats = analyze_test_presence(codebase_path, metadata)
|
||||
if test_stats:
|
||||
findings.extend(test_stats)
|
||||
|
||||
# Analyze coverage if coverage reports exist
|
||||
coverage_findings = analyze_coverage_reports(codebase_path, metadata)
|
||||
if coverage_findings:
|
||||
findings.extend(coverage_findings)
|
||||
|
||||
return findings
|
||||
|
||||
|
||||
def analyze_test_presence(codebase_path: Path, metadata: Dict) -> List[Dict]:
|
||||
"""Check for test file presence and basic test hygiene."""
|
||||
findings = []
|
||||
|
||||
# Count test files
|
||||
test_extensions = {'.test.js', '.test.ts', '.test.jsx', '.test.tsx', '.spec.js', '.spec.ts'}
|
||||
test_dirs = {'__tests__', 'tests', 'test', 'spec'}
|
||||
|
||||
test_file_count = 0
|
||||
source_file_count = 0
|
||||
|
||||
exclude_dirs = {'node_modules', '.git', 'dist', 'build', '__pycache__'}
|
||||
source_extensions = {'.js', '.jsx', '.ts', '.tsx', '.py'}
|
||||
|
||||
for file_path in codebase_path.rglob('*'):
|
||||
if file_path.is_file() and not any(excluded in file_path.parts for excluded in exclude_dirs):
|
||||
|
||||
# Check if it's a test file
|
||||
is_test = (
|
||||
any(file_path.name.endswith(ext) for ext in test_extensions) or
|
||||
any(test_dir in file_path.parts for test_dir in test_dirs)
|
||||
)
|
||||
|
||||
if is_test:
|
||||
test_file_count += 1
|
||||
elif file_path.suffix in source_extensions:
|
||||
source_file_count += 1
|
||||
|
||||
# Calculate test ratio
|
||||
if source_file_count > 0:
|
||||
test_ratio = (test_file_count / source_file_count) * 100
|
||||
|
||||
if test_ratio < 20:
|
||||
findings.append({
|
||||
'severity': 'high',
|
||||
'category': 'testing',
|
||||
'subcategory': 'test_coverage',
|
||||
'title': f'Low test file ratio ({test_ratio:.1f}%)',
|
||||
'description': f'Only {test_file_count} test files for {source_file_count} source files',
|
||||
'file': None,
|
||||
'line': None,
|
||||
'code_snippet': None,
|
||||
'impact': 'Insufficient testing leads to bugs and difficult refactoring',
|
||||
'remediation': 'Add tests for untested modules, aim for at least 80% coverage',
|
||||
'effort': 'high',
|
||||
})
|
||||
elif test_ratio < 50:
|
||||
findings.append({
|
||||
'severity': 'medium',
|
||||
'category': 'testing',
|
||||
'subcategory': 'test_coverage',
|
||||
'title': f'Moderate test file ratio ({test_ratio:.1f}%)',
|
||||
'description': f'{test_file_count} test files for {source_file_count} source files',
|
||||
'file': None,
|
||||
'line': None,
|
||||
'code_snippet': None,
|
||||
'impact': 'More tests needed to achieve recommended 80% coverage',
|
||||
'remediation': 'Continue adding tests, focus on critical paths first',
|
||||
'effort': 'medium',
|
||||
})
|
||||
|
||||
return findings
|
||||
|
||||
|
||||
def analyze_coverage_reports(codebase_path: Path, metadata: Dict) -> List[Dict]:
|
||||
"""Analyze coverage reports if they exist."""
|
||||
findings = []
|
||||
|
||||
# Look for coverage reports (Istanbul/c8 format)
|
||||
coverage_files = [
|
||||
codebase_path / 'coverage' / 'coverage-summary.json',
|
||||
codebase_path / 'coverage' / 'coverage-final.json',
|
||||
codebase_path / '.nyc_output' / 'coverage-summary.json',
|
||||
]
|
||||
|
||||
for coverage_file in coverage_files:
|
||||
if coverage_file.exists():
|
||||
try:
|
||||
with open(coverage_file, 'r') as f:
|
||||
coverage_data = json.load(f)
|
||||
|
||||
# Extract total coverage
|
||||
total = coverage_data.get('total', {})
|
||||
|
||||
line_coverage = total.get('lines', {}).get('pct', 0)
|
||||
branch_coverage = total.get('branches', {}).get('pct', 0)
|
||||
function_coverage = total.get('functions', {}).get('pct', 0)
|
||||
statement_coverage = total.get('statements', {}).get('pct', 0)
|
||||
|
||||
# Check against 80% threshold
|
||||
if line_coverage < 80:
|
||||
severity = 'high' if line_coverage < 50 else 'medium'
|
||||
findings.append({
|
||||
'severity': severity,
|
||||
'category': 'testing',
|
||||
'subcategory': 'test_coverage',
|
||||
'title': f'Line coverage below target ({line_coverage:.1f}%)',
|
||||
'description': f'Current coverage is {line_coverage:.1f}%, target is 80%',
|
||||
'file': 'coverage/coverage-summary.json',
|
||||
'line': None,
|
||||
'code_snippet': None,
|
||||
'impact': 'Low coverage means untested code paths and higher bug risk',
|
||||
'remediation': f'Add tests to increase coverage by {80 - line_coverage:.1f}%',
|
||||
'effort': 'high',
|
||||
})
|
||||
|
||||
if branch_coverage < 75:
|
||||
findings.append({
|
||||
'severity': 'medium',
|
||||
'category': 'testing',
|
||||
'subcategory': 'test_coverage',
|
||||
'title': f'Branch coverage below target ({branch_coverage:.1f}%)',
|
||||
'description': f'Current branch coverage is {branch_coverage:.1f}%, target is 75%',
|
||||
'file': 'coverage/coverage-summary.json',
|
||||
'line': None,
|
||||
'code_snippet': None,
|
||||
'impact': 'Untested branches can hide bugs in conditional logic',
|
||||
'remediation': 'Add tests for edge cases and conditional branches',
|
||||
'effort': 'medium',
|
||||
})
|
||||
|
||||
break # Found coverage, don't check other files
|
||||
|
||||
except:
|
||||
pass
|
||||
|
||||
# If no coverage report found
|
||||
if not findings:
|
||||
findings.append({
|
||||
'severity': 'medium',
|
||||
'category': 'testing',
|
||||
'subcategory': 'test_infrastructure',
|
||||
'title': 'No coverage report found',
|
||||
'description': 'Could not find coverage-summary.json',
|
||||
'file': None,
|
||||
'line': None,
|
||||
'code_snippet': None,
|
||||
'impact': 'Cannot measure test effectiveness without coverage reports',
|
||||
'remediation': 'Configure test runner to generate coverage reports (Jest: --coverage, Vitest: --coverage)',
|
||||
'effort': 'low',
|
||||
})
|
||||
|
||||
return findings
|
||||
408
skills/codebase-auditor/scripts/audit_engine.py
Normal file
408
skills/codebase-auditor/scripts/audit_engine.py
Normal file
@@ -0,0 +1,408 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Codebase Audit Engine
|
||||
|
||||
Orchestrates comprehensive codebase analysis using multiple specialized analyzers.
|
||||
Generates detailed audit reports and remediation plans based on modern SDLC best practices.
|
||||
|
||||
Usage:
|
||||
python audit_engine.py /path/to/codebase --output report.md
|
||||
python audit_engine.py /path/to/codebase --format json --output report.json
|
||||
python audit_engine.py /path/to/codebase --scope security,quality
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional
|
||||
import importlib.util
|
||||
|
||||
# Import analyzers dynamically to support progressive loading
|
||||
ANALYZERS = {
|
||||
'quality': 'analyzers.code_quality',
|
||||
'testing': 'analyzers.test_coverage',
|
||||
'security': 'analyzers.security_scan',
|
||||
'dependencies': 'analyzers.dependencies',
|
||||
'performance': 'analyzers.performance',
|
||||
'technical_debt': 'analyzers.technical_debt',
|
||||
}
|
||||
|
||||
|
||||
class AuditEngine:
|
||||
"""
|
||||
Core audit engine that orchestrates codebase analysis.
|
||||
|
||||
Uses progressive disclosure: loads only necessary analyzers based on scope.
|
||||
"""
|
||||
|
||||
def __init__(self, codebase_path: Path, scope: Optional[List[str]] = None):
|
||||
"""
|
||||
Initialize audit engine.
|
||||
|
||||
Args:
|
||||
codebase_path: Path to the codebase to audit
|
||||
scope: Optional list of analysis categories to run (e.g., ['security', 'quality'])
|
||||
If None, runs all analyzers.
|
||||
"""
|
||||
self.codebase_path = Path(codebase_path).resolve()
|
||||
self.scope = scope or list(ANALYZERS.keys())
|
||||
self.findings: Dict[str, List[Dict]] = {}
|
||||
self.metadata: Dict = {}
|
||||
|
||||
if not self.codebase_path.exists():
|
||||
raise FileNotFoundError(f"Codebase path does not exist: {self.codebase_path}")
|
||||
|
||||
def discover_project(self) -> Dict:
|
||||
"""
|
||||
Phase 1: Initial project discovery (lightweight scan).
|
||||
|
||||
Returns:
|
||||
Dictionary containing project metadata
|
||||
"""
|
||||
print("🔍 Phase 1: Discovering project structure...")
|
||||
|
||||
metadata = {
|
||||
'path': str(self.codebase_path),
|
||||
'scan_time': datetime.now().isoformat(),
|
||||
'tech_stack': self._detect_tech_stack(),
|
||||
'project_type': self._detect_project_type(),
|
||||
'total_files': self._count_files(),
|
||||
'total_lines': self._count_lines(),
|
||||
'git_info': self._get_git_info(),
|
||||
}
|
||||
|
||||
self.metadata = metadata
|
||||
return metadata
|
||||
|
||||
def _detect_tech_stack(self) -> Dict[str, bool]:
|
||||
"""Detect languages and frameworks used in the project."""
|
||||
tech_stack = {
|
||||
'javascript': (self.codebase_path / 'package.json').exists(),
|
||||
'typescript': self._file_exists_with_extension('.ts') or self._file_exists_with_extension('.tsx'),
|
||||
'python': (self.codebase_path / 'setup.py').exists() or
|
||||
(self.codebase_path / 'pyproject.toml').exists() or
|
||||
self._file_exists_with_extension('.py'),
|
||||
'react': self._check_dependency('react'),
|
||||
'vue': self._check_dependency('vue'),
|
||||
'angular': self._check_dependency('@angular/core'),
|
||||
'node': (self.codebase_path / 'package.json').exists(),
|
||||
'docker': (self.codebase_path / 'Dockerfile').exists(),
|
||||
}
|
||||
return {k: v for k, v in tech_stack.items() if v}
|
||||
|
||||
def _detect_project_type(self) -> str:
|
||||
"""Determine project type (web app, library, CLI, etc.)."""
|
||||
if (self.codebase_path / 'package.json').exists():
|
||||
try:
|
||||
with open(self.codebase_path / 'package.json', 'r') as f:
|
||||
pkg = json.load(f)
|
||||
if pkg.get('private') is False:
|
||||
return 'library'
|
||||
if 'bin' in pkg:
|
||||
return 'cli'
|
||||
return 'web_app'
|
||||
except:
|
||||
pass
|
||||
|
||||
if (self.codebase_path / 'setup.py').exists():
|
||||
return 'python_package'
|
||||
|
||||
return 'unknown'
|
||||
|
||||
def _count_files(self) -> int:
|
||||
"""Count total files in codebase (excluding common ignore patterns)."""
|
||||
exclude_dirs = {'.git', 'node_modules', '__pycache__', '.venv', 'venv', 'dist', 'build'}
|
||||
count = 0
|
||||
|
||||
for path in self.codebase_path.rglob('*'):
|
||||
if path.is_file() and not any(excluded in path.parts for excluded in exclude_dirs):
|
||||
count += 1
|
||||
|
||||
return count
|
||||
|
||||
def _count_lines(self) -> int:
|
||||
"""Count total lines of code (excluding empty lines and comments)."""
|
||||
exclude_dirs = {'.git', 'node_modules', '__pycache__', '.venv', 'venv', 'dist', 'build'}
|
||||
code_extensions = {'.js', '.jsx', '.ts', '.tsx', '.py', '.java', '.go', '.rs', '.rb'}
|
||||
total_lines = 0
|
||||
|
||||
for path in self.codebase_path.rglob('*'):
|
||||
if (path.is_file() and
|
||||
path.suffix in code_extensions and
|
||||
not any(excluded in path.parts for excluded in exclude_dirs)):
|
||||
try:
|
||||
with open(path, 'r', encoding='utf-8', errors='ignore') as f:
|
||||
total_lines += sum(1 for line in f if line.strip() and not line.strip().startswith(('//', '#', '/*', '*')))
|
||||
except:
|
||||
pass
|
||||
|
||||
return total_lines
|
||||
|
||||
def _get_git_info(self) -> Optional[Dict]:
|
||||
"""Get git repository information."""
|
||||
git_dir = self.codebase_path / '.git'
|
||||
if not git_dir.exists():
|
||||
return None
|
||||
|
||||
try:
|
||||
import subprocess
|
||||
result = subprocess.run(
|
||||
['git', '-C', str(self.codebase_path), 'log', '--oneline', '-10'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5
|
||||
)
|
||||
|
||||
commit_count = subprocess.run(
|
||||
['git', '-C', str(self.codebase_path), 'rev-list', '--count', 'HEAD'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5
|
||||
)
|
||||
|
||||
return {
|
||||
'is_git_repo': True,
|
||||
'recent_commits': result.stdout.strip().split('\n') if result.returncode == 0 else [],
|
||||
'total_commits': int(commit_count.stdout.strip()) if commit_count.returncode == 0 else 0,
|
||||
}
|
||||
except:
|
||||
return {'is_git_repo': True, 'error': 'Could not read git info'}
|
||||
|
||||
def _file_exists_with_extension(self, extension: str) -> bool:
|
||||
"""Check if any file with given extension exists."""
|
||||
return any(self.codebase_path.rglob(f'*{extension}'))
|
||||
|
||||
def _check_dependency(self, dep_name: str) -> bool:
|
||||
"""Check if a dependency exists in package.json."""
|
||||
pkg_json = self.codebase_path / 'package.json'
|
||||
if not pkg_json.exists():
|
||||
return False
|
||||
|
||||
try:
|
||||
with open(pkg_json, 'r') as f:
|
||||
pkg = json.load(f)
|
||||
deps = {**pkg.get('dependencies', {}), **pkg.get('devDependencies', {})}
|
||||
return dep_name in deps
|
||||
except:
|
||||
return False
|
||||
|
||||
def run_analysis(self, phase: str = 'full') -> Dict:
|
||||
"""
|
||||
Phase 2: Deep analysis using specialized analyzers.
|
||||
|
||||
Args:
|
||||
phase: 'quick' for lightweight scan, 'full' for comprehensive analysis
|
||||
|
||||
Returns:
|
||||
Dictionary containing all findings
|
||||
"""
|
||||
print(f"🔬 Phase 2: Running {phase} analysis...")
|
||||
|
||||
for category in self.scope:
|
||||
if category not in ANALYZERS:
|
||||
print(f"⚠️ Unknown analyzer category: {category}, skipping...")
|
||||
continue
|
||||
|
||||
print(f" Analyzing {category}...")
|
||||
analyzer_findings = self._run_analyzer(category)
|
||||
if analyzer_findings:
|
||||
self.findings[category] = analyzer_findings
|
||||
|
||||
return self.findings
|
||||
|
||||
def _run_analyzer(self, category: str) -> List[Dict]:
|
||||
"""
|
||||
Run a specific analyzer module.
|
||||
|
||||
Args:
|
||||
category: Analyzer category name
|
||||
|
||||
Returns:
|
||||
List of findings from the analyzer
|
||||
"""
|
||||
module_path = ANALYZERS.get(category)
|
||||
if not module_path:
|
||||
return []
|
||||
|
||||
try:
|
||||
# Import analyzer module dynamically
|
||||
analyzer_file = Path(__file__).parent / f"{module_path.replace('.', '/')}.py"
|
||||
|
||||
if not analyzer_file.exists():
|
||||
print(f" ⚠️ Analyzer not yet implemented: {category}")
|
||||
return []
|
||||
|
||||
spec = importlib.util.spec_from_file_location(module_path, analyzer_file)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
# Each analyzer should have an analyze() function
|
||||
if hasattr(module, 'analyze'):
|
||||
return module.analyze(self.codebase_path, self.metadata)
|
||||
else:
|
||||
print(f" ⚠️ Analyzer missing analyze() function: {category}")
|
||||
return []
|
||||
|
||||
except Exception as e:
|
||||
print(f" ❌ Error running analyzer {category}: {e}")
|
||||
return []
|
||||
|
||||
def calculate_scores(self) -> Dict[str, float]:
|
||||
"""
|
||||
Calculate health scores for each category and overall.
|
||||
|
||||
Returns:
|
||||
Dictionary of scores (0-100 scale)
|
||||
"""
|
||||
scores = {}
|
||||
|
||||
# Calculate score for each category based on findings severity
|
||||
for category, findings in self.findings.items():
|
||||
if not findings:
|
||||
scores[category] = 100.0
|
||||
continue
|
||||
|
||||
# Weighted scoring based on severity
|
||||
severity_weights = {'critical': 10, 'high': 5, 'medium': 2, 'low': 1}
|
||||
total_weight = sum(severity_weights.get(f.get('severity', 'low'), 1) for f in findings)
|
||||
|
||||
# Score decreases based on weighted issues
|
||||
# Formula: 100 - (total_weight / num_findings * penalty_factor)
|
||||
penalty = min(total_weight, 100)
|
||||
scores[category] = max(0, 100 - penalty)
|
||||
|
||||
# Overall score is weighted average
|
||||
if scores:
|
||||
scores['overall'] = sum(scores.values()) / len(scores)
|
||||
else:
|
||||
scores['overall'] = 100.0
|
||||
|
||||
return scores
|
||||
|
||||
def generate_summary(self) -> Dict:
|
||||
"""
|
||||
Generate executive summary of audit results.
|
||||
|
||||
Returns:
|
||||
Summary dictionary
|
||||
"""
|
||||
critical_count = sum(
|
||||
1 for findings in self.findings.values()
|
||||
for f in findings
|
||||
if f.get('severity') == 'critical'
|
||||
)
|
||||
|
||||
high_count = sum(
|
||||
1 for findings in self.findings.values()
|
||||
for f in findings
|
||||
if f.get('severity') == 'high'
|
||||
)
|
||||
|
||||
scores = self.calculate_scores()
|
||||
|
||||
return {
|
||||
'overall_score': round(scores.get('overall', 0), 1),
|
||||
'category_scores': {k: round(v, 1) for k, v in scores.items() if k != 'overall'},
|
||||
'critical_issues': critical_count,
|
||||
'high_issues': high_count,
|
||||
'total_issues': sum(len(findings) for findings in self.findings.values()),
|
||||
'metadata': self.metadata,
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point for CLI usage."""
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Comprehensive codebase auditor based on modern SDLC best practices (2024-25)',
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'codebase',
|
||||
type=str,
|
||||
help='Path to the codebase to audit'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--scope',
|
||||
type=str,
|
||||
help='Comma-separated list of analysis categories (quality,testing,security,dependencies,performance,technical_debt)',
|
||||
default=None
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--phase',
|
||||
type=str,
|
||||
choices=['quick', 'full'],
|
||||
default='full',
|
||||
help='Analysis depth: quick (Phase 1 only) or full (Phase 1 + 2)'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--format',
|
||||
type=str,
|
||||
choices=['markdown', 'json', 'html'],
|
||||
default='markdown',
|
||||
help='Output format for the report'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--output',
|
||||
type=str,
|
||||
help='Output file path (default: stdout)',
|
||||
default=None
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Parse scope
|
||||
scope = args.scope.split(',') if args.scope else None
|
||||
|
||||
# Initialize engine
|
||||
try:
|
||||
engine = AuditEngine(args.codebase, scope=scope)
|
||||
except FileNotFoundError as e:
|
||||
print(f"❌ Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Run audit
|
||||
print("🚀 Starting codebase audit...")
|
||||
print(f" Codebase: {args.codebase}")
|
||||
print(f" Scope: {scope or 'all'}")
|
||||
print(f" Phase: {args.phase}")
|
||||
print()
|
||||
|
||||
# Phase 1: Discovery
|
||||
metadata = engine.discover_project()
|
||||
print(f" Detected: {', '.join(metadata['tech_stack'].keys())}")
|
||||
print(f" Files: {metadata['total_files']}")
|
||||
print(f" Lines of code: {metadata['total_lines']:,}")
|
||||
print()
|
||||
|
||||
# Phase 2: Analysis (if not quick mode)
|
||||
if args.phase == 'full':
|
||||
findings = engine.run_analysis()
|
||||
|
||||
# Generate summary
|
||||
summary = engine.generate_summary()
|
||||
|
||||
# Output results
|
||||
print()
|
||||
print("📊 Audit complete!")
|
||||
print(f" Overall score: {summary['overall_score']}/100")
|
||||
print(f" Critical issues: {summary['critical_issues']}")
|
||||
print(f" High issues: {summary['high_issues']}")
|
||||
print(f" Total issues: {summary['total_issues']}")
|
||||
print()
|
||||
|
||||
# Generate report (to be implemented in report_generator.py)
|
||||
if args.output:
|
||||
print(f"📝 Report generation will be implemented in report_generator.py")
|
||||
print(f" Format: {args.format}")
|
||||
print(f" Output: {args.output}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
241
skills/codebase-auditor/scripts/remediation_planner.py
Normal file
241
skills/codebase-auditor/scripts/remediation_planner.py
Normal file
@@ -0,0 +1,241 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Remediation Planner
|
||||
|
||||
Generates prioritized action plans based on audit findings.
|
||||
Uses severity, impact, frequency, and effort to prioritize issues.
|
||||
"""
|
||||
|
||||
from typing import Dict, List
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
|
||||
def generate_remediation_plan(findings: Dict[str, List[Dict]], metadata: Dict) -> str:
|
||||
"""
|
||||
Generate a prioritized remediation plan.
|
||||
|
||||
Args:
|
||||
findings: All findings organized by category
|
||||
metadata: Project metadata
|
||||
|
||||
Returns:
|
||||
Markdown-formatted remediation plan
|
||||
"""
|
||||
plan = []
|
||||
|
||||
# Header
|
||||
plan.append("# Codebase Remediation Plan")
|
||||
plan.append(f"\n**Generated**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
plan.append(f"**Codebase**: `{metadata.get('path', 'Unknown')}`")
|
||||
plan.append("\n---\n")
|
||||
|
||||
# Flatten and prioritize all findings
|
||||
all_findings = []
|
||||
for category, category_findings in findings.items():
|
||||
for finding in category_findings:
|
||||
finding['category'] = category
|
||||
all_findings.append(finding)
|
||||
|
||||
# Calculate priority scores
|
||||
for finding in all_findings:
|
||||
finding['priority_score'] = calculate_priority_score(finding)
|
||||
|
||||
# Sort by priority score (highest first)
|
||||
all_findings.sort(key=lambda x: x['priority_score'], reverse=True)
|
||||
|
||||
# Group by priority level
|
||||
p0_issues = [f for f in all_findings if f['severity'] == 'critical']
|
||||
p1_issues = [f for f in all_findings if f['severity'] == 'high']
|
||||
p2_issues = [f for f in all_findings if f['severity'] == 'medium']
|
||||
p3_issues = [f for f in all_findings if f['severity'] == 'low']
|
||||
|
||||
# Priority 0: Critical Issues (Fix Immediately)
|
||||
if p0_issues:
|
||||
plan.append("## Priority 0: Critical Issues (Fix Immediately ⚡)")
|
||||
plan.append("\n**Timeline**: Within 24 hours")
|
||||
plan.append("**Impact**: Security vulnerabilities, production-breaking bugs, data loss risks\n")
|
||||
|
||||
for i, finding in enumerate(p0_issues, 1):
|
||||
plan.append(f"### {i}. {finding.get('title', 'Untitled')}")
|
||||
plan.append(f"**Category**: {finding.get('category', 'Unknown').replace('_', ' ').title()}")
|
||||
plan.append(f"**Location**: `{finding.get('file', 'Unknown')}`")
|
||||
plan.append(f"**Effort**: {finding.get('effort', 'unknown').upper()}")
|
||||
plan.append(f"\n**Issue**: {finding.get('description', 'No description')}")
|
||||
plan.append(f"\n**Impact**: {finding.get('impact', 'Unknown impact')}")
|
||||
plan.append(f"\n**Action**: {finding.get('remediation', 'No remediation suggested')}\n")
|
||||
plan.append("---\n")
|
||||
|
||||
# Priority 1: High Issues (Fix This Sprint)
|
||||
if p1_issues:
|
||||
plan.append("## Priority 1: High Issues (Fix This Sprint 📅)")
|
||||
plan.append("\n**Timeline**: Within current sprint (2 weeks)")
|
||||
plan.append("**Impact**: Significant quality, security, or user experience issues\n")
|
||||
|
||||
for i, finding in enumerate(p1_issues[:10], 1): # Top 10
|
||||
plan.append(f"### {i}. {finding.get('title', 'Untitled')}")
|
||||
plan.append(f"**Category**: {finding.get('category', 'Unknown').replace('_', ' ').title()}")
|
||||
plan.append(f"**Effort**: {finding.get('effort', 'unknown').upper()}")
|
||||
plan.append(f"\n**Action**: {finding.get('remediation', 'No remediation suggested')}\n")
|
||||
|
||||
if len(p1_issues) > 10:
|
||||
plan.append(f"\n*...and {len(p1_issues) - 10} more high-priority issues*\n")
|
||||
|
||||
plan.append("---\n")
|
||||
|
||||
# Priority 2: Medium Issues (Fix Next Quarter)
|
||||
if p2_issues:
|
||||
plan.append("## Priority 2: Medium Issues (Fix Next Quarter 📆)")
|
||||
plan.append("\n**Timeline**: Within 3 months")
|
||||
plan.append("**Impact**: Code maintainability, developer productivity\n")
|
||||
plan.append(f"**Total Issues**: {len(p2_issues)}\n")
|
||||
|
||||
# Group by subcategory
|
||||
subcategories = {}
|
||||
for finding in p2_issues:
|
||||
subcat = finding.get('subcategory', 'Other')
|
||||
if subcat not in subcategories:
|
||||
subcategories[subcat] = []
|
||||
subcategories[subcat].append(finding)
|
||||
|
||||
plan.append("**Grouped by Type**:\n")
|
||||
for subcat, subcat_findings in subcategories.items():
|
||||
plan.append(f"- {subcat.replace('_', ' ').title()}: {len(subcat_findings)} issues")
|
||||
|
||||
plan.append("\n---\n")
|
||||
|
||||
# Priority 3: Low Issues (Backlog)
|
||||
if p3_issues:
|
||||
plan.append("## Priority 3: Low Issues (Backlog 📋)")
|
||||
plan.append("\n**Timeline**: When time permits")
|
||||
plan.append("**Impact**: Minor improvements, stylistic issues\n")
|
||||
plan.append(f"**Total Issues**: {len(p3_issues)}\n")
|
||||
plan.append("*Address during dedicated tech debt sprints or slow periods*\n")
|
||||
plan.append("---\n")
|
||||
|
||||
# Implementation Timeline
|
||||
plan.append("## Suggested Timeline\n")
|
||||
|
||||
today = datetime.now()
|
||||
|
||||
if p0_issues:
|
||||
deadline = today + timedelta(days=1)
|
||||
plan.append(f"- **{deadline.strftime('%Y-%m-%d')}**: All P0 issues resolved")
|
||||
|
||||
if p1_issues:
|
||||
deadline = today + timedelta(weeks=2)
|
||||
plan.append(f"- **{deadline.strftime('%Y-%m-%d')}**: P1 issues addressed (end of sprint)")
|
||||
|
||||
if p2_issues:
|
||||
deadline = today + timedelta(weeks=12)
|
||||
plan.append(f"- **{deadline.strftime('%Y-%m-%d')}**: P2 issues resolved (end of quarter)")
|
||||
|
||||
# Effort Summary
|
||||
plan.append("\n## Effort Summary\n")
|
||||
|
||||
effort_estimates = calculate_effort_summary(all_findings)
|
||||
plan.append(f"**Total Estimated Effort**: {effort_estimates['total']} person-days")
|
||||
plan.append(f"- Critical/High: {effort_estimates['critical_high']} days")
|
||||
plan.append(f"- Medium: {effort_estimates['medium']} days")
|
||||
plan.append(f"- Low: {effort_estimates['low']} days")
|
||||
|
||||
# Team Assignment Suggestions
|
||||
plan.append("\n## Team Assignment Suggestions\n")
|
||||
plan.append("- **Security Team**: All P0 security issues, P1 vulnerabilities")
|
||||
plan.append("- **QA/Testing**: Test coverage improvements, test quality issues")
|
||||
plan.append("- **Infrastructure**: CI/CD improvements, build performance")
|
||||
plan.append("- **Development Team**: Code quality refactoring, complexity reduction")
|
||||
|
||||
# Footer
|
||||
plan.append("\n---\n")
|
||||
plan.append("*Remediation plan generated by Codebase Auditor Skill*")
|
||||
plan.append("\n*Priority scoring based on: Impact × 10 + Frequency × 5 - Effort × 2*")
|
||||
|
||||
return '\n'.join(plan)
|
||||
|
||||
|
||||
def calculate_priority_score(finding: Dict) -> int:
|
||||
"""
|
||||
Calculate priority score for a finding.
|
||||
|
||||
Formula: (Impact × 10) + (Frequency × 5) - (Effort × 2)
|
||||
|
||||
Args:
|
||||
finding: Individual finding
|
||||
|
||||
Returns:
|
||||
Priority score (higher = more urgent)
|
||||
"""
|
||||
# Map severity to impact (1-10)
|
||||
severity_impact = {
|
||||
'critical': 10,
|
||||
'high': 7,
|
||||
'medium': 4,
|
||||
'low': 2,
|
||||
}
|
||||
impact = severity_impact.get(finding.get('severity', 'low'), 1)
|
||||
|
||||
# Estimate frequency (1-10) based on category
|
||||
# Security/testing issues affect everything
|
||||
category = finding.get('category', '')
|
||||
if category in ['security', 'testing']:
|
||||
frequency = 10
|
||||
elif category in ['quality', 'performance']:
|
||||
frequency = 6
|
||||
else:
|
||||
frequency = 3
|
||||
|
||||
# Map effort to numeric value (1-10)
|
||||
effort_values = {
|
||||
'low': 2,
|
||||
'medium': 5,
|
||||
'high': 8,
|
||||
}
|
||||
effort = effort_values.get(finding.get('effort', 'medium'), 5)
|
||||
|
||||
# Calculate score
|
||||
score = (impact * 10) + (frequency * 5) - (effort * 2)
|
||||
|
||||
return max(0, score) # Never negative
|
||||
|
||||
|
||||
def calculate_effort_summary(findings: List[Dict]) -> Dict[str, int]:
|
||||
"""
|
||||
Calculate total effort estimates.
|
||||
|
||||
Args:
|
||||
findings: All findings
|
||||
|
||||
Returns:
|
||||
Dictionary with effort estimates in person-days
|
||||
"""
|
||||
# Map effort levels to days
|
||||
effort_days = {
|
||||
'low': 0.5,
|
||||
'medium': 2,
|
||||
'high': 5,
|
||||
}
|
||||
|
||||
critical_high_days = sum(
|
||||
effort_days.get(f.get('effort', 'medium'), 2)
|
||||
for f in findings
|
||||
if f.get('severity') in ['critical', 'high']
|
||||
)
|
||||
|
||||
medium_days = sum(
|
||||
effort_days.get(f.get('effort', 'medium'), 2)
|
||||
for f in findings
|
||||
if f.get('severity') == 'medium'
|
||||
)
|
||||
|
||||
low_days = sum(
|
||||
effort_days.get(f.get('effort', 'medium'), 2)
|
||||
for f in findings
|
||||
if f.get('severity') == 'low'
|
||||
)
|
||||
|
||||
return {
|
||||
'critical_high': round(critical_high_days, 1),
|
||||
'medium': round(medium_days, 1),
|
||||
'low': round(low_days, 1),
|
||||
'total': round(critical_high_days + medium_days + low_days, 1),
|
||||
}
|
||||
345
skills/codebase-auditor/scripts/report_generator.py
Normal file
345
skills/codebase-auditor/scripts/report_generator.py
Normal file
@@ -0,0 +1,345 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Report Generator
|
||||
|
||||
Generates audit reports in multiple formats:
|
||||
- Markdown (default, human-readable)
|
||||
- JSON (machine-readable, CI/CD integration)
|
||||
- HTML (interactive dashboard)
|
||||
"""
|
||||
|
||||
import json
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Dict, List
|
||||
|
||||
|
||||
def generate_markdown_report(summary: Dict, findings: Dict[str, List[Dict]], metadata: Dict) -> str:
|
||||
"""
|
||||
Generate a Markdown-formatted audit report.
|
||||
|
||||
Args:
|
||||
summary: Executive summary data
|
||||
findings: All findings organized by category
|
||||
metadata: Project metadata
|
||||
|
||||
Returns:
|
||||
Markdown report as string
|
||||
"""
|
||||
report = []
|
||||
|
||||
# Header
|
||||
report.append("# Codebase Audit Report")
|
||||
report.append(f"\n**Generated**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
report.append(f"**Codebase**: `{metadata.get('path', 'Unknown')}`")
|
||||
report.append(f"**Tech Stack**: {', '.join(metadata.get('tech_stack', {}).keys())}")
|
||||
report.append(f"**Total Files**: {metadata.get('total_files', 0):,}")
|
||||
report.append(f"**Lines of Code**: {metadata.get('total_lines', 0):,}")
|
||||
report.append("\n---\n")
|
||||
|
||||
# Executive Summary
|
||||
report.append("## Executive Summary")
|
||||
report.append(f"\n### Overall Health Score: **{summary.get('overall_score', 0)}/100**\n")
|
||||
|
||||
# Score breakdown
|
||||
report.append("#### Category Scores\n")
|
||||
for category, score in summary.get('category_scores', {}).items():
|
||||
emoji = score_to_emoji(score)
|
||||
report.append(f"- **{category.replace('_', ' ').title()}**: {score}/100 {emoji}")
|
||||
|
||||
# Issue summary
|
||||
report.append("\n#### Issue Summary\n")
|
||||
report.append(f"- **Critical Issues**: {summary.get('critical_issues', 0)}")
|
||||
report.append(f"- **High Issues**: {summary.get('high_issues', 0)}")
|
||||
report.append(f"- **Total Issues**: {summary.get('total_issues', 0)}")
|
||||
|
||||
report.append("\n---\n")
|
||||
|
||||
# Detailed Findings
|
||||
report.append("## Detailed Findings\n")
|
||||
|
||||
severity_order = ['critical', 'high', 'medium', 'low']
|
||||
|
||||
for severity in severity_order:
|
||||
severity_findings = []
|
||||
|
||||
for category, category_findings in findings.items():
|
||||
for finding in category_findings:
|
||||
if finding.get('severity') == severity:
|
||||
severity_findings.append((category, finding))
|
||||
|
||||
if severity_findings:
|
||||
severity_emoji = severity_to_emoji(severity)
|
||||
report.append(f"### {severity_emoji} {severity.upper()} ({len(severity_findings)} issues)\n")
|
||||
|
||||
for category, finding in severity_findings:
|
||||
report.append(f"#### {finding.get('title', 'Untitled Issue')}")
|
||||
report.append(f"\n**Category**: {category.replace('_', ' ').title()}")
|
||||
report.append(f"**Subcategory**: {finding.get('subcategory', 'N/A')}")
|
||||
|
||||
if finding.get('file'):
|
||||
file_ref = f"{finding['file']}"
|
||||
if finding.get('line'):
|
||||
file_ref += f":{finding['line']}"
|
||||
report.append(f"**Location**: `{file_ref}`")
|
||||
|
||||
report.append(f"\n{finding.get('description', 'No description')}")
|
||||
|
||||
if finding.get('code_snippet'):
|
||||
report.append(f"\n```\n{finding['code_snippet']}\n```")
|
||||
|
||||
report.append(f"\n**Impact**: {finding.get('impact', 'Unknown impact')}")
|
||||
report.append(f"\n**Remediation**: {finding.get('remediation', 'No remediation suggested')}")
|
||||
report.append(f"\n**Effort**: {finding.get('effort', 'Unknown').upper()}\n")
|
||||
report.append("---\n")
|
||||
|
||||
# Recommendations
|
||||
report.append("## Recommendations\n")
|
||||
report.append(generate_recommendations(summary, findings))
|
||||
|
||||
# Footer
|
||||
report.append("\n---\n")
|
||||
report.append("*Report generated by Codebase Auditor Skill (2024-25 Standards)*")
|
||||
|
||||
return '\n'.join(report)
|
||||
|
||||
|
||||
def generate_json_report(summary: Dict, findings: Dict[str, List[Dict]], metadata: Dict) -> str:
|
||||
"""
|
||||
Generate a JSON-formatted audit report.
|
||||
|
||||
Args:
|
||||
summary: Executive summary data
|
||||
findings: All findings organized by category
|
||||
metadata: Project metadata
|
||||
|
||||
Returns:
|
||||
JSON report as string
|
||||
"""
|
||||
report = {
|
||||
'generated_at': datetime.now().isoformat(),
|
||||
'metadata': metadata,
|
||||
'summary': summary,
|
||||
'findings': findings,
|
||||
'schema_version': '1.0.0',
|
||||
}
|
||||
|
||||
return json.dumps(report, indent=2)
|
||||
|
||||
|
||||
def generate_html_report(summary: Dict, findings: Dict[str, List[Dict]], metadata: Dict) -> str:
|
||||
"""
|
||||
Generate an HTML dashboard report.
|
||||
|
||||
Args:
|
||||
summary: Executive summary data
|
||||
findings: All findings organized by category
|
||||
metadata: Project metadata
|
||||
|
||||
Returns:
|
||||
HTML report as string
|
||||
"""
|
||||
# Simplified HTML template
|
||||
html = f"""<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Codebase Audit Report</title>
|
||||
<style>
|
||||
body {{
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
line-height: 1.6;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
}}
|
||||
.header {{
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 30px;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 20px;
|
||||
}}
|
||||
.score {{
|
||||
font-size: 48px;
|
||||
font-weight: bold;
|
||||
margin: 20px 0;
|
||||
}}
|
||||
.metrics {{
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
margin: 20px 0;
|
||||
}}
|
||||
.metric {{
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}}
|
||||
.metric-title {{
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
text-transform: uppercase;
|
||||
}}
|
||||
.metric-value {{
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
margin: 10px 0;
|
||||
}}
|
||||
.finding {{
|
||||
background: white;
|
||||
padding: 20px;
|
||||
margin: 10px 0;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #ddd;
|
||||
}}
|
||||
.finding.critical {{ border-left-color: #e53e3e; }}
|
||||
.finding.high {{ border-left-color: #dd6b20; }}
|
||||
.finding.medium {{ border-left-color: #d69e2e; }}
|
||||
.finding.low {{ border-left-color: #38a169; }}
|
||||
.badge {{
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
}}
|
||||
.badge.critical {{ background: #fed7d7; color: #742a2a; }}
|
||||
.badge.high {{ background: #feebc8; color: #7c2d12; }}
|
||||
.badge.medium {{ background: #fefcbf; color: #744210; }}
|
||||
.badge.low {{ background: #c6f6d5; color: #22543d; }}
|
||||
code {{
|
||||
background: #f7fafc;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-family: 'Courier New', monospace;
|
||||
}}
|
||||
pre {{
|
||||
background: #2d3748;
|
||||
color: #e2e8f0;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
overflow-x: auto;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>🔍 Codebase Audit Report</h1>
|
||||
<p><strong>Generated:</strong> {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
|
||||
<p><strong>Codebase:</strong> {metadata.get('path', 'Unknown')}</p>
|
||||
<div class="score">Overall Score: {summary.get('overall_score', 0)}/100</div>
|
||||
</div>
|
||||
|
||||
<div class="metrics">
|
||||
<div class="metric">
|
||||
<div class="metric-title">Critical Issues</div>
|
||||
<div class="metric-value" style="color: #e53e3e;">{summary.get('critical_issues', 0)}</div>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="metric-title">High Issues</div>
|
||||
<div class="metric-value" style="color: #dd6b20;">{summary.get('high_issues', 0)}</div>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="metric-title">Total Issues</div>
|
||||
<div class="metric-value">{summary.get('total_issues', 0)}</div>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="metric-title">Lines of Code</div>
|
||||
<div class="metric-value">{metadata.get('total_lines', 0):,}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Findings</h2>
|
||||
"""
|
||||
|
||||
# Add findings
|
||||
severity_order = ['critical', 'high', 'medium', 'low']
|
||||
for severity in severity_order:
|
||||
for category, category_findings in findings.items():
|
||||
for finding in category_findings:
|
||||
if finding.get('severity') == severity:
|
||||
html += f"""
|
||||
<div class="finding {severity}">
|
||||
<div>
|
||||
<span class="badge {severity}">{severity}</span>
|
||||
<strong>{finding.get('title', 'Untitled')}</strong>
|
||||
</div>
|
||||
<p>{finding.get('description', 'No description')}</p>
|
||||
"""
|
||||
if finding.get('file'):
|
||||
html += f"<p><strong>Location:</strong> <code>{finding['file']}"
|
||||
if finding.get('line'):
|
||||
html += f":{finding['line']}"
|
||||
html += "</code></p>"
|
||||
|
||||
if finding.get('code_snippet'):
|
||||
html += f"<pre><code>{finding['code_snippet']}</code></pre>"
|
||||
|
||||
html += f"""
|
||||
<p><strong>Impact:</strong> {finding.get('impact', 'Unknown')}</p>
|
||||
<p><strong>Remediation:</strong> {finding.get('remediation', 'No suggestion')}</p>
|
||||
</div>
|
||||
"""
|
||||
|
||||
html += """
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
return html
|
||||
|
||||
|
||||
def score_to_emoji(score: float) -> str:
|
||||
"""Convert score to emoji."""
|
||||
if score >= 90:
|
||||
return "✅"
|
||||
elif score >= 70:
|
||||
return "⚠️"
|
||||
else:
|
||||
return "❌"
|
||||
|
||||
|
||||
def severity_to_emoji(severity: str) -> str:
|
||||
"""Convert severity to emoji."""
|
||||
severity_map = {
|
||||
'critical': '🚨',
|
||||
'high': '⚠️',
|
||||
'medium': '⚡',
|
||||
'low': 'ℹ️',
|
||||
}
|
||||
return severity_map.get(severity, '')
|
||||
|
||||
|
||||
def generate_recommendations(summary: Dict, findings: Dict) -> str:
|
||||
"""Generate recommendations based on findings."""
|
||||
recommendations = []
|
||||
|
||||
critical_count = summary.get('critical_issues', 0)
|
||||
high_count = summary.get('high_issues', 0)
|
||||
overall_score = summary.get('overall_score', 0)
|
||||
|
||||
if critical_count > 0:
|
||||
recommendations.append(f"1. **Immediate Action Required**: Address all {critical_count} critical security and quality issues before deploying to production.")
|
||||
|
||||
if high_count > 5:
|
||||
recommendations.append(f"2. **Sprint Focus**: Prioritize fixing the {high_count} high-severity issues in the next sprint. These significantly impact code quality and maintainability.")
|
||||
|
||||
if overall_score < 70:
|
||||
recommendations.append("3. **Technical Debt Sprint**: Schedule a dedicated sprint to address accumulated technical debt and improve code quality metrics.")
|
||||
|
||||
if 'testing' in findings and len(findings['testing']) > 0:
|
||||
recommendations.append("4. **Testing Improvements**: Increase test coverage to meet the 80% minimum threshold. Focus on critical paths first (authentication, payment, data processing).")
|
||||
|
||||
if 'security' in findings and len(findings['security']) > 0:
|
||||
recommendations.append("5. **Security Review**: Conduct a thorough security review and penetration testing given the security issues found.")
|
||||
|
||||
if not recommendations:
|
||||
recommendations.append("1. **Maintain Standards**: Continue following best practices and maintain current quality levels.")
|
||||
recommendations.append("2. **Continuous Improvement**: Consider implementing automated code quality checks in CI/CD pipeline.")
|
||||
|
||||
return '\n'.join(recommendations)
|
||||
Reference in New Issue
Block a user