Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 14 additions & 7 deletions bbot/cli.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/env python3

import io
import os
import sys
import logging
import multiprocessing
Expand All @@ -11,16 +12,22 @@


if multiprocessing.current_process().name == "MainProcess":
# the --no-color flag is parsed later, so honor it (and the NO_COLOR env) before any color is printed
no_color = "--no-color" in sys.argv or bool(os.environ.get("NO_COLOR", ""))
if no_color:
os.environ["NO_COLOR"] = "1"
silent = "-s" in sys.argv or "--silent" in sys.argv

if not silent:
ascii_art = rf"""  ______  _____ ____ _______
| ___ \| __ \ / __ \__ __|
| |___) | |__) | | | | | |
| ___ <| __ <| | | | | |
| |___) | |__) | |__| | | |
|______/|_____/ \____/ |_|
BIGHUGE BLS OSINT TOOL {__version__}
o = "" if no_color else "\033[1;38;5;208m"
e = "" if no_color else "\033[0m"
ascii_art = rf""" {o} ______ {e} _____ ____ _______
{o}| ___ \{e}| __ \ / __ \__ __|
{o}| |___) {e}| |__) | | | | | |
{o}| ___ <{e}| __ <| | | | | |
{o}| |___) {e}| |__) | |__| | | |
{o}|______/{e}|_____/ \____/ |_|
{o}BIGHUGE{e} BLS OSINT TOOL {__version__}

www.blacklanternsecurity.com/bbot
"""
Expand Down
40 changes: 33 additions & 7 deletions bbot/core/event/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1853,22 +1853,48 @@ def format(self, event_type):
parts.append(f"{n} {sev}")
return f"{event_type}: {self.count} ({', '.join(parts)})"

severity_colors = {
"CRITICAL": "🟪",
"HIGH": "🟥",
"MEDIUM": "🟧",
"LOW": "🟨",
"INFO": "⬜",
# Standard finding color palette. Single source of truth for every output module.
# Severity selects the hue (blue, yellow, orange, red, purple); confidence dims it.
severity_colors_rgb = {
"INFO": (113, 161, 255),
"LOW": (255, 215, 0),
"MEDIUM": (255, 135, 0),
"HIGH": (255, 0, 0),
"CRITICAL": (207, 0, 255),
}
confidence_brightness = {
"CONFIRMED": 1.00,
"HIGH": 0.88,
"MEDIUM": 0.77,
"LOW": 0.66,
"UNKNOWN": 0.55,
}

confidence_colors = {
# emoji rendering of the same palette, for chat-based output (Slack, Discord, Teams)
severity_colors_emoji = {
"INFO": "🟦",
"LOW": "🟨",
"MEDIUM": "🟧",
"HIGH": "🟥",
"CRITICAL": "🟪",
}
confidence_colors_emoji = {
"CONFIRMED": "🟣",
"HIGH": "🔴",
"MEDIUM": "🟠",
"LOW": "🟡",
"UNKNOWN": "⚪",
}

# Adaptive Card style names for Microsoft Teams output
severity_card_colors = {
"CRITICAL": "Attention",
"HIGH": "Attention",
"MEDIUM": "Warning",
"LOW": "Good",
"INFO": "Accent",
}

def sanitize_data(self, data):
data = super().sanitize_data(data)
self.add_tag(f"severity-{data['severity'].lower()}")
Expand Down
3 changes: 3 additions & 0 deletions bbot/logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@


def colorize(s, level="INFO"):
# honor the NO_COLOR convention (https://no-color.org)
if os.environ.get("NO_COLOR", ""):
return s
seq = color_mapping.get(level, 15) # default white
colored = f"{color_prefix}{seq}m{s}{color_suffix}"
return colored
Expand Down
32 changes: 16 additions & 16 deletions bbot/modules/output/stdout.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import os
import sys
import json

from bbot.logger import log_to_stderr
from bbot.modules.output.base import BaseOutputModule


Expand All @@ -15,13 +16,6 @@ class Stdout(BaseOutputModule):
"in_scope_only": "Whether to only show in-scope events",
"accept_dupes": "Whether to show duplicate events, default True",
}
vuln_severity_map = {
"INFO": "HUGEINFO",
"LOW": "HUGEWARNING",
"MEDIUM": "HUGEWARNING",
"HIGH": "CRITICAL",
"CRITICAL": "CRITICAL",
}
format_choices = ["text", "json"]

async def setup(self):
Expand All @@ -35,6 +29,8 @@ async def setup(self):
self.show_event_fields = [str(s) for s in self.config.get("event_fields", [])]
self.in_scope_only = self.config.get("in_scope_only", False)
self.accept_dupes = self.config.get("accept_dupes", False)
# honor the NO_COLOR convention (https://no-color.org) and skip color when not writing to a terminal
self.use_color = sys.stdout.isatty() and not os.environ.get("NO_COLOR", "")
return True

async def filter_event(self, event):
Expand All @@ -61,16 +57,20 @@ async def handle_text(self, event, event_json):
else:
event_str = self.human_event_str(event)

# log findings in vivid colors based on severity
if event.type == "FINDING":
severity = event.data.get("severity", "INFO")
if severity in self.vuln_severity_map:
loglevel = self.vuln_severity_map[severity]
log_to_stderr(event_str, level=loglevel, logname=False)
else:
log_to_stderr(event_str, level="HUGEINFO", logname=False)
# color findings: severity picks the hue, confidence dims the brightness
if event.type == "FINDING" and isinstance(event.data, dict) and self.use_color:
event_str = self._colorize_finding(event_str, event)

print(event_str)

def _colorize_finding(self, event_str, event):
severity = str(event.data.get("severity", "INFO")).upper()
confidence = str(event.data.get("confidence", "UNKNOWN")).upper()
r, g, b = event.severity_colors_rgb.get(severity, event.severity_colors_rgb["INFO"])
mult = event.confidence_brightness.get(confidence, event.confidence_brightness["UNKNOWN"])
r, g, b = int(r * mult), int(g * mult), int(b * mult)
bold = "1;" if confidence == "CONFIRMED" else ""
return f"\033[{bold}38;2;{r};{g};{b}m{event_str}\033[0m"

async def handle_json(self, event, event_json):
print(json.dumps(event_json))
12 changes: 2 additions & 10 deletions bbot/modules/output/teams.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,18 +45,10 @@ def format_message_other(self, event):
return items

def get_severity_color(self, event):
color = "Accent"
if event.type == "FINDING":
severity = event.data.get("severity", "INFO")
if severity == "CRITICAL":
color = "Attention"
elif severity == "HIGH":
color = "Attention"
elif severity == "MEDIUM":
color = "Warning"
elif severity == "LOW":
color = "Good"
return color
return event.severity_card_colors.get(severity, "Accent")
return "Accent"

def format_message(self, event):
adaptive_card = {
Expand Down
4 changes: 2 additions & 2 deletions bbot/modules/templates/webhook.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@ def get_colors(self, event):
if event.type == "FINDING":
severity = event.data.get("severity", "INFO")
confidence = event.data.get("confidence", "UNKNOWN")
severity_color = event.severity_colors.get(severity, "⬜")
confidence_color = event.confidence_colors.get(confidence, "⚪")
severity_color = event.severity_colors_emoji.get(severity, "⬜")
confidence_color = event.confidence_colors_emoji.get(confidence, "⚪")
return f"{event.type} (Severity: {severity} / Confidence: {confidence})", severity_color, confidence_color
else:
return event.type, "🟦", ""
Expand Down
4 changes: 4 additions & 0 deletions bbot/scanner/preset/args.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import os
import re
import logging
import argparse
Expand Down Expand Up @@ -150,6 +151,8 @@ def preset_from_args(self):
)
if self.parsed.event_types:
args_preset.core.merge_custom({"modules": {"stdout": {"event_types": self.parsed.event_types}}})
if self.parsed.no_color:
os.environ["NO_COLOR"] = "1"
if self.parsed.exclude_cdn:
args_preset.explicit_scan_modules.add("portfilter")

Expand Down Expand Up @@ -348,6 +351,7 @@ def create_parser(self, *args, **kwargs):
output.add_argument("-lo", "--list-output-modules", action="store_true", help="List available output modules")
output.add_argument("--json", "-j", action="store_true", help="Output scan data in JSON format")
output.add_argument("--brief", "-br", action="store_true", help="Output only the data itself")
output.add_argument("--no-color", action="store_true", help="Disable colored terminal output")
output.add_argument("--event-types", nargs="+", default=[], help="Choose which event types to display")
output.add_argument(
"--exclude-cdn",
Expand Down
7 changes: 6 additions & 1 deletion bbot/scanner/scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,14 +183,19 @@ def __init__(

# :)
if self.name == "golden_gus":
import os
from base64 import b64decode as _d

_a = _d(
"ICAgICAgICAgICAgICBfX18KICAqd29vZiogIF9fL18gIGAuICAuLSIiIi0uCiAgICAgICAgICBcXyxgIHwgXC0nICAvICAgKWAtJykKICAgICAgICAgICAiIikgImAiICAgIFwgICgoImAiCiAgICAgICAgICBfX19ZICAsICAgIC4nNyAvfAogICAgICAgICAoXyxfX18vLi4uLWAgKF8vXy8="
).decode()
_m = _d("R3VzIGhhcyBibGVzc2VkIHlvdXIgc2Nhbi4=").decode()
no_color = bool(os.environ.get("NO_COLOR", ""))
gold = "" if no_color else "\033[1;38;5;220m"
green = "" if no_color else "\033[1;38;5;118m"
reset = "" if no_color else "\033[0m"
log_to_stderr(
f"\033[1;38;5;220m{_a}\033[0m\n \033[1;38;5;118m{_m}\033[0m",
f"{gold}{_a}{reset}\n {green}{_m}{reset}",
level="HUGESUCCESS",
logname=False,
)
Expand Down
19 changes: 19 additions & 0 deletions bbot/test/test_step_1/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -775,3 +775,22 @@ def test_cli_presets(monkeypatch, capsys, caplog, clean_default_config):
shutil.rmtree(scan_dir, ignore_errors=True)
shutil.rmtree(output_file, ignore_errors=True)
output_dir_preset_file.unlink()


@pytest.mark.asyncio
async def test_cli_no_color(monkeypatch):
from bbot.logger import colorize
from bbot.scanner import Preset

# colorize honors the NO_COLOR convention
monkeypatch.delenv("NO_COLOR", raising=False)
assert colorize("test", "INFO") != "test"
monkeypatch.setenv("NO_COLOR", "1")
assert colorize("test", "INFO") == "test"

# the --no-color CLI switch sets NO_COLOR (restored by monkeypatch at teardown)
monkeypatch.delenv("NO_COLOR", raising=False)
monkeypatch.setattr("sys.argv", ["bbot", "-t", "evilcorp.com", "--no-color"])
preset = Preset()
preset.parse_args()
assert os.environ.get("NO_COLOR") == "1"
22 changes: 19 additions & 3 deletions bbot/test/test_step_1/test_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -397,11 +397,27 @@ async def test_events(events, helpers):
dummy=True,
)

# test confidence colors and formatting
# test the standardized finding color palette
from bbot.core.event.base import FINDING

expected_colors = {"CONFIRMED": "🟣", "HIGH": "🔴", "MEDIUM": "🟠", "LOW": "🟡", "UNKNOWN": "⚪"}
assert FINDING.confidence_colors == expected_colors
assert FINDING.confidence_colors_emoji == {
"CONFIRMED": "🟣",
"HIGH": "🔴",
"MEDIUM": "🟠",
"LOW": "🟡",
"UNKNOWN": "⚪",
}
assert FINDING.severity_colors_emoji == {
"INFO": "🟦",
"LOW": "🟨",
"MEDIUM": "🟧",
"HIGH": "🟥",
"CRITICAL": "🟪",
}
assert FINDING.severity_colors_rgb["INFO"] == (113, 161, 255)
assert FINDING.severity_colors_rgb["CRITICAL"] == (207, 0, 255)
assert set(FINDING.confidence_brightness) == {"CONFIRMED", "HIGH", "MEDIUM", "LOW", "UNKNOWN"}
assert FINDING.severity_card_colors["CRITICAL"] == "Attention"

# test CONFIRMED gets bold formatting
confirmed_finding = scan.make_event(
Expand Down
39 changes: 39 additions & 0 deletions bbot/test/test_step_2/module_tests/test_module_stdout.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,45 @@ def check(self, module_test, events):
assert out.count("[IP_ADDRESS] \t127.0.0.2") == 2


class TestStdoutFindingColor(TestStdout):
module_name = "stdout"

def check(self, module_test, events):
module = module_test.module
# coloring is disabled when stdout isn't a terminal (as in captured test output)
assert module.use_color is False

# CRITICAL is purple (207,0,255); CONFIRMED is full brightness + bold
critical = module_test.scan.make_event(
{
"host": "evilcorp.com",
"severity": "CRITICAL",
"confidence": "CONFIRMED",
"description": "asdf",
"name": "Test",
},
"FINDING",
dummy=True,
)
colored = module._colorize_finding("test", critical)
assert colored == "\033[1;38;2;207;0;255mtest\033[0m"

# INFO is blue (113,161,255) dimmed to 0.55 brightness, no bold
info = module_test.scan.make_event(
{
"host": "evilcorp.com",
"severity": "INFO",
"confidence": "UNKNOWN",
"description": "asdf",
"name": "Test",
},
"FINDING",
dummy=True,
)
colored_info = module._colorize_finding("test", info)
assert colored_info == "\033[38;2;62;88;140mtest\033[0m"


class TestStdoutNoDupes(TestStdoutDupes):
config_overrides = {
"dns": {"minimal": False},
Expand Down
Loading