Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:16:43 +08:00
commit 5fce0eef2b
46 changed files with 8067 additions and 0 deletions

View 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'

View 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

View 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

View 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

View 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

View 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'

View 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

View 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()

View 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),
}

View 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)