/ common / runner / runner.go
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  }