report

package
v0.125.0 Latest Latest
Warning

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

Go to latest
Published: Sep 15, 2026 License: Apache-2.0 Imports: 28 Imported by: 0

Documentation

Overview

Package report renders a scan result in a chosen format. Each format is a Reporter over a common Data value, so the CLI (and, later, the branch diff) can emit console/markdown/HTML for humans, JUnit for CI test panels, and JSON/SARIF for machines through one interface.

Index

Constants

This section is empty.

Variables

View Source
var StreamFormats = []string{"console", "markdown", "json", "sarif", "vex", "template"}

StreamFormats are the formats `--format` accepts: the ones whose natural destination is a stream, so a terminal, a pipe or a redirect all make sense.

html and junit are deliberately absent. An HTML report is a styled document with its CSS inlined, and printing four thousand lines of it because someone typed a plausible-looking flag is not a thing to explain away. A JUnit file is read by a CI runner from a path, never by a person. Both are produced with --report into an output directory, which is the only place they were ever useful.

Narrow on purpose. Every format offered here is one a reader has to rule out.

Functions

func AutomationID added in v0.121.1

func AutomationID(project string, scope engine.Scope) string

AutomationID is what a run writes to SARIF's runs[].automationDetails.id, and it decides whether two analyses of one commit can coexist.

GitHub code scanning splits the id on its last "/" into a category and a run id, and an upload replaces whatever it last received under the same tool and category. A report carrying no id is filed under the empty category, so a monorepo publishing two products over one commit keeps only whichever pipeline finished last, with nothing in either run saying so. An upload made through the code-scanning API cannot correct this from outside: that endpoint takes no category of its own, and the id in the file is the only thing it reads.

The id is the project and, when the run was narrowed, what it was narrowed to. Both are asked for rather than found, which is what makes the id identical on every run of the same product, and an identical id is what lets an alert fixed in one run resolve instead of reappearing under a new category. The trailing "/" puts all of it in the category and leaves the run id empty.

Empty when there is no project and no narrowing. That is a scan with no descriptor at all, and also an ordinary descriptor that never set project:, which the schema does not require.

func BuildLabel added in v0.122.0

func BuildLabel(build string) string

BuildLabel is the Draugr that produced a report, spelled for somebody reading it.

A binary not built from a release tag is stamped "dev", which in a footer reads as though "dev" were the version number. Only the formats a person reads use this; the rest carry what Data holds, which is what a consumer comparing two reports can compare.

func Filename added in v0.63.0

func Filename(format string) string

Filename is what a format is written as, whether a publisher delivers it or `-o` writes it.

One table, because the alternative is two that agree until they do not. A format written under one name locally and another by a publisher breaks whatever globs for it, and a CI step that globs for a file it never finds usually warns and carries on, which is a green run with no results in it.

func FilterByPriority added in v0.82.0

func FilterByPriority(run engine.Result, minPriority string) engine.Result

FilterByPriority drops findings below the requested band, returning a copy so the caller's run is untouched. The same Data is rendered in several formats and delivered to publishers.

Findings the scanner never prioritized are kept. An empty Priority means prioritization did not run for that finding, not that it ranked low, and silently dropping it would be the worst reading of an unset field.

Only the results need filtering: the emitted rules[] is derived from the results that remain, so a rule nobody matched leaves with them. That is where most of the size saving comes from, on Draugr's demo repository, filtering to P1 takes the compact SARIF from 82 KB to 32 KB.

func Formats

func Formats() []string

Formats lists the available format names, sorted.

func StreamFormat added in v0.60.0

func StreamFormat(format string) error

StreamFormat reports whether a format may be written to stdout, and if not, why.

func Summary added in v0.123.0

func Summary(format string) string

Summary is what a format is for, or "" for a name this build does not render.

The one place that answers it. An editor's completion, the catalog's table and any listing written later read this rather than composing a sentence of their own.

func UncoveredColumns added in v0.123.0

func UncoveredColumns() []string

UncoveredColumns are the column names the "Not checked" block is headed with.

Exported so `doctor` renders the same block as the scan report rather than a second one. The two answer the same question and diverging would leave a reader comparing them with no way to tell which is stale.

Types

type Action added in v0.98.0

type Action struct {
	// Title is what to do, in the imperative.
	Title string `json:"title"`
	// Control the findings came from, and the worst priority among them.
	Control  string `json:"control,omitempty"`
	Priority string `json:"priority,omitempty"`
	// Clears is how many findings this one action resolves.
	Clears int `json:"clears"`
	// Upstream marks an action whose unit of work is something somebody else publishes: the fix
	// is to take a newer one, not to change anything inside it.
	Upstream bool `json:"upstream,omitempty"`
	// Where lists the distinct places this applies, capped.
	Where []string `json:"where,omitempty"`
	// RuleIDs are the rules this action resolves, capped, so a caller can look any of them up.
	RuleIDs []string `json:"ruleIds,omitempty"`
	// Key is what these findings grouped under: the identity that makes two of them one action.
	//
	// Opaque, and deliberately. Its shape is this package's business and changes when the grouping
	// does. What it is for is membership: a caller holding the findings can ask this
	// package which action each one belongs to and match on this, instead of inferring it from a
	// title. Title is written for a reader and is not an identity, an action fed by two controls
	// takes one of their names, and matching on that silently drops the other's findings.
	//
	// Not serialized. It is an identity for a caller holding this package's own output in memory,
	// and it contains a separator that has no business in a JSON document an assistant reads.
	Key string `json:"-"`
	// FixedVersions are the releases that clear this, in the order the advisories named them.
	//
	// Every one of them, not the newest: advisories disagree about which release resolves them,
	// and version ordering belongs to the ecosystem rather than here. One entry is the answer;
	// several means the reader's package manager settles it, and naming one of them as sufficient
	// would read as "do this and you are done" while leaving findings behind.
	FixedVersions []string `json:"fixedVersions,omitempty"`
}

Action is one thing to do and what doing it clears, for a consumer outside this package.

Exported so the MCP server answers "what should I do" with the same grouping the console prints. The keying is the subtle part. Which findings are one fix and which only look alike, and a second implementation of it would drift from this one silently, leaving an assistant and a terminal describing the same report differently.

func ActionsFor added in v0.98.0

func ActionsFor(reports map[string]sarif.Report) []Action

ActionsFor groups a run's findings into the fix list, most urgent first.

Suppressed findings are left out for the same reason the console leaves them out: an excluded finding is a decision somebody recorded, and proposing it as work is proposing to undo that decision without the reason they gave.

Keyed by control, as a run holds them, because the grouping keys on it: two controls reporting the same rule id are two things to do. A caller holding only a merged results.sarif has no control to give and can pass a single entry under "", grouping then falls back to the rule id, which is the right answer for a file that has already lost the distinction.

type Artifact added in v0.21.0

type Artifact struct {
	Format      string // the reporter format, e.g. "sarif"
	Filename    string // default base filename, e.g. "results.sarif"
	ContentType string // MIME type, e.g. "application/sarif+json"
	Bytes       []byte // the rendered report
}

Artifact is a rendered report plus the metadata a publisher needs to deliver it: a default filename, a MIME content type, and the bytes. It is the unit a Publisher (pkg/publish) delivers to a destination.

func Build added in v0.21.0

func Build(cfg saga.ReportConfig, d Data) (Artifact, error)

Build renders a report as configured and returns it as an Artifact ready to publish. The "template" format renders a user-supplied Go text/template (cfg.Template / cfg.TemplateFile); all other formats use the built-in reporter registry. cfg.Filename overrides the default output filename.

func SBOMArtifacts added in v0.41.0

func SBOMArtifacts(docs []sbom.Document) []Artifact

SBOMArtifacts converts SBOM documents into the unit publishers deliver, so SBOMs travel the same path as every other output rather than needing their own delivery mechanism.

type ComponentVerdict added in v0.54.0

type ComponentVerdict struct {
	Name string
	// Verdict is the run's policy applied to this component's findings alone. Computed by
	// running the same norn.Policy rather than re-deciding, so the parts cannot disagree with
	// the whole about what failing means.
	Verdict norn.Verdict
	// Controls names the controls that failed for this component, in order.
	Controls []string
	// Priorities counts this component's findings by band, highest first (P1…P4).
	Priorities [4]int
	// Findings is the total, suppressed ones excluded, the same rule the counts follow.
	Findings int
	// Declared counts what the descriptor gave this component, by target kind. It is the denominator:
	// "3 images not scanned" does not say whether that is all of them or three of thirty, and those
	// are different situations. One is a component nobody looked at, the other is a gap in one that
	// was mostly covered.
	Declared map[string]int
	// Unscanned is what this component has that no scanner managed to look at.
	//
	// A component whose every image failed to pull has had nothing examined, and without this it
	// renders as passing with no findings. Which is the report asserting something no scanner
	// established. The same reasoning already keeps a component the scope excluded out of the pass
	// list; a component the scan could not reach is the same situation arrived at later.
	Unscanned []engine.Unscanned
}

ComponentVerdict is one component's outcome, judged by the same policy as the run.

The unit a team owns, and the unit exposure and criticality are declared on. So the unit someone is actually deciding about. The controls table answers "is the project shippable", which is a different and usually less useful question than "is my service".

type Data

type Data struct {
	// Project is which project this run belongs to, and what a platform files it under.
	Project     string
	Release     saga.Release
	Run         engine.Result
	Verdict     norn.Result
	MinPriority string
	// Scope describes what the run was narrowed to, and is nil when it was not narrowed at all.
	//
	// A pointer so that every unscoped report. Which is nearly all of them, renders and serializes
	// exactly as it did before. Its presence is the signal: an artifact carrying a scope is a partial
	// answer, and a consumer that finds one has been told so rather than having to infer it from a
	// component list that looks complete.
	Scope *Scope
	// TopN caps how many findings the console "Fix first" table shows: 0 uses the default,
	// a negative value shows all, and a positive value shows that many. Ignored by other formats.
	TopN int
	// Evidence restores the blocks that make a run defensible, tool provenance, what each control
	// measured against, declared effects, the scanned revision, job and cache counts.
	//
	// Off by default. A developer at a terminal is asking what to fix, and answers to questions
	// they have not asked push the answer to the one they have off the screen. An auditor is a
	// real reader, just not the default one, and asks for this.
	Evidence bool
	// Descriptor is the Saga this run was produced from: the digest of the merged, effective
	// document and the files it was assembled from.
	//
	// Nil for a scan with no descriptor, which is a real case and not an error.
	Descriptor *skald.DescriptorRef
	// CI is the job the scan ran in, and nil outside one.
	//
	// It exists once, in the process doing the work, and is gone when that process exits. A
	// platform receiving this report can see a repository URL and cannot see which workflow
	// produced the scan.
	CI *ci.Context
	// Gate records the policy the verdict was produced under.
	//
	// A verdict is only as meaningful as the gate behind it, and a gate can be narrowed or turned off
	// entirely from the command line. Without this the report shows a verdict and no way to tell what
	// it was measured against, the same gap that suppression closes for findings, where the question
	// is never "did the scanner run" but "who decided this was acceptable".
	Gate GateSettings

	// View is what the report shows and how densely, one setting rather than two that have to be
	// combined. The zero value is ViewFindings.
	View View
	// Uncovered is what this descriptor declares and no enabled control examines.
	Uncovered []Gap
	// Suggestions are the things this particular run makes worth trying, decided by the caller
	// because they depend on how it was invoked rather than on what it found.
	Suggestions []Suggestion
	// Unclassified reports that no component declares exposure or criticality.
	//
	// A caveat on every band in the report: with nothing declared, each component is read as public
	// and critical, which is the most severe reading, so the bands rank severity alone. A reader
	// who does not know that is reading a ranking as though it described their application.
	Unclassified bool
	// Generated and Version stamp a report with when it ran and what produced it. A report
	// offered as evidence has to answer both; a reader who cannot tell whether they are looking
	// at today's scan or last quarter's has nothing they can rely on. Zero values are omitted,
	// so a caller that does not set them still renders a valid report.
	Generated time.Time
	Version   string
	// VEX names the author and product for the "vex" format. Nil falls back to the release,
	// which is enough for a valid document and not enough for a publishable one.
	VEX *saga.VEXConfig
	// Components breaks the verdict down by the part of the application it belongs to, when
	// there is more than one. Optional: a caller that does not compute it renders as before.
	Components []ComponentVerdict
	// Exploitability describes the feeds that enriched this run's severities, if any. Empty
	// when no enrichment was configured, in which case nothing about it is rendered.
	//
	// A report that raised a finding to critical has to be able to say on what data, obtained
	// when. "KEV said so" is not reproducible; "on KEV as of 2026-08-01" is.
	Exploitability []FeedProvenance
	// Repositories is which repository each scan read, and at which commit. Derived from what the
	// scanners recorded rather than from the descriptor, because the descriptor usually names no
	// revision at all. And "the default branch" is not something a reader can check out.
	Repositories []RepositoryProvenance
	// Tools records the build of each external scanner the run used, and whether Draugr can
	// vouch for it. Empty when nothing external ran.
	//
	// A scan runs whatever is on PATH, which is right. An operator may have an experimental build or a
	// fork, and blocking them would be Draugr mistaking "I cannot verify this" for "this is wrong".
	// But a report that cannot say which build produced its findings cannot be reproduced, so the
	// answer is to record it.
	Tools []ToolBuild
	// UnattributedFindings counts findings that belong to no component, a project-scoped control like
	// `infrastructure` produces them. Reported alongside the component breakdown, because a breakdown
	// that silently omits them makes the parts look like the whole.
	UnattributedFindings int
}

Data is everything a reporter needs to render a scan.

func (Data) GateForReport added in v0.116.0

func (d Data) GateForReport() *skald.Gate

GateForReport is the run's gate in the shape the JSON document states it, for a caller writing that document through skald rather than through this package's reporters.

func (Data) ProjectName added in v0.106.0

func (d Data) ProjectName() string

ProjectName is which project this run belongs to.

The one accessor. Everything that renders the name of the thing being scanned reads this, which is why the field it used to fall back to could be removed in one place rather than in thirty.

type FeedProvenance added in v0.56.0

type FeedProvenance struct {
	// Name is the signal: "kev" or "epss".
	Name string
	// URL is where the copy came from. Empty for a file the operator supplied, which is its own useful
	// statement. The data was brought in by hand.
	URL string
	// FetchedAt is when it was obtained. Zero for a file path, which has no fetch to record.
	FetchedAt time.Time
	// SHA256 is the digest of the bytes that were read.
	SHA256 string
	// Stale reports that the copy was older than the run's configured maxAge. Recorded here and
	// not only warned about in the logs, because the logs of the run that produced a report are
	// exactly where nobody looks six weeks later.
	Stale bool
}

FeedProvenance is one exploitability dataset as this run saw it.

type Gap added in v0.119.0

type Gap struct {
	// Component and Surface name what was declared and went unexamined.
	Component, Surface string
	// Controls are the ones that would have looked at it, every one of them off.
	Controls []string
}

Gap is one surface a descriptor declares that no enabled control looks at.

Carried in the report rather than left to whoever renders it, because a run that examined everything except the thing a component exposes to the internet reads exactly like one that examined everything, and the difference is the descriptor's own words.

type GateSettings added in v0.97.0

type GateSettings struct {
	// Threshold is the severity band that fails a control by default.
	Threshold sarif.Severity
	// PerControl overrides Threshold for named controls, on a severity gate.
	PerControl map[string]sarif.Severity
	// FailOnPriority additionally fails on a priority band, when set.
	FailOnPriority string
	// PerControlBand overrides the band for named controls, on a band gate. The other half of
	// PerControl: a threshold only applies in the vocabulary its gate asks in, so exactly one of
	// these is ever populated.
	PerControlBand map[string]string
	// Disabled is --no-gate: the verdict is reported and the command still exits 0.
	Disabled bool
}

GateSettings is the policy a verdict was produced under, as the report states it.

Held here rather than recomputed from the Saga: the effective gate is the descriptor, the configuration and the flags resolved together, and a reporter deriving it a second time is how the report and the verdict come to disagree about the run they describe.

type Reporter

type Reporter interface {
	Format() string
	Render(w io.Writer, d Data) error
}

Reporter renders Data in one format.

func For

func For(format string) (Reporter, error)

For returns the reporter for a format name.

type RepositoryProvenance added in v0.64.0

type RepositoryProvenance = sarif.RepositoryRef

RepositoryProvenance is one repository as this run read it.

An alias rather than a copy: the JSON report is rendered by pkg/skald, which cannot import this package, and two structs that must agree eventually will not.

func RepositoriesFrom added in v0.64.0

func RepositoriesFrom(run engine.Result) []RepositoryProvenance

RepositoriesFrom collects which repository each scan read, and at which commit.

type Scope added in v0.71.0

type Scope struct {
	// Components and Controls are what the caller asked for; an empty list means that axis was
	// not restricted.
	Components []string `json:"components,omitempty"`
	Controls   []string `json:"controls,omitempty"`
	// SkippedComponents are declared components this run did not scan.
	//
	// Named rather than counted: "10 not scanned" tells a reader they are missing something and
	// not which thing, and the answer is one they already have.
	SkippedComponents []string `json:"skippedComponents,omitempty"`
}

Scope is what a run was narrowed to, and what it therefore did not cover.

A scoped run is a real verdict about a real subset, which is why it still gates. It is only dangerous when it is indistinguishable from a whole one, so this travels into every artifact the run produces, and every rendering says what was left out.

type Suggestion added in v0.119.0

type Suggestion struct {
	// What to type, or the key to add to the descriptor.
	What string
	// Why, in a clause. Not an instruction: none of this is required, and a reader who already has
	// their answer should be able to pass over the whole block.
	Why string
}

Suggestion is something a reader may try, and the reason it is worth trying.

Two fields rather than a sentence. A sentence naming a flag has to be read whole to find out whether it applies, where a column of flags is scanned for the one that does.

type TemplateFinding added in v0.21.0

type TemplateFinding struct {
	Priority string
	Level    string
	Score    string
	Control  string
	Tool     string
	RuleID   string
	Message  string
	Location string
}

TemplateFinding is one finding as exposed to a template.

type TemplateView added in v0.21.0

type TemplateView struct {
	// Project is what the run is about. Exposed beside the release because a release carries only
	// a version now, and a template with nothing but that renders "1.4.0" over and over with no
	// statement about what is at 1.4.0.
	Project    string
	Release    saga.Release
	Verdict    string // "pass" | "fail"
	Pass       bool
	Priorities struct{ P1, P2, P3, P4 int }
	Controls   []norn.ControlOutcome
	Findings   []TemplateFinding // ranked most-urgent first
}

TemplateView is the data model a "template" report renders against. It is the documented, stable surface for custom templates, a friendly, flattened view of a scan (no engine internals), leading with the verdict and prioritized findings.

type ToolBuild added in v0.63.0

type ToolBuild struct {
	// Name is the executable, e.g. "trivy".
	Name string
	// Version is what it reports, or what Draugr recorded when it installed it.
	Version string
	// Level is how strongly Draugr can vouch for this build: pinned, signed, checksum, unverified, or
	// external. Not a boolean, because those are genuinely different claims, an unsigned checksum
	// proves the download was not corrupted without proving upstream published it, and collapsing that
	// into "unattested" discards a difference a reader may care about.
	Level string
	// Reason renders the level for someone who has not read its definition.
	Reason string
}

ToolBuild is the build of one external scanner, as this run found it.

type View added in v0.119.0

type View string

View is what a report shows and how densely.

One setting rather than a shape flag and a density flag that have to be combined. The three are answers to one question, what does this reader want to see, and as two flags four combinations exist of which two mean the same thing. It also gives the machine formats the same word: a consumer that acts on a report rather than reading it asks for the same view as somebody who already knows what they are looking at.

const (
	// ViewFindings is a row per finding with what it is, and what argued with its band, underneath.
	//
	// The default, and the fuller one, for the reason it carries any reasoning at all: a reader
	// meeting a ranking for the first time is deciding whether to believe it, and the line under
	// each row is where a band accounts for itself.
	ViewFindings View = "findings"
	// ViewActions is a row per thing to do, each saying how many findings it clears.
	//
	// A reader deciding what to spend an afternoon on is choosing between actions, and a list of
	// findings makes them do that grouping in their head. Not the default, because a list of fixes
	// above the controls that produced them reads as instructions from a tool the reader has not
	// yet decided to trust.
	ViewActions View = "actions"
	// ViewCompact is one line per finding, and in json and sarif no indentation and no relayed
	// rule prose.
	//
	// For a reader who already knows what they are looking at and is asking how much there is, and
	// for a consumer that parses rather than reads.
	ViewCompact View = "compact"
)

The views. Findings is the default, and is the one that explains itself.

Jump to

Keyboard shortcuts

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