refactor: Consolidate standalone services into admin-v2, add new SDK modules
Remove standalone services (ai-compliance-sdk root, developer-portal, dsms-gateway, dsms-node, night-scheduler) and legacy compliance/dsgvo pages. Add new SDK pipeline modules (academy, document-crawler, dsb-portal, incidents, whistleblower, reporting, sso, multi-tenant, industry-templates). Add drafting engine, legal corpus files (AT/CH/DE), pitch-deck, blog and Förderantrag pages. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -96,6 +96,43 @@ func main() {
|
||||
checkpointHandler := api.NewCheckpointHandler()
|
||||
v1.GET("/checkpoints", checkpointHandler.GetAll)
|
||||
v1.POST("/checkpoints/validate", checkpointHandler.Validate)
|
||||
|
||||
// Academy (Compliance E-Learning)
|
||||
academyHandler := api.NewAcademyHandler(dbPool, llmService, ragService)
|
||||
academy := v1.Group("/academy")
|
||||
{
|
||||
// Course CRUD
|
||||
academy.GET("/courses", academyHandler.ListCourses)
|
||||
academy.GET("/courses/:id", academyHandler.GetCourse)
|
||||
academy.POST("/courses", academyHandler.CreateCourse)
|
||||
academy.PUT("/courses/:id", academyHandler.UpdateCourse)
|
||||
academy.DELETE("/courses/:id", academyHandler.DeleteCourse)
|
||||
|
||||
// Statistics
|
||||
academy.GET("/statistics", academyHandler.GetStatistics)
|
||||
|
||||
// Enrollments
|
||||
academy.GET("/enrollments", academyHandler.ListEnrollments)
|
||||
academy.POST("/enrollments", academyHandler.EnrollUser)
|
||||
academy.PUT("/enrollments/:id/progress", academyHandler.UpdateProgress)
|
||||
academy.POST("/enrollments/:id/complete", academyHandler.CompleteEnrollment)
|
||||
|
||||
// Quiz
|
||||
academy.POST("/lessons/:id/quiz", academyHandler.SubmitQuiz)
|
||||
|
||||
// Certificates
|
||||
academy.POST("/enrollments/:id/certificate", academyHandler.GenerateCertificateEndpoint)
|
||||
academy.GET("/certificates/:id", academyHandler.GetCertificate)
|
||||
academy.GET("/certificates/:id/pdf", academyHandler.DownloadCertificatePDF)
|
||||
|
||||
// AI Course Generation
|
||||
academy.POST("/courses/generate", academyHandler.GenerateCourse)
|
||||
academy.POST("/lessons/:id/regenerate", academyHandler.RegenerateLesson)
|
||||
|
||||
// Video Generation
|
||||
academy.POST("/courses/:id/generate-videos", academyHandler.GenerateVideos)
|
||||
academy.GET("/courses/:id/video-status", academyHandler.GetVideoStatus)
|
||||
}
|
||||
}
|
||||
|
||||
// Create server
|
||||
|
||||
@@ -1,11 +1,45 @@
|
||||
module github.com/breakpilot/ai-compliance-sdk
|
||||
|
||||
go 1.21
|
||||
go 1.23
|
||||
|
||||
require (
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
github.com/jackc/pgx/v5 v5.5.1
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/qdrant/go-client v1.7.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
github.com/jung-kurt/gofpdf v1.16.2
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic v1.11.6 // indirect
|
||||
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.20.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
golang.org/x/arch v0.8.0 // indirect
|
||||
golang.org/x/crypto v0.23.0 // indirect
|
||||
golang.org/x/net v0.25.0 // indirect
|
||||
golang.org/x/sync v0.1.0 // indirect
|
||||
golang.org/x/sys v0.26.0 // indirect
|
||||
golang.org/x/text v0.15.0 // indirect
|
||||
google.golang.org/protobuf v1.34.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
119
admin-v2/ai-compliance-sdk/go.sum
Normal file
119
admin-v2/ai-compliance-sdk/go.sum
Normal file
@@ -0,0 +1,119 @@
|
||||
github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
||||
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
||||
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
|
||||
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.5.1 h1:5I9etrGkLrN+2XPCsi6XLlV5DITbSL/xBZdmAxFcXPI=
|
||||
github.com/jackc/pgx/v5 v5.5.1/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
|
||||
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
||||
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
|
||||
github.com/jung-kurt/gofpdf v1.16.2 h1:jgbatWHfRlPYiK85qgevsZTHviWXKwB1TTiKdz5PtRc=
|
||||
github.com/jung-kurt/gofpdf v1.16.2/go.mod h1:1hl7y57EsiPAkLbOwzpzqgx1A30nQCk/YmFV8S2vmK0=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
github.com/phpdave11/gofpdi v1.0.7/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
||||
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
||||
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
152
admin-v2/ai-compliance-sdk/internal/academy/certificate_pdf.go
Normal file
152
admin-v2/ai-compliance-sdk/internal/academy/certificate_pdf.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package academy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/jung-kurt/gofpdf"
|
||||
)
|
||||
|
||||
// CertificateData holds all data needed to generate a certificate PDF
|
||||
type CertificateData struct {
|
||||
CertificateID string
|
||||
UserName string
|
||||
CourseName string
|
||||
CompanyName string
|
||||
Score int
|
||||
IssuedAt time.Time
|
||||
ValidUntil time.Time
|
||||
}
|
||||
|
||||
// GenerateCertificatePDF generates a PDF certificate and returns the bytes
|
||||
func GenerateCertificatePDF(data CertificateData) ([]byte, error) {
|
||||
pdf := gofpdf.New("L", "mm", "A4", "") // Landscape A4
|
||||
pdf.SetAutoPageBreak(false, 0)
|
||||
pdf.AddPage()
|
||||
|
||||
pageWidth, pageHeight := pdf.GetPageSize()
|
||||
|
||||
// Background color - light gray
|
||||
pdf.SetFillColor(250, 250, 252)
|
||||
pdf.Rect(0, 0, pageWidth, pageHeight, "F")
|
||||
|
||||
// Border - decorative
|
||||
pdf.SetDrawColor(79, 70, 229) // Purple/Indigo
|
||||
pdf.SetLineWidth(3)
|
||||
pdf.Rect(10, 10, pageWidth-20, pageHeight-20, "D")
|
||||
pdf.SetLineWidth(1)
|
||||
pdf.Rect(14, 14, pageWidth-28, pageHeight-28, "D")
|
||||
|
||||
// Header - Company/BreakPilot Logo area
|
||||
companyName := data.CompanyName
|
||||
if companyName == "" {
|
||||
companyName = "BreakPilot Compliance"
|
||||
}
|
||||
|
||||
pdf.SetFont("Helvetica", "", 12)
|
||||
pdf.SetTextColor(120, 120, 120)
|
||||
pdf.SetXY(0, 25)
|
||||
pdf.CellFormat(pageWidth, 10, companyName, "", 0, "C", false, 0, "")
|
||||
|
||||
// Title
|
||||
pdf.SetFont("Helvetica", "B", 32)
|
||||
pdf.SetTextColor(30, 30, 30)
|
||||
pdf.SetXY(0, 42)
|
||||
pdf.CellFormat(pageWidth, 15, "SCHULUNGSZERTIFIKAT", "", 0, "C", false, 0, "")
|
||||
|
||||
// Decorative line
|
||||
pdf.SetDrawColor(79, 70, 229)
|
||||
pdf.SetLineWidth(1.5)
|
||||
lineY := 62.0
|
||||
pdf.Line(pageWidth/2-60, lineY, pageWidth/2+60, lineY)
|
||||
|
||||
// "Hiermit wird bescheinigt, dass"
|
||||
pdf.SetFont("Helvetica", "", 13)
|
||||
pdf.SetTextColor(80, 80, 80)
|
||||
pdf.SetXY(0, 72)
|
||||
pdf.CellFormat(pageWidth, 8, "Hiermit wird bescheinigt, dass", "", 0, "C", false, 0, "")
|
||||
|
||||
// Name
|
||||
pdf.SetFont("Helvetica", "B", 26)
|
||||
pdf.SetTextColor(30, 30, 30)
|
||||
pdf.SetXY(0, 85)
|
||||
pdf.CellFormat(pageWidth, 12, data.UserName, "", 0, "C", false, 0, "")
|
||||
|
||||
// "die folgende Compliance-Schulung erfolgreich abgeschlossen hat:"
|
||||
pdf.SetFont("Helvetica", "", 13)
|
||||
pdf.SetTextColor(80, 80, 80)
|
||||
pdf.SetXY(0, 103)
|
||||
pdf.CellFormat(pageWidth, 8, "die folgende Compliance-Schulung erfolgreich abgeschlossen hat:", "", 0, "C", false, 0, "")
|
||||
|
||||
// Course Name
|
||||
pdf.SetFont("Helvetica", "B", 20)
|
||||
pdf.SetTextColor(79, 70, 229)
|
||||
pdf.SetXY(0, 116)
|
||||
pdf.CellFormat(pageWidth, 10, data.CourseName, "", 0, "C", false, 0, "")
|
||||
|
||||
// Score
|
||||
if data.Score > 0 {
|
||||
pdf.SetFont("Helvetica", "", 12)
|
||||
pdf.SetTextColor(80, 80, 80)
|
||||
pdf.SetXY(0, 130)
|
||||
pdf.CellFormat(pageWidth, 8, fmt.Sprintf("Testergebnis: %d%%", data.Score), "", 0, "C", false, 0, "")
|
||||
}
|
||||
|
||||
// Bottom section - Dates and Signature
|
||||
bottomY := 148.0
|
||||
|
||||
// Left: Issued Date
|
||||
pdf.SetFont("Helvetica", "", 10)
|
||||
pdf.SetTextColor(100, 100, 100)
|
||||
pdf.SetXY(40, bottomY)
|
||||
pdf.CellFormat(80, 6, fmt.Sprintf("Abschlussdatum: %s", data.IssuedAt.Format("02.01.2006")), "", 0, "L", false, 0, "")
|
||||
|
||||
// Center: Valid Until
|
||||
pdf.SetXY(pageWidth/2-40, bottomY)
|
||||
pdf.CellFormat(80, 6, fmt.Sprintf("Gueltig bis: %s", data.ValidUntil.Format("02.01.2006")), "", 0, "C", false, 0, "")
|
||||
|
||||
// Right: Certificate ID
|
||||
pdf.SetXY(pageWidth-120, bottomY)
|
||||
pdf.CellFormat(80, 6, fmt.Sprintf("Zertifikats-Nr.: %s", data.CertificateID[:min(12, len(data.CertificateID))]), "", 0, "R", false, 0, "")
|
||||
|
||||
// Signature line
|
||||
sigY := 162.0
|
||||
pdf.SetDrawColor(150, 150, 150)
|
||||
pdf.SetLineWidth(0.5)
|
||||
|
||||
// Left signature
|
||||
pdf.Line(50, sigY, 130, sigY)
|
||||
pdf.SetFont("Helvetica", "", 9)
|
||||
pdf.SetTextColor(120, 120, 120)
|
||||
pdf.SetXY(50, sigY+2)
|
||||
pdf.CellFormat(80, 5, "Datenschutzbeauftragter", "", 0, "C", false, 0, "")
|
||||
|
||||
// Right signature
|
||||
pdf.Line(pageWidth-130, sigY, pageWidth-50, sigY)
|
||||
pdf.SetXY(pageWidth-130, sigY+2)
|
||||
pdf.CellFormat(80, 5, "Geschaeftsfuehrung", "", 0, "C", false, 0, "")
|
||||
|
||||
// Footer
|
||||
pdf.SetFont("Helvetica", "", 8)
|
||||
pdf.SetTextColor(160, 160, 160)
|
||||
pdf.SetXY(0, pageHeight-22)
|
||||
pdf.CellFormat(pageWidth, 5, "Dieses Zertifikat wurde elektronisch erstellt und ist ohne Unterschrift gueltig.", "", 0, "C", false, 0, "")
|
||||
pdf.SetXY(0, pageHeight-17)
|
||||
pdf.CellFormat(pageWidth, 5, fmt.Sprintf("Verifizierung unter: https://compliance.breakpilot.de/verify/%s", data.CertificateID), "", 0, "C", false, 0, "")
|
||||
|
||||
// Generate PDF bytes
|
||||
var buf bytes.Buffer
|
||||
if err := pdf.Output(&buf); err != nil {
|
||||
return nil, fmt.Errorf("failed to generate PDF: %w", err)
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
105
admin-v2/ai-compliance-sdk/internal/academy/elevenlabs_client.go
Normal file
105
admin-v2/ai-compliance-sdk/internal/academy/elevenlabs_client.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package academy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ElevenLabsClient handles text-to-speech via the ElevenLabs API
|
||||
type ElevenLabsClient struct {
|
||||
apiKey string
|
||||
voiceID string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
// NewElevenLabsClient creates a new ElevenLabs client
|
||||
func NewElevenLabsClient() *ElevenLabsClient {
|
||||
apiKey := os.Getenv("ELEVENLABS_API_KEY")
|
||||
voiceID := os.Getenv("ELEVENLABS_VOICE_ID")
|
||||
if voiceID == "" {
|
||||
voiceID = "EXAVITQu4vr4xnSDxMaL" // Default: "Sarah" voice
|
||||
}
|
||||
|
||||
return &ElevenLabsClient{
|
||||
apiKey: apiKey,
|
||||
voiceID: voiceID,
|
||||
client: &http.Client{
|
||||
Timeout: 120 * time.Second,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// IsConfigured returns true if API key is set
|
||||
func (c *ElevenLabsClient) IsConfigured() bool {
|
||||
return c.apiKey != ""
|
||||
}
|
||||
|
||||
// TextToSpeechRequest represents the API request
|
||||
type TextToSpeechRequest struct {
|
||||
Text string `json:"text"`
|
||||
ModelID string `json:"model_id"`
|
||||
VoiceSettings VoiceSettings `json:"voice_settings"`
|
||||
}
|
||||
|
||||
// VoiceSettings controls voice parameters
|
||||
type VoiceSettings struct {
|
||||
Stability float64 `json:"stability"`
|
||||
SimilarityBoost float64 `json:"similarity_boost"`
|
||||
Style float64 `json:"style"`
|
||||
}
|
||||
|
||||
// TextToSpeech converts text to speech audio (MP3)
|
||||
func (c *ElevenLabsClient) TextToSpeech(text string) ([]byte, error) {
|
||||
if !c.IsConfigured() {
|
||||
return nil, fmt.Errorf("ElevenLabs API key not configured")
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("https://api.elevenlabs.io/v1/text-to-speech/%s", c.voiceID)
|
||||
|
||||
reqBody := TextToSpeechRequest{
|
||||
Text: text,
|
||||
ModelID: "eleven_multilingual_v2",
|
||||
VoiceSettings: VoiceSettings{
|
||||
Stability: 0.5,
|
||||
SimilarityBoost: 0.75,
|
||||
Style: 0.5,
|
||||
},
|
||||
}
|
||||
|
||||
jsonBody, err := json.Marshal(reqBody)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal request: %w", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", url, bytes.NewReader(jsonBody))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("xi-api-key", c.apiKey)
|
||||
req.Header.Set("Accept", "audio/mpeg")
|
||||
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ElevenLabs API request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("ElevenLabs API error %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
audioData, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read audio response: %w", err)
|
||||
}
|
||||
|
||||
return audioData, nil
|
||||
}
|
||||
184
admin-v2/ai-compliance-sdk/internal/academy/heygen_client.go
Normal file
184
admin-v2/ai-compliance-sdk/internal/academy/heygen_client.go
Normal file
@@ -0,0 +1,184 @@
|
||||
package academy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// HeyGenClient handles avatar video generation via the HeyGen API
|
||||
type HeyGenClient struct {
|
||||
apiKey string
|
||||
avatarID string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
// NewHeyGenClient creates a new HeyGen client
|
||||
func NewHeyGenClient() *HeyGenClient {
|
||||
apiKey := os.Getenv("HEYGEN_API_KEY")
|
||||
avatarID := os.Getenv("HEYGEN_AVATAR_ID")
|
||||
if avatarID == "" {
|
||||
avatarID = "josh_lite3_20230714" // Default avatar
|
||||
}
|
||||
|
||||
return &HeyGenClient{
|
||||
apiKey: apiKey,
|
||||
avatarID: avatarID,
|
||||
client: &http.Client{
|
||||
Timeout: 300 * time.Second, // Video generation can take time
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// IsConfigured returns true if API key is set
|
||||
func (c *HeyGenClient) IsConfigured() bool {
|
||||
return c.apiKey != ""
|
||||
}
|
||||
|
||||
// CreateVideoRequest represents the HeyGen API request
|
||||
type CreateVideoRequest struct {
|
||||
VideoInputs []VideoInput `json:"video_inputs"`
|
||||
Dimension Dimension `json:"dimension"`
|
||||
}
|
||||
|
||||
// VideoInput represents a single video segment
|
||||
type VideoInput struct {
|
||||
Character Character `json:"character"`
|
||||
Voice VideoVoice `json:"voice"`
|
||||
}
|
||||
|
||||
// Character represents the avatar
|
||||
type Character struct {
|
||||
Type string `json:"type"`
|
||||
AvatarID string `json:"avatar_id"`
|
||||
}
|
||||
|
||||
// VideoVoice represents the voice/audio source
|
||||
type VideoVoice struct {
|
||||
Type string `json:"type"` // "audio" for pre-generated audio
|
||||
AudioURL string `json:"audio_url,omitempty"`
|
||||
InputText string `json:"input_text,omitempty"`
|
||||
}
|
||||
|
||||
// Dimension represents video dimensions
|
||||
type Dimension struct {
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
}
|
||||
|
||||
// CreateVideoResponse represents the HeyGen API response
|
||||
type CreateVideoResponse struct {
|
||||
Data struct {
|
||||
VideoID string `json:"video_id"`
|
||||
} `json:"data"`
|
||||
Error interface{} `json:"error"`
|
||||
}
|
||||
|
||||
// HeyGenVideoStatus represents video status from HeyGen
|
||||
type HeyGenVideoStatus struct {
|
||||
Data struct {
|
||||
Status string `json:"status"` // processing, completed, failed
|
||||
VideoURL string `json:"video_url"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// CreateVideo creates a video with the avatar and audio
|
||||
func (c *HeyGenClient) CreateVideo(audioURL string) (*CreateVideoResponse, error) {
|
||||
if !c.IsConfigured() {
|
||||
return nil, fmt.Errorf("HeyGen API key not configured")
|
||||
}
|
||||
|
||||
url := "https://api.heygen.com/v2/video/generate"
|
||||
|
||||
reqBody := CreateVideoRequest{
|
||||
VideoInputs: []VideoInput{
|
||||
{
|
||||
Character: Character{
|
||||
Type: "avatar",
|
||||
AvatarID: c.avatarID,
|
||||
},
|
||||
Voice: VideoVoice{
|
||||
Type: "audio",
|
||||
AudioURL: audioURL,
|
||||
},
|
||||
},
|
||||
},
|
||||
Dimension: Dimension{
|
||||
Width: 1920,
|
||||
Height: 1080,
|
||||
},
|
||||
}
|
||||
|
||||
jsonBody, err := json.Marshal(reqBody)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal request: %w", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", url, bytes.NewReader(jsonBody))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-Api-Key", c.apiKey)
|
||||
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("HeyGen API request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read response: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
|
||||
return nil, fmt.Errorf("HeyGen API error %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
var result CreateVideoResponse
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse response: %w", err)
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetVideoStatus checks the status of a video generation job
|
||||
func (c *HeyGenClient) GetVideoStatus(videoID string) (*HeyGenVideoStatus, error) {
|
||||
if !c.IsConfigured() {
|
||||
return nil, fmt.Errorf("HeyGen API key not configured")
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("https://api.heygen.com/v1/video_status.get?video_id=%s", videoID)
|
||||
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("X-Api-Key", c.apiKey)
|
||||
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("HeyGen API request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read response: %w", err)
|
||||
}
|
||||
|
||||
var status HeyGenVideoStatus
|
||||
if err := json.Unmarshal(body, &status); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse response: %w", err)
|
||||
}
|
||||
|
||||
return &status, nil
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package academy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
)
|
||||
|
||||
// VideoGenerator orchestrates video generation with 3-tier fallback:
|
||||
// 1. HeyGen + ElevenLabs -> Avatar video with voice
|
||||
// 2. ElevenLabs only -> Audio podcast style
|
||||
// 3. No external services -> Text + Quiz only
|
||||
type VideoGenerator struct {
|
||||
elevenLabs *ElevenLabsClient
|
||||
heyGen *HeyGenClient
|
||||
}
|
||||
|
||||
// NewVideoGenerator creates a new video generator
|
||||
func NewVideoGenerator() *VideoGenerator {
|
||||
return &VideoGenerator{
|
||||
elevenLabs: NewElevenLabsClient(),
|
||||
heyGen: NewHeyGenClient(),
|
||||
}
|
||||
}
|
||||
|
||||
// GenerationMode describes the available generation mode
|
||||
type GenerationMode string
|
||||
|
||||
const (
|
||||
ModeAvatarVideo GenerationMode = "avatar_video" // HeyGen + ElevenLabs
|
||||
ModeAudioOnly GenerationMode = "audio_only" // ElevenLabs only
|
||||
ModeTextOnly GenerationMode = "text_only" // No external services
|
||||
)
|
||||
|
||||
// GetAvailableMode returns the best available generation mode
|
||||
func (vg *VideoGenerator) GetAvailableMode() GenerationMode {
|
||||
if vg.heyGen.IsConfigured() && vg.elevenLabs.IsConfigured() {
|
||||
return ModeAvatarVideo
|
||||
}
|
||||
if vg.elevenLabs.IsConfigured() {
|
||||
return ModeAudioOnly
|
||||
}
|
||||
return ModeTextOnly
|
||||
}
|
||||
|
||||
// GenerateAudio generates audio from text using ElevenLabs
|
||||
func (vg *VideoGenerator) GenerateAudio(text string) ([]byte, error) {
|
||||
if !vg.elevenLabs.IsConfigured() {
|
||||
return nil, fmt.Errorf("ElevenLabs not configured")
|
||||
}
|
||||
|
||||
log.Printf("Generating audio for text (%d chars)...", len(text))
|
||||
return vg.elevenLabs.TextToSpeech(text)
|
||||
}
|
||||
|
||||
// GenerateVideo generates a video from audio using HeyGen
|
||||
func (vg *VideoGenerator) GenerateVideo(audioURL string) (string, error) {
|
||||
if !vg.heyGen.IsConfigured() {
|
||||
return "", fmt.Errorf("HeyGen not configured")
|
||||
}
|
||||
|
||||
log.Printf("Creating HeyGen video with audio: %s", audioURL)
|
||||
resp, err := vg.heyGen.CreateVideo(audioURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return resp.Data.VideoID, nil
|
||||
}
|
||||
|
||||
// CheckVideoStatus checks if a HeyGen video is ready
|
||||
func (vg *VideoGenerator) CheckVideoStatus(videoID string) (string, string, error) {
|
||||
if !vg.heyGen.IsConfigured() {
|
||||
return "", "", fmt.Errorf("HeyGen not configured")
|
||||
}
|
||||
|
||||
status, err := vg.heyGen.GetVideoStatus(videoID)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
return status.Data.Status, status.Data.VideoURL, nil
|
||||
}
|
||||
|
||||
// GetStatus returns the configuration status
|
||||
func (vg *VideoGenerator) GetStatus() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"mode": string(vg.GetAvailableMode()),
|
||||
"elevenLabsConfigured": vg.elevenLabs.IsConfigured(),
|
||||
"heyGenConfigured": vg.heyGen.IsConfigured(),
|
||||
}
|
||||
}
|
||||
950
admin-v2/ai-compliance-sdk/internal/api/academy.go
Normal file
950
admin-v2/ai-compliance-sdk/internal/api/academy.go
Normal file
@@ -0,0 +1,950 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/breakpilot/ai-compliance-sdk/internal/academy"
|
||||
"github.com/breakpilot/ai-compliance-sdk/internal/db"
|
||||
"github.com/breakpilot/ai-compliance-sdk/internal/llm"
|
||||
"github.com/breakpilot/ai-compliance-sdk/internal/rag"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// AcademyHandler handles all Academy-related HTTP requests
|
||||
type AcademyHandler struct {
|
||||
dbPool *db.Pool
|
||||
llmService *llm.Service
|
||||
ragService *rag.Service
|
||||
academyStore *db.AcademyMemStore
|
||||
}
|
||||
|
||||
// NewAcademyHandler creates a new Academy handler
|
||||
func NewAcademyHandler(dbPool *db.Pool, llmService *llm.Service, ragService *rag.Service) *AcademyHandler {
|
||||
return &AcademyHandler{
|
||||
dbPool: dbPool,
|
||||
llmService: llmService,
|
||||
ragService: ragService,
|
||||
academyStore: db.NewAcademyMemStore(),
|
||||
}
|
||||
}
|
||||
|
||||
func (h *AcademyHandler) getTenantID(c *gin.Context) string {
|
||||
tid := c.GetHeader("X-Tenant-ID")
|
||||
if tid == "" {
|
||||
tid = c.Query("tenantId")
|
||||
}
|
||||
if tid == "" {
|
||||
tid = "default-tenant"
|
||||
}
|
||||
return tid
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Course CRUD
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// ListCourses returns all courses for the tenant
|
||||
func (h *AcademyHandler) ListCourses(c *gin.Context) {
|
||||
tenantID := h.getTenantID(c)
|
||||
rows := h.academyStore.ListCourses(tenantID)
|
||||
|
||||
courses := make([]AcademyCourse, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
lessons := h.buildLessonsForCourse(row.ID)
|
||||
courses = append(courses, courseRowToResponse(row, lessons))
|
||||
}
|
||||
|
||||
SuccessResponse(c, courses)
|
||||
}
|
||||
|
||||
// GetCourse returns a single course with its lessons
|
||||
func (h *AcademyHandler) GetCourse(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
row, err := h.academyStore.GetCourse(id)
|
||||
if err != nil {
|
||||
ErrorResponse(c, http.StatusNotFound, "Course not found", "COURSE_NOT_FOUND")
|
||||
return
|
||||
}
|
||||
|
||||
lessons := h.buildLessonsForCourse(row.ID)
|
||||
SuccessResponse(c, courseRowToResponse(row, lessons))
|
||||
}
|
||||
|
||||
// CreateCourse creates a new course with optional lessons
|
||||
func (h *AcademyHandler) CreateCourse(c *gin.Context) {
|
||||
var req CreateCourseRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
ErrorResponse(c, http.StatusBadRequest, err.Error(), "INVALID_REQUEST")
|
||||
return
|
||||
}
|
||||
|
||||
passingScore := req.PassingScore
|
||||
if passingScore == 0 {
|
||||
passingScore = 70
|
||||
}
|
||||
|
||||
roles := req.RequiredForRoles
|
||||
if len(roles) == 0 {
|
||||
roles = []string{"all"}
|
||||
}
|
||||
|
||||
courseRow := h.academyStore.CreateCourse(&db.AcademyCourseRow{
|
||||
TenantID: req.TenantID,
|
||||
Title: req.Title,
|
||||
Description: req.Description,
|
||||
Category: req.Category,
|
||||
PassingScore: passingScore,
|
||||
DurationMinutes: req.DurationMinutes,
|
||||
RequiredForRoles: roles,
|
||||
Status: "draft",
|
||||
})
|
||||
|
||||
// Create lessons
|
||||
for i, lessonReq := range req.Lessons {
|
||||
order := lessonReq.Order
|
||||
if order == 0 {
|
||||
order = i + 1
|
||||
}
|
||||
lessonRow := h.academyStore.CreateLesson(&db.AcademyLessonRow{
|
||||
CourseID: courseRow.ID,
|
||||
Title: lessonReq.Title,
|
||||
Type: lessonReq.Type,
|
||||
ContentMarkdown: lessonReq.ContentMarkdown,
|
||||
VideoURL: lessonReq.VideoURL,
|
||||
SortOrder: order,
|
||||
DurationMinutes: lessonReq.DurationMinutes,
|
||||
})
|
||||
|
||||
// Create quiz questions for this lesson
|
||||
for j, qReq := range lessonReq.QuizQuestions {
|
||||
qOrder := qReq.Order
|
||||
if qOrder == 0 {
|
||||
qOrder = j + 1
|
||||
}
|
||||
h.academyStore.CreateQuizQuestion(&db.AcademyQuizQuestionRow{
|
||||
LessonID: lessonRow.ID,
|
||||
Question: qReq.Question,
|
||||
Options: qReq.Options,
|
||||
CorrectOptionIndex: qReq.CorrectOptionIndex,
|
||||
Explanation: qReq.Explanation,
|
||||
SortOrder: qOrder,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
lessons := h.buildLessonsForCourse(courseRow.ID)
|
||||
c.JSON(http.StatusCreated, Response{
|
||||
Success: true,
|
||||
Data: courseRowToResponse(courseRow, lessons),
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateCourse updates an existing course
|
||||
func (h *AcademyHandler) UpdateCourse(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
var req UpdateCourseRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
ErrorResponse(c, http.StatusBadRequest, err.Error(), "INVALID_REQUEST")
|
||||
return
|
||||
}
|
||||
|
||||
updates := make(map[string]interface{})
|
||||
if req.Title != nil {
|
||||
updates["title"] = *req.Title
|
||||
}
|
||||
if req.Description != nil {
|
||||
updates["description"] = *req.Description
|
||||
}
|
||||
if req.Category != nil {
|
||||
updates["category"] = *req.Category
|
||||
}
|
||||
if req.DurationMinutes != nil {
|
||||
updates["durationminutes"] = *req.DurationMinutes
|
||||
}
|
||||
if req.PassingScore != nil {
|
||||
updates["passingscore"] = *req.PassingScore
|
||||
}
|
||||
if req.RequiredForRoles != nil {
|
||||
updates["requiredforroles"] = req.RequiredForRoles
|
||||
}
|
||||
|
||||
row, err := h.academyStore.UpdateCourse(id, updates)
|
||||
if err != nil {
|
||||
ErrorResponse(c, http.StatusNotFound, "Course not found", "COURSE_NOT_FOUND")
|
||||
return
|
||||
}
|
||||
|
||||
lessons := h.buildLessonsForCourse(row.ID)
|
||||
SuccessResponse(c, courseRowToResponse(row, lessons))
|
||||
}
|
||||
|
||||
// DeleteCourse deletes a course and all related data
|
||||
func (h *AcademyHandler) DeleteCourse(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
if err := h.academyStore.DeleteCourse(id); err != nil {
|
||||
ErrorResponse(c, http.StatusNotFound, "Course not found", "COURSE_NOT_FOUND")
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(c, gin.H{
|
||||
"courseId": id,
|
||||
"deletedAt": now(),
|
||||
})
|
||||
}
|
||||
|
||||
// GetStatistics returns academy statistics for the tenant
|
||||
func (h *AcademyHandler) GetStatistics(c *gin.Context) {
|
||||
tenantID := h.getTenantID(c)
|
||||
stats := h.academyStore.GetStatistics(tenantID)
|
||||
|
||||
SuccessResponse(c, AcademyStatistics{
|
||||
TotalCourses: stats.TotalCourses,
|
||||
TotalEnrollments: stats.TotalEnrollments,
|
||||
CompletionRate: int(stats.CompletionRate),
|
||||
OverdueCount: stats.OverdueCount,
|
||||
ByCategory: stats.ByCategory,
|
||||
ByStatus: stats.ByStatus,
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Enrollments
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// ListEnrollments returns enrollments filtered by tenant and optionally course
|
||||
func (h *AcademyHandler) ListEnrollments(c *gin.Context) {
|
||||
tenantID := h.getTenantID(c)
|
||||
courseID := c.Query("courseId")
|
||||
|
||||
rows := h.academyStore.ListEnrollments(tenantID, courseID)
|
||||
|
||||
enrollments := make([]AcademyEnrollment, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
enrollments = append(enrollments, enrollmentRowToResponse(row))
|
||||
}
|
||||
|
||||
SuccessResponse(c, enrollments)
|
||||
}
|
||||
|
||||
// EnrollUser enrolls a user in a course
|
||||
func (h *AcademyHandler) EnrollUser(c *gin.Context) {
|
||||
var req EnrollUserRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
ErrorResponse(c, http.StatusBadRequest, err.Error(), "INVALID_REQUEST")
|
||||
return
|
||||
}
|
||||
|
||||
deadline, err := time.Parse(time.RFC3339, req.Deadline)
|
||||
if err != nil {
|
||||
deadline, err = time.Parse("2006-01-02", req.Deadline)
|
||||
if err != nil {
|
||||
ErrorResponse(c, http.StatusBadRequest, "Invalid deadline format. Use RFC3339 or YYYY-MM-DD.", "INVALID_DEADLINE")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
row := h.academyStore.CreateEnrollment(&db.AcademyEnrollmentRow{
|
||||
TenantID: req.TenantID,
|
||||
CourseID: req.CourseID,
|
||||
UserID: req.UserID,
|
||||
UserName: req.UserName,
|
||||
UserEmail: req.UserEmail,
|
||||
Status: "not_started",
|
||||
Progress: 0,
|
||||
Deadline: deadline,
|
||||
})
|
||||
|
||||
c.JSON(http.StatusCreated, Response{
|
||||
Success: true,
|
||||
Data: enrollmentRowToResponse(row),
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateProgress updates the progress of an enrollment
|
||||
func (h *AcademyHandler) UpdateProgress(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
var req UpdateProgressRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
ErrorResponse(c, http.StatusBadRequest, err.Error(), "INVALID_REQUEST")
|
||||
return
|
||||
}
|
||||
|
||||
enrollment, err := h.academyStore.GetEnrollment(id)
|
||||
if err != nil {
|
||||
ErrorResponse(c, http.StatusNotFound, "Enrollment not found", "ENROLLMENT_NOT_FOUND")
|
||||
return
|
||||
}
|
||||
|
||||
updates := map[string]interface{}{
|
||||
"progress": req.Progress,
|
||||
}
|
||||
|
||||
// Auto-update status based on progress
|
||||
if req.Progress >= 100 {
|
||||
updates["status"] = "completed"
|
||||
t := time.Now()
|
||||
updates["completedat"] = &t
|
||||
} else if req.Progress > 0 && enrollment.Status == "not_started" {
|
||||
updates["status"] = "in_progress"
|
||||
}
|
||||
|
||||
row, err := h.academyStore.UpdateEnrollment(id, updates)
|
||||
if err != nil {
|
||||
ErrorResponse(c, http.StatusInternalServerError, "Failed to update progress", "UPDATE_FAILED")
|
||||
return
|
||||
}
|
||||
|
||||
// Upsert lesson progress if lessonID provided
|
||||
if req.LessonID != "" {
|
||||
t := time.Now()
|
||||
h.academyStore.UpsertLessonProgress(&db.AcademyLessonProgressRow{
|
||||
EnrollmentID: id,
|
||||
LessonID: req.LessonID,
|
||||
Completed: true,
|
||||
CompletedAt: &t,
|
||||
})
|
||||
}
|
||||
|
||||
SuccessResponse(c, enrollmentRowToResponse(row))
|
||||
}
|
||||
|
||||
// CompleteEnrollment marks an enrollment as completed
|
||||
func (h *AcademyHandler) CompleteEnrollment(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
t := time.Now()
|
||||
updates := map[string]interface{}{
|
||||
"status": "completed",
|
||||
"progress": 100,
|
||||
"completedat": &t,
|
||||
}
|
||||
|
||||
row, err := h.academyStore.UpdateEnrollment(id, updates)
|
||||
if err != nil {
|
||||
ErrorResponse(c, http.StatusNotFound, "Enrollment not found", "ENROLLMENT_NOT_FOUND")
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(c, enrollmentRowToResponse(row))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Quiz
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// SubmitQuiz evaluates quiz answers for a lesson
|
||||
func (h *AcademyHandler) SubmitQuiz(c *gin.Context) {
|
||||
lessonID := c.Param("id")
|
||||
|
||||
var req SubmitQuizRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
ErrorResponse(c, http.StatusBadRequest, err.Error(), "INVALID_REQUEST")
|
||||
return
|
||||
}
|
||||
|
||||
// Get the lesson
|
||||
lesson, err := h.academyStore.GetLesson(lessonID)
|
||||
if err != nil {
|
||||
ErrorResponse(c, http.StatusNotFound, "Lesson not found", "LESSON_NOT_FOUND")
|
||||
return
|
||||
}
|
||||
|
||||
// Get quiz questions
|
||||
questions := h.academyStore.ListQuizQuestions(lessonID)
|
||||
if len(questions) == 0 {
|
||||
ErrorResponse(c, http.StatusBadRequest, "No quiz questions found for this lesson", "NO_QUIZ_QUESTIONS")
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.Answers) != len(questions) {
|
||||
ErrorResponse(c, http.StatusBadRequest,
|
||||
fmt.Sprintf("Expected %d answers, got %d", len(questions), len(req.Answers)),
|
||||
"ANSWER_COUNT_MISMATCH")
|
||||
return
|
||||
}
|
||||
|
||||
// Evaluate answers
|
||||
correctCount := 0
|
||||
results := make([]QuizQuestionResult, len(questions))
|
||||
for i, q := range questions {
|
||||
correct := req.Answers[i] == q.CorrectOptionIndex
|
||||
if correct {
|
||||
correctCount++
|
||||
}
|
||||
results[i] = QuizQuestionResult{
|
||||
QuestionID: q.ID,
|
||||
Correct: correct,
|
||||
Explanation: q.Explanation,
|
||||
}
|
||||
}
|
||||
|
||||
score := 0
|
||||
if len(questions) > 0 {
|
||||
score = int(float64(correctCount) / float64(len(questions)) * 100)
|
||||
}
|
||||
|
||||
// Determine pass/fail based on course's passing score
|
||||
passingScore := 70 // default
|
||||
course, err := h.academyStore.GetCourse(lesson.CourseID)
|
||||
if err == nil && course.PassingScore > 0 {
|
||||
passingScore = course.PassingScore
|
||||
}
|
||||
|
||||
SuccessResponse(c, SubmitQuizResponse{
|
||||
Score: score,
|
||||
Passed: score >= passingScore,
|
||||
CorrectAnswers: correctCount,
|
||||
TotalQuestions: len(questions),
|
||||
Results: results,
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Certificates
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// GenerateCertificateEndpoint generates a certificate for a completed enrollment
|
||||
func (h *AcademyHandler) GenerateCertificateEndpoint(c *gin.Context) {
|
||||
enrollmentID := c.Param("id")
|
||||
|
||||
enrollment, err := h.academyStore.GetEnrollment(enrollmentID)
|
||||
if err != nil {
|
||||
ErrorResponse(c, http.StatusNotFound, "Enrollment not found", "ENROLLMENT_NOT_FOUND")
|
||||
return
|
||||
}
|
||||
|
||||
// Check if already has certificate
|
||||
if enrollment.CertificateID != "" {
|
||||
existing, err := h.academyStore.GetCertificate(enrollment.CertificateID)
|
||||
if err == nil {
|
||||
SuccessResponse(c, certificateRowToResponse(existing))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Get course name
|
||||
courseName := "Unbekannter Kurs"
|
||||
course, err := h.academyStore.GetCourse(enrollment.CourseID)
|
||||
if err == nil {
|
||||
courseName = course.Title
|
||||
}
|
||||
|
||||
issuedAt := time.Now()
|
||||
validUntil := issuedAt.AddDate(1, 0, 0) // 1 year validity
|
||||
|
||||
cert := h.academyStore.CreateCertificate(&db.AcademyCertificateRow{
|
||||
TenantID: enrollment.TenantID,
|
||||
EnrollmentID: enrollmentID,
|
||||
CourseID: enrollment.CourseID,
|
||||
UserID: enrollment.UserID,
|
||||
UserName: enrollment.UserName,
|
||||
CourseName: courseName,
|
||||
Score: enrollment.Progress,
|
||||
IssuedAt: issuedAt,
|
||||
ValidUntil: validUntil,
|
||||
})
|
||||
|
||||
// Update enrollment with certificate ID
|
||||
h.academyStore.UpdateEnrollment(enrollmentID, map[string]interface{}{
|
||||
"certificateid": cert.ID,
|
||||
})
|
||||
|
||||
c.JSON(http.StatusCreated, Response{
|
||||
Success: true,
|
||||
Data: certificateRowToResponse(cert),
|
||||
})
|
||||
}
|
||||
|
||||
// GetCertificate returns a certificate by ID
|
||||
func (h *AcademyHandler) GetCertificate(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
cert, err := h.academyStore.GetCertificate(id)
|
||||
if err != nil {
|
||||
ErrorResponse(c, http.StatusNotFound, "Certificate not found", "CERTIFICATE_NOT_FOUND")
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(c, certificateRowToResponse(cert))
|
||||
}
|
||||
|
||||
// DownloadCertificatePDF returns the PDF for a certificate
|
||||
func (h *AcademyHandler) DownloadCertificatePDF(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
cert, err := h.academyStore.GetCertificate(id)
|
||||
if err != nil {
|
||||
ErrorResponse(c, http.StatusNotFound, "Certificate not found", "CERTIFICATE_NOT_FOUND")
|
||||
return
|
||||
}
|
||||
|
||||
if cert.PdfURL != "" {
|
||||
c.Redirect(http.StatusFound, cert.PdfURL)
|
||||
return
|
||||
}
|
||||
|
||||
// Generate PDF on-the-fly
|
||||
pdfBytes, err := academy.GenerateCertificatePDF(academy.CertificateData{
|
||||
CertificateID: cert.ID,
|
||||
UserName: cert.UserName,
|
||||
CourseName: cert.CourseName,
|
||||
CompanyName: "",
|
||||
Score: cert.Score,
|
||||
IssuedAt: cert.IssuedAt,
|
||||
ValidUntil: cert.ValidUntil,
|
||||
})
|
||||
if err != nil {
|
||||
ErrorResponse(c, http.StatusInternalServerError, "Failed to generate PDF", "PDF_GENERATION_FAILED")
|
||||
return
|
||||
}
|
||||
|
||||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=zertifikat-%s.pdf", cert.ID[:min(8, len(cert.ID))]))
|
||||
c.Data(http.StatusOK, "application/pdf", pdfBytes)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// AI Course Generation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// GenerateCourse generates a course using AI
|
||||
func (h *AcademyHandler) GenerateCourse(c *gin.Context) {
|
||||
var req GenerateCourseRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
ErrorResponse(c, http.StatusBadRequest, err.Error(), "INVALID_REQUEST")
|
||||
return
|
||||
}
|
||||
|
||||
// Get RAG context if requested
|
||||
var ragSources []SearchResult
|
||||
if req.UseRAG && h.ragService != nil {
|
||||
query := req.RAGQuery
|
||||
if query == "" {
|
||||
query = req.Topic + " Compliance Schulung"
|
||||
}
|
||||
results, _ := h.ragService.Search(c.Request.Context(), query, 5, "legal_corpus", "")
|
||||
for _, r := range results {
|
||||
ragSources = append(ragSources, SearchResult{
|
||||
ID: r.ID,
|
||||
Content: r.Content,
|
||||
Source: r.Source,
|
||||
Score: r.Score,
|
||||
Metadata: r.Metadata,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Generate course content (mock for now)
|
||||
course := h.generateMockCourse(req)
|
||||
|
||||
// Save to store
|
||||
courseRow := h.academyStore.CreateCourse(&db.AcademyCourseRow{
|
||||
TenantID: req.TenantID,
|
||||
Title: course.Title,
|
||||
Description: course.Description,
|
||||
Category: req.Category,
|
||||
PassingScore: 70,
|
||||
DurationMinutes: course.DurationMinutes,
|
||||
RequiredForRoles: []string{"all"},
|
||||
Status: "draft",
|
||||
})
|
||||
|
||||
for _, lesson := range course.Lessons {
|
||||
lessonRow := h.academyStore.CreateLesson(&db.AcademyLessonRow{
|
||||
CourseID: courseRow.ID,
|
||||
Title: lesson.Title,
|
||||
Type: lesson.Type,
|
||||
ContentMarkdown: lesson.ContentMarkdown,
|
||||
SortOrder: lesson.Order,
|
||||
DurationMinutes: lesson.DurationMinutes,
|
||||
})
|
||||
|
||||
for _, q := range lesson.QuizQuestions {
|
||||
h.academyStore.CreateQuizQuestion(&db.AcademyQuizQuestionRow{
|
||||
LessonID: lessonRow.ID,
|
||||
Question: q.Question,
|
||||
Options: q.Options,
|
||||
CorrectOptionIndex: q.CorrectOptionIndex,
|
||||
Explanation: q.Explanation,
|
||||
SortOrder: q.Order,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
lessons := h.buildLessonsForCourse(courseRow.ID)
|
||||
c.JSON(http.StatusCreated, Response{
|
||||
Success: true,
|
||||
Data: gin.H{
|
||||
"course": courseRowToResponse(courseRow, lessons),
|
||||
"ragSources": ragSources,
|
||||
"model": h.llmService.GetModel(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// RegenerateLesson regenerates a single lesson using AI
|
||||
func (h *AcademyHandler) RegenerateLesson(c *gin.Context) {
|
||||
lessonID := c.Param("id")
|
||||
|
||||
_, err := h.academyStore.GetLesson(lessonID)
|
||||
if err != nil {
|
||||
ErrorResponse(c, http.StatusNotFound, "Lesson not found", "LESSON_NOT_FOUND")
|
||||
return
|
||||
}
|
||||
|
||||
// For now, return the existing lesson
|
||||
SuccessResponse(c, gin.H{
|
||||
"lessonId": lessonID,
|
||||
"status": "regeneration_pending",
|
||||
"message": "AI lesson regeneration will be available in a future version",
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Video Generation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// GenerateVideos initiates video generation for all lessons in a course
|
||||
func (h *AcademyHandler) GenerateVideos(c *gin.Context) {
|
||||
courseID := c.Param("id")
|
||||
|
||||
_, err := h.academyStore.GetCourse(courseID)
|
||||
if err != nil {
|
||||
ErrorResponse(c, http.StatusNotFound, "Course not found", "COURSE_NOT_FOUND")
|
||||
return
|
||||
}
|
||||
|
||||
lessons := h.academyStore.ListLessons(courseID)
|
||||
lessonStatuses := make([]LessonVideoStatus, 0, len(lessons))
|
||||
for _, l := range lessons {
|
||||
if l.Type == "text" || l.Type == "video" {
|
||||
lessonStatuses = append(lessonStatuses, LessonVideoStatus{
|
||||
LessonID: l.ID,
|
||||
Status: "pending",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
SuccessResponse(c, VideoStatusResponse{
|
||||
CourseID: courseID,
|
||||
Status: "pending",
|
||||
Lessons: lessonStatuses,
|
||||
})
|
||||
}
|
||||
|
||||
// GetVideoStatus returns the video generation status for a course
|
||||
func (h *AcademyHandler) GetVideoStatus(c *gin.Context) {
|
||||
courseID := c.Param("id")
|
||||
|
||||
_, err := h.academyStore.GetCourse(courseID)
|
||||
if err != nil {
|
||||
ErrorResponse(c, http.StatusNotFound, "Course not found", "COURSE_NOT_FOUND")
|
||||
return
|
||||
}
|
||||
|
||||
lessons := h.academyStore.ListLessons(courseID)
|
||||
lessonStatuses := make([]LessonVideoStatus, 0, len(lessons))
|
||||
for _, l := range lessons {
|
||||
status := LessonVideoStatus{
|
||||
LessonID: l.ID,
|
||||
Status: "not_started",
|
||||
VideoURL: l.VideoURL,
|
||||
AudioURL: l.AudioURL,
|
||||
}
|
||||
if l.VideoURL != "" {
|
||||
status.Status = "completed"
|
||||
}
|
||||
lessonStatuses = append(lessonStatuses, status)
|
||||
}
|
||||
|
||||
overallStatus := "not_started"
|
||||
hasCompleted := false
|
||||
hasPending := false
|
||||
for _, s := range lessonStatuses {
|
||||
if s.Status == "completed" {
|
||||
hasCompleted = true
|
||||
} else {
|
||||
hasPending = true
|
||||
}
|
||||
}
|
||||
if hasCompleted && !hasPending {
|
||||
overallStatus = "completed"
|
||||
} else if hasCompleted && hasPending {
|
||||
overallStatus = "processing"
|
||||
}
|
||||
|
||||
SuccessResponse(c, VideoStatusResponse{
|
||||
CourseID: courseID,
|
||||
Status: overallStatus,
|
||||
Lessons: lessonStatuses,
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func (h *AcademyHandler) buildLessonsForCourse(courseID string) []AcademyLesson {
|
||||
lessonRows := h.academyStore.ListLessons(courseID)
|
||||
lessons := make([]AcademyLesson, 0, len(lessonRows))
|
||||
for _, lr := range lessonRows {
|
||||
var questions []AcademyQuizQuestion
|
||||
if lr.Type == "quiz" {
|
||||
qRows := h.academyStore.ListQuizQuestions(lr.ID)
|
||||
questions = make([]AcademyQuizQuestion, 0, len(qRows))
|
||||
for _, qr := range qRows {
|
||||
questions = append(questions, quizQuestionRowToResponse(qr))
|
||||
}
|
||||
}
|
||||
lessons = append(lessons, lessonRowToResponse(lr, questions))
|
||||
}
|
||||
return lessons
|
||||
}
|
||||
|
||||
func courseRowToResponse(row *db.AcademyCourseRow, lessons []AcademyLesson) AcademyCourse {
|
||||
return AcademyCourse{
|
||||
ID: row.ID,
|
||||
TenantID: row.TenantID,
|
||||
Title: row.Title,
|
||||
Description: row.Description,
|
||||
Category: row.Category,
|
||||
PassingScore: row.PassingScore,
|
||||
DurationMinutes: row.DurationMinutes,
|
||||
RequiredForRoles: row.RequiredForRoles,
|
||||
Status: row.Status,
|
||||
Lessons: lessons,
|
||||
CreatedAt: row.CreatedAt.Format(time.RFC3339),
|
||||
UpdatedAt: row.UpdatedAt.Format(time.RFC3339),
|
||||
}
|
||||
}
|
||||
|
||||
func lessonRowToResponse(row *db.AcademyLessonRow, questions []AcademyQuizQuestion) AcademyLesson {
|
||||
return AcademyLesson{
|
||||
ID: row.ID,
|
||||
CourseID: row.CourseID,
|
||||
Title: row.Title,
|
||||
Type: row.Type,
|
||||
ContentMarkdown: row.ContentMarkdown,
|
||||
VideoURL: row.VideoURL,
|
||||
AudioURL: row.AudioURL,
|
||||
Order: row.SortOrder,
|
||||
DurationMinutes: row.DurationMinutes,
|
||||
QuizQuestions: questions,
|
||||
}
|
||||
}
|
||||
|
||||
func quizQuestionRowToResponse(row *db.AcademyQuizQuestionRow) AcademyQuizQuestion {
|
||||
return AcademyQuizQuestion{
|
||||
ID: row.ID,
|
||||
LessonID: row.LessonID,
|
||||
Question: row.Question,
|
||||
Options: row.Options,
|
||||
CorrectOptionIndex: row.CorrectOptionIndex,
|
||||
Explanation: row.Explanation,
|
||||
Order: row.SortOrder,
|
||||
}
|
||||
}
|
||||
|
||||
func enrollmentRowToResponse(row *db.AcademyEnrollmentRow) AcademyEnrollment {
|
||||
e := AcademyEnrollment{
|
||||
ID: row.ID,
|
||||
TenantID: row.TenantID,
|
||||
CourseID: row.CourseID,
|
||||
UserID: row.UserID,
|
||||
UserName: row.UserName,
|
||||
UserEmail: row.UserEmail,
|
||||
Status: row.Status,
|
||||
Progress: row.Progress,
|
||||
StartedAt: row.StartedAt.Format(time.RFC3339),
|
||||
CertificateID: row.CertificateID,
|
||||
Deadline: row.Deadline.Format(time.RFC3339),
|
||||
CreatedAt: row.CreatedAt.Format(time.RFC3339),
|
||||
UpdatedAt: row.UpdatedAt.Format(time.RFC3339),
|
||||
}
|
||||
if row.CompletedAt != nil {
|
||||
e.CompletedAt = row.CompletedAt.Format(time.RFC3339)
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
func certificateRowToResponse(row *db.AcademyCertificateRow) AcademyCertificate {
|
||||
return AcademyCertificate{
|
||||
ID: row.ID,
|
||||
TenantID: row.TenantID,
|
||||
EnrollmentID: row.EnrollmentID,
|
||||
CourseID: row.CourseID,
|
||||
UserID: row.UserID,
|
||||
UserName: row.UserName,
|
||||
CourseName: row.CourseName,
|
||||
Score: row.Score,
|
||||
IssuedAt: row.IssuedAt.Format(time.RFC3339),
|
||||
ValidUntil: row.ValidUntil.Format(time.RFC3339),
|
||||
PdfURL: row.PdfURL,
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mock Course Generator (used when LLM is not available)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func (h *AcademyHandler) generateMockCourse(req GenerateCourseRequest) AcademyCourse {
|
||||
switch req.Category {
|
||||
case "dsgvo_basics":
|
||||
return h.mockDSGVOCourse(req)
|
||||
case "it_security":
|
||||
return h.mockITSecurityCourse(req)
|
||||
case "ai_literacy":
|
||||
return h.mockAILiteracyCourse(req)
|
||||
case "whistleblower_protection":
|
||||
return h.mockWhistleblowerCourse(req)
|
||||
default:
|
||||
return h.mockDSGVOCourse(req)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *AcademyHandler) mockDSGVOCourse(req GenerateCourseRequest) AcademyCourse {
|
||||
return AcademyCourse{
|
||||
Title: "DSGVO-Grundlagen fuer Mitarbeiter",
|
||||
Description: "Umfassende Einfuehrung in die Datenschutz-Grundverordnung. Vermittelt die wichtigsten Grundsaetze des Datenschutzes, Betroffenenrechte und die korrekte Handhabung personenbezogener Daten.",
|
||||
DurationMinutes: 90,
|
||||
Lessons: []AcademyLesson{
|
||||
{
|
||||
Title: "Was ist die DSGVO?",
|
||||
Type: "text",
|
||||
Order: 1,
|
||||
DurationMinutes: 15,
|
||||
ContentMarkdown: "# Was ist die DSGVO?\n\nDie Datenschutz-Grundverordnung (DSGVO) ist eine Verordnung der EU, die seit dem 25. Mai 2018 gilt. Sie schuetzt die Grundrechte natuerlicher Personen bei der Verarbeitung personenbezogener Daten.\n\n## Warum ist die DSGVO wichtig?\n\n- **Einheitlicher Datenschutz** in der gesamten EU\n- **Hohe Bussgelder** bei Verstoessen (bis 20 Mio. EUR oder 4% des Jahresumsatzes)\n- **Staerkung der Betroffenenrechte** (Auskunft, Loeschung, Widerspruch)\n\n## Zentrale Begriffe\n\n- **Personenbezogene Daten**: Alle Informationen, die sich auf eine identifizierte oder identifizierbare Person beziehen\n- **Verantwortlicher**: Die Stelle, die ueber Zweck und Mittel der Verarbeitung entscheidet\n- **Auftragsverarbeiter**: Verarbeitet Daten im Auftrag des Verantwortlichen",
|
||||
},
|
||||
{
|
||||
Title: "Die 7 Grundsaetze der DSGVO",
|
||||
Type: "text",
|
||||
Order: 2,
|
||||
DurationMinutes: 20,
|
||||
ContentMarkdown: "# Die 7 Grundsaetze der DSGVO (Art. 5)\n\n## 1. Rechtmaessigkeit, Verarbeitung nach Treu und Glauben, Transparenz\nPersonenbezogene Daten muessen auf rechtmaessige Weise verarbeitet werden.\n\n## 2. Zweckbindung\nDaten duerfen nur fuer festgelegte, eindeutige und legitime Zwecke erhoben werden.\n\n## 3. Datenminimierung\nEs duerfen nur Daten erhoben werden, die fuer den Zweck erforderlich sind.\n\n## 4. Richtigkeit\nDaten muessen sachlich richtig und auf dem neuesten Stand sein.\n\n## 5. Speicherbegrenzung\nDaten duerfen nur so lange gespeichert werden, wie es fuer den Zweck erforderlich ist.\n\n## 6. Integritaet und Vertraulichkeit\nDaten muessen vor unbefugtem Zugriff geschuetzt werden.\n\n## 7. Rechenschaftspflicht\nDer Verantwortliche muss die Einhaltung der Grundsaetze nachweisen koennen.",
|
||||
},
|
||||
{
|
||||
Title: "Betroffenenrechte (Art. 15-22 DSGVO)",
|
||||
Type: "text",
|
||||
Order: 3,
|
||||
DurationMinutes: 20,
|
||||
ContentMarkdown: "# Betroffenenrechte\n\n## Recht auf Auskunft (Art. 15)\nJede Person hat das Recht zu erfahren, ob und welche Daten ueber sie verarbeitet werden.\n\n## Recht auf Berichtigung (Art. 16)\nUnrichtige Daten muessen berichtigt werden.\n\n## Recht auf Loeschung (Art. 17)\nDas 'Recht auf Vergessenwerden' ermoeglicht die Loeschung personenbezogener Daten.\n\n## Recht auf Einschraenkung (Art. 18)\nBetroffene koennen die Verarbeitung einschraenken lassen.\n\n## Recht auf Datenuebertragbarkeit (Art. 20)\nDaten muessen in einem maschinenlesbaren Format bereitgestellt werden.\n\n## Widerspruchsrecht (Art. 21)\nBetroffene koennen der Verarbeitung widersprechen.",
|
||||
},
|
||||
{
|
||||
Title: "Datenschutz im Arbeitsalltag",
|
||||
Type: "text",
|
||||
Order: 4,
|
||||
DurationMinutes: 15,
|
||||
ContentMarkdown: "# Datenschutz im Arbeitsalltag\n\n## E-Mails\n- Keine personenbezogenen Daten unverschluesselt versenden\n- BCC statt CC bei Massenversand\n- Vorsicht bei Anhangen\n\n## Bildschirmsperre\n- Computer bei Abwesenheit sperren (Win+L / Cmd+Ctrl+Q)\n- Automatische Sperre nach 5 Minuten\n\n## Clean Desk Policy\n- Keine sensiblen Dokumente offen liegen lassen\n- Aktenvernichter fuer Papierdokumente\n\n## Homeoffice\n- VPN nutzen\n- Kein oeffentliches WLAN fuer Firmendaten\n- Bildschirm vor Mitlesern schuetzen\n\n## Datenpannen melden\n- **Sofort** den Datenschutzbeauftragten informieren\n- Innerhalb von 72 Stunden an die Aufsichtsbehoerde\n- Dokumentation der Panne",
|
||||
},
|
||||
{
|
||||
Title: "Wissenstest: DSGVO-Grundlagen",
|
||||
Type: "quiz",
|
||||
Order: 5,
|
||||
DurationMinutes: 20,
|
||||
QuizQuestions: []AcademyQuizQuestion{
|
||||
{
|
||||
Question: "Seit wann gilt die DSGVO?",
|
||||
Options: []string{"1. Januar 2016", "25. Mai 2018", "1. Januar 2020", "25. Mai 2020"},
|
||||
CorrectOptionIndex: 1,
|
||||
Explanation: "Die DSGVO gilt seit dem 25. Mai 2018 in allen EU-Mitgliedstaaten.",
|
||||
Order: 1,
|
||||
},
|
||||
{
|
||||
Question: "Was sind personenbezogene Daten?",
|
||||
Options: []string{"Nur Name und Adresse", "Alle Informationen, die sich auf eine identifizierbare Person beziehen", "Nur digitale Daten", "Nur sensible Gesundheitsdaten"},
|
||||
CorrectOptionIndex: 1,
|
||||
Explanation: "Personenbezogene Daten umfassen alle Informationen, die sich auf eine identifizierte oder identifizierbare natuerliche Person beziehen.",
|
||||
Order: 2,
|
||||
},
|
||||
{
|
||||
Question: "Wie hoch kann das Bussgeld bei DSGVO-Verstoessen maximal sein?",
|
||||
Options: []string{"1 Mio. EUR", "5 Mio. EUR", "10 Mio. EUR oder 2% des Jahresumsatzes", "20 Mio. EUR oder 4% des Jahresumsatzes"},
|
||||
CorrectOptionIndex: 3,
|
||||
Explanation: "Bei schwerwiegenden Verstoessen koennen Bussgelder von bis zu 20 Mio. EUR oder 4% des weltweiten Jahresumsatzes verhaengt werden.",
|
||||
Order: 3,
|
||||
},
|
||||
{
|
||||
Question: "Was bedeutet das Prinzip der Datenminimierung?",
|
||||
Options: []string{"Alle Daten muessen verschluesselt werden", "Es duerfen nur fuer den Zweck erforderliche Daten erhoben werden", "Daten muessen nach 30 Tagen geloescht werden", "Nur Administratoren duerfen auf Daten zugreifen"},
|
||||
CorrectOptionIndex: 1,
|
||||
Explanation: "Datenminimierung bedeutet, dass nur die fuer den jeweiligen Zweck erforderlichen Daten erhoben und verarbeitet werden duerfen.",
|
||||
Order: 4,
|
||||
},
|
||||
{
|
||||
Question: "Innerhalb welcher Frist muss eine Datenpanne der Aufsichtsbehoerde gemeldet werden?",
|
||||
Options: []string{"24 Stunden", "48 Stunden", "72 Stunden", "7 Tage"},
|
||||
CorrectOptionIndex: 2,
|
||||
Explanation: "Gemaess Art. 33 DSGVO muss eine Datenpanne innerhalb von 72 Stunden nach Bekanntwerden der Aufsichtsbehoerde gemeldet werden.",
|
||||
Order: 5,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (h *AcademyHandler) mockITSecurityCourse(req GenerateCourseRequest) AcademyCourse {
|
||||
return AcademyCourse{
|
||||
Title: "IT-Sicherheit & Cybersecurity Awareness",
|
||||
Description: "Sensibilisierung fuer IT-Sicherheitsrisiken und Best Practices im Umgang mit Phishing, Passwoertern und Social Engineering.",
|
||||
DurationMinutes: 60,
|
||||
Lessons: []AcademyLesson{
|
||||
{Title: "Phishing erkennen und vermeiden", Type: "text", Order: 1, DurationMinutes: 15,
|
||||
ContentMarkdown: "# Phishing erkennen\n\n## Typische Merkmale\n- Dringlichkeit ('Ihr Konto wird gesperrt!')\n- Unbekannter Absender\n- Verdaechtige Links\n- Rechtschreibfehler\n\n## Was tun bei Verdacht?\n1. Link NICHT anklicken\n2. Anhang NICHT oeffnen\n3. IT-Sicherheit informieren"},
|
||||
{Title: "Sichere Passwoerter und MFA", Type: "text", Order: 2, DurationMinutes: 15,
|
||||
ContentMarkdown: "# Sichere Passwoerter\n\n## Regeln\n- Mindestens 12 Zeichen\n- Gross-/Kleinbuchstaben, Zahlen, Sonderzeichen\n- Fuer jeden Dienst ein eigenes Passwort\n- Passwort-Manager verwenden\n\n## Multi-Faktor-Authentifizierung\n- Immer aktivieren wenn moeglich\n- App-basiert (z.B. Microsoft Authenticator) bevorzugen"},
|
||||
{Title: "Social Engineering", Type: "text", Order: 3, DurationMinutes: 15,
|
||||
ContentMarkdown: "# Social Engineering\n\nAngreifer nutzen menschliche Schwaechen aus.\n\n## Methoden\n- **Pretexting**: Falsche Identitaet vortaeuschen\n- **Tailgating**: Unbefugter Zutritt durch Hinterherfolgen\n- **CEO Fraud**: Gefaelschte Anweisungen vom Vorgesetzten\n\n## Schutz\n- Identitaet immer verifizieren\n- Bei Unsicherheit nachfragen"},
|
||||
{Title: "Wissenstest: IT-Sicherheit", Type: "quiz", Order: 4, DurationMinutes: 15,
|
||||
QuizQuestions: []AcademyQuizQuestion{
|
||||
{Question: "Was ist ein typisches Merkmal einer Phishing-E-Mail?", Options: []string{"Professionelles Design", "Kuenstliche Dringlichkeit", "Bekannter Absender", "Kurzer Text"}, CorrectOptionIndex: 1, Explanation: "Phishing-Mails erzeugen oft kuenstliche Dringlichkeit.", Order: 1},
|
||||
{Question: "Wie lang sollte ein sicheres Passwort mindestens sein?", Options: []string{"6 Zeichen", "8 Zeichen", "10 Zeichen", "12 Zeichen"}, CorrectOptionIndex: 3, Explanation: "Mindestens 12 Zeichen werden empfohlen.", Order: 2},
|
||||
{Question: "Was ist CEO Fraud?", Options: []string{"Hacker-Angriff auf Server", "Gefaelschte Anweisung vom Vorgesetzten", "Virus in E-Mail-Anhang", "DDoS-Attacke"}, CorrectOptionIndex: 1, Explanation: "CEO Fraud ist eine Social-Engineering-Methode mit gefaelschten Anweisungen.", Order: 3},
|
||||
}},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (h *AcademyHandler) mockAILiteracyCourse(req GenerateCourseRequest) AcademyCourse {
|
||||
return AcademyCourse{
|
||||
Title: "AI Literacy - Sicherer Umgang mit KI",
|
||||
Description: "Grundlagen kuenstlicher Intelligenz, EU AI Act und verantwortungsvoller Einsatz von KI-Werkzeugen im Unternehmen.",
|
||||
DurationMinutes: 75,
|
||||
Lessons: []AcademyLesson{
|
||||
{Title: "Was ist Kuenstliche Intelligenz?", Type: "text", Order: 1, DurationMinutes: 15,
|
||||
ContentMarkdown: "# Was ist KI?\n\nKuenstliche Intelligenz (KI) bezeichnet Systeme, die menschenaehnliche kognitive Faehigkeiten zeigen.\n\n## Arten von KI\n- **Machine Learning**: Lernt aus Daten\n- **Deep Learning**: Neuronale Netze\n- **Generative AI**: Erstellt neue Inhalte (Text, Bild)\n- **LLMs**: Large Language Models wie ChatGPT"},
|
||||
{Title: "Der EU AI Act", Type: "text", Order: 2, DurationMinutes: 20,
|
||||
ContentMarkdown: "# EU AI Act\n\n## Risikoklassen\n- **Unakzeptabel**: Social Scoring, Manipulation\n- **Hochrisiko**: Bildung, HR, Kritische Infrastruktur\n- **Begrenzt**: Chatbots, Empfehlungssysteme\n- **Minimal**: Spam-Filter\n\n## Art. 4: AI Literacy Pflicht\nAlle Mitarbeiter, die KI-Systeme nutzen, muessen geschult werden."},
|
||||
{Title: "KI sicher im Unternehmen nutzen", Type: "text", Order: 3, DurationMinutes: 20,
|
||||
ContentMarkdown: "# KI sicher nutzen\n\n## Dos\n- Ergebnisse immer pruefen\n- Keine vertraulichen Daten eingeben\n- Firmenpolicies beachten\n\n## Don'ts\n- Blindes Vertrauen in KI-Ergebnisse\n- Personenbezogene Daten in externe KI-Tools\n- KI-generierte Inhalte ohne Pruefung veroeffentlichen"},
|
||||
{Title: "Wissenstest: AI Literacy", Type: "quiz", Order: 4, DurationMinutes: 20,
|
||||
QuizQuestions: []AcademyQuizQuestion{
|
||||
{Question: "Was verlangt Art. 4 des EU AI Acts?", Options: []string{"Verbot aller KI-Systeme", "AI Literacy Schulung fuer KI-Nutzer", "Nur Open-Source KI erlaubt", "KI nur in der IT-Abteilung"}, CorrectOptionIndex: 1, Explanation: "Art. 4 EU AI Act fordert AI Literacy fuer alle Mitarbeiter, die KI-Systeme nutzen.", Order: 1},
|
||||
{Question: "Duerfen vertrauliche Firmendaten in externe KI-Tools eingegeben werden?", Options: []string{"Ja, immer", "Nur in ChatGPT", "Nein, grundsaetzlich nicht", "Nur mit VPN"}, CorrectOptionIndex: 2, Explanation: "Vertrauliche Daten duerfen nicht in externe KI-Tools eingegeben werden.", Order: 2},
|
||||
}},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (h *AcademyHandler) mockWhistleblowerCourse(req GenerateCourseRequest) AcademyCourse {
|
||||
return AcademyCourse{
|
||||
Title: "Hinweisgeberschutz (HinSchG)",
|
||||
Description: "Einführung in das Hinweisgeberschutzgesetz, interne Meldewege und Schutz von Whistleblowern.",
|
||||
DurationMinutes: 45,
|
||||
Lessons: []AcademyLesson{
|
||||
{Title: "Das Hinweisgeberschutzgesetz", Type: "text", Order: 1, DurationMinutes: 15,
|
||||
ContentMarkdown: "# Hinweisgeberschutzgesetz (HinSchG)\n\nSeit Juli 2023 muessen Unternehmen ab 50 Mitarbeitern interne Meldestellen einrichten.\n\n## Was ist geschuetzt?\n- Meldungen ueber Rechtsverstoesse\n- Verstoesse gegen EU-Recht\n- Straftaten und Ordnungswidrigkeiten"},
|
||||
{Title: "Interne Meldewege", Type: "text", Order: 2, DurationMinutes: 15,
|
||||
ContentMarkdown: "# Interne Meldewege\n\n## Wie melde ich einen Verstoss?\n1. **Interne Meldestelle** (bevorzugt)\n2. **Externe Meldestelle** (BfJ)\n3. **Offenlegung** (nur als letztes Mittel)\n\n## Schutz fuer Hinweisgeber\n- Kuendigungsschutz\n- Keine Benachteiligung\n- Vertraulichkeit"},
|
||||
{Title: "Wissenstest: Hinweisgeberschutz", Type: "quiz", Order: 3, DurationMinutes: 15,
|
||||
QuizQuestions: []AcademyQuizQuestion{
|
||||
{Question: "Ab wie vielen Mitarbeitern muessen Unternehmen eine Meldestelle einrichten?", Options: []string{"10", "25", "50", "250"}, CorrectOptionIndex: 2, Explanation: "Unternehmen ab 50 Beschaeftigten muessen eine interne Meldestelle einrichten.", Order: 1},
|
||||
{Question: "Welche Meldung ist NICHT durch das HinSchG geschuetzt?", Options: []string{"Straftaten", "Verstoesse gegen EU-Recht", "Persoenliche Beschwerden ueber Kollegen", "Umweltverstoesse"}, CorrectOptionIndex: 2, Explanation: "Persoenliche Konflikte fallen nicht unter das HinSchG.", Order: 2},
|
||||
}},
|
||||
},
|
||||
}
|
||||
}
|
||||
209
admin-v2/ai-compliance-sdk/internal/api/academy_models.go
Normal file
209
admin-v2/ai-compliance-sdk/internal/api/academy_models.go
Normal file
@@ -0,0 +1,209 @@
|
||||
package api
|
||||
|
||||
// Academy Course models
|
||||
|
||||
// AcademyCourse represents a training course in the Academy module
|
||||
type AcademyCourse struct {
|
||||
ID string `json:"id"`
|
||||
TenantID string `json:"tenantId,omitempty"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Category string `json:"category"`
|
||||
PassingScore int `json:"passingScore"`
|
||||
DurationMinutes int `json:"durationMinutes"`
|
||||
RequiredForRoles []string `json:"requiredForRoles"`
|
||||
Status string `json:"status"`
|
||||
Lessons []AcademyLesson `json:"lessons"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
}
|
||||
|
||||
// AcademyLesson represents a single lesson within a course
|
||||
type AcademyLesson struct {
|
||||
ID string `json:"id"`
|
||||
CourseID string `json:"courseId"`
|
||||
Title string `json:"title"`
|
||||
Type string `json:"type"` // video, text, quiz
|
||||
ContentMarkdown string `json:"contentMarkdown"`
|
||||
VideoURL string `json:"videoUrl,omitempty"`
|
||||
AudioURL string `json:"audioUrl,omitempty"`
|
||||
Order int `json:"order"`
|
||||
DurationMinutes int `json:"durationMinutes"`
|
||||
QuizQuestions []AcademyQuizQuestion `json:"quizQuestions,omitempty"`
|
||||
}
|
||||
|
||||
// AcademyQuizQuestion represents a single quiz question within a lesson
|
||||
type AcademyQuizQuestion struct {
|
||||
ID string `json:"id"`
|
||||
LessonID string `json:"lessonId"`
|
||||
Question string `json:"question"`
|
||||
Options []string `json:"options"`
|
||||
CorrectOptionIndex int `json:"correctOptionIndex"`
|
||||
Explanation string `json:"explanation"`
|
||||
Order int `json:"order"`
|
||||
}
|
||||
|
||||
// AcademyEnrollment represents a user's enrollment in a course
|
||||
type AcademyEnrollment struct {
|
||||
ID string `json:"id"`
|
||||
TenantID string `json:"tenantId,omitempty"`
|
||||
CourseID string `json:"courseId"`
|
||||
UserID string `json:"userId"`
|
||||
UserName string `json:"userName"`
|
||||
UserEmail string `json:"userEmail"`
|
||||
Status string `json:"status"` // not_started, in_progress, completed, expired
|
||||
Progress int `json:"progress"` // 0-100
|
||||
StartedAt string `json:"startedAt"`
|
||||
CompletedAt string `json:"completedAt,omitempty"`
|
||||
CertificateID string `json:"certificateId,omitempty"`
|
||||
Deadline string `json:"deadline"`
|
||||
CreatedAt string `json:"createdAt,omitempty"`
|
||||
UpdatedAt string `json:"updatedAt,omitempty"`
|
||||
}
|
||||
|
||||
// AcademyCertificate represents a certificate issued upon course completion
|
||||
type AcademyCertificate struct {
|
||||
ID string `json:"id"`
|
||||
TenantID string `json:"tenantId,omitempty"`
|
||||
EnrollmentID string `json:"enrollmentId"`
|
||||
CourseID string `json:"courseId"`
|
||||
UserID string `json:"userId"`
|
||||
UserName string `json:"userName"`
|
||||
CourseName string `json:"courseName"`
|
||||
Score int `json:"score"`
|
||||
IssuedAt string `json:"issuedAt"`
|
||||
ValidUntil string `json:"validUntil"`
|
||||
PdfURL string `json:"pdfUrl,omitempty"`
|
||||
}
|
||||
|
||||
// AcademyLessonProgress tracks a user's progress through a single lesson
|
||||
type AcademyLessonProgress struct {
|
||||
ID string `json:"id"`
|
||||
EnrollmentID string `json:"enrollmentId"`
|
||||
LessonID string `json:"lessonId"`
|
||||
Completed bool `json:"completed"`
|
||||
QuizScore *int `json:"quizScore,omitempty"`
|
||||
CompletedAt string `json:"completedAt,omitempty"`
|
||||
}
|
||||
|
||||
// AcademyStatistics provides aggregate statistics for the Academy module
|
||||
type AcademyStatistics struct {
|
||||
TotalCourses int `json:"totalCourses"`
|
||||
TotalEnrollments int `json:"totalEnrollments"`
|
||||
CompletionRate int `json:"completionRate"`
|
||||
OverdueCount int `json:"overdueCount"`
|
||||
ByCategory map[string]int `json:"byCategory"`
|
||||
ByStatus map[string]int `json:"byStatus"`
|
||||
}
|
||||
|
||||
// Request types
|
||||
|
||||
// CreateCourseRequest is the request body for creating a new course
|
||||
type CreateCourseRequest struct {
|
||||
TenantID string `json:"tenantId" binding:"required"`
|
||||
Title string `json:"title" binding:"required"`
|
||||
Description string `json:"description"`
|
||||
Category string `json:"category" binding:"required"`
|
||||
DurationMinutes int `json:"durationMinutes"`
|
||||
RequiredForRoles []string `json:"requiredForRoles"`
|
||||
PassingScore int `json:"passingScore"`
|
||||
Lessons []CreateLessonRequest `json:"lessons"`
|
||||
}
|
||||
|
||||
// CreateLessonRequest is the request body for creating a lesson within a course
|
||||
type CreateLessonRequest struct {
|
||||
Title string `json:"title" binding:"required"`
|
||||
Type string `json:"type" binding:"required"`
|
||||
ContentMarkdown string `json:"contentMarkdown"`
|
||||
VideoURL string `json:"videoUrl"`
|
||||
Order int `json:"order"`
|
||||
DurationMinutes int `json:"durationMinutes"`
|
||||
QuizQuestions []CreateQuizQuestionRequest `json:"quizQuestions"`
|
||||
}
|
||||
|
||||
// CreateQuizQuestionRequest is the request body for creating a quiz question
|
||||
type CreateQuizQuestionRequest struct {
|
||||
Question string `json:"question" binding:"required"`
|
||||
Options []string `json:"options" binding:"required"`
|
||||
CorrectOptionIndex int `json:"correctOptionIndex"`
|
||||
Explanation string `json:"explanation"`
|
||||
Order int `json:"order"`
|
||||
}
|
||||
|
||||
// UpdateCourseRequest is the request body for updating an existing course
|
||||
type UpdateCourseRequest struct {
|
||||
Title *string `json:"title"`
|
||||
Description *string `json:"description"`
|
||||
Category *string `json:"category"`
|
||||
DurationMinutes *int `json:"durationMinutes"`
|
||||
RequiredForRoles []string `json:"requiredForRoles"`
|
||||
PassingScore *int `json:"passingScore"`
|
||||
}
|
||||
|
||||
// EnrollUserRequest is the request body for enrolling a user in a course
|
||||
type EnrollUserRequest struct {
|
||||
TenantID string `json:"tenantId" binding:"required"`
|
||||
CourseID string `json:"courseId" binding:"required"`
|
||||
UserID string `json:"userId" binding:"required"`
|
||||
UserName string `json:"userName" binding:"required"`
|
||||
UserEmail string `json:"userEmail" binding:"required"`
|
||||
Deadline string `json:"deadline" binding:"required"`
|
||||
}
|
||||
|
||||
// UpdateProgressRequest is the request body for updating enrollment progress
|
||||
type UpdateProgressRequest struct {
|
||||
Progress int `json:"progress"`
|
||||
LessonID string `json:"lessonId"`
|
||||
}
|
||||
|
||||
// SubmitQuizRequest is the request body for submitting quiz answers
|
||||
type SubmitQuizRequest struct {
|
||||
Answers []int `json:"answers" binding:"required"`
|
||||
}
|
||||
|
||||
// SubmitQuizResponse is the response for a quiz submission
|
||||
type SubmitQuizResponse struct {
|
||||
Score int `json:"score"`
|
||||
Passed bool `json:"passed"`
|
||||
CorrectAnswers int `json:"correctAnswers"`
|
||||
TotalQuestions int `json:"totalQuestions"`
|
||||
Results []QuizQuestionResult `json:"results"`
|
||||
}
|
||||
|
||||
// QuizQuestionResult represents the result of a single quiz question
|
||||
type QuizQuestionResult struct {
|
||||
QuestionID string `json:"questionId"`
|
||||
Correct bool `json:"correct"`
|
||||
Explanation string `json:"explanation"`
|
||||
}
|
||||
|
||||
// GenerateCourseRequest is the request body for AI-generating a course
|
||||
type GenerateCourseRequest struct {
|
||||
TenantID string `json:"tenantId" binding:"required"`
|
||||
Topic string `json:"topic" binding:"required"`
|
||||
Category string `json:"category" binding:"required"`
|
||||
TargetGroup string `json:"targetGroup"`
|
||||
Language string `json:"language"`
|
||||
UseRAG bool `json:"useRag"`
|
||||
RAGQuery string `json:"ragQuery"`
|
||||
}
|
||||
|
||||
// GenerateVideosRequest is the request body for generating lesson videos
|
||||
type GenerateVideosRequest struct {
|
||||
TenantID string `json:"tenantId" binding:"required"`
|
||||
}
|
||||
|
||||
// VideoStatusResponse represents the video generation status for a course
|
||||
type VideoStatusResponse struct {
|
||||
CourseID string `json:"courseId"`
|
||||
Status string `json:"status"` // pending, processing, completed, failed
|
||||
Lessons []LessonVideoStatus `json:"lessons"`
|
||||
}
|
||||
|
||||
// LessonVideoStatus represents the video generation status for a single lesson
|
||||
type LessonVideoStatus struct {
|
||||
LessonID string `json:"lessonId"`
|
||||
Status string `json:"status"`
|
||||
VideoURL string `json:"videoUrl,omitempty"`
|
||||
AudioURL string `json:"audioUrl,omitempty"`
|
||||
}
|
||||
@@ -31,15 +31,15 @@ func (h *GenerateHandler) GenerateDSFA(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Get RAG context if requested
|
||||
var ragSources []SearchResult
|
||||
var ragSources []llm.SearchResult
|
||||
if req.UseRAG && h.ragService != nil {
|
||||
query := req.RAGQuery
|
||||
if query == "" {
|
||||
query = "DSFA Datenschutz-Folgenabschätzung Anforderungen"
|
||||
query = "DSFA Datenschutz-Folgenabschaetzung Anforderungen"
|
||||
}
|
||||
results, _ := h.ragService.Search(c.Request.Context(), query, 5, "legal_corpus", "regulation:DSGVO")
|
||||
for _, r := range results {
|
||||
ragSources = append(ragSources, SearchResult{
|
||||
ragSources = append(ragSources, llm.SearchResult{
|
||||
ID: r.ID,
|
||||
Content: r.Content,
|
||||
Source: r.Source,
|
||||
@@ -62,7 +62,7 @@ func (h *GenerateHandler) GenerateDSFA(c *gin.Context) {
|
||||
GeneratedAt: now(),
|
||||
Model: h.llmService.GetModel(),
|
||||
TokensUsed: tokensUsed,
|
||||
RAGSources: ragSources,
|
||||
RAGSources: convertLLMSources(ragSources),
|
||||
Confidence: 0.85,
|
||||
})
|
||||
}
|
||||
@@ -76,15 +76,15 @@ func (h *GenerateHandler) GenerateTOM(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Get RAG context if requested
|
||||
var ragSources []SearchResult
|
||||
var llmRagSources []llm.SearchResult
|
||||
if req.UseRAG && h.ragService != nil {
|
||||
query := req.RAGQuery
|
||||
if query == "" {
|
||||
query = "technische organisatorische Maßnahmen TOM Datenschutz"
|
||||
query = "technische organisatorische Massnahmen TOM Datenschutz"
|
||||
}
|
||||
results, _ := h.ragService.Search(c.Request.Context(), query, 5, "legal_corpus", "")
|
||||
for _, r := range results {
|
||||
ragSources = append(ragSources, SearchResult{
|
||||
llmRagSources = append(llmRagSources, llm.SearchResult{
|
||||
ID: r.ID,
|
||||
Content: r.Content,
|
||||
Source: r.Source,
|
||||
@@ -95,7 +95,7 @@ func (h *GenerateHandler) GenerateTOM(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Generate TOM content
|
||||
content, tokensUsed, err := h.llmService.GenerateTOM(c.Request.Context(), req.Context, ragSources)
|
||||
content, tokensUsed, err := h.llmService.GenerateTOM(c.Request.Context(), req.Context, llmRagSources)
|
||||
if err != nil {
|
||||
content = h.getMockTOM(req.Context)
|
||||
tokensUsed = 0
|
||||
@@ -106,7 +106,7 @@ func (h *GenerateHandler) GenerateTOM(c *gin.Context) {
|
||||
GeneratedAt: now(),
|
||||
Model: h.llmService.GetModel(),
|
||||
TokensUsed: tokensUsed,
|
||||
RAGSources: ragSources,
|
||||
RAGSources: convertLLMSources(llmRagSources),
|
||||
Confidence: 0.82,
|
||||
})
|
||||
}
|
||||
@@ -120,7 +120,7 @@ func (h *GenerateHandler) GenerateVVT(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Get RAG context if requested
|
||||
var ragSources []SearchResult
|
||||
var llmRagSources []llm.SearchResult
|
||||
if req.UseRAG && h.ragService != nil {
|
||||
query := req.RAGQuery
|
||||
if query == "" {
|
||||
@@ -128,7 +128,7 @@ func (h *GenerateHandler) GenerateVVT(c *gin.Context) {
|
||||
}
|
||||
results, _ := h.ragService.Search(c.Request.Context(), query, 5, "legal_corpus", "regulation:DSGVO")
|
||||
for _, r := range results {
|
||||
ragSources = append(ragSources, SearchResult{
|
||||
llmRagSources = append(llmRagSources, llm.SearchResult{
|
||||
ID: r.ID,
|
||||
Content: r.Content,
|
||||
Source: r.Source,
|
||||
@@ -139,7 +139,7 @@ func (h *GenerateHandler) GenerateVVT(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Generate VVT content
|
||||
content, tokensUsed, err := h.llmService.GenerateVVT(c.Request.Context(), req.Context, ragSources)
|
||||
content, tokensUsed, err := h.llmService.GenerateVVT(c.Request.Context(), req.Context, llmRagSources)
|
||||
if err != nil {
|
||||
content = h.getMockVVT(req.Context)
|
||||
tokensUsed = 0
|
||||
@@ -150,7 +150,7 @@ func (h *GenerateHandler) GenerateVVT(c *gin.Context) {
|
||||
GeneratedAt: now(),
|
||||
Model: h.llmService.GetModel(),
|
||||
TokensUsed: tokensUsed,
|
||||
RAGSources: ragSources,
|
||||
RAGSources: convertLLMSources(llmRagSources),
|
||||
Confidence: 0.88,
|
||||
})
|
||||
}
|
||||
@@ -164,7 +164,7 @@ func (h *GenerateHandler) GenerateGutachten(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Get RAG context if requested
|
||||
var ragSources []SearchResult
|
||||
var llmRagSources []llm.SearchResult
|
||||
if req.UseRAG && h.ragService != nil {
|
||||
query := req.RAGQuery
|
||||
if query == "" {
|
||||
@@ -172,7 +172,7 @@ func (h *GenerateHandler) GenerateGutachten(c *gin.Context) {
|
||||
}
|
||||
results, _ := h.ragService.Search(c.Request.Context(), query, 5, "legal_corpus", "")
|
||||
for _, r := range results {
|
||||
ragSources = append(ragSources, SearchResult{
|
||||
llmRagSources = append(llmRagSources, llm.SearchResult{
|
||||
ID: r.ID,
|
||||
Content: r.Content,
|
||||
Source: r.Source,
|
||||
@@ -183,7 +183,7 @@ func (h *GenerateHandler) GenerateGutachten(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Generate Gutachten content
|
||||
content, tokensUsed, err := h.llmService.GenerateGutachten(c.Request.Context(), req.Context, ragSources)
|
||||
content, tokensUsed, err := h.llmService.GenerateGutachten(c.Request.Context(), req.Context, llmRagSources)
|
||||
if err != nil {
|
||||
content = h.getMockGutachten(req.Context)
|
||||
tokensUsed = 0
|
||||
@@ -194,7 +194,7 @@ func (h *GenerateHandler) GenerateGutachten(c *gin.Context) {
|
||||
GeneratedAt: now(),
|
||||
Model: h.llmService.GetModel(),
|
||||
TokensUsed: tokensUsed,
|
||||
RAGSources: ragSources,
|
||||
RAGSources: convertLLMSources(llmRagSources),
|
||||
Confidence: 0.80,
|
||||
})
|
||||
}
|
||||
@@ -363,3 +363,21 @@ Das geprüfte KI-System erfüllt die wesentlichen Anforderungen der DSGVO und de
|
||||
Erstellt am: ${new Date().toISOString()}
|
||||
`
|
||||
}
|
||||
|
||||
// convertLLMSources converts llm.SearchResult to api.SearchResult for the response
|
||||
func convertLLMSources(sources []llm.SearchResult) []SearchResult {
|
||||
if sources == nil {
|
||||
return nil
|
||||
}
|
||||
result := make([]SearchResult, len(sources))
|
||||
for i, s := range sources {
|
||||
result[i] = SearchResult{
|
||||
ID: s.ID,
|
||||
Content: s.Content,
|
||||
Source: s.Source,
|
||||
Score: s.Score,
|
||||
Metadata: s.Metadata,
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
681
admin-v2/ai-compliance-sdk/internal/db/academy_store.go
Normal file
681
admin-v2/ai-compliance-sdk/internal/db/academy_store.go
Normal file
@@ -0,0 +1,681 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// AcademyMemStore provides in-memory storage for academy data
|
||||
type AcademyMemStore struct {
|
||||
mu sync.RWMutex
|
||||
courses map[string]*AcademyCourseRow
|
||||
lessons map[string]*AcademyLessonRow
|
||||
quizQuestions map[string]*AcademyQuizQuestionRow
|
||||
enrollments map[string]*AcademyEnrollmentRow
|
||||
certificates map[string]*AcademyCertificateRow
|
||||
lessonProgress map[string]*AcademyLessonProgressRow
|
||||
}
|
||||
|
||||
// Row types matching the DB schema
|
||||
type AcademyCourseRow struct {
|
||||
ID string
|
||||
TenantID string
|
||||
Title string
|
||||
Description string
|
||||
Category string
|
||||
PassingScore int
|
||||
DurationMinutes int
|
||||
RequiredForRoles []string
|
||||
Status string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type AcademyLessonRow struct {
|
||||
ID string
|
||||
CourseID string
|
||||
Title string
|
||||
Type string
|
||||
ContentMarkdown string
|
||||
VideoURL string
|
||||
AudioURL string
|
||||
SortOrder int
|
||||
DurationMinutes int
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type AcademyQuizQuestionRow struct {
|
||||
ID string
|
||||
LessonID string
|
||||
Question string
|
||||
Options []string
|
||||
CorrectOptionIndex int
|
||||
Explanation string
|
||||
SortOrder int
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
type AcademyEnrollmentRow struct {
|
||||
ID string
|
||||
TenantID string
|
||||
CourseID string
|
||||
UserID string
|
||||
UserName string
|
||||
UserEmail string
|
||||
Status string
|
||||
Progress int
|
||||
StartedAt time.Time
|
||||
CompletedAt *time.Time
|
||||
CertificateID string
|
||||
Deadline time.Time
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type AcademyCertificateRow struct {
|
||||
ID string
|
||||
TenantID string
|
||||
EnrollmentID string
|
||||
CourseID string
|
||||
UserID string
|
||||
UserName string
|
||||
CourseName string
|
||||
Score int
|
||||
IssuedAt time.Time
|
||||
ValidUntil time.Time
|
||||
PdfURL string
|
||||
}
|
||||
|
||||
type AcademyLessonProgressRow struct {
|
||||
ID string
|
||||
EnrollmentID string
|
||||
LessonID string
|
||||
Completed bool
|
||||
QuizScore *int
|
||||
CompletedAt *time.Time
|
||||
}
|
||||
|
||||
type AcademyStatisticsRow struct {
|
||||
TotalCourses int
|
||||
TotalEnrollments int
|
||||
CompletionRate float64
|
||||
OverdueCount int
|
||||
ByCategory map[string]int
|
||||
ByStatus map[string]int
|
||||
}
|
||||
|
||||
func NewAcademyMemStore() *AcademyMemStore {
|
||||
return &AcademyMemStore{
|
||||
courses: make(map[string]*AcademyCourseRow),
|
||||
lessons: make(map[string]*AcademyLessonRow),
|
||||
quizQuestions: make(map[string]*AcademyQuizQuestionRow),
|
||||
enrollments: make(map[string]*AcademyEnrollmentRow),
|
||||
certificates: make(map[string]*AcademyCertificateRow),
|
||||
lessonProgress: make(map[string]*AcademyLessonProgressRow),
|
||||
}
|
||||
}
|
||||
|
||||
// generateID creates a simple unique ID
|
||||
func generateID() string {
|
||||
return fmt.Sprintf("%d", time.Now().UnixNano())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Course CRUD
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// ListCourses returns all courses for a tenant, sorted by UpdatedAt DESC.
|
||||
func (s *AcademyMemStore) ListCourses(tenantID string) []*AcademyCourseRow {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
var result []*AcademyCourseRow
|
||||
for _, c := range s.courses {
|
||||
if c.TenantID == tenantID {
|
||||
result = append(result, c)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
return result[i].UpdatedAt.After(result[j].UpdatedAt)
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// GetCourse retrieves a single course by ID.
|
||||
func (s *AcademyMemStore) GetCourse(id string) (*AcademyCourseRow, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
c, ok := s.courses[id]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("course not found: %s", id)
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// CreateCourse inserts a new course with auto-generated ID and timestamps.
|
||||
func (s *AcademyMemStore) CreateCourse(row *AcademyCourseRow) *AcademyCourseRow {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
row.ID = generateID()
|
||||
row.CreatedAt = now
|
||||
row.UpdatedAt = now
|
||||
s.courses[row.ID] = row
|
||||
return row
|
||||
}
|
||||
|
||||
// UpdateCourse partially updates a course. Supported keys: Title, Description,
|
||||
// Category, PassingScore, DurationMinutes, RequiredForRoles, Status.
|
||||
func (s *AcademyMemStore) UpdateCourse(id string, updates map[string]interface{}) (*AcademyCourseRow, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
c, ok := s.courses[id]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("course not found: %s", id)
|
||||
}
|
||||
|
||||
for k, v := range updates {
|
||||
switch strings.ToLower(k) {
|
||||
case "title":
|
||||
if val, ok := v.(string); ok {
|
||||
c.Title = val
|
||||
}
|
||||
case "description":
|
||||
if val, ok := v.(string); ok {
|
||||
c.Description = val
|
||||
}
|
||||
case "category":
|
||||
if val, ok := v.(string); ok {
|
||||
c.Category = val
|
||||
}
|
||||
case "passingscore", "passing_score":
|
||||
switch val := v.(type) {
|
||||
case int:
|
||||
c.PassingScore = val
|
||||
case float64:
|
||||
c.PassingScore = int(val)
|
||||
}
|
||||
case "durationminutes", "duration_minutes":
|
||||
switch val := v.(type) {
|
||||
case int:
|
||||
c.DurationMinutes = val
|
||||
case float64:
|
||||
c.DurationMinutes = int(val)
|
||||
}
|
||||
case "requiredforroles", "required_for_roles":
|
||||
if val, ok := v.([]string); ok {
|
||||
c.RequiredForRoles = val
|
||||
}
|
||||
case "status":
|
||||
if val, ok := v.(string); ok {
|
||||
c.Status = val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.UpdatedAt = time.Now()
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// DeleteCourse removes a course and all related lessons, quiz questions,
|
||||
// enrollments, certificates, and lesson progress.
|
||||
func (s *AcademyMemStore) DeleteCourse(id string) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if _, ok := s.courses[id]; !ok {
|
||||
return fmt.Errorf("course not found: %s", id)
|
||||
}
|
||||
|
||||
// Collect lesson IDs for this course
|
||||
lessonIDs := make(map[string]bool)
|
||||
for lid, l := range s.lessons {
|
||||
if l.CourseID == id {
|
||||
lessonIDs[lid] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Delete quiz questions belonging to those lessons
|
||||
for qid, q := range s.quizQuestions {
|
||||
if lessonIDs[q.LessonID] {
|
||||
delete(s.quizQuestions, qid)
|
||||
}
|
||||
}
|
||||
|
||||
// Delete lessons
|
||||
for lid := range lessonIDs {
|
||||
delete(s.lessons, lid)
|
||||
}
|
||||
|
||||
// Collect enrollment IDs for this course
|
||||
enrollmentIDs := make(map[string]bool)
|
||||
for eid, e := range s.enrollments {
|
||||
if e.CourseID == id {
|
||||
enrollmentIDs[eid] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Delete lesson progress belonging to those enrollments
|
||||
for pid, p := range s.lessonProgress {
|
||||
if enrollmentIDs[p.EnrollmentID] {
|
||||
delete(s.lessonProgress, pid)
|
||||
}
|
||||
}
|
||||
|
||||
// Delete certificates belonging to those enrollments
|
||||
for cid, cert := range s.certificates {
|
||||
if cert.CourseID == id {
|
||||
delete(s.certificates, cid)
|
||||
}
|
||||
}
|
||||
|
||||
// Delete enrollments
|
||||
for eid := range enrollmentIDs {
|
||||
delete(s.enrollments, eid)
|
||||
}
|
||||
|
||||
// Delete the course itself
|
||||
delete(s.courses, id)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lesson CRUD
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// ListLessons returns all lessons for a course, sorted by SortOrder ASC.
|
||||
func (s *AcademyMemStore) ListLessons(courseID string) []*AcademyLessonRow {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
var result []*AcademyLessonRow
|
||||
for _, l := range s.lessons {
|
||||
if l.CourseID == courseID {
|
||||
result = append(result, l)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
return result[i].SortOrder < result[j].SortOrder
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// GetLesson retrieves a single lesson by ID.
|
||||
func (s *AcademyMemStore) GetLesson(id string) (*AcademyLessonRow, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
l, ok := s.lessons[id]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("lesson not found: %s", id)
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
|
||||
// CreateLesson inserts a new lesson with auto-generated ID and timestamps.
|
||||
func (s *AcademyMemStore) CreateLesson(row *AcademyLessonRow) *AcademyLessonRow {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
row.ID = generateID()
|
||||
row.CreatedAt = now
|
||||
row.UpdatedAt = now
|
||||
s.lessons[row.ID] = row
|
||||
return row
|
||||
}
|
||||
|
||||
// UpdateLesson partially updates a lesson. Supported keys: Title, Type,
|
||||
// ContentMarkdown, VideoURL, AudioURL, SortOrder, DurationMinutes.
|
||||
func (s *AcademyMemStore) UpdateLesson(id string, updates map[string]interface{}) (*AcademyLessonRow, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
l, ok := s.lessons[id]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("lesson not found: %s", id)
|
||||
}
|
||||
|
||||
for k, v := range updates {
|
||||
switch strings.ToLower(k) {
|
||||
case "title":
|
||||
if val, ok := v.(string); ok {
|
||||
l.Title = val
|
||||
}
|
||||
case "type":
|
||||
if val, ok := v.(string); ok {
|
||||
l.Type = val
|
||||
}
|
||||
case "contentmarkdown", "content_markdown":
|
||||
if val, ok := v.(string); ok {
|
||||
l.ContentMarkdown = val
|
||||
}
|
||||
case "videourl", "video_url":
|
||||
if val, ok := v.(string); ok {
|
||||
l.VideoURL = val
|
||||
}
|
||||
case "audiourl", "audio_url":
|
||||
if val, ok := v.(string); ok {
|
||||
l.AudioURL = val
|
||||
}
|
||||
case "sortorder", "sort_order":
|
||||
switch val := v.(type) {
|
||||
case int:
|
||||
l.SortOrder = val
|
||||
case float64:
|
||||
l.SortOrder = int(val)
|
||||
}
|
||||
case "durationminutes", "duration_minutes":
|
||||
switch val := v.(type) {
|
||||
case int:
|
||||
l.DurationMinutes = val
|
||||
case float64:
|
||||
l.DurationMinutes = int(val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
l.UpdatedAt = time.Now()
|
||||
return l, nil
|
||||
}
|
||||
|
||||
// DeleteLesson removes a lesson and its quiz questions.
|
||||
func (s *AcademyMemStore) DeleteLesson(id string) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if _, ok := s.lessons[id]; !ok {
|
||||
return fmt.Errorf("lesson not found: %s", id)
|
||||
}
|
||||
|
||||
// Delete quiz questions belonging to this lesson
|
||||
for qid, q := range s.quizQuestions {
|
||||
if q.LessonID == id {
|
||||
delete(s.quizQuestions, qid)
|
||||
}
|
||||
}
|
||||
|
||||
delete(s.lessons, id)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Quiz Questions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// ListQuizQuestions returns all quiz questions for a lesson, sorted by SortOrder ASC.
|
||||
func (s *AcademyMemStore) ListQuizQuestions(lessonID string) []*AcademyQuizQuestionRow {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
var result []*AcademyQuizQuestionRow
|
||||
for _, q := range s.quizQuestions {
|
||||
if q.LessonID == lessonID {
|
||||
result = append(result, q)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
return result[i].SortOrder < result[j].SortOrder
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// CreateQuizQuestion inserts a new quiz question with auto-generated ID and timestamp.
|
||||
func (s *AcademyMemStore) CreateQuizQuestion(row *AcademyQuizQuestionRow) *AcademyQuizQuestionRow {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
row.ID = generateID()
|
||||
row.CreatedAt = time.Now()
|
||||
s.quizQuestions[row.ID] = row
|
||||
return row
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Enrollments
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// ListEnrollments returns enrollments filtered by tenantID and optionally by courseID.
|
||||
// If courseID is empty, all enrollments for the tenant are returned.
|
||||
func (s *AcademyMemStore) ListEnrollments(tenantID string, courseID string) []*AcademyEnrollmentRow {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
var result []*AcademyEnrollmentRow
|
||||
for _, e := range s.enrollments {
|
||||
if e.TenantID != tenantID {
|
||||
continue
|
||||
}
|
||||
if courseID != "" && e.CourseID != courseID {
|
||||
continue
|
||||
}
|
||||
result = append(result, e)
|
||||
}
|
||||
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
return result[i].UpdatedAt.After(result[j].UpdatedAt)
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// GetEnrollment retrieves a single enrollment by ID.
|
||||
func (s *AcademyMemStore) GetEnrollment(id string) (*AcademyEnrollmentRow, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
e, ok := s.enrollments[id]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("enrollment not found: %s", id)
|
||||
}
|
||||
return e, nil
|
||||
}
|
||||
|
||||
// CreateEnrollment inserts a new enrollment with auto-generated ID and timestamps.
|
||||
func (s *AcademyMemStore) CreateEnrollment(row *AcademyEnrollmentRow) *AcademyEnrollmentRow {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
row.ID = generateID()
|
||||
row.CreatedAt = now
|
||||
row.UpdatedAt = now
|
||||
if row.StartedAt.IsZero() {
|
||||
row.StartedAt = now
|
||||
}
|
||||
s.enrollments[row.ID] = row
|
||||
return row
|
||||
}
|
||||
|
||||
// UpdateEnrollment partially updates an enrollment. Supported keys: Status,
|
||||
// Progress, CompletedAt, CertificateID, Deadline.
|
||||
func (s *AcademyMemStore) UpdateEnrollment(id string, updates map[string]interface{}) (*AcademyEnrollmentRow, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
e, ok := s.enrollments[id]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("enrollment not found: %s", id)
|
||||
}
|
||||
|
||||
for k, v := range updates {
|
||||
switch strings.ToLower(k) {
|
||||
case "status":
|
||||
if val, ok := v.(string); ok {
|
||||
e.Status = val
|
||||
}
|
||||
case "progress":
|
||||
switch val := v.(type) {
|
||||
case int:
|
||||
e.Progress = val
|
||||
case float64:
|
||||
e.Progress = int(val)
|
||||
}
|
||||
case "completedat", "completed_at":
|
||||
if val, ok := v.(*time.Time); ok {
|
||||
e.CompletedAt = val
|
||||
} else if val, ok := v.(time.Time); ok {
|
||||
e.CompletedAt = &val
|
||||
}
|
||||
case "certificateid", "certificate_id":
|
||||
if val, ok := v.(string); ok {
|
||||
e.CertificateID = val
|
||||
}
|
||||
case "deadline":
|
||||
if val, ok := v.(time.Time); ok {
|
||||
e.Deadline = val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
e.UpdatedAt = time.Now()
|
||||
return e, nil
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Certificates
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// GetCertificate retrieves a certificate by ID.
|
||||
func (s *AcademyMemStore) GetCertificate(id string) (*AcademyCertificateRow, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
cert, ok := s.certificates[id]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("certificate not found: %s", id)
|
||||
}
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
// GetCertificateByEnrollment retrieves a certificate by enrollment ID.
|
||||
func (s *AcademyMemStore) GetCertificateByEnrollment(enrollmentID string) (*AcademyCertificateRow, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
for _, cert := range s.certificates {
|
||||
if cert.EnrollmentID == enrollmentID {
|
||||
return cert, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("certificate not found for enrollment: %s", enrollmentID)
|
||||
}
|
||||
|
||||
// CreateCertificate inserts a new certificate with auto-generated ID.
|
||||
func (s *AcademyMemStore) CreateCertificate(row *AcademyCertificateRow) *AcademyCertificateRow {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
row.ID = generateID()
|
||||
if row.IssuedAt.IsZero() {
|
||||
row.IssuedAt = time.Now()
|
||||
}
|
||||
s.certificates[row.ID] = row
|
||||
return row
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lesson Progress
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// ListLessonProgress returns all progress entries for an enrollment.
|
||||
func (s *AcademyMemStore) ListLessonProgress(enrollmentID string) []*AcademyLessonProgressRow {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
var result []*AcademyLessonProgressRow
|
||||
for _, p := range s.lessonProgress {
|
||||
if p.EnrollmentID == enrollmentID {
|
||||
result = append(result, p)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// UpsertLessonProgress inserts or updates a lesson progress entry.
|
||||
// Matching is done by EnrollmentID + LessonID composite key.
|
||||
func (s *AcademyMemStore) UpsertLessonProgress(row *AcademyLessonProgressRow) *AcademyLessonProgressRow {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
// Look for existing entry with same enrollment_id + lesson_id
|
||||
for _, p := range s.lessonProgress {
|
||||
if p.EnrollmentID == row.EnrollmentID && p.LessonID == row.LessonID {
|
||||
p.Completed = row.Completed
|
||||
p.QuizScore = row.QuizScore
|
||||
p.CompletedAt = row.CompletedAt
|
||||
return p
|
||||
}
|
||||
}
|
||||
|
||||
// Insert new entry
|
||||
row.ID = generateID()
|
||||
s.lessonProgress[row.ID] = row
|
||||
return row
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Statistics
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// GetStatistics computes aggregate statistics for a tenant.
|
||||
func (s *AcademyMemStore) GetStatistics(tenantID string) *AcademyStatisticsRow {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
stats := &AcademyStatisticsRow{
|
||||
ByCategory: make(map[string]int),
|
||||
ByStatus: make(map[string]int),
|
||||
}
|
||||
|
||||
// Count courses by category
|
||||
for _, c := range s.courses {
|
||||
if c.TenantID != tenantID {
|
||||
continue
|
||||
}
|
||||
stats.TotalCourses++
|
||||
if c.Category != "" {
|
||||
stats.ByCategory[c.Category]++
|
||||
}
|
||||
}
|
||||
|
||||
// Count enrollments and compute completion rate
|
||||
var completedCount int
|
||||
now := time.Now()
|
||||
for _, e := range s.enrollments {
|
||||
if e.TenantID != tenantID {
|
||||
continue
|
||||
}
|
||||
stats.TotalEnrollments++
|
||||
stats.ByStatus[e.Status]++
|
||||
|
||||
if e.Status == "completed" {
|
||||
completedCount++
|
||||
}
|
||||
|
||||
// Overdue: not completed and past deadline
|
||||
if e.Status != "completed" && !e.Deadline.IsZero() && now.After(e.Deadline) {
|
||||
stats.OverdueCount++
|
||||
}
|
||||
}
|
||||
|
||||
if stats.TotalEnrollments > 0 {
|
||||
stats.CompletionRate = float64(completedCount) / float64(stats.TotalEnrollments) * 100.0
|
||||
}
|
||||
|
||||
return stats
|
||||
}
|
||||
371
admin-v2/ai-compliance-sdk/internal/gci/engine.go
Normal file
371
admin-v2/ai-compliance-sdk/internal/gci/engine.go
Normal file
@@ -0,0 +1,371 @@
|
||||
package gci
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Engine calculates the GCI score
|
||||
type Engine struct{}
|
||||
|
||||
// NewEngine creates a new GCI calculation engine
|
||||
func NewEngine() *Engine {
|
||||
return &Engine{}
|
||||
}
|
||||
|
||||
// Calculate computes the full GCI result for a tenant
|
||||
func (e *Engine) Calculate(tenantID string, profileID string) *GCIResult {
|
||||
now := time.Now()
|
||||
profile := GetProfile(profileID)
|
||||
auditTrail := []AuditEntry{}
|
||||
|
||||
// Step 1: Get module data (mock for now)
|
||||
modules := MockModuleData(tenantID)
|
||||
certDates := MockCertificateData()
|
||||
|
||||
// Step 2: Calculate Level 1 - Module Scores with validity
|
||||
for i := range modules {
|
||||
m := &modules[i]
|
||||
if m.Assigned > 0 {
|
||||
m.RawScore = float64(m.Completed) / float64(m.Assigned) * 100.0
|
||||
}
|
||||
// Apply validity factor
|
||||
if validUntil, ok := certDates[m.ModuleID]; ok {
|
||||
m.ValidityFactor = CalculateValidityFactor(validUntil, now)
|
||||
} else {
|
||||
m.ValidityFactor = 1.0 // No certificate tracking = assume valid
|
||||
}
|
||||
m.FinalScore = m.RawScore * m.ValidityFactor
|
||||
|
||||
if m.ValidityFactor < 1.0 {
|
||||
auditTrail = append(auditTrail, AuditEntry{
|
||||
Timestamp: now,
|
||||
Factor: "validity_decay",
|
||||
Description: fmt.Sprintf("Modul '%s': Gueltigkeitsfaktor %.2f (Zertifikat laeuft ab/abgelaufen)", m.ModuleName, m.ValidityFactor),
|
||||
Value: m.ValidityFactor,
|
||||
Impact: "negative",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: Calculate Level 2 - Risk-Weighted Scores per area
|
||||
areaModules := map[string][]ModuleScore{
|
||||
"dsgvo": {},
|
||||
"nis2": {},
|
||||
"iso27001": {},
|
||||
"ai_act": {},
|
||||
}
|
||||
for _, m := range modules {
|
||||
if _, ok := areaModules[m.Category]; ok {
|
||||
areaModules[m.Category] = append(areaModules[m.Category], m)
|
||||
}
|
||||
}
|
||||
|
||||
level2Areas := []RiskWeightedScore{}
|
||||
areaNames := map[string]string{
|
||||
"dsgvo": "DSGVO",
|
||||
"nis2": "NIS2",
|
||||
"iso27001": "ISO 27001",
|
||||
"ai_act": "EU AI Act",
|
||||
}
|
||||
|
||||
for areaID, mods := range areaModules {
|
||||
rws := RiskWeightedScore{
|
||||
AreaID: areaID,
|
||||
AreaName: areaNames[areaID],
|
||||
Modules: mods,
|
||||
}
|
||||
for _, m := range mods {
|
||||
rws.WeightedSum += m.FinalScore * m.RiskWeight
|
||||
rws.TotalWeight += m.RiskWeight
|
||||
}
|
||||
if rws.TotalWeight > 0 {
|
||||
rws.AreaScore = rws.WeightedSum / rws.TotalWeight
|
||||
}
|
||||
level2Areas = append(level2Areas, rws)
|
||||
}
|
||||
|
||||
// Step 4: Calculate Level 3 - Regulation Area Scores
|
||||
areaScores := []RegulationAreaScore{}
|
||||
for _, rws := range level2Areas {
|
||||
weight := profile.Weights[rws.AreaID]
|
||||
completedCount := 0
|
||||
for _, m := range rws.Modules {
|
||||
if m.Completed >= m.Assigned && m.Assigned > 0 {
|
||||
completedCount++
|
||||
}
|
||||
}
|
||||
ras := RegulationAreaScore{
|
||||
RegulationID: rws.AreaID,
|
||||
RegulationName: rws.AreaName,
|
||||
Score: math.Round(rws.AreaScore*100) / 100,
|
||||
Weight: weight,
|
||||
WeightedScore: rws.AreaScore * weight,
|
||||
ModuleCount: len(rws.Modules),
|
||||
CompletedCount: completedCount,
|
||||
}
|
||||
areaScores = append(areaScores, ras)
|
||||
|
||||
auditTrail = append(auditTrail, AuditEntry{
|
||||
Timestamp: now,
|
||||
Factor: "area_score",
|
||||
Description: fmt.Sprintf("Bereich '%s': Score %.1f, Gewicht %.0f%%", rws.AreaName, rws.AreaScore, weight*100),
|
||||
Value: rws.AreaScore,
|
||||
Impact: "neutral",
|
||||
})
|
||||
}
|
||||
|
||||
// Step 5: Calculate raw GCI
|
||||
rawGCI := 0.0
|
||||
totalWeight := 0.0
|
||||
for _, ras := range areaScores {
|
||||
rawGCI += ras.WeightedScore
|
||||
totalWeight += ras.Weight
|
||||
}
|
||||
if totalWeight > 0 {
|
||||
rawGCI = rawGCI / totalWeight
|
||||
}
|
||||
|
||||
// Step 6: Apply Criticality Multiplier
|
||||
criticalityMult := calculateCriticalityMultiplier(modules)
|
||||
auditTrail = append(auditTrail, AuditEntry{
|
||||
Timestamp: now,
|
||||
Factor: "criticality_multiplier",
|
||||
Description: fmt.Sprintf("Kritikalitaetsmultiplikator: %.3f", criticalityMult),
|
||||
Value: criticalityMult,
|
||||
Impact: func() string {
|
||||
if criticalityMult < 1.0 {
|
||||
return "negative"
|
||||
}
|
||||
return "neutral"
|
||||
}(),
|
||||
})
|
||||
|
||||
// Step 7: Apply Incident Adjustment
|
||||
openInc, critInc := MockIncidentData()
|
||||
incidentAdj := calculateIncidentAdjustment(openInc, critInc)
|
||||
auditTrail = append(auditTrail, AuditEntry{
|
||||
Timestamp: now,
|
||||
Factor: "incident_adjustment",
|
||||
Description: fmt.Sprintf("Vorfallsanpassung: %.3f (%d offen, %d kritisch)", incidentAdj, openInc, critInc),
|
||||
Value: incidentAdj,
|
||||
Impact: "negative",
|
||||
})
|
||||
|
||||
// Step 8: Final GCI
|
||||
finalGCI := rawGCI * criticalityMult * incidentAdj
|
||||
finalGCI = math.Max(0, math.Min(100, math.Round(finalGCI*10)/10))
|
||||
|
||||
// Step 9: Determine Maturity Level
|
||||
maturity := determineMaturityLevel(finalGCI)
|
||||
|
||||
auditTrail = append(auditTrail, AuditEntry{
|
||||
Timestamp: now,
|
||||
Factor: "final_gci",
|
||||
Description: fmt.Sprintf("GCI-Endergebnis: %.1f → Reifegrad: %s", finalGCI, MaturityLabels[maturity]),
|
||||
Value: finalGCI,
|
||||
Impact: "neutral",
|
||||
})
|
||||
|
||||
return &GCIResult{
|
||||
TenantID: tenantID,
|
||||
GCIScore: finalGCI,
|
||||
MaturityLevel: maturity,
|
||||
MaturityLabel: MaturityLabels[maturity],
|
||||
CalculatedAt: now,
|
||||
Profile: profileID,
|
||||
AreaScores: areaScores,
|
||||
CriticalityMult: criticalityMult,
|
||||
IncidentAdj: incidentAdj,
|
||||
AuditTrail: auditTrail,
|
||||
}
|
||||
}
|
||||
|
||||
// CalculateBreakdown returns the full 4-level breakdown
|
||||
func (e *Engine) CalculateBreakdown(tenantID string, profileID string) *GCIBreakdown {
|
||||
result := e.Calculate(tenantID, profileID)
|
||||
modules := MockModuleData(tenantID)
|
||||
certDates := MockCertificateData()
|
||||
now := time.Now()
|
||||
|
||||
// Recalculate module scores for the breakdown
|
||||
for i := range modules {
|
||||
m := &modules[i]
|
||||
if m.Assigned > 0 {
|
||||
m.RawScore = float64(m.Completed) / float64(m.Assigned) * 100.0
|
||||
}
|
||||
if validUntil, ok := certDates[m.ModuleID]; ok {
|
||||
m.ValidityFactor = CalculateValidityFactor(validUntil, now)
|
||||
} else {
|
||||
m.ValidityFactor = 1.0
|
||||
}
|
||||
m.FinalScore = m.RawScore * m.ValidityFactor
|
||||
}
|
||||
|
||||
// Build Level 2 areas
|
||||
areaModules := map[string][]ModuleScore{}
|
||||
for _, m := range modules {
|
||||
areaModules[m.Category] = append(areaModules[m.Category], m)
|
||||
}
|
||||
|
||||
areaNames := map[string]string{"dsgvo": "DSGVO", "nis2": "NIS2", "iso27001": "ISO 27001", "ai_act": "EU AI Act"}
|
||||
level2 := []RiskWeightedScore{}
|
||||
for areaID, mods := range areaModules {
|
||||
rws := RiskWeightedScore{AreaID: areaID, AreaName: areaNames[areaID], Modules: mods}
|
||||
for _, m := range mods {
|
||||
rws.WeightedSum += m.FinalScore * m.RiskWeight
|
||||
rws.TotalWeight += m.RiskWeight
|
||||
}
|
||||
if rws.TotalWeight > 0 {
|
||||
rws.AreaScore = rws.WeightedSum / rws.TotalWeight
|
||||
}
|
||||
level2 = append(level2, rws)
|
||||
}
|
||||
|
||||
return &GCIBreakdown{
|
||||
GCIResult: *result,
|
||||
Level1Modules: modules,
|
||||
Level2Areas: level2,
|
||||
}
|
||||
}
|
||||
|
||||
// GetHistory returns historical GCI snapshots
|
||||
func (e *Engine) GetHistory(tenantID string) []GCISnapshot {
|
||||
// Add current score to history
|
||||
result := e.Calculate(tenantID, "default")
|
||||
history := MockGCIHistory(tenantID)
|
||||
current := GCISnapshot{
|
||||
TenantID: tenantID,
|
||||
Score: result.GCIScore,
|
||||
MaturityLevel: result.MaturityLevel,
|
||||
AreaScores: make(map[string]float64),
|
||||
CalculatedAt: result.CalculatedAt,
|
||||
}
|
||||
for _, as := range result.AreaScores {
|
||||
current.AreaScores[as.RegulationID] = as.Score
|
||||
}
|
||||
history = append(history, current)
|
||||
return history
|
||||
}
|
||||
|
||||
// GetMatrix returns the compliance matrix (roles x regulations)
|
||||
func (e *Engine) GetMatrix(tenantID string) []ComplianceMatrixEntry {
|
||||
modules := MockModuleData(tenantID)
|
||||
|
||||
roles := []struct {
|
||||
ID string
|
||||
Name string
|
||||
}{
|
||||
{"management", "Geschaeftsfuehrung"},
|
||||
{"it_security", "IT-Sicherheit / CISO"},
|
||||
{"data_protection", "Datenschutz / DSB"},
|
||||
{"hr", "Personalwesen"},
|
||||
{"general", "Allgemeine Mitarbeiter"},
|
||||
}
|
||||
|
||||
// Define which modules are relevant per role
|
||||
roleModules := map[string][]string{
|
||||
"management": {"dsgvo-grundlagen", "nis2-management", "ai-governance", "iso-isms"},
|
||||
"it_security": {"nis2-risikomanagement", "nis2-incident-response", "iso-zugangssteuerung", "iso-kryptografie", "ai-hochrisiko"},
|
||||
"data_protection": {"dsgvo-grundlagen", "dsgvo-betroffenenrechte", "dsgvo-tom", "dsgvo-dsfa", "dsgvo-auftragsverarbeitung"},
|
||||
"hr": {"dsgvo-grundlagen", "dsgvo-betroffenenrechte", "nis2-management"},
|
||||
"general": {"dsgvo-grundlagen", "nis2-risikomanagement", "ai-risikokategorien", "ai-transparenz"},
|
||||
}
|
||||
|
||||
moduleMap := map[string]ModuleScore{}
|
||||
for _, m := range modules {
|
||||
moduleMap[m.ModuleID] = m
|
||||
}
|
||||
|
||||
entries := []ComplianceMatrixEntry{}
|
||||
for _, role := range roles {
|
||||
entry := ComplianceMatrixEntry{
|
||||
Role: role.ID,
|
||||
RoleName: role.Name,
|
||||
Regulations: map[string]float64{},
|
||||
}
|
||||
|
||||
regScores := map[string][]float64{}
|
||||
requiredModuleIDs := roleModules[role.ID]
|
||||
entry.RequiredModules = len(requiredModuleIDs)
|
||||
|
||||
for _, modID := range requiredModuleIDs {
|
||||
if m, ok := moduleMap[modID]; ok {
|
||||
score := 0.0
|
||||
if m.Assigned > 0 {
|
||||
score = float64(m.Completed) / float64(m.Assigned) * 100
|
||||
}
|
||||
regScores[m.Category] = append(regScores[m.Category], score)
|
||||
if m.Completed >= m.Assigned && m.Assigned > 0 {
|
||||
entry.CompletedModules++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
totalScore := 0.0
|
||||
count := 0
|
||||
for reg, scores := range regScores {
|
||||
sum := 0.0
|
||||
for _, s := range scores {
|
||||
sum += s
|
||||
}
|
||||
avg := sum / float64(len(scores))
|
||||
entry.Regulations[reg] = math.Round(avg*10) / 10
|
||||
totalScore += avg
|
||||
count++
|
||||
}
|
||||
if count > 0 {
|
||||
entry.OverallScore = math.Round(totalScore/float64(count)*10) / 10
|
||||
}
|
||||
|
||||
entries = append(entries, entry)
|
||||
}
|
||||
|
||||
return entries
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
func calculateCriticalityMultiplier(modules []ModuleScore) float64 {
|
||||
criticalModules := 0
|
||||
criticalLow := 0
|
||||
for _, m := range modules {
|
||||
if m.RiskWeight >= 2.5 {
|
||||
criticalModules++
|
||||
if m.FinalScore < 50 {
|
||||
criticalLow++
|
||||
}
|
||||
}
|
||||
}
|
||||
if criticalModules == 0 {
|
||||
return 1.0
|
||||
}
|
||||
// Reduce score if critical modules have low completion
|
||||
ratio := float64(criticalLow) / float64(criticalModules)
|
||||
return 1.0 - (ratio * 0.15) // max 15% reduction
|
||||
}
|
||||
|
||||
func calculateIncidentAdjustment(openIncidents, criticalIncidents int) float64 {
|
||||
adj := 1.0
|
||||
// Each open incident reduces by 1%
|
||||
adj -= float64(openIncidents) * 0.01
|
||||
// Each critical incident reduces by additional 3%
|
||||
adj -= float64(criticalIncidents) * 0.03
|
||||
return math.Max(0.8, adj) // minimum 80% (max 20% reduction)
|
||||
}
|
||||
|
||||
func determineMaturityLevel(score float64) string {
|
||||
switch {
|
||||
case score >= 90:
|
||||
return MaturityOptimized
|
||||
case score >= 75:
|
||||
return MaturityManaged
|
||||
case score >= 60:
|
||||
return MaturityDefined
|
||||
case score >= 40:
|
||||
return MaturityReactive
|
||||
default:
|
||||
return MaturityHighRisk
|
||||
}
|
||||
}
|
||||
188
admin-v2/ai-compliance-sdk/internal/gci/iso_gap_analysis.go
Normal file
188
admin-v2/ai-compliance-sdk/internal/gci/iso_gap_analysis.go
Normal file
@@ -0,0 +1,188 @@
|
||||
package gci
|
||||
|
||||
import "math"
|
||||
|
||||
// ISOGapAnalysis represents the complete ISO 27001 gap analysis
|
||||
type ISOGapAnalysis struct {
|
||||
TenantID string `json:"tenant_id"`
|
||||
TotalControls int `json:"total_controls"`
|
||||
CoveredFull int `json:"covered_full"`
|
||||
CoveredPartial int `json:"covered_partial"`
|
||||
NotCovered int `json:"not_covered"`
|
||||
CoveragePercent float64 `json:"coverage_percent"`
|
||||
CategorySummaries []ISOCategorySummary `json:"category_summaries"`
|
||||
ControlDetails []ISOControlDetail `json:"control_details"`
|
||||
Gaps []ISOGap `json:"gaps"`
|
||||
}
|
||||
|
||||
// ISOControlDetail shows coverage status for a single control
|
||||
type ISOControlDetail struct {
|
||||
Control ISOControl `json:"control"`
|
||||
CoverageLevel string `json:"coverage_level"` // full, partial, none
|
||||
CoveredBy []string `json:"covered_by"` // module IDs
|
||||
Score float64 `json:"score"` // 0-100
|
||||
}
|
||||
|
||||
// ISOGap represents an identified gap in ISO coverage
|
||||
type ISOGap struct {
|
||||
ControlID string `json:"control_id"`
|
||||
ControlName string `json:"control_name"`
|
||||
Category string `json:"category"`
|
||||
Priority string `json:"priority"` // high, medium, low
|
||||
Recommendation string `json:"recommendation"`
|
||||
}
|
||||
|
||||
// CalculateISOGapAnalysis performs the ISO 27001 gap analysis
|
||||
func CalculateISOGapAnalysis(tenantID string) *ISOGapAnalysis {
|
||||
modules := MockModuleData(tenantID)
|
||||
moduleMap := map[string]ModuleScore{}
|
||||
for _, m := range modules {
|
||||
moduleMap[m.ModuleID] = m
|
||||
}
|
||||
|
||||
// Build reverse mapping: control -> modules covering it
|
||||
controlCoverage := map[string][]string{}
|
||||
controlCoverageLevel := map[string]string{}
|
||||
for _, mapping := range DefaultISOModuleMappings {
|
||||
for _, controlID := range mapping.ISOControls {
|
||||
controlCoverage[controlID] = append(controlCoverage[controlID], mapping.ModuleID)
|
||||
// Use the highest coverage level
|
||||
existingLevel := controlCoverageLevel[controlID]
|
||||
if mapping.CoverageLevel == "full" || existingLevel == "" {
|
||||
controlCoverageLevel[controlID] = mapping.CoverageLevel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Analyze each control
|
||||
details := []ISOControlDetail{}
|
||||
gaps := []ISOGap{}
|
||||
coveredFull := 0
|
||||
coveredPartial := 0
|
||||
notCovered := 0
|
||||
|
||||
categoryCounts := map[string]*ISOCategorySummary{
|
||||
"A.5": {CategoryID: "A.5", CategoryName: "Organisatorische Massnahmen"},
|
||||
"A.6": {CategoryID: "A.6", CategoryName: "Personelle Massnahmen"},
|
||||
"A.7": {CategoryID: "A.7", CategoryName: "Physische Massnahmen"},
|
||||
"A.8": {CategoryID: "A.8", CategoryName: "Technologische Massnahmen"},
|
||||
}
|
||||
|
||||
for _, control := range ISOControls {
|
||||
coveredBy := controlCoverage[control.ID]
|
||||
level := controlCoverageLevel[control.ID]
|
||||
|
||||
if len(coveredBy) == 0 {
|
||||
level = "none"
|
||||
}
|
||||
|
||||
// Calculate score based on module completion
|
||||
score := 0.0
|
||||
if len(coveredBy) > 0 {
|
||||
scoreSum := 0.0
|
||||
count := 0
|
||||
for _, modID := range coveredBy {
|
||||
if m, ok := moduleMap[modID]; ok && m.Assigned > 0 {
|
||||
scoreSum += float64(m.Completed) / float64(m.Assigned) * 100
|
||||
count++
|
||||
}
|
||||
}
|
||||
if count > 0 {
|
||||
score = scoreSum / float64(count)
|
||||
}
|
||||
// Adjust for coverage level
|
||||
if level == "partial" {
|
||||
score *= 0.7 // partial coverage reduces effective score
|
||||
}
|
||||
}
|
||||
|
||||
detail := ISOControlDetail{
|
||||
Control: control,
|
||||
CoverageLevel: level,
|
||||
CoveredBy: coveredBy,
|
||||
Score: math.Round(score*10) / 10,
|
||||
}
|
||||
details = append(details, detail)
|
||||
|
||||
// Count by category
|
||||
cat := categoryCounts[control.CategoryID]
|
||||
if cat != nil {
|
||||
cat.TotalControls++
|
||||
switch level {
|
||||
case "full":
|
||||
coveredFull++
|
||||
cat.CoveredFull++
|
||||
case "partial":
|
||||
coveredPartial++
|
||||
cat.CoveredPartial++
|
||||
default:
|
||||
notCovered++
|
||||
cat.NotCovered++
|
||||
// Generate gap recommendation
|
||||
gap := ISOGap{
|
||||
ControlID: control.ID,
|
||||
ControlName: control.Name,
|
||||
Category: control.Category,
|
||||
Priority: determineGapPriority(control),
|
||||
Recommendation: generateGapRecommendation(control),
|
||||
}
|
||||
gaps = append(gaps, gap)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
totalControls := len(ISOControls)
|
||||
coveragePercent := 0.0
|
||||
if totalControls > 0 {
|
||||
coveragePercent = math.Round(float64(coveredFull+coveredPartial)/float64(totalControls)*100*10) / 10
|
||||
}
|
||||
|
||||
summaries := []ISOCategorySummary{}
|
||||
for _, catID := range []string{"A.5", "A.6", "A.7", "A.8"} {
|
||||
if cat, ok := categoryCounts[catID]; ok {
|
||||
summaries = append(summaries, *cat)
|
||||
}
|
||||
}
|
||||
|
||||
return &ISOGapAnalysis{
|
||||
TenantID: tenantID,
|
||||
TotalControls: totalControls,
|
||||
CoveredFull: coveredFull,
|
||||
CoveredPartial: coveredPartial,
|
||||
NotCovered: notCovered,
|
||||
CoveragePercent: coveragePercent,
|
||||
CategorySummaries: summaries,
|
||||
ControlDetails: details,
|
||||
Gaps: gaps,
|
||||
}
|
||||
}
|
||||
|
||||
func determineGapPriority(control ISOControl) string {
|
||||
// High priority for access, incident, and data protection controls
|
||||
highPriority := map[string]bool{
|
||||
"A.5.15": true, "A.5.17": true, "A.5.24": true, "A.5.26": true,
|
||||
"A.5.34": true, "A.8.2": true, "A.8.5": true, "A.8.7": true,
|
||||
"A.8.10": true, "A.8.20": true,
|
||||
}
|
||||
if highPriority[control.ID] {
|
||||
return "high"
|
||||
}
|
||||
// Medium for organizational and people controls
|
||||
if control.CategoryID == "A.5" || control.CategoryID == "A.6" {
|
||||
return "medium"
|
||||
}
|
||||
return "low"
|
||||
}
|
||||
|
||||
func generateGapRecommendation(control ISOControl) string {
|
||||
recommendations := map[string]string{
|
||||
"organizational": "Erstellen Sie eine Richtlinie und weisen Sie Verantwortlichkeiten zu fuer: " + control.Name,
|
||||
"people": "Implementieren Sie Schulungen und Prozesse fuer: " + control.Name,
|
||||
"physical": "Definieren Sie physische Sicherheitsmassnahmen fuer: " + control.Name,
|
||||
"technological": "Implementieren Sie technische Kontrollen fuer: " + control.Name,
|
||||
}
|
||||
if rec, ok := recommendations[control.Category]; ok {
|
||||
return rec
|
||||
}
|
||||
return "Massnahmen implementieren fuer: " + control.Name
|
||||
}
|
||||
207
admin-v2/ai-compliance-sdk/internal/gci/iso_mapping.go
Normal file
207
admin-v2/ai-compliance-sdk/internal/gci/iso_mapping.go
Normal file
@@ -0,0 +1,207 @@
|
||||
package gci
|
||||
|
||||
// ISOControl represents an ISO 27001:2022 Annex A control
|
||||
type ISOControl struct {
|
||||
ID string `json:"id"` // e.g. "A.5.1"
|
||||
Name string `json:"name"`
|
||||
Category string `json:"category"` // organizational, people, physical, technological
|
||||
CategoryID string `json:"category_id"` // A.5, A.6, A.7, A.8
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
// ISOModuleMapping maps a course/module to ISO controls
|
||||
type ISOModuleMapping struct {
|
||||
ModuleID string `json:"module_id"`
|
||||
ModuleName string `json:"module_name"`
|
||||
ISOControls []string `json:"iso_controls"` // control IDs
|
||||
CoverageLevel string `json:"coverage_level"` // full, partial, none
|
||||
}
|
||||
|
||||
// ISO 27001:2022 Annex A controls (representative selection)
|
||||
var ISOControls = []ISOControl{
|
||||
// A.5 Organizational Controls (37 controls, showing key ones)
|
||||
{ID: "A.5.1", Name: "Informationssicherheitsrichtlinien", Category: "organizational", CategoryID: "A.5", Description: "Informationssicherheitsleitlinie und themenspezifische Richtlinien"},
|
||||
{ID: "A.5.2", Name: "Rollen und Verantwortlichkeiten", Category: "organizational", CategoryID: "A.5", Description: "Definition und Zuweisung von Informationssicherheitsrollen"},
|
||||
{ID: "A.5.3", Name: "Aufgabentrennung", Category: "organizational", CategoryID: "A.5", Description: "Trennung von konfligierenden Aufgaben und Verantwortlichkeiten"},
|
||||
{ID: "A.5.4", Name: "Managementverantwortung", Category: "organizational", CategoryID: "A.5", Description: "Fuehrungskraefte muessen Sicherheitsrichtlinien einhalten und durchsetzen"},
|
||||
{ID: "A.5.5", Name: "Kontakt mit Behoerden", Category: "organizational", CategoryID: "A.5", Description: "Pflege von Kontakten zu relevanten Aufsichtsbehoerden"},
|
||||
{ID: "A.5.6", Name: "Kontakt mit Interessengruppen", Category: "organizational", CategoryID: "A.5", Description: "Kontakt zu Fachgruppen und Sicherheitsforen"},
|
||||
{ID: "A.5.7", Name: "Bedrohungsintelligenz", Category: "organizational", CategoryID: "A.5", Description: "Sammlung und Analyse von Bedrohungsinformationen"},
|
||||
{ID: "A.5.8", Name: "Informationssicherheit im Projektmanagement", Category: "organizational", CategoryID: "A.5", Description: "Integration von Sicherheit in Projektmanagement"},
|
||||
{ID: "A.5.9", Name: "Inventar der Informationswerte", Category: "organizational", CategoryID: "A.5", Description: "Inventarisierung und Verwaltung von Informationswerten"},
|
||||
{ID: "A.5.10", Name: "Zuleassige Nutzung", Category: "organizational", CategoryID: "A.5", Description: "Regeln fuer die zuleassige Nutzung von Informationswerten"},
|
||||
{ID: "A.5.11", Name: "Rueckgabe von Werten", Category: "organizational", CategoryID: "A.5", Description: "Rueckgabe von Werten bei Beendigung"},
|
||||
{ID: "A.5.12", Name: "Klassifizierung von Informationen", Category: "organizational", CategoryID: "A.5", Description: "Klassifizierungsschema fuer Informationen"},
|
||||
{ID: "A.5.13", Name: "Kennzeichnung von Informationen", Category: "organizational", CategoryID: "A.5", Description: "Kennzeichnung gemaess Klassifizierung"},
|
||||
{ID: "A.5.14", Name: "Informationsuebertragung", Category: "organizational", CategoryID: "A.5", Description: "Regeln fuer sichere Informationsuebertragung"},
|
||||
{ID: "A.5.15", Name: "Zugangssteuerung", Category: "organizational", CategoryID: "A.5", Description: "Zugangssteuerungsrichtlinie"},
|
||||
{ID: "A.5.16", Name: "Identitaetsmanagement", Category: "organizational", CategoryID: "A.5", Description: "Verwaltung des Lebenszyklus von Identitaeten"},
|
||||
{ID: "A.5.17", Name: "Authentifizierungsinformationen", Category: "organizational", CategoryID: "A.5", Description: "Verwaltung von Authentifizierungsinformationen"},
|
||||
{ID: "A.5.18", Name: "Zugriffsrechte", Category: "organizational", CategoryID: "A.5", Description: "Vergabe, Pruefung und Entzug von Zugriffsrechten"},
|
||||
{ID: "A.5.19", Name: "Informationssicherheit in Lieferantenbeziehungen", Category: "organizational", CategoryID: "A.5", Description: "Sicherheitsanforderungen an Lieferanten"},
|
||||
{ID: "A.5.20", Name: "Informationssicherheit in Lieferantenvereinbarungen", Category: "organizational", CategoryID: "A.5", Description: "Sicherheitsklauseln in Vertraegen"},
|
||||
{ID: "A.5.21", Name: "IKT-Lieferkette", Category: "organizational", CategoryID: "A.5", Description: "Management der IKT-Lieferkette"},
|
||||
{ID: "A.5.22", Name: "Ueberwachung von Lieferantenservices", Category: "organizational", CategoryID: "A.5", Description: "Ueberwachung und Pruefung von Lieferantenservices"},
|
||||
{ID: "A.5.23", Name: "Cloud-Sicherheit", Category: "organizational", CategoryID: "A.5", Description: "Informationssicherheit fuer Cloud-Dienste"},
|
||||
{ID: "A.5.24", Name: "Vorfallsmanagement - Planung", Category: "organizational", CategoryID: "A.5", Description: "Planung und Vorbereitung des Vorfallsmanagements"},
|
||||
{ID: "A.5.25", Name: "Vorfallsbeurteilung", Category: "organizational", CategoryID: "A.5", Description: "Beurteilung und Entscheidung ueber Sicherheitsereignisse"},
|
||||
{ID: "A.5.26", Name: "Vorfallsreaktion", Category: "organizational", CategoryID: "A.5", Description: "Reaktion auf Sicherheitsvorfaelle"},
|
||||
{ID: "A.5.27", Name: "Aus Vorfaellen lernen", Category: "organizational", CategoryID: "A.5", Description: "Lessons Learned aus Sicherheitsvorfaellen"},
|
||||
{ID: "A.5.28", Name: "Beweissicherung", Category: "organizational", CategoryID: "A.5", Description: "Identifikation und Sicherung von Beweisen"},
|
||||
{ID: "A.5.29", Name: "Informationssicherheit bei Stoerungen", Category: "organizational", CategoryID: "A.5", Description: "Sicherheit waehrend Stoerungen und Krisen"},
|
||||
{ID: "A.5.30", Name: "IKT-Bereitschaft fuer Business Continuity", Category: "organizational", CategoryID: "A.5", Description: "IKT-Bereitschaft zur Unterstuetzung der Geschaeftskontinuitaet"},
|
||||
{ID: "A.5.31", Name: "Rechtliche Anforderungen", Category: "organizational", CategoryID: "A.5", Description: "Einhaltung rechtlicher und vertraglicher Anforderungen"},
|
||||
{ID: "A.5.32", Name: "Geistige Eigentumsrechte", Category: "organizational", CategoryID: "A.5", Description: "Schutz geistigen Eigentums"},
|
||||
{ID: "A.5.33", Name: "Schutz von Aufzeichnungen", Category: "organizational", CategoryID: "A.5", Description: "Schutz von Aufzeichnungen vor Verlust und Manipulation"},
|
||||
{ID: "A.5.34", Name: "Datenschutz und PII", Category: "organizational", CategoryID: "A.5", Description: "Datenschutz und Schutz personenbezogener Daten"},
|
||||
{ID: "A.5.35", Name: "Unabhaengige Ueberpruefung", Category: "organizational", CategoryID: "A.5", Description: "Unabhaengige Ueberpruefung der Informationssicherheit"},
|
||||
{ID: "A.5.36", Name: "Richtlinienkonformitaet", Category: "organizational", CategoryID: "A.5", Description: "Einhaltung von Richtlinien und Standards"},
|
||||
{ID: "A.5.37", Name: "Dokumentierte Betriebsverfahren", Category: "organizational", CategoryID: "A.5", Description: "Dokumentation von Betriebsverfahren"},
|
||||
|
||||
// A.6 People Controls (8 controls)
|
||||
{ID: "A.6.1", Name: "Ueberpruefen", Category: "people", CategoryID: "A.6", Description: "Hintergrundpruefungen vor der Einstellung"},
|
||||
{ID: "A.6.2", Name: "Beschaeftigungsbedingungen", Category: "people", CategoryID: "A.6", Description: "Sicherheitsanforderungen in Arbeitsvertraegen"},
|
||||
{ID: "A.6.3", Name: "Sensibilisierung und Schulung", Category: "people", CategoryID: "A.6", Description: "Awareness-Programme und Schulungen"},
|
||||
{ID: "A.6.4", Name: "Disziplinarverfahren", Category: "people", CategoryID: "A.6", Description: "Formales Disziplinarverfahren"},
|
||||
{ID: "A.6.5", Name: "Verantwortlichkeiten nach Beendigung", Category: "people", CategoryID: "A.6", Description: "Sicherheitspflichten nach Beendigung des Beschaeftigungsverhaeltnisses"},
|
||||
{ID: "A.6.6", Name: "Vertraulichkeitsvereinbarungen", Category: "people", CategoryID: "A.6", Description: "Vertraulichkeits- und Geheimhaltungsvereinbarungen"},
|
||||
{ID: "A.6.7", Name: "Remote-Arbeit", Category: "people", CategoryID: "A.6", Description: "Sicherheitsmassnahmen fuer Remote-Arbeit"},
|
||||
{ID: "A.6.8", Name: "Meldung von Sicherheitsereignissen", Category: "people", CategoryID: "A.6", Description: "Mechanismen zur Meldung von Sicherheitsereignissen"},
|
||||
|
||||
// A.7 Physical Controls (14 controls, showing key ones)
|
||||
{ID: "A.7.1", Name: "Physische Sicherheitsperimeter", Category: "physical", CategoryID: "A.7", Description: "Definition physischer Sicherheitszonen"},
|
||||
{ID: "A.7.2", Name: "Physischer Zutritt", Category: "physical", CategoryID: "A.7", Description: "Zutrittskontrolle zu Sicherheitszonen"},
|
||||
{ID: "A.7.3", Name: "Sicherung von Bueros und Raeumen", Category: "physical", CategoryID: "A.7", Description: "Physische Sicherheit fuer Bueros und Raeume"},
|
||||
{ID: "A.7.4", Name: "Physische Sicherheitsueberwachung", Category: "physical", CategoryID: "A.7", Description: "Ueberwachung physischer Sicherheit"},
|
||||
{ID: "A.7.5", Name: "Schutz vor Umweltgefahren", Category: "physical", CategoryID: "A.7", Description: "Schutz gegen natuerliche und menschgemachte Gefahren"},
|
||||
{ID: "A.7.6", Name: "Arbeit in Sicherheitszonen", Category: "physical", CategoryID: "A.7", Description: "Regeln fuer das Arbeiten in Sicherheitszonen"},
|
||||
{ID: "A.7.7", Name: "Aufgeraemter Schreibtisch", Category: "physical", CategoryID: "A.7", Description: "Clean-Desk und Clear-Screen Richtlinie"},
|
||||
{ID: "A.7.8", Name: "Geraeteplatzierung", Category: "physical", CategoryID: "A.7", Description: "Platzierung und Schutz von Geraeten"},
|
||||
{ID: "A.7.9", Name: "Sicherheit von Geraeten ausserhalb", Category: "physical", CategoryID: "A.7", Description: "Sicherheit von Geraeten ausserhalb der Raeumlichkeiten"},
|
||||
{ID: "A.7.10", Name: "Speichermedien", Category: "physical", CategoryID: "A.7", Description: "Verwaltung von Speichermedien"},
|
||||
{ID: "A.7.11", Name: "Versorgungseinrichtungen", Category: "physical", CategoryID: "A.7", Description: "Schutz vor Ausfaellen der Versorgungseinrichtungen"},
|
||||
{ID: "A.7.12", Name: "Verkabelungssicherheit", Category: "physical", CategoryID: "A.7", Description: "Schutz der Verkabelung"},
|
||||
{ID: "A.7.13", Name: "Instandhaltung von Geraeten", Category: "physical", CategoryID: "A.7", Description: "Korrekte Instandhaltung von Geraeten"},
|
||||
{ID: "A.7.14", Name: "Sichere Entsorgung", Category: "physical", CategoryID: "A.7", Description: "Sichere Entsorgung oder Wiederverwendung"},
|
||||
|
||||
// A.8 Technological Controls (34 controls, showing key ones)
|
||||
{ID: "A.8.1", Name: "Endbenutzergeraete", Category: "technological", CategoryID: "A.8", Description: "Sicherheit von Endbenutzergeraeten"},
|
||||
{ID: "A.8.2", Name: "Privilegierte Zugriffsrechte", Category: "technological", CategoryID: "A.8", Description: "Verwaltung privilegierter Zugriffsrechte"},
|
||||
{ID: "A.8.3", Name: "Informationszugangsbeschraenkung", Category: "technological", CategoryID: "A.8", Description: "Beschraenkung des Zugangs zu Informationen"},
|
||||
{ID: "A.8.4", Name: "Zugang zu Quellcode", Category: "technological", CategoryID: "A.8", Description: "Sicherer Zugang zu Quellcode"},
|
||||
{ID: "A.8.5", Name: "Sichere Authentifizierung", Category: "technological", CategoryID: "A.8", Description: "Sichere Authentifizierungstechnologien"},
|
||||
{ID: "A.8.6", Name: "Kapazitaetsmanagement", Category: "technological", CategoryID: "A.8", Description: "Ueberwachung und Anpassung der Kapazitaet"},
|
||||
{ID: "A.8.7", Name: "Schutz gegen Malware", Category: "technological", CategoryID: "A.8", Description: "Schutz vor Schadprogrammen"},
|
||||
{ID: "A.8.8", Name: "Management technischer Schwachstellen", Category: "technological", CategoryID: "A.8", Description: "Identifikation und Behebung von Schwachstellen"},
|
||||
{ID: "A.8.9", Name: "Konfigurationsmanagement", Category: "technological", CategoryID: "A.8", Description: "Sichere Konfiguration von Systemen"},
|
||||
{ID: "A.8.10", Name: "Datensicherung", Category: "technological", CategoryID: "A.8", Description: "Erstellen und Testen von Datensicherungen"},
|
||||
{ID: "A.8.11", Name: "Datenredundanz", Category: "technological", CategoryID: "A.8", Description: "Redundanz von Informationsverarbeitungseinrichtungen"},
|
||||
{ID: "A.8.12", Name: "Protokollierung", Category: "technological", CategoryID: "A.8", Description: "Aufzeichnung und Ueberwachung von Aktivitaeten"},
|
||||
{ID: "A.8.13", Name: "Ueberwachung von Aktivitaeten", Category: "technological", CategoryID: "A.8", Description: "Ueberwachung von Netzwerken und Systemen"},
|
||||
{ID: "A.8.14", Name: "Zeitsynchronisation", Category: "technological", CategoryID: "A.8", Description: "Synchronisation von Uhren"},
|
||||
{ID: "A.8.15", Name: "Nutzung privilegierter Hilfsprogramme", Category: "technological", CategoryID: "A.8", Description: "Einschraenkung privilegierter Hilfsprogramme"},
|
||||
{ID: "A.8.16", Name: "Softwareinstallation", Category: "technological", CategoryID: "A.8", Description: "Kontrolle der Softwareinstallation"},
|
||||
{ID: "A.8.17", Name: "Netzwerksicherheit", Category: "technological", CategoryID: "A.8", Description: "Sicherheit von Netzwerken"},
|
||||
{ID: "A.8.18", Name: "Netzwerksegmentierung", Category: "technological", CategoryID: "A.8", Description: "Segmentierung von Netzwerken"},
|
||||
{ID: "A.8.19", Name: "Webfilterung", Category: "technological", CategoryID: "A.8", Description: "Filterung des Webzugangs"},
|
||||
{ID: "A.8.20", Name: "Kryptografie", Category: "technological", CategoryID: "A.8", Description: "Einsatz kryptografischer Massnahmen"},
|
||||
{ID: "A.8.21", Name: "Sichere Entwicklung", Category: "technological", CategoryID: "A.8", Description: "Sichere Entwicklungslebenszyklus"},
|
||||
{ID: "A.8.22", Name: "Sicherheitsanforderungen bei Applikationen", Category: "technological", CategoryID: "A.8", Description: "Sicherheitsanforderungen bei Anwendungen"},
|
||||
{ID: "A.8.23", Name: "Sichere Systemarchitektur", Category: "technological", CategoryID: "A.8", Description: "Sicherheitsprinzipien in der Systemarchitektur"},
|
||||
{ID: "A.8.24", Name: "Sicheres Programmieren", Category: "technological", CategoryID: "A.8", Description: "Sichere Programmierpraktiken"},
|
||||
{ID: "A.8.25", Name: "Sicherheitstests", Category: "technological", CategoryID: "A.8", Description: "Sicherheitstests in der Entwicklung und Abnahme"},
|
||||
{ID: "A.8.26", Name: "Auslagerung der Entwicklung", Category: "technological", CategoryID: "A.8", Description: "Ueberwachung ausgelagerter Entwicklung"},
|
||||
{ID: "A.8.27", Name: "Trennung von Umgebungen", Category: "technological", CategoryID: "A.8", Description: "Trennung von Entwicklungs-, Test- und Produktionsumgebungen"},
|
||||
{ID: "A.8.28", Name: "Aenderungsmanagement", Category: "technological", CategoryID: "A.8", Description: "Formales Aenderungsmanagement"},
|
||||
{ID: "A.8.29", Name: "Sicherheitstests in der Abnahme", Category: "technological", CategoryID: "A.8", Description: "Durchfuehrung von Sicherheitstests vor Abnahme"},
|
||||
{ID: "A.8.30", Name: "Datenloeschung", Category: "technological", CategoryID: "A.8", Description: "Sichere Datenloeschung"},
|
||||
{ID: "A.8.31", Name: "Datenmaskierung", Category: "technological", CategoryID: "A.8", Description: "Techniken zur Datenmaskierung"},
|
||||
{ID: "A.8.32", Name: "Verhinderung von Datenverlust", Category: "technological", CategoryID: "A.8", Description: "DLP-Massnahmen"},
|
||||
{ID: "A.8.33", Name: "Testinformationen", Category: "technological", CategoryID: "A.8", Description: "Schutz von Testinformationen"},
|
||||
{ID: "A.8.34", Name: "Audit-Informationssysteme", Category: "technological", CategoryID: "A.8", Description: "Schutz von Audit-Tools und -systemen"},
|
||||
}
|
||||
|
||||
// Default mappings: which modules cover which ISO controls
|
||||
var DefaultISOModuleMappings = []ISOModuleMapping{
|
||||
{
|
||||
ModuleID: "iso-isms", ModuleName: "ISMS Grundlagen",
|
||||
ISOControls: []string{"A.5.1", "A.5.2", "A.5.3", "A.5.4", "A.5.35", "A.5.36"},
|
||||
CoverageLevel: "full",
|
||||
},
|
||||
{
|
||||
ModuleID: "iso-risikobewertung", ModuleName: "Risikobewertung",
|
||||
ISOControls: []string{"A.5.7", "A.5.8", "A.5.9", "A.5.10", "A.5.12", "A.5.13"},
|
||||
CoverageLevel: "full",
|
||||
},
|
||||
{
|
||||
ModuleID: "iso-zugangssteuerung", ModuleName: "Zugangssteuerung",
|
||||
ISOControls: []string{"A.5.15", "A.5.16", "A.5.17", "A.5.18", "A.8.2", "A.8.3", "A.8.5"},
|
||||
CoverageLevel: "full",
|
||||
},
|
||||
{
|
||||
ModuleID: "iso-kryptografie", ModuleName: "Kryptografie",
|
||||
ISOControls: []string{"A.8.20", "A.8.21", "A.8.24"},
|
||||
CoverageLevel: "partial",
|
||||
},
|
||||
{
|
||||
ModuleID: "iso-physisch", ModuleName: "Physische Sicherheit",
|
||||
ISOControls: []string{"A.7.1", "A.7.2", "A.7.3", "A.7.4", "A.7.5", "A.7.7", "A.7.8"},
|
||||
CoverageLevel: "full",
|
||||
},
|
||||
{
|
||||
ModuleID: "dsgvo-tom", ModuleName: "Technisch-Organisatorische Massnahmen",
|
||||
ISOControls: []string{"A.5.34", "A.8.10", "A.8.12", "A.8.30", "A.8.31"},
|
||||
CoverageLevel: "partial",
|
||||
},
|
||||
{
|
||||
ModuleID: "nis2-incident-response", ModuleName: "NIS2 Incident Response",
|
||||
ISOControls: []string{"A.5.24", "A.5.25", "A.5.26", "A.5.27", "A.5.28", "A.6.8"},
|
||||
CoverageLevel: "full",
|
||||
},
|
||||
{
|
||||
ModuleID: "nis2-supply-chain", ModuleName: "NIS2 Lieferkettensicherheit",
|
||||
ISOControls: []string{"A.5.19", "A.5.20", "A.5.21", "A.5.22", "A.5.23"},
|
||||
CoverageLevel: "full",
|
||||
},
|
||||
{
|
||||
ModuleID: "nis2-risikomanagement", ModuleName: "NIS2 Risikomanagement",
|
||||
ISOControls: []string{"A.5.29", "A.5.30", "A.8.6", "A.8.7", "A.8.8", "A.8.9"},
|
||||
CoverageLevel: "partial",
|
||||
},
|
||||
{
|
||||
ModuleID: "dsgvo-grundlagen", ModuleName: "DSGVO Grundlagen",
|
||||
ISOControls: []string{"A.5.31", "A.5.34", "A.6.2", "A.6.3"},
|
||||
CoverageLevel: "partial",
|
||||
},
|
||||
}
|
||||
|
||||
// GetISOControlByID returns a control by its ID
|
||||
func GetISOControlByID(id string) (ISOControl, bool) {
|
||||
for _, c := range ISOControls {
|
||||
if c.ID == id {
|
||||
return c, true
|
||||
}
|
||||
}
|
||||
return ISOControl{}, false
|
||||
}
|
||||
|
||||
// GetISOControlsByCategory returns all controls in a category
|
||||
func GetISOControlsByCategory(categoryID string) []ISOControl {
|
||||
var result []ISOControl
|
||||
for _, c := range ISOControls {
|
||||
if c.CategoryID == categoryID {
|
||||
result = append(result, c)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ISOCategorySummary provides a summary per ISO category
|
||||
type ISOCategorySummary struct {
|
||||
CategoryID string `json:"category_id"`
|
||||
CategoryName string `json:"category_name"`
|
||||
TotalControls int `json:"total_controls"`
|
||||
CoveredFull int `json:"covered_full"`
|
||||
CoveredPartial int `json:"covered_partial"`
|
||||
NotCovered int `json:"not_covered"`
|
||||
}
|
||||
74
admin-v2/ai-compliance-sdk/internal/gci/mock_data.go
Normal file
74
admin-v2/ai-compliance-sdk/internal/gci/mock_data.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package gci
|
||||
|
||||
import "time"
|
||||
|
||||
// MockModuleData provides fallback data when academy store is empty
|
||||
func MockModuleData(tenantID string) []ModuleScore {
|
||||
return []ModuleScore{
|
||||
// DSGVO modules
|
||||
{ModuleID: "dsgvo-grundlagen", ModuleName: "DSGVO Grundlagen", Assigned: 25, Completed: 22, Category: "dsgvo", RiskWeight: 2.0},
|
||||
{ModuleID: "dsgvo-betroffenenrechte", ModuleName: "Betroffenenrechte", Assigned: 25, Completed: 18, Category: "dsgvo", RiskWeight: 2.5},
|
||||
{ModuleID: "dsgvo-tom", ModuleName: "Technisch-Organisatorische Massnahmen", Assigned: 20, Completed: 17, Category: "dsgvo", RiskWeight: 2.5},
|
||||
{ModuleID: "dsgvo-dsfa", ModuleName: "Datenschutz-Folgenabschaetzung", Assigned: 15, Completed: 10, Category: "dsgvo", RiskWeight: 2.0},
|
||||
{ModuleID: "dsgvo-auftragsverarbeitung", ModuleName: "Auftragsverarbeitung", Assigned: 20, Completed: 16, Category: "dsgvo", RiskWeight: 2.0},
|
||||
|
||||
// NIS2 modules
|
||||
{ModuleID: "nis2-risikomanagement", ModuleName: "NIS2 Risikomanagement", Assigned: 15, Completed: 11, Category: "nis2", RiskWeight: 3.0},
|
||||
{ModuleID: "nis2-incident-response", ModuleName: "NIS2 Incident Response", Assigned: 15, Completed: 9, Category: "nis2", RiskWeight: 3.0},
|
||||
{ModuleID: "nis2-supply-chain", ModuleName: "NIS2 Lieferkettensicherheit", Assigned: 10, Completed: 6, Category: "nis2", RiskWeight: 2.0},
|
||||
{ModuleID: "nis2-management", ModuleName: "NIS2 Geschaeftsleitungspflicht", Assigned: 10, Completed: 8, Category: "nis2", RiskWeight: 3.0},
|
||||
|
||||
// ISO 27001 modules
|
||||
{ModuleID: "iso-isms", ModuleName: "ISMS Grundlagen", Assigned: 20, Completed: 16, Category: "iso27001", RiskWeight: 2.0},
|
||||
{ModuleID: "iso-risikobewertung", ModuleName: "Risikobewertung", Assigned: 15, Completed: 12, Category: "iso27001", RiskWeight: 2.0},
|
||||
{ModuleID: "iso-zugangssteuerung", ModuleName: "Zugangssteuerung", Assigned: 20, Completed: 18, Category: "iso27001", RiskWeight: 2.0},
|
||||
{ModuleID: "iso-kryptografie", ModuleName: "Kryptografie", Assigned: 10, Completed: 7, Category: "iso27001", RiskWeight: 1.5},
|
||||
{ModuleID: "iso-physisch", ModuleName: "Physische Sicherheit", Assigned: 10, Completed: 9, Category: "iso27001", RiskWeight: 1.0},
|
||||
|
||||
// AI Act modules
|
||||
{ModuleID: "ai-risikokategorien", ModuleName: "KI-Risikokategorien", Assigned: 15, Completed: 12, Category: "ai_act", RiskWeight: 2.5},
|
||||
{ModuleID: "ai-transparenz", ModuleName: "KI-Transparenzpflichten", Assigned: 15, Completed: 10, Category: "ai_act", RiskWeight: 2.0},
|
||||
{ModuleID: "ai-hochrisiko", ModuleName: "Hochrisiko-KI-Systeme", Assigned: 10, Completed: 6, Category: "ai_act", RiskWeight: 2.5},
|
||||
{ModuleID: "ai-governance", ModuleName: "KI-Governance", Assigned: 10, Completed: 7, Category: "ai_act", RiskWeight: 2.0},
|
||||
}
|
||||
}
|
||||
|
||||
// MockCertificateData provides mock certificate validity dates
|
||||
func MockCertificateData() map[string]time.Time {
|
||||
now := time.Now()
|
||||
return map[string]time.Time{
|
||||
"dsgvo-grundlagen": now.AddDate(0, 8, 0), // valid 8 months
|
||||
"dsgvo-betroffenenrechte": now.AddDate(0, 3, 0), // expiring in 3 months
|
||||
"dsgvo-tom": now.AddDate(0, 10, 0), // valid
|
||||
"dsgvo-dsfa": now.AddDate(0, -1, 0), // expired 1 month ago
|
||||
"dsgvo-auftragsverarbeitung": now.AddDate(0, 6, 0),
|
||||
"nis2-risikomanagement": now.AddDate(0, 5, 0),
|
||||
"nis2-incident-response": now.AddDate(0, 2, 0), // expiring soon
|
||||
"nis2-supply-chain": now.AddDate(0, -2, 0), // expired 2 months
|
||||
"nis2-management": now.AddDate(0, 9, 0),
|
||||
"iso-isms": now.AddDate(1, 0, 0),
|
||||
"iso-risikobewertung": now.AddDate(0, 4, 0),
|
||||
"iso-zugangssteuerung": now.AddDate(0, 11, 0),
|
||||
"iso-kryptografie": now.AddDate(0, 1, 0), // expiring in 1 month
|
||||
"iso-physisch": now.AddDate(0, 7, 0),
|
||||
"ai-risikokategorien": now.AddDate(0, 6, 0),
|
||||
"ai-transparenz": now.AddDate(0, 3, 0),
|
||||
"ai-hochrisiko": now.AddDate(0, -3, 0), // expired 3 months
|
||||
"ai-governance": now.AddDate(0, 5, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// MockIncidentData returns mock incident counts for adjustment
|
||||
func MockIncidentData() (openIncidents int, criticalIncidents int) {
|
||||
return 3, 1
|
||||
}
|
||||
|
||||
// MockGCIHistory returns mock historical GCI snapshots
|
||||
func MockGCIHistory(tenantID string) []GCISnapshot {
|
||||
now := time.Now()
|
||||
return []GCISnapshot{
|
||||
{TenantID: tenantID, Score: 58.2, MaturityLevel: MaturityReactive, AreaScores: map[string]float64{"dsgvo": 62, "nis2": 48, "iso27001": 60, "ai_act": 55}, CalculatedAt: now.AddDate(0, -3, 0)},
|
||||
{TenantID: tenantID, Score: 62.5, MaturityLevel: MaturityDefined, AreaScores: map[string]float64{"dsgvo": 65, "nis2": 55, "iso27001": 63, "ai_act": 58}, CalculatedAt: now.AddDate(0, -2, 0)},
|
||||
{TenantID: tenantID, Score: 67.8, MaturityLevel: MaturityDefined, AreaScores: map[string]float64{"dsgvo": 70, "nis2": 60, "iso27001": 68, "ai_act": 62}, CalculatedAt: now.AddDate(0, -1, 0)},
|
||||
}
|
||||
}
|
||||
104
admin-v2/ai-compliance-sdk/internal/gci/models.go
Normal file
104
admin-v2/ai-compliance-sdk/internal/gci/models.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package gci
|
||||
|
||||
import "time"
|
||||
|
||||
// Level 1: Module Score
|
||||
type ModuleScore struct {
|
||||
ModuleID string `json:"module_id"`
|
||||
ModuleName string `json:"module_name"`
|
||||
Assigned int `json:"assigned"`
|
||||
Completed int `json:"completed"`
|
||||
RawScore float64 `json:"raw_score"` // completions/assigned
|
||||
ValidityFactor float64 `json:"validity_factor"` // 0.0-1.0
|
||||
FinalScore float64 `json:"final_score"` // RawScore * ValidityFactor
|
||||
RiskWeight float64 `json:"risk_weight"` // module criticality weight
|
||||
Category string `json:"category"` // dsgvo, nis2, iso27001, ai_act
|
||||
}
|
||||
|
||||
// Level 2: Risk-weighted Module Score per regulation area
|
||||
type RiskWeightedScore struct {
|
||||
AreaID string `json:"area_id"`
|
||||
AreaName string `json:"area_name"`
|
||||
Modules []ModuleScore `json:"modules"`
|
||||
WeightedSum float64 `json:"weighted_sum"`
|
||||
TotalWeight float64 `json:"total_weight"`
|
||||
AreaScore float64 `json:"area_score"` // WeightedSum / TotalWeight
|
||||
}
|
||||
|
||||
// Level 3: Regulation Area Score
|
||||
type RegulationAreaScore struct {
|
||||
RegulationID string `json:"regulation_id"` // dsgvo, nis2, iso27001, ai_act
|
||||
RegulationName string `json:"regulation_name"` // Display name
|
||||
Score float64 `json:"score"` // 0-100
|
||||
Weight float64 `json:"weight"` // regulation weight in GCI
|
||||
WeightedScore float64 `json:"weighted_score"` // Score * Weight
|
||||
ModuleCount int `json:"module_count"`
|
||||
CompletedCount int `json:"completed_count"`
|
||||
}
|
||||
|
||||
// Level 4: GCI Result
|
||||
type GCIResult struct {
|
||||
TenantID string `json:"tenant_id"`
|
||||
GCIScore float64 `json:"gci_score"` // 0-100
|
||||
MaturityLevel string `json:"maturity_level"` // Optimized, Managed, Defined, Reactive, HighRisk
|
||||
MaturityLabel string `json:"maturity_label"` // German label
|
||||
CalculatedAt time.Time `json:"calculated_at"`
|
||||
Profile string `json:"profile"` // default, nis2_relevant, ki_nutzer
|
||||
AreaScores []RegulationAreaScore `json:"area_scores"`
|
||||
CriticalityMult float64 `json:"criticality_multiplier"`
|
||||
IncidentAdj float64 `json:"incident_adjustment"`
|
||||
AuditTrail []AuditEntry `json:"audit_trail"`
|
||||
}
|
||||
|
||||
// GCI Breakdown with all 4 levels
|
||||
type GCIBreakdown struct {
|
||||
GCIResult
|
||||
Level1Modules []ModuleScore `json:"level1_modules"`
|
||||
Level2Areas []RiskWeightedScore `json:"level2_areas"`
|
||||
}
|
||||
|
||||
// MaturityLevel constants
|
||||
const (
|
||||
MaturityOptimized = "OPTIMIZED"
|
||||
MaturityManaged = "MANAGED"
|
||||
MaturityDefined = "DEFINED"
|
||||
MaturityReactive = "REACTIVE"
|
||||
MaturityHighRisk = "HIGH_RISK"
|
||||
)
|
||||
|
||||
// Maturity level labels (German)
|
||||
var MaturityLabels = map[string]string{
|
||||
MaturityOptimized: "Optimiert",
|
||||
MaturityManaged: "Gesteuert",
|
||||
MaturityDefined: "Definiert",
|
||||
MaturityReactive: "Reaktiv",
|
||||
MaturityHighRisk: "Hohes Risiko",
|
||||
}
|
||||
|
||||
// AuditEntry for score transparency
|
||||
type AuditEntry struct {
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Factor string `json:"factor"`
|
||||
Description string `json:"description"`
|
||||
Value float64 `json:"value"`
|
||||
Impact string `json:"impact"` // positive, negative, neutral
|
||||
}
|
||||
|
||||
// ComplianceMatrixEntry maps roles to regulations
|
||||
type ComplianceMatrixEntry struct {
|
||||
Role string `json:"role"`
|
||||
RoleName string `json:"role_name"`
|
||||
Regulations map[string]float64 `json:"regulations"` // regulation_id -> score
|
||||
OverallScore float64 `json:"overall_score"`
|
||||
RequiredModules int `json:"required_modules"`
|
||||
CompletedModules int `json:"completed_modules"`
|
||||
}
|
||||
|
||||
// GCI History snapshot
|
||||
type GCISnapshot struct {
|
||||
TenantID string `json:"tenant_id"`
|
||||
Score float64 `json:"score"`
|
||||
MaturityLevel string `json:"maturity_level"`
|
||||
AreaScores map[string]float64 `json:"area_scores"`
|
||||
CalculatedAt time.Time `json:"calculated_at"`
|
||||
}
|
||||
118
admin-v2/ai-compliance-sdk/internal/gci/nis2_roles.go
Normal file
118
admin-v2/ai-compliance-sdk/internal/gci/nis2_roles.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package gci
|
||||
|
||||
// NIS2Role defines a NIS2 role classification
|
||||
type NIS2Role struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
MandatoryModules []string `json:"mandatory_modules"`
|
||||
Priority int `json:"priority"` // 1=highest
|
||||
}
|
||||
|
||||
// NIS2RoleAssignment represents a user's NIS2 role
|
||||
type NIS2RoleAssignment struct {
|
||||
TenantID string `json:"tenant_id"`
|
||||
UserID string `json:"user_id"`
|
||||
UserName string `json:"user_name"`
|
||||
RoleID string `json:"role_id"`
|
||||
RoleName string `json:"role_name"`
|
||||
AssignedAt string `json:"assigned_at"`
|
||||
}
|
||||
|
||||
// NIS2 role definitions
|
||||
var NIS2Roles = map[string]NIS2Role{
|
||||
"N1": {
|
||||
ID: "N1",
|
||||
Name: "Geschaeftsleitung",
|
||||
Description: "Leitungsorgane mit persoenlicher Haftung gemaess NIS2 Art. 20",
|
||||
Priority: 1,
|
||||
MandatoryModules: []string{
|
||||
"nis2-management",
|
||||
"nis2-risikomanagement",
|
||||
"dsgvo-grundlagen",
|
||||
"iso-isms",
|
||||
},
|
||||
},
|
||||
"N2": {
|
||||
ID: "N2",
|
||||
Name: "IT-Sicherheit / CISO",
|
||||
Description: "Verantwortliche fuer IT-Sicherheit und Cybersecurity",
|
||||
Priority: 2,
|
||||
MandatoryModules: []string{
|
||||
"nis2-risikomanagement",
|
||||
"nis2-incident-response",
|
||||
"nis2-supply-chain",
|
||||
"iso-zugangssteuerung",
|
||||
"iso-kryptografie",
|
||||
},
|
||||
},
|
||||
"N3": {
|
||||
ID: "N3",
|
||||
Name: "Kritische Funktionen",
|
||||
Description: "Mitarbeiter in kritischen Geschaeftsprozessen",
|
||||
Priority: 3,
|
||||
MandatoryModules: []string{
|
||||
"nis2-risikomanagement",
|
||||
"nis2-incident-response",
|
||||
"dsgvo-tom",
|
||||
"iso-zugangssteuerung",
|
||||
},
|
||||
},
|
||||
"N4": {
|
||||
ID: "N4",
|
||||
Name: "Allgemeine Mitarbeiter",
|
||||
Description: "Alle Mitarbeiter mit IT-Zugang",
|
||||
Priority: 4,
|
||||
MandatoryModules: []string{
|
||||
"nis2-risikomanagement",
|
||||
"dsgvo-grundlagen",
|
||||
"iso-isms",
|
||||
},
|
||||
},
|
||||
"N5": {
|
||||
ID: "N5",
|
||||
Name: "Incident Response Team",
|
||||
Description: "Mitglieder des IRT/CSIRT gemaess NIS2 Art. 21",
|
||||
Priority: 2,
|
||||
MandatoryModules: []string{
|
||||
"nis2-incident-response",
|
||||
"nis2-risikomanagement",
|
||||
"nis2-supply-chain",
|
||||
"iso-zugangssteuerung",
|
||||
"iso-kryptografie",
|
||||
"iso-isms",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// GetNIS2Role returns a NIS2 role by ID
|
||||
func GetNIS2Role(roleID string) (NIS2Role, bool) {
|
||||
r, ok := NIS2Roles[roleID]
|
||||
return r, ok
|
||||
}
|
||||
|
||||
// ListNIS2Roles returns all NIS2 roles sorted by priority
|
||||
func ListNIS2Roles() []NIS2Role {
|
||||
roles := []NIS2Role{}
|
||||
// Return in priority order
|
||||
order := []string{"N1", "N2", "N5", "N3", "N4"}
|
||||
for _, id := range order {
|
||||
if r, ok := NIS2Roles[id]; ok {
|
||||
roles = append(roles, r)
|
||||
}
|
||||
}
|
||||
return roles
|
||||
}
|
||||
|
||||
// MockNIS2RoleAssignments returns mock role assignments
|
||||
func MockNIS2RoleAssignments(tenantID string) []NIS2RoleAssignment {
|
||||
return []NIS2RoleAssignment{
|
||||
{TenantID: tenantID, UserID: "user-001", UserName: "Dr. Schmidt", RoleID: "N1", RoleName: "Geschaeftsleitung", AssignedAt: "2025-06-01"},
|
||||
{TenantID: tenantID, UserID: "user-002", UserName: "M. Weber", RoleID: "N2", RoleName: "IT-Sicherheit / CISO", AssignedAt: "2025-06-01"},
|
||||
{TenantID: tenantID, UserID: "user-003", UserName: "S. Mueller", RoleID: "N5", RoleName: "Incident Response Team", AssignedAt: "2025-07-15"},
|
||||
{TenantID: tenantID, UserID: "user-004", UserName: "K. Fischer", RoleID: "N3", RoleName: "Kritische Funktionen", AssignedAt: "2025-08-01"},
|
||||
{TenantID: tenantID, UserID: "user-005", UserName: "L. Braun", RoleID: "N3", RoleName: "Kritische Funktionen", AssignedAt: "2025-08-01"},
|
||||
{TenantID: tenantID, UserID: "user-006", UserName: "A. Schwarz", RoleID: "N4", RoleName: "Allgemeine Mitarbeiter", AssignedAt: "2025-09-01"},
|
||||
{TenantID: tenantID, UserID: "user-007", UserName: "T. Wagner", RoleID: "N4", RoleName: "Allgemeine Mitarbeiter", AssignedAt: "2025-09-01"},
|
||||
}
|
||||
}
|
||||
147
admin-v2/ai-compliance-sdk/internal/gci/nis2_scoring.go
Normal file
147
admin-v2/ai-compliance-sdk/internal/gci/nis2_scoring.go
Normal file
@@ -0,0 +1,147 @@
|
||||
package gci
|
||||
|
||||
import "math"
|
||||
|
||||
// NIS2Score represents the NIS2-specific compliance score
|
||||
type NIS2Score struct {
|
||||
TenantID string `json:"tenant_id"`
|
||||
OverallScore float64 `json:"overall_score"`
|
||||
MaturityLevel string `json:"maturity_level"`
|
||||
MaturityLabel string `json:"maturity_label"`
|
||||
AreaScores []NIS2AreaScore `json:"area_scores"`
|
||||
RoleCompliance []NIS2RoleScore `json:"role_compliance"`
|
||||
}
|
||||
|
||||
// NIS2AreaScore represents a NIS2 compliance area
|
||||
type NIS2AreaScore struct {
|
||||
AreaID string `json:"area_id"`
|
||||
AreaName string `json:"area_name"`
|
||||
Score float64 `json:"score"`
|
||||
Weight float64 `json:"weight"`
|
||||
ModuleIDs []string `json:"module_ids"`
|
||||
}
|
||||
|
||||
// NIS2RoleScore represents completion per NIS2 role
|
||||
type NIS2RoleScore struct {
|
||||
RoleID string `json:"role_id"`
|
||||
RoleName string `json:"role_name"`
|
||||
AssignedUsers int `json:"assigned_users"`
|
||||
CompletionRate float64 `json:"completion_rate"`
|
||||
MandatoryTotal int `json:"mandatory_total"`
|
||||
MandatoryDone int `json:"mandatory_done"`
|
||||
}
|
||||
|
||||
// NIS2 scoring areas with weights
|
||||
// NIS2Score = 25% Management + 25% Incident + 30% IT Security + 20% Supply Chain
|
||||
var nis2Areas = []struct {
|
||||
ID string
|
||||
Name string
|
||||
Weight float64
|
||||
ModuleIDs []string
|
||||
}{
|
||||
{
|
||||
ID: "management", Name: "Management & Governance", Weight: 0.25,
|
||||
ModuleIDs: []string{"nis2-management", "dsgvo-grundlagen", "iso-isms"},
|
||||
},
|
||||
{
|
||||
ID: "incident", Name: "Vorfallsbehandlung", Weight: 0.25,
|
||||
ModuleIDs: []string{"nis2-incident-response"},
|
||||
},
|
||||
{
|
||||
ID: "it_security", Name: "IT-Sicherheit", Weight: 0.30,
|
||||
ModuleIDs: []string{"nis2-risikomanagement", "iso-zugangssteuerung", "iso-kryptografie"},
|
||||
},
|
||||
{
|
||||
ID: "supply_chain", Name: "Lieferkettensicherheit", Weight: 0.20,
|
||||
ModuleIDs: []string{"nis2-supply-chain", "dsgvo-auftragsverarbeitung"},
|
||||
},
|
||||
}
|
||||
|
||||
// CalculateNIS2Score computes the NIS2-specific compliance score
|
||||
func CalculateNIS2Score(tenantID string) *NIS2Score {
|
||||
modules := MockModuleData(tenantID)
|
||||
moduleMap := map[string]ModuleScore{}
|
||||
for _, m := range modules {
|
||||
moduleMap[m.ModuleID] = m
|
||||
}
|
||||
|
||||
areaScores := []NIS2AreaScore{}
|
||||
totalWeighted := 0.0
|
||||
|
||||
for _, area := range nis2Areas {
|
||||
areaScore := NIS2AreaScore{
|
||||
AreaID: area.ID,
|
||||
AreaName: area.Name,
|
||||
Weight: area.Weight,
|
||||
ModuleIDs: area.ModuleIDs,
|
||||
}
|
||||
|
||||
scoreSum := 0.0
|
||||
count := 0
|
||||
for _, modID := range area.ModuleIDs {
|
||||
if m, ok := moduleMap[modID]; ok {
|
||||
if m.Assigned > 0 {
|
||||
scoreSum += float64(m.Completed) / float64(m.Assigned) * 100
|
||||
}
|
||||
count++
|
||||
}
|
||||
}
|
||||
if count > 0 {
|
||||
areaScore.Score = math.Round(scoreSum/float64(count)*10) / 10
|
||||
}
|
||||
totalWeighted += areaScore.Score * areaScore.Weight
|
||||
areaScores = append(areaScores, areaScore)
|
||||
}
|
||||
|
||||
overallScore := math.Round(totalWeighted*10) / 10
|
||||
|
||||
// Calculate role compliance
|
||||
roleAssignments := MockNIS2RoleAssignments(tenantID)
|
||||
roleScores := calculateNIS2RoleScores(roleAssignments, moduleMap)
|
||||
|
||||
return &NIS2Score{
|
||||
TenantID: tenantID,
|
||||
OverallScore: overallScore,
|
||||
MaturityLevel: determineMaturityLevel(overallScore),
|
||||
MaturityLabel: MaturityLabels[determineMaturityLevel(overallScore)],
|
||||
AreaScores: areaScores,
|
||||
RoleCompliance: roleScores,
|
||||
}
|
||||
}
|
||||
|
||||
func calculateNIS2RoleScores(assignments []NIS2RoleAssignment, moduleMap map[string]ModuleScore) []NIS2RoleScore {
|
||||
// Count users per role
|
||||
roleCounts := map[string]int{}
|
||||
for _, a := range assignments {
|
||||
roleCounts[a.RoleID]++
|
||||
}
|
||||
|
||||
scores := []NIS2RoleScore{}
|
||||
for roleID, role := range NIS2Roles {
|
||||
rs := NIS2RoleScore{
|
||||
RoleID: roleID,
|
||||
RoleName: role.Name,
|
||||
AssignedUsers: roleCounts[roleID],
|
||||
MandatoryTotal: len(role.MandatoryModules),
|
||||
}
|
||||
|
||||
completionSum := 0.0
|
||||
for _, modID := range role.MandatoryModules {
|
||||
if m, ok := moduleMap[modID]; ok {
|
||||
if m.Assigned > 0 {
|
||||
rate := float64(m.Completed) / float64(m.Assigned)
|
||||
completionSum += rate
|
||||
if rate >= 0.8 { // 80%+ = considered done
|
||||
rs.MandatoryDone++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if rs.MandatoryTotal > 0 {
|
||||
rs.CompletionRate = math.Round(completionSum/float64(rs.MandatoryTotal)*100*10) / 10
|
||||
}
|
||||
scores = append(scores, rs)
|
||||
}
|
||||
|
||||
return scores
|
||||
}
|
||||
59
admin-v2/ai-compliance-sdk/internal/gci/validity.go
Normal file
59
admin-v2/ai-compliance-sdk/internal/gci/validity.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package gci
|
||||
|
||||
import (
|
||||
"math"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// GracePeriodDays is the number of days after expiry during which
|
||||
// the certificate still contributes (with declining factor)
|
||||
GracePeriodDays = 180
|
||||
|
||||
// DecayStartDays is how many days before expiry the linear decay begins
|
||||
DecayStartDays = 180
|
||||
)
|
||||
|
||||
// CalculateValidityFactor computes the validity factor for a certificate
|
||||
// based on its expiry date.
|
||||
//
|
||||
// Rules:
|
||||
// - Certificate not yet expiring (>6 months): factor = 1.0
|
||||
// - Certificate expiring within 6 months: linear decay from 1.0 to 0.5
|
||||
// - Certificate expired: linear decay from 0.5 to 0.0 over grace period
|
||||
// - Certificate expired beyond grace period: factor = 0.0
|
||||
func CalculateValidityFactor(validUntil time.Time, now time.Time) float64 {
|
||||
daysUntilExpiry := validUntil.Sub(now).Hours() / 24.0
|
||||
|
||||
if daysUntilExpiry > float64(DecayStartDays) {
|
||||
// Not yet in decay window
|
||||
return 1.0
|
||||
}
|
||||
|
||||
if daysUntilExpiry > 0 {
|
||||
// In pre-expiry decay window: linear from 1.0 to 0.5
|
||||
fraction := daysUntilExpiry / float64(DecayStartDays)
|
||||
return 0.5 + 0.5*fraction
|
||||
}
|
||||
|
||||
// Certificate is expired
|
||||
daysExpired := -daysUntilExpiry
|
||||
if daysExpired > float64(GracePeriodDays) {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
// In grace period: linear from 0.5 to 0.0
|
||||
fraction := 1.0 - (daysExpired / float64(GracePeriodDays))
|
||||
return math.Max(0, 0.5*fraction)
|
||||
}
|
||||
|
||||
// IsExpired returns true if the certificate is past its validity date
|
||||
func IsExpired(validUntil time.Time, now time.Time) bool {
|
||||
return now.After(validUntil)
|
||||
}
|
||||
|
||||
// IsExpiringSoon returns true if the certificate expires within the decay window
|
||||
func IsExpiringSoon(validUntil time.Time, now time.Time) bool {
|
||||
daysUntil := validUntil.Sub(now).Hours() / 24.0
|
||||
return daysUntil > 0 && daysUntil <= float64(DecayStartDays)
|
||||
}
|
||||
78
admin-v2/ai-compliance-sdk/internal/gci/weights.go
Normal file
78
admin-v2/ai-compliance-sdk/internal/gci/weights.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package gci
|
||||
|
||||
// WeightProfile defines regulation weights for different compliance profiles
|
||||
type WeightProfile struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Weights map[string]float64 `json:"weights"` // regulation_id -> weight (0.0-1.0)
|
||||
}
|
||||
|
||||
// Default weight profiles
|
||||
var DefaultProfiles = map[string]WeightProfile{
|
||||
"default": {
|
||||
ID: "default",
|
||||
Name: "Standard",
|
||||
Description: "Ausgewogenes Profil fuer allgemeine Compliance",
|
||||
Weights: map[string]float64{
|
||||
"dsgvo": 0.30,
|
||||
"nis2": 0.25,
|
||||
"iso27001": 0.25,
|
||||
"ai_act": 0.20,
|
||||
},
|
||||
},
|
||||
"nis2_relevant": {
|
||||
ID: "nis2_relevant",
|
||||
Name: "NIS2-relevant",
|
||||
Description: "Fuer Betreiber kritischer Infrastrukturen",
|
||||
Weights: map[string]float64{
|
||||
"dsgvo": 0.25,
|
||||
"nis2": 0.35,
|
||||
"iso27001": 0.25,
|
||||
"ai_act": 0.15,
|
||||
},
|
||||
},
|
||||
"ki_nutzer": {
|
||||
ID: "ki_nutzer",
|
||||
Name: "KI-Nutzer",
|
||||
Description: "Fuer Organisationen mit KI-Einsatz",
|
||||
Weights: map[string]float64{
|
||||
"dsgvo": 0.25,
|
||||
"nis2": 0.25,
|
||||
"iso27001": 0.20,
|
||||
"ai_act": 0.30,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// ModuleRiskWeights defines risk criticality per module type
|
||||
var ModuleRiskWeights = map[string]float64{
|
||||
"incident_response": 3.0,
|
||||
"management_awareness": 3.0,
|
||||
"data_protection": 2.5,
|
||||
"it_security": 2.5,
|
||||
"supply_chain": 2.0,
|
||||
"risk_assessment": 2.0,
|
||||
"access_control": 2.0,
|
||||
"business_continuity": 2.0,
|
||||
"employee_training": 1.5,
|
||||
"documentation": 1.5,
|
||||
"physical_security": 1.0,
|
||||
"general": 1.0,
|
||||
}
|
||||
|
||||
// GetProfile returns a weight profile by ID, defaulting to "default"
|
||||
func GetProfile(profileID string) WeightProfile {
|
||||
if p, ok := DefaultProfiles[profileID]; ok {
|
||||
return p
|
||||
}
|
||||
return DefaultProfiles["default"]
|
||||
}
|
||||
|
||||
// GetModuleRiskWeight returns the risk weight for a module category
|
||||
func GetModuleRiskWeight(category string) float64 {
|
||||
if w, ok := ModuleRiskWeights[category]; ok {
|
||||
return w
|
||||
}
|
||||
return 1.0
|
||||
}
|
||||
@@ -45,7 +45,7 @@ func (s *Service) GenerateDSFA(ctx context.Context, context map[string]interface
|
||||
}
|
||||
|
||||
// Build prompt with context and RAG sources
|
||||
prompt := s.buildDSFAPrompt(context, ragSources)
|
||||
_ = s.buildDSFAPrompt(context, ragSources)
|
||||
|
||||
// In production, this would call the Anthropic API
|
||||
// response, err := s.callAnthropicAPI(ctx, prompt)
|
||||
|
||||
@@ -88,7 +88,7 @@ func (s *Service) getMockSearchResults(query string, topK int) []SearchResult {
|
||||
// DSGVO Articles
|
||||
{
|
||||
ID: "dsgvo-art-5",
|
||||
Content: "Art. 5 DSGVO - Grundsätze für die Verarbeitung personenbezogener Daten\n\n(1) Personenbezogene Daten müssen:\na) auf rechtmäßige Weise, nach Treu und Glauben und in einer für die betroffene Person nachvollziehbaren Weise verarbeitet werden („Rechtmäßigkeit, Verarbeitung nach Treu und Glauben, Transparenz");\nb) für festgelegte, eindeutige und legitime Zwecke erhoben werden und dürfen nicht in einer mit diesen Zwecken nicht zu vereinbarenden Weise weiterverarbeitet werden („Zweckbindung");\nc) dem Zweck angemessen und erheblich sowie auf das für die Zwecke der Verarbeitung notwendige Maß beschränkt sein („Datenminimierung");",
|
||||
Content: "Art. 5 DSGVO - Grundsaetze fuer die Verarbeitung personenbezogener Daten\n\n(1) Personenbezogene Daten muessen:\na) auf rechtmaessige Weise, nach Treu und Glauben und in einer fuer die betroffene Person nachvollziehbaren Weise verarbeitet werden (Rechtmaessigkeit, Verarbeitung nach Treu und Glauben, Transparenz);\nb) fuer festgelegte, eindeutige und legitime Zwecke erhoben werden und duerfen nicht in einer mit diesen Zwecken nicht zu vereinbarenden Weise weiterverarbeitet werden (Zweckbindung);\nc) dem Zweck angemessen und erheblich sowie auf das fuer die Zwecke der Verarbeitung notwendige Mass beschraenkt sein (Datenminimierung);",
|
||||
Source: "DSGVO",
|
||||
Score: 0.95,
|
||||
Metadata: map[string]string{
|
||||
|
||||
Reference in New Issue
Block a user