refactor: 4项架构优化 — CEL缓存/POC隔离/服务缓存/结果统一

1. CEL 表达式编译缓存
   - 新增 CelProgCache,同一 POC 的所有规则/参数组合共享编译后的 Program
   - clusterpoc 热路径上消除重复的 Compile+Program 调用

2. POC 全局状态消除
   - allPocs/pocLoaded 全局变量改为 pocStore 按 PocPath 缓存
   - 不同 PocPath 的扫描独立加载,Web API 并发场景不再互相覆盖

3. serviceCache 下沉到 per-session State
   - 服务识别缓存从包级全局 map 迁移到 State.serviceCache (sync.Map)
   - BaseScanStrategy 通过 SetState 注入 session state
   - 消除多个并发扫描之间的服务识别缓存串台

4. POC 结果输出路径统一
   - 提取 buildVulnDetails/buildVulnLogMsg/saveVulnResult 三个公共函数
   - CheckMultiPoc 和 recordVulnerabilityResult 共用统一的结果构造逻辑
   - 消除 details 字段名不一致和日志格式差异
This commit is contained in:
ZacharyZcR
2026-06-14 22:23:49 +08:00
parent a115499793
commit d4f4e65dec
10 changed files with 261 additions and 230 deletions
+57 -64
View File
@@ -13,7 +13,6 @@ import (
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/shadow1ng/fscan/common"
@@ -42,30 +41,22 @@ var (
//go:embed pocs
var pocsFS embed.FS
var (
pocMu sync.Mutex
pocLoaded atomic.Bool
allPocs []*lib.Poc
cachedPocPath string
)
// pocStore 按 PocPath 缓存已加载的 POC 集合,支持多 session 使用不同 POC 路径
type pocStore struct {
mu sync.Mutex
cache map[string][]*lib.Poc // key: pocPath(空字符串表示内嵌 POC)
}
var globalPocStore = &pocStore{cache: make(map[string][]*lib.Poc)}
// WebScan 执行Web漏洞扫描
func WebScan(ctx context.Context, info *common.HostInfo, cfg *common.Config, session *common.ScanSession) {
// 初始化POC配置(用于CEL回调函数)
lib.InitPOCConfig(cfg.DNSLog)
// 加载POCDCLP: 快速路径无锁,慢路径互斥保护
if !pocLoaded.Load() {
pocMu.Lock()
if !pocLoaded.Load() {
cachedPocPath = cfg.POC.PocPath
initPocs()
if len(allPocs) > 0 {
pocLoaded.Store(true)
}
}
pocMu.Unlock()
}
// 加载POC按 PocPath 缓存,不同路径独立加载
pocs := globalPocStore.getOrLoad(cfg.POC.PocPath)
// 验证输入
if info == nil {
@@ -73,7 +64,7 @@ func WebScan(ctx context.Context, info *common.HostInfo, cfg *common.Config, ses
return
}
if len(allPocs) == 0 {
if len(pocs) == 0 {
session.LogError(i18n.GetText("poc_load_failed"))
return
}
@@ -94,14 +85,11 @@ func WebScan(ctx context.Context, info *common.HostInfo, cfg *common.Config, ses
// 根据扫描策略执行POC
if cfg.POC.PocName == "" && len(info.Info) == 0 {
// 执行所有POC
executePOCs(ctx, config.PocInfo{Target: target}, cfg, session)
executePOCs(ctx, config.PocInfo{Target: target}, cfg, session, pocs)
} else if len(info.Info) > 0 {
// 基于指纹信息执行POC
scanByFingerprints(ctx, target, info.Info, cfg, session)
scanByFingerprints(ctx, target, info.Info, cfg, session, pocs)
} else if cfg.POC.PocName != "" {
// 基于指定POC名称执行
executePOCs(ctx, config.PocInfo{Target: target, PocName: cfg.POC.PocName}, cfg, session)
executePOCs(ctx, config.PocInfo{Target: target, PocName: cfg.POC.PocName}, cfg, session, pocs)
}
}
@@ -178,8 +166,24 @@ func hasMalformedWebURLPort(host string) bool {
return strings.Contains(host, ":")
}
// getOrLoad 获取或加载指定路径的 POC 集合
func (s *pocStore) getOrLoad(pocPath string) []*lib.Poc {
s.mu.Lock()
defer s.mu.Unlock()
if pocs, ok := s.cache[pocPath]; ok {
return pocs
}
pocs := loadPocs(pocPath)
if len(pocs) > 0 {
s.cache[pocPath] = pocs
}
return pocs
}
// scanByFingerprints 根据指纹执行POC
func scanByFingerprints(ctx context.Context, target string, fingerprints []string, cfg *common.Config, session *common.ScanSession) {
func scanByFingerprints(ctx context.Context, target string, fingerprints []string, cfg *common.Config, session *common.ScanSession, pocs []*lib.Poc) {
for _, fingerprint := range fingerprints {
if fingerprint == "" {
continue
@@ -190,12 +194,12 @@ func scanByFingerprints(ctx context.Context, target string, fingerprints []strin
continue
}
executePOCs(ctx, config.PocInfo{Target: target, PocName: pocName}, cfg, session)
executePOCs(ctx, config.PocInfo{Target: target, PocName: pocName}, cfg, session, pocs)
}
}
// executePOCs 执行POC检测
func executePOCs(ctx context.Context, pocInfo config.PocInfo, cfg *common.Config, session *common.ScanSession) {
func executePOCs(ctx context.Context, pocInfo config.PocInfo, cfg *common.Config, session *common.ScanSession, pocs []*lib.Poc) {
// 验证目标
if pocInfo.Target == "" {
session.LogError(ErrEmptyTarget.Error())
@@ -222,7 +226,7 @@ func executePOCs(ctx context.Context, pocInfo config.PocInfo, cfg *common.Config
}
// 筛选POC
matchedPocs := filterPocs(pocInfo.PocName)
matchedPocs := filterPocs(pocInfo.PocName, pocs)
if len(matchedPocs) == 0 {
session.LogDebug(fmt.Sprintf("%v: %s", ErrPocNotFound, pocInfo.PocName))
return
@@ -257,28 +261,22 @@ func createBaseRequest(ctx context.Context, target string, cfg *common.Config) (
return req, nil
}
// initPocs 初始化并加载POC
// 使用cachedPocPath包级变量
func initPocs() {
// 预分配容量避免频繁扩容,典型POC数量在100-500之间
allPocs = make([]*lib.Poc, 0, 256)
if cachedPocPath == "" {
loadEmbeddedPocs()
} else {
loadExternalPocs(cachedPocPath)
// loadPocs 加载指定路径的 POC(空路径表示内嵌 POC)
func loadPocs(pocPath string) []*lib.Poc {
if pocPath == "" {
return loadEmbeddedPocs()
}
return loadExternalPocs(pocPath)
}
// loadEmbeddedPocs 加载内置POC
func loadEmbeddedPocs() {
func loadEmbeddedPocs() []*lib.Poc {
entries, err := pocsFS.ReadDir("pocs")
if err != nil {
common.LogError(i18n.Tr("webscan_builtin_poc_failed", err))
return
return nil
}
// 收集所有POC文件
var pocFiles []string
for _, entry := range entries {
if isPocFile(entry.Name()) {
@@ -286,24 +284,21 @@ func loadEmbeddedPocs() {
}
}
// 并发加载POC文件
loadPocsConcurrently(pocFiles, true, "")
return loadPocsConcurrently(pocFiles, true, "")
}
// loadExternalPocs 从外部路径加载POC
func loadExternalPocs(pocPath string) {
func loadExternalPocs(pocPath string) []*lib.Poc {
if !directoryExists(pocPath) {
common.LogError(i18n.Tr("webscan_poc_dir_not_exist", pocPath))
return
return nil
}
// 收集所有POC文件路径
var pocFiles []string
err := filepath.Walk(pocPath, func(path string, info os.FileInfo, err error) error {
if err != nil || info == nil || info.IsDir() {
return nil
}
if isPocFile(info.Name()) {
pocFiles = append(pocFiles, path)
}
@@ -312,18 +307,17 @@ func loadExternalPocs(pocPath string) {
if err != nil {
common.LogError(i18n.Tr("webscan_poc_dir_walk_failed", err))
return
return nil
}
// 并发加载POC文件
loadPocsConcurrently(pocFiles, false, pocPath)
return loadPocsConcurrently(pocFiles, false, pocPath)
}
// loadPocsConcurrently 并发加载POC文件channel 收集,无锁竞争)
func loadPocsConcurrently(pocFiles []string, isEmbedded bool, pocPath string) {
// loadPocsConcurrently 并发加载POC文件,返回加载结果
func loadPocsConcurrently(pocFiles []string, isEmbedded bool, pocPath string) []*lib.Poc {
pocCount := len(pocFiles)
if pocCount == 0 {
return
return nil
}
var wg sync.WaitGroup
@@ -359,14 +353,14 @@ func loadPocsConcurrently(pocFiles []string, isEmbedded bool, pocPath string) {
close(results)
}()
var successCount int
pocs := make([]*lib.Poc, 0, pocCount)
for poc := range results {
allPocs = append(allPocs, poc)
successCount++
pocs = append(pocs, poc)
}
failCount := pocCount - successCount
common.LogInfo(i18n.Tr("poc_load_complete", pocCount, successCount, failCount))
failCount := pocCount - len(pocs)
common.LogInfo(i18n.Tr("poc_load_complete", pocCount, len(pocs), failCount))
return pocs
}
// directoryExists 检查目录是否存在
@@ -382,16 +376,15 @@ func isPocFile(filename string) bool {
}
// filterPocs 根据POC名称筛选
func filterPocs(pocName string) []*lib.Poc {
func filterPocs(pocName string, pocs []*lib.Poc) []*lib.Poc {
if pocName == "" {
return allPocs
return pocs
}
// 转换为小写以进行不区分大小写的匹配
searchName := strings.ToLower(pocName)
var matchedPocs []*lib.Poc
for _, poc := range allPocs {
for _, poc := range pocs {
if poc != nil && strings.Contains(strings.ToLower(poc.Name), searchName) {
matchedPocs = append(matchedPocs, poc)
}