Documentation
¶
Overview ¶
Package saga defines the Draugr descriptor ("Saga"), the declarative account of an application's security surface (repositories, images, hosts, infrastructure) plus the controller configuration that drives a scan. It also handles parsing, validation, env-var substitution, and assembly of distributed meta-sources.
See docs/ARCHITECTURE.md and docs/naming.md.
Index ¶
- Constants
- Variables
- func AnnotateExposures(data []byte, reasons map[string]string) ([]byte, error)
- func GlobMatch(pattern, rel string) bool
- func Marshal(doc any) ([]byte, error)
- func Merge(model *Model, frag Fragment)
- func NarrowsScope(model *Model, frag Fragment) []string
- func NarrowsScopeIn(components []Component, frag Fragment) []string
- func OrList[T ~string](values []T) string
- func SchemaURLFor(version string) string
- func ValidInfrastructureKind(kind string) bool
- func ValidVEXJustification(j string) bool
- func ValidVEXStatus(s string) bool
- func WriteClassifications(data []byte, class map[string]Classification) ([]byte, error)
- type BuiltBy
- type Classification
- type Component
- type Config
- type ControllerSettings
- type Criticality
- type EffectPermissions
- type ExcludeRule
- type ExploitabilityConfig
- type Exposure
- type Fetcher
- type Fragment
- type FragmentConfig
- type FragmentRef
- type GateConfig
- type GateKind
- type Host
- type HostAuth
- type HostSpec
- type Image
- type Infrastructure
- type Model
- type OperatedBy
- type PublisherConfig
- type ReachabilityConfig
- type Reference
- type Release
- type ReportConfig
- type Repository
- type Resolved
- type SBOMConfig
- type SBOMFormat
- type SBOMScope
- type Source
- type VEXConfig
- type VEXDecision
- type VEXRepository
- type VEXSource
Constants ¶
const ( // FeedSourceCache reads the cache and never reaches the network. FeedSourceCache = "cache" // FeedSourceAuto reads the cache, fetching when it is missing or stale. FeedSourceAuto = "auto" )
ExploitabilitySources are the values KEV and EPSS accept besides a file path.
const ( // VEXNotAffected claims the vulnerability is present but cannot affect this product. VEXNotAffected = "not_affected" // VEXAffected admits the product is affected and something should be done about it. VEXAffected = "affected" // VEXFixed claims a remediation has already been applied. VEXFixed = "fixed" // VEXUnderInvestigation says the question is open. Draugr emits this for findings nobody has // triaged; it is not a value an exclusion may declare. VEXUnderInvestigation = "under_investigation" )
The VEX statuses a suppression may declare.
const DefaultGateBand = "P1"
DefaultGateBand is the gate in force when a descriptor writes none.
Here as well as in the policy engine because a descriptor's meaning has to be readable from the descriptor's own package: a per-control threshold refines the gate whether or not one was written, and the check that says so cannot ask a package that reads descriptors. TestTheDefaultGateIsOneValue holds the two in step.
const FragmentSchemaURL = schemaBaseURL + "/draugr.saga-fragment.schema.json"
FragmentSchemaURL is the published fragment schema tracking the latest release.
const Indent = 2
Indent is how many spaces a Saga is written with.
Shared because several commands write the same file, a survey creates it, `classify` sets exposure and criticality in place, `validate --resolved` prints it merged, and each one that picks its own indent reindents the whole document as a side effect of changing two fields. A two-field edit that rewrites sixty lines is a diff nobody can review, and the encoder's default is not a decision anyone made.
const SchemaURL = schemaBaseURL + "/draugr.saga.schema.json"
SchemaURL is the schema that tracks the latest release. Use it to follow along with new Draugr versions; use SchemaURLFor to stay matched to a specific one.
Variables ¶
var BuiltByValues = []BuiltBy{BuiltBySelf, BuiltByUpstream}
BuiltByValues are the values builtBy accepts.
var Criticalities = []Criticality{CriticalityCritical, CriticalityImportant, CriticalitySupporting}
Criticalities lists the valid criticality levels, most to least critical.
var Exposures = []Exposure{ExposurePublic, ExposureAuthenticated, ExposureInternal, ExposureRestricted}
Exposures lists the valid exposure levels, most to least exposed.
var FragmentSchemaJSON []byte
FragmentSchemaJSON is the Saga fragment's JSON Schema, embedded for the same reason.
A separate document because a fragment is a different shape: it has no `release:`, and it may not carry policy. Validating one against the Saga's schema would report a missing release on every valid fragment, which is why the two file types are distinguishable by name.
var GateThresholds = sarif.Severities
GateThresholds lists the severity bands a gate may be set to, most to least severe.
The bands the report prints, so a threshold reads the same as the counts beside it. The SARIF levels a gate used to take. Error, warning, note. Are still accepted and mapped onto the band each one means, so a descriptor written against the older vocabulary keeps working.
var InfrastructureKinds = []string{"kubernetes"}
InfrastructureKinds are the surfaces Draugr audits, and the only values `kind` accepts.
Listed rather than left open because a kind nothing serves is dropped when jobs are planned, so a component declaring `kind: k8s` is scanned for everything except the infrastructure it named, and reads as covered. The same argument as `operatedBy` beside it: the run looks the same either way.
var OperatedByValues = []OperatedBy{OperatedBySelf, OperatedByProvider}
OperatedByValues are the values operatedBy accepts.
var Priorities = []string{"P1", "P2", "P3", "P4"}
Priorities lists the priority bands a report may be narrowed to, most to least urgent.
Spelled here rather than taken from pkg/prioritization, which imports this package. The descriptor is a leaf, and a cycle to share four constants is a poor trade.
var SBOMFormats = []SBOMFormat{SBOMCycloneDXJSON, SBOMCycloneDXXML, SBOMSPDXJSON, SBOMSPDXTagValue}
SBOMFormats lists the valid SBOM document formats.
var SBOMScopes = []SBOMScope{SBOMScopeComponent, SBOMScopeProject, SBOMScopeBoth}
SBOMScopes lists the valid scopes.
var SchemaJSON []byte
SchemaJSON is the Saga's JSON Schema, embedded so the binary always carries the schema it actually enforces. That makes `draugr schema` exact, no network, no version guessing. And is what lets an air-gapped or pinned setup validate against precisely this build.
var VEXJustifications = []string{
"component_not_present",
"vulnerable_code_not_present",
"vulnerable_code_not_in_execute_path",
"vulnerable_code_cannot_be_controlled_by_adversary",
"inline_mitigations_already_exist",
}
VEXJustifications is VEX's fixed vocabulary for why a product is not affected.
var VEXStatuses = []string{VEXNotAffected, VEXAffected, VEXFixed}
VEXStatuses are the statuses an exclusion may declare.
Functions ¶
func AnnotateExposures ¶ added in v0.81.0
AnnotateExposures adds a trailing comment to each named component's `exposure`, saying what the value was read from.
A proposed exposure and a decided one are the same three characters in a file, and the value decides whether a finding is reported as P1 or P3. The survey says which ones it guessed on the way out. But that is a terminal that scrolls, and the review happens later, in an editor, by someone who may not have run the command. The reason has to be where the value is:
exposure: public # an Ingress routes into it
Only components in reasons are touched, so a value somebody decided is left without a comment rather than described as a guess. Written through the YAML node tree, so nothing else in the document moves.
func GlobMatch ¶ added in v0.69.0
GlobMatch reports whether rel matches pattern, with `**` crossing separators.
One glob dialect for the whole descriptor: `paths:`, `ignore:` and `fragments:` all mean the same thing by the same pattern. path.Match handles a single segment; `**` is split on and each side matched around it, so `**/testdata/**` and `vendor/**` both behave the way the person writing them expects.
Lives here rather than beside either caller because the dialect is part of the Saga language. It is what a descriptor means by a pattern, so it belongs with the rest of the descriptor's definition. Two copies would be two dialects the moment one of them was fixed.
func Marshal ¶ added in v0.98.0
Marshal renders a descriptor as YAML, at the indentation Draugr writes.
Here rather than beside any one caller, because the indent is the whole point and every writer has to agree on it. yaml.Marshal's default is four spaces, which is not a choice anybody made, and a file written with it is reindented end to end the first time something else edits a field in it, turning a one-line change into a whole-file diff nobody can review.
func Merge ¶ added in v0.69.0
Merge folds a fragment into a model: components by name, exclusions appended.
Components upsert and union rather than replace, so two fragments describing one component, a shared one naming its repository and a per-product one adding its image, end up as a single component with both. That is the same merge a Surveyor's fragment goes through.
func NarrowsScope ¶ added in v0.74.0
NarrowsScope reports whether merging frag into model would have narrowed an infrastructure target's namespaces, had the wider scope not been kept.
Exported so the command that merges can say so. The merge itself keeps the wider scope, which is the safe answer and also the surprising one: somebody who passed `--namespace` is entitled to know their flag did not reach the descriptor.
func NarrowsScopeIn ¶ added in v0.81.0
NarrowsScopeIn is NarrowsScope against a bare component list, for a document that is not a Model, a fragment merges into a fragment, and the question is the same either way.
func OrList ¶ added in v0.124.0
OrList is orList for callers outside this package, so one phrasing serves every place a closed vocabulary is offered back to somebody who missed it.
func SchemaURLFor ¶ added in v0.33.0
SchemaURLFor returns the published schema for a specific Draugr version, so an editor validates a Saga against the same rules the installed binary applies. Unreleased builds ("dev", or an empty version) have no published copy, so they fall back to the latest.
func ValidInfrastructureKind ¶ added in v0.116.0
ValidInfrastructureKind reports whether a kind is one Draugr audits.
func ValidVEXJustification ¶ added in v0.68.0
ValidVEXJustification reports whether j is one of VEX's not_affected justifications.
func ValidVEXStatus ¶ added in v0.68.0
ValidVEXStatus reports whether s is a status an exclusion may declare.
func WriteClassifications ¶ added in v0.6.0
func WriteClassifications(data []byte, class map[string]Classification) ([]byte, error)
WriteClassifications sets each named component's exposure and criticality in the raw Saga bytes and returns the updated document. It operates on the parsed YAML nodes, so comments and ${{ VAR }} tokens are preserved (values are not substituted); indentation is normalized to two spaces. Components not present in class are left untouched. New keys are inserted right after the component's name for readability; existing values are updated in place.
Types ¶
type BuiltBy ¶ added in v0.96.0
type BuiltBy string
BuiltBy says who publishes a thing Draugr scans: a repository, an image, or every target on a component at once.
const ( // BuiltBySelf is the default: this team builds it, so a package inside it is theirs to // upgrade. BuiltBySelf BuiltBy = "self" // BuiltByUpstream is an image this team runs and somebody else publishes. Upstream rather // than "vendor": the publisher is as often an open-source project as a company. BuiltByUpstream BuiltBy = "upstream" )
Who builds an image.
type Classification ¶ added in v0.6.0
type Classification struct {
Exposure Exposure
Criticality Criticality
}
Classification is a component's risk tags.
type Component ¶
type Component struct {
Name string `yaml:"name"`
Labels map[string]string `yaml:"labels,omitempty"`
Exposure Exposure `yaml:"exposure,omitempty"`
Criticality Criticality `yaml:"criticality,omitempty"`
// BuiltBy is who publishes this component's targets, unless one of them says otherwise.
//
// Here as well as on each target because a component that is entirely somebody else's software, a
// vendor console, an open-source service you run from source. Otherwise needs the field written
// on every repository and every image, and a target added later silently defaults back to `self`.
BuiltBy BuiltBy `yaml:"builtBy,omitempty"`
Repositories []Repository `yaml:"repositories,omitempty"`
Images []Image `yaml:"images,omitempty"`
Hosts []Host `yaml:"hosts,omitempty"`
Infrastructure []Infrastructure `yaml:"infrastructure,omitempty"`
// Controls overrides the project's per-control configuration for this component.
Controls map[string]ControllerSettings `yaml:"controls,omitempty"`
// Controllers is the older spelling, still read. Deprecated: write `controls`.
Controllers map[string]ControllerSettings `yaml:"controllers,omitempty"`
// VEX are exploitability claims somebody else made about this component, read and applied to
// its findings.
//
// Scoped to the component rather than the descriptor, and that is the whole safety argument:
// a claim is an assertion by one supplier about one artifact, and a document that could reach
// another component's findings would turn one vendor's assurance into a suppression somewhere
// nobody was looking.
//
// Note the asymmetry with `config.vex`, which is not a mistake: that describes the document
// Draugr **writes** about your product, this lists documents Draugr **reads** about a supplier's.
// A component does not author claims about itself.
VEX []VEXSource `yaml:"vex,omitempty"`
}
Component is one logical part of an application: its repositories, images, hosts, and infrastructure, plus optional per-component controller overrides and risk classification.
func UpsertComponent ¶ added in v0.69.0
UpsertComponent appends comp, or unions its surface into an existing same-named one.
The overlay that makes split descriptors work: a shared fragment naming a component's repository and a per-product fragment adding its image end up as one component holding both.
Scalars come from the component already present, so the first description of a component wins on name, classification and labels. That is why a resolution starts from the root descriptor. It keeps the file someone opened authoritative about how exposed a component is, rather than letting a fragment merged later quietly reclassify it.
func (Component) ControllerEnabled ¶
ControllerEnabled reports whether the named controller is enabled for this component, falling back to the project-level setting when the component has no override.
func (Component) PublishedBy ¶ added in v0.112.0
func (comp Component) PublishedBy(repo Repository) BuiltBy
PublishedBy resolves who publishes a repository: what it declares, else what its component declares, else self.
Most specific wins, which is the rule `controllers:` already follows. One answer to "how do overrides work" rather than one per field.
func (Component) PublishesImage ¶ added in v0.112.0
PublishesImage resolves the same for an image.
type Config ¶
type Config struct {
// Controls is the per-control configuration: which of them run, and what each is told.
//
// Named for what a reader meets everywhere else. `draugr controls` lists them, the catalog
// names them, every concept behind a `?` calls them controls, and `config.gate.controls`
// seventeen lines from here already did. A controller is the Go type that plans the jobs, and
// that word belongs in the code rather than in the file people write.
Controls map[string]ControllerSettings `yaml:"controls,omitempty"`
// Controllers is the older spelling, still read so no descriptor breaks.
//
// Deprecated: write `controls`. Folded into Controls when a descriptor loads, so nothing else
// has to know both names.
Controllers map[string]ControllerSettings `yaml:"controllers,omitempty"`
// Publishers are the destinations that rendered reports are delivered to.
Publishers []PublisherConfig `yaml:"publishers,omitempty"`
// Gate tunes the pass/fail thresholds. Policy belongs in the descriptor rather than in a
// flag every pipeline has to remember to pass.
Gate *GateConfig `yaml:"gate,omitempty"`
// Exclude suppresses findings that match, with a stated reason. Suppressed findings are still
// reported. They just stop counting toward the verdict.
Exclude []ExcludeRule `yaml:"exclude,omitempty"`
// VEX names the author and product for a generated VEX document (`--report vex`). Optional;
// without it Draugr falls back to the release, which produces a valid document rather than a
// publishable one.
VEX *VEXConfig `yaml:"vex,omitempty"`
// VEXSources are exploitability claims applied to every component in the project.
//
// The project-wide half of `components[].vex`, for the case that is common inside one
// organization: a platform team publishes one document covering everything they ship, and the
// twelve projects consuming it should say so once rather than repeat a URL on every component.
//
// Scoping is still done by the document. A statement names the package it is about, so a
// project-wide source excuses a finding only where the identifiers match, listing it here widens
// which findings are *considered*, not what a given statement is allowed to claim.
VEXSources []VEXSource `yaml:"vexSources,omitempty"`
// SBOM turns on Software Bill of Materials generation for this project's repositories and
// images. It is evidence rather than a control: an SBOM is an inventory, it finds nothing,
// and it never affects the verdict.
SBOM *SBOMConfig `yaml:"sbom,omitempty"`
// Exploitability raises a finding's severity by real-world signals, CISA KEV and FIRST EPSS.
// Before it is ranked, so "what to fix first" reflects what is being exploited rather than only
// what could be.
Exploitability *ExploitabilityConfig `yaml:"exploitability,omitempty"`
// Reachability ranks a dependency finding down when this project's code cannot reach the
// vulnerable part of it, so "what to fix first" reflects what the code actually calls.
//
// Exploitability's mirror image, and here for the same reason: both decide how findings are
// ranked, and a decision that can move a finding across the gate belongs somewhere a team
// reviews rather than in a list of tools to run.
Reachability *ReachabilityConfig `yaml:"reachability,omitempty"`
// AllowEffects acknowledges scanner effects that would otherwise stop a run, the kinds a scanner
// declares when it does more to a target than read it ("mutate", "privilege").
//
// In the descriptor rather than only a flag, because it is a decision about what may be
// done to your systems: reviewed in a pull request, and applied identically by every
// pipeline instead of remembered by whoever wrote the workflow.
AllowEffects EffectPermissions `yaml:"allowEffects,omitempty"`
}
Config holds global, per-controller configuration. Each controller's config tree is free-form (scanner-specific keys live under it); use ControllerEnabled to read the common "enabled" flag.
func (Config) ControllerEnabled ¶
ControllerEnabled reports whether the named controller is enabled at the project level. A controller is enabled when its config entry exists and its "enabled" key is not explicitly false. Absent entries are considered disabled.
type ControllerSettings ¶
ControllerSettings is a free-form configuration tree for one controller.
type Criticality ¶ added in v0.5.0
type Criticality string
Criticality is a component's business-criticality level, the operational impact if it fails or is compromised. It is the other axis of risk prioritization and is always human-declared, as it cannot be inferred from code. The levels are a fixed ladder with org-defined meaning. See docs/concepts.md (prioritization).
const ( CriticalityCritical Criticality = "critical" // failure causes outage or data loss CriticalityImportant Criticality = "important" // degraded functionality, no immediate outage CriticalitySupporting Criticality = "supporting" // limited operational impact )
Criticality levels, from most to least critical.
func (Criticality) Valid ¶ added in v0.5.0
func (c Criticality) Valid() bool
Valid reports whether c is a known criticality level. The empty value (unclassified) is not valid here; callers decide how to treat unset criticality.
type EffectPermissions ¶ added in v0.106.0
type EffectPermissions []string
EffectPermissions is which scanner effects a descriptor accepts.
One shape: a list, applying to everything the descriptor points at.
allowEffects: [network]
It was briefly also a mapping of environment to effects, so one descriptor could permit an intrusive scan of one target and refuse it for another. That put two shapes behind one key and made the strictness of the permission depend on which one an author had reached for. A descriptor describes one set of things a scan may do; a different answer is a different descriptor, which is also a separate file to review and a separate run to point at something.
func (EffectPermissions) Empty ¶ added in v0.106.0
func (p EffectPermissions) Empty() bool
Empty reports whether nothing was accepted at all.
func (*EffectPermissions) UnmarshalYAML ¶ added in v0.106.0
func (p *EffectPermissions) UnmarshalYAML(node *yaml.Node) error
UnmarshalYAML accepts a list, and refuses a mapping by name.
It exists only to refuse. A mapping of environment to effects used to parse here, so leaving it to the default decoder means somebody who wrote a correct descriptor last week gets "cannot unmarshal !!map into saga.EffectPermissions", a type error about a Go type they have never heard of, for a shape our own documentation told them to write.
type ExcludeRule ¶ added in v0.42.0
type ExcludeRule struct {
// Paths matches the finding's location. A pattern ending in "/" matches everything beneath
// that directory; otherwise it is a glob (path.Match) against the whole location, so
// "*.md" and "test/fixture.go" both work.
Paths []string `yaml:"paths,omitempty"`
// Rules matches the finding's rule id. `*` is a wildcard for any run of characters, including
// separators. A rule id is an opaque string rather than a path, and the ids that most need
// matching are compound (`license/GPL-3.0-only/github.com/somelib/thing`), so a wildcard that
// stopped at `/` could not express "this license, any package". A pattern with no `*` matches
// exactly. There is no escape for a literal `*`; no scanner emits one.
Rules []string `yaml:"rules,omitempty"`
// Reason is why this exclusion exists. Required.
Reason string `yaml:"reason"`
// AcceptedBy names who decided this finding was acceptable.
//
// The question an auditor asks of a suppression is not whether the scanner ran. It is who
// decided, and when. `reason` answers why; without this the who lives in prose if it is recorded
// at all, and a name buried in a sentence cannot be reported on. Optional, and a suppression
// without one is reported as unattributed rather than rejected.
AcceptedBy string `yaml:"acceptedBy,omitempty"`
// Expires is the date this exclusion stops applying, as YYYY-MM-DD.
//
// An exclusion accepted "until the upstream fix lands" has nothing that brings the finding back,
// so the temporary ones become permanent by default. Which is how a suppression mechanism decays
// into a way of never seeing something again. Past this date the exclusion no longer suppresses
// and the finding returns, with the report saying it lapsed rather than silently producing a
// finding that used to be accepted.
Expires string `yaml:"expires,omitempty"`
// VEX states what this suppression means as a machine-readable claim about the product,
// for `--report vex`. Optional: without it the finding is reported as `affected`, which is
// the claim that is never an overstatement.
VEX *VEXDecision `yaml:"vex,omitempty"`
// Source is the file this rule was read from, set by the loader rather than by the
// descriptor. Splitting exclusions across files is only safe if the report can still say
// which file authorized each one, so the provenance travels with the rule.
Source string `yaml:"-"`
}
ExcludeRule suppresses findings that match it. Every real repository has paths that are not the application, fixtures, examples, generated code. And rules that do not apply to them.
Two properties are deliberate. A reason is **required**, so the why is in the diff where a reviewer sees it rather than in someone's memory. And a matched finding is *suppressed*, not deleted: it stays in the report marked with its justification, so an exclusion is auditable and cannot become a blind spot nobody can see.
func (ExcludeRule) ExpiredOn ¶ added in v0.51.0
func (e ExcludeRule) ExpiredOn(now time.Time) bool
ExpiredOn reports whether this exclusion has lapsed as of the given day.
Compared by date rather than by instant: an exclusion set to expire on the 14th applies throughout the 14th and stops on the 15th, which is what a reader of the descriptor expects from a date with no time on it.
func (ExcludeRule) Matches ¶ added in v0.42.0
func (e ExcludeRule) Matches(uri, ruleID string) bool
Matches reports whether a finding at uri with rule id ruleID falls under this exclusion.
When both selectors are set they must both match. That is the narrow reading, "this rule, in this place". And it is the safe one: the alternative would silently widen "ignore the test fixture's fake key" into "ignore that rule everywhere".
type ExploitabilityConfig ¶ added in v0.56.0
type ExploitabilityConfig struct {
// KEV and EPSS each name a source: a file path, "cache" to read what `draugr feeds update`
// left without touching the network, or "auto" to fetch when the cache is missing or stale.
// Empty leaves that signal off; either may be used without the other.
KEV string `yaml:"kev,omitempty"`
EPSS string `yaml:"epss,omitempty"`
// EPSSThreshold is the EPSS probability (0–1) at or above which a finding's severity is
// raised one band. Zero means the CLI default.
//
// A pointer so "not set" and "deliberately zero" stay distinguishable: zero disables the
// EPSS bump entirely, which is a thing someone might mean.
EPSSThreshold *float64 `yaml:"epssThreshold,omitempty"`
// MaxAge is how old a cached feed may be before "auto" refetches it and a scan warns that
// it is stale. Empty means the built-in default of 24 hours, which tracks EPSS being
// republished daily.
//
// Configurable because a runner deliberately pinned to a known copy of the data has a legitimate
// reason to say "do not tell me it is old", reproducing last quarter's verdict requires last
// quarter's feed.
MaxAge string `yaml:"maxAge,omitempty"`
}
ExploitabilityConfig turns on severity enrichment from real-world exploitability signals and says where the data comes from.
In the descriptor rather than only in flags because it is a decision about how findings are ranked, and a team that agrees to use KEV needs somewhere to write that down where it gets reviewed, not a flag every pipeline has to remember to pass.
type Exposure ¶ added in v0.5.0
type Exposure string
Exposure is a component's risk-exposure level. How reachable it is to an attacker, and so how likely a weakness in it is to be hit. It is one axis of risk prioritization; higher exposure ranks a component's findings higher. The levels are a fixed ladder: an organization may redefine what each means, but not the count. Exposure may be proposed by a surveyor from topology and confirmed by a human. See docs/concepts.md (prioritization).
const ( ExposurePublic Exposure = "public" // internet-facing, no authentication ExposureAuthenticated Exposure = "authenticated" // internet-facing, behind authentication ExposureInternal Exposure = "internal" // reachable within the environment ExposureRestricted Exposure = "restricted" // namespace- / network-policy-scoped )
Exposure levels, from most to least exposed.
type Fetcher ¶ added in v0.69.0
type Fetcher interface {
// Fetch returns a directory holding the repository at the requested revision, the commit it
// resolved to, and a cleanup.
Fetch(url, revision string) (dir, resolved string, cleanup func(), err error)
}
Fetcher materializes a remote fragment reference as a local directory.
An interface because fetching means git, which lives in internal/ and cannot be imported from pkg/. The same shape as sbom.Generator: the package declares what it needs, and the wiring supplies something that can do it. A nil Fetcher makes a remote reference an error naming it, rather than a descriptor that quietly contains less than it says.
type Fragment ¶
type Fragment struct {
// Components are merged by name, a repeated name unions the two surfaces rather than replacing or
// colliding, so a component described in two places ends up whole.
Components []Component `yaml:"components,omitempty"`
// Config is the subset of a Saga's config a fragment may carry.
Config FragmentConfig `yaml:"config,omitempty"`
// Fragments are further fragments this one pulls in, resolved relative to it.
Fragments []FragmentRef `yaml:"fragments,omitempty"`
// ExposureReasons explains, per component name, what topology a proposed `exposure` was read
// from, "an Ingress routes into it", and so on.
//
// Never serialized: it is evidence about a proposal rather than part of the descriptor, and a
// fragment somebody writes by hand has no use for it. It exists so a survey can put the
// reasoning beside the value it wrote, where the value gets reviewed.
ExposureReasons map[string]string `yaml:"-" json:"-"`
}
Fragment is a partial Saga: what a `fragments:` entry loads, and what a Surveyor contributes.
One type for both because the merge is the same. A surveyor that discovers a cluster and a file a person wrote are both saying "here is some more of the application", and having two merges would mean two answers to what a repeated component name means.
func LoadFragment ¶ added in v0.69.0
LoadFragment parses a Saga fragment, substituting ${{ VAR }} from the environment.
Validated as a fragment rather than as a Saga. A fragment has no `release:` and that is correct, so checking it against the Saga's rules would reject every valid one; and a fragment that is only meaningful once merged is a fragment nobody can check on its own.
type FragmentConfig ¶ added in v0.69.0
type FragmentConfig struct {
// Exclude suppresses findings that match, with a stated reason. Appended to whatever the
// descriptor and other fragments already carry.
Exclude []ExcludeRule `yaml:"exclude,omitempty"`
}
FragmentConfig is the part of Config a fragment is allowed to set.
A separate type rather than a validated Config, so the restriction is enforced by the decoder and shows up in the published schema. An editor says `gate` is not allowed here, rather than the user finding out when a scan behaves unexpectedly.
type FragmentRef ¶ added in v0.69.0
type FragmentRef struct {
// Path selects the fragment files. Globs are the same dialect as `paths:` and `ignore:`, `*`
// within a segment, `**` across them, so `**/draugr.saga-fragment.yaml` collects one fragment
// from every component in a monorepo. Relative to the file that names it, so a fragment keeps
// working when its directory moves.
Path string `yaml:"path"`
// URL is a git repository to read the fragments from. Empty means the local filesystem.
URL string `yaml:"url,omitempty"`
// Revision is the branch, tag or commit to read. Required when URL is set, and deliberately
// not defaulted to the repository's default branch: a fragment that tracks a moving branch is
// a gate that changes with no commit in your own repository. The revision a run resolved to
// is recorded in the report, so a tag that moves is visible afterwards.
Revision string `yaml:"revision,omitempty"`
}
FragmentRef names Saga fragments to merge into the descriptor that lists it.
A fragment adds scope or adds attributed suppressions; it can never change policy. That is what makes splitting a descriptor safe to review: including a file cannot quietly lower the gate or switch a control off, so the worst a `fragments:` entry can do is add findings or add suppressions that are individually attributed and counted in the report.
func (FragmentRef) Remote ¶ added in v0.69.0
func (f FragmentRef) Remote() bool
Remote reports whether this reference reads from another repository.
func (FragmentRef) String ¶ added in v0.69.0
func (f FragmentRef) String() string
String names the reference the way an error should, so a message about one fragment among several says which.
type GateConfig ¶ added in v0.43.0
type GateConfig struct {
// FailOn is the threshold that fails the build, written in either vocabulary.
//
// `P1` asks what band a finding landed in for the component it was found in, folding in the
// exposure and criticality declared below. `high` asks what the scanner called the flaw on its
// own terms. One field rather than two, so writing both is not expressible: a verdict with two
// possible reasons cannot be read back to the rule that produced it, and a shape nobody can
// write is a better guarantee than an error about it.
//
// Empty is the default, which is P1. The ranking this product computes is the one it judges
// by, and a tool whose default gate is the number a scanner printed has its central claim
// switched off until somebody configures it.
FailOn string `yaml:"failOn,omitempty"`
// Controls sets a per-control threshold, overriding FailOn for that control only.
//
// The same vocabulary as FailOn, because the run asks one question: a band under a severity
// gate, or a severity under a band gate, is a second question asked of one control and puts
// the reader back where two keys left them.
//
// A refinement of FailOn, so it needs one.
//
// This exists because one threshold cannot serve every control. License policy is owned by
// legal and vulnerability policy by security; "fail the build on a forbidden license but
// only warn on a medium CVE" is a reasonable position that a single global threshold makes
// unsayable.
Controls map[string]string `yaml:"controls,omitempty"`
// FailOnPriority also fails the build on any finding at or above a priority band.
//
// Deprecated: write the band in FailOn instead, which takes either vocabulary. Still read, so
// a descriptor that predates the merge keeps working, and refused alongside FailOn because
// the two together are the contradiction one field exists to prevent.
//
// Severity rates a flaw in the abstract; priority folds in what the descriptor says about the
// component it was found in. A team that has classified its components usually wants the gate on
// the second, and until now could only say so with a flag. Which every pipeline has to remember,
// and which nothing reviews.
//
// In the descriptor for the same reason as the rest of this block: it is a decision about
// this application, reviewed in a pull request and applied identically by every runner. A
// --fail-on-priority flag still overrides it, so a stricter run stays possible without
// editing the file.
//
// This is the default gate. A descriptor that names nothing fails on P1, because the ranking
// this product computes is the one it should be judged by.
FailOnPriority string `yaml:"failOnPriority,omitempty"`
}
GateConfig tunes which findings fail the build.
func (*GateConfig) Resolved ¶ added in v0.116.0
func (g *GateConfig) Resolved() (GateKind, string)
Resolved is the gate this block asks for, from whichever spelling it was written in.
One accessor, because reading the older field is the whole point of keeping it and every caller doing it themselves is a caller that will forget the new one. That has already happened once: a reader of `failOnPriority` alone reported no gate at all for a descriptor that had written the band in `failOn`.
Validation refuses both together, so precedence here only decides what a descriptor that never reaches validation gets, and the current spelling wins.
type GateKind ¶ added in v0.116.0
type GateKind int
GateKind is which question a threshold asks.
type Host ¶
type Host struct {
Name string `yaml:"name"`
URL string `yaml:"url"`
Type string `yaml:"type,omitempty"`
// Auth authenticates the dynamic scan of this endpoint. Absent means probe it anonymously.
Auth *HostAuth `yaml:"auth,omitempty"`
// Spec drives the dynamic scan from an OpenAPI document instead of crawling.
Spec *HostSpec `yaml:"spec,omitempty"`
}
Host is a running endpoint. Type is "browser" (browser-facing UI) or "api" (programmatic); it tunes which security-header checks apply. Optional; defaults to "browser".
type HostAuth ¶ added in v0.93.0
type HostAuth struct {
// Type is "bearer", an `Authorization: Bearer <token>` header, or "header" for a named one.
Type string `yaml:"type"`
// Header is the header name, required when Type is "header" (e.g. X-API-Key).
Header string `yaml:"header,omitempty"`
// TokenEnv names the environment variable holding the credential.
TokenEnv string `yaml:"tokenEnv"`
}
HostAuth says how to authenticate to an endpoint, by naming the environment variable that holds the credential.
There is deliberately no field for the credential itself. A descriptor is committed, so a token written into one is a leaked token. And `secrets` would rightly flag it. Making the value inexpressible is a stronger guarantee than warning about it.
type HostSpec ¶ added in v0.93.0
type HostSpec struct {
// Path is the OpenAPI or Swagger document, resolved like every other path in a
// descriptor: relative to where Draugr runs, not to the descriptor or the repository.
Path string `yaml:"path"`
// Methods are the HTTP methods to exercise. Empty means GET and HEAD.
//
// A specification lists POST, PUT and DELETE too, and a scanner handed one will exercise them, a
// scan of a staging API that deletes its fixtures is a scan nobody runs twice. Naming a write
// method here is how that is accepted: explicit, per endpoint, and visible in review.
Methods []string `yaml:"methods,omitempty"`
}
HostSpec points the dynamic scan at an OpenAPI document describing this endpoint.
An API usually has no HTML to crawl, so probing it blind reaches whatever a scanner can guess. A specification lists every route and method the service declares, which turns guessing into exercising what is actually there.
type Image ¶
type Image struct {
Image string `yaml:"image"`
Digest string `yaml:"digest,omitempty"`
// BuiltBy says who builds this image: "self" (the default) or "upstream" for one this
// component runs but somebody else publishes.
//
// It decides what the report tells a reader to do about a vulnerable package inside it. Nobody
// running a scan can upgrade a library inside an image they do not build. The fix is a newer
// image, or a wait for whoever publishes it. Advice they cannot take, at the top of a list called
// "fix first", teaches them the list is not worth reading.
//
// Declared rather than detected, because nothing in an image says who built it. Defaults to
// "self" so a descriptor that says nothing keeps describing its own work, which is the common
// case for a hand-written one. A surveyed cluster is the case that needs saying.
BuiltBy BuiltBy `yaml:"builtBy,omitempty"`
}
Image is a container image reference. Digest is the immutable content digest ("sha256:…") of the image the tag pointed to; when present it makes result caching content-addressed (a rebuilt image under the same tag re-scans). A surveyor can capture the running digest, or you can pin it by hand for reproducible caches.
type Infrastructure ¶
type Infrastructure struct {
Kind string `yaml:"kind"`
Ref string `yaml:"ref,omitempty"`
// Namespaces narrows the audit to the namespaces this component owns. Empty means the whole
// cluster.
//
// On a shared cluster the cluster is not the unit anyone owns. Most of what the benchmark's
// policies section examines is namespace-scoped, so a team owning three namespaces of eighty
// otherwise receives seventy-seven namespaces' worth of findings it cannot act on. And a number
// that will never reach zero is a number people stop reading.
//
// It also fixes what the component's risk classification means. `exposure` and `criticality`
// describe a component, so declaring them against a whole shared cluster asserts them on
// everybody else's workloads too.
Namespaces []string `yaml:"namespaces,omitempty"`
// OperatedBy says who runs this surface: "self", or "provider" for a managed service.
//
// It states a fact rather than a judgement, and what follows from it. That a finding about the
// provider's half is not something this team can go and fix. Is derived rather than asserted.
// "managed" was the obvious word and is ambiguous: managed by whom, and a managed service is
// still yours to pay for.
//
// Declared rather than detected, because whether a cluster is managed is a fact about a
// contract and not something visible in what a scanner reads. The same argument that puts
// exposure and criticality here.
//
// It narrows what it excuses. On a managed cluster the provider runs the control plane, the
// API server and etcd; RBAC, Pod Security and network policy remain the team's, and those
// are usually the findings that matter. Marking a whole cluster as somebody else's problem
// would hide the half that is not.
OperatedBy OperatedBy `yaml:"operatedBy,omitempty"`
}
Infrastructure is an infrastructure surface. Kind is one of InfrastructureKinds; Ref names the concrete instance.
type Model ¶
type Model struct {
// Project is which project this descriptor describes, and the name a platform files its runs
// under. Lowercase letters, digits and dashes.
Project string `yaml:"project,omitempty"`
Release Release `yaml:"release"`
Config Config `yaml:"config,omitempty"`
Components []Component `yaml:"components,omitempty"`
Fragments []FragmentRef `yaml:"fragments,omitempty"`
References []Reference `yaml:"references,omitempty"`
}
Model is a parsed Saga descriptor, the declarative account of an application's security surface plus the controller configuration that drives a scan.
func Load ¶
Load parses a Saga descriptor from YAML bytes, substituting ${{ VAR }} references from the environment and validating the result.
func LoadFile ¶
LoadFile reads and parses a Saga descriptor, merging any local fragments it names.
Remote fragments need a Fetcher, which needs git, which lives in internal/, so a descriptor using one gets an error here naming it rather than a descriptor that quietly contains less than it says. Callers that can fetch use ResolveFile.
func (*Model) ExcludeWarnings ¶ added in v0.123.0
ExcludeWarnings reports exclusion paths that are legal, match nothing, and were almost certainly meant to name a directory. root is the directory the descriptor sits in; patterns are tested against it, and a tree that is not there produces nothing.
Warnings rather than errors. A pattern that matches nothing is legal and sometimes deliberate, written ahead of the file it covers, and refusing it would break a descriptor that is right about the future. What is not acceptable is saying nothing: the scan's own report names a dead rule as a footnote under a count, read after a scan somebody was running for another reason, and `draugr validate` is where a descriptor is checked and costs nothing to run.
The trap is that `config.exclude[].paths` matches inside one path segment unless the pattern ends in `/`, so `tests` and `tests*` both match the directory entry itself and nothing under it. On a real descriptor that was seventeen findings a team believed were excused and the gate was judging.
func (*Model) ProjectName ¶ added in v0.106.0
ProjectName is which project this descriptor describes.
Kept as an accessor now that it reads one field, because every call site went through it and the field it used to fall back to was removed in one place rather than in thirty.
type OperatedBy ¶ added in v0.96.0
type OperatedBy string
OperatedBy says who runs an infrastructure surface.
const ( // OperatedBySelf is the default: this team runs it, so every finding is theirs to act on. OperatedBySelf OperatedBy = "self" // OperatedByProvider is a managed service, where part of the surface is not reachable by // the team that owns the workloads on it. OperatedByProvider OperatedBy = "provider" )
Who operates a surface.
func (OperatedBy) Valid ¶ added in v0.96.0
func (o OperatedBy) Valid() bool
Valid reports whether the value is one Draugr defines.
type PublisherConfig ¶ added in v0.21.0
type PublisherConfig struct {
Kind string `yaml:"kind"`
Dir string `yaml:"dir,omitempty"` // file: output directory
// Reports narrows what this destination is given, and is where the two lists finally meet.
//
// Left out, a destination is handed every report `config.reports` renders, which is what a
// descriptor written before this meant and still means. That cross product is almost never
// what anybody wants: writing HTML and JSON to a directory while posting the markdown to a
// pull request was not expressible, so every destination got all three and picked out what it
// recognized.
//
// A format named here is rendered whether or not `config.reports` also names it, so a
// descriptor that publishes and keeps no local artifacts need not declare the same format
// twice. Rendering still happens once per distinct report, however many destinations ask for
// it.
Reports []ReportConfig `yaml:"reports,omitempty"`
// github / github-pr-comment: Repo defaults to $GITHUB_REPOSITORY; the token to $GITHUB_TOKEN
// (or TokenEnv). github: Commit/Ref default to $GITHUB_SHA / $GITHUB_REF.
Repo string `yaml:"repo,omitempty"`
Commit string `yaml:"commit,omitempty"`
Ref string `yaml:"ref,omitempty"`
TokenEnv string `yaml:"tokenEnv,omitempty"` // env var holding the token; default GITHUB_TOKEN
// github-pr-comment / azure-pr-comment: posts the markdown report as a sticky pull-request
// comment. PR defaults to the number parsed from $GITHUB_REF (refs/pull/<n>/merge) or
// $SYSTEM_PULLREQUEST_PULLREQUESTID; Marker identifies the sticky comment to update
// (default a Draugr marker).
PR int `yaml:"pr,omitempty"`
Marker string `yaml:"marker,omitempty"`
// azure-pr-comment: Org is the collection URI (default $SYSTEM_TEAMFOUNDATIONCOLLECTIONURI)
// and Project the team project (default $SYSTEM_TEAMPROJECT). Repo defaults to
// $BUILD_REPOSITORY_NAME and the token to $SYSTEM_ACCESSTOKEN (or TokenEnv).
Org string `yaml:"org,omitempty"`
Project string `yaml:"project,omitempty"`
// draugr-api: URL is where the server is reached, defaulting to $DRAUGR_API_URL. The token
// comes from $DRAUGR_API_TOKEN (or TokenEnv) and never from this file, which is one people
// commit.
URL string `yaml:"url,omitempty"`
// DefaultURL is what draugr.config.yaml said, carried here so a publisher can consult it last.
// Never read from a descriptor, `yaml:"-"`, so writing it in a Saga does nothing and the schema
// does not offer it.
//
// A separate field rather than filling URL, because the two sit at opposite ends of the
// precedence chain: URL is somebody's explicit choice for this project and beats the
// environment, while this is the organization's default and loses to it. Merging them would
// make an ambient value indistinguishable from an intentional one.
DefaultURL string `yaml:"-"`
}
PublisherConfig configures one destination for rendered reports. Kind selects the publisher (e.g. "file", "github"); the remaining fields are read by that publisher. Known kinds and their required fields are validated by the publishing layer (pkg/publish) when the scan runs.
Secrets are never stored here: the github publisher reads its token from an environment variable (TokenEnv, default GITHUB_TOKEN), not from the Saga.
type ReachabilityConfig ¶ added in v0.102.0
type ReachabilityConfig struct {
// Analyzers names the tools that decide reachability, e.g. "govulncheck". An analyzer is
// named rather than inferred so the descriptor says which tool reached the verdict, and so
// `draugr doctor` can tell you what to install before a scan finds out for you.
//
// Empty leaves reachability off, which is the same as omitting the block.
Analyzers []string `yaml:"analyzers,omitempty"`
}
ReachabilityConfig turns on reachability analysis and names the analyzers that do it.
Beside ExploitabilityConfig rather than in a control's scanner block, because the two are the same kind of decision pointing in opposite directions: one raises a finding's rank on evidence that it is being exploited, the other lowers it on evidence that this code cannot reach it. Both move findings across a gate, and both should be visible in a diff to whoever owns that gate.
It is also the honest surface. Every other entry in a control's scanner block adds findings, so enabling one there means "check this too". An analyzer named here adds none. It ranks findings already found, downward, which can turn a failing gate green. That is not something to discover from the reference docs after the fact.
type Reference ¶
Reference links a manual/human security control (e.g. threat model, architecture diagram).
type Release ¶
type Release struct {
Version string `yaml:"version"`
}
Release identifies what is being assessed. Its version, and nothing else: what a release is called is the project's name, and naming it twice let a descriptor state two.
type ReportConfig ¶ added in v0.21.0
type ReportConfig struct {
Format string `yaml:"format"`
// Template and TemplateFile supply the Go text/template for the "template" format (set
// exactly one). Ignored by other formats.
Template string `yaml:"template,omitempty"`
TemplateFile string `yaml:"templateFile,omitempty"`
// Filename overrides the artifact's default output filename (used by file-based publishers).
Filename string `yaml:"filename,omitempty"`
// MinPriority narrows this report to findings at or above a priority band, leaving every
// other report complete.
//
// Per report, because the reports answer to different readers. A SARIF upload becomes review
// comments somebody reads on every pull request, where a few hundred findings they did not
// cause is the reason nobody reads any of them; the JSON beside it is evidence, and evidence
// is not something to trim.
//
// Narrowing an artifact is otherwise refused, and for a good reason: a file that claims to be the
// scan and is not misleads whatever consumes it. What makes this different is that it is
// declared, written in the descriptor, and stated inside the artifact it produced. A scope
// somebody chose and can read back is a scope; an undeclared one is the problem.
MinPriority string `yaml:"minPriority,omitempty"`
}
ReportConfig selects one report format to render on a scan. Known formats are validated by the reporting layer (pkg/report) when the scan runs, not here. The Saga stays a leaf.
type Repository ¶
type Repository struct {
URL string `yaml:"url"`
Revision string `yaml:"revision,omitempty"`
// Paths restricts the scan to these directories. Empty scans the whole repository.
//
// Files at the repository root are always included regardless: manifests and the scanners' own
// configuration live there, and a tool that cannot see go.mod or .trivyignore does not fail. It
// reports less against a tree it did not fully understand, which is indistinguishable from a
// clean scan.
Paths []string `yaml:"paths,omitempty"`
// Ignore removes matching paths from the scan, applied after Paths so it can carve out of
// one. Gitignore-style: a trailing `/` is a directory, `*` matches within a path segment,
// `**` across them.
Ignore []string `yaml:"ignore,omitempty"`
// BuiltBy says who publishes this repository: "self" (the default) or "upstream" for one this
// component uses and somebody else maintains. Falls back to the component's own `builtBy`.
//
// It decides what the report tells a reader to do. A denied license in the dependency tree of a
// repository this team does not publish is not a license they chose and not one they can swap
// out: the answers are to stop using the component or to record an exception, and "change the
// code" is neither. The same holds for a vulnerable dependency, a flaw in the source, and a
// credential committed there. The declaration is about who can change the thing, which does not
// vary by what found the problem.
//
// Declared rather than detected, for the reason an image's is: a git remote is not a statement
// of ownership. Plenty of teams publish from a fork and plenty consume from one.
//
// It changes the action and nothing else. The finding keeps its severity and its band, a flaw in
// somebody else's software is exactly as dangerous. And it is still counted and still reaches the
// gate.
BuiltBy BuiltBy `yaml:"builtBy,omitempty"`
}
Repository is a source repository at a revision, optionally scoped to part of its tree.
type Resolved ¶ added in v0.69.0
Resolved is a descriptor with every fragment merged in, plus where each part came from.
func ResolveFile ¶ added in v0.69.0
ResolveFile loads a descriptor and merges every fragment it names.
The root is the merge base and fragments are applied after it, which is the opposite of the usual "most specific wins" layering and is deliberate. Components merge by union keeping the first value for scalars, so making the root the base is what keeps the file someone opened authoritative about a component's classification. Fragments are applied later and win less, which is right because they are additive.
func (*Resolved) Digest ¶ added in v0.106.0
Digest identifies the descriptor that actually ran: root and fragments merged, environment substituted, serialized canonically.
This is the fact worth having, and no single file carries it. Two runs with identical descriptors and different fragments produce different digests; two runs whose descriptors differ only in comments or key order produce the same one. Neither is true of a digest over the root file.
func (*Resolved) Effective ¶ added in v0.107.0
Effective is the descriptor that actually ran: root and fragments merged, environment substituted, serialized canonically.
The same bytes Digest is taken over, which is the point of returning them. A digest is only worth something to somebody who can reproduce it, and a reader holding this can, rather than being asked to trust that a number describes a file they cannot see.
It is the merged form, so it is not any file in the repository. Reading it answers "what did this run actually apply", which is a different question from "what is committed" and the one somebody asks when a finding was suppressed and they cannot see why.
type SBOMConfig ¶ added in v0.41.0
type SBOMConfig struct {
// Enabled generates one SBOM per distinct repository and image in the project.
Enabled bool `yaml:"enabled"`
// Format is the document format. Empty means SBOMCycloneDXJSON.
Format SBOMFormat `yaml:"format,omitempty"`
// Scope is what each document covers: one target, the whole project, or both. Empty means
// SBOMScopeComponent, which is the behavior a descriptor written before this field had.
Scope SBOMScope `yaml:"scope,omitempty"`
}
SBOMConfig turns on SBOM generation and chooses the document format.
type SBOMFormat ¶ added in v0.41.0
type SBOMFormat string
SBOMFormat is the document format for a generated SBOM. Both supported formats are open specifications that downstream tooling already reads.
const ( // SBOMCycloneDXJSON is the default: the OWASP format in JSON, ECMA-424, and the one that // composes. A CycloneDX document can carry nested components and describe how complete it is, // which is what a document covering a whole project needs. It is also the format security tooling // reads most readily, and the one VEX is expressed in. SBOMCycloneDXJSON SBOMFormat = "cyclonedx-json" // SBOMCycloneDXXML is CycloneDX in XML, which some enterprise tooling still expects. SBOMCycloneDXXML SBOMFormat = "cyclonedx-xml" // SBOMSPDXJSON is SPDX in JSON, ISO/IEC 5962, and what a procurement or license-compliance // process is most likely to ask for by name. SBOMSPDXJSON SBOMFormat = "spdx-json" // SBOMSPDXTagValue is SPDX in its original tag-value encoding, still required by some // compliance tooling. SBOMSPDXTagValue SBOMFormat = "spdx-tag-value" )
The SBOM document formats Draugr can emit: the two open specifications, each in both of its standard encodings. Which one you want is decided by whatever consumes the document, so the choice is yours rather than ours.
Syft can emit more (its own syft-json, GitHub's dependency-snapshot format, a bare PURL list), but those are either vendor-specific or not an SBOM. Keeping this list to the interchange formats means every document Draugr produces is one a third party can read.
func (SBOMFormat) Valid ¶ added in v0.41.0
func (f SBOMFormat) Valid() bool
Valid reports whether f is a known SBOM format. The empty value is not valid here; it means "the default" to callers, which resolve it before use.
type SBOMScope ¶ added in v0.68.0
type SBOMScope string
SBOMScope is what a generated SBOM document covers.
The distinction exists because an SBOM is requested per *product*, a customer questionnaire, EO 14028 and the CRA all ask for the bill of materials of the thing you shipped, while Draugr scans per repository and image. A project with four repositories and three images produces seven documents and no answer to the question being asked.
const ( // SBOMScopeComponent is the default: one document per distinct repository and image. SBOMScopeComponent SBOMScope = "component" // SBOMScopeProject is one document covering the whole release, assembled along the hierarchy // the Saga already declares. SBOMScopeProject SBOMScope = "project" // SBOMScopeBoth emits the per-target documents and the assembled one. The parts are the // evidence for the whole, and an auditor asking where a package came from wants both. SBOMScopeBoth SBOMScope = "both" )
The scopes a Saga may ask for.
func (SBOMScope) PerTarget ¶ added in v0.68.0
PerTarget reports whether this scope asks for the per-repository and per-image documents.
type Source ¶ added in v0.69.0
type Source struct {
// Path is the file, as written in the descriptor that named it (or the root's own path).
Path string
// URL is the repository it came from, empty for a local file.
URL string
// Revision is what the descriptor asked for, and Resolved is the commit that turned out to
// be. Both empty for a local file.
Revision string
Resolved string
// Root marks the descriptor the resolution started from.
Root bool
// Digest is the content digest of the file, before ${{ VAR }} substitution.
//
// Per source rather than only over the merged result, because "which files was this assembled
// from, and were they these ones" is the question an exclusion raises: a suppression arriving
// from a fragment somebody cannot pin is a decision with no author.
Digest string
}
Source is one file that contributed to a resolved descriptor.
Kept beside the merged Model rather than folded into it, so a report can say where each part came from. Splitting a descriptor is only safe if the result is still answerable. A suppression nobody can trace to a file is worse than one in a long file.
type VEXConfig ¶ added in v0.68.0
type VEXConfig struct {
// Author identifies who is making these statements: an organization, ideally with a way to reach
// them ("Example Ltd <security@example.com>"). Defaults to the release name, which is a project
// rather than a party. Enough to produce a valid document, not enough for one you would hand to
// a customer.
Author string `yaml:"author,omitempty"`
// Product identifies the thing the statements are about, as an IRI. A package URL is the
// conventional choice ("pkg:oci/example/api@sha256:…").
//
// Defaults to a purl built from the release name and version. Set it to whatever the SBOM
// a consumer holds calls this product: a VEX statement is matched to a component by
// identifier, so a document naming the product differently is one nothing will apply.
Product string `yaml:"product,omitempty"`
}
VEXConfig names the party making the claims in a generated VEX document, and the product they are about.
Both are optional and both are worth setting for a document you publish. A VEX document is an assertion by a supplier about their own product, so the two things a consumer needs are *who says so* and *what about*. Draugr can guess the second from the release and cannot guess the first at all. It knows a project name, not a legal entity or a contact.
type VEXDecision ¶ added in v0.68.0
type VEXDecision struct {
// Status is the claim: not_affected, affected, or fixed.
//
// under_investigation is deliberately not accepted here. It is what an untriaged finding already
// reports, and claiming it on a finding you have suppressed says two contradictory things. That
// the matter is open, and that it is settled.
Status string `yaml:"status"`
// Justification is why the product is not affected, from VEX's fixed vocabulary. Valid only
// with not_affected, where VEX requires either this or a prose statement; without it the
// exclusion's `reason` is emitted as the prose form instead.
//
// A closed vocabulary because the whole value of the field is that a consumer can act on it
// without reading English.
Justification string `yaml:"justification,omitempty"`
}
VEXDecision states what a suppression means in VEX terms. Optional, and absent is a perfectly good answer. See the status table below for what Draugr emits without it.
This is separate from `reason` because they answer different questions. A reason is written for a human reviewing the descriptor; a VEX status is a machine-readable claim about the product, which downstream consumers act on. Deriving one from the other would mean inferring a compliance statement from free text, and detection, severity and evidence in Draugr do not involve inference.
type VEXRepository ¶ added in v0.100.0
type VEXRepository struct {
// URL is the repository holding the document.
URL string `yaml:"url"`
// Ref is the branch, tag or commit to read. Empty takes the default branch.
//
// Worth pinning for a claim you are gating on: a branch moves, and a supplier revising their
// analysis would change what your gate accepts with nothing in your descriptor having
// changed. Either way the report records the commit actually read, so the run can be
// reproduced after the fact even when this was left open.
Ref string `yaml:"ref,omitempty"`
// Path is the document's path inside the repository.
Path string `yaml:"path"`
}
VEXRepository locates a VEX document inside a git repository.
Cloned with the same machinery as any other repository Draugr reads, which means it authenticates the same way: whatever credentials git already has on the machine, an SSH key, a credential helper, the header a CI checkout configured. Draugr holds no credentials of its own, which is why a private supplier repository works and why no token belongs in this descriptor.
type VEXSource ¶ added in v0.100.0
type VEXSource struct {
// Path is a document on disk, resolved **relative to where Draugr runs**, not to the descriptor,
// and not to the repository. That is the same rule every other path in a descriptor follows (see
// HostSpec.Path), and stating it here is deliberate: a path whose base a reader has to guess is
// one that works on a laptop and silently misses in CI.
Path string `yaml:"path,omitempty"`
// URL is a document to fetch over HTTPS. Fetched once per run and cached; the report records
// the URL, when it was fetched and the digest of what came back, so a run stays reproducible
// from its own evidence rather than from the network still agreeing later.
URL string `yaml:"url,omitempty"`
// Repository is a document inside a git repository, for a supplier who publishes VEX there
// rather than at a stable URL.
Repository *VEXRepository `yaml:"repository,omitempty"`
}
VEXSource is where one supplier's VEX document comes from.
Exactly one of Path, URL or Repository. Three ways rather than one because a supplier's document is somewhere different in every arrangement that actually occurs: committed alongside your descriptor, published at a URL, or living in a repository of theirs.
Naming a source is the opt-in. There is no trust setting: a document you pointed at is one you accepted, and the report names its author on every finding it excused so the judgement stays with the reader rather than being made by a flag.