@@ -15,7 +15,6 @@ import (
1515 "github.com/golang/glog"
1616 dbmodels "github.com/kubeflow/hub/catalog/internal/catalog/modelcatalog/models"
1717 "github.com/kubeflow/hub/catalog/internal/db/service"
18- "github.com/kubeflow/hub/internal/platform/apiutils"
1918 models "github.com/kubeflow/hub/internal/platform/db/entity"
2019)
2120
@@ -127,6 +126,41 @@ func (pr *performanceRecord) UnmarshalJSON(data []byte) error {
127126 return nil
128127}
129128
129+ // securityEvaluationRecord represents a single security evaluation result from security-evaluations.ndjson
130+ type securityEvaluationRecord struct {
131+ // Core fields needed to associate security data with model
132+ ID string `json:"id"`
133+ ModelID string `json:"model_id"`
134+
135+ // CustomProperties captures remaining fields dynamically
136+ CustomProperties map [string ]any `json:"-"`
137+ }
138+
139+ // UnmarshalJSON implements custom JSON unmarshaling to capture all undefined fields as CustomProperties
140+ func (sr * securityEvaluationRecord ) UnmarshalJSON (data []byte ) error {
141+ var raw map [string ]any
142+ decoder := json .NewDecoder (bytes .NewReader (data ))
143+ decoder .UseNumber ()
144+ if err := decoder .Decode (& raw ); err != nil {
145+ return err
146+ }
147+
148+ if id , ok := raw ["id" ].(string ); ok {
149+ sr .ID = id
150+ }
151+ if modelID , ok := raw ["model_id" ].(string ); ok {
152+ sr .ModelID = modelID
153+ }
154+
155+ if sr .CustomProperties == nil {
156+ sr .CustomProperties = make (map [string ]any )
157+ }
158+
159+ maps .Copy (sr .CustomProperties , raw )
160+
161+ return nil
162+ }
163+
130164type PerformanceMetricsLoader struct {
131165 path []string
132166 modelRepo dbmodels.CatalogModelRepository
@@ -324,6 +358,7 @@ func processModelArtifactsBatch(dirPath string, modelID int32, modelName string,
324358 // Parse all metrics files
325359 var evaluationRecords []evaluationRecord
326360 var performanceRecords []performanceRecord
361+ var securityRecords []securityEvaluationRecord
327362
328363 // Parse evaluation metrics if file exists
329364 evaluationsPath := filepath .Join (dirPath , "evaluations.ndjson" )
@@ -347,7 +382,18 @@ func processModelArtifactsBatch(dirPath string, modelID int32, modelName string,
347382 }
348383 }
349384
350- totalRecords := len (evaluationRecords ) + len (performanceRecords )
385+ // Parse security evaluation metrics if file exists
386+ securityPath := filepath .Join (dirPath , "security-evaluations.ndjson" )
387+ if _ , err := os .Stat (securityPath ); err == nil {
388+ records , err := parseSecurityEvaluationFile (securityPath )
389+ if err != nil {
390+ glog .Errorf ("Failed to parse security evaluations file for %s: %v" , modelName , err )
391+ } else {
392+ securityRecords = records
393+ }
394+ }
395+
396+ totalRecords := len (evaluationRecords ) + len (performanceRecords ) + len (securityRecords )
351397 if totalRecords == 0 {
352398 return 0 , nil
353399 }
@@ -393,6 +439,22 @@ func processModelArtifactsBatch(dirPath string, modelID int32, modelName string,
393439 }
394440 }
395441
442+ // Check security evaluation artifacts; deduplicate within the file before the DB check
443+ seenSecurityIDs := make (map [string ]bool , len (securityRecords ))
444+ for _ , secRecord := range securityRecords {
445+ if seenSecurityIDs [secRecord .ID ] {
446+ glog .Warningf ("Duplicate security artifact ID %s in file, skipping" , secRecord .ID )
447+ continue
448+ }
449+ seenSecurityIDs [secRecord .ID ] = true
450+ if ! existingArtifactsMap [secRecord .ID ] {
451+ artifact := createSecurityArtifact (secRecord , modelID , metricsArtifactTypeID , nil , nil )
452+ artifactsToInsert = append (artifactsToInsert , artifact )
453+ } else {
454+ glog .V (2 ).Infof ("Security artifact %s already exists, skipping" , secRecord .ID )
455+ }
456+ }
457+
396458 if len (artifactsToInsert ) == 0 {
397459 glog .V (2 ).Infof ("All artifacts already exist for model %s, nothing to insert" , modelName )
398460 return 0 , nil
@@ -578,7 +640,7 @@ func createPerformanceArtifact(perfRecord performanceRecord, modelID int32, type
578640 }
579641 }
580642 if createTime == nil {
581- createTime = apiutils . Of (time .Now ().UnixMilli ())
643+ createTime = new (time.Now ().UnixMilli ())
582644 }
583645
584646 if updatedAtNum , ok := perfRecord .CustomProperties ["updated_at" ].(json.Number ); ok {
@@ -590,7 +652,7 @@ func createPerformanceArtifact(perfRecord performanceRecord, modelID int32, type
590652 }
591653 }
592654 if updateTime == nil {
593- updateTime = apiutils . Of (time .Now ().UnixMilli ())
655+ updateTime = new (time.Now ().UnixMilli ())
594656 }
595657 delete (perfRecord .CustomProperties , "updated_at" )
596658 delete (perfRecord .CustomProperties , "created_at" )
@@ -655,6 +717,126 @@ func createPerformanceArtifact(perfRecord performanceRecord, modelID int32, type
655717 return metricsArtifact
656718}
657719
720+ // parseSecurityEvaluationFile reads and parses a security-evaluations.ndjson file
721+ func parseSecurityEvaluationFile (filePath string ) ([]securityEvaluationRecord , error ) {
722+ file , err := os .Open (filePath )
723+ if err != nil {
724+ return nil , fmt .Errorf ("failed to open security evaluation file %s: %v" , filePath , err )
725+ }
726+ defer file .Close ()
727+
728+ scanner := bufio .NewScanner (file )
729+ securityRecords := []securityEvaluationRecord {}
730+
731+ for scanner .Scan () {
732+ line := scanner .Text ()
733+ if strings .TrimSpace (line ) == "" {
734+ continue
735+ }
736+
737+ var secRecord securityEvaluationRecord
738+ if err := json .Unmarshal ([]byte (line ), & secRecord ); err != nil {
739+ glog .Errorf ("Failed to parse security evaluation record: %v" , err )
740+ continue
741+ }
742+
743+ securityRecords = append (securityRecords , secRecord )
744+ }
745+
746+ if err := scanner .Err (); err != nil {
747+ return nil , fmt .Errorf ("error reading security evaluation file: %v" , err )
748+ }
749+
750+ return securityRecords , nil
751+ }
752+
753+ // createSecurityArtifact creates a metrics artifact from a security evaluation record
754+ func createSecurityArtifact (secRecord securityEvaluationRecord , modelID int32 , typeID int32 , existingID * int32 , existingCreateTime * int64 ) * dbmodels.CatalogMetricsArtifactImpl {
755+ artifactName := fmt .Sprintf ("security-%s" , secRecord .ID )
756+
757+ createTime := existingCreateTime
758+ var updateTime * int64
759+
760+ if existingCreateTime == nil {
761+ if createdAtNum , ok := secRecord .CustomProperties ["created_at" ].(json.Number ); ok {
762+ createdAt , err := createdAtNum .Int64 ()
763+ if err == nil {
764+ createTime = & createdAt
765+ } else {
766+ glog .Warningf ("%s: invalid created_at value: %v" , artifactName , err )
767+ }
768+ }
769+ }
770+ if createTime == nil {
771+ createTime = new (time.Now ().UnixMilli ())
772+ }
773+
774+ if updatedAtNum , ok := secRecord .CustomProperties ["updated_at" ].(json.Number ); ok {
775+ updatedAt , err := updatedAtNum .Int64 ()
776+ if err == nil {
777+ updateTime = & updatedAt
778+ } else {
779+ glog .Warningf ("%s: invalid updated_at value: %v" , artifactName , err )
780+ }
781+ }
782+ if updateTime == nil {
783+ updateTime = new (time.Now ().UnixMilli ())
784+ }
785+ delete (secRecord .CustomProperties , "updated_at" )
786+ delete (secRecord .CustomProperties , "created_at" )
787+
788+ properties := []models.Properties {}
789+ customProperties := []models.Properties {}
790+
791+ for key , value := range secRecord .CustomProperties {
792+ prop := models.Properties {Name : key }
793+
794+ switch v := value .(type ) {
795+ case string :
796+ prop .StringValue = & v
797+ case float64 :
798+ prop .DoubleValue = & v
799+ case int64 :
800+ prop .SetInt64Value (v )
801+ case int :
802+ intVal := int32 (v )
803+ prop .IntValue = & intVal
804+ case bool :
805+ prop .BoolValue = & v
806+ case json.Number :
807+ if n , err := v .Int64 (); err == nil {
808+ prop .SetInt64Value (n )
809+ } else if f , err := v .Float64 (); err == nil {
810+ prop .DoubleValue = & f
811+ } else {
812+ strVal := v .String ()
813+ prop .StringValue = & strVal
814+ }
815+ default :
816+ strVal := fmt .Sprintf ("%v" , v )
817+ prop .StringValue = & strVal
818+ }
819+
820+ customProperties = append (customProperties , prop )
821+ }
822+
823+ metricsArtifact := & dbmodels.CatalogMetricsArtifactImpl {
824+ ID : existingID ,
825+ TypeID : & typeID ,
826+ Attributes : & dbmodels.CatalogMetricsArtifactAttributes {
827+ Name : & artifactName ,
828+ ExternalID : & secRecord .ID ,
829+ CreateTimeSinceEpoch : createTime ,
830+ LastUpdateTimeSinceEpoch : updateTime ,
831+ MetricsType : dbmodels .MetricsTypeSecurityMetrics ,
832+ },
833+ Properties : & properties ,
834+ CustomProperties : & customProperties ,
835+ }
836+
837+ return metricsArtifact
838+ }
839+
658840// enrichCatalogModelFromMetadata updates CatalogModel with additional fields from metadata.json
659841func enrichCatalogModelFromMetadata (existingModel dbmodels.CatalogModel , metadata metadataJSON , modelRepo dbmodels.CatalogModelRepository ) error {
660842 // Build custom properties to add/update
0 commit comments