Add rules, fixes, and llm modules
This commit is contained in:
265
config_auditor/rules.py
Normal file
265
config_auditor/rules.py
Normal file
@@ -0,0 +1,265 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass, field
|
||||
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)
|
||||
Reference in New Issue
Block a user