knowledge_api.go
1 // Copyright (c) 2024-2026 Tencent Zhuque Lab. All rights reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 // 15 // Requirement: Any integration or derivative work must explicitly attribute 16 // Tencent Zhuque Lab (https://github.com/Tencent/AI-Infra-Guard) in its 17 // documentation or user interface, as detailed in the NOTICE file. 18 19 package websocket 20 21 import ( 22 "encoding/json" 23 "fmt" 24 "net/http" 25 "os" 26 "path/filepath" 27 "regexp" 28 "sort" 29 "strconv" 30 "strings" 31 "time" 32 33 "trpc.group/trpc-go/trpc-go/log" 34 35 "github.com/Tencent/AI-Infra-Guard/common/fingerprints/parser" 36 "github.com/Tencent/AI-Infra-Guard/pkg/vulstruct" 37 "github.com/gin-gonic/gin" 38 "gopkg.in/yaml.v3" 39 ) 40 41 // 合法性校验 42 var validName = regexp.MustCompile(`^[a-zA-Z0-9\\._ -]+$`) 43 44 func isValidName(name string) bool { 45 if strings.Contains(name, "..") { 46 return false 47 } 48 return validName.MatchString(name) 49 } 50 51 // safeJoinPath joins base and elem using filepath.Join, then verifies the result 52 // is still within base to prevent path traversal attacks (defense in depth). 53 func safeJoinPath(base, elem string) (string, error) { 54 cleanBase := filepath.Clean(base) 55 joined := filepath.Clean(filepath.Join(cleanBase, elem)) 56 if joined != cleanBase && !strings.HasPrefix(joined, cleanBase+string(os.PathSeparator)) { 57 return "", fmt.Errorf("path traversal detected: %q is outside base %q", elem, base) 58 } 59 return joined, nil 60 } 61 62 // FingerprintWithTime 包含指纹数据和文件修改时间的结构体 63 type FingerprintWithTime struct { 64 FingerPrint parser.FingerPrint 65 ModTime time.Time 66 } 67 68 // 评测集数据结构定义 69 type EvaluationDataItem struct { 70 Prompt string `json:"prompt"` 71 } 72 73 type EvaluationDataset struct { 74 Name string `json:"name"` 75 Description string `json:"description"` 76 DescriptionZh string `json:"description_zh,omitempty"` 77 Author string `json:"author,omitempty"` 78 Source []string `json:"source,omitempty"` 79 Count int `json:"count"` 80 Default bool `json:"default"` 81 Tags []string `json:"tags,omitempty"` 82 Recommendation int `json:"recommendation,omitempty"` 83 Language string `json:"language,omitempty"` 84 Data []EvaluationDataItem `json:"data"` 85 } 86 87 // 获取指纹列表,支持分页和名字模糊 88 func HandleListFingerprints(c *gin.Context) { 89 // 1. 解析分页参数 90 pageStr := c.DefaultQuery("page", "1") 91 sizeStr := c.DefaultQuery("size", "20") 92 page, _ := strconv.Atoi(pageStr) 93 size, _ := strconv.Atoi(sizeStr) 94 if page < 1 { 95 page = 1 96 } 97 if size < 1 { 98 size = 10 99 } 100 101 // 2. 获取查询参数 102 nameQuery := strings.ToLower(c.DefaultQuery("q", "")) 103 104 // 3. 读取 data/fingerprints/ 下所有分类和YAML文件 105 var allFingerprintsWithTime []FingerprintWithTime 106 root := "data/fingerprints" 107 filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error { 108 if err != nil { 109 return nil // 忽略错误 110 } 111 if !d.IsDir() && strings.HasSuffix(d.Name(), ".yaml") { 112 content, _ := os.ReadFile(path) 113 fp, err := parser.InitFingerPrintFromData(content) 114 if err == nil && fp != nil { 115 // 获取文件修改时间 116 fileInfo, _ := d.Info() 117 modTime := time.Time{} 118 if fileInfo != nil { 119 modTime = fileInfo.ModTime() 120 } 121 allFingerprintsWithTime = append(allFingerprintsWithTime, FingerprintWithTime{ 122 FingerPrint: *fp, 123 ModTime: modTime, 124 }) 125 } 126 } 127 return nil 128 }) 129 130 // 4. 条件过滤 131 var filteredFingerprintsWithTime []FingerprintWithTime 132 if nameQuery == "" { 133 filteredFingerprintsWithTime = allFingerprintsWithTime 134 } else { 135 for _, fpWithTime := range allFingerprintsWithTime { 136 fp := fpWithTime.FingerPrint 137 if strings.Contains(strings.ToLower(fp.Info.Name), nameQuery) { 138 filteredFingerprintsWithTime = append(filteredFingerprintsWithTime, fpWithTime) 139 continue 140 } 141 if strings.Contains(strings.ToLower(fp.Info.Desc), nameQuery) { 142 filteredFingerprintsWithTime = append(filteredFingerprintsWithTime, fpWithTime) 143 continue 144 } 145 if strings.Contains(strings.ToLower(fp.Info.Author), nameQuery) { 146 filteredFingerprintsWithTime = append(filteredFingerprintsWithTime, fpWithTime) 147 continue 148 } 149 } 150 } 151 152 // 5. 复合排序:如果有Recommendation,先按Recommendation降序;否则按文件修改时间降序 153 sort.Slice(filteredFingerprintsWithTime, func(i, j int) bool { 154 fpI := filteredFingerprintsWithTime[i].FingerPrint 155 fpJ := filteredFingerprintsWithTime[j].FingerPrint 156 157 // 如果两个都有Recommendation(>0),按Recommendation降序 158 if fpI.Info.Recommendation > 0 && fpJ.Info.Recommendation > 0 { 159 if fpI.Info.Recommendation != fpJ.Info.Recommendation { 160 return fpI.Info.Recommendation > fpJ.Info.Recommendation 161 } 162 // Recommendation相同时,按文件修改时间降序 163 return filteredFingerprintsWithTime[i].ModTime.After(filteredFingerprintsWithTime[j].ModTime) 164 } 165 166 // 如果只有一个有Recommendation,有Recommendation的排前面 167 if fpI.Info.Recommendation > 0 && fpJ.Info.Recommendation <= 0 { 168 return true 169 } 170 if fpI.Info.Recommendation <= 0 && fpJ.Info.Recommendation > 0 { 171 return false 172 } 173 174 // 如果两个都没有Recommendation,按文件修改时间降序 175 return filteredFingerprintsWithTime[i].ModTime.After(filteredFingerprintsWithTime[j].ModTime) 176 }) 177 178 // 提取指纹数据用于分页和返回 179 var filteredFingerprints []parser.FingerPrint 180 for _, fpWithTime := range filteredFingerprintsWithTime { 181 filteredFingerprints = append(filteredFingerprints, fpWithTime.FingerPrint) 182 } 183 184 // 6. 分页 185 total := len(filteredFingerprints) 186 start := (page - 1) * size 187 end := start + size 188 if start > total { 189 start = total 190 } 191 if end > total { 192 end = total 193 } 194 items := filteredFingerprints[start:end] 195 196 // 7. 返回 197 c.JSON(http.StatusOK, gin.H{ 198 "status": 0, 199 "message": "success", 200 "data": gin.H{ 201 "total": total, 202 "page": page, 203 "size": size, 204 "items": items, 205 }, 206 }) 207 } 208 209 // 创建指纹 210 func HandleCreateFingerprint(c *gin.Context) { 211 // 1. 解析请求体,获取file_content字段 212 type FingerprintUploadRequest struct { 213 FileContent string `json:"file_content" binding:"required"` 214 } 215 var req FingerprintUploadRequest 216 if err := c.ShouldBindJSON(&req); err != nil { 217 c.JSON(http.StatusBadRequest, gin.H{"status": 1, "message": "参数解析失败"}) 218 return 219 } 220 221 // 2. 解析YAML为parser.FingerPrint结构体 222 var fp parser.FingerPrint 223 if err := yaml.Unmarshal([]byte(req.FileContent), &fp); err != nil { 224 c.JSON(http.StatusBadRequest, gin.H{"status": 1, "message": "YAML解析失败: " + err.Error()}) 225 return 226 } 227 if fp.Info.Name == "" { 228 c.JSON(http.StatusBadRequest, gin.H{"status": 1, "message": "指纹名称不能为空"}) 229 return 230 } 231 232 if _, err := parser.InitFingerPrintFromData([]byte(req.FileContent)); err != nil { 233 c.JSON(http.StatusBadRequest, gin.H{"status": 1, "message": "指纹内容校验失败: " + err.Error()}) 234 return 235 } 236 237 if !isValidName(fp.Info.Name) { 238 c.JSON(http.StatusBadRequest, gin.H{"status": 1, "message": "指纹名称非法"}) 239 return 240 } 241 yamlPath, err := safeJoinPath("data/fingerprints", fp.Info.Name+".yaml") 242 if err != nil { 243 c.JSON(http.StatusBadRequest, gin.H{"status": 1, "message": "非法路径"}) 244 return 245 } 246 if _, err := os.Stat(yamlPath); err == nil { 247 c.JSON(http.StatusConflict, gin.H{"status": 1, "message": "指纹已存在"}) 248 return 249 } 250 251 if err := os.WriteFile(yamlPath, []byte(req.FileContent), 0644); err != nil { 252 c.JSON(http.StatusInternalServerError, gin.H{"status": 1, "message": "文件写入失败: " + err.Error()}) 253 return 254 } 255 256 c.JSON(http.StatusOK, gin.H{"status": 0, "message": "创建指纹成功"}) 257 } 258 259 // 批量删除指纹处理函数 260 type BatchDeleteRequest struct { 261 Name []string `json:"name"` 262 } 263 264 func HandleDeleteFingerprint(c *gin.Context) { 265 var req BatchDeleteRequest 266 if err := c.ShouldBindJSON(&req); err != nil || len(req.Name) == 0 { 267 c.JSON(http.StatusBadRequest, gin.H{"status": 1, "message": "参数错误", "data": nil}) 268 return 269 } 270 271 var deleted []string 272 var notFound []string 273 var invalid []string 274 275 for _, name := range req.Name { 276 // 使用已存在的合法性校验函数防止路径遍历攻击 277 if !isValidName(name) { 278 invalid = append(invalid, name) 279 continue 280 } 281 yamlPath, pathErr := safeJoinPath("data/fingerprints", name+".yaml") 282 if pathErr != nil { 283 invalid = append(invalid, name) 284 continue 285 } 286 if _, err := os.Stat(yamlPath); os.IsNotExist(err) { 287 notFound = append(notFound, name) 288 continue 289 } 290 if err := os.Remove(yamlPath); err == nil { 291 deleted = append(deleted, name) 292 } 293 } 294 295 msg := "删除完成" 296 if len(notFound) > 0 { 297 msg += ",部分指纹未找到: " + strings.Join(notFound, ", ") 298 } 299 if len(invalid) > 0 { 300 msg += ",部分指纹名称非法: " + strings.Join(invalid, ", ") 301 } 302 303 c.JSON(http.StatusOK, gin.H{ 304 "status": 0, 305 "message": msg, 306 "data": gin.H{ 307 "deleted": deleted, 308 "notFound": notFound, 309 }, 310 }) 311 } 312 313 // 编辑指纹处理函数 314 func HandleEditFingerprint(c *gin.Context) { 315 // 1. 获取原指纹名称 316 oldName := c.Param("name") 317 if oldName == "" { 318 c.JSON(http.StatusBadRequest, gin.H{"status": 1, "message": "指纹名称不能为空"}) 319 return 320 } 321 322 type FingerprintUploadRequest struct { 323 FileContent string `json:"file_content" binding:"required"` 324 } 325 var req FingerprintUploadRequest 326 if err := c.ShouldBindJSON(&req); err != nil { 327 c.JSON(http.StatusBadRequest, gin.H{"status": 1, "message": "参数解析失败"}) 328 return 329 } 330 // 2. 解析YAML为parser.FingerPrint结构体 331 var fp parser.FingerPrint 332 if err := yaml.Unmarshal([]byte(req.FileContent), &fp); err != nil { 333 c.JSON(http.StatusBadRequest, gin.H{"status": 1, "message": "YAML解析失败: " + err.Error()}) 334 return 335 } 336 if fp.Info.Name == "" { 337 c.JSON(http.StatusBadRequest, gin.H{"status": 1, "message": "指纹名称不能为空"}) 338 return 339 } 340 341 // 新增:用和读取时一致的解析逻辑做一次完整校验 342 if _, err := parser.InitFingerPrintFromData([]byte(req.FileContent)); err != nil { 343 c.JSON(http.StatusBadRequest, gin.H{"status": 1, "message": "指纹内容校验失败: " + err.Error()}) 344 return 345 } 346 347 // 3. 校验原文件是否存在 348 if !isValidName(oldName) || !isValidName(fp.Info.Name) { 349 c.JSON(http.StatusBadRequest, gin.H{"status": 1, "message": "指纹名称非法"}) 350 return 351 } 352 oldPath, err := safeJoinPath("data/fingerprints", oldName+".yaml") 353 if err != nil { 354 c.JSON(http.StatusBadRequest, gin.H{"status": 1, "message": "非法路径"}) 355 return 356 } 357 if _, err := os.Stat(oldPath); os.IsNotExist(err) { 358 c.JSON(http.StatusNotFound, gin.H{"status": 1, "message": "原指纹不存在"}) 359 return 360 } 361 newPath, err := safeJoinPath("data/fingerprints", fp.Info.Name+".yaml") 362 if err != nil { 363 c.JSON(http.StatusBadRequest, gin.H{"status": 1, "message": "非法路径"}) 364 return 365 } 366 367 // 4. 校验新文件名是否已存在(且不是原文件) 368 if newPath != oldPath { 369 if _, err := os.Stat(newPath); err == nil { 370 c.JSON(http.StatusConflict, gin.H{"status": 1, "message": "新指纹名称已存在"}) 371 return 372 } 373 } 374 375 // 5. 如果新旧文件名不同,删除原文件 376 if oldName != fp.Info.Name { 377 _ = os.Remove(oldPath) // 删除老文件 378 } 379 380 // 6. 写入新内容(新文件名) 381 if err := os.WriteFile(newPath, []byte(req.FileContent), 0644); err != nil { 382 c.JSON(http.StatusInternalServerError, gin.H{"status": 1, "message": "文件写入失败: " + err.Error()}) 383 return 384 } 385 386 c.JSON(http.StatusOK, gin.H{"status": 0, "message": "修改指纹成功"}) 387 } 388 389 // 漏洞库分页+条件查询接口 390 func HandleListVulnerabilities() gin.HandlerFunc { 391 return func(c *gin.Context) { 392 // 1. 解析分页和查询参数 393 pageStr := c.DefaultQuery("page", "1") 394 sizeStr := c.DefaultQuery("size", "20") 395 query := strings.ToLower(c.DefaultQuery("q", "")) 396 page, _ := strconv.Atoi(pageStr) 397 size, _ := strconv.Atoi(sizeStr) 398 if page < 1 { 399 page = 1 400 } 401 if size < 1 { 402 size = 10 403 } 404 405 engine := vulstruct.NewAdvisoryEngine() 406 // load from directory 407 dir := "data/vuln" 408 err := engine.LoadFromDirectory(dir) 409 if err != nil { 410 c.JSON(http.StatusInternalServerError, gin.H{"status": 1, "message": "加载漏洞库失败: " + err.Error()}) 411 return 412 } 413 filteredVuls := make([]vulstruct.VersionVul, 0) 414 if query == "" { 415 filteredVuls = engine.GetAll() 416 } else { 417 for _, vul := range engine.GetAll() { 418 if strings.Contains(strings.ToLower(vul.Info.CVEName), query) { 419 filteredVuls = append(filteredVuls, vul) 420 continue 421 } 422 if strings.Contains(strings.ToLower(vul.Info.Summary), query) { 423 filteredVuls = append(filteredVuls, vul) 424 continue 425 } 426 if strings.Contains(strings.ToLower(vul.Info.FingerPrintName), query) { 427 filteredVuls = append(filteredVuls, vul) 428 continue 429 } 430 if strings.Contains(strings.ToLower(vul.Info.Details), query) { 431 filteredVuls = append(filteredVuls, vul) 432 continue 433 } 434 for _, ref := range vul.References { 435 if strings.Contains(strings.ToLower(ref), query) { 436 filteredVuls = append(filteredVuls, vul) 437 break 438 } 439 } 440 } 441 } 442 // 5. 分页 443 total := len(filteredVuls) 444 start := (page - 1) * size 445 end := start + size 446 if start > total { 447 start = total 448 } 449 if end > total { 450 end = total 451 } 452 items := filteredVuls[start:end] 453 454 // 6. 返回 455 c.JSON(http.StatusOK, gin.H{ 456 "status": 0, 457 "message": "success", 458 "data": gin.H{ 459 "page": page, 460 "size": size, 461 "total": total, 462 "items": items, 463 }, 464 }) 465 } 466 } 467 468 // 添加漏洞信息(带严格校验) 469 func HandleCreateVulnerability() gin.HandlerFunc { 470 return func(c *gin.Context) { 471 // 1. 解析请求体,获取file_content 472 type VulnUploadRequest struct { 473 FileContent string `json:"file_content" binding:"required"` 474 } 475 var req VulnUploadRequest 476 if err := c.ShouldBindJSON(&req); err != nil { 477 c.JSON(http.StatusBadRequest, gin.H{"status": 1, "message": "参数解析失败"}) 478 return 479 } 480 481 // 2. 反序列化为vulstruct.VersionVul,校验CVE编号等必填字段 482 var vul vulstruct.VersionVul 483 if err := yaml.Unmarshal([]byte(req.FileContent), &vul); err != nil { 484 c.JSON(http.StatusBadRequest, gin.H{"status": 1, "message": "YAML解析失败: " + err.Error()}) 485 return 486 } 487 if vul.Info.CVEName == "" { 488 c.JSON(http.StatusBadRequest, gin.H{"status": 1, "message": "CVE编号不能为空"}) 489 return 490 } 491 if !isValidName(vul.Info.CVEName) { 492 c.JSON(http.StatusBadRequest, gin.H{"status": 1, "message": "CVE编号非法"}) 493 return 494 } 495 if vul.Info.FingerPrintName != "" && !isValidName(vul.Info.FingerPrintName) { 496 c.JSON(http.StatusBadRequest, gin.H{"status": 1, "message": "指纹分类名称非法"}) 497 return 498 } 499 500 // 4. 用vulstruct.NewAdvisoryEngine加载临时文件做完整业务校验 501 _, err := vulstruct.ReadVersionVul([]byte(req.FileContent)) 502 if err != nil { 503 c.JSON(http.StatusBadRequest, gin.H{"status": 1, "message": "漏洞内容校验失败: " + err.Error()}) 504 return 505 } 506 507 // 5. 校验通过后,正式写入到目标目录(如已存在则报冲突) 508 dir := "data/vuln" 509 if vul.Info.FingerPrintName != "" { 510 var pathErr error 511 dir, pathErr = safeJoinPath(dir, vul.Info.FingerPrintName) 512 if pathErr != nil { 513 c.JSON(http.StatusBadRequest, gin.H{"status": 1, "message": "非法路径"}) 514 return 515 } 516 } 517 if err := os.MkdirAll(dir, 0755); err != nil { 518 c.JSON(http.StatusInternalServerError, gin.H{"status": 1, "message": "创建目录失败: " + err.Error()}) 519 return 520 } 521 fileName := strings.ToUpper(vul.Info.CVEName) + ".yaml" 522 filePath, err := safeJoinPath(dir, fileName) 523 if err != nil { 524 c.JSON(http.StatusBadRequest, gin.H{"status": 1, "message": "非法路径"}) 525 return 526 } 527 if _, err := os.Stat(filePath); err == nil { 528 c.JSON(http.StatusConflict, gin.H{"status": 1, "message": "该CVE编号的漏洞已存在"}) 529 return 530 } 531 data, err := yaml.Marshal(&vul) 532 if err != nil { 533 c.JSON(http.StatusInternalServerError, gin.H{"status": 1, "message": "YAML序列化失败: " + err.Error()}) 534 return 535 } 536 if err := os.WriteFile(filePath, data, 0644); err != nil { 537 c.JSON(http.StatusInternalServerError, gin.H{"status": 1, "message": "文件写入失败: " + err.Error()}) 538 return 539 } 540 541 // 6. 返回结果 542 c.JSON(http.StatusOK, gin.H{"status": 0, "message": "创建漏洞库成功"}) 543 } 544 } 545 546 // 编辑漏洞处理函数 547 func HandleEditVulnerability(c *gin.Context) { 548 // 1. 获取原CVE编号 549 oldCVE := c.Param("cve") 550 if oldCVE == "" { 551 c.JSON(http.StatusBadRequest, gin.H{"status": 1, "message": "CVE编号不能为空"}) 552 return 553 } 554 if !isValidName(oldCVE) { 555 c.JSON(http.StatusBadRequest, gin.H{"status": 1, "message": "原CVE编号非法"}) 556 return 557 } 558 559 type VulnUploadRequest struct { 560 FileContent string `json:"file_content" binding:"required"` 561 } 562 var req VulnUploadRequest 563 if err := c.ShouldBindJSON(&req); err != nil { 564 c.JSON(http.StatusBadRequest, gin.H{"status": 1, "message": "参数解析失败"}) 565 return 566 } 567 // 2. 反序列化为vulstruct.VersionVul,校验CVE编号等必填字段 568 var vul vulstruct.VersionVul 569 if err := yaml.Unmarshal([]byte(req.FileContent), &vul); err != nil { 570 c.JSON(http.StatusBadRequest, gin.H{"status": 1, "message": "YAML解析失败: " + err.Error()}) 571 return 572 } 573 if vul.Info.CVEName == "" { 574 c.JSON(http.StatusBadRequest, gin.H{"status": 1, "message": "CVE编号不能为空"}) 575 return 576 } 577 if !isValidName(vul.Info.CVEName) { 578 c.JSON(http.StatusBadRequest, gin.H{"status": 1, "message": "CVE编号非法"}) 579 return 580 } 581 if vul.Info.FingerPrintName != "" && !isValidName(vul.Info.FingerPrintName) { 582 c.JSON(http.StatusBadRequest, gin.H{"status": 1, "message": "指纹分类名称非法"}) 583 return 584 } 585 // 4. 用vulstruct.NewAdvisoryEngine加载临时文件做完整业务校验 586 _, err := vulstruct.ReadVersionVul([]byte(req.FileContent)) 587 if err != nil { 588 c.JSON(http.StatusBadRequest, gin.H{"status": 1, "message": "漏洞内容校验失败: " + err.Error()}) 589 return 590 } 591 592 // 5. 在所有分类目录下查找原文件 593 var oldPath string 594 found := false 595 baseDir := "data/vuln" 596 _ = filepath.Walk(baseDir, func(path string, info os.FileInfo, err error) error { 597 if err == nil && !info.IsDir() && strings.EqualFold(info.Name(), strings.ToUpper(oldCVE)+".yaml") { 598 oldPath = path 599 found = true 600 return filepath.SkipDir // 找到就停止遍历 601 } 602 return nil 603 }) 604 if !found { 605 c.JSON(http.StatusNotFound, gin.H{"status": 1, "message": "原漏洞不存在"}) 606 return 607 } 608 609 // 6. 生成新文件路径 610 newDir := "data/vuln" 611 if vul.Info.FingerPrintName != "" { 612 newDir, err = safeJoinPath(newDir, vul.Info.FingerPrintName) 613 if err != nil { 614 c.JSON(http.StatusBadRequest, gin.H{"status": 1, "message": "非法路径"}) 615 return 616 } 617 } 618 if err := os.MkdirAll(newDir, 0755); err != nil { 619 c.JSON(http.StatusInternalServerError, gin.H{"status": 1, "message": "创建目录失败: " + err.Error()}) 620 return 621 } 622 newPath, err := safeJoinPath(newDir, strings.ToUpper(vul.Info.CVEName)+".yaml") 623 if err != nil { 624 c.JSON(http.StatusBadRequest, gin.H{"status": 1, "message": "非法路径"}) 625 return 626 } 627 628 // 7. 校验新文件名是否已存在(且不是原文件) 629 if newPath != oldPath { 630 if _, err := os.Stat(newPath); err == nil { 631 c.JSON(http.StatusConflict, gin.H{"status": 1, "message": "新CVE编号的漏洞已存在"}) 632 return 633 } 634 } 635 636 // 8. 删除原文件 637 if err := os.Remove(oldPath); err != nil { 638 c.JSON(http.StatusInternalServerError, gin.H{"status": 1, "message": "删除原文件失败: " + err.Error()}) 639 return 640 } 641 642 // 9. 写入新内容(新文件名/新目录) 643 data, err := yaml.Marshal(&vul) 644 if err != nil { 645 c.JSON(http.StatusInternalServerError, gin.H{"status": 1, "message": "YAML序列化失败: " + err.Error()}) 646 return 647 } 648 if err := os.WriteFile(newPath, data, 0644); err != nil { 649 c.JSON(http.StatusInternalServerError, gin.H{"status": 1, "message": "文件写入失败: " + err.Error()}) 650 return 651 } 652 653 c.JSON(http.StatusOK, gin.H{"status": 0, "message": "修改漏洞成功"}) 654 } 655 656 // 批量删除漏洞处理函数 657 type BatchDeleteVulnRequest struct { 658 CVEs []string `json:"cves"` 659 } 660 661 func HandleBatchDeleteVulnerabilities(c *gin.Context) { 662 var req BatchDeleteVulnRequest 663 if err := c.ShouldBindJSON(&req); err != nil || len(req.CVEs) == 0 { 664 c.JSON(http.StatusBadRequest, gin.H{"status": 1, "message": "参数解析失败或CVE列表为空"}) 665 return 666 } 667 668 baseDir := "data/vuln" 669 var notFound []string 670 var failed []string 671 672 for _, cve := range req.CVEs { 673 if !isValidName(cve) { 674 notFound = append(notFound, cve) 675 continue 676 } 677 found := false 678 _ = filepath.Walk(baseDir, func(path string, info os.FileInfo, err error) error { 679 if err == nil && !info.IsDir() && strings.EqualFold(info.Name(), strings.ToUpper(cve)+".yaml") { 680 // 找到就删除 681 if err := os.Remove(path); err != nil { 682 failed = append(failed, cve) 683 } 684 found = true 685 return filepath.SkipDir 686 } 687 return nil 688 }) 689 if !found { 690 notFound = append(notFound, cve) 691 } 692 } 693 694 if len(failed) > 0 { 695 c.JSON(500, gin.H{"status": 1, "message": "部分删除失败", "failed": failed}) 696 return 697 } 698 if len(notFound) > 0 { 699 c.JSON(404, gin.H{"status": 1, "message": "部分CVE未找到", "not_found": notFound}) 700 return 701 } 702 703 c.JSON(http.StatusOK, gin.H{"status": 0, "message": "批量删除成功"}) 704 } 705 706 // ================== 评测集管理接口 ================== 707 708 // 获取评测集列表,支持分页和名字模糊搜索 709 func HandleListEvaluations(c *gin.Context) { 710 // 1. 解析分页参数 711 pageStr := c.DefaultQuery("page", "1") 712 sizeStr := c.DefaultQuery("size", "20") 713 detail := c.DefaultQuery("detail", "false") 714 page, _ := strconv.Atoi(pageStr) 715 size, _ := strconv.Atoi(sizeStr) 716 if page < 1 { 717 page = 1 718 } 719 if size < 1 { 720 size = 10 721 } 722 723 // 2. 获取查询参数 724 nameQuery := strings.ToLower(c.DefaultQuery("q", "")) 725 726 // 3. 读取 data/eval/ 下所有JSON文件 727 var allEvaluations []EvaluationDataset 728 root := "data/eval" 729 filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error { 730 if err != nil { 731 return nil // 忽略错误 732 } 733 if !d.IsDir() && strings.HasSuffix(d.Name(), ".json") { 734 content, readErr := os.ReadFile(path) 735 if readErr == nil { 736 var eval EvaluationDataset 737 err = json.Unmarshal(content, &eval) 738 if err != nil { 739 log.Error(path, err.Error()) 740 return err 741 } 742 // 转换为摘要格式(不包含data字段) 743 if detail != "true" { 744 eval.Data = nil 745 } 746 allEvaluations = append(allEvaluations, eval) 747 } 748 } 749 return nil 750 }) 751 752 // 4. 条件过滤 753 var filteredEvaluations []EvaluationDataset 754 if nameQuery == "" { 755 filteredEvaluations = allEvaluations 756 } else { 757 for _, eval := range allEvaluations { 758 if strings.Contains(strings.ToLower(eval.Name), nameQuery) { 759 filteredEvaluations = append(filteredEvaluations, eval) 760 continue 761 } 762 if strings.Contains(strings.ToLower(eval.Description), nameQuery) { 763 filteredEvaluations = append(filteredEvaluations, eval) 764 continue 765 } 766 if strings.Contains(strings.ToLower(eval.Author), nameQuery) { 767 filteredEvaluations = append(filteredEvaluations, eval) 768 continue 769 } 770 // 搜索标签 771 for _, tag := range eval.Tags { 772 if strings.Contains(strings.ToLower(tag), nameQuery) { 773 filteredEvaluations = append(filteredEvaluations, eval) 774 break 775 } 776 } 777 } 778 } 779 780 // 5. 默认排序:按照Recommendation降序排列 781 sort.Slice(filteredEvaluations, func(i, j int) bool { 782 return filteredEvaluations[i].Recommendation > filteredEvaluations[j].Recommendation 783 }) 784 785 // 6. 分页 786 total := len(filteredEvaluations) 787 start := (page - 1) * size 788 end := start + size 789 if start > total { 790 start = total 791 } 792 if end > total { 793 end = total 794 } 795 items := filteredEvaluations[start:end] 796 797 // 7. 返回 798 c.JSON(http.StatusOK, gin.H{ 799 "status": 0, 800 "message": "success", 801 "data": gin.H{ 802 "total": total, 803 "page": page, 804 "size": size, 805 "items": items, 806 }, 807 }) 808 } 809 810 // 获取评测集详情,返回包含data的完整信息 811 func HandleGetEvaluationDetail(c *gin.Context) { 812 // 1. 获取评测集名称 813 name := c.Param("name") 814 if name == "" { 815 c.JSON(http.StatusBadRequest, gin.H{"status": 1, "message": "评测集名称不能为空"}) 816 return 817 } 818 819 // 2. 验证名称合法性 820 if !isValidName(name) { 821 c.JSON(http.StatusBadRequest, gin.H{"status": 1, "message": "评测集名称非法"}) 822 return 823 } 824 825 // 3. 读取评测集文件 826 var allEvaluations []EvaluationDataset 827 root := "data/eval" 828 filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error { 829 if err != nil { 830 return nil // 忽略错误 831 } 832 if !d.IsDir() && strings.HasSuffix(d.Name(), ".json") { 833 content, readErr := os.ReadFile(path) 834 if readErr == nil { 835 var eval EvaluationDataset 836 if parseErr := json.Unmarshal(content, &eval); parseErr == nil { 837 allEvaluations = append(allEvaluations, eval) 838 } 839 } 840 } 841 return nil 842 }) 843 844 for _, eval := range allEvaluations { 845 if eval.Name == name { 846 c.JSON(http.StatusOK, gin.H{ 847 "status": 0, 848 "message": "success", 849 "data": eval, 850 }) 851 return 852 } 853 } 854 855 // 5. 返回完整的评测集信息(包含data字段) 856 c.JSON(http.StatusOK, gin.H{ 857 "status": 0, 858 "message": "success", 859 "data": nil, 860 }) 861 } 862 863 // 创建评测集 864 func HandleCreateEvaluation(c *gin.Context) { 865 // 1. 解析请求体,获取file_content字段 866 type EvaluationUploadRequest struct { 867 FileContent string `json:"file_content" binding:"required"` 868 } 869 var req EvaluationUploadRequest 870 if err := c.ShouldBindJSON(&req); err != nil { 871 c.JSON(http.StatusBadRequest, gin.H{"status": 1, "message": "参数解析失败"}) 872 return 873 } 874 875 // 2. 解析JSON为EvaluationDataset结构体 876 var eval EvaluationDataset 877 if err := json.Unmarshal([]byte(req.FileContent), &eval); err != nil { 878 c.JSON(http.StatusBadRequest, gin.H{"status": 1, "message": "JSON解析失败: " + err.Error()}) 879 return 880 } 881 if eval.Name == "" { 882 c.JSON(http.StatusBadRequest, gin.H{"status": 1, "message": "评测集名称不能为空"}) 883 return 884 } 885 886 // 3. 验证数据完整性 887 if len(eval.Data) == 0 { 888 c.JSON(http.StatusBadRequest, gin.H{"status": 1, "message": "评测数据不能为空"}) 889 return 890 } 891 892 // 更新count字段为实际数据条数 893 eval.Count = len(eval.Data) 894 895 // 验证数据项 896 for i, item := range eval.Data { 897 if item.Prompt == "" { 898 c.JSON(http.StatusBadRequest, gin.H{"status": 1, "message": fmt.Sprintf("第%d条数据的prompt不能为空", i+1)}) 899 return 900 } 901 } 902 903 // 4. 检查评测集名称是否已存在 904 if !isValidName(eval.Name) { 905 c.JSON(http.StatusBadRequest, gin.H{"status": 1, "message": "评测集名称非法,只允许字母、数字、下划线和横线"}) 906 return 907 } 908 jsonPath, err := safeJoinPath("data/eval", eval.Name+".json") 909 if err != nil { 910 c.JSON(http.StatusBadRequest, gin.H{"status": 1, "message": "非法路径"}) 911 return 912 } 913 if _, err := os.Stat(jsonPath); err == nil { 914 c.JSON(http.StatusConflict, gin.H{"status": 1, "message": "评测集已存在"}) 915 return 916 } 917 918 // 5. 序列化并写入JSON文件 919 updatedContent, err := json.MarshalIndent(eval, "", " ") 920 if err != nil { 921 c.JSON(http.StatusInternalServerError, gin.H{"status": 1, "message": "JSON序列化失败: " + err.Error()}) 922 return 923 } 924 925 if err := os.WriteFile(jsonPath, updatedContent, 0644); err != nil { 926 c.JSON(http.StatusInternalServerError, gin.H{"status": 1, "message": "文件写入失败: " + err.Error()}) 927 return 928 } 929 930 // 6. 返回精简响应 931 c.JSON(http.StatusOK, gin.H{"status": 0, "message": "创建评测集成功"}) 932 } 933 934 // 编辑评测集处理函数 935 func HandleEditEvaluation(c *gin.Context) { 936 // 1. 获取原评测集名称 937 oldName := c.Param("name") 938 if oldName == "" { 939 c.JSON(http.StatusBadRequest, gin.H{"status": 1, "message": "评测集名称不能为空"}) 940 return 941 } 942 943 type EvaluationUploadRequest struct { 944 FileContent string `json:"file_content" binding:"required"` 945 } 946 var req EvaluationUploadRequest 947 if err := c.ShouldBindJSON(&req); err != nil { 948 c.JSON(http.StatusBadRequest, gin.H{"status": 1, "message": "参数解析失败"}) 949 return 950 } 951 952 // 2. 解析JSON为EvaluationDataset结构体 953 var eval EvaluationDataset 954 if err := json.Unmarshal([]byte(req.FileContent), &eval); err != nil { 955 c.JSON(http.StatusBadRequest, gin.H{"status": 1, "message": "JSON解析失败: " + err.Error()}) 956 return 957 } 958 if eval.Name == "" { 959 c.JSON(http.StatusBadRequest, gin.H{"status": 1, "message": "评测集名称不能为空"}) 960 return 961 } 962 963 // 验证数据完整性 964 if len(eval.Data) == 0 { 965 c.JSON(http.StatusBadRequest, gin.H{"status": 1, "message": "评测数据不能为空"}) 966 return 967 } 968 969 // 更新count字段为实际数据条数 970 eval.Count = len(eval.Data) 971 972 // 验证数据项 973 for i, item := range eval.Data { 974 if item.Prompt == "" { 975 c.JSON(http.StatusBadRequest, gin.H{"status": 1, "message": fmt.Sprintf("第%d条数据的prompt不能为空", i+1)}) 976 return 977 } 978 } 979 980 // 3. 校验原文件是否存在 981 if !isValidName(oldName) || !isValidName(eval.Name) { 982 c.JSON(http.StatusBadRequest, gin.H{"status": 1, "message": "评测集名称非法,只允许字母、数字、下划线和横线"}) 983 return 984 } 985 oldPath, err := safeJoinPath("data/eval", oldName+".json") 986 if err != nil { 987 c.JSON(http.StatusBadRequest, gin.H{"status": 1, "message": "非法路径"}) 988 return 989 } 990 if _, err := os.Stat(oldPath); os.IsNotExist(err) { 991 c.JSON(http.StatusNotFound, gin.H{"status": 1, "message": "原评测集不存在"}) 992 return 993 } 994 newPath, err := safeJoinPath("data/eval", eval.Name+".json") 995 if err != nil { 996 c.JSON(http.StatusBadRequest, gin.H{"status": 1, "message": "非法路径"}) 997 return 998 } 999 1000 // 4. 校验新文件名是否已存在(且不是原文件) 1001 if newPath != oldPath { 1002 if _, err := os.Stat(newPath); err == nil { 1003 c.JSON(http.StatusConflict, gin.H{"status": 1, "message": "新评测集名称已存在"}) 1004 return 1005 } 1006 } 1007 1008 // 5. 如果新旧文件名不同,删除原文件 1009 if oldName != eval.Name { 1010 _ = os.Remove(oldPath) // 删除老文件 1011 } 1012 1013 // 6. 序列化并写入新内容(新文件名) 1014 updatedContent, err := json.MarshalIndent(eval, "", " ") 1015 if err != nil { 1016 c.JSON(http.StatusInternalServerError, gin.H{"status": 1, "message": "JSON序列化失败: " + err.Error()}) 1017 return 1018 } 1019 1020 if err := os.WriteFile(newPath, updatedContent, 0644); err != nil { 1021 c.JSON(http.StatusInternalServerError, gin.H{"status": 1, "message": "文件写入失败: " + err.Error()}) 1022 return 1023 } 1024 1025 c.JSON(http.StatusOK, gin.H{"status": 0, "message": "修改评测集成功"}) 1026 } 1027 1028 // 批量删除评测集处理函数 1029 type BatchDeleteEvaluationRequest struct { 1030 Names []string `json:"names"` 1031 } 1032 1033 func HandleDeleteEvaluation(c *gin.Context) { 1034 var req BatchDeleteEvaluationRequest 1035 if err := c.ShouldBindJSON(&req); err != nil || len(req.Names) == 0 { 1036 c.JSON(http.StatusBadRequest, gin.H{"status": 1, "message": "参数错误", "data": nil}) 1037 return 1038 } 1039 1040 for _, name := range req.Names { 1041 // 使用已存在的合法性校验函数防止路径遍历攻击 1042 if !isValidName(name) { 1043 c.JSON(http.StatusBadRequest, gin.H{"status": 1, "message": "评测集名称非法,只允许字母、数字、下划线和横线"}) 1044 return 1045 } 1046 jsonPath, pathErr := safeJoinPath("data/eval", name+".json") 1047 if pathErr != nil { 1048 c.JSON(http.StatusBadRequest, gin.H{"status": 1, "message": "非法路径"}) 1049 return 1050 } 1051 if _, err := os.Stat(jsonPath); os.IsNotExist(err) { 1052 c.JSON(http.StatusBadRequest, gin.H{"status": 1, "message": "评测集不存在"}) 1053 return 1054 } 1055 if err := os.Remove(jsonPath); err != nil { 1056 c.JSON(http.StatusBadRequest, gin.H{"status": 1, "message": "删除失败: " + err.Error()}) 1057 return 1058 } 1059 } 1060 c.JSON(http.StatusOK, gin.H{ 1061 "status": 0, 1062 "message": "ok", 1063 }) 1064 }