Files
config-auditor-cli/config_auditor/rules.py
7000pctAUTO ecc88227fe
Some checks failed
CI / test (push) Has been cancelled
CI / build (push) Has been cancelled
fix: resolve CI linting failures by removing unused imports
2026-01-30 18:13:47 +00:00

266 lines
7.8 KiB
Python

from abc import ABC, abstractmethod
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Dict, List, Optional
from packaging import version
import re
@dataclass
class Issue:
severity: str
category: str
message: str
file: Path
line: Optional[int] = None
suggestion: Optional[str] = None
rule_id: Optional[str] = None
@dataclass
class RuleResult:
passed: bool
message: str
severity: str
suggestion: Optional[str] = None
class Rule(ABC):
@property
@abstractmethod
def rule_id(self) -> str:
pass
@property
@abstractmethod
def severity(self) -> str:
pass
@abstractmethod
def evaluate(self, data: Dict[str, Any], file_path: Path) -> RuleResult:
pass
class DeprecatedPackageRule(Rule):
DEPRECATED_PACKAGES = {
"request": "requests",
"urllib2": "urllib.request",
"httplib": "http.client",
"StringIO": "io.StringIO",
}
@property
def rule_id(self) -> str:
return "deprecated-package"
@property
def severity(self) -> str:
return "critical"
def evaluate(self, data: Dict[str, Any], file_path: Path) -> RuleResult:
deps = data.get("dependencies", {})
dev_deps = data.get("devDependencies", {})
all_deps = {**deps, **dev_deps}
for pkg in all_deps:
if pkg in self.DEPRECATED_PACKAGES:
return RuleResult(
passed=False,
message=f"Deprecated package '{pkg}' found. Use '{self.DEPRECATED_PACKAGES[pkg]}' instead.",
severity=self.severity,
suggestion=f"Replace '{pkg}' with '{self.DEPRECATED_PACKAGES[pkg]}' in dependencies"
)
return RuleResult(passed=True, message="No deprecated packages found", severity="info")
class OutdatedVersionRule(Rule):
@property
def rule_id(self) -> str:
return "outdated-version"
@property
def severity(self) -> str:
return "warning"
def evaluate(self, data: Dict[str, Any], file_path: Path) -> RuleResult:
deps = data.get("dependencies", {})
dev_deps = data.get("devDependencies", {})
all_deps = {**deps, **dev_deps}
outdated = []
for pkg, ver in all_deps.items():
if ver and ver.startswith("^"):
try:
v = version.parse(ver[1:])
if v.major == 0:
outdated.append(f"{pkg}@{ver} (major version 0, may have breaking changes)")
except Exception:
pass
if outdated:
return RuleResult(
passed=False,
message=f"Potentially outdated dependencies: {', '.join(outdated)}",
severity=self.severity,
suggestion="Review these dependencies for stability concerns"
)
return RuleResult(passed=True, message="Dependencies appear up to date", severity="info")
class MissingTypeCheckingRule(Rule):
@property
def rule_id(self) -> str:
return "missing-type-checking"
@property
def severity(self) -> str:
return "warning"
def evaluate(self, data: Dict[str, Any], file_path: Path) -> RuleResult:
if data.get("compilerOptions", {}).get("strict") is not True:
return RuleResult(
passed=False,
message="TypeScript strict mode is not enabled",
severity=self.severity,
suggestion='Set "compilerOptions": { "strict": true } in tsconfig.json'
)
return RuleResult(passed=True, message="TypeScript strict mode is enabled", severity="info")
class SecurityVulnerabilityRule(Rule):
VULNERABLE_PATTERNS = [
(r"\"version\":\s*\"0\.\d+\.\d+\"", "Versions starting with 0.x may have security issues"),
(r"\"private\":\s*false", "Package should not be public if marked private"),
]
@property
def rule_id(self) -> str:
return "security-vulnerability"
@property
def severity(self) -> str:
return "critical"
def evaluate(self, data: Dict[str, Any], file_path: Path) -> RuleResult:
import json
content = json.dumps(data)
for pattern, message in self.VULNERABLE_PATTERNS:
if re.search(pattern, content):
return RuleResult(
passed=False,
message=message,
severity=self.severity,
suggestion="Review and update the configuration"
)
return RuleResult(passed=True, message="No obvious security vulnerabilities detected", severity="info")
class MissingScriptsRule(Rule):
@property
def rule_id(self) -> str:
return "missing-scripts"
@property
def severity(self) -> str:
return "warning"
def evaluate(self, data: Dict[str, Any], file_path: Path) -> RuleResult:
scripts = data.get("scripts", {})
required_scripts = ["test", "build"]
missing = [s for s in required_scripts if s not in scripts]
if missing:
return RuleResult(
passed=False,
message=f"Missing recommended scripts: {', '.join(missing)}",
severity=self.severity,
suggestion=f"Add scripts for: {', '.join(missing)}"
)
return RuleResult(passed=True, message="Required scripts are present", severity="info")
class PythonProjectMetaRule(Rule):
@property
def rule_id(self) -> str:
return "python-project-meta"
@property
def severity(self) -> str:
return "info"
def evaluate(self, data: Dict[str, Any], file_path: Path) -> RuleResult:
issues = []
if "tool" in data:
if "poetry" in data.get("tool", {}):
poetry = data["tool"]["poetry"]
if not poetry.get("name"):
issues.append("Missing poetry project name")
if not poetry.get("version"):
issues.append("Missing poetry project version")
elif "pytest" in data.get("tool", {}):
if not data["tool"]["pytest"].get("testpaths"):
issues.append("Missing pytest testpaths")
if issues:
return RuleResult(
passed=False,
message=f"Python project issues: {', '.join(issues)}",
severity=self.severity,
suggestion="Add missing project metadata"
)
return RuleResult(passed=True, message="Python project metadata is complete", severity="info")
class RuleRegistry:
RULES = {
"json": [
DeprecatedPackageRule(),
OutdatedVersionRule(),
MissingTypeCheckingRule(),
SecurityVulnerabilityRule(),
MissingScriptsRule(),
],
"yaml": [
SecurityVulnerabilityRule(),
],
"toml": [
PythonProjectMetaRule(),
],
}
def __init__(self):
self._rules = {k: list(v) for k, v in self.RULES.items()}
def evaluate(self, format_type: str, data: Dict[str, Any], file_path: Path) -> List[Issue]:
issues = []
rules = self._rules.get(format_type, [])
for rule in rules:
result = rule.evaluate(data, file_path)
if not result.passed:
issues.append(Issue(
severity=result.severity,
category=rule.rule_id,
message=result.message,
file=file_path,
suggestion=result.suggestion,
rule_id=rule.rule_id
))
return issues
def add_rule(self, format_type: str, rule: Rule):
if format_type not in self._rules:
self._rules[format_type] = []
self._rules[format_type].append(rule)