saga

package
v0.46.2 Latest Latest
Warning

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

Go to latest
Published: Jul 30, 2026 License: Apache-2.0 Imports: 10 Imported by: 0

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

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

Criticalities lists the valid criticality levels, most to least critical.

Exposures lists the valid exposure levels, most to least exposed.

View Source
var GateLevels = []string{"error", "warning", "note"}

GateLevels lists the severity thresholds a gate may be set to, most to least severe.

SBOMFormats lists the valid SBOM document formats.

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

Functions

func SchemaURLFor added in v0.33.0

func SchemaURLFor(version string) string

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 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 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"`
	Repositories   []Repository                  `yaml:"repositories,omitempty"`
	Images         []Image                       `yaml:"images,omitempty"`
	Hosts          []Host                        `yaml:"hosts,omitempty"`
	Infrastructure []Infrastructure              `yaml:"infrastructure,omitempty"`
	Controllers    map[string]ControllerSettings `yaml:"controllers,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 (Component) ControllerEnabled

func (comp Component) ControllerEnabled(name string, project Config) bool

ControllerEnabled reports whether the named controller is enabled for this component, falling back to the project-level setting when the component has no override.

type Config

type Config struct {
	Controllers map[string]ControllerSettings `yaml:"controllers,omitempty"`
	// Reports are the report formats to render on a scan (e.g. json, sarif, markdown, html).
	// Publishers deliver every rendered report to a destination.
	Reports []ReportConfig `yaml:"reports,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"`
	// 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"`
	// 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 []string `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

func (c Config) ControllerEnabled(name string) bool

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

type ControllerSettings map[string]any

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 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 licence, 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"`
}

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) 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 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.

func (Exposure) Valid added in v0.5.0

func (e Exposure) Valid() bool

Valid reports whether e is a known exposure level. The empty value (unclassified) is not valid here; callers decide how to treat unset exposure.

type Fragment

type Fragment struct {
	Components []Component `yaml:"components,omitempty"`
}

Fragment is a partial Saga contributed by a Surveyor. The engine merges fragments into the Model.

type GateConfig added in v0.43.0

type GateConfig struct {
	// Controls sets a per-control severity threshold, overriding --fail-on for that control
	// only. Values are SARIF levels: error, warning, note.
	//
	// This exists because one threshold cannot serve every control. Licence policy is owned by
	// legal and vulnerability policy by security; "fail the build on a forbidden licence 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"`
}

GateConfig tunes which findings fail the build.

type Host

type Host struct {
	Name string `yaml:"name"`
	URL  string `yaml:"url"`
	Type string `yaml:"type,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 Image

type Image struct {
	Image  string `yaml:"image"`
	Digest string `yaml:"digest,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"`
}

Infrastructure is an infrastructure surface. Kind is e.g. "kubernetes"; Ref names the concrete instance.

type MetaSource

type MetaSource struct {
	RepoURL  string `yaml:"repoUrl"`
	Path     string `yaml:"path"`
	Revision string `yaml:"revision,omitempty"`
}

MetaSource points at a Saga fragment kept close to a component's source.

type Model

type Model struct {
	Release               Release      `yaml:"release"`
	Config                Config       `yaml:"config,omitempty"`
	Components            []Component  `yaml:"components,omitempty"`
	ComponentsMetaSources []MetaSource `yaml:"componentsMetaSources,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

func Load(data []byte) (*Model, error)

Load parses a Saga descriptor from YAML bytes, substituting ${{ VAR }} references from the environment and validating the result.

func LoadFile

func LoadFile(path string) (*Model, error)

LoadFile reads and parses a Saga descriptor from a file path.

func (*Model) Validate

func (m *Model) Validate() error

Validate checks the descriptor for structural correctness, returning all problems at once (joined) rather than only the first.

type PublisherConfig added in v0.21.0

type PublisherConfig struct {
	Kind string `yaml:"kind"`
	Dir  string `yaml:"dir,omitempty"` // file: output directory

	// 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: posts the markdown report as a sticky pull-request comment. PR defaults
	// to the number parsed from $GITHUB_REF (refs/pull/<n>/merge); Marker identifies the sticky
	// comment to update (default a Draugr marker).
	PR     int    `yaml:"pr,omitempty"`
	Marker string `yaml:"marker,omitempty"`
}

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 Reference

type Reference struct {
	Type string `yaml:"type"`
	Link string `yaml:"link"`
}

Reference links a manual/human security control (e.g. threat model, architecture diagram).

type Release

type Release struct {
	Name    string `yaml:"name"`
	Version string `yaml:"version"`
	Stage   string `yaml:"stage,omitempty"`
}

Release identifies what is being assessed.

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"`
}

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    []string `yaml:"paths,omitempty"`
}

Repository is a source repository at a revision, optionally scoped to paths.

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 SBOMSPDXJSON.
	Format SBOMFormat `yaml:"format,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 (
	// SBOMSPDXJSON is the default: SPDX in JSON, the ISO standard (ISO/IEC 5962), and what
	// Draugr's own releases publish.
	SBOMSPDXJSON SBOMFormat = "spdx-json"
	// SBOMSPDXTagValue is SPDX in its original tag-value encoding, still required by some
	// compliance tooling.
	SBOMSPDXTagValue SBOMFormat = "spdx-tag-value"
	// SBOMCycloneDXJSON is the OWASP format in JSON, common in security tooling.
	SBOMCycloneDXJSON SBOMFormat = "cyclonedx-json"
	// SBOMCycloneDXXML is CycloneDX in XML, which some enterprise tooling still expects.
	SBOMCycloneDXXML SBOMFormat = "cyclonedx-xml"
)

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.

Jump to

Keyboard shortcuts

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