From c0976ef2a8e6a55caf73c23a8b4307a0947f908b Mon Sep 17 00:00:00 2001 From: Jennifer Power Date: Tue, 31 Mar 2026 17:00:19 -0400 Subject: [PATCH 1/2] feat: embed generated types into "sugared" types Signed-off-by: Jennifer Power --- control.go | 102 ++++++------------------------- control_catalog.go | 129 +++++++++++++--------------------------- control_catalog_yaml.go | 40 +++++++++++++ control_yaml.go | 47 +++++++++++++++ generated_types.go | 50 ++++++++++++++++ 5 files changed, 197 insertions(+), 171 deletions(-) create mode 100644 control_catalog_yaml.go create mode 100644 control_yaml.go diff --git a/control.go b/control.go index 8171725..c57b75e 100644 --- a/control.go +++ b/control.go @@ -1,90 +1,26 @@ -package gemara - -import "github.com/goccy/go-yaml" - -// Control describes a safeguard or countermeasure with a clear objective and assessment requirements -type Control struct { - // id allows this entry to be referenced by other elements - Id string `json:"id" yaml:"id"` - - // title describes the purpose of this control at a glance - Title string `json:"title" yaml:"title"` - - // objective is a unified statement of intent, which may encompass multiple situationally applicable requirements - Objective string `json:"objective" yaml:"objective"` - - // group references by id a catalog group that this control belongs to - Group string `json:"group" yaml:"group"` - - // assessment-requirements is a list of requirements that must be verified to confirm the control objective has been met - AssessmentRequirements []AssessmentRequirement `json:"assessment-requirements" yaml:"assessment-requirements"` - - // guidelines documents relationships between this control and Layer 1 guideline artifacts - Guidelines []MultiEntryMapping `json:"guidelines,omitempty" yaml:"guidelines,omitempty"` - - // threats documents relationships between this control and Layer 2 threat artifacts - Threats []MultiEntryMapping `json:"threats,omitempty" yaml:"threats,omitempty"` +// SPDX-License-Identifier: Apache-2.0 - // state is the lifecycle state of this control - State Lifecycle `json:"state" yaml:"state"` - - // replaced-by references the control that supersedes this one when deprecated or retired - ReplacedBy *EntryMapping `json:"replaced-by,omitempty" yaml:"replaced-by,omitempty"` - - references_cache []string -} - -// UnmarshalYAML allows decoding controls from older/alternate YAML schemas. -// In particular, it supports using `family` instead of the struct's `group` key. -func (c *Control) UnmarshalYAML(data []byte) error { - type controlYAML struct { - Id string `yaml:"id"` - Title string `yaml:"title"` - Objective string `yaml:"objective"` - Group string `yaml:"group,omitempty"` - Family string `yaml:"family,omitempty"` - - AssessmentRequirements []AssessmentRequirement `yaml:"assessment-requirements,omitempty"` - - Guidelines []MultiEntryMapping `yaml:"guidelines,omitempty"` - Threats []MultiEntryMapping `yaml:"threats,omitempty"` - - State Lifecycle `yaml:"state"` - ReplacedBy *EntryMapping `yaml:"replaced-by,omitempty"` - } - - var tmp controlYAML - if err := yaml.Unmarshal(data, &tmp); err != nil { - return err - } +package gemara - c.Id = tmp.Id - c.Title = tmp.Title - c.Objective = tmp.Objective - if tmp.Group != "" { - c.Group = tmp.Group - } else { - c.Group = tmp.Family - } +import "sync" - c.AssessmentRequirements = tmp.AssessmentRequirements - c.Guidelines = tmp.Guidelines - c.Threats = tmp.Threats - c.State = tmp.State - c.ReplacedBy = tmp.ReplacedBy +// SugarControl wraps the generated Control with cached +// cross-reference lookups. +type SugarControl struct { + Control - return nil + referencesOnce sync.Once + referencesCache []string } -func (c *Control) GetMappingReferences() (refs []string) { - if len(c.references_cache) > 0 { - return c.references_cache - } - for _, ref := range c.Guidelines { - refs = append(refs, ref.ReferenceId) - } - for _, ref := range c.Threats { - refs = append(refs, ref.ReferenceId) - } - return refs +func (c *SugarControl) GetMappingReferences() []string { + c.referencesOnce.Do(func() { + for _, ref := range c.Guidelines { + c.referencesCache = append(c.referencesCache, ref.ReferenceId) + } + for _, ref := range c.Threats { + c.referencesCache = append(c.referencesCache, ref.ReferenceId) + } + }) + return c.referencesCache } diff --git a/control_catalog.go b/control_catalog.go index 8c88356..0e20f59 100644 --- a/control_catalog.go +++ b/control_catalog.go @@ -1,104 +1,57 @@ -package gemara - -import ( - "slices" - - "github.com/goccy/go-yaml" -) - -// ControlCatalog describes a set of related controls and relevant metadata -type ControlCatalog struct { - // title describes the purpose of this catalog at a glance - Title string `json:"title" yaml:"title"` - - // metadata provides detailed data about this catalog - Metadata Metadata `json:"metadata" yaml:"metadata"` - - // controls is a list of unique controls defined by this catalog - Controls []Control `json:"controls,omitempty" yaml:"controls,omitempty"` - - // groups contains a list of groups that can be referenced by entries in this catalog - Groups []Group `json:"groups,omitempty" yaml:"groups,omitempty"` - - // extends references catalogs that this catalog builds upon - Extends []ArtifactMapping `json:"extends,omitempty" yaml:"extends,omitempty"` +// SPDX-License-Identifier: Apache-2.0 - Imports []MultiEntryMapping `json:"imports,omitempty" yaml:"imports,omitempty"` - - groups_cache []string - controls_cache map[string][]Control - requirements_cache map[string][]AssessmentRequirement -} - -// UnmarshalYAML allows decoding control catalogs from older/alternate YAML schemas. -// It supports mapping `families` -> `groups`. -func (c *ControlCatalog) UnmarshalYAML(data []byte) error { - type controlCatalogYAML struct { - Groups []Group `yaml:"groups,omitempty"` - Families []Group `yaml:"families,omitempty"` - - Title string `yaml:"title"` - Metadata Metadata `yaml:"metadata"` - - Extends []ArtifactMapping `yaml:"extends,omitempty"` - Imports []MultiEntryMapping `yaml:"imports,omitempty"` - - Controls []Control `yaml:"controls,omitempty"` - } +package gemara - var tmp controlCatalogYAML - if err := yaml.Unmarshal(data, &tmp); err != nil { - return err - } +import "sync" - c.Groups = tmp.Groups - if len(c.Groups) == 0 { - c.Groups = tmp.Families - } - c.Controls = tmp.Controls +// SugarControlCatalog wraps the generated ControlCatalog with +// pre-built indexes for efficient group, control, and requirement lookups. +type SugarControlCatalog struct { + ControlCatalog - c.Title = tmp.Title - c.Metadata = tmp.Metadata - c.Extends = tmp.Extends + groupsOnce sync.Once + groupsCache []string - // Keep imports exactly as decoded (nil vs empty can matter to tests). - c.Imports = tmp.Imports + controlsOnce sync.Once + controlsCache map[string][]Control - return nil + requirementsOnce sync.Once + requirementsCache map[string][]AssessmentRequirement } -func (c *ControlCatalog) GetGroupNames() (groups []string) { - if len(c.groups_cache) > 0 { - return c.groups_cache - } - for _, group := range c.Groups { - groups = append(groups, group.Title) - } - return groups +func (c *SugarControlCatalog) GetGroupNames() []string { + c.groupsOnce.Do(func() { + for _, group := range c.Groups { + c.groupsCache = append(c.groupsCache, group.Title) + } + }) + return c.groupsCache } -func (c *ControlCatalog) GetControlsForGroup(group string) (controls []Control) { - if c.controls_cache != nil && len(c.controls_cache[group]) > 0 { - return c.controls_cache[group] - } - for _, control := range c.Controls { - if control.Group == group { - controls = append(controls, control) +func (c *SugarControlCatalog) GetControlsForGroup(group string) []Control { + c.controlsOnce.Do(func() { + c.controlsCache = make(map[string][]Control) + for _, control := range c.Controls { + c.controlsCache[control.Group] = append( + c.controlsCache[control.Group], control, + ) } - } - return controls + }) + return c.controlsCache[group] } -func (c *ControlCatalog) GetRequirementForApplicability(applicability string) (reqs []AssessmentRequirement) { - if c.requirements_cache != nil && len(c.requirements_cache[applicability]) > 0 { - return c.requirements_cache[applicability] - } - for _, control := range c.Controls { - for _, assessment := range control.AssessmentRequirements { - if slices.Contains(assessment.Applicability, applicability) { - reqs = append(reqs, assessment) +func (c *SugarControlCatalog) GetRequirementForApplicability(applicability string) []AssessmentRequirement { + c.requirementsOnce.Do(func() { + c.requirementsCache = make(map[string][]AssessmentRequirement) + for _, control := range c.Controls { + for _, req := range control.AssessmentRequirements { + for _, app := range req.Applicability { + c.requirementsCache[app] = append( + c.requirementsCache[app], req, + ) + } } } - } - return reqs + }) + return c.requirementsCache[applicability] } diff --git a/control_catalog_yaml.go b/control_catalog_yaml.go new file mode 100644 index 0000000..ff6b286 --- /dev/null +++ b/control_catalog_yaml.go @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: Apache-2.0 + +package gemara + +import "github.com/goccy/go-yaml" + +// UnmarshalYAML allows decoding control catalogs from older/alternate YAML schemas. +// It supports mapping `families` -> `groups`. +func (c *ControlCatalog) UnmarshalYAML(data []byte) error { + type controlCatalogYAML struct { + Groups []Group `yaml:"groups,omitempty"` + Families []Group `yaml:"families,omitempty"` + + Title string `yaml:"title"` + Metadata Metadata `yaml:"metadata"` + + Extends []ArtifactMapping `yaml:"extends,omitempty"` + Imports []MultiEntryMapping `yaml:"imports,omitempty"` + + Controls []Control `yaml:"controls,omitempty"` + } + + var tmp controlCatalogYAML + if err := yaml.Unmarshal(data, &tmp); err != nil { + return err + } + + c.Groups = tmp.Groups + if len(c.Groups) == 0 { + c.Groups = tmp.Families + } + c.Controls = tmp.Controls + + c.Title = tmp.Title + c.Metadata = tmp.Metadata + c.Extends = tmp.Extends + c.Imports = tmp.Imports + + return nil +} diff --git a/control_yaml.go b/control_yaml.go new file mode 100644 index 0000000..4e766d1 --- /dev/null +++ b/control_yaml.go @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: Apache-2.0 + +package gemara + +import "github.com/goccy/go-yaml" + +// UnmarshalYAML allows decoding controls from older/alternate YAML schemas. +// In particular, it supports using `family` instead of the struct's `group` key. +func (c *Control) UnmarshalYAML(data []byte) error { + type controlYAML struct { + Id string `yaml:"id"` + Title string `yaml:"title"` + Objective string `yaml:"objective"` + Group string `yaml:"group,omitempty"` + Family string `yaml:"family,omitempty"` + + AssessmentRequirements []AssessmentRequirement `yaml:"assessment-requirements,omitempty"` + + Guidelines []MultiEntryMapping `yaml:"guidelines,omitempty"` + Threats []MultiEntryMapping `yaml:"threats,omitempty"` + + State Lifecycle `yaml:"state"` + ReplacedBy *EntryMapping `yaml:"replaced-by,omitempty"` + } + + var tmp controlYAML + if err := yaml.Unmarshal(data, &tmp); err != nil { + return err + } + + c.Id = tmp.Id + c.Title = tmp.Title + c.Objective = tmp.Objective + if tmp.Group != "" { + c.Group = tmp.Group + } else { + c.Group = tmp.Family + } + + c.AssessmentRequirements = tmp.AssessmentRequirements + c.Guidelines = tmp.Guidelines + c.Threats = tmp.Threats + c.State = tmp.State + c.ReplacedBy = tmp.ReplacedBy + + return nil +} diff --git a/generated_types.go b/generated_types.go index 1e23a04..15eb4e4 100644 --- a/generated_types.go +++ b/generated_types.go @@ -246,6 +246,56 @@ type Catalog struct { Imports []MultiEntryMapping `json:"imports,omitempty" yaml:"imports,omitempty"` } +// ControlCatalog describes a set of related controls and relevant metadata +type ControlCatalog struct { + // title describes the purpose of this catalog at a glance + Title string `json:"title" yaml:"title"` + + // metadata provides detailed data about this catalog + Metadata Metadata `json:"metadata" yaml:"metadata"` + + // controls is a list of unique controls defined by this catalog + Controls []Control `json:"controls,omitempty" yaml:"controls,omitempty"` + + // groups contains a list of groups that can be referenced by entries in this catalog + Groups []Group `json:"groups,omitempty" yaml:"groups,omitempty"` + + // extends references catalogs that this catalog builds upon + Extends []ArtifactMapping `json:"extends,omitempty" yaml:"extends,omitempty"` + + Imports []MultiEntryMapping `json:"imports,omitempty" yaml:"imports,omitempty"` +} + +// Control describes a safeguard or countermeasure with a clear objective and assessment requirements +type Control struct { + // id allows this entry to be referenced by other elements + Id string `json:"id" yaml:"id"` + + // title describes the purpose of this control at a glance + Title string `json:"title" yaml:"title"` + + // objective is a unified statement of intent, which may encompass multiple situationally applicable requirements + Objective string `json:"objective" yaml:"objective"` + + // group references by id a catalog group that this control belongs to + Group string `json:"group" yaml:"group"` + + // assessment-requirements is a list of requirements that must be verified to confirm the control objective has been met + AssessmentRequirements []AssessmentRequirement `json:"assessment-requirements" yaml:"assessment-requirements"` + + // guidelines documents relationships between this control and Layer 1 guideline artifacts + Guidelines []MultiEntryMapping `json:"guidelines,omitempty" yaml:"guidelines,omitempty"` + + // threats documents relationships between this control and Layer 2 threat artifacts + Threats []MultiEntryMapping `json:"threats,omitempty" yaml:"threats,omitempty"` + + // state is the lifecycle state of this control + State Lifecycle `json:"state" yaml:"state"` + + // replaced-by references the control that supersedes this one when deprecated or retired + ReplacedBy *EntryMapping `json:"replaced-by,omitempty" yaml:"replaced-by,omitempty"` +} + // AssessmentRequirement describes a tightly scoped, verifiable condition that must be satisfied and confirmed by an evaluator type AssessmentRequirement struct { // id allows this entry to be referenced by other elements From 3832ce0fc529230e0cb6bee867b0082bd7665c86 Mon Sep 17 00:00:00 2001 From: Jennifer Power Date: Tue, 31 Mar 2026 17:26:46 -0400 Subject: [PATCH 2/2] feat: add Sugar() accessors and rename to SugaredControl Signed-off-by: Jennifer Power --- control.go | 14 ++++++++----- control_catalog.go | 52 +++++++++++++++++++++++++++++++--------------- 2 files changed, 44 insertions(+), 22 deletions(-) diff --git a/control.go b/control.go index c57b75e..d247039 100644 --- a/control.go +++ b/control.go @@ -1,19 +1,23 @@ -// SPDX-License-Identifier: Apache-2.0 - package gemara import "sync" -// SugarControl wraps the generated Control with cached +// SugaredControl wraps the generated Control with cached // cross-reference lookups. -type SugarControl struct { +type SugaredControl struct { Control referencesOnce sync.Once referencesCache []string } -func (c *SugarControl) GetMappingReferences() []string { +// Sugar wraps this Control in a SugaredControl for convenient +// cached helper access. +func (c Control) Sugar() *SugaredControl { + return &SugaredControl{Control: c} +} + +func (c *SugaredControl) GetMappingReferences() []string { c.referencesOnce.Do(func() { for _, ref := range c.Guidelines { c.referencesCache = append(c.referencesCache, ref.ReferenceId) diff --git a/control_catalog.go b/control_catalog.go index 0e20f59..b52ec91 100644 --- a/control_catalog.go +++ b/control_catalog.go @@ -1,25 +1,43 @@ -// SPDX-License-Identifier: Apache-2.0 - package gemara import "sync" -// SugarControlCatalog wraps the generated ControlCatalog with +// SugaredControlCatalog wraps the generated ControlCatalog with // pre-built indexes for efficient group, control, and requirement lookups. -type SugarControlCatalog struct { +type SugaredControlCatalog struct { ControlCatalog - groupsOnce sync.Once - groupsCache []string + groupsOnce sync.Once + groupsCache []string + + sugarControlsOnce sync.Once + sugarControlsCache []*SugaredControl - controlsOnce sync.Once - controlsCache map[string][]Control + controlsByGroupOnce sync.Once + controlsByGroupCache map[string][]*SugaredControl requirementsOnce sync.Once requirementsCache map[string][]AssessmentRequirement } -func (c *SugarControlCatalog) GetGroupNames() []string { +// Sugar wraps this ControlCatalog in a SugaredControlCatalog for +// convenient cached helper access. +func (c ControlCatalog) Sugar() *SugaredControlCatalog { + return &SugaredControlCatalog{ControlCatalog: c} +} + +// SugaredControls returns all controls as cached SugaredControl instances. +func (c *SugaredControlCatalog) SugaredControls() []*SugaredControl { + c.sugarControlsOnce.Do(func() { + c.sugarControlsCache = make([]*SugaredControl, len(c.Controls)) + for i := range c.Controls { + c.sugarControlsCache[i] = &SugaredControl{Control: c.Controls[i]} + } + }) + return c.sugarControlsCache +} + +func (c *SugaredControlCatalog) GetGroupNames() []string { c.groupsOnce.Do(func() { for _, group := range c.Groups { c.groupsCache = append(c.groupsCache, group.Title) @@ -28,19 +46,19 @@ func (c *SugarControlCatalog) GetGroupNames() []string { return c.groupsCache } -func (c *SugarControlCatalog) GetControlsForGroup(group string) []Control { - c.controlsOnce.Do(func() { - c.controlsCache = make(map[string][]Control) - for _, control := range c.Controls { - c.controlsCache[control.Group] = append( - c.controlsCache[control.Group], control, +func (c *SugaredControlCatalog) GetControlsForGroup(group string) []*SugaredControl { + c.controlsByGroupOnce.Do(func() { + c.controlsByGroupCache = make(map[string][]*SugaredControl) + for _, sc := range c.SugaredControls() { + c.controlsByGroupCache[sc.Group] = append( + c.controlsByGroupCache[sc.Group], sc, ) } }) - return c.controlsCache[group] + return c.controlsByGroupCache[group] } -func (c *SugarControlCatalog) GetRequirementForApplicability(applicability string) []AssessmentRequirement { +func (c *SugaredControlCatalog) GetRequirementForApplicability(applicability string) []AssessmentRequirement { c.requirementsOnce.Do(func() { c.requirementsCache = make(map[string][]AssessmentRequirement) for _, control := range c.Controls {