updates
This commit is contained in:
393
Backend/venv/lib/python3.12/site-packages/safety/alerts/utils.py
Normal file
393
Backend/venv/lib/python3.12/site-packages/safety/alerts/utils.py
Normal file
@@ -0,0 +1,393 @@
|
||||
# type: ignore
|
||||
import hashlib
|
||||
import os
|
||||
import sys
|
||||
from functools import wraps
|
||||
from typing import Optional, List, Dict, Any
|
||||
|
||||
from packaging.version import parse as parse_version
|
||||
from packaging.specifiers import SpecifierSet
|
||||
from pathlib import Path
|
||||
|
||||
import click
|
||||
|
||||
# Jinja2 will only be installed if the optional deps are installed.
|
||||
# It's fine if our functions fail, but don't let this top level
|
||||
# import error out.
|
||||
from safety.models import is_pinned_requirement
|
||||
from safety.output_utils import (
|
||||
get_unpinned_hint,
|
||||
get_specifier_range_info,
|
||||
get_fix_hint_for_unpinned,
|
||||
)
|
||||
|
||||
try:
|
||||
import jinja2
|
||||
except ImportError:
|
||||
jinja2 = None
|
||||
|
||||
import requests
|
||||
from safety.meta import get_meta_http_headers
|
||||
|
||||
|
||||
def highest_base_score(vulns: List[Dict[str, Any]]) -> float:
|
||||
"""
|
||||
Calculates the highest CVSS base score from a list of vulnerabilities.
|
||||
|
||||
Args:
|
||||
vulns (List[Dict[str, Any]]): The list of vulnerabilities.
|
||||
|
||||
Returns:
|
||||
float: The highest CVSS base score.
|
||||
"""
|
||||
highest_base_score = 0
|
||||
for vuln in vulns:
|
||||
if vuln["severity"] is not None:
|
||||
highest_base_score = max(
|
||||
highest_base_score,
|
||||
(vuln["severity"].get("cvssv3", {}) or {}).get("base_score", 10),
|
||||
)
|
||||
|
||||
return highest_base_score
|
||||
|
||||
|
||||
def generate_branch_name(pkg: str, remediation: Dict[str, Any]) -> str:
|
||||
"""
|
||||
Generates a branch name for a given package and remediation.
|
||||
|
||||
Args:
|
||||
pkg (str): The package name.
|
||||
remediation (Dict[str, Any]): The remediation data.
|
||||
|
||||
Returns:
|
||||
str: The generated branch name.
|
||||
"""
|
||||
return f"{pkg}/{remediation['requirement']['specifier']}/{remediation['recommended_version']}"
|
||||
|
||||
|
||||
def generate_issue_title(pkg: str, remediation: Dict[str, Any]) -> str:
|
||||
"""
|
||||
Generates an issue title for a given package and remediation.
|
||||
|
||||
Args:
|
||||
pkg (str): The package name.
|
||||
remediation (Dict[str, Any]): The remediation data.
|
||||
|
||||
Returns:
|
||||
str: The generated issue title.
|
||||
"""
|
||||
return f"Security Vulnerability in {pkg}{remediation['requirement']['specifier']}"
|
||||
|
||||
|
||||
def get_hint(remediation: Dict[str, Any]) -> str:
|
||||
"""
|
||||
Generates a hint for a given remediation.
|
||||
|
||||
Args:
|
||||
remediation (Dict[str, Any]): The remediation data.
|
||||
|
||||
Returns:
|
||||
str: The generated hint.
|
||||
"""
|
||||
pinned = is_pinned_requirement(
|
||||
SpecifierSet(remediation["requirement"]["specifier"])
|
||||
)
|
||||
hint = ""
|
||||
|
||||
if not pinned:
|
||||
fix_hint = get_fix_hint_for_unpinned(remediation)
|
||||
hint = (
|
||||
f"{fix_hint}\n\n{get_unpinned_hint(remediation['requirement']['name'])} "
|
||||
f"{get_specifier_range_info(style=False)}"
|
||||
)
|
||||
|
||||
return hint
|
||||
|
||||
|
||||
def generate_title(
|
||||
pkg: str, remediation: Dict[str, Any], vulns: List[Dict[str, Any]]
|
||||
) -> str:
|
||||
"""
|
||||
Generates a title for a pull request or issue.
|
||||
|
||||
Args:
|
||||
pkg (str): The package name.
|
||||
remediation (Dict[str, Any]): The remediation data.
|
||||
vulns (List[Dict[str, Any]]): The list of vulnerabilities.
|
||||
|
||||
Returns:
|
||||
str: The generated title.
|
||||
"""
|
||||
suffix = "y" if len(vulns) == 1 else "ies"
|
||||
from_dependency = (
|
||||
remediation["version"]
|
||||
if remediation["version"]
|
||||
else remediation["requirement"]["specifier"]
|
||||
)
|
||||
return f"Update {pkg} from {from_dependency} to {remediation['recommended_version']} to fix {len(vulns)} vulnerabilit{suffix}"
|
||||
|
||||
|
||||
def generate_body(
|
||||
pkg: str, remediation: Dict[str, Any], vulns: List[Dict[str, Any]], *, api_key: str
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Generates the body content for a pull request.
|
||||
|
||||
Args:
|
||||
pkg (str): The package name.
|
||||
remediation (Dict[str, Any]): The remediation data.
|
||||
vulns (List[Dict[str, Any]]): The list of vulnerabilities.
|
||||
api_key (str): The API key for fetching changelog data.
|
||||
|
||||
Returns:
|
||||
str: The generated body content.
|
||||
"""
|
||||
changelog = fetch_changelog(
|
||||
pkg,
|
||||
remediation["version"],
|
||||
remediation["recommended_version"],
|
||||
api_key=api_key,
|
||||
from_spec=remediation.get("requirement", {}).get("specifier", None),
|
||||
)
|
||||
|
||||
p = Path(__file__).parent / "templates"
|
||||
env = jinja2.Environment(loader=jinja2.FileSystemLoader(Path(p))) # type: ignore
|
||||
template = env.get_template("pr.jinja2")
|
||||
|
||||
overall_impact = cvss3_score_to_label(highest_base_score(vulns))
|
||||
|
||||
context = {
|
||||
"pkg": pkg,
|
||||
"remediation": remediation,
|
||||
"vulns": vulns,
|
||||
"changelog": changelog,
|
||||
"overall_impact": overall_impact,
|
||||
"summary_changelog": False,
|
||||
"hint": get_hint(remediation),
|
||||
}
|
||||
|
||||
result = template.render(context)
|
||||
|
||||
# GitHub has a PR body length limit of 65536. If we're going over that, skip the changelog and just use a link.
|
||||
if len(result) < 65500:
|
||||
return result
|
||||
|
||||
context["summary_changelog"] = True
|
||||
|
||||
return template.render(context)
|
||||
|
||||
|
||||
def generate_issue_body(
|
||||
pkg: str, remediation: Dict[str, Any], vulns: List[Dict[str, Any]], *, api_key: str
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Generates the body content for an issue.
|
||||
|
||||
Args:
|
||||
pkg (str): The package name.
|
||||
remediation (Dict[str, Any]): The remediation data.
|
||||
vulns (List[Dict[str, Any]]): The list of vulnerabilities.
|
||||
api_key (str): The API key for fetching changelog data.
|
||||
|
||||
Returns:
|
||||
str: The generated body content.
|
||||
"""
|
||||
changelog = fetch_changelog(
|
||||
pkg,
|
||||
remediation["version"],
|
||||
remediation["recommended_version"],
|
||||
api_key=api_key,
|
||||
from_spec=remediation.get("requirement", {}).get("specifier", None),
|
||||
)
|
||||
|
||||
p = Path(__file__).parent / "templates"
|
||||
env = jinja2.Environment(loader=jinja2.FileSystemLoader(Path(p))) # type: ignore
|
||||
template = env.get_template("issue.jinja2")
|
||||
|
||||
overall_impact = cvss3_score_to_label(highest_base_score(vulns))
|
||||
|
||||
context = {
|
||||
"pkg": pkg,
|
||||
"remediation": remediation,
|
||||
"vulns": vulns,
|
||||
"changelog": changelog,
|
||||
"overall_impact": overall_impact,
|
||||
"summary_changelog": False,
|
||||
"hint": get_hint(remediation),
|
||||
}
|
||||
|
||||
result = template.render(context)
|
||||
|
||||
# GitHub has a PR body length limit of 65536. If we're going over that, skip the changelog and just use a link.
|
||||
if len(result) < 65500:
|
||||
return result
|
||||
|
||||
context["summary_changelog"] = True
|
||||
|
||||
return template.render(context)
|
||||
|
||||
|
||||
def generate_commit_message(pkg: str, remediation: Dict[str, Any]) -> str:
|
||||
"""
|
||||
Generates a commit message for a given package and remediation.
|
||||
|
||||
Args:
|
||||
pkg (str): The package name.
|
||||
remediation (Dict[str, Any]): The remediation data.
|
||||
|
||||
Returns:
|
||||
str: The generated commit message.
|
||||
"""
|
||||
from_dependency = (
|
||||
remediation["version"]
|
||||
if remediation["version"]
|
||||
else remediation["requirement"]["specifier"]
|
||||
)
|
||||
|
||||
return (
|
||||
f"Update {pkg} from {from_dependency} to {remediation['recommended_version']}"
|
||||
)
|
||||
|
||||
|
||||
def git_sha1(raw_contents: bytes) -> str:
|
||||
"""
|
||||
Calculates the SHA-1 hash of the given raw contents.
|
||||
|
||||
Args:
|
||||
raw_contents (bytes): The raw contents to hash.
|
||||
|
||||
Returns:
|
||||
str: The SHA-1 hash.
|
||||
"""
|
||||
return hashlib.sha1(
|
||||
b"blob " + str(len(raw_contents)).encode("ascii") + b"\0" + raw_contents
|
||||
).hexdigest()
|
||||
|
||||
|
||||
def fetch_changelog(
|
||||
package: str,
|
||||
from_version: Optional[str],
|
||||
to_version: str,
|
||||
*,
|
||||
api_key: str,
|
||||
from_spec: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Fetches the changelog for a package from a specified version to another version.
|
||||
|
||||
Args:
|
||||
package (str): The package name.
|
||||
from_version (Optional[str]): The starting version.
|
||||
to_version (str): The ending version.
|
||||
api_key (str): The API key for fetching changelog data.
|
||||
from_spec (Optional[str]): The specifier for the starting version.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The fetched changelog data.
|
||||
"""
|
||||
to_version_parsed = parse_version(to_version)
|
||||
|
||||
if from_version:
|
||||
from_version_parsed = parse_version(from_version)
|
||||
else:
|
||||
from_version_parsed = None
|
||||
from_spec = SpecifierSet(from_spec)
|
||||
|
||||
changelog = {}
|
||||
|
||||
headers = {"X-Api-Key": api_key}
|
||||
headers.update(get_meta_http_headers())
|
||||
|
||||
r = requests.get(
|
||||
"https://pyup.io/api/v1/changelogs/{}/".format(package), headers=headers
|
||||
)
|
||||
|
||||
if r.status_code == 200:
|
||||
data = r.json()
|
||||
if data:
|
||||
# sort the changelog by release
|
||||
sorted_log = sorted(
|
||||
data.items(), key=lambda v: parse_version(v[0]), reverse=True
|
||||
)
|
||||
|
||||
# go over each release and add it to the log if it's within the "upgrade
|
||||
# range" e.g. update from 1.2 to 1.3 includes a changelog for 1.2.1 but
|
||||
# not for 0.4.
|
||||
for version, log in sorted_log:
|
||||
parsed_version = parse_version(version)
|
||||
version_check = from_version and (parsed_version > from_version_parsed)
|
||||
spec_check = (
|
||||
from_spec
|
||||
and isinstance(from_spec, SpecifierSet)
|
||||
and from_spec.contains(parsed_version)
|
||||
)
|
||||
|
||||
if version_check or spec_check and parsed_version <= to_version_parsed:
|
||||
changelog[version] = log
|
||||
|
||||
return changelog
|
||||
|
||||
|
||||
def cvss3_score_to_label(score: float) -> Optional[str]:
|
||||
"""
|
||||
Converts a CVSS v3 score to a severity label.
|
||||
|
||||
Args:
|
||||
score (float): The CVSS v3 score.
|
||||
|
||||
Returns:
|
||||
Optional[str]: The severity label.
|
||||
"""
|
||||
if 0.1 <= score <= 3.9:
|
||||
return "low"
|
||||
elif 4.0 <= score <= 6.9:
|
||||
return "medium"
|
||||
elif 7.0 <= score <= 8.9:
|
||||
return "high"
|
||||
elif 9.0 <= score <= 10.0:
|
||||
return "critical"
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def require_files_report(func):
|
||||
@wraps(func)
|
||||
def inner(obj: Any, *args: Any, **kwargs: Any) -> Any:
|
||||
"""
|
||||
Decorator that ensures a report is generated against a file.
|
||||
|
||||
Args:
|
||||
obj (Any): The object containing the report.
|
||||
*args (Any): Additional arguments.
|
||||
**kwargs (Any): Additional keyword arguments.
|
||||
|
||||
Returns:
|
||||
Any: The result of the decorated function.
|
||||
"""
|
||||
if obj.report["report_meta"]["scan_target"] != "files":
|
||||
click.secho(
|
||||
"This report was generated against an environment, but this alert command requires "
|
||||
"a scan report that was generated against a file. To learn more about the "
|
||||
"`safety alert` command visit https://docs.pyup.io/docs/safety-2-alerts",
|
||||
fg="red",
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
files = obj.report["report_meta"]["scanned"]
|
||||
obj.requirements_files = {}
|
||||
for f in files:
|
||||
if not os.path.exists(f):
|
||||
cwd = os.getcwd()
|
||||
click.secho(
|
||||
"A requirements file scanned in the report, {}, does not exist (looking in {}).".format(
|
||||
f, cwd
|
||||
),
|
||||
fg="red",
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
obj.requirements_files[f] = open(f, "rb").read()
|
||||
|
||||
return func(obj, *args, **kwargs)
|
||||
|
||||
return inner
|
||||
Reference in New Issue
Block a user