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
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
171 lines
5.0 KiB
Go
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
|
|
}
|