159 lines
4.9 KiB
Python
159 lines
4.9 KiB
Python
import abc
|
|
from dataclasses import InitVar, field
|
|
from pathlib import Path
|
|
from typing import List, Optional, Union
|
|
|
|
from dparse import filetypes as parse_strategy
|
|
from dparse import parse as parse_specification
|
|
from dparse.dependencies import Dependency as ParsedDependency
|
|
from packaging.requirements import Requirement
|
|
from packaging.specifiers import SpecifierSet
|
|
from pydantic import VERSION as pydantic_version
|
|
from pydantic import Field
|
|
from pydantic.dataclasses import dataclass
|
|
from typing_extensions import Annotated, ClassVar
|
|
|
|
try:
|
|
from pydantic_core import ArgsKwargs
|
|
except ImportError:
|
|
pass
|
|
|
|
from .vulnerability import RemediationModel, Vulnerability
|
|
|
|
NOT_IMPLEMENTED_ERROR_MSG = (
|
|
"Needs implementation for the specific " "specification type."
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class Specification(metaclass=abc.ABCMeta):
|
|
raw: str
|
|
found: Optional[Path]
|
|
vulnerabilities: List[Vulnerability] = field(default_factory=lambda: [])
|
|
remediation: Optional[RemediationModel] = None
|
|
|
|
@abc.abstractmethod
|
|
def is_pinned(self) -> bool:
|
|
raise NotImplementedError(NOT_IMPLEMENTED_ERROR_MSG)
|
|
|
|
@abc.abstractmethod
|
|
def is_vulnerable(self, *args, **kwargs) -> bool:
|
|
raise NotImplementedError(NOT_IMPLEMENTED_ERROR_MSG)
|
|
|
|
|
|
def get_dep(specification: Union[str, ParsedDependency]):
|
|
_dep = specification
|
|
|
|
if isinstance(specification, str):
|
|
deps = parse_specification(
|
|
specification, file_type=parse_strategy.requirements_txt
|
|
).dependencies
|
|
_dep = deps[0] if deps else None
|
|
|
|
if not isinstance(_dep, ParsedDependency):
|
|
raise ValueError(
|
|
f"The '{specification}' specification is "
|
|
"not a valid Python specificaiton."
|
|
)
|
|
|
|
return _dep
|
|
|
|
|
|
@dataclass(config={"arbitrary_types_allowed": True})
|
|
class PythonSpecification(Requirement, Specification):
|
|
dep: ClassVar[Optional[ParsedDependency]] = Field(default=None, exclude=True)
|
|
|
|
def __load_req(self, specification: Union[str, ParsedDependency]):
|
|
self.dep = get_dep(specification)
|
|
|
|
raw_line = self.dep.line
|
|
to_parse = self.dep.line
|
|
# Hash and comments are only a pip feature, so removing them.
|
|
if "#" in to_parse:
|
|
to_parse = self.dep.line.split("#")[0]
|
|
|
|
for req_hash in self.dep.hashes:
|
|
to_parse = to_parse.replace(req_hash, "")
|
|
|
|
to_parse = to_parse.replace("\\", "").rstrip()
|
|
|
|
try:
|
|
# Try to build a PEP Requirement from the cleaned line
|
|
super().__init__(to_parse)
|
|
except Exception:
|
|
raise ValueError(
|
|
f"The '{raw_line}' specification is "
|
|
"not a valid Python specificaiton."
|
|
)
|
|
|
|
if not pydantic_version.startswith("1."):
|
|
from pydantic import model_validator
|
|
|
|
@model_validator(mode='before')
|
|
def pre_root(cls, values):
|
|
args, kwargs = values.args, values.kwargs
|
|
|
|
try:
|
|
specification = args[0]
|
|
except IndexError:
|
|
raise ValueError('Specification is required')
|
|
|
|
_dep = get_dep(specification)
|
|
|
|
return ArgsKwargs((), {'raw': _dep.line, 'found': None if not kwargs else kwargs.get('found', None), 'dep': _dep})
|
|
|
|
def __post_init__(self):
|
|
self.__load_req(specification=self.raw)
|
|
else:
|
|
def __init__(
|
|
self, specification: Union[str, ParsedDependency], found: Optional[Path] = None
|
|
) -> None:
|
|
self.__load_req(specification=specification)
|
|
self.raw = self.dep.line
|
|
self.found = found
|
|
|
|
def __eq__(self, other):
|
|
return str(self) == str(other)
|
|
|
|
def is_pinned(self) -> bool:
|
|
if not self.specifier or len(self.specifier) != 1:
|
|
return False
|
|
|
|
specifier = next(iter(self.specifier))
|
|
|
|
return (
|
|
specifier.operator == "==" and "*" != specifier.version[-1]
|
|
) or specifier.operator == "==="
|
|
|
|
def is_vulnerable(
|
|
self, vulnerable_spec: SpecifierSet, insecure_versions: List[str]
|
|
):
|
|
if self.is_pinned():
|
|
try:
|
|
return vulnerable_spec.contains(next(iter(self.specifier)).version, prereleases=True)
|
|
except Exception:
|
|
# Ugly for now...
|
|
return False
|
|
|
|
return any(
|
|
self.specifier.filter(
|
|
vulnerable_spec.filter(insecure_versions, prereleases=True),
|
|
prereleases=True,
|
|
)
|
|
)
|
|
|
|
def to_dict(self, **kwargs):
|
|
specifier_obj = self.specifier
|
|
if "specifier_obj" not in kwargs:
|
|
specifier_obj = str(self.specifier)
|
|
|
|
return {
|
|
"raw": self.raw,
|
|
"extras": list(self.extras),
|
|
"marker": str(self.marker) if self.marker else None,
|
|
"name": self.name,
|
|
"specifier": specifier_obj,
|
|
"url": self.url,
|
|
"found": self.found,
|
|
}
|