Each of the four oversized files (training/store.go 1569 LOC, ucca/rules.go 1231 LOC, ucca_handlers.go 1135 LOC, document_export.go 1101 LOC) is split by logical group into same-package files, all under the 500-line hard cap. Zero behavior changes, no renamed exported symbols. Also fixed pre-existing hazard_library split (missing functions and duplicate UUID keys from a prior session). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
199 lines
4.6 KiB
Go
199 lines
4.6 KiB
Go
package iace
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bytes"
|
|
"encoding/xml"
|
|
"fmt"
|
|
)
|
|
|
|
// buildAssessmentMap builds a map from hazardID string to the latest RiskAssessment.
|
|
func buildAssessmentMap(assessments []RiskAssessment) map[string]*RiskAssessment {
|
|
m := make(map[string]*RiskAssessment)
|
|
for i := range assessments {
|
|
a := &assessments[i]
|
|
key := a.HazardID.String()
|
|
if existing, ok := m[key]; !ok || a.Version > existing.Version {
|
|
m[key] = a
|
|
}
|
|
}
|
|
return m
|
|
}
|
|
|
|
// riskLevelColor returns RGB values for PDF fill color based on risk level.
|
|
func riskLevelColor(level RiskLevel) (r, g, b int) {
|
|
switch level {
|
|
case RiskLevelNotAcceptable:
|
|
return 180, 0, 0
|
|
case RiskLevelVeryHigh:
|
|
return 220, 40, 40
|
|
case RiskLevelCritical:
|
|
return 255, 80, 80
|
|
case RiskLevelHigh:
|
|
return 255, 165, 80
|
|
case RiskLevelMedium:
|
|
return 255, 230, 100
|
|
case RiskLevelLow:
|
|
return 180, 230, 140
|
|
case RiskLevelNegligible:
|
|
return 140, 210, 140
|
|
default:
|
|
return 240, 240, 240
|
|
}
|
|
}
|
|
|
|
// riskLevelLabel returns a German display label for a risk level.
|
|
func riskLevelLabel(level RiskLevel) string {
|
|
switch level {
|
|
case RiskLevelNotAcceptable:
|
|
return "Nicht akzeptabel"
|
|
case RiskLevelVeryHigh:
|
|
return "Sehr hoch"
|
|
case RiskLevelCritical:
|
|
return "Kritisch"
|
|
case RiskLevelHigh:
|
|
return "Hoch"
|
|
case RiskLevelMedium:
|
|
return "Mittel"
|
|
case RiskLevelLow:
|
|
return "Niedrig"
|
|
case RiskLevelNegligible:
|
|
return "Vernachlaessigbar"
|
|
default:
|
|
return string(level)
|
|
}
|
|
}
|
|
|
|
// reductionTypeLabel returns a German label for a reduction type.
|
|
func reductionTypeLabel(rt ReductionType) string {
|
|
switch rt {
|
|
case ReductionTypeDesign:
|
|
return "Konstruktiv"
|
|
case ReductionTypeProtective:
|
|
return "Schutzmassnahme"
|
|
case ReductionTypeInformation:
|
|
return "Information"
|
|
default:
|
|
return string(rt)
|
|
}
|
|
}
|
|
|
|
// mitigationStatusLabel returns a German label for a mitigation status.
|
|
func mitigationStatusLabel(status MitigationStatus) string {
|
|
switch status {
|
|
case MitigationStatusPlanned:
|
|
return "Geplant"
|
|
case MitigationStatusImplemented:
|
|
return "Umgesetzt"
|
|
case MitigationStatusVerified:
|
|
return "Verifiziert"
|
|
case MitigationStatusRejected:
|
|
return "Abgelehnt"
|
|
default:
|
|
return string(status)
|
|
}
|
|
}
|
|
|
|
// regulationLabel returns a German label for a regulation type.
|
|
func regulationLabel(reg RegulationType) string {
|
|
switch reg {
|
|
case RegulationNIS2:
|
|
return "NIS-2 Richtlinie"
|
|
case RegulationAIAct:
|
|
return "EU AI Act"
|
|
case RegulationCRA:
|
|
return "Cyber Resilience Act"
|
|
case RegulationMachineryRegulation:
|
|
return "EU Maschinenverordnung 2023/1230"
|
|
default:
|
|
return string(reg)
|
|
}
|
|
}
|
|
|
|
// escapeXML escapes special XML characters in text content.
|
|
func escapeXML(s string) string {
|
|
var buf bytes.Buffer
|
|
if err := xml.EscapeText(&buf, []byte(s)); err != nil {
|
|
return s
|
|
}
|
|
return buf.String()
|
|
}
|
|
|
|
// countByRiskLevel counts assessments per risk level.
|
|
func countByRiskLevel(assessments []RiskAssessment) map[RiskLevel]int {
|
|
counts := make(map[RiskLevel]int)
|
|
for _, a := range assessments {
|
|
counts[a.RiskLevel]++
|
|
}
|
|
return counts
|
|
}
|
|
|
|
// pdfTruncate truncates a string for PDF cell display.
|
|
func pdfTruncate(s string, maxLen int) string {
|
|
runes := []rune(s)
|
|
if len(runes) <= maxLen {
|
|
return s
|
|
}
|
|
if maxLen <= 3 {
|
|
return string(runes[:maxLen])
|
|
}
|
|
return string(runes[:maxLen-3]) + "..."
|
|
}
|
|
|
|
// cellRef builds an Excel cell reference like "A1", "B12".
|
|
func cellRef(col string, row int) string {
|
|
return fmt.Sprintf("%s%d", col, row)
|
|
}
|
|
|
|
// rgbHex converts RGB values to a hex color string (without #).
|
|
func rgbHex(r, g, b int) string {
|
|
return fmt.Sprintf("%02X%02X%02X", r, g, b)
|
|
}
|
|
|
|
// addZipEntry writes a text file into a zip archive.
|
|
func addZipEntry(zw *zip.Writer, name, content string) error {
|
|
w, err := zw.Create(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = w.Write([]byte(content))
|
|
return err
|
|
}
|
|
|
|
// docxHeading builds a DOCX paragraph with a heading style.
|
|
func docxHeading(text string, level int) string {
|
|
sizes := map[int]int{1: 64, 2: 52, 3: 44}
|
|
sz, ok := sizes[level]
|
|
if !ok {
|
|
sz = 44
|
|
}
|
|
escaped := escapeXML(text)
|
|
return fmt.Sprintf(` <w:p>
|
|
<w:pPr>
|
|
<w:pStyle w:val="Heading%d"/>
|
|
<w:spacing w:after="200"/>
|
|
</w:pPr>
|
|
<w:r>
|
|
<w:rPr><w:b/><w:sz w:val="%d"/><w:szCs w:val="%d"/></w:rPr>
|
|
<w:t xml:space="preserve">%s</w:t>
|
|
</w:r>
|
|
</w:p>
|
|
`, level, sz, sz, escaped)
|
|
}
|
|
|
|
// docxParagraph builds a DOCX paragraph, optionally italic.
|
|
func docxParagraph(text string, italic bool) string {
|
|
escaped := escapeXML(text)
|
|
rpr := ""
|
|
if italic {
|
|
rpr = "<w:rPr><w:i/></w:rPr>"
|
|
}
|
|
return fmt.Sprintf(` <w:p>
|
|
<w:r>
|
|
%s
|
|
<w:t xml:space="preserve">%s</w:t>
|
|
</w:r>
|
|
</w:p>
|
|
`, rpr, escaped)
|
|
}
|