Skip to content

Commit

Permalink
Added human-readable output format for vuln-scan results
Browse files Browse the repository at this point in the history
  • Loading branch information
akenion committed Oct 27, 2023
1 parent 2e35b9f commit 41483b6
Show file tree
Hide file tree
Showing 5 changed files with 92 additions and 12 deletions.
26 changes: 21 additions & 5 deletions wordfence/cli/reporting.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,18 @@ def write_row(self, data: List[str]) -> None:
self._target.write(value + self.delimiter)


class RowlessWriter(ReportWriter):

def allows_headers(self) -> bool:
return False

def write_row(self, data: List[str]) -> None:
pass

def write_record(self, record) -> None:
raise NotImplementedError()


class ReportFormat:

def __init__(
Expand Down Expand Up @@ -176,9 +188,12 @@ def add_target(self, stream: IO) -> None:
writer = self.format.initialize_writer(stream, self.columns)
self.writers.append(writer)

def _write_row(self, data: List[str]):
def _write_row(self, data: List[str], record: ReportRecord):
for writer in self.writers:
writer.write_row(data)
if isinstance(writer, RowlessWriter):
writer.write_record(record)
else:
writer.write_row(data)

def _write_headers(self) -> None:
if self.headers_written or not self.write_headers:
Expand All @@ -195,7 +210,7 @@ def _format_record(self, record: ReportRecord) -> List[str]:
return row

def _write_record(self, record: ReportRecord) -> None:
self._write_row(self._format_record(record))
self._write_row(self._format_record(record), record)

def write_records(self, records: Iterable[ReportRecord]) -> None:
self._write_headers()
Expand All @@ -213,7 +228,8 @@ def has_writers(self) -> bool:
def get_config_options(
formats: Type[ReportFormatEnum],
columns: Type[ReportColumnEnum],
default_columns: List[ReportColumnEnum]
default_columns: List[ReportColumnEnum],
default_format: str = 'csv'
) -> Dict[str, Dict[str, Any]]:
return {
"output": {
Expand Down Expand Up @@ -249,7 +265,7 @@ def get_config_options(
"description": "Output format used for result data.",
"context": "ALL",
"argument_type": "OPTION",
"default": 'csv',
"default": default_format,
"meta": {
"valid_options": formats.get_options()
},
Expand Down
53 changes: 50 additions & 3 deletions wordfence/cli/vulnscan/reporting.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from typing import List, Dict, Callable, Any, Optional

from ...intel.vulnerabilities import ScannableSoftware, Vulnerability, Software
from ...intel.vulnerabilities import ScannableSoftware, Vulnerability, \
Software, ProductionVulnerability
from ...api.intelligence import VulnerabilityFeedVariant
from ...util.terminal import Color, escape, RESET
from ..reporting import Report, ReportColumnEnum, ReportFormatEnum, \
ReportRecord, ReportManager, ReportFormat, ReportColumn, \
get_config_options, \
RowlessWriter, get_config_options, \
REPORT_FORMAT_CSV, REPORT_FORMAT_TSV, REPORT_FORMAT_NULL_DELIMITED, \
REPORT_FORMAT_LINE_DELIMITED
from ..config import Config
Expand Down Expand Up @@ -68,11 +70,55 @@ def is_compatible(
variant == self.feed_variant


class HumanReadableWriter(RowlessWriter):

def get_severity_color(self, severity: str) -> str:
if severity == 'none' or severity == 'low':
return escape(color=Color.WHITE, bold=True)
if severity == 'high' or severity == 'critical':
return escape(color=Color.RED, bold=True)
return escape(color=Color.YELLOW, bold=True)

def format_record(self, record) -> str:
vuln = record.vulnerability
sw = record.software
yellow = escape(color=Color.YELLOW)
link = vuln.get_wordfence_link()
blue = escape(color=Color.BLUE)
white = escape(color=Color.WHITE)
severity = None
if isinstance(record.vulnerability, ProductionVulnerability):
if record.vulnerability.cvss is not None:
severity = record.vulnerability.cvss.rating
if severity is None:
severity_message = ''
else:
severity = severity.lower()
severity_color = self.get_severity_color(severity)
severity_message = f'{severity_color}{severity}{yellow} severity '
return (
f'{yellow}Found {severity_message}vulnerability {vuln.title} in '
f'{sw.slug}({sw.version})\n'
f'{white}Details: {blue}{link}{RESET}'
)

def write_record(self, record) -> None:
self._target.write(self.format_record(record))
self._target.write('\n')


REPORT_FORMAT_HUMAN = ReportFormat(
'human',
lambda stream, columns: HumanReadableWriter(stream)
)


class VulnScanReportFormat(ReportFormatEnum):
CSV = REPORT_FORMAT_CSV
TSV = REPORT_FORMAT_TSV
NULL_DELIMITED = REPORT_FORMAT_NULL_DELIMITED
LINE_DELIMITED = REPORT_FORMAT_LINE_DELIMITED
HUMAN = REPORT_FORMAT_HUMAN


class VulnScanReportRecord(ReportRecord):
Expand Down Expand Up @@ -129,7 +175,8 @@ def add_result(
VulnScanReportColumn.SLUG,
VulnScanReportColumn.VERSION,
VulnScanReportColumn.LINK
]
],
'human'
)


Expand Down
6 changes: 6 additions & 0 deletions wordfence/cli/vulnscan/vulnscan.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,12 @@ def _scan_site(
)

def invoke(self) -> int:
if self.config.output_format == 'human' \
and not self.context.allows_color:
log.warning(
'The human output format requires a terminal with color '
'support to function properly.'
)
feed_variant = VulnerabilityFeedVariant.for_path(self.config.feed)
report_manager = VulnScanReportManager(self.config, feed_variant)
io_manager = report_manager.get_io_manager()
Expand Down
4 changes: 2 additions & 2 deletions wordfence/logging/formatting.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import logging

from ..util.terminal import Color, escape
from ..util.terminal import Color, escape, RESET


class ColoredFormatter(logging.Formatter):
Expand All @@ -17,4 +17,4 @@ def get_style(self, level) -> str:
def format(self, record) -> str:
style = self.get_style(record.levelno)
message = super().format(record)
return f'{style}{message}'
return f'{style}{message}{RESET}'
15 changes: 13 additions & 2 deletions wordfence/util/terminal.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,21 @@ class Color(IntEnum):
MAGENTA = 35
CYAN = 36
WHITE = 37
RESET = 0


ESC = '\x1b'


def escape(color: Color) -> str:
return f'{ESC}[{color.value}m'
def escape(color: Color, bold: bool = False) -> str:
fields = []
if bold:
fields.append('1')
else:
fields.append('22')
fields.append(str(color.value))
sequence = ';'.join(fields)
return f'{ESC}[{sequence}m'


RESET = escape(color=Color.RESET)

0 comments on commit 41483b6

Please sign in to comment.