Documentation
¶
Overview ¶
Package output defines the ResultEvent finding model emitted by every module and the handlers that format and deliver results. It supports the console, JSONL, and HTML output formats (the latter via an embedded ag-grid report template) and carries the metadata — severity, confidence, evidence, matched request/response — that the executor fills in before findings are stored or reported.
Index ¶
- Constants
- func BuildEvidence(label, request, response string) string
- func EscapeOneLine(s string) string
- func FormatPhaseFindingLine(phaseTag string, r *ResultEvent) string
- func GenerateDocumentReport(items []any, outputPath string, meta HTMLReportMeta) error
- func GenerateHTMLReport(items []any, outputPath string, meta HTMLReportMeta) error
- func GenerateHTMLReportStreaming(produce ReportItemProducer, outputPath string, meta HTMLReportMeta) error
- func GenerateMarkdownReport(items []any, outputPath string, meta HTMLReportMeta) error
- func GeneratePDFReport(items []any, outputPath string, meta HTMLReportMeta) error
- func GenerateSARIFReport(items []any, outputPath string, meta HTMLReportMeta) error
- func GroupingBranch(moduleID, rule, value, severity string, tags []string, ...) (moduleKey, valueKey string, ok bool)
- func GroupingKey(moduleID, severity, value, host string, perHost bool) string
- func IsSuspectBundle(moduleID, severity string, tags []string, bundleSuspect map[string]struct{}) bool
- func MatchedURL(r *ResultEvent) string
- func NormalizeStringSet(items []string) map[string]struct{}
- func NormalizeTagSet(tags []string) map[string]struct{}
- func NormalizedValueKey(values []string) string
- func RuleGroupKey(moduleID, rule string) string
- func TagsIntersect(tags []string, set map[string]struct{}) bool
- type EvidenceGrade
- type EvidencePair
- type HTMLReportData
- type HTMLReportMeta
- type Info
- type MultiWriter
- type RecordKind
- type ReportData
- type ReportFinding
- type ReportItemProducer
- type ReportModule
- type ReportTrimInfo
- type ResultEvent
- type StandardWriter
- type Writer
Constants ¶
const EvidenceSeparator = "\n---------\n"
EvidenceSeparator delimits the request and response halves of a single evidence entry. The primary Request/Response pair and every AdditionalEvidence entry share this format. This is the single source of truth for the delimiter; the storage layer's dedup/merge path references it (see pkg/database) so stored evidence stays splittable the same way modules emit it.
const SuspectBundleTag = "secret-generic"
SuspectBundleTag marks a finding as eligible for its module's Suspect-tier bundle (see IsSuspectBundle): a low-signal, family-less secret-shaped match — a generic API-key / password / token pattern that names no specific provider — which folds into the per-host "Low-confidence secret-shaped matches" rollup instead of standing on its own. A NAMED provider family (a Google / Slack / Storyblok rule) is deliberately left untagged, so it stays a per-rule finding even at Suspect severity and is never collapsed into the generic rollup with unrelated families. secret-detect sets it on its generic-namespace rules.
Variables ¶
This section is empty.
Functions ¶
func BuildEvidence ¶
BuildEvidence renders one request/response pair into a single AdditionalEvidence entry. When label is non-empty it is prefixed as a "# [label]" marker line so a reviewer — and the UI's evidence tabs — can tell a baseline pair from an attack pair from a confirmation round. Returns "" when both halves are empty (so an empty pair is never recorded).
func EscapeOneLine ¶
EscapeOneLine renders embedded control characters (newlines, carriage returns, tabs) as their literal backslash-escape sequences so a finding's extracted value always prints as a single console line — while preserving the visual cue that the value spanned multiple lines (e.g. a JS snippet), rather than collapsing the structure away into spaces. Values without such characters pass through untouched.
func FormatPhaseFindingLine ¶
func FormatPhaseFindingLine(phaseTag string, r *ResultEvent) string
FormatPhaseFindingLine renders a phase-prefixed one-line summary of a finding for live display on stderr during a scan, e.g.:
❯ dynamic-assessment │ [passive] [crypto-weakness-detect] [• low] GET https://h/p
It mirrors the chevron/pipe prefix of the phase status line plus the bracketed [type] [module] severity METHOD URL layout of the console result writer (formatScreen), so streamed findings read identically whether they reach the terminal via stdout (console format) or this stderr echo (jsonl/html format, where the stdout result stream is deferred to files). The trailing newline is included.
func GenerateDocumentReport ¶
func GenerateDocumentReport(items []any, outputPath string, meta HTMLReportMeta) error
func GenerateHTMLReport ¶
func GenerateHTMLReport(items []any, outputPath string, meta HTMLReportMeta) error
GenerateHTMLReport renders the embedded template.html template with the provided items to outputPath. If title is empty it defaults to "Vigolium Scan Report".
Each item should be a ready-to-marshal envelope (e.g. struct with Type and Data fields). Items are streamed one at a time to keep memory usage constant regardless of result set size.
func GenerateHTMLReportStreaming ¶
func GenerateHTMLReportStreaming(produce ReportItemProducer, outputPath string, meta HTMLReportMeta) error
GenerateHTMLReportStreaming renders the embedded template.html to outputPath by pulling items from produce one at a time, so the full result set never lives in memory. The output is byte-identical to a GenerateHTMLReport call over the same items. The report is written to a temp sibling and atomically renamed into place on success, so a mid-stream error never leaves a half-written report behind.
func GenerateMarkdownReport ¶
func GenerateMarkdownReport(items []any, outputPath string, meta HTMLReportMeta) error
func GeneratePDFReport ¶
func GeneratePDFReport(items []any, outputPath string, meta HTMLReportMeta) error
func GenerateSARIFReport ¶
func GenerateSARIFReport(items []any, outputPath string, meta HTMLReportMeta) error
GenerateSARIFReport writes the run's findings as a SARIF 2.1.0 log. It shares the signature of every other report generator (GenerateHTMLReport, GenerateMarkdownReport, …) so it drops into reportGenerator and the scan commands' reportFormats table unchanged.
func GroupingBranch ¶
func GroupingBranch(moduleID, rule, value, severity string, tags []string, byModule, byRule, bundleSuspect, tagSet map[string]struct{}) (moduleKey, valueKey string, ok bool)
GroupingBranch decides how one finding folds into a group, shared by the DB grouping pass and the live console grouper so the by-module / suspect-bundle / by-rule / value branches can't drift between the two. value is the finding's already-normalized extracted-value key (see NormalizedValueKey); severity is its severity name; tags is its tag list; byModule, byRule, bundleSuspect and tagSet are the resolved option sets.
It returns the module component of the grouping key (moduleID, or the rule-folded key for a by-rule module), the value component (emptied for by-module, suspect-bundle, and by-rule so every value collapses into one group), and ok=false when the finding is ungroupable — no stable extracted value, or the tag gate rejects it. A returned empty valueKey with ok=true therefore marks a collapse-all group, where distinct values get unioned onto the survivor.
The suspect-bundle branch (see IsSuspectBundle) sits between by-module and by-rule: a module can be BOTH by-rule and bundle-suspect (secret-detect is), so its Suspect-severity findings collapse by module (every low-confidence rule on a host folds into one bundle) while its higher-severity findings stay per-rule. Severity is also a component of the outer GroupingKey, so the two tiers never share a group regardless.
func GroupingKey ¶
GroupingKey assembles the finding grouping key from its components, prefixing the host when perHost is set. The single source of truth for the key layout (and its separator) shared by the live console grouper and the DB grouping pass — callers compute value via NormalizedValueKey (or "" to collapse a whole module) and pass it in.
func IsSuspectBundle ¶
func IsSuspectBundle(moduleID, severity string, tags []string, bundleSuspect map[string]struct{}) bool
IsSuspectBundle reports whether a finding folds into a module's Suspect-tier bundle: the module is listed in bundleSuspect, the finding's severity is "suspect", AND the finding carries SuspectBundleTag — the marker a module sets only on its generic, family-less matches. Such findings group by module (the rule/value is dropped from the key), collapsing the low-signal noise on a host into a single bundle, while a NAMED provider family (which the module leaves untagged) stays per-rule so distinct families are never merged into one rollup. The same module's higher-severity findings group by rule regardless.
Currently only secret-detect uses this — its generic-namespace rules (the "Generic Password" / "Generic API Key" matchers) are the noisy, family-less tier that warrants one per-host rollup; a recognisable provider key (Google, Storyblok, …) keeps its own triage row even when severity-downgraded to Suspect.
func MatchedURL ¶
func MatchedURL(r *ResultEvent) string
MatchedURL returns the location a finding points at, preferring the precise matched-at over the request URL and finally the host. Single source of truth for the Matched→URL→Host fallback used by screen formatting and grouping.
func NormalizeStringSet ¶
NormalizeStringSet trims each entry and drops empties and duplicates, returning a lookup set (nil when nothing usable remains). Unlike NormalizeTagSet it does NOT lowercase — it is for exact-match identifiers such as module IDs, which are matched case-sensitively against module_id in the DB.
func NormalizeTagSet ¶
NormalizeTagSet lowercases and trims a tag filter into a set, returning nil when nothing usable remains (meaning "match regardless of tags").
func NormalizedValueKey ¶
NormalizedValueKey builds a stable grouping key from a finding's extracted results: each entry trimmed, empties dropped, the remainder sorted (so order doesn't matter), joined with a separator that can't appear inside a single value. Returns "" when nothing usable remains (callers treat that as "ungroupable"). This is the single source of truth for the value-identity contract shared by the live console grouper and the DB grouping pass.
func RuleGroupKey ¶
RuleGroupKey folds a module's per-finding rule identity (e.g. secret-detect's secret-scan rule name, carried in Info.Name / module_name) into the module component of a grouping key. ByRule grouping then collapses repeats of the SAME rule on a host while keeping DISTINCT rules — a "Looker Client ID" vs an "AWS Access Key", both emitted by secret-detect — as separate findings. Uses groupKeySeparator so the composite slots cleanly into GroupingKey's module position (every component there is control-char delimited, so the extra slot is unambiguous).
func TagsIntersect ¶
TagsIntersect reports whether any of tags is present in set (built by NormalizeTagSet). An empty/nil set never matches — callers gate on len(set) first.
Types ¶
type EvidenceGrade ¶
type EvidenceGrade string
EvidenceGrade records how far a result progressed through confirmation.
const ( EvidenceGradeObservation EvidenceGrade = "E0" EvidenceGradeCandidate EvidenceGrade = "E1" EvidenceGradeDifferential EvidenceGrade = "E2" EvidenceGradeBypass EvidenceGrade = "E3" EvidenceGradeImpact EvidenceGrade = "E4" )
type EvidencePair ¶
EvidencePair is one AdditionalEvidence entry parsed back into structured parts: an optional Label plus the Request/Response halves. Prose is set instead (with Request/Response empty) when the entry carries no separator — some modules and JS extensions push free-form strings into AdditionalEvidence despite the documented pair format, so renderers can present real pairs structurally and prose as-is rather than guessing.
func ParseEvidence ¶
func ParseEvidence(entry string) EvidencePair
ParseEvidence is the inverse of BuildEvidence: it parses one AdditionalEvidence entry back into its structured parts, recognizing the optional "# [label]" marker line and the EvidenceSeparator between the request and response halves. An entry with no separator is returned as Prose, so a renderer can tell a genuine pair from arbitrary text. This is the single parser for the evidence format that BuildEvidence produces, so every consumer splits it identically.
func (EvidencePair) IsPair ¶
func (e EvidencePair) IsPair() bool
IsPair reports whether the entry parsed as a request/response pair (as opposed to a free-form prose entry).
type HTMLReportData ¶
type HTMLReportData struct {
Title string
GeneratedAt string
ScanDuration string
ScanTarget string
VigoliumVersion string
DataExpected string
ResultsJSON template.JS
}
HTMLReportData is the template data passed to template.html.
type HTMLReportMeta ¶
type HTMLReportMeta struct {
Title string
Version string
ScanDuration string
ScanTarget string
GeneratedAt string
// OnTrim, when set, receives the body-capping summary after the report is
// written instead of the generator printing its own inline "Note:" line. Lets
// a caller fold a short trim summary into its own export listing. Only the
// HTML format trims; nil keeps the default inline note.
OnTrim func(ReportTrimInfo)
}
HTMLReportMeta carries metadata for the HTML report generation.
type Info ¶
type Info struct {
Name string `json:"name,omitempty"`
Tags []string `json:"tags,omitempty"`
Description string `json:"description,omitempty"`
Reference []string `json:"reference,omitempty"`
Severity severity.Severity `json:"severity,omitempty"`
Confidence severity.Confidence `json:"confidence,omitempty"`
}
Info contains module metadata
type MultiWriter ¶
type MultiWriter struct {
// contains filtered or unexported fields
}
func NewMultiWriter ¶
func NewMultiWriter(writers ...Writer) *MultiWriter
NewMultiWriter creates a new MultiWriter instance
func (*MultiWriter) Close ¶
func (mw *MultiWriter) Close()
func (*MultiWriter) ShowsFindingsOnStdout ¶
func (mw *MultiWriter) ShowsFindingsOnStdout() bool
ShowsFindingsOnStdout reports whether any underlying writer renders findings to stdout in human-readable form (see StandardWriter.ShowsFindingsOnStdout).
func (*MultiWriter) Write ¶
func (mw *MultiWriter) Write(event *ResultEvent) error
func (*MultiWriter) WriteFileOnly ¶
func (mw *MultiWriter) WriteFileOnly(event *ResultEvent) error
type RecordKind ¶
type RecordKind string
RecordKind separates reconnaissance and unconfirmed hypotheses from reportable vulnerabilities while keeping one backward-compatible event shape.
const ( RecordKindFinding RecordKind = "finding" RecordKindCandidate RecordKind = "candidate" RecordKindObservation RecordKind = "observation" )
type ReportData ¶
type ReportData struct {
Title string
GeneratedAt string
ScanDuration string
VigoliumVersion string
Target string
TotalFindings int
TotalRequests int
CriticalCount int
HighCount int
MediumCount int
LowCount int
InfoCount int
ActiveModules int
PassiveModules int
CriticalFindings []ReportFinding
HighFindings []ReportFinding
MediumFindings []ReportFinding
LowFindings []ReportFinding
InfoFindings []ReportFinding
Modules []ReportModule
}
type ReportFinding ¶
type ReportFinding struct {
ID int
Title string
ModuleID string
ModuleName string
Description string
DescriptionHTML template.HTML
// ModuleTitle is the module's own Name(), kept separate because ModuleName
// above holds the *display* name — parseFinding prefers the module's short
// description there, which reads as a sentence. A renderer that needs a
// compact label (SARIF's reportingDescriptor.name) wants this one.
ModuleTitle string
Severity string
Confidence string
CWE string
CVSSScore float64
Remediation string
URL string
MatchedAt []string
ExtractedResults []string
Request string
Response string
AdditionalEvidence []string
Tags []string
FoundAt string
FindingHash string
SourceFile string
RepoName string
}
type ReportItemProducer ¶
ReportItemProducer streams export envelopes to emit, one at a time, in the order they should appear in the report. It must return the first error from emit (fatal) and should log-and-skip its own data-source errors. Used by GenerateHTMLReportStreaming so the report renders without ever materializing the whole result set.
type ReportModule ¶
type ReportModule struct {
ID string
Name string
Type string
Severity string
// Description and ShortDescription are carried for renderers that describe
// the rule behind a finding rather than the finding itself (SARIF's
// reportingDescriptor). They are only populated when the export included
// module envelopes — `vigolium export` drops them by default (--exclude
// module) — so every consumer must treat them as optional.
Description string
ShortDescription string
}
type ReportTrimInfo ¶
ReportTrimInfo summarizes the body-capping the HTML report applied, so a caller can surface it in its own summary instead of the inline note.
func (ReportTrimInfo) Summary ¶
func (t ReportTrimInfo) Summary() string
Summary renders a compact one-line description of what was trimmed (no surrounding parentheses), e.g. "trimmed 12, omitted 3 bodies · 8.0 MB". Empty when nothing was trimmed.
func (ReportTrimInfo) Trimmed ¶
func (t ReportTrimInfo) Trimmed() bool
Trimmed reports whether any body was shortened or dropped.
type ResultEvent ¶
type ResultEvent struct {
// Module identification (JSON tag kept as "template-id" for Nuclei output compatibility)
ModuleID string `json:"template-id"`
// Info contains module metadata, serialized as a nested "info" object
// (matching Nuclei's JSONL output).
Info Info `json:"info"`
// RecordKind defaults to finding for compatibility with existing modules.
// Candidate and observation events are persisted but excluded from finding
// totals, notifications, and cross-module confirmed-result suppression.
RecordKind RecordKind `json:"record_kind,omitempty"`
EvidenceGrade EvidenceGrade `json:"evidence_grade,omitempty"`
// Type is the type of the result event (always "http" for this scanner)
Type string `json:"type"`
// Target information
Host string `json:"host,omitempty"`
Scheme string `json:"scheme,omitempty"`
URL string `json:"url,omitempty"`
IP string `json:"ip,omitempty"`
// Match details
Matched string `json:"matched-at,omitempty"`
ExtractedResults []string `json:"extracted-results,omitempty"`
MatcherStatus bool `json:"matcher-status"`
// Request/Response data
Request string `json:"request,omitempty"`
Response string `json:"response,omitempty"`
AdditionalEvidence []string `json:"additional_evidence,omitempty"`
// Metadata
Metadata map[string]interface{} `json:"meta,omitempty"`
Timestamp time.Time `json:"timestamp"`
// Fuzzing fields (kept for compatibility with fuzzing results)
IsFuzzingResult bool `json:"is_fuzzing_result,omitempty"`
FuzzingParameter string `json:"fuzzing_parameter,omitempty"`
// Error field for error reporting
Error string `json:"error,omitempty"`
// Internal fields (not serialized to JSON)
DisableNotify bool `json:"-"`
ModuleType string `json:"-"`
FindingSource string `json:"-"`
ModuleShort string `json:"-"`
// DedupKey, when non-empty, lets a module choose an explicit deduplication
// identity instead of the default content hash (module/description/severity/
// matched): ID() derives the finding's identity from this key alone. Use it when
// findings should group by something other than their content — e.g. the OAST
// collector sets the per-payload callback nonce so each distinct callback host is
// its own finding (callbacks for the same payload coalesce; different payloads
// stay separate) regardless of the protocol-derived description/severity an
// upgrade changes. Empty for the default content-based identity.
DedupKey string `json:"-"`
}
ResultEvent is a wrapped result event for a single scan output. The format is designed to be compatible with Nuclei's JSONL output.
func (*ResultEvent) EffectiveRecordKind ¶
func (r *ResultEvent) EffectiveRecordKind() RecordKind
EffectiveRecordKind returns a validated kind, treating the zero value and unknown legacy values as findings.
func (*ResultEvent) ID ¶
func (r *ResultEvent) ID() string
ID returns a unique identifier for deduplication purposes. When DedupKey is set the identity is derived from it alone (an explicit dedup grouping chosen by the producer, e.g. the OAST per-payload nonce); otherwise it is the content hash of the module, description, severity, and matched location.
func (*ResultEvent) IsFinding ¶
func (r *ResultEvent) IsFinding() bool
IsFinding reports whether this event is a reportable vulnerability.
type StandardWriter ¶
type StandardWriter struct {
DisableStdout bool
IncludeResponseInOutput bool
JSONOutput bool
PhaseTag string // Phase label for console output prefix (e.g. "scan", "known-issue-scan")
// OnEvent, when set, is invoked for every finding this writer emits, before
// any rendering. It exists so a single seam can observe the whole finding
// stream — the machine event stream (--events) counts and reports findings
// through it — instead of each of the runner's ~six OnResult callbacks
// growing its own copy, which is how one of them ends up missed.
//
// It runs on the emitting goroutine BEFORE the writer takes its mutex, so it
// is NOT serialized against other writers — a hook must be concurrency-safe
// on its own, cheap, and must not call back into the writer. (Calling it
// under the lock would put a stdout write inside the writer's mutex.)
OnEvent func(*ResultEvent)
// contains filtered or unexported fields
}
StandardWriter is a writer writing output to file and screen for results.
func NewStandardWriter ¶
func NewStandardWriter(options *types.Options) (*StandardWriter, error)
func (*StandardWriter) Close ¶
func (w *StandardWriter) Close()
Close closes the output writing interface
func (*StandardWriter) ShowsFindingsOnStdout ¶
func (w *StandardWriter) ShowsFindingsOnStdout() bool
ShowsFindingsOnStdout reports whether findings passed to Write are rendered to stdout in human-readable form during the scan. It's false when stdout is suppressed (silent, or a deferred jsonl/html format that emits its output post-scan) or when stdout carries raw JSON. Callers use this to decide whether to echo a compact human-readable finding line elsewhere (e.g. stderr) so live progress stays visible when nothing reaches stdout during the scan.
func (*StandardWriter) Write ¶
func (w *StandardWriter) Write(event *ResultEvent) error
Write writes the event to file and/or screen.
func (*StandardWriter) WriteFileOnly ¶
func (w *StandardWriter) WriteFileOnly(event *ResultEvent) error
WriteFileOnly writes the event to file only, skipping screen output.
type Writer ¶
type Writer interface {
// Close closes the output writer interface
Close()
// Write writes the event to file and/or screen.
Write(*ResultEvent) error
// WriteFileOnly writes the event to file only, skipping screen output.
WriteFileOnly(*ResultEvent) error
}
Writer is an interface which writes output to somewhere for scan events.