match_face/.venv/Lib/site-packages/pipenv/patched/safety/safety.py

629 lines
22 KiB
Python

# -*- coding: utf-8 -*-
import errno
import itertools
import json
import logging
import os
import sys
import time
from datetime import datetime
from typing import List
import pipenv.patched.pip._vendor.requests as requests
from pipenv.patched.pip._vendor.packaging.specifiers import SpecifierSet
from pipenv.patched.pip._vendor.packaging.utils import canonicalize_name
from pipenv.patched.pip._vendor.packaging.version import parse as parse_version, parse
from .constants import (API_MIRRORS, CACHE_FILE, OPEN_MIRRORS, REQUEST_TIMEOUT, API_BASE_URL)
from .errors import (DatabaseFetchError, DatabaseFileNotFoundError,
InvalidKeyError, TooManyRequestsError, NetworkConnectionError,
RequestTimeoutError, ServerError, MalformedDatabase)
from .models import Vulnerability, CVE, Severity
from .util import RequirementFile, read_requirements, Package, build_telemetry_data, sync_safety_context, SafetyContext, \
validate_expiration_date, is_a_remote_mirror
if sys.version_info < (3, 10):
from pipenv.vendor import importlib_metadata
else:
import importlib.metadata as importlib_metadata
session = requests.session()
LOG = logging.getLogger(__name__)
def get_from_cache(db_name, cache_valid_seconds=0):
LOG.debug('Trying to get from cache...')
if os.path.exists(CACHE_FILE):
LOG.info('Cache file path: %s', CACHE_FILE)
with open(CACHE_FILE) as f:
try:
data = json.loads(f.read())
LOG.debug('Trying to get the %s from the cache file', db_name)
LOG.debug('Databases in CACHE file: %s', ', '.join(data))
if db_name in data:
LOG.debug('db_name %s', db_name)
if "cached_at" in data[db_name]:
if data[db_name]["cached_at"] + cache_valid_seconds > time.time():
LOG.debug('Getting the database from cache at %s, cache setting: %s',
data[db_name]["cached_at"], cache_valid_seconds)
return data[db_name]["db"]
LOG.debug('Cached file is too old, it was cached at %s', data[db_name]["cached_at"])
else:
LOG.debug('There is not the cached_at key in %s database', data[db_name])
except json.JSONDecodeError:
LOG.debug('JSONDecodeError trying to get the cached database.')
else:
LOG.debug("Cache file doesn't exist...")
return False
def write_to_cache(db_name, data):
# cache is in: ~/safety/cache.json
# and has the following form:
# {
# "insecure.json": {
# "cached_at": 12345678
# "db": {}
# },
# "insecure_full.json": {
# "cached_at": 12345678
# "db": {}
# },
# }
if not os.path.exists(os.path.dirname(CACHE_FILE)):
try:
os.makedirs(os.path.dirname(CACHE_FILE))
with open(CACHE_FILE, "w") as _:
_.write(json.dumps({}))
LOG.debug('Cache file created')
except OSError as exc: # Guard against race condition
LOG.debug('Unable to create the cache file because: %s', exc.errno)
if exc.errno != errno.EEXIST:
raise
with open(CACHE_FILE, "r") as f:
try:
cache = json.loads(f.read())
except json.JSONDecodeError:
LOG.debug('JSONDecodeError in the local cache, dumping the full cache file.')
cache = {}
with open(CACHE_FILE, "w") as f:
cache[db_name] = {
"cached_at": time.time(),
"db": data
}
f.write(json.dumps(cache))
LOG.debug('Safety updated the cache file for %s database.', db_name)
def fetch_database_url(mirror, db_name, key, cached, proxy, telemetry=True):
headers = {}
if key:
headers["X-Api-Key"] = key
if not proxy:
proxy = {}
if cached:
cached_data = get_from_cache(db_name=db_name, cache_valid_seconds=cached)
if cached_data:
LOG.info('Database %s returned from cache.', db_name)
return cached_data
url = mirror + db_name
telemetry_data = {'telemetry': json.dumps(build_telemetry_data(telemetry=telemetry))}
try:
r = session.get(url=url, timeout=REQUEST_TIMEOUT, headers=headers, proxies=proxy, params=telemetry_data)
except requests.exceptions.ConnectionError:
raise NetworkConnectionError()
except requests.exceptions.Timeout:
raise RequestTimeoutError()
except requests.exceptions.RequestException:
raise DatabaseFetchError()
if r.status_code == 403:
raise InvalidKeyError(key=key, reason=r.text)
if r.status_code == 429:
raise TooManyRequestsError(reason=r.text)
if r.status_code != 200:
raise ServerError(reason=r.reason)
try:
data = r.json()
except json.JSONDecodeError as e:
raise MalformedDatabase(reason=e)
if cached:
LOG.info('Writing %s to cache because cached value was %s', db_name, cached)
write_to_cache(db_name, data)
return data
def fetch_policy(key, proxy):
url = f"{API_BASE_URL}policy/"
headers = {"X-Api-Key": key}
if not proxy:
proxy = {}
try:
LOG.debug(f'Getting policy')
r = session.get(url=url, timeout=REQUEST_TIMEOUT, headers=headers, proxies=proxy)
LOG.debug(r.text)
return r.json()
except:
import pipenv.vendor.click as click
LOG.exception("Error fetching policy")
click.secho(
"Warning: couldn't fetch policy from pyup.io.",
fg="yellow",
file=sys.stderr
)
return {"safety_policy": "", "audit_and_monitor": False}
def post_results(key, proxy, safety_json, policy_file):
url = f"{API_BASE_URL}result/"
headers = {"X-Api-Key": key}
if not proxy:
proxy = {}
# safety_json is in text form already. policy_file is a text YAML
audit_report = {
"safety_json": json.loads(safety_json),
"policy_file": policy_file
}
try:
LOG.debug(f'Posting results: {audit_report}')
r = session.post(url=url, timeout=REQUEST_TIMEOUT, headers=headers, proxies=proxy, json=audit_report)
LOG.debug(r.text)
return r.json()
except:
import pipenv.vendor.click as click
LOG.exception("Error posting results")
click.secho(
"Warning: couldn't upload results to pyup.io.",
fg="yellow",
file=sys.stderr
)
return {}
def fetch_database_file(path, db_name):
full_path = os.path.join(path, db_name)
if not os.path.exists(full_path):
raise DatabaseFileNotFoundError(db=path)
with open(full_path) as f:
return json.loads(f.read())
def fetch_database(full=False, key=False, db=False, cached=0, proxy=None, telemetry=True):
if key:
mirrors = API_MIRRORS
elif db:
mirrors = [db]
else:
mirrors = OPEN_MIRRORS
db_name = "insecure_full.json" if full else "insecure.json"
for mirror in mirrors:
# mirror can either be a local path or a URL
if is_a_remote_mirror(mirror):
data = fetch_database_url(mirror, db_name=db_name, key=key, cached=cached, proxy=proxy, telemetry=telemetry)
else:
data = fetch_database_file(mirror, db_name=db_name)
if data:
return data
raise DatabaseFetchError()
def get_vulnerabilities(pkg, spec, db):
for entry in db[pkg]:
for entry_spec in entry["specs"]:
if entry_spec == spec:
yield entry
def get_vulnerability_from(vuln_id, cve, data, specifier, db, name, pkg, ignore_vulns):
base_domain = db.get('$meta', {}).get('base_domain')
pkg_meta = db.get('$meta', {}).get('packages', {}).get(name, {})
insecure_versions = pkg_meta.get("insecure_versions", [])
secure_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)
pkg_refreshed = pkg._replace(insecure_versions=insecure_versions, secure_versions=secure_versions,
latest_version_without_known_vulnerabilities=latest_version_without_known_vulnerabilities,
latest_version=latest_version,
more_info_url=f"{base_domain}{pkg_meta.get('more_info_path', '')}")
ignored = (ignore_vulns and vuln_id in ignore_vulns and (
not ignore_vulns[vuln_id]['expires'] or ignore_vulns[vuln_id]['expires'] > datetime.utcnow()))
more_info_url = f"{base_domain}{data.get('more_info_path', '')}"
severity = None
if cve and (cve.cvssv2 or cve.cvssv3):
severity = Severity(source=cve.name, cvssv2=cve.cvssv2, cvssv3=cve.cvssv3)
return Vulnerability(
vulnerability_id=vuln_id,
package_name=name,
pkg=pkg_refreshed,
ignored=ignored,
ignored_reason=ignore_vulns.get(vuln_id, {}).get('reason', None) if ignore_vulns else None,
ignored_expires=ignore_vulns.get(vuln_id, {}).get('expires', None) if ignore_vulns else None,
vulnerable_spec=specifier,
all_vulnerable_specs=data.get("specs", []),
analyzed_version=pkg_refreshed.version,
advisory=data.get("advisory"),
is_transitive=data.get("transitive", False),
published_date=data.get("published_date"),
fixed_versions=[ver for ver in data.get("fixed_versions", []) if ver],
closest_versions_without_known_vulnerabilities=data.get("closest_secure_versions", []),
resources=data.get("vulnerability_resources"),
CVE=cve,
severity=severity,
affected_versions=data.get("affected_versions", []),
more_info_url=more_info_url
)
def get_cve_from(data, db_full):
cve_data = data.get("cve", '')
if not cve_data:
return None
cve_id = cve_data.split(",")[0].strip()
cve_meta = db_full.get("$meta", {}).get("cve", {}).get(cve_id, {})
return CVE(name=cve_id, cvssv2=cve_meta.get("cvssv2", None),
cvssv3=cve_meta.get("cvssv3", None))
def ignore_vuln_if_needed(vuln_id, cve, ignore_vulns, ignore_severity_rules):
if not ignore_severity_rules or not isinstance(ignore_vulns, dict):
return
severity = None
if cve:
if cve.cvssv2 and cve.cvssv2.get("base_score", None):
severity = cve.cvssv2.get("base_score", None)
if cve.cvssv3 and cve.cvssv3.get("base_score", None):
severity = cve.cvssv3.get("base_score", None)
ignore_severity_below = float(ignore_severity_rules.get('ignore-cvss-severity-below', 0.0))
ignore_unknown_severity = bool(ignore_severity_rules.get('ignore-cvss-unknown-severity', False))
if severity:
if float(severity) < ignore_severity_below:
reason = 'Ignored by severity rule in policy file, {0} < {1}'.format(float(severity),
ignore_severity_below)
ignore_vulns[vuln_id] = {'reason': reason, 'expires': None}
elif ignore_unknown_severity:
reason = 'Unknown CVSS severity, ignored by severity rule in policy file.'
ignore_vulns[vuln_id] = {'reason': reason, 'expires': None}
@sync_safety_context
def check(packages, key=False, db_mirror=False, cached=0, ignore_vulns=None, ignore_severity_rules=None, proxy=None,
include_ignored=False, is_env_scan=True, telemetry=True, params=None, project=None):
SafetyContext().command = 'check'
db = fetch_database(key=key, db=db_mirror, cached=cached, proxy=proxy, telemetry=telemetry)
db_full = None
vulnerable_packages = frozenset(db.keys())
vulnerabilities = []
for pkg in packages:
# Ignore recursive files not resolved
if isinstance(pkg, RequirementFile):
continue
# normalize the package name, the safety-db is converting underscores to dashes and uses
# lowercase
name = canonicalize_name(pkg.name)
if name in vulnerable_packages:
# we have a candidate here, build the spec set
for specifier in db[name]:
spec_set = SpecifierSet(specifiers=specifier)
if spec_set.contains(pkg.version):
if not db_full:
db_full = fetch_database(full=True, key=key, db=db_mirror, cached=cached, proxy=proxy,
telemetry=telemetry)
for data in get_vulnerabilities(pkg=name, spec=specifier, db=db_full):
vuln_id = data.get("id").replace("pyup.io-", "")
cve = get_cve_from(data, db_full)
ignore_vuln_if_needed(vuln_id, cve, ignore_vulns, ignore_severity_rules)
vulnerability = get_vulnerability_from(vuln_id, cve, data, specifier, db_full, name, pkg,
ignore_vulns)
should_add_vuln = not (vulnerability.is_transitive and is_env_scan)
if (include_ignored or vulnerability.vulnerability_id not in ignore_vulns) and should_add_vuln:
vulnerabilities.append(vulnerability)
return vulnerabilities, db_full
def precompute_remediations(remediations, package_metadata, vulns,
ignored_vulns):
for vuln in vulns:
if vuln.ignored:
ignored_vulns.add(vuln.vulnerability_id)
continue
if vuln.package_name in remediations.keys():
remediations[vuln.package_name]['vulns_found'] = remediations[vuln.package_name].get('vulns_found', 0) + 1
else:
vulns_count = 1
package_metadata[vuln.package_name] = {'insecure_versions': vuln.pkg.insecure_versions,
'secure_versions': vuln.pkg.secure_versions, 'version': vuln.pkg.version}
remediations[vuln.package_name] = {'vulns_found': vulns_count, 'version': vuln.pkg.version,
'more_info_url': vuln.pkg.more_info_url}
def get_closest_ver(versions, version):
results = {'minor': None, 'major': None}
if not version or not versions:
return results
sorted_versions = sorted(versions, key=lambda ver: parse_version(ver), reverse=True)
for v in sorted_versions:
index = parse_version(v)
current_v = parse_version(version)
if index > current_v:
results['major'] = index
if index < current_v:
results['minor'] = index
break
return results
def compute_sec_ver_for_user(package, ignored_vulns, db_full):
pkg_meta = db_full.get('$meta', {}).get('packages', {}).get(package, {})
versions = set(pkg_meta.get("insecure_versions", []) + pkg_meta.get("secure_versions", []))
affected_versions = []
for vuln in db_full.get(package, []):
vuln_id = vuln.get('id', None)
if vuln_id and vuln_id not in ignored_vulns:
affected_versions += vuln.get('affected_versions', [])
affected_v = set(affected_versions)
sec_ver_for_user = list(versions.difference(affected_v))
return sorted(sec_ver_for_user, key=lambda ver: parse_version(ver), reverse=True)
def compute_sec_ver(remediations, package_metadata, ignored_vulns, db_full):
"""
Compute the secure_versions and the closest_secure_version for each remediation using the affected_versions
of each no ignored vulnerability of the same package, there is only a remediation for each package.
"""
for pkg_name in remediations.keys():
pkg = package_metadata.get(pkg_name, {})
if not ignored_vulns:
secure_v = pkg.get('secure_versions', [])
else:
secure_v = compute_sec_ver_for_user(package=pkg_name, ignored_vulns=ignored_vulns, db_full=db_full)
remediations[pkg_name]['secure_versions'] = secure_v
remediations[pkg_name]['closest_secure_version'] = get_closest_ver(secure_v,
pkg.get('version', None))
def calculate_remediations(vulns, db_full):
remediations = {}
package_metadata = {}
ignored_vulns = set()
if not db_full:
return remediations
precompute_remediations(remediations, package_metadata, vulns, ignored_vulns)
compute_sec_ver(remediations, package_metadata, ignored_vulns, db_full)
return remediations
@sync_safety_context
def review(report=None, params=None):
SafetyContext().command = 'review'
vulnerable = []
vulnerabilities = report.get('vulnerabilities', []) + report.get('ignored_vulnerabilities', [])
remediations = {}
for key, value in report.get('remediations', {}).items():
recommended = value.get('recommended_version', None)
secure_v = value.get('other_recommended_versions', [])
major = None
if recommended:
secure_v.append(recommended)
major = parse(recommended)
remediations[key] = {'vulns_found': value.get('vulnerabilities_found', 0),
'version': value.get('current_version'),
'secure_versions': secure_v,
'closest_secure_version': {'major': major, 'minor': None},
# minor isn't supported in review
'more_info_url': value.get('more_info_url')}
packages = report.get('scanned_packages', [])
pkgs = {pkg_name: Package(**pkg_values) for pkg_name, pkg_values in packages.items()}
ctx = SafetyContext()
found_packages = list(pkgs.values())
ctx.packages = found_packages
ctx.review = report.get('report_meta', [])
ctx.key = ctx.review.get('api_key', False)
cvssv2 = None
cvssv3 = None
for vuln in vulnerabilities:
vuln['pkg'] = pkgs.get(vuln.get('package_name', None))
XVE_ID = vuln.get('CVE', None) # Trying to get first the CVE ID
severity = vuln.get('severity', None)
if severity and severity.get('source', False):
cvssv2 = severity.get('cvssv2', None)
cvssv3 = severity.get('cvssv3', None)
# Trying to get the PVE ID if it exists, otherwise it will be the same CVE ID of above
XVE_ID = severity.get('source', False)
vuln['severity'] = Severity(source=XVE_ID, cvssv2=cvssv2, cvssv3=cvssv3)
else:
vuln['severity'] = None
ignored_expires = vuln.get('ignored_expires', None)
if ignored_expires:
vuln['ignored_expires'] = validate_expiration_date(ignored_expires)
vuln['CVE'] = CVE(name=XVE_ID, cvssv2=cvssv2, cvssv3=cvssv3) if XVE_ID else None
vulnerable.append(Vulnerability(**vuln))
return vulnerable, remediations, found_packages
@sync_safety_context
def get_licenses(key=False, db_mirror=False, cached=0, proxy=None, telemetry=True):
key = key if key else os.environ.get("SAFETY_API_KEY", False)
if not key and not db_mirror:
raise InvalidKeyError(message="The API-KEY was not provided.")
if db_mirror:
mirrors = [db_mirror]
else:
mirrors = API_MIRRORS
db_name = "licenses.json"
for mirror in mirrors:
# mirror can either be a local path or a URL
if is_a_remote_mirror(mirror):
licenses = fetch_database_url(mirror, db_name=db_name, key=key, cached=cached, proxy=proxy,
telemetry=telemetry)
else:
licenses = fetch_database_file(mirror, db_name=db_name)
if licenses:
return licenses
raise DatabaseFetchError()
def get_announcements(key, proxy, telemetry=True):
LOG.info('Getting announcements')
announcements = []
headers = {}
if key:
headers["X-Api-Key"] = key
url = f"{API_BASE_URL}announcements/"
method = 'post'
data = build_telemetry_data(telemetry=telemetry)
request_kwargs = {'headers': headers, 'proxies': proxy, 'timeout': 3}
data_keyword = 'json'
source = os.environ.get('SAFETY_ANNOUNCEMENTS_URL', None)
if source:
LOG.debug(f'Getting the announcement from a different source: {source}')
url = source
method = 'get'
data = {
'telemetry': json.dumps(data)}
data_keyword = 'params'
request_kwargs[data_keyword] = data
request_kwargs['url'] = url
LOG.debug(f'Telemetry data sent: {data}')
try:
request_func = getattr(session, method)
r = request_func(**request_kwargs)
LOG.debug(r.text)
except Exception as e:
LOG.info('Unexpected but HANDLED Exception happened getting the announcements: %s', e)
return announcements
if r.status_code == 200:
try:
announcements = r.json()
if 'announcements' in announcements.keys():
announcements = announcements.get('announcements', [])
else:
LOG.info('There is not announcements key in the JSON response, is this a wrong structure?')
announcements = []
except json.JSONDecodeError as e:
LOG.info('Unexpected but HANDLED Exception happened decoding the announcement response: %s', e)
LOG.info('Announcements fetched')
return announcements
def get_packages(files=False, stdin=False) -> List[Package]:
if files:
return list(itertools.chain.from_iterable(read_requirements(f, resolve=True) for f in files))
if stdin:
return list(read_requirements(sys.stdin))
return [
Package(
name=dist.metadata["Name"],
version=str(dist.version),
found=str(dist.locate_file("")),
insecure_versions=[],
secure_versions=[],
latest_version=None,
latest_version_without_known_vulnerabilities=None,
more_info_url=None,
)
for dist in importlib_metadata.distributions()
if dist.metadata["Name"] not in {"python", "wsgiref", "argparse"}
]
def read_vulnerabilities(fh):
try:
data = json.load(fh)
except json.JSONDecodeError as e:
raise MalformedDatabase(reason=e, fetched_from=fh.name)
except TypeError as e:
raise MalformedDatabase(reason=e, fetched_from=fh.name)
return data
def close_session():
LOG.debug('Closing requests session.')
session.close()