Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 21 additions & 81 deletions control.go
Original file line number Diff line number Diff line change
@@ -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
}
139 changes: 55 additions & 84 deletions control_catalog.go
Original file line number Diff line number Diff line change
@@ -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]
}
40 changes: 40 additions & 0 deletions control_catalog_yaml.go
Original file line number Diff line number Diff line change
@@ -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
}
47 changes: 47 additions & 0 deletions control_yaml.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading