From e8cdcf77694c89f952066be61e402b3ac4044f6b Mon Sep 17 00:00:00 2001 From: root Date: Thu, 4 Jun 2026 11:25:47 +0000 Subject: [PATCH 1/4] =?UTF-8?q?fix(cyberhub):=20=E4=BC=98=E5=85=88?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=20POC=20=E5=8E=9F=E5=A7=8B=20YAML?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/cyberhub/api.go | 2 ++ pkg/cyberhub/client_test.go | 66 +++++++++++++++++++++++++++++++++++++ pkg/cyberhub/provider.go | 57 ++++++++++++++++++-------------- 3 files changed, 101 insertions(+), 24 deletions(-) diff --git a/pkg/cyberhub/api.go b/pkg/cyberhub/api.go index 7d72175..400f1bb 100644 --- a/pkg/cyberhub/api.go +++ b/pkg/cyberhub/api.go @@ -24,6 +24,8 @@ type fingerprintListResponse struct { type pocResponse struct { *types.Template `json:",inline" yaml:",inline"` + RawContent string `json:"raw_content,omitempty" yaml:"raw_content,omitempty"` + RawContentDraft string `json:"raw_content_draft,omitempty" yaml:"raw_content_draft,omitempty"` } type pocListResponse struct { diff --git a/pkg/cyberhub/client_test.go b/pkg/cyberhub/client_test.go index 018ebf6..630253b 100644 --- a/pkg/cyberhub/client_test.go +++ b/pkg/cyberhub/client_test.go @@ -3,6 +3,8 @@ package cyberhub import ( "net/url" "testing" + + "github.com/chainreactors/sdk/pkg/types" ) func TestApplyFilterParams_DedupTags(t *testing.T) { @@ -152,3 +154,67 @@ func TestApplyDefaultPOCStatus_WithReviewStatus(t *testing.T) { t.Fatalf("expected no default status when review_status set, got %q", params.Get("status")) } } + +func TestPOCTemplateFromResponsePrefersRawContent(t *testing.T) { + resp := pocResponse{ + Template: &types.Template{Id: "json-template"}, + RawContent: `id: raw-template +info: + name: Raw Template +variables: + s1: '{{rand_int(1000, 9999)}}' +http: + - method: GET + path: + - '{{BaseURL}}/check?s={{s1}}' +`, + } + + tpl := pocTemplateFromResponse(resp, false) + if tpl == nil { + t.Fatal("expected template") + } + if tpl.Id != "raw-template" { + t.Fatalf("expected raw template id, got %q", tpl.Id) + } + if tpl.Variables.Len() == 0 { + t.Fatal("expected variables parsed from raw content") + } +} + +func TestPOCTemplateFromResponseHonorsDraftRawContent(t *testing.T) { + resp := pocResponse{ + RawContent: `id: active-template +info: + name: Active Template +http: + - method: GET +`, + RawContentDraft: `id: draft-template +info: + name: Draft Template +http: + - method: POST +`, + } + + tpl := pocTemplateFromResponse(resp, true) + if tpl == nil { + t.Fatal("expected template") + } + if tpl.Id != "draft-template" { + t.Fatalf("expected draft template id, got %q", tpl.Id) + } +} + +func TestPOCTemplateFromResponseFallsBackToStructuredTemplate(t *testing.T) { + resp := pocResponse{Template: &types.Template{Id: "json-template"}} + + tpl := pocTemplateFromResponse(resp, true) + if tpl == nil { + t.Fatal("expected template") + } + if tpl.Id != "json-template" { + t.Fatalf("expected structured template id, got %q", tpl.Id) + } +} diff --git a/pkg/cyberhub/provider.go b/pkg/cyberhub/provider.go index 642e9d0..0208034 100644 --- a/pkg/cyberhub/provider.go +++ b/pkg/cyberhub/provider.go @@ -2,7 +2,6 @@ package cyberhub import ( "context" - "strings" "sync" "time" @@ -89,46 +88,56 @@ func parseFinger(raw string) *types.Finger { return nil } +func parsePOCTemplate(raw string) (tpl *types.Template) { + defer func() { + if recover() != nil { + tpl = nil + } + }() + + var template types.Template + if err := yaml.Unmarshal([]byte(raw), &template); err == nil && (template.Id != "" || template.Info.Name != "") { + return &template + } + return nil +} + +func pocTemplateFromResponse(resp pocResponse, useDraft bool) *types.Template { + if useDraft && resp.RawContentDraft != "" { + if tpl := parsePOCTemplate(resp.RawContentDraft); tpl != nil { + return tpl + } + } + if resp.RawContent != "" { + if tpl := parsePOCTemplate(resp.RawContent); tpl != nil { + return tpl + } + } + return resp.Template +} + // ExportFingers 导出完整指纹记录,包含 RawContent 与 RawContentDraft。 -// 自动修正 Engine 字段:CyberHub 可能将 xray 数据标记为 fingerprinthub, -// 通过 Finger.Tags 中的 neutron/xray/ai_converted 标签识别并修正为 xray。 +// Engine 字段保持 CyberHub 原始值;source1 tag 的额外 xray 路由由 +// fingers.FullFingers 在合并时处理,避免把 fingerprinthub 被动能力替换掉。 func (p *Provider) ExportFingers(ctx context.Context) ([]FingerprintExport, error) { records, err := p.client().exportFingers(ctx, p.filter) if err != nil { return nil, err } - for i := range records { - records[i].Engine = resolveEngine(records[i].Engine, records[i].Finger) - } return records, nil } -// resolveEngine 根据 tags 修正 CyberHub 返回的 engine 字段。 -// 只要 Finger.Tags 包含 "xray" 即修正为 xray 引擎。 -func resolveEngine(engine string, finger *types.Finger) string { - if engine == "xray" || engine == "fingers" || engine == "" { - return engine - } - if finger != nil { - for _, tag := range finger.Tags { - if strings.EqualFold(tag, "xray") { - return "xray" - } - } - } - return engine -} - // POCs 导出 POC 模板数据 func (p *Provider) POCs(ctx context.Context) ([]*types.Template, error) { responses, err := p.client().exportPOCs(ctx, p.filter) if err != nil { return nil, err } + useDraft := p.filter != nil && p.filter.Draft tpls := make([]*types.Template, 0, len(responses)) for _, resp := range responses { - if resp.Template != nil { - tpls = append(tpls, resp.Template) + if tpl := pocTemplateFromResponse(resp, useDraft); tpl != nil { + tpls = append(tpls, tpl) } } return tpls, nil From f9be3ea6038407d83047f65e34941de93aba5414 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 4 Jun 2026 11:26:05 +0000 Subject: [PATCH 2/4] =?UTF-8?q?fix(neutron):=20=E9=9A=94=E7=A6=BB=E6=A8=A1?= =?UTF-8?q?=E6=9D=BF=E7=BC=96=E8=AF=91=E5=8F=98=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- neutron/engine.go | 3 +- neutron/engine_test.go | 66 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/neutron/engine.go b/neutron/engine.go index fe70782..fc137d5 100644 --- a/neutron/engine.go +++ b/neutron/engine.go @@ -219,10 +219,9 @@ func (e *Engine) compileOptions() *protocols.ExecuterOptions { func (e *Engine) compileTemplates(allTemplates []*types.Template) []*types.Template { compiledTemplates := make([]*types.Template, 0, len(allTemplates)) - options := e.compileOptions() for _, t := range allTemplates { - if err := t.Compile(options); err != nil { + if err := t.Compile(e.compileOptions()); err != nil { continue } compiledTemplates = append(compiledTemplates, t) diff --git a/neutron/engine_test.go b/neutron/engine_test.go index e35d157..5deeef3 100644 --- a/neutron/engine_test.go +++ b/neutron/engine_test.go @@ -2,9 +2,12 @@ package neutron import ( "context" + "net/http" + "net/http/httptest" "testing" "github.com/chainreactors/sdk/pkg/types" + "gopkg.in/yaml.v3" ) func TestConfigWithCapacity(t *testing.T) { @@ -61,3 +64,66 @@ func TestNoCapacityByDefault(t *testing.T) { t.Fatal("engine should have no capacity by default") } } + +func TestCompileTemplatesIsolatesTemplateVariables(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte("path=" + r.URL.Path)) + })) + defer server.Close() + + first := parseTemplateForTest(t, `id: first-template +info: + name: First Template + severity: info +variables: + token: first +http: + - method: GET + path: + - "{{BaseURL}}/{{token}}" + matchers: + - type: word + words: + - "path=/first" +`) + second := parseTemplateForTest(t, `id: second-template +info: + name: Second Template + severity: info +variables: + token: second +http: + - method: GET + path: + - "{{BaseURL}}/{{token}}" + matchers: + - type: word + words: + - "path=/second" +`) + + engine := &Engine{config: NewConfig()} + compiled := engine.compileTemplates([]*types.Template{first, second}) + if len(compiled) != 2 { + t.Fatalf("compiled templates = %d, want 2", len(compiled)) + } + + for _, tpl := range compiled { + result, err := tpl.Execute(server.URL, nil) + if err != nil { + t.Fatalf("execute %s: %v", tpl.Id, err) + } + if result == nil || !result.Matched { + t.Fatalf("expected %s to match its own variable-expanded path", tpl.Id) + } + } +} + +func parseTemplateForTest(t *testing.T, raw string) *types.Template { + t.Helper() + var tpl types.Template + if err := yaml.Unmarshal([]byte(raw), &tpl); err != nil { + t.Fatalf("parse template: %v", err) + } + return &tpl +} From 72c6cef2f7f574e6aa115a5c485b5f94e5d96505 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 4 Jun 2026 11:26:43 +0000 Subject: [PATCH 3/4] =?UTF-8?q?fix(fingers):=20=E4=B8=BB=E5=8A=A8=E6=89=AB?= =?UTF-8?q?=E6=8F=8F=E4=BF=9D=E7=95=99=E8=B7=AF=E5=BE=84=E5=B9=B6=E8=A7=A3?= =?UTF-8?q?=E6=9E=90=E9=87=8D=E5=AE=9A=E5=90=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fingers/engine.go | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/fingers/engine.go b/fingers/engine.go index 01f37d9..15cdcfe 100644 --- a/fingers/engine.go +++ b/fingers/engine.go @@ -15,8 +15,8 @@ import ( "github.com/chainreactors/fingers/alias" "github.com/chainreactors/fingers/common" "github.com/chainreactors/fingers/favicon" - fingersEngine "github.com/chainreactors/fingers/fingers" "github.com/chainreactors/fingers/fingerprinthub" + fingersEngine "github.com/chainreactors/fingers/fingers" "github.com/chainreactors/fingers/resources" "github.com/chainreactors/fingers/xray" "github.com/chainreactors/logs" @@ -503,6 +503,7 @@ func (e *Engine) scanHTTPTarget(ctx *Context, url string, level int) *TargetResu if parsedURL.Port != "" && parsedURL.Port != "80" && parsedURL.Port != "443" { baseURL += ":" + parsedURL.Port } + templateBaseURL := activeTemplateBaseURL(baseURL, parsedURL.Path) client := ctx.GetClient() @@ -544,6 +545,7 @@ func (e *Engine) scanHTTPTarget(ctx *Context, url string, level int) *TargetResu if transport == nil { transport = http.DefaultTransport } + transport = wrapRedirectResolvingTransport(transport) activeCallback := func(frame *common.Framework, vuln *common.Vuln) { if frame != nil { @@ -556,13 +558,13 @@ func (e *Engine) scanHTTPTarget(ctx *Context, url string, level int) *TargetResu if fpHub := e.engine.FingerPrintHub(); fpHub != nil { safeHTTPActiveMatch("fingerprinthub", func() { - fpHub.HTTPActiveMatch(baseURL, level, transport, activeCallback) + fpHub.HTTPActiveMatch(templateBaseURL, level, transport, activeCallback) }) } if xrayEng := e.engine.Xray(); xrayEng != nil { safeHTTPActiveMatch("xray", func() { - xrayEng.HTTPActiveMatch(baseURL, level, transport, activeCallback) + xrayEng.HTTPActiveMatch(templateBaseURL, level, transport, activeCallback) }) } @@ -881,3 +883,31 @@ func pathJoin(base, append string) string { return base + append } + +func activeTemplateBaseURL(baseURL, targetPath string) string { + if targetPath == "" || targetPath == "/" { + return baseURL + } + return baseURL + strings.TrimRight(targetPath, "/") +} + +type redirectResolvingTransport struct { + base http.RoundTripper +} + +func wrapRedirectResolvingTransport(base http.RoundTripper) http.RoundTripper { + if base == nil { + base = http.DefaultTransport + } + return redirectResolvingTransport{base: base} +} + +func (t redirectResolvingTransport) RoundTrip(req *http.Request) (*http.Response, error) { + client := &http.Client{ + Transport: t.base, + } + clone := req.Clone(req.Context()) + clone.Body = req.Body + clone.GetBody = req.GetBody + return client.Do(clone) +} From f8c962e151ab934b0a11134f2ab563b1cf58a12b Mon Sep 17 00:00:00 2001 From: root Date: Thu, 4 Jun 2026 17:44:01 +0000 Subject: [PATCH 4/4] =?UTF-8?q?fix(fingers):=20source1=20tag=20=E8=A6=86?= =?UTF-8?q?=E7=9B=96=E5=BC=95=E6=93=8E=E8=80=8C=E9=9D=9E=E5=A4=8D=E5=88=B6?= =?UTF-8?q?=E5=89=AF=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit source1 tag 的 fingerprinthub 指纹改为直接路由到 xray 引擎, 不再额外复制一份导致被动+主动匹配双跑。回退 fullFingerKey 到上游设计(去掉 enginePrefix)。 Co-Authored-By: Claude Opus 4.6 (1M context) --- fingers/config.go | 22 +++++++++- fingers/config_task_test.go | 82 +++++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+), 2 deletions(-) diff --git a/fingers/config.go b/fingers/config.go index 33bfdc1..2a5bb86 100644 --- a/fingers/config.go +++ b/fingers/config.go @@ -3,6 +3,7 @@ package fingers import ( "context" "fmt" + "strings" "github.com/chainreactors/logs" "github.com/chainreactors/neutron/protocols" @@ -293,14 +294,19 @@ func (f FullFingers) MergeExports(exports []cyberhub.FingerprintExport, useDraft rawContent = r.RawContentDraft } - switch r.Engine { + engine := r.Engine + if engine == "fingerprinthub" && hasTag(r.Finger, xrayRouteTag) { + engine = "xray" + } + + switch engine { case "fingerprinthub", "xray": if rawContent != "" { tmpl := parseTemplate(rawContent, execOpts) if tmpl != nil { ff.Template = tmpl ff.RawContent = rawContent - ff.Engine = r.Engine + ff.Engine = engine } } default: @@ -317,6 +323,18 @@ func (f FullFingers) MergeExports(exports []cyberhub.FingerprintExport, useDraft return f } +func hasTag(finger *types.Finger, tag string) bool { + if finger == nil { + return false + } + for _, t := range finger.Tags { + if strings.EqualFold(strings.TrimSpace(t), tag) { + return true + } + } + return false +} + func parseTemplate(rawYAML string, opts *protocols.ExecuterOptions) *types.Template { tmpl := &types.Template{} if err := yaml.Unmarshal([]byte(rawYAML), tmpl); err != nil { diff --git a/fingers/config_task_test.go b/fingers/config_task_test.go index ed00ebe..4281bea 100644 --- a/fingers/config_task_test.go +++ b/fingers/config_task_test.go @@ -36,6 +36,88 @@ func TestConfigSourceSelectionAndFiltering(t *testing.T) { } } +func TestMergeExportsSource1OverridesEngineToXray(t *testing.T) { + rawTemplate := `id: source1-route-test +info: + name: source1 route test + author: tester + severity: info +http: + - method: GET + path: + - "{{BaseURL}}/" + matchers: + - type: word + words: + - source1-route-marker +` + + exports := []cyberhub.FingerprintExport{{ + Finger: &types.Finger{ + Name: "source1 route test", + Protocol: "http", + Tags: []string{"neutron", "source1"}, + }, + Engine: "fingerprinthub", + RawContent: rawTemplate, + }} + + full := (FullFingers{}).MergeExports(exports, false) + if got := len(full.TemplateItems("fingerprinthub")); got != 0 { + t.Fatalf("fingerprinthub template count = %d, want 0 (source1 overrides to xray)", got) + } + if got := len(full.TemplateItems("xray")); got != 1 { + t.Fatalf("xray template count = %d, want 1", got) + } +} + +func TestMergeExportsWithoutSource1StaysFingerprinthub(t *testing.T) { + rawTemplate := `id: no-source1-test +info: + name: no source1 test + author: tester + severity: info +http: + - method: GET + path: + - "{{BaseURL}}/" + matchers: + - type: word + words: + - marker +` + + exports := []cyberhub.FingerprintExport{{ + Finger: &types.Finger{ + Name: "no source1 test", + Protocol: "http", + Tags: []string{"neutron"}, + }, + Engine: "fingerprinthub", + RawContent: rawTemplate, + }} + + full := (FullFingers{}).MergeExports(exports, false) + if got := len(full.TemplateItems("fingerprinthub")); got != 1 { + t.Fatalf("fingerprinthub template count = %d, want 1", got) + } + if got := len(full.TemplateItems("xray")); got != 0 { + t.Fatalf("xray template count = %d, want 0 (no source1 tag)", got) + } +} + +func TestHasTag(t *testing.T) { + if hasTag(&types.Finger{Tags: []string{"xray"}}, "source1") { + t.Fatal("xray tag should not match source1") + } + if !hasTag(&types.Finger{Tags: []string{" source1 "}}, "source1") { + t.Fatal("source1 tag with whitespace should match") + } + if hasTag(nil, "source1") { + t.Fatal("nil finger should return false") + } +} + func TestExecuteMatchTaskUsesSDKResult(t *testing.T) { eng := newDetailTestEngine(t, NewConfig(), &types.Finger{ Name: "execute-app",