runner.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 runner 实现运行器 20 package runner 21 22 import ( 23 "bufio" 24 "fmt" 25 "math" 26 "net/http" 27 "os" 28 "strconv" 29 "strings" 30 "sync/atomic" 31 "time" 32 33 "github.com/Tencent/AI-Infra-Guard/common/fingerprints/parser" 34 "github.com/Tencent/AI-Infra-Guard/common/fingerprints/preload" 35 "github.com/Tencent/AI-Infra-Guard/common/utils" 36 "github.com/Tencent/AI-Infra-Guard/internal/gologger" 37 "github.com/Tencent/AI-Infra-Guard/internal/options" 38 "github.com/Tencent/AI-Infra-Guard/pkg/httpx" 39 "github.com/Tencent/AI-Infra-Guard/pkg/vulstruct" 40 41 "github.com/liushuochen/gotable" 42 "github.com/logrusorgru/aurora" 43 "github.com/projectdiscovery/fastdialer/fastdialer" 44 "github.com/projectdiscovery/hmap/store/hybrid" 45 "github.com/remeh/sizedwaitgroup" 46 "go.uber.org/ratelimit" 47 48 // automatic fd max increase if running as root 49 _ "github.com/projectdiscovery/fdmax/autofdmax" 50 ) 51 52 // Runner struct 保存运行指纹扫描所需的所有组件 53 type Runner struct { 54 Options *options.Options // 配置选项 55 hp *httpx.HTTPX // HTTP 客户端 56 hm *hybrid.HybridMap // 混合存储 57 rateLimiter ratelimit.Limiter // 速率限制器 58 result chan HttpResult // 结果通道 59 fpEngine *preload.Runner // 指纹引擎 60 advEngine *vulstruct.AdvisoryEngine // 漏洞建议引擎 61 total int // 总目标数 62 done chan struct{} // 用于优雅关闭的通道 63 callback func(interface{}) 64 } 65 66 type Step01 struct { 67 Text string 68 } 69 70 // New 初始化一个新的 Runner 实例 71 func New(options2 *options.Options) (*Runner, error) { 72 runner := &Runner{ 73 Options: options2, 74 total: 0, 75 done: make(chan struct{}), // 初始化done通道用于优雅关闭 76 } 77 78 // 依次初始化各个组件 79 if err := runner.initStorage(); err != nil { 80 return nil, err 81 } 82 83 if err := runner.processTargets(); err != nil { 84 return nil, err 85 } 86 87 if err := runner.initComponents(); err != nil { 88 return nil, err 89 } 90 91 if err := runner.initFingerprints(); err != nil { 92 return nil, err 93 } 94 95 if err := runner.initVulnerabilityDB(); err != nil { 96 return nil, err 97 } 98 99 return runner, nil 100 } 101 102 // initFingerprints initializes the fingerprint detection engine 103 func (r *Runner) initFingerprints() error { 104 options2 := r.Options 105 fps := make([]parser.FingerPrint, 0) 106 var err error 107 if r.Options.LoadRemote { 108 // 从远程加载 109 fps, err = utils.LoadRemoteFingerPrints(options2.FPTemplates) 110 if err != nil { 111 return err 112 } 113 } else { 114 // 初始化指纹 115 if !utils.IsFileExists(options2.FPTemplates) { 116 gologger.Fatalf("没有指定指纹模板文件:%s", options2.FPTemplates) 117 } 118 if utils.IsDir(options2.FPTemplates) { 119 files, err := utils.ScanDir(options2.FPTemplates) 120 if err != nil { 121 gologger.Fatalf("无法扫描指纹模板目录:%s", options2.FPTemplates) 122 } 123 for _, filename := range files { 124 if !strings.HasSuffix(filename, ".yaml") { 125 continue 126 } 127 data, err := os.ReadFile(filename) 128 if err != nil { 129 gologger.Fatalf("无法读取指纹模板文件:%s", filename) 130 } 131 fp, err := parser.InitFingerPrintFromData(data) 132 if err != nil { 133 gologger.WithError(err).Fatalf("无法解析指纹模板文件:%s", filename) 134 } 135 fps = append(fps, *fp) 136 } 137 } else { 138 data, err := os.ReadFile(options2.FPTemplates) 139 if err != nil { 140 gologger.Fatalf("无法读取指纹模板文件:%s", options2.FPTemplates) 141 } 142 fp, err := parser.InitFingerPrintFromData(data) 143 if err != nil { 144 gologger.Fatalf("无法解析指纹模板文件:%s", options2.FPTemplates) 145 } 146 fps = append(fps, *fp) 147 } 148 } 149 if len(fps) == 0 { 150 gologger.Fatalf("没有指定指纹模板") 151 } 152 r.fpEngine = preload.New(r.hp, fps) 153 //text := fmt.Sprintf("加载指纹库,数量:%d", len(fps)+len(preload.CollectedFpReqs())) 154 text := fmt.Sprintf("Loading fingerprints:%d", len(fps)+len(preload.CollectedFpReqs())) 155 gologger.Infoln(text) 156 if r.Options.Callback != nil { 157 r.Options.Callback(Step01{Text: text}) 158 } 159 160 r.result = make(chan HttpResult) 161 return nil 162 } 163 164 // initStorage 初始化混合存储 165 func (r *Runner) initStorage() error { 166 hm, err := hybrid.New(hybrid.DefaultDiskOptions) 167 if err != nil { 168 return fmt.Errorf("could not create temporary input file: %s", err) 169 } 170 r.hm = hm 171 return nil 172 } 173 174 // processTargetList 处理目标列表 175 // 支持处理CIDR格式的IP段和单个目标 176 func (r *Runner) processTargetList(targets []string) { 177 for _, t := range targets { 178 if utils.IsCIDR(t) { 179 // 处理CIDR格式 180 cidrIps, err := IPAddresses(t) 181 if err != nil { 182 r.hm.Set(t, nil) 183 r.total++ 184 } else { 185 // 展开CIDR中的所有IP 186 for _, ip := range cidrIps { 187 r.hm.Set(ip, nil) 188 r.total++ 189 } 190 } 191 } else { 192 // 处理单个目标 193 r.hm.Set(t, nil) 194 r.total++ 195 } 196 } 197 } 198 199 // processTargets 处理所有输入的目标 200 // 支持从命令行参数和文件读取目标 201 func (r *Runner) processTargets() error { 202 // 处理命令行指定的目标 203 if r.Options.Target != nil { 204 r.processTargetList(r.Options.Target) 205 } 206 207 // 处理目标文件 208 if r.Options.TargetFile != "" { 209 if utils.IsFileExists(r.Options.TargetFile) { 210 file, err := os.Open(r.Options.TargetFile) 211 if err != nil { 212 return err 213 } 214 defer file.Close() 215 scanner := bufio.NewScanner(file) 216 targets := make([]string, 0) 217 for scanner.Scan() { 218 t := strings.TrimSpace(scanner.Text()) 219 if t != "" { 220 targets = append(targets, t) 221 } 222 } 223 r.processTargetList(targets) 224 } 225 } 226 227 if r.Options.LocalScan { 228 op, err := utils.GetLocalOpenPorts() 229 if err != nil { 230 gologger.Fatalf("get local open port failed,err:%s", err) 231 } 232 var targets []string 233 for _, p := range op { 234 targets = append(targets, p.Address+":"+strconv.Itoa(p.Port)) 235 } 236 r.processTargetList(targets) 237 } 238 if r.total > 0 { 239 gologger.Infof("加载目标数量:%d", r.total) 240 } 241 return nil 242 } 243 244 // initComponents 初始化基础组件 245 // 包括速率限制器、HTTP客户端等 246 func (r *Runner) initComponents() error { 247 // 初始化速率限制器 248 r.rateLimiter = ratelimit.New(r.Options.RateLimit) 249 r.result = make(chan HttpResult) 250 251 // 初始化DNS解析器 252 dialer, err := fastdialer.NewDialer(fastdialer.DefaultOptions) 253 if err != nil { 254 return fmt.Errorf("could not create resolver cache: %s", err) 255 } 256 257 // 配置HTTP客户端选项 258 httpOptions := &httpx.HTTPOptions{ 259 Timeout: time.Duration(r.Options.TimeOut) * time.Second, 260 RetryMax: 1, 261 FollowRedirects: true, 262 HTTPProxy: r.Options.ProxyURL, 263 Unsafe: false, 264 DefaultUserAgent: httpx.GetRandomUserAgent(), 265 Dialer: dialer, 266 CustomHeaders: r.Options.Headers, 267 } 268 269 // 创建HTTP客户端 270 hp, err := httpx.NewHttpx(httpOptions) 271 if err != nil { 272 return err 273 } 274 r.hp = hp 275 return nil 276 } 277 278 // extractContent 处理 HTTP 响应并提取相关信息 279 func (r *Runner) extractContent(fullUrl string, resp *httpx.Response, respTime string) { 280 builder := &strings.Builder{} 281 builder.WriteString(fullUrl) 282 283 builder.WriteString(" [") 284 // 根据状态码设置不同颜色 285 switch { 286 case resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusMultipleChoices: 287 builder.WriteString(aurora.Green(strconv.Itoa(resp.StatusCode)).String()) // 2xx 绿色 288 case resp.StatusCode >= http.StatusMultipleChoices && resp.StatusCode < http.StatusBadRequest: 289 builder.WriteString(aurora.Yellow(strconv.Itoa(resp.StatusCode)).String()) // 3xx 黄色 290 case resp.StatusCode >= http.StatusBadRequest && resp.StatusCode < http.StatusInternalServerError: 291 builder.WriteString(aurora.Red(strconv.Itoa(resp.StatusCode)).String()) // 4xx 红色 292 case resp.StatusCode > http.StatusInternalServerError: 293 builder.WriteString(aurora.Bold(aurora.Yellow(strconv.Itoa(resp.StatusCode))).String()) // 5xx 加粗黄色 294 } 295 builder.WriteString("] ") 296 // 检测是否跳转,跳转则转过去,新建一个结果 297 if resp.StatusCode >= http.StatusMultipleChoices && resp.StatusCode < http.StatusBadRequest { 298 newUrl := resp.GetHeader("Location") 299 _ = r.runDomainRequest(newUrl) 300 } 301 302 title := resp.Title 303 builder.WriteString(" [") 304 builder.WriteString(title) 305 builder.WriteString("] ") 306 307 iconData, err := utils.GetFaviconBytes(r.hp, fullUrl, resp.Data) 308 faviconHash := utils.FaviconHash(iconData) 309 if err != nil { 310 faviconHash = 0 311 } 312 // 内部指纹 313 fpResults := r.fpEngine.RunFpReqs(fullUrl, 10, faviconHash) 314 ads := make([]vulstruct.VersionVul, 0) 315 isInternal := true 316 if strings.Contains(fullUrl, "127.0.0.1") { 317 isInternal = false 318 } 319 if strings.Contains(fullUrl, "localhost") { 320 isInternal = false 321 } 322 if len(fpResults) > 0 { 323 for _, item := range fpResults { 324 builder.WriteString("[") 325 builder.WriteString(item.Name) 326 if item.Type != "" { 327 builder.WriteString(":") 328 builder.WriteString(item.Type) 329 } 330 if item.Version != "" { 331 builder.WriteString(":") 332 builder.WriteString(item.Version) 333 } 334 builder.WriteString("]") 335 builder.WriteString(" ") 336 337 advisories, err := r.advEngine.GetAdvisories(item.Name, item.Version, isInternal) 338 if err != nil { 339 gologger.Errorf("get advisory error: %s", err) 340 } else { 341 // 添加漏洞信息 342 ads = append(ads, advisories...) 343 } 344 builder.WriteString(" ") 345 } 346 } 347 348 result := HttpResult{ 349 URL: fullUrl, 350 Title: title, 351 ContentLength: resp.ContentLength, 352 StatusCode: resp.StatusCode, 353 ResponseTime: respTime, 354 Fingers: fpResults, 355 s: builder.String(), 356 Advisories: ads, 357 Resp: resp.DataStr, 358 } 359 r.result <- result 360 } 361 362 // runHostRequest 尝试使用 HTTP 和 HTTPS 连接到主机 363 func (r *Runner) runHostRequest(domain string) error { 364 retried := false 365 protocol := httpx.HTTP 366 retry: 367 fullUrl := fmt.Sprintf("%s://%s", protocol, domain) 368 timeStart := time.Now() 369 headers := map[string]string{ 370 "tr": "a2802f09d2ddb7830a6f4b00910ab4f0", 371 } 372 resp, err := r.hp.Get(fullUrl, headers) 373 if err != nil { 374 if !retried { 375 if protocol == httpx.HTTP { 376 protocol = httpx.HTTPS 377 } else { 378 protocol = httpx.HTTP 379 } 380 retried = true 381 goto retry 382 } 383 return err 384 } 385 r.extractContent(fullUrl, resp, time.Since(timeStart).String()) 386 return nil 387 } 388 389 // runDomainRequest makes a request to a specific URL and processes the response 390 func (r *Runner) runDomainRequest(fullUrl string) error { 391 timeStart := time.Now() 392 reqUrl := fullUrl 393 headers := map[string]string{ 394 "tr": "a2802f09d2ddb7830a6f4b00910ab4f0", 395 } 396 resp, err := r.hp.Get(reqUrl, headers) 397 if err != nil { 398 return err 399 } 400 r.extractContent(reqUrl, resp, time.Since(timeStart).String()) 401 return nil 402 } 403 404 // Close cleans up resources used by the Runner 405 func (r *Runner) Close() { 406 r.hp.Options.Dialer.Close() 407 _ = r.hm.Close() 408 } 409 410 func (r *Runner) callbackProcess(current, total int) { 411 if r.Options.Callback != nil { 412 r.Options.Callback(CallbackProcessInfo{ 413 Current: current, 414 Total: total, 415 }) 416 } 417 } 418 419 // RunEnumeration 开始扫描所有目标 420 func (r *Runner) RunEnumeration() { 421 // 检查是否有输入目标 422 if r.total == 0 { 423 gologger.Fatalf("没有指定输入,输入 -h 查看帮助") 424 return 425 } 426 r.callbackProcess(0, r.total) 427 428 // 启动输出处理协程 429 outputWg := sizedwaitgroup.New(1) 430 outputWg.Add() 431 go r.handleOutput(&outputWg) 432 433 timeStart := time.Now() 434 wg := sizedwaitgroup.New(r.Options.RateLimit) 435 var numTarget uint64 = 0 436 437 r.hm.Scan(func(k, _ []byte) error { 438 wg.Add() 439 target := string(k) 440 if !strings.HasPrefix(target, "http") { 441 go func() { 442 defer wg.Done() 443 r.rateLimiter.Take() 444 err := r.runHostRequest(target) 445 if err != nil { 446 if r.Options.Callback != nil { 447 r.Options.Callback(CallbackErrorInfo{ 448 Target: target, 449 Error: err, 450 }) 451 } 452 } 453 atomic.AddUint64(&numTarget, 1) 454 r.callbackProcess(int(atomic.LoadUint64(&numTarget)), r.total) 455 }() 456 } else { 457 go func() { 458 defer wg.Done() 459 r.rateLimiter.Take() 460 err := r.runDomainRequest(target) 461 if err != nil { 462 if r.Options.Callback != nil { 463 r.Options.Callback(CallbackErrorInfo{ 464 Target: target, 465 Error: err, 466 }) 467 } 468 } 469 atomic.AddUint64(&numTarget, 1) 470 r.callbackProcess(int(atomic.LoadUint64(&numTarget)), r.total) 471 }() 472 } 473 return nil 474 }) 475 wg.Wait() 476 close(r.result) 477 outputWg.Wait() 478 duration := time.Since(timeStart) 479 gologger.Infof("扫描完成~耗时:%s", utils.Duration2String(duration)) 480 } 481 482 // handleOutput 处理扫描结果的输出 483 func (r *Runner) handleOutput(wg *sizedwaitgroup.SizedWaitGroup) { 484 defer wg.Done() 485 486 f, err := r.createOutputFile() 487 if err != nil { 488 gologger.Fatalf("创建输出文件失败: %v", err) 489 return 490 } 491 if f != nil { 492 defer f.Close() 493 } 494 var results []HttpResult 495 for result := range r.result { 496 results = append(results, result) 497 r.writeResult(f, result) 498 } 499 // summary table 500 if len(results) > 0 { 501 table, err := gotable.Create("Target", "StatusCode", "Title", "FingerPrint") 502 if err != nil { 503 gologger.Errorf("create table error: %v", err) 504 return 505 } 506 vulTable, err := gotable.Create("CVE", "Severity", "VulName", "Target", "Suggestions") 507 if err != nil { 508 gologger.Errorf("create table error:%v", err) 509 return 510 } 511 var showVulTable bool = false 512 for _, row := range results { 513 data := make(map[string]string) 514 var fpString string = "" 515 for _, fp := range row.Fingers { 516 fpString += fp.Name 517 if fp.Type != "" { 518 fpString += ":" + fp.Type 519 } 520 if fp.Version != "" { 521 fpString += ":" + fp.Version 522 } 523 } 524 data = map[string]string{ 525 "Target": row.URL, 526 "StatusCode": fmt.Sprintf("%d", row.StatusCode), 527 "Title": row.Title, 528 "FingerPrint": fpString, 529 } 530 table.AddRow(data) 531 532 // write into vulTable 533 for _, ad := range row.Advisories { 534 showVulTable = true 535 var adRow = []string{ 536 ad.Info.CVEName, 537 ad.Info.Severity, 538 ad.Info.Summary, 539 row.URL, 540 ad.Info.SecurityAdvise, 541 } 542 vulTable.AddRow(adRow) 543 } 544 } 545 fmt.Println("Application Summary:") 546 fmt.Println(table.String()) 547 if showVulTable { 548 fmt.Println("Vulnerability Summary:") 549 fmt.Println(vulTable.String()) 550 } 551 } 552 553 if r.Options.Callback != nil { 554 advies := make([]vulstruct.Info, 0) 555 for _, item := range results { 556 for _, ad := range item.Advisories { 557 advies = append(advies, ad.Info) 558 } 559 } 560 score := r.CalcSecScore(advies) 561 r.Options.Callback(score) 562 } 563 } 564 565 // createOutputFile 创建输出文件 566 func (r *Runner) createOutputFile() (*os.File, error) { 567 if r.Options.Output == "" { 568 return nil, nil 569 } 570 return os.OpenFile(r.Options.Output, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) 571 } 572 573 // writeResult 写入扫描结果 574 func (r *Runner) writeResult(f *os.File, result HttpResult) { 575 fmt.Println(result.s) 576 if f != nil { 577 _, _ = f.WriteString(result.s + "\n") 578 } 579 if r.Options.Callback != nil { 580 vuls := make([]vulstruct.Info, 0) 581 for _, item := range result.Advisories { 582 vuls = append(vuls, item.Info) 583 } 584 var fpString string = "" 585 for _, fp := range result.Fingers { 586 fpString += fp.Name 587 if fp.Type != "" { 588 fpString += ":" + fp.Type 589 } 590 if fp.Version != "" { 591 fpString += ":" + fp.Version 592 } 593 } 594 if r.Options.Callback != nil { 595 r.Options.Callback(CallbackScanResult{ 596 TargetURL: result.URL, 597 StatusCode: result.StatusCode, 598 Title: result.Title, 599 Fingerprint: fpString, 600 Vulnerabilities: vuls, 601 Resp: result.Resp, 602 }) 603 } 604 } 605 if len(result.Advisories) > 0 { 606 fmt.Println("\n存在漏洞:") 607 for _, item := range result.Advisories { 608 builder := strings.Builder{} 609 builderFile := strings.Builder{} 610 serverity := item.Info.Severity 611 name := item.Info.CVEName 612 if serverity == "HIGH" || serverity == "CRITICAL" { 613 builder.WriteString(aurora.Red(fmt.Sprintf("%s [%s]", name, serverity)).String()) // 高危红色 614 } else if serverity == "MEDIUM" { 615 builder.WriteString(aurora.Yellow(fmt.Sprintf("%s [%s]", name, serverity)).String()) // 中危黄色 616 } else { 617 builder.WriteString(aurora.Bold(fmt.Sprintf("%s [%s]", name, serverity)).String()) // 低危加粗 618 } 619 builderFile.WriteString(fmt.Sprintf("%s [%s]\n", name, serverity)) 620 builder.WriteString(": " + item.Info.Summary + "\n" + item.Info.Details + "\n") 621 builderFile.WriteString(": " + item.Info.Summary + "\n" + item.Info.Details + "\n") 622 if len(item.Info.SecurityAdvise) > 0 { 623 builder.WriteString("修复建议: " + item.Info.SecurityAdvise + "\n") 624 builderFile.WriteString("修复建议: " + item.Info.SecurityAdvise + "\n") 625 } 626 fmt.Println(builder.String()) 627 _, _ = f.WriteString(builderFile.String() + "\n") 628 } 629 } 630 } 631 632 // GetFpAndVulList 获取指纹和漏洞列表 633 func (r *Runner) GetFpAndVulList() []FpInfos { 634 fingerprints := make([]parser.FingerPrint, 0) 635 for _, fp := range r.fpEngine.GetFps() { 636 fp2 := fp 637 fingerprints = append(fingerprints, fp2) 638 } 639 640 fps := make([]FpInfos, 0) 641 for _, fp := range fingerprints { 642 ads, err := r.advEngine.GetAdvisories(fp.Info.Name, "", false) 643 if err != nil { 644 gologger.WithError(err).Errorln("获取漏洞列表失败", fp) 645 continue 646 } 647 fps = append(fps, FpInfos{ 648 FpName: fp.Info.Name, 649 Vuls: ads, 650 Desc: fp.Info.Desc, 651 }) 652 } 653 return fps 654 } 655 656 // ShowFpAndVulList displays the list of available fingerprints and vulnerabilities 657 // 显示指纹和漏洞列表 658 func (r *Runner) ShowFpAndVulList(vul bool) { 659 data := r.GetFpAndVulList() 660 if vul { 661 gologger.Infoln("漏洞列表:") 662 table, err := gotable.Create("组件名称", "组件简介", "漏洞数量") 663 if err != nil { 664 gologger.Errorf("create table error: %v", err) 665 return 666 } 667 for _, item := range data { 668 table.AddRow([]string{item.FpName, item.Desc, strconv.Itoa(len(item.Vuls))}) 669 } 670 fmt.Println(table) 671 } 672 } 673 674 // initVulnerabilityDB initializes the vulnerability advisory engine 675 func (r *Runner) initVulnerabilityDB() error { 676 engine := vulstruct.NewAdvisoryEngine() 677 var err error 678 if r.Options.LoadRemote { 679 // load from hostname 680 err = engine.LoadFromHost(r.Options.AdvTemplates) 681 } else { 682 // load from directory 683 vulDir := strings.TrimRight(r.Options.AdvTemplates, "/") 684 if r.Options.Language == "en" { 685 vulDir = vulDir + "_en" 686 } 687 err = engine.LoadFromDirectory(vulDir) 688 } 689 if err != nil { 690 gologger.Fatalf("无法初始化漏洞库:%s", err) 691 } 692 r.advEngine = engine 693 // Load vulnerability version database 694 text := fmt.Sprintf("Loading vulnerability database, count:%d", r.advEngine.GetCount()) 695 gologger.Infoln(text) 696 if r.Options.Callback != nil { 697 r.Options.Callback(Step01{Text: text}) 698 } 699 return nil 700 } 701 702 // CalcSecScore 计算安全分数 703 func (r *Runner) CalcSecScore(advisories []vulstruct.Info) CallbackReportInfo { 704 var total, high, middle, low int = 0, 0, 0, 0 705 total = len(advisories) 706 for _, item := range advisories { 707 severity := strings.ToLower(strings.TrimSpace(item.Severity)) 708 if severity == "high" || severity == "critical" || severity == "高危" || severity == "严重" { 709 high++ 710 } else if severity == "medium" || severity == "中危" { 711 middle++ 712 } else { 713 low++ 714 } 715 } 716 if total == 0 { 717 return CallbackReportInfo{ 718 SecScore: 100, 719 HighRisk: 0, 720 MediumRisk: 0, 721 LowRisk: 0, 722 } 723 } 724 // 计算加权风险比例 725 weightedRisk := (float64(high)/float64(total))*0.7 + 726 (float64(middle)/float64(total))*0.5 + 727 (float64(low)/float64(total))*0.3 728 729 // 计算安全评分(百分制) 730 safetyScore := 100 - weightedRisk*100 731 732 // 确保评分在0-100范围内 733 if safetyScore < 0 { 734 safetyScore = 0 735 } 736 if safetyScore >= 100 { 737 safetyScore = 100 738 } 739 740 ret := CallbackReportInfo{ 741 SecScore: int(math.Round(safetyScore)), 742 HighRisk: high, 743 MediumRisk: middle, 744 LowRisk: low, 745 } 746 return ret 747 }