Files
breakpilot-compliance/ai-compliance-sdk/internal/iace/hazard_blocks.go
T
Benjamin Admin d0d1b38f5c
Build + Deploy / build-admin-compliance (push) Successful in 11s
Build + Deploy / build-backend-compliance (push) Successful in 10s
Build + Deploy / build-ai-sdk (push) Successful in 1m7s
Build + Deploy / build-developer-portal (push) Successful in 1m23s
Build + Deploy / build-tts (push) Successful in 1m43s
Build + Deploy / build-document-crawler (push) Successful in 50s
Build + Deploy / build-dsms-gateway (push) Successful in 33s
Build + Deploy / build-dsms-node (push) Successful in 17s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / loc-budget (push) Failing after 18s
CI / secret-scan (push) Has been skipped
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-python-backend (push) Successful in 41s
CI / test-python-document-crawler (push) Successful in 28s
CI / test-python-dsms-gateway (push) Successful in 32s
CI / validate-canonical-controls (push) Successful in 15s
CI / nodejs-build (push) Successful in 2m44s
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / test-go (push) Failing after 44s
Build + Deploy / trigger-orca (push) Successful in 2m22s
fix(iace): coarser block grouping by category+component only
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-13 11:41:26 +02:00

171 lines
5.0 KiB
Go

package iace
import (
"sort"
"github.com/google/uuid"
)
// HazardBlock groups related hazards under a parent hazard.
// The parent is the hazard with the highest inherent risk in the group.
// Child hazards are covered by the same or similar protective measures.
type HazardBlock struct {
ParentHazard HazardBlockEntry `json:"parent_hazard"`
Children []HazardBlockEntry `json:"children"`
BlockKey string `json:"block_key"`
SharedMeasureCount int `json:"shared_measure_count"`
// If true, the parent's measures cover all children → children
// don't need individual risk assessment.
ChildrenCoveredByParent bool `json:"children_covered_by_parent"`
}
// HazardBlockEntry is a hazard with its assessment and linked measures.
type HazardBlockEntry struct {
Hazard Hazard `json:"hazard"`
Assessment *RiskAssessment `json:"assessment,omitempty"`
MitigationIDs []uuid.UUID `json:"mitigation_ids"`
}
// ComputeHazardBlocks groups hazards into blocks based on category + component.
// Within each block, the hazard with the highest risk becomes the parent.
// Children whose measures are a subset of the parent's measures are marked as covered.
func ComputeHazardBlocks(
hazards []Hazard,
assessments []RiskAssessment,
mitigations []Mitigation,
) []HazardBlock {
if len(hazards) == 0 {
return nil
}
// Build assessment lookup: hazard_id → latest assessment
assessMap := make(map[uuid.UUID]*RiskAssessment)
for i := range assessments {
a := &assessments[i]
if existing, ok := assessMap[a.HazardID]; !ok || a.Version > existing.Version {
assessMap[a.HazardID] = a
}
}
// Build mitigation lookup: hazard_id → []mitigation_ids
mitsByHazard := make(map[uuid.UUID][]uuid.UUID)
for _, m := range mitigations {
mitsByHazard[m.HazardID] = append(mitsByHazard[m.HazardID], m.ID)
}
// Group by blockKey = category + ":" + componentID
groups := make(map[string][]HazardBlockEntry)
for _, h := range hazards {
key := buildBlockKey(h)
entry := HazardBlockEntry{
Hazard: h,
Assessment: assessMap[h.ID],
MitigationIDs: mitsByHazard[h.ID],
}
groups[key] = append(groups[key], entry)
}
// Build blocks: sort each group by risk, first is parent
var blocks []HazardBlock
for key, entries := range groups {
sortByRiskDesc(entries, assessMap)
parent := entries[0]
children := entries[1:]
// Check if parent's measures cover children
parentMitSet := toUUIDSet(parent.MitigationIDs)
allCovered := true
for _, child := range children {
if !mitigationsCoveredBy(child, parent, mitigations) {
allCovered = false
break
}
}
block := HazardBlock{
ParentHazard: parent,
Children: children,
BlockKey: key,
SharedMeasureCount: len(parentMitSet),
ChildrenCoveredByParent: allCovered && len(children) > 0,
}
blocks = append(blocks, block)
}
// Sort blocks: largest (most children) first, then by parent risk
sort.Slice(blocks, func(i, j int) bool {
ri := inherentRisk(blocks[i].ParentHazard, assessMap)
rj := inherentRisk(blocks[j].ParentHazard, assessMap)
if len(blocks[i].Children) != len(blocks[j].Children) {
return len(blocks[i].Children) > len(blocks[j].Children)
}
return ri > rj
})
return blocks
}
func buildBlockKey(h Hazard) string {
// Group by category + component. Hazards at the same component in the
// same category form one block — the zone is typically different but the
// protective measures (e.g. Schutzzaun, Sicherheitszuhaltung) are shared.
return h.Category + ":" + h.ComponentID.String()
}
func sortByRiskDesc(entries []HazardBlockEntry, assessMap map[uuid.UUID]*RiskAssessment) {
sort.Slice(entries, func(i, j int) bool {
ri := inherentRisk(entries[i], assessMap)
rj := inherentRisk(entries[j], assessMap)
return ri > rj
})
}
func inherentRisk(entry HazardBlockEntry, assessMap map[uuid.UUID]*RiskAssessment) float64 {
if entry.Assessment != nil {
return entry.Assessment.InherentRisk
}
if a, ok := assessMap[entry.Hazard.ID]; ok {
return a.InherentRisk
}
return 0
}
// mitigationsCoveredBy checks if child's measures are functionally covered
// by parent's measures (same reduction type and hazard category).
func mitigationsCoveredBy(child, parent HazardBlockEntry, allMits []Mitigation) bool {
if len(child.MitigationIDs) == 0 {
return true // No measures needed → covered by default
}
mitMap := make(map[uuid.UUID]Mitigation)
for _, m := range allMits {
mitMap[m.ID] = m
}
// Check: for each child mitigation type, parent has same type
parentTypes := make(map[ReductionType]bool)
for _, mid := range parent.MitigationIDs {
if m, ok := mitMap[mid]; ok {
parentTypes[m.ReductionType] = true
}
}
for _, mid := range child.MitigationIDs {
if m, ok := mitMap[mid]; ok {
if !parentTypes[m.ReductionType] {
return false
}
}
}
return true
}
func toUUIDSet(ids []uuid.UUID) map[uuid.UUID]bool {
s := make(map[uuid.UUID]bool, len(ids))
for _, id := range ids {
s[id] = true
}
return s
}