266 lines
7.8 KiB
Python
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)
|