updates
This commit is contained in:
@@ -0,0 +1,435 @@
|
||||
import json
|
||||
from collections import namedtuple
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import Any, List, Optional, Set, Union, Dict
|
||||
|
||||
from dparse.dependencies import Dependency
|
||||
from dparse import parse, filetypes
|
||||
from packaging.specifiers import SpecifierSet
|
||||
from packaging.requirements import Requirement
|
||||
from packaging.utils import canonicalize_name
|
||||
from packaging.version import parse as parse_version
|
||||
|
||||
from packaging.version import Version
|
||||
|
||||
from safety.errors import InvalidRequirementError
|
||||
|
||||
try:
|
||||
from packaging.version import LegacyVersion as legacyType
|
||||
except ImportError:
|
||||
legacyType = None
|
||||
|
||||
from .requirements import is_pinned_requirement
|
||||
|
||||
class DictConverter(object):
|
||||
"""
|
||||
A class to convert objects to dictionaries.
|
||||
"""
|
||||
|
||||
def to_dict(self, **kwargs: Any) -> Dict:
|
||||
pass
|
||||
|
||||
|
||||
announcement_nmt = namedtuple('Announcement', ['type', 'message'])
|
||||
remediation_nmt = namedtuple('Remediation', ['Package', 'closest_secure_version', 'secure_versions',
|
||||
'latest_package_version'])
|
||||
cve_nmt = namedtuple('Cve', ['name', 'cvssv2', 'cvssv3'])
|
||||
severity_nmt = namedtuple('Severity', ['source', 'cvssv2', 'cvssv3'])
|
||||
vulnerability_nmt = namedtuple('Vulnerability',
|
||||
['vulnerability_id', 'package_name', 'pkg', 'ignored', 'ignored_reason', 'ignored_expires',
|
||||
'vulnerable_spec', 'all_vulnerable_specs', 'analyzed_version', 'analyzed_requirement',
|
||||
'advisory', 'is_transitive', 'published_date', 'fixed_versions',
|
||||
'closest_versions_without_known_vulnerabilities', 'resources', 'CVE', 'severity',
|
||||
'affected_versions', 'more_info_url'])
|
||||
RequirementFile = namedtuple('RequirementFile', ['path'])
|
||||
|
||||
|
||||
class SafetyRequirement(Requirement):
|
||||
"""
|
||||
A subclass of Requirement that includes additional attributes and methods for safety requirements.
|
||||
"""
|
||||
def __init__(self, requirement: Union[str, Dependency], found: Optional[str] = None) -> None:
|
||||
"""
|
||||
Initialize a SafetyRequirement.
|
||||
|
||||
Args:
|
||||
requirement (Union[str, Dependency]): The requirement as a string or Dependency object.
|
||||
found (Optional[str], optional): Where the requirement was found. Defaults to None.
|
||||
|
||||
Raises:
|
||||
InvalidRequirementError: If the requirement cannot be parsed.
|
||||
"""
|
||||
dep = requirement
|
||||
|
||||
if isinstance(requirement, str):
|
||||
deps = parse(requirement, file_type=filetypes.requirements_txt).dependencies
|
||||
dep = deps[0] if deps else None
|
||||
|
||||
if not dep:
|
||||
raise InvalidRequirementError(line=str(requirement))
|
||||
|
||||
raw_line = dep.line
|
||||
to_parse = dep.line
|
||||
# Hash and comments are only a pip feature, so removing them.
|
||||
if '#' in to_parse:
|
||||
to_parse = dep.line.split('#')[0]
|
||||
|
||||
for req_hash in 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(SafetyRequirement, self).__init__(to_parse)
|
||||
except Exception:
|
||||
raise InvalidRequirementError(line=requirement.line if isinstance(requirement, Dependency) else requirement)
|
||||
|
||||
self.raw = raw_line
|
||||
self.found = found
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return str(self) == str(other)
|
||||
|
||||
def to_dict(self, **kwargs: Any) -> Dict:
|
||||
"""
|
||||
Convert the requirement to a dictionary.
|
||||
|
||||
Args:
|
||||
**kwargs: Additional keyword arguments.
|
||||
|
||||
Returns:
|
||||
dict: The dictionary representation of the requirement.
|
||||
"""
|
||||
specifier_obj = self.specifier
|
||||
if not "specifier_obj" 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
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class Package(DictConverter):
|
||||
"""
|
||||
A class representing a package.
|
||||
"""
|
||||
name: str
|
||||
version: Optional[str]
|
||||
requirements: List[SafetyRequirement]
|
||||
found: Optional[str] = None
|
||||
absolute_path: Optional[str] = None
|
||||
insecure_versions: List[str] = field(default_factory=lambda: [])
|
||||
secure_versions: List[str] = field(default_factory=lambda: [])
|
||||
latest_version_without_known_vulnerabilities: Optional[str] = None
|
||||
latest_version: Optional[str] = None
|
||||
more_info_url: Optional[str] = None
|
||||
|
||||
def has_unpinned_req(self) -> bool:
|
||||
"""
|
||||
Check if the package has unpinned requirements.
|
||||
|
||||
Returns:
|
||||
bool: True if there are unpinned requirements, False otherwise.
|
||||
"""
|
||||
for req in self.requirements:
|
||||
if not is_pinned_requirement(req.specifier):
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_unpinned_req(self) -> filter:
|
||||
"""
|
||||
Get the unpinned requirements.
|
||||
|
||||
Returns:
|
||||
filter: A filter object with the unpinned requirements.
|
||||
"""
|
||||
return filter(lambda r: not is_pinned_requirement(r.specifier), self.requirements)
|
||||
|
||||
def filter_by_supported_versions(self, versions: List[str]) -> List[str]:
|
||||
"""
|
||||
Filter the versions by supported versions.
|
||||
|
||||
Args:
|
||||
versions (List[str]): The list of versions.
|
||||
|
||||
Returns:
|
||||
List[str]: The list of supported versions.
|
||||
"""
|
||||
allowed = []
|
||||
|
||||
for version in versions:
|
||||
try:
|
||||
parse_version(version)
|
||||
allowed.append(version)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return allowed
|
||||
|
||||
def get_versions(self, db_full: Dict) -> Set[str]:
|
||||
"""
|
||||
Get the versions from the database.
|
||||
|
||||
Args:
|
||||
db_full (Dict): The full database.
|
||||
|
||||
Returns:
|
||||
Set[str]: The set of versions.
|
||||
"""
|
||||
pkg_meta = db_full.get('meta', {}).get('packages', {}).get(self.name, {})
|
||||
versions = self.filter_by_supported_versions(
|
||||
pkg_meta.get("insecure_versions", []) + pkg_meta.get("secure_versions", []))
|
||||
return set(versions)
|
||||
|
||||
def refresh_from(self, db_full: Dict) -> None:
|
||||
"""
|
||||
Refresh the package information from the database.
|
||||
|
||||
Args:
|
||||
db_full (Dict): The full database.
|
||||
"""
|
||||
base_domain = db_full.get('meta', {}).get('base_domain')
|
||||
pkg_meta = db_full.get('meta', {}).get('packages', {}).get(canonicalize_name(self.name), {})
|
||||
|
||||
kwargs = {'insecure_versions': self.filter_by_supported_versions(pkg_meta.get("insecure_versions", [])),
|
||||
'secure_versions': self.filter_by_supported_versions(pkg_meta.get("secure_versions", [])),
|
||||
'latest_version_without_known_vulnerabilities': pkg_meta.get("latest_secure_version",
|
||||
None),
|
||||
'latest_version': pkg_meta.get("latest_version", None),
|
||||
'more_info_url': f"{base_domain}{pkg_meta.get('more_info_path', '')}"}
|
||||
|
||||
self.update(kwargs)
|
||||
|
||||
def to_dict(self, **kwargs: Any) -> Dict:
|
||||
"""
|
||||
Convert the package to a dictionary.
|
||||
|
||||
Args:
|
||||
**kwargs: Additional keyword arguments.
|
||||
|
||||
Returns:
|
||||
dict: The dictionary representation of the package.
|
||||
"""
|
||||
if kwargs.get('short_version', False):
|
||||
return {
|
||||
'name': self.name,
|
||||
'version': self.version,
|
||||
'requirements': self.requirements
|
||||
}
|
||||
|
||||
return {'name': self.name,
|
||||
'version': self.version,
|
||||
'requirements': self.requirements,
|
||||
'found': None,
|
||||
'insecure_versions': self.insecure_versions,
|
||||
'secure_versions': self.secure_versions,
|
||||
'latest_version_without_known_vulnerabilities': self.latest_version_without_known_vulnerabilities,
|
||||
'latest_version': self.latest_version,
|
||||
'more_info_url': self.more_info_url
|
||||
}
|
||||
|
||||
def update(self, new: Dict) -> None:
|
||||
"""
|
||||
Update the package attributes with new values.
|
||||
|
||||
Args:
|
||||
new (Dict): The new attribute values.
|
||||
"""
|
||||
for key, value in new.items():
|
||||
if hasattr(self, key):
|
||||
setattr(self, key, value)
|
||||
|
||||
|
||||
class Announcement(announcement_nmt):
|
||||
"""
|
||||
A class representing an announcement.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class Remediation(remediation_nmt, DictConverter):
|
||||
"""
|
||||
A class representing a remediation.
|
||||
"""
|
||||
|
||||
def to_dict(self) -> Dict:
|
||||
"""
|
||||
Convert the remediation to a dictionary.
|
||||
|
||||
Returns:
|
||||
Dict: The dictionary representation of the remediation.
|
||||
"""
|
||||
return {'package': self.Package.name,
|
||||
'closest_secure_version': self.closest_secure_version,
|
||||
'secure_versions': self.secure_versions,
|
||||
'latest_package_version': self.latest_package_version
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class Fix:
|
||||
"""
|
||||
A class representing a fix.
|
||||
"""
|
||||
dependency: Any = None
|
||||
previous_version: Any = None
|
||||
previous_spec: Optional[str] = None
|
||||
other_options: List[str] = field(default_factory=lambda: [])
|
||||
updated_version: Any = None
|
||||
update_type: str = ""
|
||||
package: str = ""
|
||||
status: str = ""
|
||||
applied_at: str = ""
|
||||
fix_type: str = ""
|
||||
more_info_url: str = ""
|
||||
|
||||
|
||||
class CVE(cve_nmt, DictConverter):
|
||||
"""
|
||||
A class representing a CVE.
|
||||
"""
|
||||
|
||||
def to_dict(self) -> Dict:
|
||||
"""
|
||||
Convert the CVE to a dictionary.
|
||||
|
||||
Returns:
|
||||
Dict: The dictionary representation of the CVE.
|
||||
"""
|
||||
return {'name': self.name, 'cvssv2': self.cvssv2, 'cvssv3': self.cvssv3}
|
||||
|
||||
|
||||
class Severity(severity_nmt, DictConverter):
|
||||
"""
|
||||
A class representing the severity of a vulnerability.
|
||||
"""
|
||||
def to_dict(self) -> Dict:
|
||||
"""
|
||||
Convert the severity to a dictionary.
|
||||
|
||||
Returns:
|
||||
Dict: The dictionary representation of the severity.
|
||||
"""
|
||||
result = {'severity': {'source': self.source}}
|
||||
|
||||
result['severity']['cvssv2'] = self.cvssv2
|
||||
result['severity']['cvssv3'] = self.cvssv3
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class SafetyEncoder(json.JSONEncoder):
|
||||
"""
|
||||
A custom JSON encoder for Safety related objects.
|
||||
"""
|
||||
def default(self, value: Any) -> Any:
|
||||
"""
|
||||
Override the default method to handle custom objects.
|
||||
|
||||
Args:
|
||||
value (Any): The value to encode.
|
||||
|
||||
Returns:
|
||||
Any: The encoded value.
|
||||
"""
|
||||
if isinstance(value, SafetyRequirement):
|
||||
return value.to_dict()
|
||||
elif isinstance(value, Version) or (legacyType and isinstance(value, legacyType)):
|
||||
return str(value)
|
||||
else:
|
||||
return super().default(value)
|
||||
|
||||
|
||||
class Vulnerability(vulnerability_nmt):
|
||||
"""
|
||||
A class representing a vulnerability.
|
||||
"""
|
||||
|
||||
def to_dict(self) -> Dict:
|
||||
"""
|
||||
Convert the vulnerability to a dictionary.
|
||||
|
||||
Returns:
|
||||
Dict: The dictionary representation of the vulnerability.
|
||||
"""
|
||||
empty_list_if_none = ['fixed_versions', 'closest_versions_without_known_vulnerabilities', 'resources']
|
||||
result = {
|
||||
}
|
||||
|
||||
ignore = ['pkg']
|
||||
|
||||
for field, value in zip(self._fields, self):
|
||||
if field in ignore:
|
||||
continue
|
||||
|
||||
if value is None and field in empty_list_if_none:
|
||||
value = []
|
||||
|
||||
if isinstance(value, set):
|
||||
result[field] = list(value)
|
||||
elif isinstance(value, CVE):
|
||||
val = None
|
||||
if value.name.startswith("CVE"):
|
||||
val = value.name
|
||||
result[field] = val
|
||||
elif isinstance(value, DictConverter):
|
||||
result.update(value.to_dict())
|
||||
elif isinstance(value, SpecifierSet) or isinstance(value, datetime):
|
||||
result[field] = str(value)
|
||||
else:
|
||||
result[field] = value
|
||||
|
||||
return result
|
||||
|
||||
def get_advisory(self) -> str:
|
||||
"""
|
||||
Get the advisory for the vulnerability.
|
||||
|
||||
Returns:
|
||||
str: The advisory text.
|
||||
"""
|
||||
return self.advisory.replace('\r', '') if self.advisory else "No advisory found for this vulnerability."
|
||||
|
||||
def to_model_dict(self) -> Dict:
|
||||
"""
|
||||
Convert the vulnerability to a dictionary for the model.
|
||||
|
||||
Returns:
|
||||
Dict: The dictionary representation of the vulnerability for the model.
|
||||
"""
|
||||
try:
|
||||
affected_spec = next(iter(self.vulnerable_spec))
|
||||
except Exception:
|
||||
affected_spec = ""
|
||||
|
||||
repr = {
|
||||
"id": self.vulnerability_id,
|
||||
"package_name": self.package_name,
|
||||
"vulnerable_spec": affected_spec,
|
||||
"analyzed_specification": self.analyzed_requirement.raw
|
||||
}
|
||||
|
||||
if self.ignored:
|
||||
repr["ignored"] = {"reason": self.ignored_reason,
|
||||
"expires": self.ignored_expires}
|
||||
|
||||
return repr
|
||||
|
||||
|
||||
@dataclass
|
||||
class Safety:
|
||||
"""
|
||||
A class representing Safety settings.
|
||||
"""
|
||||
client: Any
|
||||
keys: Any
|
||||
|
||||
Reference in New Issue
Block a user