Help us shape the future of Go! Take the 2026 Go Developer Survey

security

package
v0.4.0 Latest Latest
Warning

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

Go to latest
Published: Oct 11, 2026 License: AGPL-3.0 Imports: 10 Imported by: 0

Documentation

Overview

Package security turns a scanner's findings into a number, and that number into a decision.

Requirements §23, design 09. The split matters: the scanner knows what is wrong, this package knows what it costs and what follows, and neither learns the other's vocabulary (R-251). Everything here is arithmetic and policy — there is no I/O in the scoring, which is why it can be tested without Docker, a scanner, or a database.

Index

Constants

View Source
const (
	WeightCritical = 25
	WeightHigh     = 10
	WeightMedium   = 3
	WeightLow      = 1
)

The weights (R-313).

One critical finding costs more than fifty low ones, deliberately. A score that averaged, or that scaled with the size of the app, would let a real problem hide behind a long tail of noise — and a threshold set against it would mean something different for every app.

`unknown` scores as low: a finding whose severity nobody has decided is not free, and is not critical either.

Variables

This section is empty.

Functions

func Due

func Due(clock Clock, doc policy.Document, insecureSince *time.Time) bool

Due reports whether an insecure app's grace has run out.

func Fixable

func Fixable(findings []api.Finding) []api.Finding

Fixable returns the findings a fix exists for.

The distinction host policy may act on (R-313): "what is wrong with this app" and "what could its owner do about it today" are different questions, and an installation that only acts on the second should not be scored on the first. A finding with no fix is still real — it is hidden only where policy says the number should not count it, and the two agree because the same filter drives both.

func Ranked

func Ranked(findings []api.Finding) []api.Finding

Ranked returns the findings worst-first.

Severity, then ID, so the same scan orders the same way twice — a list whose order depends on the scanner's output order changes under the reader for no reason.

func Refusal

func Refusal(standing Standing, worst []api.Finding) error

Refusal is the plan-time error for an app that may not be deployed.

Written here rather than at the call site so the message is the same wherever the refusal comes from, and so the three findings that cost the most travel with it — R-105: an error somebody can act on, or paste into the assistant that wrote the app.

func Score

func Score(findings []api.Finding) int

Score is the number, 0 to 100.

func Worst

func Worst(findings []api.Finding, n int) []api.Finding

Worst returns the findings that cost the most, for an error or a summary.

Sorted by severity and then by ID, so the same scan produces the same list twice — an error message whose contents depend on map iteration is an error message people learn to ignore.

Types

type Auditor

type Auditor interface {
	Write(ctx context.Context, e audit.Event) error
}

Auditor records what happened (R-319).

type Clock

type Clock interface{ Now() time.Time }

Clock is the time source, so the grace period is testable without sleeping.

type Counts

type Counts struct {
	Critical int `json:"critical"`
	High     int `json:"high"`
	Medium   int `json:"medium"`
	Low      int `json:"low"`
	Unknown  int `json:"unknown"`
}

Counts is how many findings of each severity, for a sentence a person reads.

func Count

func Count(findings []api.Finding) Counts

Count tallies findings by severity.

type Placed

type Placed struct {
	Score   *int
	Verdict Verdict

	// Scanning says a scan of the app is running now, so a list can show it
	// rather than "Not scanned" until the scan ends.
	Scanning bool
}

Placed is the number this installation means, and what it means.

type PolicyLoader

type PolicyLoader interface {
	Document(ctx context.Context) (policy.Document, error)
}

PolicyLoader is host policy, narrowed to the one call this needs.

type Report

type Report struct {
	Standing Standing      `json:"standing"`
	Scan     *state.Scan   `json:"scan,omitempty"`
	Counts   Counts        `json:"counts"`
	Worst    []api.Finding `json:"worst,omitempty"`

	// Scanner is what would run, or what did. Empty when the installation has
	// none configured, which is what makes a threshold inert (R-317).
	Scanner string `json:"scanner,omitempty"`

	// ScanningSince is when a scan of this app that has not finished began,
	// and absent when none is running. Whoever started it: a deploy, detection,
	// or somebody asking — so every client can show that a scan is under way
	// rather than only the one that asked for it (R-261).
	ScanningSince *time.Time `json:"scanning_since,omitempty"`

	// IgnoringUnfixable says the score and the list above leave out findings
	// with no fix, because host policy says so (R-313). Said rather than
	// implied: a list somebody cannot explain the length of is a list they
	// stop trusting.
	IgnoringUnfixable bool `json:"ignoring_unfixable"`
}

Report is an app's security standing, with the scan behind it.

type Scanner

type Scanner interface {
	Scan(ctx context.Context, req api.ScanRequest) (api.ScanResult, error)
}

Scanner is what this package needs from an adapter, narrowed to the one call.

type Scores

type Scores struct {
	All     *int
	Fixable *int
}

Scores is one app's two numbers, as the list query returns them.

type Service

type Service struct {
	Scans       *state.Scans
	Deployments *state.Deployments
	Registry    *api.Registry
	Policy      PolicyLoader
	Auditor     Auditor
	Logger      *zap.Logger
	// contains filtered or unexported fields
}

Service runs scans and answers "where does this app stand".

The adapter produces findings, this produces the score, records it, and tells the audit log. Nothing here knows what a CVE is and nothing in the adapter knows what a threshold is (R-251).

func (*Service) Allows

func (s *Service) Allows(ctx context.Context, appID, specID string) (Standing, error)

Allows reports whether this revision may be deployed (R-314).

Called from the deploy path with the score of the image that was just built, and from the planner with whatever is already known about the revision.

func (*Service) Configured

func (s *Service) Configured() (string, bool)

Configured reports whether this installation can scan at all.

func (*Service) Place

func (s *Service) Place(ctx context.Context, scores map[string]Scores) (map[string]Placed, error)

Place puts a set of already-fetched scores against policy, in one pass.

For a list: the alternative is a policy load and a scanner lookup per row, which is the shape that turns a page of twenty apps into forty queries. The scores come from the caller because the list query already fetched them.

func (*Service) Report

func (s *Service) Report(ctx context.Context, appID, specID string) (Report, error)

Report is where an app stands right now.

The scan of the revision it is running, not the newest scan of the app: an app whose newest scan is of a revision it is not running has not been scanned in the sense that matters.

func (*Service) Reuse

func (s *Service) Reuse(ctx context.Context, appID, specID, source string) (state.Scan, bool, error)

Reuse finds a successful scan of this app's source by the scanner that would run now, and makes it the scan of specID: a deploy of that source uses it rather than scanning the same thing again (R-312, design 09 §4.1).

Source is what names the source exactly — a commit, an image digest, an upload's archive digest — and a scan by another scanner does not count: its findings are not the ones this installation's scanner would report. Found is false when there is nothing to reuse, and the caller scans.

A scan already attached to specID is returned as it is. One attached to another revision, or to none, is recorded again against specID, so the threshold check and every screen that asks for the revision's score find it.

func (*Service) Scan

func (s *Service) Scan(ctx context.Context, req api.ScanRequest, principal audit.Event) (state.Scan, error)

Scan runs a scan and records it, whatever the outcome.

A failure is recorded as a scan with no score rather than swallowed: a scanner that cannot run is a thing somebody has to fix, and silence would leave an unscannable app looking clean (R-318).

func (*Service) Scanning

func (s *Service) Scanning(appID string) (time.Time, bool)

Scanning reports when a scan of appID that is still running began, and whether one is.

type Standing

type Standing struct {
	Verdict Verdict `json:"verdict"`

	// Score is nil for a revision with no score, which is not a score of zero
	// (R-318). The wire keeps them apart because the console has to.
	Score     *int `json:"score"`
	Threshold int  `json:"threshold"`

	// Scanned is when the score was taken. Zero when there is none.
	Scanned time.Time `json:"scanned,omitzero"`

	// StopAt is when this app will be stopped, when it is insecure and policy
	// says to stop. Zero otherwise.
	StopAt time.Time `json:"stop_at,omitzero"`
}

Standing is an app's position against the installation's threshold.

func Evaluate

func Evaluate(doc policy.Document, score *int, scanned time.Time, scannerConfigured bool, insecureSince *time.Time) Standing

Evaluate places one app against policy.

`score` is nil for a revision that has never been scanned, or whose only scan failed — which is not the same as a score of zero and is not treated as one (R-318).

func (Standing) Deployable

func (s Standing) Deployable() bool

Deployable reports whether a standing permits a deploy (R-314).

Unscanned blocks: with a threshold in force, "we have never looked" is not a pass. Inert does not: an installation with no scanner has nothing to look with, and a policy that blocks every deploy because a component is missing is worse than one that is visibly off.

type Verdict

type Verdict string

Verdict is what policy says about an app's standing.

const (
	// VerdictOK: at or above the threshold, or no threshold in force.
	VerdictOK Verdict = "ok"
	// VerdictUnscanned: a threshold is in force and this revision has no score.
	VerdictUnscanned Verdict = "unscanned"
	// VerdictInsecure: below the threshold.
	VerdictInsecure Verdict = "insecure"
	// VerdictInert: a threshold is set and no scanner is configured, so nothing
	// is enforced and the console says so (R-317).
	VerdictInert Verdict = "inert"
)

Jump to

Keyboard shortcuts

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