diff --git a/control.go b/control.go index 8171725..d247039 100644 --- a/control.go +++ b/control.go @@ -1,90 +1,30 @@ package gemara -import "github.com/goccy/go-yaml" +import "sync" -// 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"` +// SugaredControl wraps the generated Control with cached +// cross-reference lookups. +type SugaredControl struct { + Control - // 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"` - - references_cache []string + referencesOnce sync.Once + referencesCache []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 - } - - 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 +// Sugar wraps this Control in a SugaredControl for convenient +// cached helper access. +func (c Control) Sugar() *SugaredControl { + return &SugaredControl{Control: c} } -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 *SugaredControl) 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..b52ec91 100644 --- a/control_catalog.go +++ b/control_catalog.go @@ -1,104 +1,75 @@ package gemara -import ( - "slices" +import "sync" - "github.com/goccy/go-yaml" -) +// SugaredControlCatalog wraps the generated ControlCatalog with +// pre-built indexes for efficient group, control, and requirement lookups. +type SugaredControlCatalog struct { + ControlCatalog -// 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"` + groupsOnce sync.Once + groupsCache []string - // metadata provides detailed data about this catalog - Metadata Metadata `json:"metadata" yaml:"metadata"` + sugarControlsOnce sync.Once + sugarControlsCache []*SugaredControl - // controls is a list of unique controls defined by this catalog - Controls []Control `json:"controls,omitempty" yaml:"controls,omitempty"` + controlsByGroupOnce sync.Once + controlsByGroupCache map[string][]*SugaredControl - // 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"` - - groups_cache []string - controls_cache map[string][]Control - requirements_cache map[string][]AssessmentRequirement + requirementsOnce sync.Once + requirementsCache 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"` - } - - 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 - - // Keep imports exactly as decoded (nil vs empty can matter to tests). - c.Imports = tmp.Imports +// Sugar wraps this ControlCatalog in a SugaredControlCatalog for +// convenient cached helper access. +func (c ControlCatalog) Sugar() *SugaredControlCatalog { + return &SugaredControlCatalog{ControlCatalog: c} +} - return nil +// 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 *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 *SugaredControlCatalog) 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 *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 controls + }) + return c.controlsByGroupCache[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 *SugaredControlCatalog) 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