All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 32s
CI / test-python-backend-compliance (push) Successful in 29s
CI / test-python-document-crawler (push) Successful in 20s
CI / test-python-dsms-gateway (push) Successful in 18s
- 9 Regulation-JSON-Dateien (DSGVO 80, AI Act 60, NIS2 40, BDSG 30, TTDSG 20, DSA 35, Data Act 25, EU-Maschinen 15, DORA 20) - Condition-Tree-Engine fuer automatische Pflichtenselektion (all_of/any_of, 80+ Field-Paths) - Generischer JSONRegulationModule-Loader mit YAML-Fallback - Bidirektionales TOM-Control-Mapping (291 Obligation→Control, 92 Control→Obligation) - Gap-Analyse-Engine (Compliance-%, Priority Actions, Domain Breakdown) - ScopeDecision→UnifiedFacts Bridge fuer Auto-Profiling - 4 neue API-Endpoints (assess-from-scope, tom-controls, gap-analysis, reverse-lookup) - Frontend: Auto-Profiling Button, Regulation-Filter Chips, TOM-Panel, Gap-Analyse-View - 18 Unit Tests (Condition Engine, v2 Loader, TOM Mapper) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
260 lines
7.4 KiB
Go
260 lines
7.4 KiB
Go
package ucca
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
)
|
|
|
|
// ControlStatus represents the implementation status of a control
|
|
type ControlStatus string
|
|
|
|
const (
|
|
ControlImplemented ControlStatus = "IMPLEMENTED"
|
|
ControlPartial ControlStatus = "PARTIAL"
|
|
ControlNotImplemented ControlStatus = "NOT_IMPLEMENTED"
|
|
ControlNotApplicable ControlStatus = "NOT_APPLICABLE"
|
|
)
|
|
|
|
// GapAnalysisRequest is the input for gap analysis
|
|
type GapAnalysisRequest struct {
|
|
// Obligations that apply (from assessment)
|
|
Obligations []Obligation `json:"obligations"`
|
|
// Current implementation status of controls
|
|
ControlStatusMap map[string]ControlStatus `json:"control_status_map"` // control_id -> status
|
|
}
|
|
|
|
// GapAnalysisResult is the output of gap analysis
|
|
type GapAnalysisResult struct {
|
|
CompliancePercent float64 `json:"compliance_percent"` // 0-100
|
|
TotalControls int `json:"total_controls"`
|
|
ImplementedControls int `json:"implemented_controls"`
|
|
PartialControls int `json:"partial_controls"`
|
|
MissingControls int `json:"missing_controls"`
|
|
Gaps []GapItem `json:"gaps"`
|
|
PriorityActions []PriorityAction `json:"priority_actions"`
|
|
ByDomain map[string]DomainGap `json:"by_domain"`
|
|
}
|
|
|
|
// GapItem represents a single compliance gap
|
|
type GapItem struct {
|
|
ControlID string `json:"control_id"`
|
|
ControlTitle string `json:"control_title"`
|
|
ControlDomain string `json:"control_domain"`
|
|
Status ControlStatus `json:"status"`
|
|
Priority string `json:"priority"`
|
|
ObligationIDs []string `json:"obligation_ids"`
|
|
RequiredByCount int `json:"required_by_count"`
|
|
Impact string `json:"impact"` // "critical", "high", "medium", "low"
|
|
}
|
|
|
|
// PriorityAction is a recommended action to close gaps
|
|
type PriorityAction struct {
|
|
Rank int `json:"rank"`
|
|
Action string `json:"action"`
|
|
ControlIDs []string `json:"control_ids"`
|
|
Impact string `json:"impact"`
|
|
Effort string `json:"effort"` // "low", "medium", "high"
|
|
}
|
|
|
|
// DomainGap summarizes gaps per TOM domain
|
|
type DomainGap struct {
|
|
DomainID string `json:"domain_id"`
|
|
DomainName string `json:"domain_name"`
|
|
TotalControls int `json:"total_controls"`
|
|
ImplementedControls int `json:"implemented_controls"`
|
|
CompliancePercent float64 `json:"compliance_percent"`
|
|
}
|
|
|
|
// TOMGapAnalyzer performs gap analysis between obligations and control implementation
|
|
type TOMGapAnalyzer struct {
|
|
mapper *TOMObligationMapper
|
|
tomIndex *TOMControlIndex
|
|
}
|
|
|
|
// NewTOMGapAnalyzer creates a new gap analyzer
|
|
func NewTOMGapAnalyzer(mapper *TOMObligationMapper, tomIndex *TOMControlIndex) *TOMGapAnalyzer {
|
|
return &TOMGapAnalyzer{
|
|
mapper: mapper,
|
|
tomIndex: tomIndex,
|
|
}
|
|
}
|
|
|
|
// Analyze performs gap analysis
|
|
func (g *TOMGapAnalyzer) Analyze(req *GapAnalysisRequest) *GapAnalysisResult {
|
|
result := &GapAnalysisResult{
|
|
Gaps: []GapItem{},
|
|
PriorityActions: []PriorityAction{},
|
|
ByDomain: make(map[string]DomainGap),
|
|
}
|
|
|
|
// Derive required controls from obligations
|
|
requirements := g.mapper.DeriveControlsFromObligations(req.Obligations)
|
|
result.TotalControls = len(requirements)
|
|
|
|
// Track domain stats
|
|
domainTotal := make(map[string]int)
|
|
domainImplemented := make(map[string]int)
|
|
|
|
for _, ctrl := range requirements {
|
|
controlID := ctrl.Control.ID
|
|
domain := ctrl.Control.DomainID
|
|
|
|
domainTotal[domain]++
|
|
|
|
status, hasStatus := req.ControlStatusMap[controlID]
|
|
if !hasStatus {
|
|
status = ControlNotImplemented
|
|
}
|
|
|
|
switch status {
|
|
case ControlImplemented:
|
|
result.ImplementedControls++
|
|
domainImplemented[domain]++
|
|
case ControlPartial:
|
|
result.PartialControls++
|
|
// Count partial as 0.5 for domain
|
|
domainImplemented[domain]++ // simplified
|
|
result.Gaps = append(result.Gaps, GapItem{
|
|
ControlID: controlID,
|
|
ControlTitle: ctrl.Control.Title,
|
|
ControlDomain: domain,
|
|
Status: ControlPartial,
|
|
Priority: ctrl.Priority,
|
|
ObligationIDs: ctrl.ObligationIDs,
|
|
RequiredByCount: ctrl.RequiredByCount,
|
|
Impact: ctrl.Priority,
|
|
})
|
|
case ControlNotImplemented:
|
|
result.MissingControls++
|
|
result.Gaps = append(result.Gaps, GapItem{
|
|
ControlID: controlID,
|
|
ControlTitle: ctrl.Control.Title,
|
|
ControlDomain: domain,
|
|
Status: ControlNotImplemented,
|
|
Priority: ctrl.Priority,
|
|
ObligationIDs: ctrl.ObligationIDs,
|
|
RequiredByCount: ctrl.RequiredByCount,
|
|
Impact: ctrl.Priority,
|
|
})
|
|
case ControlNotApplicable:
|
|
result.TotalControls-- // Don't count N/A
|
|
domainTotal[domain]--
|
|
}
|
|
}
|
|
|
|
// Calculate compliance percent
|
|
if result.TotalControls > 0 {
|
|
implemented := float64(result.ImplementedControls) + float64(result.PartialControls)*0.5
|
|
result.CompliancePercent = (implemented / float64(result.TotalControls)) * 100
|
|
} else {
|
|
result.CompliancePercent = 100
|
|
}
|
|
|
|
// Sort gaps by priority
|
|
priorityRank := map[string]int{"critical": 0, "high": 1, "medium": 2, "low": 3}
|
|
sort.Slice(result.Gaps, func(i, j int) bool {
|
|
ri := priorityRank[result.Gaps[i].Priority]
|
|
rj := priorityRank[result.Gaps[j].Priority]
|
|
if ri != rj {
|
|
return ri < rj
|
|
}
|
|
return result.Gaps[i].RequiredByCount > result.Gaps[j].RequiredByCount
|
|
})
|
|
|
|
// Build domain gaps
|
|
for domain, total := range domainTotal {
|
|
if total <= 0 {
|
|
continue
|
|
}
|
|
impl := domainImplemented[domain]
|
|
pct := float64(impl) / float64(total) * 100
|
|
domainName := domain
|
|
if ctrls := g.tomIndex.GetControlsByDomain(domain); len(ctrls) > 0 {
|
|
domainName = domain // Use domain ID as name
|
|
}
|
|
result.ByDomain[domain] = DomainGap{
|
|
DomainID: domain,
|
|
DomainName: domainName,
|
|
TotalControls: total,
|
|
ImplementedControls: impl,
|
|
CompliancePercent: pct,
|
|
}
|
|
}
|
|
|
|
// Generate priority actions
|
|
result.PriorityActions = g.generatePriorityActions(result.Gaps)
|
|
|
|
return result
|
|
}
|
|
|
|
func (g *TOMGapAnalyzer) generatePriorityActions(gaps []GapItem) []PriorityAction {
|
|
var actions []PriorityAction
|
|
rank := 1
|
|
|
|
// Group critical gaps by domain
|
|
domainGaps := make(map[string][]GapItem)
|
|
for _, gap := range gaps {
|
|
if gap.Status == ControlNotImplemented {
|
|
domainGaps[gap.ControlDomain] = append(domainGaps[gap.ControlDomain], gap)
|
|
}
|
|
}
|
|
|
|
// Generate actions for domains with most critical gaps
|
|
type domainPriority struct {
|
|
domain string
|
|
gaps []GapItem
|
|
critCount int
|
|
}
|
|
var dp []domainPriority
|
|
for domain, gs := range domainGaps {
|
|
crit := 0
|
|
for _, g := range gs {
|
|
if g.Priority == "critical" {
|
|
crit++
|
|
}
|
|
}
|
|
dp = append(dp, domainPriority{domain, gs, crit})
|
|
}
|
|
sort.Slice(dp, func(i, j int) bool {
|
|
if dp[i].critCount != dp[j].critCount {
|
|
return dp[i].critCount > dp[j].critCount
|
|
}
|
|
return len(dp[i].gaps) > len(dp[j].gaps)
|
|
})
|
|
|
|
for _, d := range dp {
|
|
if rank > 10 {
|
|
break
|
|
}
|
|
var controlIDs []string
|
|
for _, g := range d.gaps {
|
|
controlIDs = append(controlIDs, g.ControlID)
|
|
}
|
|
|
|
impact := "medium"
|
|
if d.critCount > 0 {
|
|
impact = "critical"
|
|
} else if len(d.gaps) > 3 {
|
|
impact = "high"
|
|
}
|
|
|
|
effort := "medium"
|
|
if len(d.gaps) > 5 {
|
|
effort = "high"
|
|
} else if len(d.gaps) <= 2 {
|
|
effort = "low"
|
|
}
|
|
|
|
actions = append(actions, PriorityAction{
|
|
Rank: rank,
|
|
Action: fmt.Sprintf("Domain %s: %d fehlende Controls implementieren", d.domain, len(d.gaps)),
|
|
ControlIDs: controlIDs,
|
|
Impact: impact,
|
|
Effort: effort,
|
|
})
|
|
rank++
|
|
}
|
|
|
|
return actions
|
|
}
|