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 35s
CI / test-python-backend-compliance (push) Successful in 33s
CI / test-python-document-crawler (push) Successful in 21s
CI / test-python-dsms-gateway (push) Successful in 19s
- Hazard-Library: +79 neue Eintraege in 12 Kategorien (software_fault, hmi_error, mechanical_hazard, electrical_hazard, thermal_hazard, emc_hazard, configuration_error, safety_function_failure, logging_audit_failure, integration_error, environmental_hazard, maintenance_hazard) — Gesamtanzahl: ~116 Eintraege in 24 Kategorien - Controls-Library: neue Datei controls_library.go mit 200 Eintraegen in 6 Domaenen (REQ/ARCH/SWDEV/VER/CYBER/DOC) - Handler: GET /sdk/v1/iace/controls-library (?domain=, ?category=) - SEPA: CalculateInherentRisk() + 4. Param Avoidance (0=disabled, 1-5: 3=neutral); RiskComputeInput.Avoidance, RiskAssessment.Avoidance, AssessRiskRequest.Avoidance — backward-kompatibel (A=0 → S×E×P) - Tests: engine_test.go + hazard_library_test.go aktualisiert - Scripts: ingest-ce-corpus.sh — 15 CE/Safety-Dokumente (EUR-Lex, NIST, ENISA, NASA, OWASP, MITRE CWE) in bp_compliance_ce und bp_compliance_datenschutz - Docs: docs-src/services/sdk-modules/iace.md + mkdocs.yml Nav-Eintrag Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1865 lines
55 KiB
Go
1865 lines
55 KiB
Go
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/breakpilot/ai-compliance-sdk/internal/iace"
|
|
"github.com/breakpilot/ai-compliance-sdk/internal/rbac"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// ============================================================================
|
|
// Handler Struct & Constructor
|
|
// ============================================================================
|
|
|
|
// IACEHandler handles HTTP requests for the IACE module (Inherent-risk Adjusted
|
|
// Control Effectiveness). It provides endpoints for project management, component
|
|
// onboarding, regulatory classification, hazard/risk analysis, evidence management,
|
|
// CE technical file generation, and post-market monitoring.
|
|
type IACEHandler struct {
|
|
store *iace.Store
|
|
engine *iace.RiskEngine
|
|
classifier *iace.Classifier
|
|
checker *iace.CompletenessChecker
|
|
}
|
|
|
|
// NewIACEHandler creates a new IACEHandler with all required dependencies.
|
|
func NewIACEHandler(store *iace.Store) *IACEHandler {
|
|
return &IACEHandler{
|
|
store: store,
|
|
engine: iace.NewRiskEngine(),
|
|
classifier: iace.NewClassifier(),
|
|
checker: iace.NewCompletenessChecker(),
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Helper: Tenant ID extraction
|
|
// ============================================================================
|
|
|
|
// getTenantID extracts the tenant UUID from the X-Tenant-Id header.
|
|
// It first checks the rbac middleware context; if not present, falls back to the
|
|
// raw header value.
|
|
func getTenantID(c *gin.Context) (uuid.UUID, error) {
|
|
// Prefer value set by RBAC middleware
|
|
tid := rbac.GetTenantID(c)
|
|
if tid != uuid.Nil {
|
|
return tid, nil
|
|
}
|
|
|
|
tenantStr := c.GetHeader("X-Tenant-Id")
|
|
if tenantStr == "" {
|
|
return uuid.Nil, fmt.Errorf("X-Tenant-Id header required")
|
|
}
|
|
return uuid.Parse(tenantStr)
|
|
}
|
|
|
|
// ============================================================================
|
|
// Project Management
|
|
// ============================================================================
|
|
|
|
// CreateProject handles POST /projects
|
|
// Creates a new IACE compliance project for a machine or system.
|
|
func (h *IACEHandler) CreateProject(c *gin.Context) {
|
|
tenantID, err := getTenantID(c)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
var req iace.CreateProjectRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
project, err := h.store.CreateProject(c.Request.Context(), tenantID, req)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusCreated, gin.H{"project": project})
|
|
}
|
|
|
|
// ListProjects handles GET /projects
|
|
// Lists all IACE projects for the authenticated tenant.
|
|
func (h *IACEHandler) ListProjects(c *gin.Context) {
|
|
tenantID, err := getTenantID(c)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
projects, err := h.store.ListProjects(c.Request.Context(), tenantID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
if projects == nil {
|
|
projects = []iace.Project{}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, iace.ProjectListResponse{
|
|
Projects: projects,
|
|
Total: len(projects),
|
|
})
|
|
}
|
|
|
|
// GetProject handles GET /projects/:id
|
|
// Returns a project with its components, classifications, and completeness gates.
|
|
func (h *IACEHandler) GetProject(c *gin.Context) {
|
|
projectID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
|
|
return
|
|
}
|
|
|
|
project, err := h.store.GetProject(c.Request.Context(), projectID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if project == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "project not found"})
|
|
return
|
|
}
|
|
|
|
components, _ := h.store.ListComponents(c.Request.Context(), projectID)
|
|
classifications, _ := h.store.GetClassifications(c.Request.Context(), projectID)
|
|
|
|
if components == nil {
|
|
components = []iace.Component{}
|
|
}
|
|
if classifications == nil {
|
|
classifications = []iace.RegulatoryClassification{}
|
|
}
|
|
|
|
// Build completeness context to compute gates
|
|
ctx := h.buildCompletenessContext(c, project, components, classifications)
|
|
result := h.checker.Check(ctx)
|
|
|
|
c.JSON(http.StatusOK, iace.ProjectDetailResponse{
|
|
Project: *project,
|
|
Components: components,
|
|
Classifications: classifications,
|
|
CompletenessGates: result.Gates,
|
|
})
|
|
}
|
|
|
|
// UpdateProject handles PUT /projects/:id
|
|
// Partially updates a project's mutable fields.
|
|
func (h *IACEHandler) UpdateProject(c *gin.Context) {
|
|
projectID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
|
|
return
|
|
}
|
|
|
|
var req iace.UpdateProjectRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
project, err := h.store.UpdateProject(c.Request.Context(), projectID, req)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if project == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "project not found"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"project": project})
|
|
}
|
|
|
|
// ArchiveProject handles DELETE /projects/:id
|
|
// Archives a project by setting its status to archived.
|
|
func (h *IACEHandler) ArchiveProject(c *gin.Context) {
|
|
projectID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
|
|
return
|
|
}
|
|
|
|
if err := h.store.ArchiveProject(c.Request.Context(), projectID); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "project archived"})
|
|
}
|
|
|
|
// ============================================================================
|
|
// Onboarding
|
|
// ============================================================================
|
|
|
|
// InitFromProfile handles POST /projects/:id/init-from-profile
|
|
// Initializes a project from a company profile and compliance scope JSON payload.
|
|
func (h *IACEHandler) InitFromProfile(c *gin.Context) {
|
|
projectID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
|
|
return
|
|
}
|
|
|
|
project, err := h.store.GetProject(c.Request.Context(), projectID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if project == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "project not found"})
|
|
return
|
|
}
|
|
|
|
var req iace.InitFromProfileRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Store the profile and scope in project metadata
|
|
profileData := map[string]json.RawMessage{
|
|
"company_profile": req.CompanyProfile,
|
|
"compliance_scope": req.ComplianceScope,
|
|
}
|
|
metadataBytes, _ := json.Marshal(profileData)
|
|
metadataRaw := json.RawMessage(metadataBytes)
|
|
updateReq := iace.UpdateProjectRequest{
|
|
Metadata: &metadataRaw,
|
|
}
|
|
|
|
project, err = h.store.UpdateProject(c.Request.Context(), projectID, updateReq)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Advance project status to onboarding
|
|
if err := h.store.UpdateProjectStatus(c.Request.Context(), projectID, iace.ProjectStatusOnboarding); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Add audit trail entry
|
|
userID := rbac.GetUserID(c)
|
|
h.store.AddAuditEntry(
|
|
c.Request.Context(), projectID, "project", projectID,
|
|
iace.AuditActionUpdate, userID.String(), nil, metadataBytes,
|
|
)
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"message": "project initialized from profile",
|
|
"project": project,
|
|
})
|
|
}
|
|
|
|
// CreateComponent handles POST /projects/:id/components
|
|
// Adds a new component to a project.
|
|
func (h *IACEHandler) CreateComponent(c *gin.Context) {
|
|
projectID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
|
|
return
|
|
}
|
|
|
|
var req iace.CreateComponentRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Override project ID from URL path
|
|
req.ProjectID = projectID
|
|
|
|
component, err := h.store.CreateComponent(c.Request.Context(), req)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Audit trail
|
|
userID := rbac.GetUserID(c)
|
|
newVals, _ := json.Marshal(component)
|
|
h.store.AddAuditEntry(
|
|
c.Request.Context(), projectID, "component", component.ID,
|
|
iace.AuditActionCreate, userID.String(), nil, newVals,
|
|
)
|
|
|
|
c.JSON(http.StatusCreated, gin.H{"component": component})
|
|
}
|
|
|
|
// ListComponents handles GET /projects/:id/components
|
|
// Lists all components for a project.
|
|
func (h *IACEHandler) ListComponents(c *gin.Context) {
|
|
projectID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
|
|
return
|
|
}
|
|
|
|
components, err := h.store.ListComponents(c.Request.Context(), projectID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
if components == nil {
|
|
components = []iace.Component{}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"components": components,
|
|
"total": len(components),
|
|
})
|
|
}
|
|
|
|
// UpdateComponent handles PUT /projects/:id/components/:cid
|
|
// Updates a component with the provided fields.
|
|
func (h *IACEHandler) UpdateComponent(c *gin.Context) {
|
|
_, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
|
|
return
|
|
}
|
|
|
|
componentID, err := uuid.Parse(c.Param("cid"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid component ID"})
|
|
return
|
|
}
|
|
|
|
var updates map[string]interface{}
|
|
if err := c.ShouldBindJSON(&updates); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
component, err := h.store.UpdateComponent(c.Request.Context(), componentID, updates)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if component == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "component not found"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"component": component})
|
|
}
|
|
|
|
// DeleteComponent handles DELETE /projects/:id/components/:cid
|
|
// Deletes a component from a project.
|
|
func (h *IACEHandler) DeleteComponent(c *gin.Context) {
|
|
_, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
|
|
return
|
|
}
|
|
|
|
componentID, err := uuid.Parse(c.Param("cid"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid component ID"})
|
|
return
|
|
}
|
|
|
|
if err := h.store.DeleteComponent(c.Request.Context(), componentID); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "component deleted"})
|
|
}
|
|
|
|
// CheckCompleteness handles POST /projects/:id/completeness-check
|
|
// Loads all project data, evaluates all 25 CE completeness gates, updates the
|
|
// project's completeness score, and returns the result.
|
|
func (h *IACEHandler) CheckCompleteness(c *gin.Context) {
|
|
projectID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
|
|
return
|
|
}
|
|
|
|
project, err := h.store.GetProject(c.Request.Context(), projectID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if project == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "project not found"})
|
|
return
|
|
}
|
|
|
|
// Load all related entities
|
|
components, _ := h.store.ListComponents(c.Request.Context(), projectID)
|
|
classifications, _ := h.store.GetClassifications(c.Request.Context(), projectID)
|
|
hazards, _ := h.store.ListHazards(c.Request.Context(), projectID)
|
|
|
|
// Collect all assessments and mitigations across all hazards
|
|
var allAssessments []iace.RiskAssessment
|
|
var allMitigations []iace.Mitigation
|
|
for _, hazard := range hazards {
|
|
assessments, _ := h.store.ListAssessments(c.Request.Context(), hazard.ID)
|
|
allAssessments = append(allAssessments, assessments...)
|
|
|
|
mitigations, _ := h.store.ListMitigations(c.Request.Context(), hazard.ID)
|
|
allMitigations = append(allMitigations, mitigations...)
|
|
}
|
|
|
|
evidence, _ := h.store.ListEvidence(c.Request.Context(), projectID)
|
|
techFileSections, _ := h.store.ListTechFileSections(c.Request.Context(), projectID)
|
|
|
|
// Determine if the project has AI components
|
|
hasAI := false
|
|
for _, comp := range components {
|
|
if comp.ComponentType == iace.ComponentTypeAIModel {
|
|
hasAI = true
|
|
break
|
|
}
|
|
}
|
|
|
|
// Build completeness context
|
|
completenessCtx := &iace.CompletenessContext{
|
|
Project: project,
|
|
Components: components,
|
|
Classifications: classifications,
|
|
Hazards: hazards,
|
|
Assessments: allAssessments,
|
|
Mitigations: allMitigations,
|
|
Evidence: evidence,
|
|
TechFileSections: techFileSections,
|
|
HasAI: hasAI,
|
|
}
|
|
|
|
// Run the checker
|
|
result := h.checker.Check(completenessCtx)
|
|
|
|
// Build risk summary for the project update
|
|
riskSummary := map[string]int{
|
|
"total_hazards": len(hazards),
|
|
}
|
|
for _, a := range allAssessments {
|
|
riskSummary[string(a.RiskLevel)]++
|
|
}
|
|
|
|
// Update project completeness score and risk summary
|
|
if err := h.store.UpdateProjectCompleteness(
|
|
c.Request.Context(), projectID, result.Score, riskSummary,
|
|
); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"completeness": result,
|
|
})
|
|
}
|
|
|
|
// ============================================================================
|
|
// Classification
|
|
// ============================================================================
|
|
|
|
// Classify handles POST /projects/:id/classify
|
|
// Runs all regulatory classifiers (AI Act, Machinery Regulation, CRA, NIS2),
|
|
// upserts each result into the store, and returns classifications.
|
|
func (h *IACEHandler) Classify(c *gin.Context) {
|
|
projectID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
|
|
return
|
|
}
|
|
|
|
project, err := h.store.GetProject(c.Request.Context(), projectID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if project == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "project not found"})
|
|
return
|
|
}
|
|
|
|
components, err := h.store.ListComponents(c.Request.Context(), projectID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Run all classifiers
|
|
results := h.classifier.ClassifyAll(project, components)
|
|
|
|
// Upsert each classification result into the store
|
|
var classifications []iace.RegulatoryClassification
|
|
for _, r := range results {
|
|
reqsJSON, _ := json.Marshal(r.Requirements)
|
|
|
|
classification, err := h.store.UpsertClassification(
|
|
c.Request.Context(),
|
|
projectID,
|
|
r.Regulation,
|
|
r.ClassificationResult,
|
|
r.RiskLevel,
|
|
r.Confidence,
|
|
r.Reasoning,
|
|
nil, // ragSources - not available from rule-based classifier
|
|
reqsJSON,
|
|
)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if classification != nil {
|
|
classifications = append(classifications, *classification)
|
|
}
|
|
}
|
|
|
|
// Advance project status to classification
|
|
h.store.UpdateProjectStatus(c.Request.Context(), projectID, iace.ProjectStatusClassification)
|
|
|
|
// Audit trail
|
|
userID := rbac.GetUserID(c)
|
|
newVals, _ := json.Marshal(classifications)
|
|
h.store.AddAuditEntry(
|
|
c.Request.Context(), projectID, "classification", projectID,
|
|
iace.AuditActionCreate, userID.String(), nil, newVals,
|
|
)
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"classifications": classifications,
|
|
"total": len(classifications),
|
|
})
|
|
}
|
|
|
|
// GetClassifications handles GET /projects/:id/classifications
|
|
// Returns all regulatory classifications for a project.
|
|
func (h *IACEHandler) GetClassifications(c *gin.Context) {
|
|
projectID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
|
|
return
|
|
}
|
|
|
|
classifications, err := h.store.GetClassifications(c.Request.Context(), projectID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
if classifications == nil {
|
|
classifications = []iace.RegulatoryClassification{}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"classifications": classifications,
|
|
"total": len(classifications),
|
|
})
|
|
}
|
|
|
|
// ClassifySingle handles POST /projects/:id/classify/:regulation
|
|
// Runs a single regulatory classifier for the specified regulation type.
|
|
func (h *IACEHandler) ClassifySingle(c *gin.Context) {
|
|
projectID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
|
|
return
|
|
}
|
|
|
|
regulation := iace.RegulationType(c.Param("regulation"))
|
|
|
|
project, err := h.store.GetProject(c.Request.Context(), projectID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if project == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "project not found"})
|
|
return
|
|
}
|
|
|
|
components, err := h.store.ListComponents(c.Request.Context(), projectID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Run the appropriate classifier
|
|
var result iace.ClassificationResult
|
|
switch regulation {
|
|
case iace.RegulationAIAct:
|
|
result = h.classifier.ClassifyAIAct(project, components)
|
|
case iace.RegulationMachineryRegulation:
|
|
result = h.classifier.ClassifyMachineryRegulation(project, components)
|
|
case iace.RegulationCRA:
|
|
result = h.classifier.ClassifyCRA(project, components)
|
|
case iace.RegulationNIS2:
|
|
result = h.classifier.ClassifyNIS2(project, components)
|
|
default:
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("unknown regulation type: %s", regulation)})
|
|
return
|
|
}
|
|
|
|
// Upsert the classification result
|
|
reqsJSON, _ := json.Marshal(result.Requirements)
|
|
|
|
classification, err := h.store.UpsertClassification(
|
|
c.Request.Context(),
|
|
projectID,
|
|
result.Regulation,
|
|
result.ClassificationResult,
|
|
result.RiskLevel,
|
|
result.Confidence,
|
|
result.Reasoning,
|
|
nil,
|
|
reqsJSON,
|
|
)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"classification": classification})
|
|
}
|
|
|
|
// ============================================================================
|
|
// Hazard & Risk
|
|
// ============================================================================
|
|
|
|
// ListHazardLibrary handles GET /hazard-library
|
|
// Returns built-in hazard library entries merged with any custom DB entries,
|
|
// optionally filtered by ?category and ?componentType.
|
|
func (h *IACEHandler) ListHazardLibrary(c *gin.Context) {
|
|
category := c.Query("category")
|
|
componentType := c.Query("componentType")
|
|
|
|
// Start with built-in templates from Go code
|
|
builtinEntries := iace.GetBuiltinHazardLibrary()
|
|
|
|
// Apply filters to built-in entries
|
|
var entries []iace.HazardLibraryEntry
|
|
for _, entry := range builtinEntries {
|
|
if category != "" && entry.Category != category {
|
|
continue
|
|
}
|
|
if componentType != "" && !containsString(entry.ApplicableComponentTypes, componentType) {
|
|
continue
|
|
}
|
|
entries = append(entries, entry)
|
|
}
|
|
|
|
// Merge with custom DB entries (tenant-specific)
|
|
dbEntries, err := h.store.ListHazardLibrary(c.Request.Context(), category, componentType)
|
|
if err == nil && len(dbEntries) > 0 {
|
|
// Add DB entries that are not built-in (avoid duplicates)
|
|
builtinIDs := make(map[string]bool)
|
|
for _, e := range entries {
|
|
builtinIDs[e.ID.String()] = true
|
|
}
|
|
for _, dbEntry := range dbEntries {
|
|
if !builtinIDs[dbEntry.ID.String()] {
|
|
entries = append(entries, dbEntry)
|
|
}
|
|
}
|
|
}
|
|
|
|
if entries == nil {
|
|
entries = []iace.HazardLibraryEntry{}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"hazard_library": entries,
|
|
"total": len(entries),
|
|
})
|
|
}
|
|
|
|
// ListControlsLibrary handles GET /controls-library
|
|
// Returns the built-in controls library, optionally filtered by ?domain and ?category.
|
|
func (h *IACEHandler) ListControlsLibrary(c *gin.Context) {
|
|
domain := c.Query("domain")
|
|
category := c.Query("category")
|
|
|
|
all := iace.GetControlsLibrary()
|
|
|
|
var filtered []iace.ControlLibraryEntry
|
|
for _, entry := range all {
|
|
if domain != "" && entry.Domain != domain {
|
|
continue
|
|
}
|
|
if category != "" && !containsString(entry.MapsToHazardCategories, category) {
|
|
continue
|
|
}
|
|
filtered = append(filtered, entry)
|
|
}
|
|
|
|
if filtered == nil {
|
|
filtered = []iace.ControlLibraryEntry{}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"controls": filtered,
|
|
"total": len(filtered),
|
|
})
|
|
}
|
|
|
|
// containsString checks if a string slice contains the given value.
|
|
func containsString(slice []string, val string) bool {
|
|
for _, s := range slice {
|
|
if s == val {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// CreateHazard handles POST /projects/:id/hazards
|
|
// Creates a new hazard within a project.
|
|
func (h *IACEHandler) CreateHazard(c *gin.Context) {
|
|
projectID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
|
|
return
|
|
}
|
|
|
|
var req iace.CreateHazardRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Override project ID from URL path
|
|
req.ProjectID = projectID
|
|
|
|
hazard, err := h.store.CreateHazard(c.Request.Context(), req)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Audit trail
|
|
userID := rbac.GetUserID(c)
|
|
newVals, _ := json.Marshal(hazard)
|
|
h.store.AddAuditEntry(
|
|
c.Request.Context(), projectID, "hazard", hazard.ID,
|
|
iace.AuditActionCreate, userID.String(), nil, newVals,
|
|
)
|
|
|
|
c.JSON(http.StatusCreated, gin.H{"hazard": hazard})
|
|
}
|
|
|
|
// ListHazards handles GET /projects/:id/hazards
|
|
// Lists all hazards for a project.
|
|
func (h *IACEHandler) ListHazards(c *gin.Context) {
|
|
projectID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
|
|
return
|
|
}
|
|
|
|
hazards, err := h.store.ListHazards(c.Request.Context(), projectID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
if hazards == nil {
|
|
hazards = []iace.Hazard{}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"hazards": hazards,
|
|
"total": len(hazards),
|
|
})
|
|
}
|
|
|
|
// UpdateHazard handles PUT /projects/:id/hazards/:hid
|
|
// Updates a hazard with the provided fields.
|
|
func (h *IACEHandler) UpdateHazard(c *gin.Context) {
|
|
_, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
|
|
return
|
|
}
|
|
|
|
hazardID, err := uuid.Parse(c.Param("hid"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid hazard ID"})
|
|
return
|
|
}
|
|
|
|
var updates map[string]interface{}
|
|
if err := c.ShouldBindJSON(&updates); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
hazard, err := h.store.UpdateHazard(c.Request.Context(), hazardID, updates)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if hazard == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "hazard not found"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"hazard": hazard})
|
|
}
|
|
|
|
// SuggestHazards handles POST /projects/:id/hazards/suggest
|
|
// Returns hazard library matches based on the project's components.
|
|
// TODO: Enhance with LLM-based suggestions for more intelligent matching.
|
|
func (h *IACEHandler) SuggestHazards(c *gin.Context) {
|
|
projectID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
|
|
return
|
|
}
|
|
|
|
components, err := h.store.ListComponents(c.Request.Context(), projectID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Collect unique component types from the project
|
|
componentTypes := make(map[string]bool)
|
|
for _, comp := range components {
|
|
componentTypes[string(comp.ComponentType)] = true
|
|
}
|
|
|
|
// Match built-in hazard templates against project component types
|
|
var suggestions []iace.HazardLibraryEntry
|
|
seen := make(map[uuid.UUID]bool)
|
|
|
|
builtinEntries := iace.GetBuiltinHazardLibrary()
|
|
for _, entry := range builtinEntries {
|
|
for _, applicableType := range entry.ApplicableComponentTypes {
|
|
if componentTypes[applicableType] && !seen[entry.ID] {
|
|
seen[entry.ID] = true
|
|
suggestions = append(suggestions, entry)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// Also check DB for custom tenant-specific hazard templates
|
|
for compType := range componentTypes {
|
|
dbEntries, err := h.store.ListHazardLibrary(c.Request.Context(), "", compType)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
for _, entry := range dbEntries {
|
|
if !seen[entry.ID] {
|
|
seen[entry.ID] = true
|
|
suggestions = append(suggestions, entry)
|
|
}
|
|
}
|
|
}
|
|
|
|
if suggestions == nil {
|
|
suggestions = []iace.HazardLibraryEntry{}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"suggestions": suggestions,
|
|
"total": len(suggestions),
|
|
"component_types": componentTypeKeys(componentTypes),
|
|
"_note": "TODO: LLM-based suggestion ranking not yet implemented",
|
|
})
|
|
}
|
|
|
|
// AssessRisk handles POST /projects/:id/hazards/:hid/assess
|
|
// Performs a quantitative risk assessment for a hazard using the IACE risk engine.
|
|
func (h *IACEHandler) AssessRisk(c *gin.Context) {
|
|
projectID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
|
|
return
|
|
}
|
|
|
|
hazardID, err := uuid.Parse(c.Param("hid"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid hazard ID"})
|
|
return
|
|
}
|
|
|
|
// Verify hazard exists
|
|
hazard, err := h.store.GetHazard(c.Request.Context(), hazardID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if hazard == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "hazard not found"})
|
|
return
|
|
}
|
|
|
|
var req iace.AssessRiskRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Override hazard ID from URL path
|
|
req.HazardID = hazardID
|
|
|
|
userID := rbac.GetUserID(c)
|
|
|
|
// Calculate risk using the engine
|
|
inherentRisk := h.engine.CalculateInherentRisk(req.Severity, req.Exposure, req.Probability, req.Avoidance)
|
|
controlEff := h.engine.CalculateControlEffectiveness(req.ControlMaturity, req.ControlCoverage, req.TestEvidenceStrength)
|
|
residualRisk := h.engine.CalculateResidualRisk(req.Severity, req.Exposure, req.Probability, controlEff)
|
|
riskLevel := h.engine.DetermineRiskLevel(residualRisk)
|
|
acceptable, acceptanceReason := h.engine.IsAcceptable(residualRisk, false, req.AcceptanceJustification != "")
|
|
|
|
// Determine version by checking existing assessments
|
|
existingAssessments, _ := h.store.ListAssessments(c.Request.Context(), hazardID)
|
|
version := len(existingAssessments) + 1
|
|
|
|
assessment := &iace.RiskAssessment{
|
|
HazardID: hazardID,
|
|
Version: version,
|
|
AssessmentType: iace.AssessmentTypeInitial,
|
|
Severity: req.Severity,
|
|
Exposure: req.Exposure,
|
|
Probability: req.Probability,
|
|
Avoidance: req.Avoidance,
|
|
InherentRisk: inherentRisk,
|
|
ControlMaturity: req.ControlMaturity,
|
|
ControlCoverage: req.ControlCoverage,
|
|
TestEvidenceStrength: req.TestEvidenceStrength,
|
|
CEff: controlEff,
|
|
ResidualRisk: residualRisk,
|
|
RiskLevel: riskLevel,
|
|
IsAcceptable: acceptable,
|
|
AcceptanceJustification: req.AcceptanceJustification,
|
|
AssessedBy: userID,
|
|
}
|
|
|
|
if err := h.store.CreateRiskAssessment(c.Request.Context(), assessment); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Update hazard status
|
|
h.store.UpdateHazard(c.Request.Context(), hazardID, map[string]interface{}{
|
|
"status": string(iace.HazardStatusAssessed),
|
|
})
|
|
|
|
// Audit trail
|
|
newVals, _ := json.Marshal(assessment)
|
|
h.store.AddAuditEntry(
|
|
c.Request.Context(), projectID, "risk_assessment", assessment.ID,
|
|
iace.AuditActionCreate, userID.String(), nil, newVals,
|
|
)
|
|
|
|
c.JSON(http.StatusCreated, gin.H{
|
|
"assessment": assessment,
|
|
"acceptable": acceptable,
|
|
"acceptance_reason": acceptanceReason,
|
|
})
|
|
}
|
|
|
|
// GetRiskSummary handles GET /projects/:id/risk-summary
|
|
// Returns an aggregated risk overview for a project.
|
|
func (h *IACEHandler) GetRiskSummary(c *gin.Context) {
|
|
projectID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
|
|
return
|
|
}
|
|
|
|
summary, err := h.store.GetRiskSummary(c.Request.Context(), projectID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"risk_summary": summary})
|
|
}
|
|
|
|
// CreateMitigation handles POST /projects/:id/hazards/:hid/mitigations
|
|
// Creates a new mitigation measure for a hazard.
|
|
func (h *IACEHandler) CreateMitigation(c *gin.Context) {
|
|
projectID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
|
|
return
|
|
}
|
|
|
|
hazardID, err := uuid.Parse(c.Param("hid"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid hazard ID"})
|
|
return
|
|
}
|
|
|
|
var req iace.CreateMitigationRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Override hazard ID from URL path
|
|
req.HazardID = hazardID
|
|
|
|
mitigation, err := h.store.CreateMitigation(c.Request.Context(), req)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Update hazard status to mitigated
|
|
h.store.UpdateHazard(c.Request.Context(), hazardID, map[string]interface{}{
|
|
"status": string(iace.HazardStatusMitigated),
|
|
})
|
|
|
|
// Audit trail
|
|
userID := rbac.GetUserID(c)
|
|
newVals, _ := json.Marshal(mitigation)
|
|
h.store.AddAuditEntry(
|
|
c.Request.Context(), projectID, "mitigation", mitigation.ID,
|
|
iace.AuditActionCreate, userID.String(), nil, newVals,
|
|
)
|
|
|
|
c.JSON(http.StatusCreated, gin.H{"mitigation": mitigation})
|
|
}
|
|
|
|
// UpdateMitigation handles PUT /mitigations/:mid
|
|
// Updates a mitigation measure with the provided fields.
|
|
func (h *IACEHandler) UpdateMitigation(c *gin.Context) {
|
|
mitigationID, err := uuid.Parse(c.Param("mid"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid mitigation ID"})
|
|
return
|
|
}
|
|
|
|
var updates map[string]interface{}
|
|
if err := c.ShouldBindJSON(&updates); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
mitigation, err := h.store.UpdateMitigation(c.Request.Context(), mitigationID, updates)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if mitigation == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "mitigation not found"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"mitigation": mitigation})
|
|
}
|
|
|
|
// VerifyMitigation handles POST /mitigations/:mid/verify
|
|
// Marks a mitigation as verified with a verification result.
|
|
func (h *IACEHandler) VerifyMitigation(c *gin.Context) {
|
|
mitigationID, err := uuid.Parse(c.Param("mid"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid mitigation ID"})
|
|
return
|
|
}
|
|
|
|
var req struct {
|
|
VerificationResult string `json:"verification_result" binding:"required"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
userID := rbac.GetUserID(c)
|
|
|
|
if err := h.store.VerifyMitigation(
|
|
c.Request.Context(), mitigationID, req.VerificationResult, userID.String(),
|
|
); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "mitigation verified"})
|
|
}
|
|
|
|
// ReassessRisk handles POST /projects/:id/hazards/:hid/reassess
|
|
// Creates a post-mitigation risk reassessment for a hazard.
|
|
func (h *IACEHandler) ReassessRisk(c *gin.Context) {
|
|
projectID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
|
|
return
|
|
}
|
|
|
|
hazardID, err := uuid.Parse(c.Param("hid"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid hazard ID"})
|
|
return
|
|
}
|
|
|
|
// Verify hazard exists
|
|
hazard, err := h.store.GetHazard(c.Request.Context(), hazardID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if hazard == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "hazard not found"})
|
|
return
|
|
}
|
|
|
|
var req iace.AssessRiskRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
userID := rbac.GetUserID(c)
|
|
|
|
// Calculate risk using the engine
|
|
inherentRisk := h.engine.CalculateInherentRisk(req.Severity, req.Exposure, req.Probability, req.Avoidance)
|
|
controlEff := h.engine.CalculateControlEffectiveness(req.ControlMaturity, req.ControlCoverage, req.TestEvidenceStrength)
|
|
residualRisk := h.engine.CalculateResidualRisk(req.Severity, req.Exposure, req.Probability, controlEff)
|
|
riskLevel := h.engine.DetermineRiskLevel(residualRisk)
|
|
|
|
// For reassessment, check if all reduction steps have been applied
|
|
mitigations, _ := h.store.ListMitigations(c.Request.Context(), hazardID)
|
|
allReductionStepsApplied := len(mitigations) > 0
|
|
for _, m := range mitigations {
|
|
if m.Status != iace.MitigationStatusVerified {
|
|
allReductionStepsApplied = false
|
|
break
|
|
}
|
|
}
|
|
|
|
acceptable, acceptanceReason := h.engine.IsAcceptable(residualRisk, allReductionStepsApplied, req.AcceptanceJustification != "")
|
|
|
|
// Determine version
|
|
existingAssessments, _ := h.store.ListAssessments(c.Request.Context(), hazardID)
|
|
version := len(existingAssessments) + 1
|
|
|
|
assessment := &iace.RiskAssessment{
|
|
HazardID: hazardID,
|
|
Version: version,
|
|
AssessmentType: iace.AssessmentTypePostMitigation,
|
|
Severity: req.Severity,
|
|
Exposure: req.Exposure,
|
|
Probability: req.Probability,
|
|
Avoidance: req.Avoidance,
|
|
InherentRisk: inherentRisk,
|
|
ControlMaturity: req.ControlMaturity,
|
|
ControlCoverage: req.ControlCoverage,
|
|
TestEvidenceStrength: req.TestEvidenceStrength,
|
|
CEff: controlEff,
|
|
ResidualRisk: residualRisk,
|
|
RiskLevel: riskLevel,
|
|
IsAcceptable: acceptable,
|
|
AcceptanceJustification: req.AcceptanceJustification,
|
|
AssessedBy: userID,
|
|
}
|
|
|
|
if err := h.store.CreateRiskAssessment(c.Request.Context(), assessment); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Audit trail
|
|
newVals, _ := json.Marshal(assessment)
|
|
h.store.AddAuditEntry(
|
|
c.Request.Context(), projectID, "risk_assessment", assessment.ID,
|
|
iace.AuditActionCreate, userID.String(), nil, newVals,
|
|
)
|
|
|
|
c.JSON(http.StatusCreated, gin.H{
|
|
"assessment": assessment,
|
|
"acceptable": acceptable,
|
|
"acceptance_reason": acceptanceReason,
|
|
"all_reduction_steps_applied": allReductionStepsApplied,
|
|
})
|
|
}
|
|
|
|
// ============================================================================
|
|
// Evidence & Verification
|
|
// ============================================================================
|
|
|
|
// UploadEvidence handles POST /projects/:id/evidence
|
|
// Creates a new evidence record for a project.
|
|
func (h *IACEHandler) UploadEvidence(c *gin.Context) {
|
|
projectID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
|
|
return
|
|
}
|
|
|
|
var req struct {
|
|
MitigationID *uuid.UUID `json:"mitigation_id,omitempty"`
|
|
VerificationPlanID *uuid.UUID `json:"verification_plan_id,omitempty"`
|
|
FileName string `json:"file_name" binding:"required"`
|
|
FilePath string `json:"file_path" binding:"required"`
|
|
FileHash string `json:"file_hash" binding:"required"`
|
|
FileSize int64 `json:"file_size" binding:"required"`
|
|
MimeType string `json:"mime_type" binding:"required"`
|
|
Description string `json:"description,omitempty"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
userID := rbac.GetUserID(c)
|
|
|
|
evidence := &iace.Evidence{
|
|
ProjectID: projectID,
|
|
MitigationID: req.MitigationID,
|
|
VerificationPlanID: req.VerificationPlanID,
|
|
FileName: req.FileName,
|
|
FilePath: req.FilePath,
|
|
FileHash: req.FileHash,
|
|
FileSize: req.FileSize,
|
|
MimeType: req.MimeType,
|
|
Description: req.Description,
|
|
UploadedBy: userID,
|
|
}
|
|
|
|
if err := h.store.CreateEvidence(c.Request.Context(), evidence); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Audit trail
|
|
newVals, _ := json.Marshal(evidence)
|
|
h.store.AddAuditEntry(
|
|
c.Request.Context(), projectID, "evidence", evidence.ID,
|
|
iace.AuditActionCreate, userID.String(), nil, newVals,
|
|
)
|
|
|
|
c.JSON(http.StatusCreated, gin.H{"evidence": evidence})
|
|
}
|
|
|
|
// ListEvidence handles GET /projects/:id/evidence
|
|
// Lists all evidence records for a project.
|
|
func (h *IACEHandler) ListEvidence(c *gin.Context) {
|
|
projectID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
|
|
return
|
|
}
|
|
|
|
evidence, err := h.store.ListEvidence(c.Request.Context(), projectID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
if evidence == nil {
|
|
evidence = []iace.Evidence{}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"evidence": evidence,
|
|
"total": len(evidence),
|
|
})
|
|
}
|
|
|
|
// CreateVerificationPlan handles POST /projects/:id/verification-plan
|
|
// Creates a new verification plan for a project.
|
|
func (h *IACEHandler) CreateVerificationPlan(c *gin.Context) {
|
|
projectID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
|
|
return
|
|
}
|
|
|
|
var req iace.CreateVerificationPlanRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Override project ID from URL path
|
|
req.ProjectID = projectID
|
|
|
|
plan, err := h.store.CreateVerificationPlan(c.Request.Context(), req)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Audit trail
|
|
userID := rbac.GetUserID(c)
|
|
newVals, _ := json.Marshal(plan)
|
|
h.store.AddAuditEntry(
|
|
c.Request.Context(), projectID, "verification_plan", plan.ID,
|
|
iace.AuditActionCreate, userID.String(), nil, newVals,
|
|
)
|
|
|
|
c.JSON(http.StatusCreated, gin.H{"verification_plan": plan})
|
|
}
|
|
|
|
// UpdateVerificationPlan handles PUT /verification-plan/:vid
|
|
// Updates a verification plan with the provided fields.
|
|
func (h *IACEHandler) UpdateVerificationPlan(c *gin.Context) {
|
|
planID, err := uuid.Parse(c.Param("vid"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid verification plan ID"})
|
|
return
|
|
}
|
|
|
|
var updates map[string]interface{}
|
|
if err := c.ShouldBindJSON(&updates); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
plan, err := h.store.UpdateVerificationPlan(c.Request.Context(), planID, updates)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if plan == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "verification plan not found"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"verification_plan": plan})
|
|
}
|
|
|
|
// CompleteVerification handles POST /verification-plan/:vid/complete
|
|
// Marks a verification plan as completed with a result.
|
|
func (h *IACEHandler) CompleteVerification(c *gin.Context) {
|
|
planID, err := uuid.Parse(c.Param("vid"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid verification plan ID"})
|
|
return
|
|
}
|
|
|
|
var req struct {
|
|
Result string `json:"result" binding:"required"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
userID := rbac.GetUserID(c)
|
|
|
|
if err := h.store.CompleteVerification(
|
|
c.Request.Context(), planID, req.Result, userID.String(),
|
|
); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "verification completed"})
|
|
}
|
|
|
|
// ============================================================================
|
|
// CE Technical File
|
|
// ============================================================================
|
|
|
|
// GenerateTechFile handles POST /projects/:id/tech-file/generate
|
|
// Generates technical file sections for a project.
|
|
// TODO: Integrate LLM for intelligent content generation based on project data.
|
|
func (h *IACEHandler) GenerateTechFile(c *gin.Context) {
|
|
projectID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
|
|
return
|
|
}
|
|
|
|
project, err := h.store.GetProject(c.Request.Context(), projectID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if project == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "project not found"})
|
|
return
|
|
}
|
|
|
|
// Define the standard CE technical file sections to generate
|
|
sectionDefinitions := []struct {
|
|
SectionType string
|
|
Title string
|
|
}{
|
|
{"general_description", "General Description of the Machinery"},
|
|
{"risk_assessment_report", "Risk Assessment Report"},
|
|
{"hazard_log_combined", "Combined Hazard Log"},
|
|
{"essential_requirements", "Essential Health and Safety Requirements"},
|
|
{"design_specifications", "Design Specifications and Drawings"},
|
|
{"test_reports", "Test Reports and Verification Results"},
|
|
{"standards_applied", "Applied Harmonised Standards"},
|
|
{"declaration_of_conformity", "EU Declaration of Conformity"},
|
|
}
|
|
|
|
// Check if project has AI components for additional sections
|
|
components, _ := h.store.ListComponents(c.Request.Context(), projectID)
|
|
hasAI := false
|
|
for _, comp := range components {
|
|
if comp.ComponentType == iace.ComponentTypeAIModel {
|
|
hasAI = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if hasAI {
|
|
sectionDefinitions = append(sectionDefinitions,
|
|
struct {
|
|
SectionType string
|
|
Title string
|
|
}{"ai_intended_purpose", "AI System Intended Purpose"},
|
|
struct {
|
|
SectionType string
|
|
Title string
|
|
}{"ai_model_description", "AI Model Description and Training Data"},
|
|
struct {
|
|
SectionType string
|
|
Title string
|
|
}{"ai_risk_management", "AI Risk Management System"},
|
|
struct {
|
|
SectionType string
|
|
Title string
|
|
}{"ai_human_oversight", "AI Human Oversight Measures"},
|
|
)
|
|
}
|
|
|
|
// Generate each section with placeholder content
|
|
// TODO: Replace placeholder content with LLM-generated content based on project data
|
|
var sections []iace.TechFileSection
|
|
existingSections, _ := h.store.ListTechFileSections(c.Request.Context(), projectID)
|
|
existingMap := make(map[string]bool)
|
|
for _, s := range existingSections {
|
|
existingMap[s.SectionType] = true
|
|
}
|
|
|
|
for _, def := range sectionDefinitions {
|
|
// Skip sections that already exist
|
|
if existingMap[def.SectionType] {
|
|
continue
|
|
}
|
|
|
|
content := fmt.Sprintf(
|
|
"[Auto-generated placeholder for '%s']\n\n"+
|
|
"Machine: %s\nManufacturer: %s\nType: %s\n\n"+
|
|
"TODO: Replace this placeholder with actual content. "+
|
|
"LLM-based generation will be integrated in a future release.",
|
|
def.Title,
|
|
project.MachineName,
|
|
project.Manufacturer,
|
|
project.MachineType,
|
|
)
|
|
|
|
section, err := h.store.CreateTechFileSection(
|
|
c.Request.Context(), projectID, def.SectionType, def.Title, content,
|
|
)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
sections = append(sections, *section)
|
|
}
|
|
|
|
// Update project status
|
|
h.store.UpdateProjectStatus(c.Request.Context(), projectID, iace.ProjectStatusTechFile)
|
|
|
|
// Audit trail
|
|
userID := rbac.GetUserID(c)
|
|
h.store.AddAuditEntry(
|
|
c.Request.Context(), projectID, "tech_file", projectID,
|
|
iace.AuditActionCreate, userID.String(), nil, nil,
|
|
)
|
|
|
|
c.JSON(http.StatusCreated, gin.H{
|
|
"sections_created": len(sections),
|
|
"sections": sections,
|
|
"_note": "TODO: LLM-based content generation not yet implemented",
|
|
})
|
|
}
|
|
|
|
// ListTechFileSections handles GET /projects/:id/tech-file
|
|
// Lists all technical file sections for a project.
|
|
func (h *IACEHandler) ListTechFileSections(c *gin.Context) {
|
|
projectID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
|
|
return
|
|
}
|
|
|
|
sections, err := h.store.ListTechFileSections(c.Request.Context(), projectID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
if sections == nil {
|
|
sections = []iace.TechFileSection{}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"sections": sections,
|
|
"total": len(sections),
|
|
})
|
|
}
|
|
|
|
// UpdateTechFileSection handles PUT /projects/:id/tech-file/:section
|
|
// Updates the content of a technical file section (identified by section_type).
|
|
func (h *IACEHandler) UpdateTechFileSection(c *gin.Context) {
|
|
projectID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
|
|
return
|
|
}
|
|
|
|
sectionType := c.Param("section")
|
|
if sectionType == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "section type required"})
|
|
return
|
|
}
|
|
|
|
var req struct {
|
|
Content string `json:"content" binding:"required"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Find the section by project ID and section type
|
|
sections, err := h.store.ListTechFileSections(c.Request.Context(), projectID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
var sectionID uuid.UUID
|
|
found := false
|
|
for _, s := range sections {
|
|
if s.SectionType == sectionType {
|
|
sectionID = s.ID
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("tech file section '%s' not found", sectionType)})
|
|
return
|
|
}
|
|
|
|
if err := h.store.UpdateTechFileSection(c.Request.Context(), sectionID, req.Content); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Audit trail
|
|
userID := rbac.GetUserID(c)
|
|
h.store.AddAuditEntry(
|
|
c.Request.Context(), projectID, "tech_file_section", sectionID,
|
|
iace.AuditActionUpdate, userID.String(), nil, nil,
|
|
)
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "tech file section updated"})
|
|
}
|
|
|
|
// ApproveTechFileSection handles POST /projects/:id/tech-file/:section/approve
|
|
// Marks a technical file section as approved.
|
|
func (h *IACEHandler) ApproveTechFileSection(c *gin.Context) {
|
|
projectID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
|
|
return
|
|
}
|
|
|
|
sectionType := c.Param("section")
|
|
if sectionType == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "section type required"})
|
|
return
|
|
}
|
|
|
|
// Find the section by project ID and section type
|
|
sections, err := h.store.ListTechFileSections(c.Request.Context(), projectID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
var sectionID uuid.UUID
|
|
found := false
|
|
for _, s := range sections {
|
|
if s.SectionType == sectionType {
|
|
sectionID = s.ID
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("tech file section '%s' not found", sectionType)})
|
|
return
|
|
}
|
|
|
|
userID := rbac.GetUserID(c)
|
|
|
|
if err := h.store.ApproveTechFileSection(c.Request.Context(), sectionID, userID.String()); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Audit trail
|
|
h.store.AddAuditEntry(
|
|
c.Request.Context(), projectID, "tech_file_section", sectionID,
|
|
iace.AuditActionApprove, userID.String(), nil, nil,
|
|
)
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "tech file section approved"})
|
|
}
|
|
|
|
// ExportTechFile handles GET /projects/:id/tech-file/export
|
|
// Exports all tech file sections as a combined JSON document.
|
|
// TODO: Implement PDF export with proper formatting.
|
|
func (h *IACEHandler) ExportTechFile(c *gin.Context) {
|
|
projectID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
|
|
return
|
|
}
|
|
|
|
project, err := h.store.GetProject(c.Request.Context(), projectID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if project == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "project not found"})
|
|
return
|
|
}
|
|
|
|
sections, err := h.store.ListTechFileSections(c.Request.Context(), projectID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Check if all sections are approved
|
|
allApproved := true
|
|
for _, s := range sections {
|
|
if s.Status != iace.TechFileSectionStatusApproved {
|
|
allApproved = false
|
|
break
|
|
}
|
|
}
|
|
|
|
classifications, _ := h.store.GetClassifications(c.Request.Context(), projectID)
|
|
riskSummary, _ := h.store.GetRiskSummary(c.Request.Context(), projectID)
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"project": project,
|
|
"sections": sections,
|
|
"classifications": classifications,
|
|
"risk_summary": riskSummary,
|
|
"all_approved": allApproved,
|
|
"export_format": "json",
|
|
"_note": "PDF export will be available in a future release",
|
|
})
|
|
}
|
|
|
|
// ============================================================================
|
|
// Monitoring
|
|
// ============================================================================
|
|
|
|
// CreateMonitoringEvent handles POST /projects/:id/monitoring
|
|
// Creates a new post-market monitoring event.
|
|
func (h *IACEHandler) CreateMonitoringEvent(c *gin.Context) {
|
|
projectID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
|
|
return
|
|
}
|
|
|
|
var req iace.CreateMonitoringEventRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Override project ID from URL path
|
|
req.ProjectID = projectID
|
|
|
|
event, err := h.store.CreateMonitoringEvent(c.Request.Context(), req)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Audit trail
|
|
userID := rbac.GetUserID(c)
|
|
newVals, _ := json.Marshal(event)
|
|
h.store.AddAuditEntry(
|
|
c.Request.Context(), projectID, "monitoring_event", event.ID,
|
|
iace.AuditActionCreate, userID.String(), nil, newVals,
|
|
)
|
|
|
|
c.JSON(http.StatusCreated, gin.H{"monitoring_event": event})
|
|
}
|
|
|
|
// ListMonitoringEvents handles GET /projects/:id/monitoring
|
|
// Lists all monitoring events for a project.
|
|
func (h *IACEHandler) ListMonitoringEvents(c *gin.Context) {
|
|
projectID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
|
|
return
|
|
}
|
|
|
|
events, err := h.store.ListMonitoringEvents(c.Request.Context(), projectID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
if events == nil {
|
|
events = []iace.MonitoringEvent{}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"monitoring_events": events,
|
|
"total": len(events),
|
|
})
|
|
}
|
|
|
|
// UpdateMonitoringEvent handles PUT /projects/:id/monitoring/:eid
|
|
// Updates a monitoring event with the provided fields.
|
|
func (h *IACEHandler) UpdateMonitoringEvent(c *gin.Context) {
|
|
_, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
|
|
return
|
|
}
|
|
|
|
eventID, err := uuid.Parse(c.Param("eid"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid monitoring event ID"})
|
|
return
|
|
}
|
|
|
|
var updates map[string]interface{}
|
|
if err := c.ShouldBindJSON(&updates); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
event, err := h.store.UpdateMonitoringEvent(c.Request.Context(), eventID, updates)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if event == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "monitoring event not found"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"monitoring_event": event})
|
|
}
|
|
|
|
// GetAuditTrail handles GET /projects/:id/audit-trail
|
|
// Returns all audit trail entries for a project, newest first.
|
|
func (h *IACEHandler) GetAuditTrail(c *gin.Context) {
|
|
projectID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
|
|
return
|
|
}
|
|
|
|
entries, err := h.store.ListAuditTrail(c.Request.Context(), projectID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
if entries == nil {
|
|
entries = []iace.AuditTrailEntry{}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"audit_trail": entries,
|
|
"total": len(entries),
|
|
})
|
|
}
|
|
|
|
// ============================================================================
|
|
// Internal Helpers
|
|
// ============================================================================
|
|
|
|
// buildCompletenessContext constructs the CompletenessContext needed by the checker
|
|
// by loading all related entities for a project.
|
|
func (h *IACEHandler) buildCompletenessContext(
|
|
c *gin.Context,
|
|
project *iace.Project,
|
|
components []iace.Component,
|
|
classifications []iace.RegulatoryClassification,
|
|
) *iace.CompletenessContext {
|
|
projectID := project.ID
|
|
|
|
hazards, _ := h.store.ListHazards(c.Request.Context(), projectID)
|
|
|
|
var allAssessments []iace.RiskAssessment
|
|
var allMitigations []iace.Mitigation
|
|
for _, hazard := range hazards {
|
|
assessments, _ := h.store.ListAssessments(c.Request.Context(), hazard.ID)
|
|
allAssessments = append(allAssessments, assessments...)
|
|
|
|
mitigations, _ := h.store.ListMitigations(c.Request.Context(), hazard.ID)
|
|
allMitigations = append(allMitigations, mitigations...)
|
|
}
|
|
|
|
evidence, _ := h.store.ListEvidence(c.Request.Context(), projectID)
|
|
techFileSections, _ := h.store.ListTechFileSections(c.Request.Context(), projectID)
|
|
|
|
hasAI := false
|
|
for _, comp := range components {
|
|
if comp.ComponentType == iace.ComponentTypeAIModel {
|
|
hasAI = true
|
|
break
|
|
}
|
|
}
|
|
|
|
return &iace.CompletenessContext{
|
|
Project: project,
|
|
Components: components,
|
|
Classifications: classifications,
|
|
Hazards: hazards,
|
|
Assessments: allAssessments,
|
|
Mitigations: allMitigations,
|
|
Evidence: evidence,
|
|
TechFileSections: techFileSections,
|
|
HasAI: hasAI,
|
|
}
|
|
}
|
|
|
|
// componentTypeKeys extracts keys from a map[string]bool and returns them as a sorted slice.
|
|
func componentTypeKeys(m map[string]bool) []string {
|
|
keys := make([]string, 0, len(m))
|
|
for k := range m {
|
|
keys = append(keys, k)
|
|
}
|
|
// Sort for deterministic output
|
|
sortStrings(keys)
|
|
return keys
|
|
}
|
|
|
|
// sortStrings sorts a slice of strings in place using a simple insertion sort.
|
|
func sortStrings(s []string) {
|
|
for i := 1; i < len(s); i++ {
|
|
for j := i; j > 0 && strings.Compare(s[j-1], s[j]) > 0; j-- {
|
|
s[j-1], s[j] = s[j], s[j-1]
|
|
}
|
|
}
|
|
}
|