Services: Admin-Compliance, Backend-Compliance, AI-Compliance-SDK, Consent-SDK, Developer-Portal, PCA-Platform, DSMS Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
584 lines
20 KiB
Go
584 lines
20 KiB
Go
package ucca
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// =============================================================================
|
|
// License Policy Engine
|
|
// Handles license/copyright compliance for standards and norms
|
|
// =============================================================================
|
|
|
|
// LicensedContentFacts represents the license-related facts from the wizard
|
|
type LicensedContentFacts struct {
|
|
Present bool `json:"present"`
|
|
Publisher string `json:"publisher"` // DIN_MEDIA, VDI, VDE, ISO, etc.
|
|
LicenseType string `json:"license_type"` // SINGLE_WORKSTATION, NETWORK_INTRANET, etc.
|
|
AIUsePermitted string `json:"ai_use_permitted"` // YES, NO, UNKNOWN
|
|
ProofUploaded bool `json:"proof_uploaded"`
|
|
OperationMode string `json:"operation_mode"` // LINK_ONLY, NOTES_ONLY, FULLTEXT_RAG, TRAINING
|
|
DistributionScope string `json:"distribution_scope"` // SINGLE_USER, COMPANY_INTERNAL, etc.
|
|
ContentType string `json:"content_type"` // NORM_FULLTEXT, CUSTOMER_NOTES, etc.
|
|
}
|
|
|
|
// LicensePolicyResult represents the evaluation result
|
|
type LicensePolicyResult struct {
|
|
Allowed bool `json:"allowed"`
|
|
EffectiveMode string `json:"effective_mode"` // The mode that will actually be used
|
|
Reason string `json:"reason"`
|
|
Gaps []LicenseGap `json:"gaps"`
|
|
RequiredControls []LicenseControl `json:"required_controls"`
|
|
StopLine *LicenseStopLine `json:"stop_line,omitempty"` // If hard blocked
|
|
OutputRestrictions *OutputRestrictions `json:"output_restrictions"`
|
|
EscalationLevel string `json:"escalation_level"`
|
|
RiskScore int `json:"risk_score"`
|
|
}
|
|
|
|
// LicenseGap represents a license-related gap
|
|
type LicenseGap struct {
|
|
ID string `json:"id"`
|
|
Title string `json:"title"`
|
|
Description string `json:"description"`
|
|
Controls []string `json:"controls"`
|
|
Severity string `json:"severity"`
|
|
}
|
|
|
|
// LicenseControl represents a required control for license compliance
|
|
type LicenseControl struct {
|
|
ID string `json:"id"`
|
|
Title string `json:"title"`
|
|
Description string `json:"description"`
|
|
WhatToDo string `json:"what_to_do"`
|
|
Evidence []string `json:"evidence_needed"`
|
|
}
|
|
|
|
// LicenseStopLine represents a hard block
|
|
type LicenseStopLine struct {
|
|
ID string `json:"id"`
|
|
Title string `json:"title"`
|
|
Message string `json:"message"`
|
|
Outcome string `json:"outcome"` // NOT_ALLOWED, NOT_ALLOWED_UNTIL_LICENSE_CLEARED
|
|
}
|
|
|
|
// OutputRestrictions defines how outputs should be filtered
|
|
type OutputRestrictions struct {
|
|
AllowQuotes bool `json:"allow_quotes"`
|
|
MaxQuoteLength int `json:"max_quote_length"` // in characters
|
|
RequireCitation bool `json:"require_citation"`
|
|
AllowCopy bool `json:"allow_copy"`
|
|
AllowExport bool `json:"allow_export"`
|
|
}
|
|
|
|
// LicensePolicyEngine evaluates license compliance
|
|
type LicensePolicyEngine struct {
|
|
// Configuration can be added here
|
|
}
|
|
|
|
// NewLicensePolicyEngine creates a new license policy engine
|
|
func NewLicensePolicyEngine() *LicensePolicyEngine {
|
|
return &LicensePolicyEngine{}
|
|
}
|
|
|
|
// Evaluate evaluates the license facts and returns the policy result
|
|
func (e *LicensePolicyEngine) Evaluate(facts *LicensedContentFacts) *LicensePolicyResult {
|
|
result := &LicensePolicyResult{
|
|
Allowed: true,
|
|
EffectiveMode: "LINK_ONLY", // Default safe mode
|
|
Gaps: []LicenseGap{},
|
|
RequiredControls: []LicenseControl{},
|
|
OutputRestrictions: &OutputRestrictions{
|
|
AllowQuotes: false,
|
|
MaxQuoteLength: 0,
|
|
RequireCitation: true,
|
|
AllowCopy: false,
|
|
AllowExport: false,
|
|
},
|
|
RiskScore: 0,
|
|
}
|
|
|
|
// If no licensed content, return early with no restrictions
|
|
if !facts.Present {
|
|
result.EffectiveMode = "UNRESTRICTED"
|
|
result.OutputRestrictions = &OutputRestrictions{
|
|
AllowQuotes: true,
|
|
MaxQuoteLength: -1, // unlimited
|
|
RequireCitation: false,
|
|
AllowCopy: true,
|
|
AllowExport: true,
|
|
}
|
|
return result
|
|
}
|
|
|
|
// Evaluate based on operation mode and license status
|
|
switch facts.OperationMode {
|
|
case "LINK_ONLY":
|
|
e.evaluateLinkOnlyMode(facts, result)
|
|
case "NOTES_ONLY":
|
|
e.evaluateNotesOnlyMode(facts, result)
|
|
case "EXCERPT_ONLY":
|
|
e.evaluateExcerptOnlyMode(facts, result)
|
|
case "FULLTEXT_RAG":
|
|
e.evaluateFulltextRAGMode(facts, result)
|
|
case "TRAINING":
|
|
e.evaluateTrainingMode(facts, result)
|
|
default:
|
|
// Unknown mode, default to LINK_ONLY
|
|
e.evaluateLinkOnlyMode(facts, result)
|
|
}
|
|
|
|
// Check publisher-specific restrictions
|
|
e.applyPublisherRestrictions(facts, result)
|
|
|
|
// Check distribution scope vs license type
|
|
e.checkDistributionScope(facts, result)
|
|
|
|
return result
|
|
}
|
|
|
|
// evaluateLinkOnlyMode - safest mode, always allowed
|
|
func (e *LicensePolicyEngine) evaluateLinkOnlyMode(facts *LicensedContentFacts, result *LicensePolicyResult) {
|
|
result.EffectiveMode = "LINK_ONLY"
|
|
result.Allowed = true
|
|
result.Reason = "Link-only Modus ist ohne spezielle Lizenz erlaubt"
|
|
result.RiskScore = 0
|
|
|
|
// Very restrictive output
|
|
result.OutputRestrictions = &OutputRestrictions{
|
|
AllowQuotes: false,
|
|
MaxQuoteLength: 0,
|
|
RequireCitation: true,
|
|
AllowCopy: false,
|
|
AllowExport: false,
|
|
}
|
|
|
|
// Recommend control for proper setup
|
|
result.RequiredControls = append(result.RequiredControls, LicenseControl{
|
|
ID: "CTRL-LINK-ONLY-MODE",
|
|
Title: "Link-only / Evidence Navigator aktivieren",
|
|
Description: "Nur Verweise und Checklisten, kein Volltext",
|
|
WhatToDo: "System auf LINK_ONLY konfigurieren, keine Normtexte indexieren",
|
|
Evidence: []string{"System-Konfiguration", "Stichproben-Audit"},
|
|
})
|
|
}
|
|
|
|
// evaluateNotesOnlyMode - customer notes, usually allowed
|
|
func (e *LicensePolicyEngine) evaluateNotesOnlyMode(facts *LicensedContentFacts, result *LicensePolicyResult) {
|
|
result.EffectiveMode = "NOTES_ONLY"
|
|
result.Allowed = true
|
|
result.Reason = "Notes-only Modus mit kundeneigenen Zusammenfassungen"
|
|
result.RiskScore = 10
|
|
|
|
// Allow paraphrased content
|
|
result.OutputRestrictions = &OutputRestrictions{
|
|
AllowQuotes: false, // No direct quotes from norms
|
|
MaxQuoteLength: 0,
|
|
RequireCitation: true,
|
|
AllowCopy: true, // Can copy own notes
|
|
AllowExport: true, // Can export own notes
|
|
}
|
|
|
|
result.RequiredControls = append(result.RequiredControls, LicenseControl{
|
|
ID: "CTRL-NOTES-ONLY-RAG",
|
|
Title: "Notes-only RAG (kundeneigene Paraphrasen)",
|
|
Description: "Nur kundeneigene Zusammenfassungen indexieren",
|
|
WhatToDo: "UI-Flow fuer Notes-Erstellung, kein Copy/Paste von Originaltexten",
|
|
Evidence: []string{"Notes-Provenance-Log", "Stichproben"},
|
|
})
|
|
|
|
// Add gap if license type is unknown
|
|
if facts.LicenseType == "UNKNOWN" {
|
|
result.Gaps = append(result.Gaps, LicenseGap{
|
|
ID: "GAP_LICENSE_UNKNOWN",
|
|
Title: "Lizenzlage unklar",
|
|
Description: "Die Lizenzlage sollte geklaert werden",
|
|
Controls: []string{"CTRL-LICENSE-PROOF"},
|
|
Severity: "WARN",
|
|
})
|
|
result.EscalationLevel = "E2"
|
|
}
|
|
}
|
|
|
|
// evaluateExcerptOnlyMode - short quotes under citation rights
|
|
func (e *LicensePolicyEngine) evaluateExcerptOnlyMode(facts *LicensedContentFacts, result *LicensePolicyResult) {
|
|
result.EffectiveMode = "EXCERPT_ONLY"
|
|
result.RiskScore = 30
|
|
|
|
// Check if AI use is permitted
|
|
if facts.AIUsePermitted == "NO" || facts.AIUsePermitted == "UNKNOWN" {
|
|
// Downgrade to NOTES_ONLY
|
|
result.EffectiveMode = "NOTES_ONLY"
|
|
result.Reason = "Excerpt-Modus nicht erlaubt ohne AI-Freigabe, Downgrade auf Notes-only"
|
|
result.Gaps = append(result.Gaps, LicenseGap{
|
|
ID: "GAP_AI_USE_NOT_PERMITTED",
|
|
Title: "AI/TDM-Nutzung nicht erlaubt",
|
|
Description: "Zitate erfordern AI-Nutzungserlaubnis",
|
|
Controls: []string{"CTRL-LICENSE-PROOF", "CTRL-NOTES-ONLY-RAG"},
|
|
Severity: "WARN",
|
|
})
|
|
result.EscalationLevel = "E2"
|
|
} else {
|
|
result.Allowed = true
|
|
result.Reason = "Kurze Zitate im Rahmen des Zitatrechts"
|
|
}
|
|
|
|
result.OutputRestrictions = &OutputRestrictions{
|
|
AllowQuotes: facts.AIUsePermitted == "YES",
|
|
MaxQuoteLength: 150, // Max 150 characters per quote
|
|
RequireCitation: true,
|
|
AllowCopy: false,
|
|
AllowExport: false,
|
|
}
|
|
|
|
result.RequiredControls = append(result.RequiredControls, LicenseControl{
|
|
ID: "CTRL-OUTPUT-GUARD-QUOTES",
|
|
Title: "Output-Guard: Quote-Limits",
|
|
Description: "Zitatlänge begrenzen",
|
|
WhatToDo: "Max. 150 Zeichen pro Zitat, immer mit Quellenangabe",
|
|
Evidence: []string{"Output-Guard-Konfiguration"},
|
|
})
|
|
}
|
|
|
|
// evaluateFulltextRAGMode - requires explicit license proof
|
|
func (e *LicensePolicyEngine) evaluateFulltextRAGMode(facts *LicensedContentFacts, result *LicensePolicyResult) {
|
|
result.RiskScore = 60
|
|
|
|
// Check if AI use is explicitly permitted AND proof is uploaded
|
|
if facts.AIUsePermitted == "YES" && facts.ProofUploaded {
|
|
result.EffectiveMode = "FULLTEXT_RAG"
|
|
result.Allowed = true
|
|
result.Reason = "Volltext-RAG mit nachgewiesener AI-Lizenz"
|
|
|
|
result.OutputRestrictions = &OutputRestrictions{
|
|
AllowQuotes: true,
|
|
MaxQuoteLength: 500, // Still limited
|
|
RequireCitation: true,
|
|
AllowCopy: false, // No copy to prevent redistribution
|
|
AllowExport: false, // No export
|
|
}
|
|
|
|
result.RequiredControls = append(result.RequiredControls,
|
|
LicenseControl{
|
|
ID: "CTRL-LICENSE-GATED-INGEST",
|
|
Title: "License-gated Ingest",
|
|
Description: "Technische Durchsetzung der Lizenzpruefung",
|
|
WhatToDo: "Ingest-Pipeline prueft Lizenz vor Indexierung",
|
|
Evidence: []string{"Ingest-Audit-Logs"},
|
|
},
|
|
LicenseControl{
|
|
ID: "CTRL-TENANT-ISOLATION-STANDARDS",
|
|
Title: "Tenant-Isolation",
|
|
Description: "Strikte Trennung lizenzierter Inhalte",
|
|
WhatToDo: "Keine Cross-Tenant-Suche, kein Export",
|
|
Evidence: []string{"Tenant-Isolation-Dokumentation"},
|
|
},
|
|
)
|
|
} else {
|
|
// NOT ALLOWED - downgrade to LINK_ONLY
|
|
result.Allowed = false
|
|
result.EffectiveMode = "LINK_ONLY"
|
|
result.Reason = "Volltext-RAG ohne Lizenznachweis nicht erlaubt"
|
|
|
|
result.Gaps = append(result.Gaps, LicenseGap{
|
|
ID: "GAP_FULLTEXT_WITHOUT_PROOF",
|
|
Title: "Volltext-RAG ohne Lizenznachweis",
|
|
Description: "Volltext-RAG erfordert nachgewiesene AI-Nutzungserlaubnis",
|
|
Controls: []string{"CTRL-LICENSE-PROOF", "CTRL-LINK-ONLY-MODE"},
|
|
Severity: "BLOCK",
|
|
})
|
|
|
|
result.EscalationLevel = "E3"
|
|
|
|
// Set stop line
|
|
result.StopLine = &LicenseStopLine{
|
|
ID: "STOP_FULLTEXT_WITHOUT_PROOF",
|
|
Title: "Volltext-RAG blockiert",
|
|
Message: "Volltext-RAG erfordert einen Nachweis der AI-Nutzungserlaubnis. Bitte laden Sie den Lizenzvertrag hoch oder wechseln Sie auf Link-only Modus.",
|
|
Outcome: "NOT_ALLOWED_UNTIL_LICENSE_CLEARED",
|
|
}
|
|
|
|
result.OutputRestrictions = &OutputRestrictions{
|
|
AllowQuotes: false,
|
|
MaxQuoteLength: 0,
|
|
RequireCitation: true,
|
|
AllowCopy: false,
|
|
AllowExport: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
// evaluateTrainingMode - most restrictive, rarely allowed
|
|
func (e *LicensePolicyEngine) evaluateTrainingMode(facts *LicensedContentFacts, result *LicensePolicyResult) {
|
|
result.RiskScore = 80
|
|
|
|
// Training is almost always blocked for standards
|
|
if facts.AIUsePermitted == "YES" && facts.ProofUploaded && facts.LicenseType == "AI_LICENSE" {
|
|
result.EffectiveMode = "TRAINING"
|
|
result.Allowed = true
|
|
result.Reason = "Training mit expliziter AI-Training-Lizenz"
|
|
result.EscalationLevel = "E3" // Still requires review
|
|
} else {
|
|
// HARD BLOCK
|
|
result.Allowed = false
|
|
result.EffectiveMode = "LINK_ONLY"
|
|
result.Reason = "Training auf Standards ohne explizite AI-Training-Lizenz verboten"
|
|
|
|
result.StopLine = &LicenseStopLine{
|
|
ID: "STOP_TRAINING_WITHOUT_PROOF",
|
|
Title: "Training blockiert",
|
|
Message: "Modell-Training mit lizenzierten Standards ist ohne explizite AI-Training-Lizenz nicht zulaessig. DIN Media hat dies ausdruecklich ausgeschlossen.",
|
|
Outcome: "NOT_ALLOWED",
|
|
}
|
|
|
|
result.Gaps = append(result.Gaps, LicenseGap{
|
|
ID: "GAP_TRAINING_ON_STANDARDS",
|
|
Title: "Training auf Standards verboten",
|
|
Description: "Modell-Training erfordert explizite AI-Training-Lizenz",
|
|
Controls: []string{"CTRL-LICENSE-PROOF"},
|
|
Severity: "BLOCK",
|
|
})
|
|
|
|
result.EscalationLevel = "E3"
|
|
}
|
|
|
|
result.OutputRestrictions = &OutputRestrictions{
|
|
AllowQuotes: false,
|
|
MaxQuoteLength: 0,
|
|
RequireCitation: true,
|
|
AllowCopy: false,
|
|
AllowExport: false,
|
|
}
|
|
}
|
|
|
|
// applyPublisherRestrictions applies publisher-specific rules
|
|
func (e *LicensePolicyEngine) applyPublisherRestrictions(facts *LicensedContentFacts, result *LicensePolicyResult) {
|
|
// DIN Media specific restrictions
|
|
if facts.Publisher == "DIN_MEDIA" {
|
|
if facts.AIUsePermitted != "YES" {
|
|
// DIN Media explicitly prohibits AI use without license
|
|
if facts.OperationMode == "FULLTEXT_RAG" || facts.OperationMode == "TRAINING" {
|
|
result.Allowed = false
|
|
result.EffectiveMode = "LINK_ONLY"
|
|
|
|
result.StopLine = &LicenseStopLine{
|
|
ID: "STOP_DIN_FULLTEXT_AI_NOT_ALLOWED",
|
|
Title: "DIN Media AI-Nutzung blockiert",
|
|
Message: "DIN Media untersagt die AI-Nutzung von Normen ohne explizite Genehmigung. Ein AI-Lizenzmodell ist erst ab Q4/2025 geplant. Bitte nutzen Sie Link-only oder Notes-only Modus.",
|
|
Outcome: "NOT_ALLOWED_UNTIL_LICENSE_CLEARED",
|
|
}
|
|
|
|
result.Gaps = append(result.Gaps, LicenseGap{
|
|
ID: "GAP_DIN_MEDIA_WITHOUT_AI_LICENSE",
|
|
Title: "DIN Media ohne AI-Lizenz",
|
|
Description: "DIN Media verbietet AI-Nutzung ohne explizite Genehmigung",
|
|
Controls: []string{"CTRL-LINK-ONLY-MODE", "CTRL-NO-CRAWLING-DIN"},
|
|
Severity: "BLOCK",
|
|
})
|
|
|
|
result.EscalationLevel = "E3"
|
|
result.RiskScore = 70
|
|
}
|
|
}
|
|
|
|
// Always add no-crawling control for DIN Media
|
|
result.RequiredControls = append(result.RequiredControls, LicenseControl{
|
|
ID: "CTRL-NO-CRAWLING-DIN",
|
|
Title: "Crawler-Block fuer DIN Media",
|
|
Description: "Keine automatisierten Abrufe von DIN-Normen-Portalen",
|
|
WhatToDo: "Domain-Denylist konfigurieren, nur manueller Import",
|
|
Evidence: []string{"Domain-Denylist", "Fetch-Logs"},
|
|
})
|
|
}
|
|
}
|
|
|
|
// checkDistributionScope checks if distribution scope matches license type
|
|
func (e *LicensePolicyEngine) checkDistributionScope(facts *LicensedContentFacts, result *LicensePolicyResult) {
|
|
// Single workstation license with broad distribution
|
|
if facts.LicenseType == "SINGLE_WORKSTATION" {
|
|
if facts.DistributionScope == "COMPANY_INTERNAL" ||
|
|
facts.DistributionScope == "SUBSIDIARIES" ||
|
|
facts.DistributionScope == "EXTERNAL_CUSTOMERS" {
|
|
|
|
result.Gaps = append(result.Gaps, LicenseGap{
|
|
ID: "GAP_DISTRIBUTION_SCOPE_MISMATCH",
|
|
Title: "Verteilungsumfang uebersteigt Lizenz",
|
|
Description: "Einzelplatz-Lizenz erlaubt keine unternehmensweite Nutzung",
|
|
Controls: []string{"CTRL-LICENSE-PROOF", "CTRL-LINK-ONLY-MODE"},
|
|
Severity: "WARN",
|
|
})
|
|
|
|
result.EscalationLevel = "E3"
|
|
result.RiskScore += 20
|
|
}
|
|
}
|
|
|
|
// Network license with external distribution
|
|
if facts.LicenseType == "NETWORK_INTRANET" {
|
|
if facts.DistributionScope == "EXTERNAL_CUSTOMERS" {
|
|
result.Gaps = append(result.Gaps, LicenseGap{
|
|
ID: "GAP_DISTRIBUTION_SCOPE_EXTERNAL",
|
|
Title: "Externe Verteilung mit Intranet-Lizenz",
|
|
Description: "Intranet-Lizenz erlaubt keine externe Verteilung",
|
|
Controls: []string{"CTRL-LICENSE-PROOF"},
|
|
Severity: "WARN",
|
|
})
|
|
|
|
result.EscalationLevel = "E2"
|
|
result.RiskScore += 15
|
|
}
|
|
}
|
|
}
|
|
|
|
// CanIngestFulltext checks if fulltext ingestion is allowed
|
|
func (e *LicensePolicyEngine) CanIngestFulltext(facts *LicensedContentFacts) bool {
|
|
if !facts.Present {
|
|
return true // No licensed content, no restrictions
|
|
}
|
|
|
|
switch facts.OperationMode {
|
|
case "LINK_ONLY":
|
|
return false // Only metadata/references
|
|
case "NOTES_ONLY":
|
|
return false // Only customer notes, not fulltext
|
|
case "EXCERPT_ONLY":
|
|
return false // Only short excerpts
|
|
case "FULLTEXT_RAG":
|
|
return facts.AIUsePermitted == "YES" && facts.ProofUploaded
|
|
case "TRAINING":
|
|
return facts.AIUsePermitted == "YES" && facts.ProofUploaded && facts.LicenseType == "AI_LICENSE"
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// CanIngestNotes checks if customer notes can be ingested
|
|
func (e *LicensePolicyEngine) CanIngestNotes(facts *LicensedContentFacts) bool {
|
|
if !facts.Present {
|
|
return true
|
|
}
|
|
|
|
// Notes are allowed in most modes
|
|
return facts.OperationMode == "NOTES_ONLY" ||
|
|
facts.OperationMode == "EXCERPT_ONLY" ||
|
|
facts.OperationMode == "FULLTEXT_RAG" ||
|
|
facts.OperationMode == "TRAINING"
|
|
}
|
|
|
|
// GetEffectiveMode returns the effective operation mode after policy evaluation
|
|
func (e *LicensePolicyEngine) GetEffectiveMode(facts *LicensedContentFacts) string {
|
|
result := e.Evaluate(facts)
|
|
return result.EffectiveMode
|
|
}
|
|
|
|
// LicenseIngestDecision represents the decision for ingesting a document
|
|
type LicenseIngestDecision struct {
|
|
AllowFulltext bool `json:"allow_fulltext"`
|
|
AllowNotes bool `json:"allow_notes"`
|
|
AllowMetadata bool `json:"allow_metadata"`
|
|
Reason string `json:"reason"`
|
|
EffectiveMode string `json:"effective_mode"`
|
|
}
|
|
|
|
// DecideIngest returns the ingest decision for a document
|
|
func (e *LicensePolicyEngine) DecideIngest(facts *LicensedContentFacts) *LicenseIngestDecision {
|
|
result := e.Evaluate(facts)
|
|
|
|
decision := &LicenseIngestDecision{
|
|
AllowMetadata: true, // Metadata is always allowed
|
|
AllowNotes: e.CanIngestNotes(facts),
|
|
AllowFulltext: e.CanIngestFulltext(facts),
|
|
Reason: result.Reason,
|
|
EffectiveMode: result.EffectiveMode,
|
|
}
|
|
|
|
return decision
|
|
}
|
|
|
|
// LicenseAuditEntry represents an audit log entry for license decisions
|
|
type LicenseAuditEntry struct {
|
|
Timestamp time.Time `json:"timestamp"`
|
|
TenantID string `json:"tenant_id"`
|
|
DocumentID string `json:"document_id,omitempty"`
|
|
Facts *LicensedContentFacts `json:"facts"`
|
|
Decision string `json:"decision"` // ALLOW, DENY, DOWNGRADE
|
|
EffectiveMode string `json:"effective_mode"`
|
|
Reason string `json:"reason"`
|
|
StopLineID string `json:"stop_line_id,omitempty"`
|
|
}
|
|
|
|
// FormatAuditEntry creates an audit entry for logging
|
|
func (e *LicensePolicyEngine) FormatAuditEntry(tenantID string, documentID string, facts *LicensedContentFacts, result *LicensePolicyResult) *LicenseAuditEntry {
|
|
decision := "ALLOW"
|
|
if !result.Allowed {
|
|
decision = "DENY"
|
|
} else if result.EffectiveMode != facts.OperationMode {
|
|
decision = "DOWNGRADE"
|
|
}
|
|
|
|
entry := &LicenseAuditEntry{
|
|
Timestamp: time.Now().UTC(),
|
|
TenantID: tenantID,
|
|
DocumentID: documentID,
|
|
Facts: facts,
|
|
Decision: decision,
|
|
EffectiveMode: result.EffectiveMode,
|
|
Reason: result.Reason,
|
|
}
|
|
|
|
if result.StopLine != nil {
|
|
entry.StopLineID = result.StopLine.ID
|
|
}
|
|
|
|
return entry
|
|
}
|
|
|
|
// FormatHumanReadableSummary creates a human-readable summary of the evaluation
|
|
func (e *LicensePolicyEngine) FormatHumanReadableSummary(result *LicensePolicyResult) string {
|
|
var sb strings.Builder
|
|
|
|
sb.WriteString("=== Lizenz-Policy Bewertung ===\n\n")
|
|
|
|
if result.Allowed {
|
|
sb.WriteString(fmt.Sprintf("Status: ERLAUBT\n"))
|
|
} else {
|
|
sb.WriteString(fmt.Sprintf("Status: BLOCKIERT\n"))
|
|
}
|
|
|
|
sb.WriteString(fmt.Sprintf("Effektiver Modus: %s\n", result.EffectiveMode))
|
|
sb.WriteString(fmt.Sprintf("Risiko-Score: %d\n", result.RiskScore))
|
|
sb.WriteString(fmt.Sprintf("Begruendung: %s\n\n", result.Reason))
|
|
|
|
if result.StopLine != nil {
|
|
sb.WriteString("!!! STOP-LINE !!!\n")
|
|
sb.WriteString(fmt.Sprintf(" %s: %s\n", result.StopLine.ID, result.StopLine.Title))
|
|
sb.WriteString(fmt.Sprintf(" %s\n\n", result.StopLine.Message))
|
|
}
|
|
|
|
if len(result.Gaps) > 0 {
|
|
sb.WriteString("Identifizierte Luecken:\n")
|
|
for _, gap := range result.Gaps {
|
|
sb.WriteString(fmt.Sprintf(" - [%s] %s: %s\n", gap.Severity, gap.ID, gap.Title))
|
|
}
|
|
sb.WriteString("\n")
|
|
}
|
|
|
|
if len(result.RequiredControls) > 0 {
|
|
sb.WriteString("Erforderliche Massnahmen:\n")
|
|
for _, ctrl := range result.RequiredControls {
|
|
sb.WriteString(fmt.Sprintf(" - %s: %s\n", ctrl.ID, ctrl.Title))
|
|
}
|
|
sb.WriteString("\n")
|
|
}
|
|
|
|
if result.OutputRestrictions != nil {
|
|
sb.WriteString("Output-Einschraenkungen:\n")
|
|
sb.WriteString(fmt.Sprintf(" Zitate erlaubt: %v\n", result.OutputRestrictions.AllowQuotes))
|
|
sb.WriteString(fmt.Sprintf(" Max. Zitatlaenge: %d Zeichen\n", result.OutputRestrictions.MaxQuoteLength))
|
|
sb.WriteString(fmt.Sprintf(" Copy erlaubt: %v\n", result.OutputRestrictions.AllowCopy))
|
|
sb.WriteString(fmt.Sprintf(" Export erlaubt: %v\n", result.OutputRestrictions.AllowExport))
|
|
}
|
|
|
|
return sb.String()
|
|
}
|