Skip to content

Commit 08bcf60

Browse files
M09Icclaude
andcommitted
feat(cstx): add Go sub-module for SCO standardized parsing
New sub-module `github.com/chainreactors/utils/cstx` wraps libcstx_ffi to convert parsers types (GOGOResult, SprayResult, etc.) into strongly typed SCO nodes (Ip, Port, App, Vuln, Framework, ...). API: `cstx.Parse(tool, input) ([]SCONode, error)` Types generated from proto/cstx/sco.proto via cstx-codegen. Build with `-tags cstx_native` for FFI, stub mode without. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 069633b commit 08bcf60

13 files changed

Lines changed: 1510 additions & 0 deletions

‎cstx/.gitattributes‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
lib/**/*.a filter=lfs diff=lfs merge=lfs -text

‎cstx/Makefile‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
CSTX_CORE ?= $(realpath ../../asm/mapping/cstx/cstx-core)
2+
CSTX_REPO ?= chainreactors/cstx
3+
CSTX_VERSION ?= v0.0.1
4+
5+
UNAME_S := $(shell uname -s | tr A-Z a-z)
6+
UNAME_M := $(shell uname -m)
7+
ifeq ($(UNAME_M),x86_64)
8+
ARCH := amd64
9+
else ifeq ($(UNAME_M),aarch64)
10+
ARCH := arm64
11+
else ifeq ($(UNAME_M),arm64)
12+
ARCH := arm64
13+
else
14+
ARCH := $(UNAME_M)
15+
endif
16+
PLATFORM := $(UNAME_S)_$(ARCH)
17+
18+
.PHONY: lib lib-local test clean download download-all
19+
20+
# Download from GitHub Release (current platform)
21+
download:
22+
@./scripts/download-ffi.sh $(CSTX_VERSION) $(PLATFORM)
23+
24+
# Download all platforms
25+
download-all:
26+
@./scripts/download-ffi.sh $(CSTX_VERSION) linux_amd64
27+
@./scripts/download-ffi.sh $(CSTX_VERSION) linux_arm64
28+
@./scripts/download-ffi.sh $(CSTX_VERSION) darwin_amd64
29+
@./scripts/download-ffi.sh $(CSTX_VERSION) darwin_arm64
30+
@./scripts/download-ffi.sh $(CSTX_VERSION) windows_amd64
31+
32+
# Build from local cstx-core source
33+
lib-local:
34+
cd $(CSTX_CORE) && cargo build --release -p cstx-ffi
35+
mkdir -p lib/$(PLATFORM)
36+
cp $(CSTX_CORE)/target/release/libcstx_ffi.a lib/$(PLATFORM)/
37+
38+
# Alias
39+
lib: download
40+
41+
test:
42+
CGO_ENABLED=1 go test -tags cstx_native -v ./...
43+
44+
clean:
45+
rm -rf lib/*/libcstx_ffi.a lib/*/libcstx_ffi.so lib/*/libcstx_ffi.dylib

‎cstx/cgo_darwin_amd64.go‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
//go:build cstx_native && darwin && amd64
2+
3+
package cstx
4+
5+
// #cgo LDFLAGS: -L${SRCDIR}/lib/darwin_amd64 -lcstx_ffi -lm -framework Security -framework CoreFoundation
6+
import "C"

‎cstx/cgo_darwin_arm64.go‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
//go:build cstx_native && darwin && arm64
2+
3+
package cstx
4+
5+
// #cgo LDFLAGS: -L${SRCDIR}/lib/darwin_arm64 -lcstx_ffi -lm -framework Security -framework CoreFoundation
6+
import "C"

‎cstx/cgo_linux_amd64.go‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
//go:build cstx_native && linux && amd64
2+
3+
package cstx
4+
5+
// #cgo LDFLAGS: -L${SRCDIR}/lib/linux_amd64 -lcstx_ffi -lm -ldl -lpthread
6+
import "C"

‎cstx/cgo_linux_arm64.go‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
//go:build cstx_native && linux && arm64
2+
3+
package cstx
4+
5+
// #cgo LDFLAGS: -L${SRCDIR}/lib/linux_arm64 -lcstx_ffi -lm -ldl -lpthread
6+
import "C"

‎cstx/cgo_windows_amd64.go‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
//go:build cstx_native && windows && amd64
2+
3+
package cstx
4+
5+
// #cgo LDFLAGS: -L${SRCDIR}/lib/windows_amd64 -lcstx_ffi -lm -lpthread -lws2_32 -luserenv -lbcrypt -lntdll
6+
import "C"

‎cstx/cstx.go‎

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
//go:build cstx_native
2+
3+
package cstx
4+
5+
/*
6+
#include "cstx_ffi.h"
7+
#include <stdlib.h>
8+
*/
9+
import "C"
10+
import (
11+
"bytes"
12+
"encoding/json"
13+
"fmt"
14+
"reflect"
15+
"unsafe"
16+
17+
"github.com/chainreactors/utils/parsers"
18+
)
19+
20+
21+
// Parse converts tool output into SCO-standardized nodes.
22+
// tool is the artifact name: "gogo", "spray", "zombie", "neutron", "aiscan", etc.
23+
// input accepts []byte (raw JSONL) or a slice of parsers result structs
24+
// (e.g. []*parsers.GOGOResult, []*parsers.SprayResult).
25+
func Parse(tool string, input any) ([]SCONode, error) {
26+
data, err := toJSONL(tool, input)
27+
if err != nil {
28+
return nil, err
29+
}
30+
raw, err := transform(tool, data)
31+
if err != nil {
32+
return nil, err
33+
}
34+
return parseSCONodes(raw)
35+
}
36+
37+
func parseSCONodes(data []byte) ([]SCONode, error) {
38+
var rawNodes []json.RawMessage
39+
if err := json.Unmarshal(data, &rawNodes); err != nil {
40+
return nil, fmt.Errorf("cstx parse: %w", err)
41+
}
42+
nodes := make([]SCONode, 0, len(rawNodes))
43+
for _, r := range rawNodes {
44+
n, err := ParseSCONode(r)
45+
if err != nil {
46+
continue
47+
}
48+
if n != nil {
49+
nodes = append(nodes, n)
50+
}
51+
}
52+
return nodes, nil
53+
}
54+
55+
func transform(tool string, data []byte) ([]byte, error) {
56+
cs := C.CString(tool)
57+
defer C.free(unsafe.Pointer(cs))
58+
59+
var out *C.char
60+
var outLen C.size_t
61+
var dp *C.uchar
62+
if len(data) > 0 {
63+
dp = (*C.uchar)(unsafe.Pointer(&data[0]))
64+
}
65+
rc := C.cstx_transform(cs, dp, C.size_t(len(data)), &out, &outLen)
66+
if out != nil {
67+
defer C.cstx_free_string(out)
68+
}
69+
if rc != 0 {
70+
if out != nil {
71+
return nil, fmt.Errorf("cstx transform: %s", C.GoStringN(out, C.int(outLen)))
72+
}
73+
return nil, fmt.Errorf("cstx transform failed")
74+
}
75+
return C.GoBytes(unsafe.Pointer(out), C.int(outLen)), nil
76+
}
77+
78+
func toJSONL(tool string, input any) ([]byte, error) {
79+
if b, ok := input.([]byte); ok {
80+
return b, nil
81+
}
82+
83+
rv := reflect.ValueOf(input)
84+
if rv.Kind() == reflect.Ptr {
85+
rv = rv.Elem()
86+
}
87+
if rv.Kind() != reflect.Slice {
88+
return marshalSingle(tool, input)
89+
}
90+
91+
var buf bytes.Buffer
92+
enc := json.NewEncoder(&buf)
93+
enc.SetEscapeHTML(false)
94+
for i := 0; i < rv.Len(); i++ {
95+
elem := rv.Index(i).Interface()
96+
adapted := adaptForCSTX(tool, elem)
97+
if err := enc.Encode(adapted); err != nil {
98+
return nil, fmt.Errorf("cstx marshal [%d]: %w", i, err)
99+
}
100+
}
101+
return buf.Bytes(), nil
102+
}
103+
104+
func marshalSingle(tool string, v any) ([]byte, error) {
105+
adapted := adaptForCSTX(tool, v)
106+
b, err := json.Marshal(adapted)
107+
if err != nil {
108+
return nil, err
109+
}
110+
b = append(b, '\n')
111+
return b, nil
112+
}
113+
114+
// adaptForCSTX converts Go parsers types to CSTX-compatible JSON.
115+
// Frameworks (map[string]*Framework) → []*Framework (array)
116+
// Vulns (map[string]*Vuln) → []*Vuln (array)
117+
func adaptForCSTX(tool string, v any) any {
118+
switch tool {
119+
case "gogo", "aiscan":
120+
return adaptGogo(v)
121+
case "spray":
122+
return adaptSpray(v)
123+
default:
124+
return v
125+
}
126+
}
127+
128+
type gogoLine struct {
129+
IP string `json:"ip"`
130+
Port string `json:"port"`
131+
Protocol string `json:"protocol"`
132+
Status string `json:"status,omitempty"`
133+
Uri string `json:"uri,omitempty"`
134+
Host string `json:"host,omitempty"`
135+
Title string `json:"title,omitempty"`
136+
Midware string `json:"midware,omitempty"`
137+
Frameworks []*parsers.Framework `json:"frameworks,omitempty"`
138+
Vulns []*parsers.Vuln `json:"vulns,omitempty"`
139+
Extracteds map[string][]string `json:"extracted,omitempty"`
140+
}
141+
142+
func adaptGogo(v any) any {
143+
var r *parsers.GOGOResult
144+
switch x := v.(type) {
145+
case *parsers.GOGOResult:
146+
r = x
147+
case parsers.GOGOResult:
148+
r = &x
149+
default:
150+
return v
151+
}
152+
return &gogoLine{
153+
IP: r.Ip, Port: r.Port, Protocol: r.Protocol,
154+
Status: r.Status, Uri: r.Uri, Host: r.Host,
155+
Title: r.Title, Midware: r.Midware,
156+
Frameworks: mapToSlice(r.Frameworks),
157+
Vulns: vulnMapToSlice(r.Vulns),
158+
Extracteds: r.Extracteds,
159+
}
160+
}
161+
162+
type sprayLine struct {
163+
URL string `json:"url"`
164+
Title string `json:"title,omitempty"`
165+
Status int `json:"status"`
166+
Host string `json:"host,omitempty"`
167+
Path string `json:"path,omitempty"`
168+
BodyLength int `json:"body_length,omitempty"`
169+
HeaderLength int `json:"header_length,omitempty"`
170+
RedirectURL string `json:"redirect_url,omitempty"`
171+
ContentType string `json:"content_type,omitempty"`
172+
Frameworks []*parsers.Framework `json:"frameworks,omitempty"`
173+
}
174+
175+
func adaptSpray(v any) any {
176+
var r *parsers.SprayResult
177+
switch x := v.(type) {
178+
case *parsers.SprayResult:
179+
r = x
180+
case parsers.SprayResult:
181+
r = &x
182+
default:
183+
return v
184+
}
185+
return &sprayLine{
186+
URL: r.UrlString, Title: r.Title, Status: r.Status,
187+
Host: r.Host, Path: r.Path,
188+
BodyLength: r.BodyLength, HeaderLength: r.HeaderLength,
189+
RedirectURL: r.RedirectURL, ContentType: r.ContentType,
190+
Frameworks: mapToSlice(r.Frameworks),
191+
}
192+
}
193+
194+
func mapToSlice(m parsers.Frameworks) []*parsers.Framework {
195+
if len(m) == 0 {
196+
return nil
197+
}
198+
s := make([]*parsers.Framework, 0, len(m))
199+
for _, f := range m {
200+
s = append(s, f)
201+
}
202+
return s
203+
}
204+
205+
func vulnMapToSlice(m parsers.Vulns) []*parsers.Vuln {
206+
if len(m) == 0 {
207+
return nil
208+
}
209+
s := make([]*parsers.Vuln, 0, len(m))
210+
for _, v := range m {
211+
s = append(s, v)
212+
}
213+
return s
214+
}

0 commit comments

Comments
 (0)