Skip to content

Commit 39b9d31

Browse files
authored
Merge pull request #3024 from blacklanternsecurity/finding-severity-status-line
Show FINDING severity breakdown in scan status line
2 parents 0c4063b + c3e4c07 commit 39b9d31

3 files changed

Lines changed: 62 additions & 4 deletions

File tree

bbot/core/event/base.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@ class BaseEvent:
126126
_suppress_chain_dupes = False
127127
# Shared compiled regex for discovery context formatting (class-level to avoid per-instance overhead)
128128
_discovery_context_regex = re.compile(r"\{(?:event|module)[^}]*\}")
129+
# Stats class for the status line — override in subclasses for custom formatting
130+
_stats_class = None
129131

130132
# using __slots__ dramatically reduces memory usage in large scans
131133
__slots__ = [
@@ -1655,6 +1657,35 @@ def redirect_location(self):
16551657
class FINDING(ClosestHostEvent):
16561658
_always_emit = True
16571659
_quick_emit = True
1660+
1661+
class _stats_class:
1662+
_severity_order = ["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"]
1663+
1664+
def __init__(self):
1665+
self.count = 0
1666+
self.severities = {}
1667+
1668+
def increment(self, event):
1669+
self.count += 1
1670+
sev = event.data.get("severity", "UNKNOWN")
1671+
try:
1672+
self.severities[sev] += 1
1673+
except KeyError:
1674+
self.severities[sev] = 1
1675+
1676+
def format(self, event_type):
1677+
if not self.severities:
1678+
return f"{event_type}: {self.count}"
1679+
parts = []
1680+
for sev in self._severity_order:
1681+
n = self.severities.get(sev, 0)
1682+
if n:
1683+
parts.append(f"{n} {sev}")
1684+
for sev, n in sorted(self.severities.items()):
1685+
if sev not in self._severity_order and n:
1686+
parts.append(f"{n} {sev}")
1687+
return f"{event_type}: {self.count} ({', '.join(parts)})"
1688+
16581689
severity_colors = {
16591690
"CRITICAL": "🟪",
16601691
"HIGH": "🟥",

bbot/scanner/scanner.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -745,11 +745,9 @@ def modules_status(self, _log=False, detailed=False):
745745
self.info(f"{self.name}: Modules running (incoming:processing:outgoing) {modules_status_str}")
746746
else:
747747
self.info(f"{self.name}: No modules running")
748-
event_type_summary = sorted(self.stats.events_emitted_by_type.items(), key=lambda x: x[-1], reverse=True)
748+
event_type_summary = self.stats.event_type_summary()
749749
if event_type_summary:
750-
self.info(
751-
f"{self.name}: Events produced so far: {', '.join([f'{k}: {v}' for k, v in event_type_summary])}"
752-
)
750+
self.info(f"{self.name}: Events produced so far: {', '.join(event_type_summary)}")
753751
else:
754752
self.info(f"{self.name}: No events produced yet")
755753

bbot/scanner/stats.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,19 @@ def _increment(d, k):
1616
d[k] = 1
1717

1818

19+
class EventTypeStats:
20+
"""Tracks count for an event type and formats it for the status line."""
21+
22+
def __init__(self):
23+
self.count = 0
24+
25+
def increment(self, event):
26+
self.count += 1
27+
28+
def format(self, event_type):
29+
return f"{event_type}: {self.count}"
30+
31+
1932
class SpeedCounter:
2033
"""
2134
A simple class for keeping a rolling tally of the number of events inside a specific time window
@@ -45,8 +58,23 @@ def __init__(self, scan):
4558
self.scan = scan
4659
self.module_stats = {}
4760
self.events_emitted_by_type = {}
61+
self._type_stats = {}
4862
self.speedometer = SpeedCounter(scan.status_frequency)
4963

64+
def _get_type_stats(self, event):
65+
event_type = event.type
66+
try:
67+
return self._type_stats[event_type]
68+
except KeyError:
69+
stats_class = getattr(event, "_stats_class", None) or EventTypeStats
70+
self._type_stats[event_type] = stats_class()
71+
return self._type_stats[event_type]
72+
73+
def event_type_summary(self):
74+
"""Return a formatted list of event type counts, sorted by count descending."""
75+
entries = sorted(self._type_stats.items(), key=lambda x: x[1].count, reverse=True)
76+
return [stats.format(event_type) for event_type, stats in entries if stats.count > 0]
77+
5078
def _get_attribution_module(self, event):
5179
"""Return the module that should get credit for producing this event.
5280
@@ -65,6 +93,7 @@ def _get_attribution_module(self, event):
6593

6694
def event_produced(self, event):
6795
_increment(self.events_emitted_by_type, event.type)
96+
self._get_type_stats(event).increment(event)
6897
module = self._get_attribution_module(event)
6998
module_stat = self.get(module)
7099
if module_stat is not None:

0 commit comments

Comments
 (0)