diff --git a/gogo/gogo.go b/gogo/gogo.go index 08f98ca..f79dadb 100644 --- a/gogo/gogo.go +++ b/gogo/gogo.go @@ -11,6 +11,7 @@ import ( "github.com/chainreactors/gogo/v2/engine" "github.com/chainreactors/gogo/v2/pkg" "github.com/chainreactors/logs" + neutrontemplates "github.com/chainreactors/neutron/templates" sdkfingers "github.com/chainreactors/sdk/fingers" "github.com/chainreactors/sdk/neutron" "github.com/chainreactors/sdk/pkg/types" @@ -84,6 +85,23 @@ func buildTemplateMap(templates []*types.Template) map[string][]*types.Template return templateMap } +// buildChainExecutor 构建 gogo 漏洞扫描所需的 chain 执行器。 +// +// gogo 的 engine.NeutronScan 依赖包级全局 pkg.ChainExec 来解析模板链,而该全局 +// 原本只由 pkg.LoadTemplates 赋值。SDK 注入模板时走的是 buildTemplateMap 直接填 +// pkg.TemplateMap 的旁路,绕过了 LoadTemplates,因此必须在这里同步构建 ChainExec, +// 否则开启 exploit 扫描(Exploit != "none")时 ChainExec.Execute 会打在 nil 上导致 +// ants worker panic。语义与 pkg.LoadTemplates 中的 chainExec 构建保持一致。 +func buildChainExecutor(templates []*types.Template) *neutrontemplates.ChainExecutor { + chainExec := neutrontemplates.NewChainExecutor(neutrontemplates.ChainConfig{}) + for _, template := range templates { + if template.Id != "" { + chainExec.Add(template.Id, template.Chains) + } + } + return chainExec +} + // Init 初始化引擎(加载指纹库等) func (e *Engine) Init() error { e.mu.Lock() @@ -166,6 +184,7 @@ func (e *Engine) applyInjectedNeutron() bool { } templateMap := buildTemplateMap(templates) pkg.TemplateMap = templateMap + pkg.ChainExec = buildChainExecutor(templates) templateCount := 0 for _, values := range templateMap { templateCount += len(values) diff --git a/gogo/gogo_chainexec_test.go b/gogo/gogo_chainexec_test.go new file mode 100644 index 0000000..832a79e --- /dev/null +++ b/gogo/gogo_chainexec_test.go @@ -0,0 +1,46 @@ +package gogo + +import ( + "testing" + + neutrontemplates "github.com/chainreactors/neutron/templates" + "github.com/chainreactors/sdk/pkg/types" +) + +// Regression for the nil-pointer panic in engine.NeutronScan: when the SDK +// injects templates it populates pkg.TemplateMap directly (buildTemplateMap), +// bypassing pkg.LoadTemplates which is the only other place that builds +// pkg.ChainExec. buildChainExecutor must rebuild that executor so exploit +// scans don't call ChainExec.Execute on a nil receiver. +func TestBuildChainExecutorRegistersIDsAndChains(t *testing.T) { + tmpls := []*types.Template{ + {Id: "entry", Chains: []string{"leaf"}}, + {Id: "leaf"}, + {Id: ""}, // no id: must be skipped without panicking + } + + ce := buildChainExecutor(tmpls) + if ce == nil { + t.Fatal("buildChainExecutor returned nil executor") + } + if !ce.Has("entry") || !ce.Has("leaf") { + t.Fatalf("registered ids = entry:%v leaf:%v, want both true", ce.Has("entry"), ce.Has("leaf")) + } + + // "leaf" is a chain target of "entry", so only "entry" is an entrypoint. + eps := ce.Entrypoints() + if len(eps) != 1 || eps[0] != "entry" { + t.Fatalf("entrypoints = %v, want [entry]", eps) + } + + // Executing from the entrypoint must walk entry -> leaf without panic + // (the panic being fixed is exactly this call on a nil receiver). + var visited []string + ce.Execute(eps, func(id string, _ map[string]interface{}) *neutrontemplates.ChainResult { + visited = append(visited, id) + return &neutrontemplates.ChainResult{} + }) + if len(visited) != 2 { + t.Fatalf("chain walk visited %v, want entry+leaf", visited) + } +} diff --git a/neutron/config_templates_test.go b/neutron/config_templates_test.go index c680f73..052b07d 100644 --- a/neutron/config_templates_test.go +++ b/neutron/config_templates_test.go @@ -67,10 +67,14 @@ func TestExecuteTaskAndResultHelpers(t *testing.T) { t.Fatal("expected explicit empty templates to fail validation") } + // OperatorResult (parsers.NeutronResult) embeds parsers.Result, so Matched is + // a promoted field and can't be set in a composite literal — assign it directly. + matchedResult := &types.OperatorResult{} + matchedResult.Matched = true matched := &ExecuteResult{ success: true, template: &types.Template{Id: "demo"}, - data: &NeutronResult{Result: &types.OperatorResult{Matched: true}}, + data: &NeutronResult{Result: matchedResult}, } if !matched.Success() || !matched.Matched() || matched.Template().Id != "demo" || matched.Data() != matched.Result() { t.Fatalf("unexpected matched result helpers: %+v", matched) diff --git a/proton/engine_test.go b/proton/engine_test.go index d59b817..9ae5af3 100644 --- a/proton/engine_test.go +++ b/proton/engine_test.go @@ -457,7 +457,10 @@ func TestEngine_ConfigFilter_IDs(t *testing.T) { }) eng := mustEngine(t, NewConfig().WithTemplatePaths(tmplDir).WithIDs("password-extract")) - findings := eng.ScanData([]byte("PRIVATE KEY\npassword = test\n"), "all.txt") + // The file extractor drops dummy values like "test"; use a realistic value so + // only the ID filter (not the FP filter) decides the outcome. "PRIVATE KEY" stays + // in the input to assert the excluded private-key-detect rule does not fire. + findings := eng.ScanData([]byte("PRIVATE KEY\npassword = test123\n"), "all.txt") if len(findings) != 1 { t.Fatalf("expected 1 finding, got %d", len(findings)) }