Documentation
¶
Overview ¶
Package report implements the Reports library: point-in-time, immutable, Ed25519-signed compliance artifacts. It generates four kinds (executive summary, framework attestation, exception register, remediation activity), each computed once from data that already exists and stored as a frozen JSON document; the row is never recomputed. Each kind renders to downloadable faces (PDF / CSV / OSCAL SAR / JSON).
DEFERRED: the Templates gallery and retention sweeps (see the spec excludes). Recurring generation + email delivery lives in internal/reportschedule.
Spec: api-reports.
Index ¶
- Constants
- Variables
- func ExportFilename(rep Report, face string) string
- func VerifySignature(pub ed25519.PublicKey, contentSHA256 string, sig []byte) bool
- type AttestationContent
- type AttestationRollup
- type AttestedHost
- type CorpusEntry
- type Coverage
- type EngineEntry
- type ErrAmbiguousScore
- type ErrInvalidProvenance
- type ExceptionContent
- type ExceptionRow
- type ExceptionSummary
- type ExecutiveContent
- type FrameworkCount
- type GenerateRequest
- type GroupScoper
- type Kind
- type ReadModelProvenance
- type RemediationActRow
- type RemediationContent
- type RemediationSummary
- type RenderPayload
- type RenderProcessor
- type Report
- type Scope
- type ScoreProvenance
- type Service
- func (s *Service) Export(ctx context.Context, id uuid.UUID, face string) ([]byte, string, error)
- func (s *Service) Frameworks(ctx context.Context) ([]FrameworkCount, error)
- func (s *Service) Generate(ctx context.Context, generatedBy string, req GenerateRequest) (Report, error)
- func (s *Service) Get(ctx context.Context, id uuid.UUID) (Report, error)
- func (s *Service) List(ctx context.Context) ([]Report, error)
- func (s *Service) Signer() *Signer
- func (s *Service) WithAsyncRender() *Service
- func (s *Service) WithGroups(g GroupScoper) *Service
- func (s *Service) WithSigner(signer *Signer) *Service
- type Signer
- type TopFailingRule
Constants ¶
const ( FaceJSON = "json" FacePDF = "pdf" FaceCSV = "csv" )
Face identifiers.
const FaceOSCALSAR = "oscal_sar"
FaceOSCALSAR is the fleet OSCAL assessment-results face (attestation-only).
const RenderJobType = "report.render"
RenderJobType is the job_type for an async report-face render.
Variables ¶
ErrGroupScopeUnavailable is returned by Generate when a group scope is requested but no group resolver was wired (WithGroups). In production the resolver is always wired, so this is a programmer/config error.
var ErrInvalidFace = errors.New("report: invalid face")
ErrInvalidFace is returned by Export for an unknown face (or a face that does not apply to the report's kind). Handlers map it to 400.
var ErrInvalidKind = errors.New("report: invalid kind")
ErrInvalidKind is returned by Generate for an unknown report kind. Handlers map it to 400.
var ErrNotFound = errors.New("report: not found")
ErrNotFound is returned by Get when no report has the given id. Handlers map it to 404.
Functions ¶
func ExportFilename ¶
ExportFilename builds a download filename for a report face, e.g. "openwatch-executive-all-hosts-2026-06-21.pdf" or "openwatch-attestation-production-cis-2026-06-21.csv".
Types ¶
type AttestationContent ¶
type AttestationContent struct {
// Framework is the lens (a framework_refs key), or "" for all.
Framework string `json:"framework"`
// HostsTotal is the active in-scope host count; HostsAttested is how
// many of those have a completed scan to attest from.
HostsTotal int `json:"hosts_total"`
HostsAttested int `json:"hosts_attested"`
// Attested lists, per attested host, the scan the attestation is over.
Attested []AttestedHost `json:"attested"`
// Rollup is the headline compliance aggregate, FROZEN into the signed
// snapshot at generation time (computed once from the frozen scans'
// immutable scan_results, framework-lensed). Because it is part of the
// content it is signed + tamper-evident, and the in-app view, the PDF
// cover, and the signature all read the same numbers (P1: one snapshot,
// identical across every face). It is bounded (aggregates + a small
// top-N), never the per-(host, rule) rows.
Rollup AttestationRollup `json:"rollup"`
}
AttestationContent is the frozen snapshot for a Framework Attestation: it captures WHICH scan attests each in-scope host (point-in-time, since scan_results are immutable), not the bulk rows themselves - the CSV / OSCAL faces reconstruct those from the referenced scans on demand.
type AttestationRollup ¶
type AttestationRollup struct {
// LegacyCompliancePct is DECODE ONLY. It is the pooled whole-percent
// number artifacts carried before 2026-09-03: passing over every rule
// outcome in the attested set, so a host with 700 rules outvoted one with
// 50, and a whole percent could never equal the one-decimal fleet score.
//
// No constructor sets it. It exists so a legacy artifact still decodes
// and still renders the number it was signed with. omitempty is what
// keeps it out of every artifact generated from now on.
LegacyCompliancePct *int `json:"compliance_pct,omitempty"`
// ScorePct is the equal-host mean of the attested hosts' scores, to one
// decimal, null when no attested host produced a verdict.
//
// Null and 0.0 are different facts. Zero means every evaluated rule
// failed; null means nothing was evaluated, and collapsing the second
// into the first is bugs/OW-023 and OW-024.
ScorePct *float64 `json:"score_pct"`
// TotalChecks is every (host, rule) outcome counted; the rest are the
// per-status splits.
TotalChecks int `json:"total_checks"`
Passing int `json:"passing"`
Failing int `json:"failing"`
Skipped int `json:"skipped"`
Errored int `json:"errored"`
// TopFailing lists the rules failing on the most hosts (capped).
TopFailing []TopFailingRule `json:"top_failing"`
// Provenance is the frozen envelope. Nil on a legacy artifact, which is
// exactly how a legacy artifact is identified: a missing artifact_class
// is the legacy shape and nothing backfills one.
Provenance *ScoreProvenance `json:"provenance,omitempty"`
}
AttestationRollup is the bounded compliance aggregate stored on an attestation snapshot: pass/fail/total counts, fleet compliance percent, and a sampled top-failing list, over the frozen scans (framework-lensed).
type AttestedHost ¶
type AttestedHost struct {
HostID uuid.UUID `json:"host_id"`
ScanID uuid.UUID `json:"scan_id"`
ScannedAt time.Time `json:"scanned_at"`
}
AttestedHost ties an in-scope host to the completed scan that attests it (its latest as of generation time) and when that scan finished.
type CorpusEntry ¶ added in v0.8.0
type CorpusEntry struct {
// Version is null for a curated corpus, which has a digest and no version.
Version *string `json:"corpus_version"`
Digest string `json:"corpus_digest"`
ContributorsScored int `json:"contributors_scored"`
}
CorpusEntry is one corpus that measured part of a frozen score.
type Coverage ¶
type Coverage struct {
HostsTotal int `json:"hosts_total"`
HostsFresh int `json:"hosts_fresh"`
HostsStale int `json:"hosts_stale"`
HostsUnreachable int `json:"hosts_unreachable"`
}
Coverage is the staleness disclosure behind every report: of the in-scope active hosts, how many have fresh compliance data versus stale-or-never-scanned, and how many are currently unreachable. It is the basis of the coverage caveat - the honesty that lets a reader trust or discount the headline numbers. hosts_fresh + hosts_stale == hosts_total; hosts_unreachable is an independent reachability count (a host can be both stale and unreachable).
type EngineEntry ¶ added in v0.8.0
type EngineEntry struct {
EngineVersion string `json:"engine_version"`
ContributorsScored int `json:"contributors_scored"`
}
EngineEntry is one engine version behind a frozen score.
type ErrAmbiguousScore ¶ added in v0.8.0
type ErrAmbiguousScore struct{ Kind string }
ErrAmbiguousScore reports an artifact carrying both score fields.
A current artifact must expose ONE authoritative number. Carrying the pooled legacy percent beside the equal-host mean would publish two plausible scores over the same population and leave every reader to pick, which is the ambiguity the removal of passing_fraction was meant to end.
func (ErrAmbiguousScore) Error ¶ added in v0.8.0
func (e ErrAmbiguousScore) Error() string
type ErrInvalidProvenance ¶ added in v0.8.0
type ErrInvalidProvenance struct{ Reason string }
ErrInvalidProvenance reports provenance that is present but unusable.
A present-but-empty envelope is not a legacy artifact. Treating it as one let "provenance": {} and "provenance": null read as "signed before the formula changed", which is a claim about history that the bytes do not support. Absence means legacy; anything else present must be valid.
func (ErrInvalidProvenance) Error ¶ added in v0.8.0
func (e ErrInvalidProvenance) Error() string
type ExceptionContent ¶
type ExceptionContent struct {
Summary ExceptionSummary `json:"summary"`
Exceptions []ExceptionRow `json:"exceptions"`
// Provenance is the read-model envelope. It carries no score and no
// contributor count, because this artifact aggregates no scan set.
Provenance *ReadModelProvenance `json:"provenance,omitempty"`
}
ExceptionContent is the frozen snapshot for an Exception Register: a point-in-time summary of compliance waivers plus the register rows. The CSV face writes one row per exception; the PDF face renders the bounded summary (counts + expiring-soon).
type ExceptionRow ¶
type ExceptionRow struct {
HostName string `json:"host_name"`
RuleID string `json:"rule_id"`
Status string `json:"status"`
Reason string `json:"reason"`
RequestedBy string `json:"requested_by"`
RequestedAt time.Time `json:"requested_at"`
ReviewedBy string `json:"reviewed_by"`
ReviewedAt *time.Time `json:"reviewed_at"`
ExpiresAt *time.Time `json:"expires_at"`
Active bool `json:"active"`
}
ExceptionRow is one waiver in the register. Requester/reviewer are resolved to usernames (or "" when unresolved) so the register reads without a second lookup. Active is true for an approved, unexpired waiver.
type ExceptionSummary ¶
type ExceptionSummary struct {
Total int `json:"total"`
Active int `json:"active"`
Requested int `json:"requested"`
Approved int `json:"approved"`
Rejected int `json:"rejected"`
Revoked int `json:"revoked"`
Expired int `json:"expired"`
ExpiringSoon int `json:"expiring_soon"`
}
ExceptionSummary is the bounded rollup of the in-scope waivers by state. Active counts approved waivers not past their expiry; ExpiringSoon is the subset of Active whose expiry falls within the next 30 days.
type ExecutiveContent ¶
type ExecutiveContent struct {
// LegacyCompliancePct is DECODE ONLY, the pooled whole-percent number
// artifacts carried before 2026-09-03. See AttestationRollup for why it
// is kept and why no constructor sets it.
LegacyCompliancePct *int `json:"compliance_pct,omitempty"`
// ScorePct is the equal-host mean of the in-scope hosts' scores, to one
// decimal, null when no host produced a verdict.
ScorePct *float64 `json:"score_pct"`
// HostCount is the number of active (non-deleted) hosts.
HostCount int `json:"host_count"`
// PassingRules / FailingRules are host_rule_state rows by status.
PassingRules int `json:"passing_rules"`
FailingRules int `json:"failing_rules"`
// CriticalIssues is the count of failing rows with critical severity.
CriticalIssues int `json:"critical_issues"`
// TopFailingRules lists the rules failing on the most hosts.
TopFailingRules []TopFailingRule `json:"top_failing_rules"`
// Coverage describes how much of the in-scope fleet the numbers
// actually reflect (fresh vs stale/never-scanned, and unreachable).
Coverage Coverage `json:"coverage"`
// Provenance is the frozen envelope; nil on a legacy artifact.
Provenance *ScoreProvenance `json:"provenance,omitempty"`
}
ExecutiveContent is the JSON posture document stored for an executive summary report. It is computed once at generation time from host_rule_state and frozen.
type FrameworkCount ¶
type FrameworkCount struct {
Framework string `json:"framework"`
RuleCount int `json:"rule_count"`
}
FrameworkCount is one entry in the fleet framework catalog: a framework_refs key present somewhere in the fleet and the number of distinct rules mapped to it. Backs the report scope picker's framework lens.
type GenerateRequest ¶
type GenerateRequest struct {
// Kind selects the report kind; "" defaults to executive. attestation
// produces the Framework Attestation (CSV/OSCAL bulk faces).
Kind Kind
// GroupID scopes the report to one group's member hosts.
GroupID *uuid.UUID
// Framework scopes the report to one framework lens.
Framework string
// PeriodDays is the look-back window for time-windowed kinds
// (remediation): the report covers requests in the last PeriodDays. 0
// defaults to defaultPeriodDays; ignored by point-in-time kinds.
PeriodDays int
}
GenerateRequest is the (all-optional) input to Generate. An empty request generates the all-hosts, all-frameworks executive summary — the pre-A1 behavior.
type GroupScoper ¶
type GroupScoper interface {
ScopeGroup(ctx context.Context, groupID uuid.UUID) (name string, hostIDs []uuid.UUID, err error)
// ScopeGroupIn resolves the same thing inside a caller-supplied
// transaction, so a signed artifact's scope and its content come from one
// snapshot. Resolving membership on the pool first let a host join or
// leave in between, and nothing in the artifact would say so.
ScopeGroupIn(ctx context.Context, q db.Queryer, groupID uuid.UUID) (name string, hostIDs []uuid.UUID, err error)
}
GroupScoper resolves a group id to its display name and member host ids, so the report service can scope a fleet computation to one group without depending on the group package's types. internal/group's Service satisfies it via ScopeGroup.
type Kind ¶
type Kind string
Kind is the report flavor. The MVP only generates KindExecutive.
const ( // KindExecutive is the Fleet Compliance Executive Summary. KindExecutive Kind = "executive" // KindAttestation is the Framework Attestation: the auditor/GRC bulk // evidence path. Its snapshot freezes the latest completed scan per // in-scope host; its bulk faces (CSV now, OSCAL SAR next) reconstruct // per-(host, rule) outcomes from those immutable scan_results. KindAttestation Kind = "attestation" // KindException is the Exception Register: a Compliance/GRC point-in-time // read-model of compliance waivers (compliance_exceptions) - who waived // which rule on which host, the justification, the approver, and the // expiry. Faces: a CSV register + a bounded PDF summary (counts by // status + active + expiring-soon). KindException Kind = "exception" // KindRemediation is the Remediation Activity report: an Operations // read-model of remediation execute/rollback requests over a time // window (remediation_requests filtered on requested_at). Faces: a CSV // activity log + a bounded PDF summary (counts by outcome). KindRemediation Kind = "remediation" )
type ReadModelProvenance ¶ added in v0.8.0
type ReadModelProvenance struct {
ArtifactClass string `json:"artifact_class"`
CorpusIdentityStatus string `json:"corpus_identity_status"`
// Kept as explicit nulls to honor decision 06's always-present rule.
// Omitting them would narrow that decision and would need a superseding
// record.
CorpusVersion *string `json:"corpus_version"`
CorpusDigest *string `json:"corpus_digest"`
}
ReadModelProvenance is the envelope on an artifact that aggregates no scan set.
compliance_exceptions carries host_id and rule_id and no scan id, so contributor counts have no referent here. Attributing a corpus could only mean the corpus installed at generation time, which decision 08 forbids. The word is not_applicable rather than unavailable because corpus identity does not APPLY to a read model rather than being missing from it.
func NewReadModelProvenance ¶ added in v0.8.0
func NewReadModelProvenance() *ReadModelProvenance
NewReadModelProvenance builds the read-model envelope.
type RemediationActRow ¶
type RemediationActRow struct {
HostName string `json:"host_name"`
RuleID string `json:"rule_id"`
Status string `json:"status"`
Mechanism string `json:"mechanism"`
RequestedBy string `json:"requested_by"`
RequestedAt time.Time `json:"requested_at"`
ReviewedBy string `json:"reviewed_by"`
ReviewedAt *time.Time `json:"reviewed_at"`
}
RemediationActRow is one remediation request in the activity log. Requester/reviewer are resolved to usernames.
type RemediationContent ¶
type RemediationContent struct {
// PeriodFrom/PeriodTo bound the window (by requested_at). PeriodTo is
// the generation instant; PeriodFrom is PeriodTo minus the period.
PeriodFrom time.Time `json:"period_from"`
PeriodTo time.Time `json:"period_to"`
Summary RemediationSummary `json:"summary"`
Activities []RemediationActRow `json:"activities"`
// Provenance is the read-model envelope; see ExceptionContent.
Provenance *ReadModelProvenance `json:"provenance,omitempty"`
}
RemediationContent is the frozen snapshot for a Remediation Activity report: the period it covers, a summary of requests by outcome, and the activity rows. The CSV face writes one row per request; the PDF face renders the bounded summary.
type RemediationSummary ¶
type RemediationSummary struct {
Total int `json:"total"`
Executed int `json:"executed"`
RolledBack int `json:"rolled_back"`
Failed int `json:"failed"`
Rejected int `json:"rejected"`
Pending int `json:"pending"`
}
RemediationSummary is the bounded rollup of in-window requests by outcome.
type RenderPayload ¶
RenderPayload is the job payload: which snapshot to render faces for.
type RenderProcessor ¶
type RenderProcessor struct {
// contains filtered or unexported fields
}
RenderProcessor renders a report's faces for a claimed report.render job and publishes ReportReady. It is registered on the in-process worker via WithReportProcessor; its ProcessJob signature matches the worker's other processors.
func NewRenderProcessor ¶
func NewRenderProcessor(svc *Service, bus *eventbus.Bus) *RenderProcessor
NewRenderProcessor builds the processor over a report Service (for Export) and an event bus (to publish ReportReady). A nil bus renders the faces but publishes nothing.
func (*RenderProcessor) ProcessJob ¶
func (p *RenderProcessor) ProcessJob(ctx context.Context, j *queue.Job)
ProcessJob renders every face that applies to the report's kind (warming the cache and flipping each 'pending' row to 'ready' via Export's upsert) and publishes a ReportReady event. A render error fails the job so it can be retried; faces already rendered are idempotent (deterministic bytes).
type Report ¶
type Report struct {
ID uuid.UUID
Title string
Kind Kind
ScopeLabel string
Scope Scope
DataAsOf time.Time
GeneratedBy string
Format string
Content json.RawMessage
// ContentSHA256 is the snapshot's content address: the hex SHA-256 of
// the canonical (marshaled) Content. Identical content yields an
// identical hash; it is the stable identity the signature signs over.
ContentSHA256 string
// Signature is the Ed25519 signature over the content address (nil
// when the snapshot was generated without a signer). SigningKeyID is
// the fingerprint of the key that produced it.
Signature []byte
SigningKeyID string
CreatedAt time.Time
}
Report is one row of the report_snapshots table. Content holds the rendered JSON posture document (see ExecutiveContent for the executive shape).
type Scope ¶
type Scope struct {
// GroupID, when set, scopes the report to that group's member hosts.
GroupID *uuid.UUID `json:"group_id,omitempty"`
// GroupName is the group's display name at generation time, frozen
// onto the report so the label survives a later group rename/delete.
GroupName string `json:"group_name,omitempty"`
// Framework, when non-empty, scopes the report to rules whose
// framework_refs contain this key (same lens as the fleet rollup).
Framework string `json:"framework,omitempty"`
}
Scope is the structured slice of the fleet a report summarizes: an optional group and/or framework lens. The zero value (no group, no framework) is the all-hosts, all-frameworks scope. It is stored as the reports.scope JSONB column and echoed on the API so a caller can see (and reproduce) exactly what a report covers.
type ScoreProvenance ¶ added in v0.8.0
type ScoreProvenance struct {
ArtifactClass string `json:"artifact_class"`
FormulaVersion *int `json:"formula_version"`
AggregationMethod *string `json:"aggregation_method"`
Lens *string `json:"lens"`
EngineIdentityStatus string `json:"engine_identity_status"`
Engines []EngineEntry `json:"engines"`
HostsWithoutEngineIdentity *int `json:"hosts_without_engine_identity"`
EngineVersion *string `json:"engine_version"`
CorpusIdentityStatus string `json:"corpus_identity_status"`
Corpora []CorpusEntry `json:"corpora"`
HostsWithoutCorpusIdentity *int `json:"hosts_without_corpus_identity"`
CorpusVersion *string `json:"corpus_version"`
CorpusDigest *string `json:"corpus_digest"`
// Participation counts. A mean over 2 of 200 hosts and a mean over 200 of
// 200 are different claims that a percentage alone cannot tell apart, and
// on a signed artifact the reader cannot go and look.
HostsTotal int `json:"hosts_total"`
HostsScored int `json:"hosts_scored"`
HostsWithoutScore int `json:"hosts_without_score"`
}
ScoreProvenance is the frozen envelope on a score-bearing artifact.
Every field is present. A value that is unknown is null, never absent and never an empty string: bugs/OW-009 is the case where an empty non-nil slice serialized as "" and became indistinguishable from a real value.
func NewScoreProvenance ¶ added in v0.8.0
func NewScoreProvenance(env compliance.Envelope, agg compliance.Aggregate) *ScoreProvenance
NewScoreProvenance converts a validated envelope plus its participation counts into the frozen wire shape.
It takes a compliance.Envelope rather than the raw parts, so the validation that refuses an envelope whose counts do not reconcile runs before anything is signed. An artifact showing two count sets that do not add up is the same defect as a mean displayed beside pooled totals it cannot be derived from.
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service owns the reports library: generating an executive summary from current posture, and listing/fetching stored reports.
func NewService ¶
func (*Service) Export ¶
Export returns a rendered face of the report (its bytes + media type), or ErrNotFound for an unknown id / ErrInvalidFace for a face that does not apply to the report's kind. The JSON face is the canonical content (any kind); the PDF face is executive-only; the CSV face is attestation-only. Rendered faces are cached in report_faces.
func (*Service) Frameworks ¶
func (s *Service) Frameworks(ctx context.Context) ([]FrameworkCount, error)
Frameworks returns the distinct framework_refs keys present anywhere in the fleet, each with the count of distinct rules mapped to it, most-populated first. Backs the report scope picker's framework lens.
Scoped to current corpora (internal/corpus), so the picker offers only frameworks some host is still scanned against. A framework that exists on retired rows alone would otherwise stay on the menu and produce a report whose every rule is frozen.
func (*Service) Generate ¶
func (s *Service) Generate(ctx context.Context, generatedBy string, req GenerateRequest) (Report, error)
Generate computes the Fleet Compliance Executive Summary from current posture (host_rule_state pass/fail counts + critical, the active host count, and the top failing rules) and inserts an immutable report row. The optional req scopes the summary to a group's member hosts and/or a framework lens; an empty req covers all hosts and all frameworks (the pre-A1 behavior). generatedBy is the actor recorded on the artifact (an email or "scheduler"). The returned Report carries the stored JSON content and the resolved scope.
func (*Service) WithAsyncRender ¶
WithAsyncRender enables async rendering of attestation bulk faces: Generate then marks the faces 'pending' and enqueues a report.render job (a RenderProcessor on the in-process worker renders them and publishes ReportReady). Enable it only when a worker is running to drain the queue; without it Generate stays synchronous and faces render lazily on first download.
func (*Service) WithGroups ¶
func (s *Service) WithGroups(g GroupScoper) *Service
WithGroups wires the group resolver used for group-scoped reports. Returns the receiver for chaining at construction time.
func (*Service) WithSigner ¶
WithSigner wires the Ed25519 signer that signs new snapshots over their content address. Without it, snapshots are generated unsigned (signature + signing_key_id stay null).
type Signer ¶
type Signer struct {
// contains filtered or unexported fields
}
Signer signs report snapshots with an Ed25519 key.
func NewSigner ¶
NewSigner builds a Signer. When keyFile is non-empty it loads a 32-byte raw Ed25519 seed from that path (mode 0600 expected); when empty it generates an ephemeral key for development (Ephemeral() reports true so the caller can warn). The key id is a stable fingerprint of the public key.
func (*Signer) Ephemeral ¶
Ephemeral reports whether the signer is a per-boot development key (no durable key was configured).
type TopFailingRule ¶
type TopFailingRule struct {
RuleID string `json:"rule_id"`
FailingHostCount int `json:"failing_host_count"`
}
TopFailingRule is one entry in the executive summary's top-failing list: a rule id and how many hosts it fails on.