@@ -21,11 +21,11 @@ import (
2121// metadataJSON represents the minimal structure needed from metadata.json files
2222// Only the ID field is needed to look up existing models
2323type metadataJSON struct {
24- ID string `json:"id"` // Maps to model name for lookup
25- OverallAccuracy * float64 `json:"overall_accuracy"` // Overall accuracy score for the model
26- Size * string `json:"size"` // Model parameter count (e.g., "8B params")
27- TensorType * string `json:"tensor_type"` // Data precision (e.g., "FP16", "INT4")
28- VariantGroupID * string `json:"variant_group_id"` // UUID linking model variants together
24+ ID string `json:"id"` // Maps to model name for lookup
25+ OverallAccuracy * float64 `json:"overall_accuracy"` // Overall accuracy score for the model
26+ Size * string `json:"size"` // Model parameter count (e.g., "8B params")
27+ TensorType * string `json:"tensor_type"` // Data precision (e.g., "FP16", "INT4")
28+ VariantGroupID * string `json:"variant_group_id"` // UUID linking model variants together
2929 MinVRAMGB * float64 `json:"min_vram_gb"` // Minimum VRAM required in GB (e.g., 466.0)
3030 ModelcarImageSize * float64 `json:"modelcar_image_size"` // Modelcar image size in GB (e.g., 405.19)
3131 ModelcarImageSizeBytes * int64 `json:"modelcar_image_size_bytes"` // Modelcar image size in bytes (e.g., 405186009411)
@@ -137,6 +137,41 @@ func (pr *performanceRecord) UnmarshalJSON(data []byte) error {
137137 return nil
138138}
139139
140+ // securityEvaluationRecord represents a single security evaluation result from security-evaluations.ndjson
141+ type securityEvaluationRecord struct {
142+ // Core fields needed to associate security data with model
143+ ID string `json:"id"`
144+ ModelID string `json:"model_id"`
145+
146+ // CustomProperties captures remaining fields dynamically
147+ CustomProperties map [string ]any `json:"-"`
148+ }
149+
150+ // UnmarshalJSON implements custom JSON unmarshaling to capture all undefined fields as CustomProperties
151+ func (sr * securityEvaluationRecord ) UnmarshalJSON (data []byte ) error {
152+ var raw map [string ]any
153+ decoder := json .NewDecoder (bytes .NewReader (data ))
154+ decoder .UseNumber ()
155+ if err := decoder .Decode (& raw ); err != nil {
156+ return err
157+ }
158+
159+ if id , ok := raw ["id" ].(string ); ok {
160+ sr .ID = id
161+ }
162+ if modelID , ok := raw ["model_id" ].(string ); ok {
163+ sr .ModelID = modelID
164+ }
165+
166+ if sr .CustomProperties == nil {
167+ sr .CustomProperties = make (map [string ]any )
168+ }
169+
170+ maps .Copy (sr .CustomProperties , raw )
171+
172+ return nil
173+ }
174+
140175type PerformanceMetricsLoader struct {
141176 path []string
142177 modelRepo dbmodels.CatalogModelRepository
@@ -334,6 +369,7 @@ func processModelArtifactsBatch(dirPath string, modelID int32, modelName string,
334369 // Parse all metrics files
335370 var evaluationRecords []evaluationRecord
336371 var performanceRecords []performanceRecord
372+ var securityRecords []securityEvaluationRecord
337373
338374 // Parse evaluation metrics if file exists
339375 evaluationsPath := filepath .Join (dirPath , "evaluations.ndjson" )
@@ -357,7 +393,18 @@ func processModelArtifactsBatch(dirPath string, modelID int32, modelName string,
357393 }
358394 }
359395
360- totalRecords := len (evaluationRecords ) + len (performanceRecords ) + len (coldStartMatrix )
396+ // Parse security evaluation metrics if file exists
397+ securityPath := filepath .Join (dirPath , "security-evaluations.ndjson" )
398+ if _ , err := os .Stat (securityPath ); err == nil {
399+ records , err := parseSecurityEvaluationFile (securityPath )
400+ if err != nil {
401+ glog .Errorf ("Failed to parse security evaluations file for %s: %v" , modelName , err )
402+ } else {
403+ securityRecords = records
404+ }
405+ }
406+
407+ totalRecords := len (evaluationRecords ) + len (performanceRecords ) + len (securityRecords ) + len (coldStartMatrix )
361408 if totalRecords == 0 {
362409 return 0 , nil
363410 }
@@ -418,6 +465,22 @@ func processModelArtifactsBatch(dirPath string, modelID int32, modelName string,
418465 }
419466 }
420467
468+ // Check security evaluation artifacts; deduplicate within the file before the DB check
469+ seenSecurityIDs := make (map [string ]bool , len (securityRecords ))
470+ for _ , secRecord := range securityRecords {
471+ if seenSecurityIDs [secRecord .ID ] {
472+ glog .Warningf ("Duplicate security artifact ID %s in file, skipping" , secRecord .ID )
473+ continue
474+ }
475+ seenSecurityIDs [secRecord .ID ] = true
476+ if ! existingArtifactsMap [secRecord .ID ] {
477+ artifact := createSecurityArtifact (secRecord , modelID , metricsArtifactTypeID , nil , nil )
478+ artifactsToInsert = append (artifactsToInsert , artifact )
479+ } else {
480+ glog .V (2 ).Infof ("Security artifact %s already exists, skipping" , secRecord .ID )
481+ }
482+ }
483+
421484 if len (artifactsToInsert ) == 0 {
422485 glog .V (2 ).Infof ("All artifacts already exist for model %s, nothing to insert" , modelName )
423486 return 0 , nil
@@ -728,6 +791,126 @@ func createColdStartArtifact(entry coldStartEntry, externalID string, typeID int
728791 }
729792}
730793
794+ // parseSecurityEvaluationFile reads and parses a security-evaluations.ndjson file
795+ func parseSecurityEvaluationFile (filePath string ) ([]securityEvaluationRecord , error ) {
796+ file , err := os .Open (filePath )
797+ if err != nil {
798+ return nil , fmt .Errorf ("failed to open security evaluation file %s: %v" , filePath , err )
799+ }
800+ defer file .Close ()
801+
802+ scanner := bufio .NewScanner (file )
803+ securityRecords := []securityEvaluationRecord {}
804+
805+ for scanner .Scan () {
806+ line := scanner .Text ()
807+ if strings .TrimSpace (line ) == "" {
808+ continue
809+ }
810+
811+ var secRecord securityEvaluationRecord
812+ if err := json .Unmarshal ([]byte (line ), & secRecord ); err != nil {
813+ glog .Errorf ("Failed to parse security evaluation record: %v" , err )
814+ continue
815+ }
816+
817+ securityRecords = append (securityRecords , secRecord )
818+ }
819+
820+ if err := scanner .Err (); err != nil {
821+ return nil , fmt .Errorf ("error reading security evaluation file: %v" , err )
822+ }
823+
824+ return securityRecords , nil
825+ }
826+
827+ // createSecurityArtifact creates a metrics artifact from a security evaluation record
828+ func createSecurityArtifact (secRecord securityEvaluationRecord , modelID int32 , typeID int32 , existingID * int32 , existingCreateTime * int64 ) * dbmodels.CatalogMetricsArtifactImpl {
829+ artifactName := fmt .Sprintf ("security-%s" , secRecord .ID )
830+
831+ createTime := existingCreateTime
832+ var updateTime * int64
833+
834+ if existingCreateTime == nil {
835+ if createdAtNum , ok := secRecord .CustomProperties ["created_at" ].(json.Number ); ok {
836+ createdAt , err := createdAtNum .Int64 ()
837+ if err == nil {
838+ createTime = & createdAt
839+ } else {
840+ glog .Warningf ("%s: invalid created_at value: %v" , artifactName , err )
841+ }
842+ }
843+ }
844+ if createTime == nil {
845+ createTime = new (time.Now ().UnixMilli ())
846+ }
847+
848+ if updatedAtNum , ok := secRecord .CustomProperties ["updated_at" ].(json.Number ); ok {
849+ updatedAt , err := updatedAtNum .Int64 ()
850+ if err == nil {
851+ updateTime = & updatedAt
852+ } else {
853+ glog .Warningf ("%s: invalid updated_at value: %v" , artifactName , err )
854+ }
855+ }
856+ if updateTime == nil {
857+ updateTime = new (time.Now ().UnixMilli ())
858+ }
859+ delete (secRecord .CustomProperties , "updated_at" )
860+ delete (secRecord .CustomProperties , "created_at" )
861+
862+ properties := []models.Properties {}
863+ customProperties := []models.Properties {}
864+
865+ for key , value := range secRecord .CustomProperties {
866+ prop := models.Properties {Name : key }
867+
868+ switch v := value .(type ) {
869+ case string :
870+ prop .StringValue = & v
871+ case float64 :
872+ prop .DoubleValue = & v
873+ case int64 :
874+ prop .SetInt64Value (v )
875+ case int :
876+ intVal := int32 (v )
877+ prop .IntValue = & intVal
878+ case bool :
879+ prop .BoolValue = & v
880+ case json.Number :
881+ if n , err := v .Int64 (); err == nil {
882+ prop .SetInt64Value (n )
883+ } else if f , err := v .Float64 (); err == nil {
884+ prop .DoubleValue = & f
885+ } else {
886+ strVal := v .String ()
887+ prop .StringValue = & strVal
888+ }
889+ default :
890+ strVal := fmt .Sprintf ("%v" , v )
891+ prop .StringValue = & strVal
892+ }
893+
894+ customProperties = append (customProperties , prop )
895+ }
896+
897+ metricsArtifact := & dbmodels.CatalogMetricsArtifactImpl {
898+ ID : existingID ,
899+ TypeID : & typeID ,
900+ Attributes : & dbmodels.CatalogMetricsArtifactAttributes {
901+ Name : & artifactName ,
902+ ExternalID : & secRecord .ID ,
903+ CreateTimeSinceEpoch : createTime ,
904+ LastUpdateTimeSinceEpoch : updateTime ,
905+ MetricsType : dbmodels .MetricsTypeSecurityMetrics ,
906+ },
907+ Properties : & properties ,
908+ CustomProperties : & customProperties ,
909+ }
910+
911+ return metricsArtifact
912+ }
913+
731914// enrichCatalogModelFromMetadata updates CatalogModel with additional fields from metadata.json
732915func enrichCatalogModelFromMetadata (existingModel dbmodels.CatalogModel , metadata metadataJSON , modelRepo dbmodels.CatalogModelRepository ) error {
733916 // Build custom properties to add/update
0 commit comments