Cybersecurity-Projects/PROJECTS/intermediate/credential-enumeration/src/output/terminal.nim

236 lines
6.9 KiB
Nim

# ©AngelaMos | 2026
# terminal.nim
#
# ANSI terminal renderer with box-drawing output
#
# Renders the full credential enumeration report to the terminal
# using Unicode box-drawing characters for bordered sections.
# visualLen computes display width by skipping ANSI escape
# sequences and UTF-8 continuation bytes so padding aligns
# correctly despite embedded color codes. truncateVisual truncates
# strings at a visual-width boundary without splitting escape
# sequences. sevBadge produces colored severity labels using the
# SeverityColors and SeverityLabels maps from config. Three box
# helpers (boxLine, boxBottom, boxMid) draw top, bottom, and
# mid-section borders at a fixed 78-column width.
# renderModuleHeader prints a bordered header with the module name,
# category description, finding count, and duration. renderFinding
# shows a severity badge, truncated description, file path with
# permissions and modification date, and an optional credential
# preview. renderSummary totals findings across all modules and
# displays a severity badge breakdown. renderTerminal orchestrates
# the full output sequence: banner, target and module metadata,
# per-module sections (skipping empty modules unless verbose), and
# the summary footer.
#
# Connects to:
# types.nim - Severity, Finding, Report, CollectorResult,
# Credential, ReportMetadata
# config.nim - BoxVertical, BoxTopLeft, BoxTopRight, BoxBottomLeft,
# BoxBottomRight, BoxHorizontal, BoxTeeRight, BoxTeeLeft,
# SeverityColors, SeverityLabels, ColorBold, ColorReset,
# ColorDim, ColorCyan, ColorBoldRed, Banner,
# BannerTagline, AppVersion, Arrow, CrossMark,
# ModuleDescriptions
{.push raises: [].}
import std/[strutils, options]
import ../types
import ../config
const
BoxWidth = 78
InnerWidth = BoxWidth - 2
proc visualLen(s: string): int =
var i = 0
while i < s.len:
if s[i] == '\e':
while i < s.len and s[i] != 'm':
inc i
if i < s.len:
inc i
elif (s[i].ord and 0xC0) == 0x80:
inc i
else:
inc result
inc i
proc truncateVisual(s: string, maxLen: int): string =
var vLen = 0
var i = 0
while i < s.len:
if s[i] == '\e':
let start = i
while i < s.len and s[i] != 'm':
inc i
if i < s.len:
inc i
result.add(s[start ..< i])
elif (s[i].ord and 0xC0) == 0x80:
result.add(s[i])
inc i
else:
if vLen >= maxLen - 3:
result.add("...")
return
result.add(s[i])
inc vLen
inc i
proc writeBoxLine(content: string) =
try:
stdout.write content
let vLen = visualLen(content)
let pad = BoxWidth - vLen - 1
if pad > 0:
stdout.write " ".repeat(pad)
stdout.writeLine BoxVertical
except CatchableError:
discard
proc sevBadge(sev: Severity): string =
SeverityColors[sev] & ColorBold & " " & SeverityLabels[sev] & " " & ColorReset
proc boxLine(width: int): string =
BoxTopLeft & BoxHorizontal.repeat(width - 2) & BoxTopRight
proc boxBottom(width: int): string =
BoxBottomLeft & BoxHorizontal.repeat(width - 2) & BoxBottomRight
proc boxMid(width: int): string =
BoxTeeRight & BoxHorizontal.repeat(width - 2) & BoxTeeLeft
proc renderBanner*(quiet: bool) =
if quiet:
return
try:
stdout.write ColorBoldRed
stdout.writeLine Banner
stdout.write ColorReset
stdout.writeLine ""
stdout.write " "
stdout.write ColorDim
stdout.write BannerTagline
stdout.write " v"
stdout.write AppVersion
stdout.writeLine ColorReset
stdout.writeLine ""
except CatchableError:
discard
proc renderModuleHeader(
name: string, desc: string, findingCount: int, durationMs: int64
) =
try:
stdout.writeLine boxLine(BoxWidth)
let label =
BoxVertical & " " & ColorBold & ColorCyan & name.toUpperAscii() & ColorReset &
ColorDim & " " & Arrow & " " & desc & ColorReset
let stats =
$findingCount & " findings" & ColorDim & " (" & $durationMs & "ms)" & ColorReset
let usedWidth = 2 + name.len + 3 + desc.len
let statsVisual = visualLen(stats)
let gap = BoxWidth - usedWidth - statsVisual - 2
stdout.write label
if gap > 0:
stdout.write " ".repeat(gap)
else:
stdout.write " "
stdout.write stats
stdout.writeLine " " & BoxVertical
stdout.writeLine boxMid(BoxWidth)
except CatchableError:
discard
proc renderFinding(f: Finding) =
let descLine =
BoxVertical & " " & sevBadge(f.severity) & " " &
truncateVisual(f.description, InnerWidth - 14)
writeBoxLine(descLine)
var detail = BoxVertical & " " & ColorDim & f.path & " [" & f.permissions & "]"
if f.modified != "unknown":
detail &= " mod:" & f.modified
detail &= ColorReset
writeBoxLine(detail)
if f.credential.isSome:
let cred = f.credential.get()
if cred.preview.len > 0:
let previewLine =
BoxVertical & " " & ColorDim & Arrow & " " & cred.preview & ColorReset
writeBoxLine(previewLine)
proc renderModuleErrors(errors: seq[string]) =
if errors.len == 0:
return
for err in errors:
let errLine =
BoxVertical & " " & ColorBoldRed & CrossMark & ColorReset & " " & ColorDim & err &
ColorReset
writeBoxLine(errLine)
proc renderSummary(report: Report) =
try:
stdout.writeLine ""
stdout.writeLine boxLine(BoxWidth)
writeBoxLine(BoxVertical & " " & ColorBold & "SUMMARY" & ColorReset)
stdout.writeLine boxMid(BoxWidth)
var totalFindings = 0
for sev in Severity:
totalFindings += report.summary[sev]
let countLine =
BoxVertical & " " & ColorBold & $totalFindings & ColorReset & " findings across " &
ColorBold & $report.results.len & ColorReset & " modules" & ColorDim & " (" &
$report.metadata.durationMs & "ms)" & ColorReset
writeBoxLine(countLine)
var badgeLine = BoxVertical & " "
for sev in countdown(svCritical, svInfo):
let count = report.summary[sev]
if count > 0:
badgeLine &= sevBadge(sev) & " " & $count & " "
writeBoxLine(badgeLine)
stdout.writeLine boxBottom(BoxWidth)
stdout.writeLine ""
except CatchableError:
discard
proc renderTerminal*(report: Report, quiet: bool, verbose: bool) =
renderBanner(quiet)
try:
if not quiet:
stdout.write ColorDim & " Target: " & ColorReset
stdout.writeLine report.metadata.target
stdout.write ColorDim & " Modules: " & ColorReset
stdout.writeLine report.metadata.modules.join(", ")
stdout.writeLine ""
except CatchableError:
discard
for res in report.results:
if res.findings.len == 0 and res.errors.len == 0 and not verbose:
continue
renderModuleHeader(
res.name, ModuleDescriptions[res.category], res.findings.len, res.durationMs
)
for finding in res.findings:
renderFinding(finding)
renderModuleErrors(res.errors)
try:
stdout.writeLine boxBottom(BoxWidth)
except CatchableError:
discard
renderSummary(report)