mirror of
https://github.com/chainreactors/spray.git
synced 2026-10-24 17:04:26 +00:00
feat: enhance extract system with severity, context capture, matchers, and new rules
- parsers.Extracted: add Severity and Items (ExtractItem with ctx/line/offset) - ProtonExtract: preserve severity from templates, capture N-char context around matches - Add --extract-context CLI flag (default 0, e.g. --extract-context 50) - Add word matchers pre-filtering to 14 existing templates for performance - Add 9 new HaE-inspired rules: lfi-indicator, upload-form, url-as-value, location-redirect, mac-address, sensitive-field, linkfinder, url-schemes, authorization-header - Total proton templates: 45 (was 35) - Update ColorString/TreeString to render severity and context - Comprehensive tests for severity, context, new templates, tag mappings Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
This commit is contained in:
1 parent
5d3ddeae56
commit
729dee3d06
14 files changed
+891
-28
No files matched your search
+1
-1
@@ -73,7 +73,7 @@ func Spray() {
|
||||
DefaultConfig: DefaultConfig,
|
||||
Version: ver,
|
||||
}); err != nil {
|
||||
logs.Log.Errorf(err.Error())
|
||||
logs.Log.Errorf("%s", err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package baseline
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/chainreactors/parsers"
|
||||
"github.com/chainreactors/words/rule"
|
||||
"github.com/expr-lang/expr"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestExpr(t *testing.T) {
|
||||
bl := &Baseline{
|
||||
//UrlString: "11111",
|
||||
Dir: true,
|
||||
SprayResult: &parsers.SprayResult{
|
||||
UrlString: "11111",
|
||||
},
|
||||
}
|
||||
exp, err := expr.Compile("baseline.Dir")
|
||||
fmt.Printf("%v\n", err)
|
||||
params := map[string]interface{}{
|
||||
"baseline": bl,
|
||||
}
|
||||
res, err := expr.Run(exp, params)
|
||||
fmt.Println(res, err)
|
||||
}
|
||||
|
||||
func TestRule(t *testing.T) {
|
||||
word := "admin"
|
||||
input, _ := ioutil.ReadFile("../templates/rule/authbypass.rule")
|
||||
ss := rule.EvalWithString(string(input), word)
|
||||
for i, s := range ss {
|
||||
fmt.Printf("%d: %s\n", i, s)
|
||||
}
|
||||
}
|
||||
@@ -251,3 +251,117 @@ func TestE2E_MixedStatusCodes(t *testing.T) {
|
||||
t.Fatalf("RunWithArgs: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// E2E: scan with --extract flags completes without error
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func extractServer() *httptest.Server {
|
||||
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.WriteHeader(200)
|
||||
fmt.Fprintf(w, `<html><head><title>Page %s</title></head><body>
|
||||
ip=10.20.30.40 [email protected]
|
||||
token=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.rTCH8cLoGxAm_xw68z-zXVKi9ie6xJn9tnVWjd_9ftE
|
||||
password="s3cret" version: 1.2.3
|
||||
AccessKeyId=AKIAIOSFODNN7EXAMPLE unique content %s
|
||||
</body></html>`, r.URL.Path, r.URL.Path)
|
||||
}))
|
||||
}
|
||||
|
||||
func TestE2E_ExtractByTag(t *testing.T) {
|
||||
server := extractServer()
|
||||
defer server.Close()
|
||||
|
||||
wordlist := writeTempFile(t, "xleakpage1\n")
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
_, err := runSpray(t, ctx, []string{
|
||||
"-u", server.URL,
|
||||
"-d", wordlist,
|
||||
"--extract", "ip",
|
||||
"--no-bar", "-q", "--no-stat",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("RunWithArgs with --extract ip: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestE2E_ExtractMultipleTags(t *testing.T) {
|
||||
server := extractServer()
|
||||
defer server.Close()
|
||||
|
||||
wordlist := writeTempFile(t, "xleakpage2\n")
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
_, err := runSpray(t, ctx, []string{
|
||||
"-u", server.URL,
|
||||
"-d", wordlist,
|
||||
"--extract", "ip",
|
||||
"--extract", "mail",
|
||||
"--extract", "jwt",
|
||||
"--no-bar", "-q", "--no-stat",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("RunWithArgs with multiple --extract: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestE2E_ExtractCategoryTag(t *testing.T) {
|
||||
server := extractServer()
|
||||
defer server.Close()
|
||||
|
||||
wordlist := writeTempFile(t, "xleakpage3\n")
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
_, err := runSpray(t, ctx, []string{
|
||||
"-u", server.URL,
|
||||
"-d", wordlist,
|
||||
"--extract", "cloud",
|
||||
"--no-bar", "-q", "--no-stat",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("RunWithArgs with --extract cloud: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestE2E_ExtractCustomRegex(t *testing.T) {
|
||||
server := extractServer()
|
||||
defer server.Close()
|
||||
|
||||
wordlist := writeTempFile(t, "xleakpage4\n")
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
_, err := runSpray(t, ctx, []string{
|
||||
"-u", server.URL,
|
||||
"-d", wordlist,
|
||||
"--extract", `version[\s:]+(\d+\.\d+\.\d+)`,
|
||||
"--no-bar", "-q", "--no-stat",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("RunWithArgs with custom regex extract: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestE2E_ReconPlugin(t *testing.T) {
|
||||
server := extractServer()
|
||||
defer server.Close()
|
||||
|
||||
wordlist := writeTempFile(t, "xleakpage5\n")
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
_, err := runSpray(t, ctx, []string{
|
||||
"-u", server.URL,
|
||||
"-d", wordlist,
|
||||
"--recon",
|
||||
"--no-bar", "-q", "--no-stat",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("RunWithArgs with --recon: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/chainreactors/logs"
|
||||
)
|
||||
|
||||
func TestFormatParsesResultFile(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
input := filepath.Join(dir, "results.json")
|
||||
|
||||
content := `{"url":"http://127.0.0.1:80","host":"127.0.0.1","title":"home","status":200}
|
||||
`
|
||||
if err := os.WriteFile(input, []byte(content), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var out bytes.Buffer
|
||||
oldLog := logs.Log
|
||||
logs.Log = logs.NewLogger(oldLog.Level)
|
||||
logs.Log.SetOutput(&out)
|
||||
defer func() {
|
||||
logs.Log = oldLog
|
||||
}()
|
||||
|
||||
opt := Option{}
|
||||
opt.Format = input
|
||||
opt.OutputProbe = "url,title"
|
||||
Format(opt)
|
||||
|
||||
if got := strings.TrimSpace(out.String()); got != "http://127.0.0.1:80\thome" {
|
||||
t.Fatalf("unexpected formatted output: got %q", got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package ihttp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
// TestFastHTTP_SetHost_NoDNS 确认使用 IP + SetHost 只修改 Host 头,不影响底层 dial 地址。
|
||||
// 这里 Host 使用 di.proxy.alipay.com(真实可解析的域名),用来证明 fasthttp 只根据 URL 里的 IP 进行 dial,
|
||||
// 而不会拿 Host 头去做 DNS 解析。
|
||||
func TestFastHTTP_SetHost_NoDNS(t *testing.T) {
|
||||
//var dialAddrs []string
|
||||
|
||||
client := &fasthttp.Client{
|
||||
//Dial: func(addr string) (net.Conn, error) {
|
||||
// // 记录底层实际要连接的地址
|
||||
// dialAddrs = append(dialAddrs, addr)
|
||||
// // 立刻返回错误,避免真实网络连接
|
||||
// return nil, net.ErrClosed
|
||||
//},
|
||||
}
|
||||
|
||||
// base 使用 IP,不包含域名
|
||||
base := "http://83.229.127.172:1234"
|
||||
// Host 使用你提供的域名
|
||||
hostHeader := "di.proxy.alipay.com"
|
||||
|
||||
req := fasthttp.AcquireRequest()
|
||||
defer fasthttp.ReleaseRequest(req)
|
||||
resp := fasthttp.AcquireResponse()
|
||||
defer fasthttp.ReleaseResponse(resp)
|
||||
|
||||
req.Header.SetMethod("GET")
|
||||
req.SetRequestURI(base + "/test")
|
||||
req.Header.SetHost(hostHeader)
|
||||
//req.UseHostHeader = true
|
||||
|
||||
// 确认 Host 头已被 SetHost 覆盖
|
||||
|
||||
// 执行请求,由我们自定义的 Dial 接管底层 dial
|
||||
err := client.DoTimeout(req, resp, 2*time.Second)
|
||||
fmt.Println(err)
|
||||
}
|
||||
+7
-2
@@ -112,6 +112,7 @@ type PluginOptions struct {
|
||||
Advance bool `short:"a" long:"advance" description:"Bool, enable all plugin" config:"all" `
|
||||
Extracts []string `long:"extract" description:"Strings, extract response, e.g.: --extract js --extract ip --extract version:(.*?)" config:"extract"`
|
||||
ExtractConfig string `long:"extract-config" description:"String, extract config filename" config:"extract-config"`
|
||||
ExtractContext int `long:"extract-context" default:"0" description:"Int, chars of context around extracted values, e.g.: --extract-context 50" config:"extract-context"`
|
||||
ActivePlugin bool `long:"active" description:"Bool, enable active finger path"`
|
||||
ReconPlugin bool `long:"recon" description:"Bool, enable recon" config:"recon"`
|
||||
BakPlugin bool `long:"bak" description:"Bool, enable bak found" config:"bak"`
|
||||
@@ -287,14 +288,18 @@ func (opt *Option) Prepare() error {
|
||||
return err
|
||||
}
|
||||
|
||||
if opt.ExtractContext > 0 {
|
||||
pkg.ExtractContextSize = opt.ExtractContext
|
||||
}
|
||||
|
||||
if opt.Extracts != nil {
|
||||
var knownNames []string
|
||||
for _, e := range opt.Extracts {
|
||||
// Check if it's a known template name/tag or a raw regex
|
||||
if _, ok := pkg.ExtractRegexps[e]; ok {
|
||||
knownNames = append(knownNames, e)
|
||||
} else if pkg.IsProtonExtractor(e) {
|
||||
knownNames = append(knownNames, e)
|
||||
} else {
|
||||
// Treat as raw regex pattern — add as custom proton extractor
|
||||
pkg.AddCustomExtractor(e, e)
|
||||
knownNames = append(knownNames, e)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRunWithArgsVersion(t *testing.T) {
|
||||
var out bytes.Buffer
|
||||
|
||||
if err := RunWithArgs(context.Background(), []string{"--version"}, RunOptions{
|
||||
Output: &out,
|
||||
Version: "dev",
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if got := strings.TrimSpace(out.String()); got != "dev" {
|
||||
t.Fatalf("unexpected version output: got %q want %q", got, "dev")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunWithArgsHelp(t *testing.T) {
|
||||
var out bytes.Buffer
|
||||
|
||||
if err := RunWithArgs(context.Background(), []string{"--help"}, RunOptions{
|
||||
Output: &out,
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if got := out.String(); !strings.Contains(got, "WIKI: https://chainreactors.github.io/wiki/spray") ||
|
||||
!strings.Contains(got, "spray -u http://example.com") {
|
||||
t.Fatalf("unexpected help output: %q", got)
|
||||
}
|
||||
}
|
||||
@@ -4,14 +4,14 @@ go 1.25.4
|
||||
|
||||
require (
|
||||
github.com/chainreactors/files v0.0.0-20240716182835-7884ee1e77f0
|
||||
github.com/chainreactors/fingers v1.1.1-0.20260127104843-a77f7d9e7b6c
|
||||
github.com/chainreactors/logs v0.0.0-20250312104344-9f30fa69d3c9
|
||||
github.com/chainreactors/neutron v0.0.0-20260519161201-b0482d8bb58b
|
||||
github.com/chainreactors/fingers v1.2.0
|
||||
github.com/chainreactors/logs v0.0.0-20260508055944-c678762ed15c
|
||||
github.com/chainreactors/neutron v0.0.0-20260521032511-94073805bf61
|
||||
github.com/chainreactors/parsers v0.0.0-20250605044448-6bc270f12c0e
|
||||
github.com/chainreactors/proton v0.0.0-20260520014408-07a02dfb1b00
|
||||
github.com/chainreactors/proton v0.2.0
|
||||
github.com/chainreactors/proxyclient v1.1.0
|
||||
github.com/chainreactors/utils v0.0.0-20260520013422-17d44b97430a
|
||||
github.com/chainreactors/words v0.0.0-20241002061906-25d8893158d9
|
||||
github.com/chainreactors/utils v0.0.0-20260520090033-cfefcb28cc6f
|
||||
github.com/chainreactors/words v0.0.0-20260520145736-270600e60fb4
|
||||
github.com/charmbracelet/lipgloss v1.1.0
|
||||
github.com/expr-lang/expr v1.16.9
|
||||
github.com/gookit/config/v2 v2.2.5
|
||||
@@ -20,7 +20,7 @@ require (
|
||||
github.com/valyala/fasthttp v1.53.0
|
||||
github.com/vbauerster/mpb/v8 v8.7.3
|
||||
golang.org/x/time v0.5.0
|
||||
sigs.k8s.io/yaml v1.4.0
|
||||
sigs.k8s.io/yaml v1.6.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -86,6 +86,8 @@ require (
|
||||
github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52 // indirect
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.3 // indirect
|
||||
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
|
||||
golang.org/x/net v0.49.0 // indirect
|
||||
@@ -97,3 +99,5 @@ require (
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
replace github.com/chainreactors/parsers => /mnt/chainreactors/parsers
|
||||
@@ -103,27 +103,41 @@ github.com/chainreactors/files v0.0.0-20240716182835-7884ee1e77f0/go.mod h1:NSxG
|
||||
github.com/chainreactors/fingers v0.0.0-20240702104653-a66e34aa41df/go.mod h1:l8AO6ZbIL8WQ8PkihCK/MD6Iww/O+LY/osAhRJjThs4=
|
||||
github.com/chainreactors/fingers v1.1.1-0.20260127104843-a77f7d9e7b6c h1:QZK1820vc2O0yFK5ve90i+oj3dNuMTEkomOwbDTyR0U=
|
||||
github.com/chainreactors/fingers v1.1.1-0.20260127104843-a77f7d9e7b6c/go.mod h1:vXGHeolL31QtHMPVs/+fMhispMS86rDpRL3ayEMOlXY=
|
||||
github.com/chainreactors/fingers v1.2.0 h1:x699lshOI8gAsoedk/QQDtpMPA8mXfcKWmUgQ292UbM=
|
||||
github.com/chainreactors/fingers v1.2.0/go.mod h1:owlti2VYK2YpnLqdZ1HWACK4upuyFgjDOfSdm36pAjw=
|
||||
github.com/chainreactors/logs v0.0.0-20240207121836-c946f072f81f/go.mod h1:6Mv6W70JrtL6VClulZhmMRZnoYpcTahcDTKLMNEjK0o=
|
||||
github.com/chainreactors/logs v0.0.0-20241030063019-8ca66a3ee307/go.mod h1:6Mv6W70JrtL6VClulZhmMRZnoYpcTahcDTKLMNEjK0o=
|
||||
github.com/chainreactors/logs v0.0.0-20250312104344-9f30fa69d3c9 h1:u3qAuV4/IY5lvE0utSl5w0p6MdTNpXYA2FJee93c21A=
|
||||
github.com/chainreactors/logs v0.0.0-20250312104344-9f30fa69d3c9/go.mod h1:6Mv6W70JrtL6VClulZhmMRZnoYpcTahcDTKLMNEjK0o=
|
||||
github.com/chainreactors/logs v0.0.0-20260508055944-c678762ed15c h1:6Net2Mgo/qo6ADFBZJWWScKuMfZ0rbzLqSCVDuLKFdc=
|
||||
github.com/chainreactors/logs v0.0.0-20260508055944-c678762ed15c/go.mod h1:VrXmYPbNN5AVoo1sc5aeyPVBYqubMdb3KO/tn5rRZpo=
|
||||
github.com/chainreactors/neutron v0.0.0-20251216154716-7c28cb6fdf03/go.mod h1:CktNFh1I1sNwFI9Ml+t0X/uuEoO4eAW5Dmb/cuzV8BY=
|
||||
github.com/chainreactors/neutron v0.0.0-20260203032004-95c9e8431214/go.mod h1:CktNFh1I1sNwFI9Ml+t0X/uuEoO4eAW5Dmb/cuzV8BY=
|
||||
github.com/chainreactors/neutron v0.0.0-20260519161201-b0482d8bb58b h1:bX3sRTEkXUIyqD9zsywNItE/gt7vKQGZnRv8esMFaMI=
|
||||
github.com/chainreactors/neutron v0.0.0-20260519161201-b0482d8bb58b/go.mod h1:Gh/QvVFVB9Gt0OeUrPpKq+kORAIHOD2O5a8MbKmu2IY=
|
||||
github.com/chainreactors/neutron v0.0.0-20260521032511-94073805bf61 h1:6axVdbNI86GgkOZ9VEY/6bdHcXNvGUvsoyWq5QpynqY=
|
||||
github.com/chainreactors/neutron v0.0.0-20260521032511-94073805bf61/go.mod h1:6+ADabXxJWTHcyHr6Wpdlamy6bBN4Y44sBrCJvd6Igs=
|
||||
github.com/chainreactors/parsers v0.0.0-20250605044448-6bc270f12c0e h1:8AgGNkG1JoO6CIGlMNOecUCaQCnB/Ko/WI3Y6VgVPrI=
|
||||
github.com/chainreactors/parsers v0.0.0-20250605044448-6bc270f12c0e/go.mod h1:7rXdYz6jrdjF0WUH1ICcAXKIKKjKmJo2PU8u43V7jkA=
|
||||
github.com/chainreactors/proton v0.0.0-20260520014408-07a02dfb1b00 h1:4xzKQLv5YOTtLyhkZvBd79NkU05xAnk7MpcbkcUwpP4=
|
||||
github.com/chainreactors/proton v0.0.0-20260520014408-07a02dfb1b00/go.mod h1:IAEgPisnitDd8HZ58cvVSRbBCfH3paB1DacJUCAUnxY=
|
||||
github.com/chainreactors/proton v0.2.0 h1:2QG/6TxL85rlQlKyCevFnokMkU5SXfWuRVWw3J8JbQ0=
|
||||
github.com/chainreactors/proton v0.2.0/go.mod h1:X70Tonkeb5OW2JUvUhW22D4BHpY3xRx6lvglR/A5nIM=
|
||||
github.com/chainreactors/proxyclient v1.1.0 h1:hOwEj1Vwz4FTXKErnBnoFrS75lT0BkRCmtdVL2V1Bcs=
|
||||
github.com/chainreactors/proxyclient v1.1.0/go.mod h1:xSNRChMYF8en5O5ZQVmCOmNTTyQhvsrk0D0vluh/JKk=
|
||||
github.com/chainreactors/utils v0.0.0-20240528085651-ba1b255482c1/go.mod h1:JA4eiQZm+7AsfjXBcIzIdVKBEhDCb16eNtWFCGTxlvs=
|
||||
github.com/chainreactors/utils v0.0.0-20240704062557-662d623b74f4/go.mod h1:JA4eiQZm+7AsfjXBcIzIdVKBEhDCb16eNtWFCGTxlvs=
|
||||
github.com/chainreactors/utils v0.0.0-20240716182459-e85f2b01ee16/go.mod h1:LajXuvESQwP+qCMAvlcoSXppQCjuLlBrnQpu9XQ1HtU=
|
||||
github.com/chainreactors/utils v0.0.0-20250831165528-f06246b0f311/go.mod h1:LajXuvESQwP+qCMAvlcoSXppQCjuLlBrnQpu9XQ1HtU=
|
||||
github.com/chainreactors/utils v0.0.0-20260507101628-fd69d955ae21/go.mod h1:LajXuvESQwP+qCMAvlcoSXppQCjuLlBrnQpu9XQ1HtU=
|
||||
github.com/chainreactors/utils v0.0.0-20260520013422-17d44b97430a h1:P+p4v3c7vCAsqQZ5NMx4XUxJiTevnFNgapUcPUKINhs=
|
||||
github.com/chainreactors/utils v0.0.0-20260520013422-17d44b97430a/go.mod h1:LajXuvESQwP+qCMAvlcoSXppQCjuLlBrnQpu9XQ1HtU=
|
||||
github.com/chainreactors/utils v0.0.0-20260520090033-cfefcb28cc6f h1:6KakKIsbclMSobG1m7EeA08rVQj+qvGYb64ZYkSj/FE=
|
||||
github.com/chainreactors/utils v0.0.0-20260520090033-cfefcb28cc6f/go.mod h1:LajXuvESQwP+qCMAvlcoSXppQCjuLlBrnQpu9XQ1HtU=
|
||||
github.com/chainreactors/words v0.0.0-20241002061906-25d8893158d9 h1:BXaPgD1pfBPvFSy9DHZDxW0bHX96ONZT6HW+WbXlZCk=
|
||||
github.com/chainreactors/words v0.0.0-20241002061906-25d8893158d9/go.mod h1:zfz367PUmyaX6oAqV9SktVqyRXKlEh0sel9Wsq9dd2c=
|
||||
github.com/chainreactors/words v0.0.0-20260520145736-270600e60fb4 h1:lvnDYEkatmZFHP5i321qQXK9L4vKRfso/uUfr5tOeC8=
|
||||
github.com/chainreactors/words v0.0.0-20260520145736-270600e60fb4/go.mod h1:zfz367PUmyaX6oAqV9SktVqyRXKlEh0sel9Wsq9dd2c=
|
||||
github.com/charlievieth/fastwalk v1.0.14 h1:3Eh5uaFGwHZd8EGwTjJnSpBkfwfsak9h6ICgnWlhAyg=
|
||||
github.com/charlievieth/fastwalk v1.0.14/go.mod h1:diVcUreiU1aQ4/Wu3NbxxH4/KYdKpLDojrQ1Bb2KgNY=
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
|
||||
@@ -565,6 +579,10 @@ go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqe
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
|
||||
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
||||
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
||||
go.yaml.in/yaml/v3 v3.0.3 h1:bXOww4E/J3f66rav3pX3m8w6jDE4knZjGOw8b5Y6iNE=
|
||||
go.yaml.in/yaml/v3 v3.0.3/go.mod h1:tBHosrYAkRZjRAOREWbDnBXUf08JOwYq++0QNwQiWzI=
|
||||
go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc=
|
||||
go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
@@ -1051,3 +1069,5 @@ rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
|
||||
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
|
||||
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
|
||||
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
|
||||
Binary file not shown.
+78
-17
@@ -1,6 +1,7 @@
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -10,7 +11,7 @@ import (
|
||||
"github.com/chainreactors/neutron/protocols"
|
||||
"github.com/chainreactors/parsers"
|
||||
"github.com/chainreactors/proton/protocols/file"
|
||||
protonTmpl "github.com/chainreactors/proton/templates"
|
||||
protonTmpl "github.com/chainreactors/proton/template"
|
||||
yaml "sigs.k8s.io/yaml/goyaml.v3"
|
||||
)
|
||||
|
||||
@@ -20,6 +21,7 @@ var (
|
||||
protonTemplateMap map[string]*protonTmpl.Template // id → template
|
||||
protonTagMap map[string][]string // tag → []id
|
||||
protonMu sync.RWMutex
|
||||
ExtractContextSize int // 0=disabled, >0=chars of context each side
|
||||
)
|
||||
|
||||
// LoadProtonTemplates parses YAML template docs and builds a Scanner.
|
||||
@@ -50,6 +52,7 @@ func LoadProtonTemplates(yamlDocs [][]byte) error {
|
||||
tag = strings.TrimSpace(tag)
|
||||
protonTagMap[tag] = append(protonTagMap[tag], tmpl.Id)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
rebuildScanner()
|
||||
@@ -146,6 +149,35 @@ func rebuildScanner() {
|
||||
ProtonScanner = file.NewScanner(rules, nil)
|
||||
}
|
||||
|
||||
type extractHit struct {
|
||||
value string
|
||||
line int
|
||||
offset int
|
||||
severity string
|
||||
}
|
||||
|
||||
func captureContext(content []byte, lineOffset int, value string, ctxSize int) string {
|
||||
searchEnd := len(content)
|
||||
if nlPos := bytes.IndexByte(content[lineOffset:], '\n'); nlPos >= 0 {
|
||||
searchEnd = lineOffset + nlPos
|
||||
}
|
||||
line := content[lineOffset:searchEnd]
|
||||
idx := bytes.Index(line, []byte(value))
|
||||
if idx == -1 {
|
||||
return ""
|
||||
}
|
||||
matchStart := lineOffset + idx
|
||||
start := matchStart - ctxSize
|
||||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
end := matchStart + len(value) + ctxSize
|
||||
if end > len(content) {
|
||||
end = len(content)
|
||||
}
|
||||
return string(content[start:end])
|
||||
}
|
||||
|
||||
// ProtonExtract runs proton scanner on in-memory content and returns
|
||||
// parsers.Extracted results compatible with spray's output format.
|
||||
func ProtonExtract(content []byte) parsers.Extracteds {
|
||||
@@ -157,36 +189,49 @@ func ProtonExtract(content []byte) parsers.Extracteds {
|
||||
return nil
|
||||
}
|
||||
|
||||
resultMap := make(map[string]map[string]struct{})
|
||||
resultMap := make(map[string][]extractHit)
|
||||
|
||||
for _, group := range scanner.Groups {
|
||||
findings := scanner.ScanData(content, "response", group)
|
||||
for _, f := range findings {
|
||||
if resultMap[f.TemplateID] == nil {
|
||||
resultMap[f.TemplateID] = make(map[string]struct{})
|
||||
}
|
||||
for _, e := range f.Extracts {
|
||||
resultMap[f.TemplateID][e.Value] = struct{}{}
|
||||
}
|
||||
for _, events := range f.Matches {
|
||||
for _, e := range events {
|
||||
resultMap[f.TemplateID][e.Value] = struct{}{}
|
||||
}
|
||||
resultMap[f.TemplateID] = append(resultMap[f.TemplateID], extractHit{
|
||||
value: e.Value, line: e.Line, offset: e.Offset, severity: f.Severity,
|
||||
})
|
||||
}
|
||||
// f.Matches contains matcher hits (gate only), not extractor results — skip
|
||||
if f.Result != nil {
|
||||
for _, val := range f.Result.OutputExtracts {
|
||||
resultMap[f.TemplateID][val] = struct{}{}
|
||||
resultMap[f.TemplateID] = append(resultMap[f.TemplateID], extractHit{
|
||||
value: val, severity: f.Severity,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var extracteds parsers.Extracteds
|
||||
for name, vals := range resultMap {
|
||||
displayName := strings.TrimPrefix(name, "spray-")
|
||||
extracted := &parsers.Extracted{Name: displayName}
|
||||
for v := range vals {
|
||||
extracted.ExtractResult = append(extracted.ExtractResult, v)
|
||||
for templateID, hits := range resultMap {
|
||||
displayName := strings.TrimPrefix(templateID, "spray-")
|
||||
extracted := &parsers.Extracted{
|
||||
Name: displayName,
|
||||
Severity: hits[0].severity,
|
||||
}
|
||||
seen := make(map[string]struct{})
|
||||
for _, h := range hits {
|
||||
if _, ok := seen[h.value]; ok {
|
||||
continue
|
||||
}
|
||||
seen[h.value] = struct{}{}
|
||||
extracted.ExtractResult = append(extracted.ExtractResult, h.value)
|
||||
if ExtractContextSize > 0 {
|
||||
extracted.Items = append(extracted.Items, parsers.ExtractItem{
|
||||
Value: h.value,
|
||||
Line: h.line,
|
||||
Offset: h.offset,
|
||||
Ctx: captureContext(content, h.offset, h.value, ExtractContextSize),
|
||||
})
|
||||
}
|
||||
}
|
||||
if len(extracted.ExtractResult) > 0 {
|
||||
extracteds = append(extracteds, extracted)
|
||||
@@ -222,3 +267,19 @@ func ProtonExtractorNames() []string {
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// IsProtonExtractor checks if a name matches a proton template ID or tag.
|
||||
func IsProtonExtractor(name string) bool {
|
||||
protonMu.RLock()
|
||||
defer protonMu.RUnlock()
|
||||
if _, ok := protonTemplateMap["spray-"+name]; ok {
|
||||
return true
|
||||
}
|
||||
if _, ok := protonTemplateMap[name]; ok {
|
||||
return true
|
||||
}
|
||||
if _, ok := protonTagMap[name]; ok {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,495 @@
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
yaml "sigs.k8s.io/yaml/goyaml.v3"
|
||||
)
|
||||
|
||||
func mustLoadTemplates(t *testing.T) {
|
||||
t.Helper()
|
||||
data := LoadEmbeddedConfig("proton_rules")
|
||||
if len(data) == 0 {
|
||||
t.Fatal("proton_rules embedded data is empty")
|
||||
}
|
||||
|
||||
var templates []interface{}
|
||||
if err := yaml.Unmarshal(data, &templates); err != nil {
|
||||
t.Fatalf("unmarshal proton_rules: %v", err)
|
||||
}
|
||||
docs := make([][]byte, 0, len(templates))
|
||||
for _, tmpl := range templates {
|
||||
doc, err := yaml.Marshal(tmpl)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
docs = append(docs, doc)
|
||||
}
|
||||
|
||||
if err := LoadProtonTemplates(docs); err != nil {
|
||||
t.Fatalf("LoadProtonTemplates: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProtonTemplatesLoad(t *testing.T) {
|
||||
mustLoadTemplates(t)
|
||||
|
||||
names := ProtonExtractorNames()
|
||||
if len(names) == 0 {
|
||||
t.Fatal("no proton templates loaded")
|
||||
}
|
||||
|
||||
want := []string{
|
||||
"spray-ipv4-address",
|
||||
"spray-jwt-token",
|
||||
"spray-js-file",
|
||||
"spray-email-address",
|
||||
"spray-aws-access-key",
|
||||
"spray-password-leak",
|
||||
}
|
||||
nameSet := make(map[string]bool)
|
||||
for _, n := range names {
|
||||
nameSet[n] = true
|
||||
}
|
||||
for _, w := range want {
|
||||
if !nameSet[w] {
|
||||
t.Errorf("missing template: %s", w)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestProtonTagMapping(t *testing.T) {
|
||||
mustLoadTemplates(t)
|
||||
|
||||
tests := []struct {
|
||||
tag string
|
||||
wantMatch string
|
||||
}{
|
||||
{"ip", "spray-ipv4-address"},
|
||||
{"jwt", "spray-jwt-token"},
|
||||
{"js", "spray-js-file"},
|
||||
{"mail", "spray-email-address"},
|
||||
{"phone", "spray-phone-number"},
|
||||
{"idcard", "spray-id-card"},
|
||||
{"password", "spray-password-leak"},
|
||||
{"username", "spray-username-leak"},
|
||||
{"inter-ip", "spray-internal-ip"},
|
||||
{"oss", "spray-oss-key"},
|
||||
{"s3", "spray-s3-bucket"},
|
||||
{"aws-ak", "spray-aws-access-key"},
|
||||
{"rsa-key", "spray-rsa-private-key"},
|
||||
{"jdbc", "spray-jdbc-connection"},
|
||||
{"swagger", "spray-swagger-endpoint"},
|
||||
{"lfi", "spray-lfi-indicator"},
|
||||
{"upload", "spray-upload-form"},
|
||||
{"ssrf", "spray-url-as-value"},
|
||||
{"redirect", "spray-location-redirect"},
|
||||
{"mac", "spray-mac-address"},
|
||||
{"sensitive-field", "spray-sensitive-field"},
|
||||
{"linkfinder", "spray-linkfinder"},
|
||||
{"url-schemes", "spray-url-schemes"},
|
||||
{"auth-header", "spray-auth-header"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.tag, func(t *testing.T) {
|
||||
if !IsProtonExtractor(tt.tag) {
|
||||
t.Fatalf("IsProtonExtractor(%q) = false, want true", tt.tag)
|
||||
}
|
||||
protonMu.RLock()
|
||||
ids := protonTagMap[tt.tag]
|
||||
protonMu.RUnlock()
|
||||
found := false
|
||||
for _, id := range ids {
|
||||
if id == tt.wantMatch {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("tag %q -> got %v, want to include %s", tt.tag, ids, tt.wantMatch)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProtonCategoryTags(t *testing.T) {
|
||||
mustLoadTemplates(t)
|
||||
|
||||
categories := []struct {
|
||||
tag string
|
||||
minCount int
|
||||
}{
|
||||
{"cloud", 7},
|
||||
{"token", 9},
|
||||
{"credential", 9},
|
||||
{"pentest", 20},
|
||||
{"info", 10},
|
||||
{"crawl", 6},
|
||||
}
|
||||
|
||||
for _, cat := range categories {
|
||||
t.Run(cat.tag, func(t *testing.T) {
|
||||
if !IsProtonExtractor(cat.tag) {
|
||||
t.Fatalf("IsProtonExtractor(%q) = false", cat.tag)
|
||||
}
|
||||
protonMu.RLock()
|
||||
ids := protonTagMap[cat.tag]
|
||||
protonMu.RUnlock()
|
||||
if len(ids) < cat.minCount {
|
||||
t.Errorf("tag %q has %d templates, want >= %d", cat.tag, len(ids), cat.minCount)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnableExtractorsByTag(t *testing.T) {
|
||||
mustLoadTemplates(t)
|
||||
|
||||
EnableExtractors([]string{"ip"})
|
||||
|
||||
protonMu.RLock()
|
||||
scanner := ProtonScanner
|
||||
protonMu.RUnlock()
|
||||
|
||||
if scanner == nil || len(scanner.Groups) == 0 {
|
||||
t.Fatal("scanner is empty after EnableExtractors([ip])")
|
||||
}
|
||||
|
||||
EnableExtractors(nil)
|
||||
}
|
||||
|
||||
func TestEnableExtractorsByID(t *testing.T) {
|
||||
mustLoadTemplates(t)
|
||||
|
||||
EnableExtractors([]string{"spray-jwt-token"})
|
||||
|
||||
protonMu.RLock()
|
||||
scanner := ProtonScanner
|
||||
protonMu.RUnlock()
|
||||
|
||||
if scanner == nil || len(scanner.Groups) == 0 {
|
||||
t.Fatal("scanner is empty after EnableExtractors([spray-jwt-token])")
|
||||
}
|
||||
|
||||
EnableExtractors(nil)
|
||||
}
|
||||
|
||||
func TestEnableExtractorsShortName(t *testing.T) {
|
||||
mustLoadTemplates(t)
|
||||
|
||||
EnableExtractors([]string{"ipv4-address"})
|
||||
|
||||
protonMu.RLock()
|
||||
scanner := ProtonScanner
|
||||
protonMu.RUnlock()
|
||||
|
||||
if scanner == nil || len(scanner.Groups) == 0 {
|
||||
t.Fatal("scanner is empty after EnableExtractors([ipv4-address]) — spray- prefix resolution failed")
|
||||
}
|
||||
|
||||
EnableExtractors(nil)
|
||||
}
|
||||
|
||||
func TestProtonExtractIP(t *testing.T) {
|
||||
mustLoadTemplates(t)
|
||||
|
||||
EnableExtractors([]string{"ip"})
|
||||
defer EnableExtractors(nil)
|
||||
|
||||
body := []byte(`{"origin": "192.168.1.100", "host": "example.com"}`)
|
||||
results := ProtonExtract(body)
|
||||
|
||||
if len(results) == 0 {
|
||||
t.Fatal("ProtonExtract returned no results for body containing IP")
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, r := range results {
|
||||
for _, v := range r.ExtractResult {
|
||||
if strings.Contains(v, "192.168.1.100") {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("expected to extract 192.168.1.100, got %v", results)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProtonExtractJWT(t *testing.T) {
|
||||
mustLoadTemplates(t)
|
||||
|
||||
EnableExtractors([]string{"jwt"})
|
||||
defer EnableExtractors(nil)
|
||||
|
||||
jwt := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U"
|
||||
body := []byte(`<html><body>token: ` + jwt + `</body></html>`)
|
||||
results := ProtonExtract(body)
|
||||
|
||||
if len(results) == 0 {
|
||||
t.Fatal("ProtonExtract returned no results for body containing JWT")
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, r := range results {
|
||||
for _, v := range r.ExtractResult {
|
||||
if strings.Contains(v, "eyJ") {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("expected to extract JWT, got %v", results)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProtonExtractEmail(t *testing.T) {
|
||||
mustLoadTemplates(t)
|
||||
|
||||
EnableExtractors([]string{"mail"})
|
||||
defer EnableExtractors(nil)
|
||||
|
||||
body := []byte(`<html><body>Contact us at [email protected] for support</body></html>`)
|
||||
results := ProtonExtract(body)
|
||||
|
||||
if len(results) == 0 {
|
||||
t.Fatal("ProtonExtract returned no results for body containing email")
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, r := range results {
|
||||
for _, v := range r.ExtractResult {
|
||||
if strings.Contains(v, "[email protected]") {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("expected to extract [email protected], got %v", results)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProtonExtractPassword(t *testing.T) {
|
||||
mustLoadTemplates(t)
|
||||
|
||||
EnableExtractors([]string{"password"})
|
||||
defer EnableExtractors(nil)
|
||||
|
||||
body := []byte(`config.password = "s3cretP@ss"`)
|
||||
results := ProtonExtract(body)
|
||||
|
||||
if len(results) == 0 {
|
||||
t.Fatal("ProtonExtract returned no results for body containing password leak")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProtonExtractNoMatch(t *testing.T) {
|
||||
mustLoadTemplates(t)
|
||||
|
||||
EnableExtractors([]string{"ip"})
|
||||
defer EnableExtractors(nil)
|
||||
|
||||
body := []byte(`<html><body>Hello World, nothing interesting here</body></html>`)
|
||||
results := ProtonExtract(body)
|
||||
|
||||
if len(results) != 0 {
|
||||
t.Errorf("expected no extracts for clean body, got %v", results)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProtonExtractMultipleTags(t *testing.T) {
|
||||
mustLoadTemplates(t)
|
||||
|
||||
EnableExtractors([]string{"ip", "mail"})
|
||||
defer EnableExtractors(nil)
|
||||
|
||||
body := []byte(`server=192.168.0.1 [email protected]`)
|
||||
results := ProtonExtract(body)
|
||||
|
||||
nameSet := make(map[string]bool)
|
||||
for _, r := range results {
|
||||
nameSet[r.Name] = true
|
||||
}
|
||||
|
||||
if !nameSet["ipv4-address"] {
|
||||
t.Error("missing ipv4-address extraction")
|
||||
}
|
||||
if !nameSet["email-address"] {
|
||||
t.Error("missing email-address extraction")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddCustomExtractor(t *testing.T) {
|
||||
mustLoadTemplates(t)
|
||||
|
||||
AddCustomExtractor("version", `version[:\s]+(\d+\.\d+\.\d+)`)
|
||||
defer EnableExtractors(nil)
|
||||
|
||||
if !IsProtonExtractor("custom-version") {
|
||||
t.Error("custom extractor not found by ID")
|
||||
}
|
||||
|
||||
body := []byte(`<html>version: 2.1.0</html>`)
|
||||
EnableExtractors([]string{"custom-version"})
|
||||
results := ProtonExtract(body)
|
||||
|
||||
if len(results) == 0 {
|
||||
t.Fatal("custom extractor returned no results")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProtonExtractSeverity(t *testing.T) {
|
||||
mustLoadTemplates(t)
|
||||
|
||||
EnableExtractors([]string{"ip"})
|
||||
defer EnableExtractors(nil)
|
||||
|
||||
body := []byte(`{"origin": "192.168.1.100"}`)
|
||||
results := ProtonExtract(body)
|
||||
|
||||
if len(results) == 0 {
|
||||
t.Fatal("no results")
|
||||
}
|
||||
for _, r := range results {
|
||||
if r.Severity == "" {
|
||||
t.Errorf("Severity is empty for %s", r.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestProtonExtractContext(t *testing.T) {
|
||||
mustLoadTemplates(t)
|
||||
|
||||
oldCtx := ExtractContextSize
|
||||
ExtractContextSize = 20
|
||||
defer func() { ExtractContextSize = oldCtx }()
|
||||
|
||||
EnableExtractors([]string{"ip"})
|
||||
defer EnableExtractors(nil)
|
||||
|
||||
body := []byte(`server_addr = "192.168.1.100" # internal`)
|
||||
results := ProtonExtract(body)
|
||||
|
||||
if len(results) == 0 {
|
||||
t.Fatal("no results")
|
||||
}
|
||||
for _, r := range results {
|
||||
if len(r.Items) == 0 {
|
||||
t.Fatal("Items is empty when ExtractContextSize > 0")
|
||||
}
|
||||
item := r.Items[0]
|
||||
if item.Value == "" {
|
||||
t.Error("item.Value is empty")
|
||||
}
|
||||
if item.Ctx == "" {
|
||||
t.Error("item.Ctx is empty — context capture failed")
|
||||
}
|
||||
if !strings.Contains(item.Ctx, "192.168.1.100") {
|
||||
t.Errorf("context should contain the value, got: %s", item.Ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestProtonExtractContextDisabled(t *testing.T) {
|
||||
mustLoadTemplates(t)
|
||||
|
||||
oldCtx := ExtractContextSize
|
||||
ExtractContextSize = 0
|
||||
defer func() { ExtractContextSize = oldCtx }()
|
||||
|
||||
EnableExtractors([]string{"ip"})
|
||||
defer EnableExtractors(nil)
|
||||
|
||||
body := []byte(`{"origin": "192.168.1.100"}`)
|
||||
results := ProtonExtract(body)
|
||||
|
||||
if len(results) == 0 {
|
||||
t.Fatal("no results")
|
||||
}
|
||||
for _, r := range results {
|
||||
if len(r.Items) != 0 {
|
||||
t.Error("Items should be empty when ExtractContextSize=0")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestProtonExtractLFI(t *testing.T) {
|
||||
mustLoadTemplates(t)
|
||||
|
||||
EnableExtractors([]string{"lfi"})
|
||||
defer EnableExtractors(nil)
|
||||
|
||||
body := []byte("root:x:0:0:root:/root:/bin/bash\ndaemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin\n")
|
||||
results := ProtonExtract(body)
|
||||
|
||||
if len(results) == 0 {
|
||||
t.Fatal("LFI indicator not detected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProtonExtractAuthHeader(t *testing.T) {
|
||||
mustLoadTemplates(t)
|
||||
|
||||
EnableExtractors([]string{"auth-header"})
|
||||
defer EnableExtractors(nil)
|
||||
|
||||
body := []byte("HTTP/1.1 200 OK\r\nAuthorization: Bearer eyJhbGciOiJSUzI1NiJ9.test\r\n\r\n")
|
||||
results := ProtonExtract(body)
|
||||
|
||||
if len(results) == 0 {
|
||||
t.Fatal("Authorization header not extracted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProtonExtractSensitiveField(t *testing.T) {
|
||||
mustLoadTemplates(t)
|
||||
|
||||
EnableExtractors([]string{"sensitive-field"})
|
||||
defer EnableExtractors(nil)
|
||||
|
||||
body := []byte(`{"api_key": "sk-1234567890abcdef", "name": "test"}`)
|
||||
results := ProtonExtract(body)
|
||||
|
||||
if len(results) == 0 {
|
||||
t.Fatal("sensitive field not extracted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProtonExtractLocationRedirect(t *testing.T) {
|
||||
mustLoadTemplates(t)
|
||||
|
||||
EnableExtractors([]string{"redirect"})
|
||||
defer EnableExtractors(nil)
|
||||
|
||||
body := []byte("HTTP/1.1 302 Found\r\nLocation: https://example.com/login\r\n\r\n")
|
||||
results := ProtonExtract(body)
|
||||
|
||||
if len(results) == 0 {
|
||||
t.Fatal("Location redirect not extracted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsProtonExtractor(t *testing.T) {
|
||||
mustLoadTemplates(t)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
want bool
|
||||
}{
|
||||
{"ip", true},
|
||||
{"cloud", true},
|
||||
{"spray-jwt-token", true},
|
||||
{"jwt-token", true},
|
||||
{"nonexistent-xyz", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := IsProtonExtractor(tt.name); got != tt.want {
|
||||
t.Errorf("IsProtonExtractor(%q) = %v, want %v", tt.name, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,9 @@ var sprayDictData []byte
|
||||
//go:embed data/extract.bin
|
||||
var extractData []byte
|
||||
|
||||
//go:embed data/proton_rules.bin
|
||||
var protonRulesData []byte
|
||||
|
||||
//go:embed data/port.bin
|
||||
var portData []byte
|
||||
|
||||
@@ -34,6 +37,8 @@ func loadEmbeddedConfig(typ string) []byte {
|
||||
return encode.MustDeflateDeCompress(sprayDictData)
|
||||
}else if typ == "extract" {
|
||||
return encode.MustDeflateDeCompress(extractData)
|
||||
}else if typ == "proton_rules" {
|
||||
return encode.MustDeflateDeCompress(protonRulesData)
|
||||
}else if typ == "port" {
|
||||
return encode.MustDeflateDeCompress(portData)
|
||||
}
|
||||
|
||||
+1
-1
Submodule templates updated: 7d5675ccab...eff4469dc2.
Reference in new issue
Block a user