Documentation
¶
Overview ¶
Package spec is L0 of the SigComply CLI: parsers and validators for the declarative artifacts that drive the engine — framework specs, policy specs, evidence type schemas, source plugin manifests, the project config (.sigcomply.yaml), and the manual evidence catalog.
L0 owns the on-disk format. Nothing here executes; specs are read, validated, and handed up to L1 (types) and L2 (registries).
See docs/architecture/02-layers.md for the full layer contract.
Index ¶
- Constants
- func LoadEvidenceType(data []byte) (core.EvidenceType, error)
- func LoadPolicy(data []byte) (core.Policy, error)
- func ValidatePassWhen(p core.Policy) error
- type BindingEntry
- type CIConfig
- type CloudConfig
- type ControlConfig
- type ControlSpec
- type CustomPeriod
- type ExceptionScope
- type ExtensionsConfig
- type FiscalCalendarConfig
- type FrameworkSpec
- type OutputConfig
- type PeriodConfig
- type PluginConfigField
- type PluginCredentialRequirement
- type PluginManifest
- type PolicyConfig
- type PolicyException
- type PolicyRefSpec
- type ProjectConfig
- func (c *ProjectConfig) BindingsFor(policyID string) map[string][]BindingEntry
- func (c *ProjectConfig) CadenceFor(policyID string) string
- func (c *ProjectConfig) EvidenceModeOverrideFor(policyID string) (mode, catalogEntry string)
- func (c *ProjectConfig) ExceptionsFor(policyID string) []PolicyException
- func (c *ProjectConfig) ParametersFor(policyID string) map[string]any
- type Risk
- type RiskRegister
- type RosterConfig
- type ScopeConfig
- type VaultConfig
- type Vendor
- type VendorRegister
Constants ¶
const ( // TreatmentModify applies controls to reduce the risk — the option // that selects Annex A controls, and the reason a risk carries a // controls list at all. TreatmentModify = "modify" // TreatmentRetain accepts the risk at its evaluated level without // further treatment. It is the one option that produces no control, // so it is the one that must name who accepted it. TreatmentRetain = "retain" // TreatmentAvoid stops the activity that gives rise to the risk. TreatmentAvoid = "avoid" // insurance, or an outsourcing arrangement. ISO/IEC 27005 3.2.9 // notes that transferring is a *form of* sharing, not a synonym, // which is why the broader word is the canonical one. TreatmentShare = "share" )
Risk treatment options, in ISO/IEC 27005's vocabulary — the four ways an organization can respond to an evaluated risk.
The names are the standard's, not colloquial synonyms: "retain" is what most people call accepting a risk, and "modify" is what most people call mitigating it. Using the standard's words keeps the register legible to the auditor reading it beside the clause.
const ( RiskLevelCritical = "critical" RiskLevelHigh = "high" RiskLevelModerate = "moderate" RiskLevelLow = "low" )
Risk levels. Deliberately the same four words as the vendor tiers, so an operator learns one vocabulary rather than two.
Two levels are carried, not three, and that is deliberate: ISO/IEC 27001 6.1.2 d) 3) asks the organization to "determine the levels of risk" — singular per risk — and 6.1.3 f) separately requires acceptance of the *residual* risk. The before/after pair that most register templates carry as "inherent" and "residual" is convention; the word "inherent" appears nowhere in the standard, and neither do asset, threat or vulnerability, all of which the 2005 edition mandated and the 2022 edition deliberately dropped. Forcing a methodology the standard declined to impose is not this tool's job.
A level is descriptive. It NEVER decides whether a risk owes a treatment, an owner, or an acceptance record — every risk owes all three at every level. This is the same guardrail the vendor tiers carry, and for the same reason: an operator-chosen value that removes an obligation would make under-declaring the register the cheapest way to look compliant.
const ( // TierCritical and TierHigh owe independent assurance: a SOC 2 // Type II, an ISO 27001 certificate, or a penetration-test report. TierCritical = "critical" TierHigh = "high" // TierModerate owes a lighter artifact — a completed security // questionnaire, a trust-page snapshot, or a data-processing // agreement — but still owes a folder. TierModerate = "moderate" // TierLow owes no upload, but owes a rationale and an approver. TierLow = "low" )
Risk tiers. The tier is the organization's own assessment of how much a third party matters, and it is mechanical rather than decorative — but it changes *which artifact* a vendor owes, never *whether* it owes one.
That distinction is load-bearing. An earlier draft let the lower tiers carry no obligation at all, which would have reproduced the failure mode this codebase already names as the worst one available: an operator-chosen value that silently removes a requirement, leaves the compliance score untouched, and so makes under-declaring the estate *raise* the score. It is the same defect as declaring an ISO clause not_applicable, which the planner refuses outright.
So TierLow does not mean "no obligation". It means the obligation is discharged by a signed-off written justification instead of an uploaded artifact, and the register must carry TierRationale and ApprovedBy to claim it — exactly what the Statement of Applicability already demands of an excluded Annex A control.
const DefaultLocalVaultPath = "./.sigcomply/vault"
DefaultLocalVaultPath is where a backend-less project stores signed evidence by default. It keeps a first run zero-config: no vault: block is required to get going. Production deployments override with an s3/gcs/azure_blob backend (which must be write-once / versioned for real tamper-resistance — see Invariant #3).
const ManualSlotName = "_manual"
ManualSlotName is the synthetic slot name the planner creates for manual policies. The collector and evaluator use it as the key in RecordsByPolicy[policyID].
const RisksKey = "risks"
RisksKey is the subkey under `experimental:` that carries the risk register. It lives under the escape hatch for the same reason VendorsKey and ScopeKey do: the loader runs with KnownFields(true), so a brand-new top-level key would hard-fail every older pinned CLI that predates it. See docs/architecture/08-project-config.md §Config evolution policy.
const RosterKey = "roster"
RosterKey is the subkey under `experimental:` that designates the organization's roster source. Like experimental.scope it lives under the escape hatch so a config written for a newer CLI keeps loading on an older pinned one (docs/architecture/08-project-config.md §Config evolution policy).
const ScopeKey = "scope"
ScopeKey is the subkey under `experimental:` that carries the estate declaration. It lives under the escape hatch rather than at the top level so a config written for a newer CLI keeps loading on an older pinned one — the loader runs with KnownFields(true), which would hard-fail any brand-new top-level key. See docs/architecture/08-project-config.md §Config evolution policy.
const VendorsKey = "vendors"
VendorsKey is the subkey under `experimental:` that carries the third-party register. It lives under the escape hatch rather than at the top level for the same reason ScopeKey does: the loader runs with KnownFields(true), so a brand-new top-level key would hard-fail every older pinned CLI that predates it. See docs/architecture/08-project-config.md §Config evolution policy.
Variables ¶
This section is empty.
Functions ¶
func LoadEvidenceType ¶
func LoadEvidenceType(data []byte) (core.EvidenceType, error)
LoadEvidenceType parses a JSON Schema document declaring an evidence type and returns the L1 core.EvidenceType. See docs/architecture/04-source-plugins.md §Evidence types for the canonical file shape.
The Schema field on the returned value is the original bytes verbatim — record-against-schema validation happens at L4 (M6) using a JSON Schema library; this loader only checks the wrapper fields it needs.
func LoadPolicy ¶
LoadPolicy parses a policy.yaml document and returns the L1 core.Policy with all fields populated.
Out of scope at L0: verifying that `control` exists in the framework's control catalog, that `rule` resolves in the rule registry, and that each `slot.type` exists in the evidence-type registry. Those are cross-spec checks handled by the planner (L3).
func ValidatePassWhen ¶ added in v0.68.0
ValidatePassWhen checks a policy's pass_when clauses and slot roles against the DSL's semantic rules. The YAML loader calls it for every project-local policy; each built-in framework's tests call it for every Go-built policy, which never pass through the loader.
It checks: every clause slot and every matches_in in_slot (in condition, filter and nested trees) is declared; operators, fields and values are well-formed; matches_in has in_slot and remote_field, a known normalize, and no matches_in inside its where; the matches_in-only keys appear on no other operator; slot roles are valid, at most one slot is the roster, a roster_subject slot needs a roster slot, and role slots accept disjoint evidence types (a record's envelope path carries no slot name, so an overlap would be ambiguous).
Types ¶
type BindingEntry ¶
type BindingEntry struct {
Source string `yaml:"source"`
SlotParams map[string]any `yaml:"slot_params"`
}
BindingEntry is one element of a slot binding list. It accepts both the string shape ("aws.iam") and the object shape with optional slot_params ({source: aws.iam, slot_params: {...}}).
func (*BindingEntry) UnmarshalYAML ¶
func (b *BindingEntry) UnmarshalYAML(node *yaml.Node) error
UnmarshalYAML accepts either a scalar string or a mapping; both are valid shapes in .sigcomply.yaml.
type CIConfig ¶
type CIConfig struct {
FailOnViolation *bool `yaml:"fail_on_violation"`
FailSeverity core.Severity `yaml:"fail_severity"`
}
CIConfig models CI exit-code behavior knobs.
type CloudConfig ¶
CloudConfig models the cloud submission section.
type ControlConfig ¶ added in v0.35.2
type ControlConfig struct {
Applicability string `yaml:"applicability"` // "" | applicable | not_applicable
Reason string `yaml:"reason"`
ApprovedBy string `yaml:"approved_by"`
// Justification is the reason a control is *included*, the other
// half of what ISO/IEC 27001:2022 6.1.3 d asks a Statement of
// Applicability to record. Reason answers "why not this control";
// Justification answers "why this one". Optional: when it is absent
// `report --view soa` derives one from the checks standing behind
// the control, which is accurate but generic — an auditor reading a
// control the organization reasoned carefully about deserves that
// reasoning.
Justification string `yaml:"justification"`
}
ControlConfig is a control-level decision — the coarse governance axis authored per control (the unit auditors and the ISO Statement of Applicability think in), not per check. applicability: not_applicable cascades to every policy mapping to this control. Reason/ApprovedBy give the audit trail; Owner/InheritedFrom are reserved for later.
type ControlSpec ¶
type ControlSpec struct {
ID string `yaml:"id"`
Name string `yaml:"name"`
Description string `yaml:"description"`
Category string `yaml:"category"`
BaselineSeverity core.Severity `yaml:"baseline_severity"`
// Kind is "catalog" (the default) or "management_system". A
// project-local framework extension needs this for the same reason
// the shipped ones do: without it a management-system requirement
// can be declared not_applicable, which cascades every policy under
// it to N/A and raises the compliance score for declining to have a
// management system. Decoding is strict, so an unrecognized value
// is rejected rather than silently read as the default.
Kind core.ControlKind `yaml:"kind"`
}
ControlSpec mirrors core.Control for YAML decoding.
type CustomPeriod ¶
type CustomPeriod struct {
ID string `yaml:"id"`
Start string `yaml:"start"`
End string `yaml:"end"`
}
CustomPeriod is one entry in a custom fiscal_calendar.periods list.
type ExceptionScope ¶
type ExceptionScope struct {
ResourceID string `yaml:"resource_id"`
ResourcePattern string `yaml:"resource_pattern"`
}
ExceptionScope narrows an exception to specific resources.
type ExtensionsConfig ¶
type ExtensionsConfig struct {
Path string `yaml:"path"`
}
ExtensionsConfig overrides extension discovery paths.
type FiscalCalendarConfig ¶
type FiscalCalendarConfig struct {
Type string `yaml:"type"`
Starts string `yaml:"starts"`
Periods []CustomPeriod `yaml:"periods"`
}
FiscalCalendarConfig models the period.fiscal_calendar subsection.
type FrameworkSpec ¶
type FrameworkSpec struct {
SchemaVersion string `yaml:"schema_version"`
IDValue string `yaml:"id"`
VersionValue string `yaml:"version"`
DisplayName string `yaml:"display_name"`
Description string `yaml:"description"`
ControlsList []ControlSpec `yaml:"controls"`
PoliciesList []PolicyRefSpec `yaml:"policies"`
}
FrameworkSpec is the loaded representation of a framework.yaml file. It satisfies core.Framework so callers may register the value directly with the framework registry.
A framework.yaml carries the framework's identity, the catalog of controls it certifies against, and the list of policy IDs that contribute evaluations. Policy specs themselves live in separate files (one directory per policy, per docs/architecture/03-policy- spec.md) — the framework spec only references them by ID.
func LoadFramework ¶
func LoadFramework(data []byte) (*FrameworkSpec, error)
LoadFramework parses a framework.yaml document. The returned value satisfies core.Framework and may be registered directly.
func (*FrameworkSpec) Controls ¶
func (f *FrameworkSpec) Controls() []core.Control
Controls implements core.Framework.
func (*FrameworkSpec) Policies ¶
func (f *FrameworkSpec) Policies() []core.PolicyRef
Policies implements core.Framework.
func (*FrameworkSpec) Version ¶
func (f *FrameworkSpec) Version() string
Version implements core.Framework.
type OutputConfig ¶
type OutputConfig struct {
Format string `yaml:"format"`
JSONPath string `yaml:"json_path"`
Verbose bool `yaml:"verbose"`
}
OutputConfig models output preferences.
type PeriodConfig ¶
type PeriodConfig struct {
FiscalCalendar FiscalCalendarConfig `yaml:"fiscal_calendar"`
TimeBasis string `yaml:"time_basis"`
}
PeriodConfig models the period section.
type PluginConfigField ¶
type PluginConfigField struct {
Type string `yaml:"type"`
Required bool `yaml:"required"`
Default any `yaml:"default"`
Enum []any `yaml:"enum"`
Description string `yaml:"description"`
}
PluginConfigField describes one knob in the plugin's config_schema. Default is left as `any` because the static type is named by the Type field (`string`, `int`, `bool`, etc.).
type PluginCredentialRequirement ¶
type PluginCredentialRequirement struct {
Source string `yaml:"source"`
EnvVar string `yaml:"env_var,omitempty"`
}
PluginCredentialRequirement declares one credential the plugin needs. `Source` names the mechanism (e.g. env_or_default_chain, env, file).
type PluginManifest ¶
type PluginManifest struct {
SchemaVersion string `yaml:"schema_version"`
ID string `yaml:"id"`
DisplayName string `yaml:"display_name"`
Version string `yaml:"version"`
Description string `yaml:"description"`
Emits []string `yaml:"emits"`
Singleton bool `yaml:"singleton"`
ConfigSchema map[string]PluginConfigField `yaml:"config_schema"`
RequiresCredentials []PluginCredentialRequirement `yaml:"requires_credentials"`
}
PluginManifest is the in-memory shape of a plugin's plugin.yaml. It declares the plugin's identity, the evidence types it emits, and the config_schema customers fill in under sources.<id> in .sigcomply.yaml. See docs/architecture/04-source-plugins.md.
There is no L1 core type for plugin manifests — they are metadata the planner (L3) reads to validate project config and build SlotRequest values; nothing flows from a manifest into the evidence or aggregation paths.
func LoadPluginManifest ¶
func LoadPluginManifest(data []byte) (PluginManifest, error)
LoadPluginManifest parses a plugin manifest YAML document. Unknown fields are rejected (typo-safety); refer to the comment-doc on the PluginManifest struct for the field set.
type PolicyConfig ¶ added in v0.35.2
type PolicyConfig struct {
Bindings map[string][]BindingEntry `yaml:"bindings"`
Parameters map[string]any `yaml:"parameters"`
Cadence string `yaml:"cadence"`
EvidenceMode string `yaml:"evidence_mode"`
CatalogEntry string `yaml:"catalog_entry"`
Exceptions []PolicyException `yaml:"exceptions"`
}
PolicyConfig is the per-policy configuration object — everything a project can override or declare about one policy, co-located under its ID in the policies: map. Every field is optional; an absent policy entry means "framework defaults, auto-bound sources, no exceptions."
policies:
soc2.cc6.1.mfa_enforced_all_users:
cadence: hourly
bindings: { evidence: [okta, acme.internal_iam] }
parameters: { exempt_service_accounts: false }
exceptions:
- scope: { resource_id: "okta_user:bot@acme.com" }
state: waived
reason: "Legacy deploy bot; retired by Q3."
expires_at: 2026-09-30
New per-policy dimensions (severity, scope, owner, enabled) are added here as new optional fields — additive, no schema bump.
type PolicyException ¶ added in v0.35.2
type PolicyException struct {
Scope ExceptionScope `yaml:"scope"`
State string `yaml:"state"`
Reason string `yaml:"reason"`
ApprovedBy string `yaml:"approved_by"`
ApprovedAt string `yaml:"approved_at"`
ExpiresAt string `yaml:"expires_at"`
}
PolicyException is one declarative waiver or N/A entry on a policy. The owning policy is the map key in PolicyConfig — there is no policy field. Multiple entries on one policy support distinct resource scopes.
type PolicyRefSpec ¶
type PolicyRefSpec struct {
ID string `yaml:"id"`
}
PolicyRefSpec is one entry in the framework's policies list — a reference to a policy by ID. The actual policy.yaml lives elsewhere.
type ProjectConfig ¶
type ProjectConfig struct {
SchemaVersion string `yaml:"schema_version"`
Framework string `yaml:"framework"`
Period PeriodConfig `yaml:"period"`
Vault VaultConfig `yaml:"vault"`
Sources map[string]map[string]any `yaml:"sources"`
// Policies holds all per-policy configuration as one object per
// policy ID — bindings, parameter overrides, cadence override,
// evidence_mode override, and scoped exceptions co-located in a single
// place. This replaces the former family of parallel policy-ID-keyed
// maps (bindings/policy_parameters/policy_cadences/policy_overrides/
// exceptions). The shape is what makes the config extensible without a
// schema bump: a new per-policy dimension (severity, scope, owner) is a
// new optional field on PolicyConfig — additive, never a new top-level
// section. See docs/architecture/08-project-config.md §The binding model.
Policies map[string]PolicyConfig `yaml:"policies"`
// Controls holds control-level decisions — the coarse, governance
// axis that is naturally per-control, not per-check: applicability
// (the ISO Statement of Applicability), and room for ownership and
// inheritance. A control marked not_applicable cascades to every
// policy that maps to it (planner sets status=na). Fine-grained,
// resource-scoped waivers stay under Policies[id].exceptions.
Controls map[string]ControlConfig `yaml:"controls"`
Cloud CloudConfig `yaml:"cloud"`
CIEnvironment map[string]any `yaml:"ci_environment"`
Output OutputConfig `yaml:"output"`
CI CIConfig `yaml:"ci"`
Extensions ExtensionsConfig `yaml:"extensions"`
// Experimental is the forward-compatibility escape hatch. The loader
// runs with KnownFields(true) so a typo in a recognized key is a loud
// error — but that strictness would also make any *new* top-level key
// break older CLIs that predate it. New, not-yet-stable fields are
// introduced under `experimental:` first: every CLI version that has
// this field tolerates (and ignores) experimental subkeys it does not
// understand, so a newer config never hard-fails an older pinned CLI.
// A field graduates from `experimental.<name>` to a first-class
// top-level key in a later release. The loader does not interpret
// anything in here; individual features opt in by reading their own
// key. See docs/architecture/08-project-config.md §Config evolution.
Experimental map[string]any `yaml:"experimental"`
// ProjectLocalPolicies are PolicyRefs for policies discovered under
// .sigcomply/policies/*/policy.yaml at bootstrap and registered into
// the policy registry. The planner unions these with the active
// framework's own Policies() so a customer-authored YAML policy is
// actually planned and evaluated. yaml:"-" — populated by the
// orchestrator's project-local loader, never decoded from the file.
ProjectLocalPolicies []core.PolicyRef `yaml:"-"`
}
ProjectConfig is the parsed shape of .sigcomply.yaml. See docs/architecture/08-project-config.md for the canonical field reference. Cross-spec resolution (does this binding's source exist? is this parameter declared on the named policy?) is the planner's job (L3 / M4) — this loader only validates what's expressible without the registries.
func LoadProjectConfig ¶
func LoadProjectConfig(data []byte) (ProjectConfig, error)
LoadProjectConfig parses a .sigcomply.yaml document. The file is the customer's source of truth, so all error messages name the field path to make corrections obvious.
func (*ProjectConfig) BindingsFor ¶ added in v0.35.2
func (c *ProjectConfig) BindingsFor(policyID string) map[string][]BindingEntry
BindingsFor returns the slot→entries map a project declared for a policy, or nil if the policy has no entry. nil means "auto-bind every configured source whose Emits() intersects the slot" — the planner's substitutability default.
func (*ProjectConfig) CadenceFor ¶ added in v0.35.2
func (c *ProjectConfig) CadenceFor(policyID string) string
CadenceFor returns the cadence override for a policy, or "" (use the framework default).
func (*ProjectConfig) EvidenceModeOverrideFor ¶ added in v0.35.2
func (c *ProjectConfig) EvidenceModeOverrideFor(policyID string) (mode, catalogEntry string)
EvidenceModeOverrideFor returns the (mode, catalog_entry) override for a policy. mode is "" when the project does not override the framework's evidence_mode for this policy.
func (*ProjectConfig) ExceptionsFor ¶ added in v0.35.2
func (c *ProjectConfig) ExceptionsFor(policyID string) []PolicyException
ExceptionsFor returns the scoped waiver/NA entries a project declared for a policy, or nil.
func (*ProjectConfig) ParametersFor ¶ added in v0.35.2
func (c *ProjectConfig) ParametersFor(policyID string) map[string]any
ParametersFor returns the parameter overrides a project declared for a policy, or nil.
type Risk ¶ added in v0.82.0
type Risk struct {
// ID is a short stable slug, e.g. "r-001" or "laptop_theft".
ID string `yaml:"id"`
// Description is what the risk actually is, in the operator's words.
Description string `yaml:"description"`
// Owner is the person accountable for the risk — ISO 27001 6.1.2 c)
// requires every risk to have one.
Owner string `yaml:"owner"`
// Level is the evaluated risk level before treatment.
Level string `yaml:"level"`
// Treatment is the chosen option: modify, retain, avoid or share.
Treatment string `yaml:"treatment"`
// Controls names the Annex A control IDs chosen to treat this risk.
// This is the edge the Statement of Applicability joins on, and the
// whole reason the register exists.
Controls []string `yaml:"controls"`
// ResidualLevel is the level that remains after treatment.
ResidualLevel string `yaml:"residual_level"`
// AcceptedBy and AcceptanceRationale record who signed off on
// retaining a risk and why. Required when Treatment is "retain",
// which is the only option that produces no control.
AcceptedBy string `yaml:"accepted_by"`
AcceptanceRationale string `yaml:"acceptance_rationale"`
// AssessedAt is the date this risk was last assessed (YYYY-MM-DD).
// Compared arithmetically against the audit period — the only thing
// that catches a register nobody has revisited in three years.
AssessedAt string `yaml:"assessed_at"`
}
Risk is one row of the register.
Every field is declared, never parsed or inferred — the same idiom as the vendor register's assurance_period_end. The CLI checks the shape and the arithmetic of the dates; it never reads a document to confirm any of it, and it never second-guesses the operator's evaluation.
type RiskRegister ¶ added in v0.82.0
type RiskRegister struct {
// DeclaredBy and DeclaredAt are an optional audit trail: who last
// reviewed the register and when.
DeclaredBy string
DeclaredAt string
// Risks is the declared register, sorted by ID.
Risks []Risk
// UnknownKeys names subkeys under experimental.risks the loader did
// not recognize, so a typo is reported rather than silently ignored.
UnknownKeys []string
}
RiskRegister is the operator's declaration of the information security risks this project has assessed, and the Annex A controls chosen to treat them.
It exists to close the one gap the Statement of Applicability cannot close on its own. `report --view soa` already discharges ISO/IEC 27001:2022 6.1.3 d) — which controls are necessary, why each is included, whether it is implemented, and why any were excluded. What it has never been able to say is *which risk made a control necessary*, because nothing in the vault or the framework carries that edge. Risk→control traceability is a classic certification finding, and the answer only exists in the operator's head until they write it down somewhere the tool can read.
This is a register, not a replacement for a document. The four ISO clause entries that ask for the risk assessment process, the risk treatment process and the retained documented information (C.6.1.2, C.6.1.3, C.8.2, C.8.3) are untouched and still owe their uploads. The argument in docs/architecture/13-isms-clauses-and-soa.md — that rendering a management artifact as structured fields reproduces the overclaim those entries exist to retire — is why this is additive: a risk register is genuinely tabular where minutes are not, and it supplements the document rather than standing in for it.
Nothing in here crosses the aggregation boundary. Risk descriptions are a map of where an organization believes it is weakest and owner addresses are personal identifiers; both stay vault-side. The SoA is a vault-read-only report, and no risk field has any path to the SubmissionPayload (Invariant #1).
func LoadRiskRegister ¶ added in v0.82.0
func LoadRiskRegister(cfg *ProjectConfig) (*RiskRegister, error)
LoadRiskRegister projects the experimental.risks block out of a loaded project config. Returns (nil, nil) when no register is declared.
func (*RiskRegister) ControlRisks ¶ added in v0.82.0
func (r *RiskRegister) ControlRisks() map[string][]string
ControlRisks indexes the register by control ID, returning the sorted risk IDs that name each control. This is the join the Statement of Applicability consumes. A nil register yields a nil map, so the SoA renders exactly as before when no register is declared.
func (*RiskRegister) Declared ¶ added in v0.82.0
func (r *RiskRegister) Declared() bool
Declared reports whether a register was declared at all. A nil register is the undeclared case, which is not an error: the risk register is optional, and a project without one behaves exactly as it did before this existed.
func (*RiskRegister) DeclaredControls ¶ added in v0.82.0
func (r *RiskRegister) DeclaredControls() []string
DeclaredControls returns every control ID named by any risk, sorted. Used to report a risk that names a control the framework does not have — a typo that would otherwise just fail to join, silently.
type RosterConfig ¶ added in v0.68.0
type RosterConfig struct {
// Source is the designated roster source ID. Required when the
// block is present. Bracketed multi-instance IDs are not supported.
Source string
// Aliases maps source ID → lowercased account name → roster email,
// for accounts whose own email is absent or differs from the
// roster's. Account names match a record's id or payload.username,
// case-insensitively.
Aliases map[string]map[string]string
// NonHuman maps source ID → lowercased account names that are not
// people (CI bots, deploy users). Sorted and de-duplicated.
NonHuman map[string][]string
// UnknownKeys are subkeys of experimental.roster this CLI does not
// understand — tolerated, never fatal, but surfaced as warnings.
UnknownKeys []string
}
RosterConfig designates the one source whose people are the authoritative roster, plus the operator's declarations for accounts that cannot be linked to the roster by email alone.
The roster must come from a system other than the one being checked: the planner never binds the roster source to a policy's accounts slot (a directory cannot vouch for its own accounts).
Nothing in here crosses the aggregation boundary. Aliases carry email addresses and account names — identity data that must never reach the cloud payload (root CLAUDE.md §Privacy invariant).
func LoadRosterConfig ¶ added in v0.68.0
func LoadRosterConfig(cfg *ProjectConfig) (*RosterConfig, error)
LoadRosterConfig projects the experimental.roster block out of a loaded project config. It returns (nil, nil) when the block is absent: no roster source is designated, so roster slots stay unbound and the policies that need one are skipped.
Validation here is shape-only. Whether the named sources are configured, and whether the roster source emits what a roster slot accepts, needs the registries and belongs to the planner.
type ScopeConfig ¶ added in v0.67.0
type ScopeConfig struct {
// DeclaredBy and DeclaredAt are the audit trail for the assertion,
// matching the approved_by/approved_at idiom used by exceptions and
// control applicability. Both optional.
DeclaredBy string
DeclaredAt string
// RequiredSources lists the source IDs this project asserts are in
// scope. Sorted on load so the derived report is deterministic
// (Core Principle #7 — auditors diff runs).
RequiredSources []string
// UnknownKeys are subkeys of experimental.scope this CLI does not
// understand. They are tolerated, never fatal — that tolerance is
// the whole point of the experimental: hatch — but they are
// surfaced so a typo is loud without breaking the run.
UnknownKeys []string
}
ScopeConfig is the operator's declaration of the estate this project asserts coverage over — the authoritative baseline the run is checked against, and the auditor-legible scope object.
Without it the CLI can only evaluate the evidence it happened to collect, against itself: a source the operator forgot to configure leaves its policies with an unbound required slot, which is skipped at evaluation and drops out of the compliance-score denominator. The run then reports a perfect score for an estate it never looked at. The declaration is what turns that silence into a finding.
Semantics are deliberately per-project, not per-organization: one project is one repo is one framework (ARCHITECTURE.md Core Principle #9), so this declares the estate *this project* asserts coverage over. Rolling several projects up into one view is the Cloud dashboard's job.
Nothing in here crosses the aggregation boundary. DeclaredBy is an email address and must never reach the cloud payload (root CLAUDE.md §Privacy invariant); the scope report is a vault-side artifact only.
func LoadScopeConfig ¶ added in v0.67.0
func LoadScopeConfig(cfg *ProjectConfig) (*ScopeConfig, error)
LoadScopeConfig projects the experimental.scope block out of a loaded project config. It returns (nil, nil) when the block is absent — the undeclared case, which leaves every existing behavior untouched.
Validation here is shape-only. Whether a declared source ID actually exists is a cross-reference question that needs the registries, and so belongs to the planner (see the layering note on LoadProjectConfig).
This shares its shape with LoadVendorRegister by design: both implement the experimental: hatch contract, and the duplication is the contract.
type VaultConfig ¶
VaultConfig selects an output vault backend and carries its backend-specific settings as an open bag — symmetric with the `sources:` config, where each plugin reads the keys it needs. Only `backend` is interpreted by the spec layer; every other key flows through to the backend's factory untouched. This is what makes a new destination backend additive: it reads its own keys from Config, with no typed struct field and no central validation switch to edit (see docs/architecture/05-vault-layout.md §Adding a backend).
The YAML stays flat for ergonomics — `vault: {backend: s3, bucket: x, region: y}` — via the custom UnmarshalYAML below: `backend` is lifted out and the remaining keys become Config.
func (VaultConfig) Bool ¶ added in v0.35.2
func (v VaultConfig) Bool(key string) bool
Bool returns the bool value of a Config key, or false if absent/non-bool.
func (VaultConfig) Str ¶ added in v0.35.2
func (v VaultConfig) Str(key string) string
Str returns the string value of a Config key, or "" if absent/non-string.
func (*VaultConfig) UnmarshalYAML ¶ added in v0.35.2
func (v *VaultConfig) UnmarshalYAML(node *yaml.Node) error
UnmarshalYAML accepts the flat `vault:` mapping, lifting `backend` into its own field and passing all other keys through as Config. Decoding into a map deliberately bypasses the parent decoder's KnownFields strictness for vault keys — the bag is open, exactly like `sources:`.
type Vendor ¶ added in v0.73.0
type Vendor struct {
// ID is the stable slug used to derive this vendor's policy ID and
// evidence folder. Changing it re-files the vendor's evidence, so it
// is deliberately separate from Name.
ID string
// Name is the third party as a human names it, used in policy
// descriptions and the `evidence due` listing.
Name string
// Tier is the organization's risk assessment. See the tier
// constants for what it obliges.
Tier string
// Subservice marks a vendor that performs part of the service
// itself — a SOC 2 subservice organization — rather than merely
// supplying the organization. It is what makes the complementary
// user entity controls (CUECs) in the vendor's own report relevant,
// so the CUEC mapping entry is the place those are reconciled.
Subservice bool
// Services is a free-text note on what the vendor does for the
// organization. Optional, advisory, never evaluated.
Services string
// AssurancePeriodEnd is the last day the vendor's own assurance
// report actually covers — the end of the SOC 2 Type II period, or
// the certificate's expiry.
//
// This is what closes the single most common CC9.2 finding. The
// temporal window only proves *when the file was uploaded*, so
// without this a FY2023 Type II uploaded this morning passes, and
// goes on passing every year forever. Comparing a declared coverage
// date against the audit period catches that arithmetically.
//
// It is DECLARED, never parsed: the CLI does not open the PDF and
// will not claim to. What it gives an auditor is an assertion
// recorded in version control, checked for currency on every run.
// Optional — absent means the freshness check does not run, and the
// guide says so in those words.
AssurancePeriodEnd string
// TierRationale and ApprovedBy are why this vendor sits at this
// tier, and who signed that off. Required for TierLow, which is the
// only tier that discharges its obligation with words instead of an
// artifact. Optional (but encouraged) elsewhere.
TierRationale string
ApprovedBy string
// Providers names the configured sources this vendor supplies —
// either a provider token ("aws", "github") or a full source ID
// ("aws.iam"). It exists for one purpose: to let the planner check
// the register against something observable.
//
// Nothing else can. A vendor the operator simply leaves out is
// invisible, because the register is a declaration with no
// counterpart to diff. But every *configured source* is itself a
// vendor the project demonstrably depends on, so the sources block
// is a baseline the register must at least cover. Joining the two
// needs a field, and this is it.
//
// Advisory, never fatal, and it discovers nothing: it cross-checks
// the register against sources the operator already declared, so it
// stays inside the operator-chosen estate. Optional — a vendor that
// supplies no configured source (a payroll processor, a law firm)
// leaves it empty, and that is the normal case.
Providers []string
}
Vendor is one third party in the register.
func (*Vendor) RequiresEvidence ¶ added in v0.73.0
RequiresEvidence reports whether this vendor must have a document on file each period. Every tier but TierLow does; see the tier constants for why TierLow is an approved exemption rather than an absence.
type VendorRegister ¶ added in v0.73.0
type VendorRegister struct {
// DeclaredBy and DeclaredAt are the audit trail for the assertion,
// matching the approved_by/approved_at idiom used by exceptions,
// control applicability and the scope declaration. Both optional.
DeclaredBy string
DeclaredAt string
// Vendors is the register itself, sorted by ID on load so every
// derived artifact is deterministic (Core Principle #7 — auditors
// diff runs).
Vendors []Vendor
// UnknownKeys are subkeys of experimental.vendors this CLI does not
// understand. Tolerated, never fatal — that tolerance is the point
// of the experimental: hatch — but surfaced so a typo is loud
// without breaking the run.
UnknownKeys []string
}
VendorRegister is the operator's declaration of the third parties this project depends on — the inventory SOC 2 CC9.1/CC9.2 and ISO 27001 A.5.19–A.5.23 expect an organization to maintain, expressed as reviewable configuration rather than as a PDF nobody can diff.
It is the set that the per-vendor assurance policies fan out over. Without it the vendor policies behave exactly as they did before this existed: one flat entry, one folder, one document.
Nothing in here crosses the aggregation boundary. Vendor names are third-party identifiers and DeclaredBy is an email address; both stay vault-side. The aggregator collapses the per-vendor results back onto their template policy before anything is submitted, so the cloud learns how many vendors lack evidence and never which ones exist.
func LoadVendorRegister ¶ added in v0.73.0
func LoadVendorRegister(cfg *ProjectConfig) (*VendorRegister, error)
LoadVendorRegister projects the experimental.vendors block out of a loaded project config. It returns (nil, nil) when the block is absent — the undeclared case, which leaves every existing behavior untouched.
Validation here is shape-only, matching LoadScopeConfig: whether a declared vendor has anything to do with a configured source is not a question this layer can answer. project one optional block out of experimental:, and the shared shape — tolerate unknown subkeys, round-trip through the marshaller, name the YAML key in every error — is the contract for that hatch. Factoring the two together would hide it behind a generic helper and make the next experimental block harder to write, not easier.
This shares its shape with LoadScopeConfig line for line, deliberately. Both project one optional block out of experimental:, and the shared shape — tolerate unknown subkeys, round-trip through the marshaller, name the YAML key in every error — is the contract for that hatch. Factoring the two together would hide it behind a generic helper and make the next experimental block harder to write, not easier.
func (*VendorRegister) EvidencedVendors ¶ added in v0.73.0
func (r *VendorRegister) EvidencedVendors() []Vendor
EvidencedVendors returns the vendors that owe an uploaded artifact, in load order (already sorted by ID).
func (*VendorRegister) SubserviceVendors ¶ added in v0.73.0
func (r *VendorRegister) SubserviceVendors() []Vendor
SubserviceVendors returns the vendors marked as subservice organizations — the ones whose own reports carry complementary user entity controls the organization must operate itself.