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

694 lines
26 KiB
Python

import json
import logging
import os
import textwrap
from datetime import datetime
import pipenv.vendor.click as click
from pipenv.patched.safety.constants import RED, YELLOW
from pipenv.patched.safety.util import get_safety_version, Package, get_terminal_size, \
SafetyContext, build_telemetry_data, build_git_data, is_a_remote_mirror
LOG = logging.getLogger(__name__)
def build_announcements_section_content(announcements, columns=get_terminal_size().columns,
start_line_decorator=' ', end_line_decorator=' '):
section = ''
for i, announcement in enumerate(announcements):
color = ''
if announcement.get('type') == 'error':
color = RED
elif announcement.get('type') == 'warning':
color = YELLOW
item = '{message}'.format(
message=format_long_text('* ' + announcement.get('message'), color, columns,
start_line_decorator, end_line_decorator))
section += '{item}'.format(item=item)
if i + 1 < len(announcements):
section += '\n'
return section
def add_empty_line():
return format_long_text('')
def style_lines(lines, columns, pre_processed_text='', start_line=' ' * 4, end_line=' ' * 4):
styled_text = pre_processed_text
for line in lines:
styled_line = ''
left_padding = ' ' * line.get('left_padding', 0)
for i, word in enumerate(line.get('words', [])):
if word.get('style', {}):
text = ''
if i == 0:
text = left_padding # Include the line padding in the word to avoid Github issues
left_padding = '' # Clean left padding to avoid be added two times
text += word.get('value', '')
styled_line += click.style(text=text, **word.get('style', {}))
else:
styled_line += word.get('value', '')
styled_text += format_long_text(styled_line, columns=columns, start_line_decorator=start_line,
end_line_decorator=end_line,
left_padding=left_padding, **line.get('format', {})) + '\n'
return styled_text
def format_vulnerability(vulnerability, full_mode, only_text=False, columns=get_terminal_size().columns):
common_format = {'left_padding': 3, 'format': {'sub_indent': ' ' * 3, 'max_lines': None}}
styled_vulnerability = [
{'words': [{'style': {'bold': True}, 'value': 'Vulnerability ID: '}, {'value': vulnerability.vulnerability_id}]},
]
vulnerability_spec = [
{'words': [{'style': {'bold': True}, 'value': 'Affected spec: '}, {'value': vulnerability.vulnerable_spec}]}]
cve = vulnerability.CVE
cvssv2_line = None
cve_lines = []
if cve:
if full_mode and cve.cvssv2:
b = cve.cvssv2.get("base_score", "-")
s = cve.cvssv2.get("impact_score", "-")
v = cve.cvssv2.get("vector_string", "-")
# Reset sub_indent as the left_margin is going to be applied in this case
cvssv2_line = {'format': {'sub_indent': ''}, 'words': [
{'value': f'CVSS v2, BASE SCORE {b}, IMPACT SCORE {s}, VECTOR STRING {v}'},
]}
if cve.cvssv3 and "base_severity" in cve.cvssv3.keys():
cvss_base_severity_style = {'bold': True}
base_severity = cve.cvssv3.get("base_severity", "-")
if base_severity.upper() in ['HIGH', 'CRITICAL']:
cvss_base_severity_style['fg'] = 'red'
b = cve.cvssv3.get("base_score", "-")
if full_mode:
s = cve.cvssv3.get("impact_score", "-")
v = cve.cvssv3.get("vector_string", "-")
cvssv3_text = f'CVSS v3, BASE SCORE {b}, IMPACT SCORE {s}, VECTOR STRING {v}'
else:
cvssv3_text = f'CVSS v3, BASE SCORE {b} '
cve_lines = [
{'words': [{'style': {'bold': True}, 'value': '{0} is '.format(cve.name)},
{'style': cvss_base_severity_style,
'value': f'{base_severity} SEVERITY => '},
{'value': cvssv3_text},
]},
]
if cvssv2_line:
cve_lines.append(cvssv2_line)
elif cve.name:
cve_lines = [
{'words': [{'style': {'bold': True}, 'value': cve.name}]}
]
advisory_format = {'sub_indent': ' ' * 3, 'max_lines': None} if full_mode else {'sub_indent': ' ' * 3,
'max_lines': 2}
basic_vuln_data_lines = [
{'format': advisory_format, 'words': [
{'style': {'bold': True}, 'value': 'ADVISORY: '},
{'value': vulnerability.advisory.replace('\n', '')}]}
]
if SafetyContext().key:
fixed_version_line = {'words': [
{'style': {'bold': True}, 'value': 'Fixed versions: '},
{'value': ', '.join(vulnerability.fixed_versions) if vulnerability.fixed_versions else 'No known fix'}
]}
basic_vuln_data_lines.append(fixed_version_line)
more_info_line = [{'words': [{'style': {'bold': True}, 'value': 'For more information, please visit '},
{'value': click.style(vulnerability.more_info_url)}]}]
vuln_title = f'-> Vulnerability found in {vulnerability.package_name} version {vulnerability.analyzed_version}\n'
styled_text = click.style(vuln_title, fg='red')
to_print = styled_vulnerability
if not vulnerability.ignored:
to_print += vulnerability_spec + basic_vuln_data_lines + cve_lines
else:
generic_reason = 'This vulnerability is being ignored'
if vulnerability.ignored_expires:
generic_reason += f" until {vulnerability.ignored_expires.strftime('%Y-%m-%d %H:%M:%S UTC')}. " \
f"See your configurations"
specific_reason = None
if vulnerability.ignored_reason:
specific_reason = [
{'words': [{'style': {'bold': True}, 'value': 'Reason: '}, {'value': vulnerability.ignored_reason}]}]
expire_section = [{'words': [
{'style': {'bold': True, 'fg': 'green'}, 'value': f'{generic_reason}.'}, ]}]
if specific_reason:
expire_section += specific_reason
to_print += expire_section
if cve:
to_print += more_info_line
to_print = [{**common_format, **line} for line in to_print]
content = style_lines(to_print, columns, styled_text, start_line='', end_line='', )
return click.unstyle(content) if only_text else content
def format_license(license, only_text=False, columns=get_terminal_size().columns):
to_print = [
{'words': [{'style': {'bold': True}, 'value': license['package']},
{'value': ' version {0} found using license '.format(license['version'])},
{'style': {'bold': True}, 'value': license['license']}
]
},
]
content = style_lines(to_print, columns, '-> ', start_line='', end_line='')
return click.unstyle(content) if only_text else content
def build_remediation_section(remediations, only_text=False, columns=get_terminal_size().columns, kwargs=None):
columns -= 2
left_padding = ' ' * 3
if not kwargs:
# Reset default params in the format_long_text func
kwargs = {'left_padding': '', 'columns': columns, 'start_line_decorator': '', 'end_line_decorator': '',
'sub_indent': left_padding}
END_SECTION = '+' + '=' * columns + '+'
if not remediations:
return []
content = ''
total_vulns = 0
total_packages = len(remediations.keys())
for pkg in remediations.keys():
total_vulns += remediations[pkg]['vulns_found']
upgrade_to = remediations[pkg]['closest_secure_version']['major']
downgrade_to = remediations[pkg]['closest_secure_version']['minor']
fix_version = None
if upgrade_to:
fix_version = str(upgrade_to)
elif downgrade_to:
fix_version = str(downgrade_to)
new_line = '\n'
other_options = [str(fix) for fix in remediations[pkg].get('secure_versions', []) if str(fix) != fix_version]
raw_recommendation = f"We recommend upgrading to version {upgrade_to} of {pkg}."
if other_options:
raw_other_options = ', '.join(other_options)
raw_pre_other_options = 'Other versions without known vulnerabilities are:'
if len(other_options) == 1:
raw_pre_other_options = 'Other version without known vulnerabilities is'
raw_recommendation = f"{raw_recommendation} {raw_pre_other_options} " \
f"{raw_other_options}"
remediation_content = [
f'{left_padding}The closest version with no known vulnerabilities is ' + click.style(upgrade_to, bold=True),
new_line,
click.style(f'{left_padding}{raw_recommendation}', bold=True, fg='green')
]
if not fix_version:
remediation_content = [new_line,
click.style(f'{left_padding}There is no known fix for this vulnerability.', bold=True, fg='yellow')]
text = 'vulnerabilities' if remediations[pkg]['vulns_found'] > 1 else 'vulnerability'
raw_rem_title = f"-> {pkg} version {remediations[pkg]['version']} was found, " \
f"which has {remediations[pkg]['vulns_found']} {text}"
remediation_title = click.style(raw_rem_title, fg=RED, bold=True)
content += new_line + format_long_text(remediation_title, **kwargs) + new_line
pre_content = remediation_content + [
f"{left_padding}For more information, please visit {remediations[pkg]['more_info_url']}",
f'{left_padding}Always check for breaking changes when upgrading packages.',
new_line]
for i, element in enumerate(pre_content):
content += format_long_text(element, **kwargs)
if i + 1 < len(pre_content):
content += '\n'
title = format_long_text(click.style(f'{left_padding}REMEDIATIONS', fg='green', bold=True), **kwargs)
body = [content]
if not is_using_api_key():
vuln_text = 'vulnerabilities were' if total_vulns != 1 else 'vulnerability was'
pkg_text = 'packages' if total_packages > 1 else 'package'
msg = "{0} {1} found in {2} {3}. " \
"For detailed remediation & fix recommendations, upgrade to a commercial license."\
.format(total_vulns, vuln_text, total_packages, pkg_text)
content = '\n' + format_long_text(msg, left_padding=' ', columns=columns) + '\n'
body = [content]
body.append(END_SECTION)
content = [title] + body
if only_text:
content = [click.unstyle(item) for item in content]
return content
def get_final_brief(total_vulns_found, total_remediations, ignored, total_ignored, kwargs=None):
if not kwargs:
kwargs = {}
total_vulns = max(0, total_vulns_found - total_ignored)
vuln_text = 'vulnerabilities' if total_ignored > 1 else 'vulnerability'
pkg_text = 'packages were' if len(ignored.keys()) > 1 else 'package was'
policy_file_text = ' using a safety policy file' if is_using_a_safety_policy_file() else ''
vuln_brief = f" {total_vulns} vulnerabilit{'y was' if total_vulns == 1 else 'ies were'} found."
ignored_text = f' {total_ignored} {vuln_text} from {len(ignored.keys())} {pkg_text} ignored.' if ignored else ''
remediation_text = f" {total_remediations} remediation{' was' if total_remediations == 1 else 's were'} " \
f"recommended." if is_using_api_key() else ''
raw_brief = f"Scan was completed{policy_file_text}.{vuln_brief}{ignored_text}{remediation_text}"
return format_long_text(raw_brief, start_line_decorator=' ', **kwargs)
def get_final_brief_license(licenses, kwargs=None):
if not kwargs:
kwargs = {}
licenses_text = ' Scan was completed.'
if licenses:
licenses_text = 'The following software licenses were present in your system: {0}'.format(', '.join(licenses))
return format_long_text("{0}".format(licenses_text), start_line_decorator=' ', **kwargs)
def format_long_text(text, color='', columns=get_terminal_size().columns, start_line_decorator=' ', end_line_decorator=' ', left_padding='', max_lines=None, styling=None, indent='', sub_indent=''):
if not styling:
styling = {}
if color:
styling.update({'fg': color})
columns -= len(start_line_decorator) + len(end_line_decorator)
formatted_lines = []
lines = text.replace('\r', '').splitlines()
for line in lines:
base_format = "{:" + str(columns) + "}"
if line == '':
empty_line = base_format.format(" ")
formatted_lines.append("{0}{1}{2}".format(start_line_decorator, empty_line, end_line_decorator))
wrapped_lines = textwrap.wrap(line, width=columns, max_lines=max_lines, initial_indent=indent, subsequent_indent=sub_indent, placeholder='...')
for wrapped_line in wrapped_lines:
try:
new_line = left_padding + wrapped_line.encode('utf-8')
except TypeError:
new_line = left_padding + wrapped_line
if styling:
new_line = click.style(new_line, **styling)
formatted_lines.append(f"{start_line_decorator}{new_line}{end_line_decorator}")
return "\n".join(formatted_lines)
def get_printable_list_of_scanned_items(scanning_target):
context = SafetyContext()
result = []
scanned_items_data = []
if scanning_target == 'environment':
locations = set([pkg.found for pkg in context.packages if isinstance(pkg, Package)])
for path in locations:
result.append([{'styled': False, 'value': '-> ' + path}])
scanned_items_data.append(path)
if len(locations) <= 0:
msg = 'No locations found in the environment'
result.append([{'styled': False, 'value': msg}])
scanned_items_data.append(msg)
elif scanning_target == 'stdin':
scanned_stdin = [pkg.name for pkg in context.packages if isinstance(pkg, Package)]
value = 'No found packages in stdin'
scanned_items_data = [value]
if len(scanned_stdin) > 0:
value = ', '.join(scanned_stdin)
scanned_items_data = scanned_stdin
result.append(
[{'styled': False, 'value': value}])
elif scanning_target == 'files':
for file in context.params.get('files', []):
result.append([{'styled': False, 'value': f'-> {file.name}'}])
scanned_items_data.append(file.name)
elif scanning_target == 'file':
file = context.params.get('file', None)
name = file.name if file else ''
result.append([{'styled': False, 'value': f'-> {name}'}])
scanned_items_data.append(name)
return result, scanned_items_data
REPORT_HEADING = format_long_text(click.style('REPORT', bold=True))
def build_report_brief_section(columns=None, primary_announcement=None, report_type=1, **kwargs):
if not columns:
columns = get_terminal_size().columns
styled_brief_lines = []
if primary_announcement:
styled_brief_lines.append(
build_primary_announcement(columns=columns, primary_announcement=primary_announcement))
for line in get_report_brief_info(report_type=report_type, **kwargs):
ln = ''
padding = ' ' * 2
for i, words in enumerate(line):
processed_words = words.get('value', '')
if words.get('style', False):
text = ''
if i == 0:
text = padding
padding = ''
text += processed_words
processed_words = click.style(text, bold=True)
ln += processed_words
styled_brief_lines.append(format_long_text(ln, color='', columns=columns, start_line_decorator='',
left_padding=padding, end_line_decorator='', sub_indent=' ' * 2))
return "\n".join([add_empty_line(), REPORT_HEADING, add_empty_line(), '\n'.join(styled_brief_lines)])
def build_report_for_review_vuln_report(as_dict=False):
ctx = SafetyContext()
report_from_file = ctx.review
packages = ctx.packages
if as_dict:
return report_from_file
policy_f_name = report_from_file.get('policy_file', None)
safety_policy_used = []
if policy_f_name:
safety_policy_used = [
{'style': False, 'value': '\nScanning using a security policy file'},
{'style': True, 'value': ' {0}'.format(policy_f_name)},
]
action_executed = [
{'style': True, 'value': 'Scanning dependencies'},
{'style': False, 'value': ' in your '},
{'style': True, 'value': report_from_file.get('scan_target', '-') + ':'},
]
scanned_items = []
for name in report_from_file.get('scanned', []):
scanned_items.append([{'styled': False, 'value': '-> ' + name}])
nl = [{'style': False, 'value': ''}]
using_sentence = build_using_sentence(report_from_file.get('api_key', None),
report_from_file.get('local_database_path_used', None))
scanned_count_sentence = build_scanned_count_sentence(packages)
old_timestamp = report_from_file.get('timestamp', None)
old_timestamp = [{'style': False, 'value': 'Report generated '}, {'style': True, 'value': old_timestamp}]
now = str(datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
current_timestamp = [{'style': False, 'value': 'Timestamp '}, {'style': True, 'value': now}]
brief_info = [[{'style': False, 'value': 'Safety '},
{'style': True, 'value': 'v' + report_from_file.get('safety_version', '-')},
{'style': False, 'value': ' is scanning for '},
{'style': True, 'value': 'Vulnerabilities'},
{'style': True, 'value': '...'}] + safety_policy_used, action_executed
] + [nl] + scanned_items + [nl] + [using_sentence] + [scanned_count_sentence] + [old_timestamp] + \
[current_timestamp]
return brief_info
def build_using_sentence(key, db):
key_sentence = []
custom_integration = os.environ.get('SAFETY_CUSTOM_INTEGRATION',
'false').lower() == 'true'
if key:
key_sentence = [{'style': True, 'value': 'an API KEY'},
{'style': False, 'value': ' and the '}]
db_name = 'PyUp Commercial'
elif db:
if is_a_remote_mirror(db):
if custom_integration:
return []
db_name = f"remote URL {db}"
else:
db_name = f"local file {db}"
else:
db_name = 'non-commercial'
database_sentence = [{'style': True, 'value': db_name + ' database'}]
return [{'style': False, 'value': 'Using '}] + key_sentence + database_sentence
def build_scanned_count_sentence(packages):
scanned_count = 'No packages found'
if len(packages) >= 1:
scanned_count = 'Found and scanned {0} {1}'.format(len(packages),
'packages' if len(packages) > 1 else 'package')
return [{'style': True, 'value': scanned_count}]
def add_warnings_if_needed(brief_info):
ctx = SafetyContext()
warnings = []
if ctx.packages:
if ctx.params.get('continue_on_error', False):
warnings += [[{'style': True,
'value': '* Continue-on-error is enabled, so returning successful (0) exit code in all cases.'}]]
if ctx.params.get('ignore_severity_rules', False) and not is_using_api_key():
warnings += [[{'style': True,
'value': '* Could not filter by severity, please upgrade your account to include severity data.'}]]
if warnings:
brief_info += [[{'style': False, 'value': ''}]] + warnings
def get_report_brief_info(as_dict=False, report_type=1, **kwargs):
LOG.info('get_report_brief_info: %s, %s, %s', as_dict, report_type, kwargs)
context = SafetyContext()
packages = [pkg for pkg in context.packages if isinstance(pkg, Package)]
brief_data = {}
command = context.command
if command == 'review':
review = build_report_for_review_vuln_report(as_dict)
return review
key = context.key
db = context.db_mirror
scanning_types = {'check': {'name': 'Vulnerabilities', 'action': 'Scanning dependencies', 'scanning_target': 'environment'}, # Files, Env or Stdin
'license': {'name': 'Licenses', 'action': 'Scanning licenses', 'scanning_target': 'environment'}, # Files or Env
'review': {'name': 'Report', 'action': 'Reading the report',
'scanning_target': 'file'}} # From file
targets = ['stdin', 'environment', 'files', 'file']
for target in targets:
if context.params.get(target, False):
scanning_types[command]['scanning_target'] = target
break
scanning_target = scanning_types.get(context.command, {}).get('scanning_target', '')
brief_data['scan_target'] = scanning_target
scanned_items, data = get_printable_list_of_scanned_items(scanning_target)
brief_data['scanned'] = data
nl = [{'style': False, 'value': ''}]
action_executed = [
{'style': True, 'value': scanning_types.get(context.command, {}).get('action', '')},
{'style': False, 'value': ' in your '},
{'style': True, 'value': scanning_target + ':'},
]
policy_file = context.params.get('policy_file', None)
safety_policy_used = []
brief_data['policy_file'] = policy_file.get('filename', '-') if policy_file else None
brief_data['policy_file_source'] = 'server' if brief_data['policy_file'] and 'server-safety-policy' in brief_data['policy_file'] else 'local'
if policy_file and policy_file.get('filename', False):
safety_policy_used = [
{'style': False, 'value': '\nScanning using a security policy file'},
{'style': True, 'value': ' {0}'.format(policy_file.get('filename', '-'))},
]
audit_and_monitor = []
if context.params.get('audit_and_monitor'):
logged_url = context.params.get('audit_and_monitor_url') if context.params.get('audit_and_monitor_url') else "https://pyup.io"
audit_and_monitor = [
{'style': False, 'value': '\nLogging scan results to'},
{'style': True, 'value': ' {0}'.format(logged_url)},
]
current_time = str(datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
brief_data['api_key'] = bool(key)
brief_data['local_database_path'] = db if db else None
brief_data['safety_version'] = get_safety_version()
brief_data['timestamp'] = current_time
brief_data['packages_found'] = len(packages)
# Vuln report
additional_data = []
if report_type == 1:
brief_data['vulnerabilities_found'] = kwargs.get('vulnerabilities_found', 0)
brief_data['vulnerabilities_ignored'] = kwargs.get('vulnerabilities_ignored', 0)
brief_data['remediations_recommended'] = 0
additional_data = [
[{'style': True, 'value': str(brief_data['vulnerabilities_found'])},
{'style': True, 'value': f' vulnerabilit{"y" if brief_data["vulnerabilities_found"] == 1 else "ies"} found'}],
[{'style': True, 'value': str(brief_data['vulnerabilities_ignored'])},
{'style': True, 'value': f' vulnerabilit{"y" if brief_data["vulnerabilities_ignored"] == 1 else "ies"} ignored'}],
]
if is_using_api_key():
brief_data['remediations_recommended'] = kwargs.get('remediations_recommended', 0)
additional_data.extend(
[[{'style': True, 'value': str(brief_data['remediations_recommended'])},
{'style': True, 'value':
f' remediation{"" if brief_data["remediations_recommended"] == 1 else "s"} recommended'}]])
elif report_type == 2:
brief_data['licenses_found'] = kwargs.get('licenses_found', 0)
additional_data = [
[{'style': True, 'value': str(brief_data['licenses_found'])},
{'style': True, 'value': f' license {"type" if brief_data["licenses_found"] == 1 else "types"} found'}],
]
brief_data['telemetry'] = build_telemetry_data()
brief_data['git'] = build_git_data()
brief_data['project'] = context.params.get('project', None)
brief_data['json_version'] = 1
using_sentence = build_using_sentence(key, db)
using_sentence_section = [nl] if not using_sentence else [nl] + [build_using_sentence(key, db)]
scanned_count_sentence = build_scanned_count_sentence(packages)
timestamp = [{'style': False, 'value': 'Timestamp '}, {'style': True, 'value': current_time}]
brief_info = [[{'style': False, 'value': 'Safety '},
{'style': True, 'value': 'v' + get_safety_version()},
{'style': False, 'value': ' is scanning for '},
{'style': True, 'value': scanning_types.get(context.command, {}).get('name', '')},
{'style': True, 'value': '...'}] + safety_policy_used + audit_and_monitor, action_executed
] + [nl] + scanned_items + using_sentence_section + [scanned_count_sentence] + [timestamp]
brief_info.extend(additional_data)
add_warnings_if_needed(brief_info)
LOG.info('Brief info data: %s', brief_data)
LOG.info('Brief info, styled output: %s', '\n\n LINE ---->\n ' + '\n\n LINE ---->\n '.join(map(str, brief_info)))
return brief_data if as_dict else brief_info
def build_primary_announcement(primary_announcement, columns=None, only_text=False):
lines = json.loads(primary_announcement.get('message'))
for line in lines:
if 'words' not in line:
raise ValueError('Missing words keyword')
if len(line['words']) <= 0:
raise ValueError('No words in this line')
for word in line['words']:
if 'value' not in word or not word['value']:
raise ValueError('Empty word or without value')
message = style_lines(lines, columns, start_line='', end_line='')
return click.unstyle(message) if only_text else message
def is_using_api_key():
return bool(SafetyContext().key)
def is_using_a_safety_policy_file():
return bool(SafetyContext().params.get('policy_file', None))
def should_add_nl(output, found_vulns):
if output == 'bare' and not found_vulns:
return False
return True