Skip to content

Commit 95dc85a

Browse files
committed
fix: add proton template loader
1 parent 5611d4c commit 95dc85a

1 file changed

Lines changed: 224 additions & 0 deletions

File tree

‎pkg/proton.go‎

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
package pkg
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"strings"
7+
"sync"
8+
9+
"github.com/chainreactors/neutron/operators"
10+
"github.com/chainreactors/neutron/protocols"
11+
"github.com/chainreactors/parsers"
12+
"github.com/chainreactors/proton/protocols/file"
13+
protonTmpl "github.com/chainreactors/proton/templates"
14+
yaml "sigs.k8s.io/yaml/goyaml.v3"
15+
)
16+
17+
var (
18+
ProtonScanner *file.Scanner
19+
protonTemplates []*protonTmpl.Template
20+
protonTemplateMap map[string]*protonTmpl.Template // id → template
21+
protonTagMap map[string][]string // tag → []id
22+
protonMu sync.RWMutex
23+
)
24+
25+
// LoadProtonTemplates parses YAML template docs and builds a Scanner.
26+
func LoadProtonTemplates(yamlDocs [][]byte) error {
27+
protonMu.Lock()
28+
defer protonMu.Unlock()
29+
30+
protonTemplates = nil
31+
protonTemplateMap = make(map[string]*protonTmpl.Template)
32+
protonTagMap = make(map[string][]string)
33+
34+
opts := &protocols.ExecuterOptions{Options: &protocols.Options{}}
35+
36+
for _, doc := range yamlDocs {
37+
var tmpl protonTmpl.Template
38+
if err := yaml.Unmarshal(doc, &tmpl); err != nil {
39+
continue
40+
}
41+
if len(tmpl.RequestsFile) == 0 {
42+
continue
43+
}
44+
if err := tmpl.Compile(opts); err != nil {
45+
continue
46+
}
47+
protonTemplates = append(protonTemplates, &tmpl)
48+
protonTemplateMap[tmpl.Id] = &tmpl
49+
for _, tag := range tmpl.GetTags() {
50+
tag = strings.TrimSpace(tag)
51+
protonTagMap[tag] = append(protonTagMap[tag], tmpl.Id)
52+
}
53+
}
54+
55+
rebuildScanner()
56+
return nil
57+
}
58+
59+
// AddCustomExtractor creates a one-off regex extractor at runtime (for --extract <regex>).
60+
func AddCustomExtractor(name, pattern string) {
61+
protonMu.Lock()
62+
defer protonMu.Unlock()
63+
64+
req := &file.Request{
65+
Extensions: []string{"all"},
66+
}
67+
req.Operators.Extractors = append(req.Operators.Extractors, &operators.Extractor{
68+
Name: name,
69+
Type: "regex",
70+
Regex: []string{pattern},
71+
})
72+
opts := &protocols.ExecuterOptions{Options: &protocols.Options{}}
73+
if err := req.Compile(opts); err != nil {
74+
return
75+
}
76+
77+
tmpl := &protonTmpl.Template{
78+
Id: "custom-" + name,
79+
RequestsFile: []*file.Request{req},
80+
}
81+
tmpl.Info.Name = name
82+
tmpl.Info.Severity = "info"
83+
tmpl.Info.Tags = "spray, custom"
84+
85+
protonTemplates = append(protonTemplates, tmpl)
86+
protonTemplateMap[tmpl.Id] = tmpl
87+
rebuildScanner()
88+
}
89+
90+
// EnableExtractors activates a subset of templates by name or tag.
91+
// Called after LoadProtonTemplates to filter what runs during scanning.
92+
// Empty names = all templates active.
93+
func EnableExtractors(names []string) {
94+
protonMu.Lock()
95+
defer protonMu.Unlock()
96+
97+
if len(names) == 0 {
98+
rebuildScanner()
99+
return
100+
}
101+
102+
ids := make(map[string]bool)
103+
for _, n := range names {
104+
if _, ok := protonTemplateMap["spray-"+n]; ok {
105+
ids["spray-"+n] = true
106+
} else if _, ok := protonTemplateMap[n]; ok {
107+
ids[n] = true
108+
} else if tagIDs, ok := protonTagMap[n]; ok {
109+
for _, id := range tagIDs {
110+
ids[id] = true
111+
}
112+
} else {
113+
ids["custom-"+n] = true
114+
}
115+
}
116+
117+
var rules []file.Rule
118+
for _, tmpl := range protonTemplates {
119+
if !ids[tmpl.Id] {
120+
continue
121+
}
122+
for _, req := range tmpl.RequestsFile {
123+
rules = append(rules, file.Rule{
124+
ID: tmpl.Id,
125+
Name: tmpl.Info.Name,
126+
Severity: tmpl.Info.Severity,
127+
Requests: []*file.Request{req},
128+
})
129+
}
130+
}
131+
ProtonScanner = file.NewScanner(rules, nil)
132+
}
133+
134+
func rebuildScanner() {
135+
var rules []file.Rule
136+
for _, tmpl := range protonTemplates {
137+
for _, req := range tmpl.RequestsFile {
138+
rules = append(rules, file.Rule{
139+
ID: tmpl.Id,
140+
Name: tmpl.Info.Name,
141+
Severity: tmpl.Info.Severity,
142+
Requests: []*file.Request{req},
143+
})
144+
}
145+
}
146+
ProtonScanner = file.NewScanner(rules, nil)
147+
}
148+
149+
// ProtonExtract runs proton scanner on in-memory content and returns
150+
// parsers.Extracted results compatible with spray's output format.
151+
func ProtonExtract(content []byte) parsers.Extracteds {
152+
protonMu.RLock()
153+
scanner := ProtonScanner
154+
protonMu.RUnlock()
155+
156+
if scanner == nil || len(scanner.Groups) == 0 || len(content) == 0 {
157+
return nil
158+
}
159+
160+
resultMap := make(map[string]map[string]struct{})
161+
162+
for _, group := range scanner.Groups {
163+
findings := scanner.ScanData(content, "response", group)
164+
for _, f := range findings {
165+
if resultMap[f.TemplateID] == nil {
166+
resultMap[f.TemplateID] = make(map[string]struct{})
167+
}
168+
for _, e := range f.Extracts {
169+
resultMap[f.TemplateID][e.Value] = struct{}{}
170+
}
171+
for _, events := range f.Matches {
172+
for _, e := range events {
173+
resultMap[f.TemplateID][e.Value] = struct{}{}
174+
}
175+
}
176+
if f.Result != nil {
177+
for _, val := range f.Result.OutputExtracts {
178+
resultMap[f.TemplateID][val] = struct{}{}
179+
}
180+
}
181+
}
182+
}
183+
184+
var extracteds parsers.Extracteds
185+
for name, vals := range resultMap {
186+
displayName := strings.TrimPrefix(name, "spray-")
187+
extracted := &parsers.Extracted{Name: displayName}
188+
for v := range vals {
189+
extracted.ExtractResult = append(extracted.ExtractResult, v)
190+
}
191+
if len(extracted.ExtractResult) > 0 {
192+
extracteds = append(extracteds, extracted)
193+
}
194+
}
195+
return extracteds
196+
}
197+
198+
// LoadProtonTemplatesFromDir loads all .yaml template files from a directory.
199+
func LoadProtonTemplatesFromDir(dir string) error {
200+
var docs [][]byte
201+
filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
202+
if err != nil || info.IsDir() || !strings.HasSuffix(path, ".yaml") {
203+
return nil
204+
}
205+
data, err := os.ReadFile(path)
206+
if err != nil {
207+
return nil
208+
}
209+
docs = append(docs, data)
210+
return nil
211+
})
212+
return LoadProtonTemplates(docs)
213+
}
214+
215+
// ProtonExtractorNames returns all available template IDs.
216+
func ProtonExtractorNames() []string {
217+
protonMu.RLock()
218+
defer protonMu.RUnlock()
219+
names := make([]string, 0, len(protonTemplateMap))
220+
for id := range protonTemplateMap {
221+
names = append(names, id)
222+
}
223+
return names
224+
}

0 commit comments

Comments
 (0)