output

package
v0.5.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 3, 2026 License: MIT Imports: 33 Imported by: 0

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

View Source
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.

View Source
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

func BuildEvidence(label, request, response string) string

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

func EscapeOneLine(s string) string

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

func GroupingKey(moduleID, severity, value, host string, perHost bool) string

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

func NormalizeStringSet(items []string) map[string]struct{}

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

func NormalizeTagSet(tags []string) map[string]struct{}

NormalizeTagSet lowercases and trims a tag filter into a set, returning nil when nothing usable remains (meaning "match regardless of tags").

func NormalizedValueKey

func NormalizedValueKey(values []string) string

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

func RuleGroupKey(moduleID, rule string) string

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

func TagsIntersect(tags []string, set map[string]struct{}) bool

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

type EvidencePair struct {
	Label    string
	Request  string
	Response string
	Prose    string
}

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
	ReportSharedURL 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
	ReportSharedURL 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

type ReportItemProducer func(emit func(any) error) error

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

type ReportTrimInfo struct {
	BodiesTruncated int
	BodiesDropped   int
	BytesOmitted    int64
}

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL