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

security

package
v0.6.1 Latest Latest
Warning

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

Go to latest
Published: Oct 14, 2026 License: AGPL-3.0 Imports: 11 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 AwaitsImage added in v0.5.0

func AwaitsImage(scan state.Scan) bool

AwaitsImage reports whether a scan read the source alone, before there was a revision: detection's scan of a repository Pando builds, or Scan now on an app not yet accepted. Its score is the source's and not the app's. The app runs an image nobody has built yet, and R-311 says the score comes from what the app deploys. The findings stand; the number waits for a scan of the image.

func ContentAddressed added in v0.5.0

func ContentAddressed(image string) bool

ContentAddressed reports whether an image reference names its content: an image ID (sha256:…) or a reference pinned by digest (repo@sha256:…). Only such a name says two images are the same image.

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 Union added in v0.5.0

func Union(parts ...api.ScanResult) api.ScanResult

Union is one scan's result from scans of its parts: the image's findings and the source's, in that order, with a finding both report kept once.

What a scanner given an image and a source together reports (design 09 §3): a dependency found in both costs what it costs once. Two findings are the same finding when they have the same ID at the same target — the fields every scanner fills, so nothing here reads one scanner's vocabulary (R-251). The scanner named is the parts' own, and the time is when the last part finished.

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 DetectionScanner added in v0.5.0

type DetectionScanner struct {
	Service *Service
	Logger  *zap.Logger
}

DetectionScanner is the service as detection uses it (R-311, R-312): scans with no spec ID — there is no revision yet, and there may never be one — and never fatal. The proposal is what detection produces, and a scanner that could not read a checkout or an image is recorded as a failed scan on the app already.

func (DetectionScanner) RecordParts added in v0.5.0

func (d DetectionScanner) RecordParts(ctx context.Context, req api.ScanRequest, parts []api.ScanResult, failed error)

RecordParts records what ScanPart read as one scan of the source and the image.

func (DetectionScanner) ScanPart added in v0.5.0

ScanPart scans the source or the image detection built, recording nothing: the two run at once, beside the build and the trial run.

func (DetectionScanner) ScanSource added in v0.5.0

func (d DetectionScanner) ScanSource(ctx context.Context, req api.ScanRequest)

ScanSource scans and records in one call: the checkout, and the published image the proposal runs by digest when it runs one, so a deploy of the same source and image reuses the scan.

func (DetectionScanner) Scans added in v0.5.0

func (d DetectionScanner) Scans() bool

Scans reports whether there is a scanner to scan with.

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

	// Scope is what the scan above read: source, image or image_and_source
	// (R-311). Empty with no scan.
	Scope Scope `json:"scope,omitempty"`

	// AwaitingImage says the scan above read the source alone, before there
	// was a revision, so its score is the source's and not the app's (R-311):
	// standing.score is empty until a scan reads the image the app runs. The
	// scan's own score and findings are still in scan.
	AwaitingImage bool `json:"awaiting_image,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 Scope added in v0.5.0

type Scope string

Scope is what a scan read (R-311): the source, the image, or both. Said on the wire rather than left for each client to work out, so a number is never read as covering more than it does.

const (
	// ScopeSource: the source alone, with no image.
	ScopeSource Scope = "source"
	// ScopeImage: an image alone — an image app, whose only source is the
	// image's own digest, or Scan now on a deployed app.
	ScopeImage Scope = "image"
	// ScopeImageAndSource: an image and the source it came from.
	ScopeImageAndSource Scope = "image_and_source"
)

func ScopeOf added in v0.5.0

func ScopeOf(scan state.Scan) Scope

ScopeOf says what a scan read.

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) RecordParts added in v0.5.0

func (s *Service) RecordParts(ctx context.Context, req api.ScanRequest, parts []api.ScanResult, failed error, principal audit.Event) (state.Scan, error)

RecordParts records one scan of what req names — its source and its image together — from the results of scanning each part on its own: the union of their findings, as one scan of both would report them (Union). A part that failed fails the scan, which is recorded with no score (R-318), as Scan records one. The scan is keyed by req's source and image, so a deploy of the same source and image reuses it (R-312).

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, image string) (state.Scan, bool, error)

Reuse finds a successful scan of this app's source and image by the scanner that would run now, and makes it the scan of specID: a deploy of that source and image 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 image is what the deploy built or pulled, by digest, or empty when it has none. The scan reused has to have read both (R-311): detection's scan of the source alone does not stand in for the image a deploy built from it, and neither does a scan of another build of the same commit. An image named by anything but its digest is never reused, since the name says nothing about what is in it. A scan by another scanner does not count either: 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) ScanPart added in v0.5.0

func (s *Service) ScanPart(ctx context.Context, req api.ScanRequest) (api.ScanResult, error)

ScanPart runs the scanner over one part of what an app runs — its source, or its image — and records nothing. Detection scans the source while the image builds and the image once it is built (R-311), then records the two as one scan with RecordParts. The app is shown as scanning while a part runs.

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