mirror of
https://github.com/shadow1ng/fscan.git
synced 2026-09-22 03:10:42 +08:00
@@ -0,0 +1,191 @@
|
||||
package fingerprint
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// calcPriority 测试
|
||||
// =============================================================================
|
||||
|
||||
func TestCalcPriority_FaviconHighest(t *testing.T) {
|
||||
fp := &EnhancedFingerprint{}
|
||||
p := calcPriority(fp, "favicon")
|
||||
if p != 100 {
|
||||
t.Errorf("favicon 优先级应为 100,实际 %d", p)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalcPriority_RegexMedium(t *testing.T) {
|
||||
fp := &EnhancedFingerprint{}
|
||||
p := calcPriority(fp, "regex")
|
||||
if p != 50 {
|
||||
t.Errorf("regex 优先级应为 50,实际 %d", p)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalcPriority_WordLow(t *testing.T) {
|
||||
fp := &EnhancedFingerprint{}
|
||||
p := calcPriority(fp, "word")
|
||||
if p != 30 {
|
||||
t.Errorf("word 优先级应为 30,实际 %d", p)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalcPriority_UnknownTypeZero(t *testing.T) {
|
||||
fp := &EnhancedFingerprint{}
|
||||
p := calcPriority(fp, "unknown")
|
||||
if p != 0 {
|
||||
t.Errorf("未知类型优先级应为 0,实际 %d", p)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalcPriority_VerifiedBonus(t *testing.T) {
|
||||
fp := &EnhancedFingerprint{}
|
||||
fp.Info.Metadata = map[string]interface{}{
|
||||
"verified": true,
|
||||
}
|
||||
p := calcPriority(fp, "word")
|
||||
// word(30) + verified(20) = 50
|
||||
if p != 50 {
|
||||
t.Errorf("word+verified 优先级应为 50,实际 %d", p)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalcPriority_VerifiedFavicon(t *testing.T) {
|
||||
fp := &EnhancedFingerprint{}
|
||||
fp.Info.Metadata = map[string]interface{}{
|
||||
"verified": true,
|
||||
}
|
||||
p := calcPriority(fp, "favicon")
|
||||
// favicon(100) + verified(20) = 120
|
||||
if p != 120 {
|
||||
t.Errorf("favicon+verified 优先级应为 120,实际 %d", p)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalcPriority_VerifiedFalse(t *testing.T) {
|
||||
fp := &EnhancedFingerprint{}
|
||||
fp.Info.Metadata = map[string]interface{}{
|
||||
"verified": false,
|
||||
}
|
||||
p := calcPriority(fp, "regex")
|
||||
// verified=false 不加分
|
||||
if p != 50 {
|
||||
t.Errorf("verified=false 时优先级应为 50,实际 %d", p)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalcPriority_NilMetadata(t *testing.T) {
|
||||
fp := &EnhancedFingerprint{}
|
||||
// Metadata 为 nil,不加分
|
||||
p := calcPriority(fp, "favicon")
|
||||
if p != 100 {
|
||||
t.Errorf("nil metadata 时 favicon 优先级应为 100,实际 %d", p)
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// matchRegex 测试 - 需要初始化 enhancedDB
|
||||
// =============================================================================
|
||||
|
||||
func initEnhancedDBForTest(t *testing.T) {
|
||||
t.Helper()
|
||||
if enhancedDB == nil {
|
||||
if err := LoadEnhancedFingerprints(); err != nil {
|
||||
t.Fatalf("LoadEnhancedFingerprints 失败: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMatchRegex_BodyMatch(t *testing.T) {
|
||||
initEnhancedDBForTest(t)
|
||||
|
||||
matcher := createMatcher("regex", nil, []string{`nginx/[\d.]+`}, nil, "body", "", false)
|
||||
result := matchRegex(matcher, "Server: nginx/1.18.0 running", "")
|
||||
if !result {
|
||||
t.Error("body 中应匹配 nginx 版本正则")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMatchRegex_HeaderMatch(t *testing.T) {
|
||||
initEnhancedDBForTest(t)
|
||||
|
||||
matcher := createMatcher("regex", nil, []string{`X-Powered-By: PHP/[\d.]+`}, nil, "header", "", false)
|
||||
result := matchRegex(matcher, "", "X-Powered-By: PHP/7.4.3")
|
||||
if !result {
|
||||
t.Error("header 中应匹配 PHP 版本正则")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMatchRegex_NoMatch(t *testing.T) {
|
||||
initEnhancedDBForTest(t)
|
||||
|
||||
matcher := createMatcher("regex", nil, []string{`apache/[\d.]+`}, nil, "body", "", false)
|
||||
result := matchRegex(matcher, "nginx server running", "")
|
||||
if result {
|
||||
t.Error("不应匹配 apache 正则")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMatchRegex_ANDConditionAllMatch(t *testing.T) {
|
||||
initEnhancedDBForTest(t)
|
||||
|
||||
matcher := createMatcher("regex", nil, []string{`nginx`, `1\.18`}, nil, "body", "and", false)
|
||||
result := matchRegex(matcher, "nginx/1.18.0 server", "")
|
||||
if !result {
|
||||
t.Error("AND 条件下两个正则都匹配应返回 true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMatchRegex_ANDConditionPartialMatch(t *testing.T) {
|
||||
initEnhancedDBForTest(t)
|
||||
|
||||
matcher := createMatcher("regex", nil, []string{`nginx`, `apache`}, nil, "body", "and", false)
|
||||
result := matchRegex(matcher, "nginx server", "")
|
||||
if result {
|
||||
t.Error("AND 条件下只有一个匹配应返回 false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMatchRegex_ORConditionOneMatch(t *testing.T) {
|
||||
initEnhancedDBForTest(t)
|
||||
|
||||
matcher := createMatcher("regex", nil, []string{`nginx`, `apache`}, nil, "body", "or", false)
|
||||
result := matchRegex(matcher, "apache httpd", "")
|
||||
if !result {
|
||||
t.Error("OR 条件下至少一个匹配应返回 true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMatchRegex_CaseInsensitive(t *testing.T) {
|
||||
initEnhancedDBForTest(t)
|
||||
|
||||
matcher := createMatcher("regex", nil, []string{`NGINX`}, nil, "body", "", true)
|
||||
result := matchRegex(matcher, "nginx/1.18.0", "")
|
||||
if !result {
|
||||
t.Error("大小写不敏感模式下应匹配")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMatchRegex_InvalidPattern(t *testing.T) {
|
||||
initEnhancedDBForTest(t)
|
||||
|
||||
// 无效正则不应崩溃
|
||||
matcher := createMatcher("regex", nil, []string{`[invalid regex(`}, nil, "body", "", false)
|
||||
result := matchRegex(matcher, "test content", "")
|
||||
if result {
|
||||
t.Error("无效正则不应产生匹配")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMatchRegex_EmptyPatterns(t *testing.T) {
|
||||
initEnhancedDBForTest(t)
|
||||
|
||||
matcher := createMatcher("regex", nil, []string{}, nil, "body", "and", false)
|
||||
result := matchRegex(matcher, "nginx", "")
|
||||
// AND 条件且无 pattern:isAnd && len(Regex) > 0 为 false
|
||||
if result {
|
||||
t.Error("AND 条件下空 patterns 应返回 false")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
package WebScan
|
||||
|
||||
import (
|
||||
"crypto/md5" //nolint:gosec
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/shadow1ng/fscan/webscan/fingerprint"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// removeDuplicateElement 测试
|
||||
// =============================================================================
|
||||
|
||||
func TestRemoveDuplicateElement_Basic(t *testing.T) {
|
||||
input := []string{"nginx", "apache", "nginx", "iis", "apache"}
|
||||
result := removeDuplicateElement(input)
|
||||
|
||||
if len(result) != 3 {
|
||||
t.Errorf("期望3个唯一元素,实际 %d: %v", len(result), result)
|
||||
}
|
||||
|
||||
seen := make(map[string]int)
|
||||
for _, v := range result {
|
||||
seen[v]++
|
||||
if seen[v] > 1 {
|
||||
t.Errorf("元素 %q 出现了多次", v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveDuplicateElement_Empty(t *testing.T) {
|
||||
result := removeDuplicateElement([]string{})
|
||||
if len(result) != 0 {
|
||||
t.Errorf("空输入应返回空切片,实际 %d", len(result))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveDuplicateElement_NoDup(t *testing.T) {
|
||||
input := []string{"a", "b", "c"}
|
||||
result := removeDuplicateElement(input)
|
||||
if len(result) != 3 {
|
||||
t.Errorf("无重复时应保留全部元素,实际 %d", len(result))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveDuplicateElement_AllSame(t *testing.T) {
|
||||
input := []string{"dup", "dup", "dup", "dup"}
|
||||
result := removeDuplicateElement(input)
|
||||
if len(result) != 1 {
|
||||
t.Errorf("全部相同时应只保留1个,实际 %d", len(result))
|
||||
}
|
||||
if result[0] != "dup" {
|
||||
t.Errorf("保留的元素应为 'dup',实际 %q", result[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveDuplicateElement_PreservesOrder(t *testing.T) {
|
||||
input := []string{"c", "a", "b", "a", "c"}
|
||||
result := removeDuplicateElement(input)
|
||||
if len(result) != 3 {
|
||||
t.Fatalf("期望3个元素,实际 %d", len(result))
|
||||
}
|
||||
// 第一次出现的顺序应被保留
|
||||
if result[0] != "c" || result[1] != "a" || result[2] != "b" {
|
||||
t.Errorf("顺序不符合预期: %v", result)
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// matchByMd5 测试
|
||||
// =============================================================================
|
||||
|
||||
func TestMatchByMd5_KnownHash(t *testing.T) {
|
||||
// 从真实的 Md5Datas 取第一条:{"BIG-IP", "04d9541338e525258daf47cc844d59f3"}
|
||||
if len(fingerprint.Md5Datas) == 0 {
|
||||
t.Skip("Md5Datas 为空,跳过测试")
|
||||
}
|
||||
|
||||
entry := fingerprint.Md5Datas[0]
|
||||
|
||||
// 找到能产生这个 md5 的数据——直接暴力:构造一个有已知 md5 的 body
|
||||
// 实际上 md5 是 favicon 的 hash,这里测试找不到匹配的情况
|
||||
emptyResult := matchByMd5([]byte("no match content here"))
|
||||
if emptyResult != "" {
|
||||
t.Logf("意外匹配了 %q(不影响功能,跳过断言)", emptyResult)
|
||||
}
|
||||
|
||||
// 验证函数正确返回空字符串——主要检测无崩溃
|
||||
_ = entry
|
||||
}
|
||||
|
||||
func TestMatchByMd5_NoMatch(t *testing.T) {
|
||||
result := matchByMd5([]byte("definitely not matching any fingerprint 12345"))
|
||||
if result != "" {
|
||||
t.Errorf("不应匹配任何指纹,实际匹配了 %q", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMatchByMd5_Empty(t *testing.T) {
|
||||
// 空 body 的 md5 固定值 d41d8cd98f00b204e9800998ecf8427e
|
||||
// 检查是否在数据库中(不是,所以应返回空)
|
||||
result := matchByMd5([]byte{})
|
||||
// 不强断言结果,只验证不崩溃
|
||||
_ = result
|
||||
}
|
||||
|
||||
// 构造一个真实 md5 让 matchByMd5 命中
|
||||
func TestMatchByMd5_ActualMatch(t *testing.T) {
|
||||
if len(fingerprint.Md5Datas) == 0 {
|
||||
t.Skip("Md5Datas 为空")
|
||||
}
|
||||
|
||||
// 找一条已知 md5,反向构造:我们不能反推原始数据
|
||||
// 但可以直接测试 md5 计算逻辑:手动计算 body 的 md5 并与函数对比
|
||||
body := []byte("test content for md5 check")
|
||||
//nolint:gosec
|
||||
expected := fmt.Sprintf("%x", md5.Sum(body))
|
||||
|
||||
// matchByMd5 内部会对 body 计算 md5,然后在 Md5Datas 中查找
|
||||
// 因为这个 md5 不在 Md5Datas 中,应返回 ""
|
||||
result := matchByMd5(body)
|
||||
if result != "" {
|
||||
t.Logf("巧合命中: body_md5=%s matched=%q", expected, result)
|
||||
}
|
||||
// 主要验证逻辑路径可以走通
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// matchByRegex 测试
|
||||
// =============================================================================
|
||||
|
||||
func TestMatchByRegex_CodeType(t *testing.T) {
|
||||
// 宝塔指纹:Type="code",匹配 body
|
||||
data := CheckDatas{
|
||||
Body: []byte("app.bt.cn/static/app.png"),
|
||||
Headers: "",
|
||||
}
|
||||
result := matchByRegex(data)
|
||||
found := false
|
||||
for _, name := range result {
|
||||
if name == "宝塔" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("应匹配宝塔指纹,实际结果: %v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMatchByRegex_HeaderType(t *testing.T) {
|
||||
// CloudFlare 指纹:Type="headers",匹配 headers
|
||||
data := CheckDatas{
|
||||
Body: []byte(""),
|
||||
Headers: "CF-RAY: cloudflare-abc123",
|
||||
}
|
||||
result := matchByRegex(data)
|
||||
found := false
|
||||
for _, name := range result {
|
||||
if name == "CloudFlare" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("应匹配CloudFlare指纹,实际结果: %v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMatchByRegex_NoMatch(t *testing.T) {
|
||||
data := CheckDatas{
|
||||
Body: []byte("hello world nothing special"),
|
||||
Headers: "Content-Type: text/plain",
|
||||
}
|
||||
result := matchByRegex(data)
|
||||
// 普通内容不应匹配特征指纹
|
||||
// 不强断言数量,只验证不崩溃
|
||||
_ = result
|
||||
}
|
||||
|
||||
func TestMatchByRegex_EmptyData(t *testing.T) {
|
||||
data := CheckDatas{}
|
||||
result := matchByRegex(data)
|
||||
if result == nil {
|
||||
result = []string{}
|
||||
}
|
||||
// 空数据不崩溃即可
|
||||
_ = result
|
||||
}
|
||||
|
||||
func TestMatchByRegex_DeepInserve(t *testing.T) {
|
||||
// 深信服防火墙:body 中包含 "SANGFOR FW"
|
||||
data := CheckDatas{
|
||||
Body: []byte(`<html>SANGFOR FW product page</html>`),
|
||||
Headers: "",
|
||||
}
|
||||
result := matchByRegex(data)
|
||||
found := false
|
||||
for _, name := range result {
|
||||
if name == "深信服防火墙类产品" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("应匹配深信服防火墙指纹,实际结果: %v", result)
|
||||
}
|
||||
}
|
||||
+109
-1
@@ -1,6 +1,114 @@
|
||||
package lib
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// UnmarshalYAML 测试
|
||||
// =============================================================================
|
||||
|
||||
func TestStrMapUnmarshalYAML(t *testing.T) {
|
||||
t.Run("正常键值对", func(t *testing.T) {
|
||||
data := []byte("key1: val1\nkey2: val2\n")
|
||||
var m StrMap
|
||||
if err := yaml.Unmarshal(data, &m); err != nil {
|
||||
t.Fatalf("yaml.Unmarshal error = %v", err)
|
||||
}
|
||||
if len(m) != 2 {
|
||||
t.Fatalf("len = %d, want 2", len(m))
|
||||
}
|
||||
if m[0].Key != "key1" || m[0].Value != "val1" {
|
||||
t.Errorf("m[0] = %+v, want {key1 val1}", m[0])
|
||||
}
|
||||
if m[1].Key != "key2" || m[1].Value != "val2" {
|
||||
t.Errorf("m[1] = %+v, want {key2 val2}", m[1])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("单项", func(t *testing.T) {
|
||||
data := []byte("only: one\n")
|
||||
var m StrMap
|
||||
if err := yaml.Unmarshal(data, &m); err != nil {
|
||||
t.Fatalf("yaml.Unmarshal error = %v", err)
|
||||
}
|
||||
if len(m) != 1 || m[0].Key != "only" || m[0].Value != "one" {
|
||||
t.Fatalf("m = %+v", m)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("randomInt 值保留为字符串", func(t *testing.T) {
|
||||
data := []byte("port: randomInt(1000, 9000)\n")
|
||||
var m StrMap
|
||||
if err := yaml.Unmarshal(data, &m); err != nil {
|
||||
t.Fatalf("yaml.Unmarshal error = %v", err)
|
||||
}
|
||||
if len(m) != 1 || m[0].Value != "randomInt(1000, 9000)" {
|
||||
t.Fatalf("m = %+v", m)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestListMapUnmarshalYAML(t *testing.T) {
|
||||
t.Run("正常列表值", func(t *testing.T) {
|
||||
data := []byte("users:\n - admin\n - root\npasses:\n - 123\n - 456\n")
|
||||
var m ListMap
|
||||
if err := yaml.Unmarshal(data, &m); err != nil {
|
||||
t.Fatalf("yaml.Unmarshal error = %v", err)
|
||||
}
|
||||
if len(m) != 2 {
|
||||
t.Fatalf("len = %d, want 2", len(m))
|
||||
}
|
||||
if m[0].Key != "users" || len(m[0].Value) != 2 || m[0].Value[0] != "admin" || m[0].Value[1] != "root" {
|
||||
t.Errorf("m[0] = %+v", m[0])
|
||||
}
|
||||
if m[1].Key != "passes" || len(m[1].Value) != 2 || m[1].Value[0] != "123" || m[1].Value[1] != "456" {
|
||||
t.Errorf("m[1] = %+v", m[1])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("单个列表", func(t *testing.T) {
|
||||
data := []byte("cmd:\n - whoami\n")
|
||||
var m ListMap
|
||||
if err := yaml.Unmarshal(data, &m); err != nil {
|
||||
t.Fatalf("yaml.Unmarshal error = %v", err)
|
||||
}
|
||||
if len(m) != 1 || m[0].Key != "cmd" || m[0].Value[0] != "whoami" {
|
||||
t.Fatalf("m = %+v", m)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("数字值转字符串", func(t *testing.T) {
|
||||
data := []byte("ports:\n - 80\n - 443\n")
|
||||
var m ListMap
|
||||
if err := yaml.Unmarshal(data, &m); err != nil {
|
||||
t.Fatalf("yaml.Unmarshal error = %v", err)
|
||||
}
|
||||
if m[0].Value[0] != "80" || m[0].Value[1] != "443" {
|
||||
t.Errorf("数字未转为字符串: %+v", m[0].Value)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestStrMapUnmarshalYAML_InvalidValue(t *testing.T) {
|
||||
// value 是嵌套 map,不是字符串,应报错
|
||||
data := []byte("key:\n nested: val\n")
|
||||
var m StrMap
|
||||
if err := yaml.Unmarshal(data, &m); err == nil {
|
||||
t.Fatal("期望错误,实际 nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestListMapUnmarshalYAML_InvalidValue(t *testing.T) {
|
||||
// value 是普通字符串而非列表,应报错
|
||||
data := []byte("key: notalist\n")
|
||||
var m ListMap
|
||||
if err := yaml.Unmarshal(data, &m); err == nil {
|
||||
t.Fatal("期望错误,实际 nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeHTTPProxyURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/cel-go/common/types"
|
||||
)
|
||||
|
||||
func TestRegisterCryptoImplementations(t *testing.T) {
|
||||
overloads := registerCryptoImplementations()
|
||||
|
||||
// 建立 operator → index 映射
|
||||
idx := make(map[string]int, len(overloads))
|
||||
for i, o := range overloads {
|
||||
idx[o.Operator] = i
|
||||
}
|
||||
|
||||
t.Run("md5_string", func(t *testing.T) {
|
||||
i, ok := idx["md5_string"]
|
||||
if !ok {
|
||||
t.Fatal("overload md5_string not found")
|
||||
}
|
||||
unary := overloads[i].Unary
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input types.String
|
||||
want types.String
|
||||
wantErr bool
|
||||
}{
|
||||
{"hello", "hello", "5d41402abc4b2a76b9719d911017c592", false},
|
||||
{"empty", "", "d41d8cd98f00b204e9800998ecf8427e", false},
|
||||
{"abc", "abc", "900150983cd24fb0d6963f7d28e17f72", false},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := unary(tc.input)
|
||||
if types.IsError(result) {
|
||||
t.Fatalf("unexpected error: %v", result)
|
||||
}
|
||||
got, ok := result.(types.String)
|
||||
if !ok {
|
||||
t.Fatalf("expected types.String, got %T", result)
|
||||
}
|
||||
if got != tc.want {
|
||||
t.Errorf("md5(%q) = %q, want %q", tc.input, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("md5_string_wrong_type", func(t *testing.T) {
|
||||
i := idx["md5_string"]
|
||||
result := overloads[i].Unary(types.Int(42))
|
||||
if !types.IsError(result) {
|
||||
t.Errorf("expected error for non-String input, got %v", result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("shiro_key_valid", func(t *testing.T) {
|
||||
i, ok := idx["shiro_key"]
|
||||
if !ok {
|
||||
t.Fatal("overload shiro_key not found")
|
||||
}
|
||||
binary := overloads[i].Binary
|
||||
|
||||
// kPH+bIxk5D2deZiIxcaaaA== 是常见 shiro 默认 key
|
||||
result := binary(types.String("kPH+bIxk5D2deZiIxcaaaA=="), types.String("cbc"))
|
||||
if types.IsError(result) {
|
||||
t.Fatalf("unexpected error: %v", result)
|
||||
}
|
||||
got, ok := result.(types.String)
|
||||
if !ok {
|
||||
t.Fatalf("expected types.String, got %T", result)
|
||||
}
|
||||
if got == "" {
|
||||
t.Error("shiro_key returned empty string")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("shiro_key_invalid_base64", func(t *testing.T) {
|
||||
i := idx["shiro_key"]
|
||||
binary := overloads[i].Binary
|
||||
|
||||
// 无效 base64,GetShrioCookie 会返回 "",函数返回 NewErr
|
||||
result := binary(types.String("!!!not_valid_base64!!!"), types.String("cbc"))
|
||||
if !types.IsError(result) {
|
||||
t.Errorf("expected error for invalid base64 key, got %v", result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("shiro_key_wrong_key_type", func(t *testing.T) {
|
||||
i := idx["shiro_key"]
|
||||
result := overloads[i].Binary(types.Int(1), types.String("cbc"))
|
||||
if !types.IsError(result) {
|
||||
t.Error("expected error for non-String key")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("shiro_key_wrong_mode_type", func(t *testing.T) {
|
||||
i := idx["shiro_key"]
|
||||
result := overloads[i].Binary(types.String("kPH+bIxk5D2deZiIxcaaaA=="), types.Int(0))
|
||||
if !types.IsError(result) {
|
||||
t.Error("expected error for non-String mode")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/cel-go/common/types"
|
||||
)
|
||||
|
||||
func TestRegisterEncodingImplementations(t *testing.T) {
|
||||
overloads := registerEncodingImplementations()
|
||||
|
||||
idx := make(map[string]int, len(overloads))
|
||||
for i, o := range overloads {
|
||||
idx[o.Operator] = i
|
||||
}
|
||||
|
||||
t.Run("base64_string", func(t *testing.T) {
|
||||
unary := overloads[idx["base64_string"]].Unary
|
||||
|
||||
tests := []struct {
|
||||
input types.String
|
||||
want types.String
|
||||
}{
|
||||
{"hello", "aGVsbG8="},
|
||||
{"", ""},
|
||||
{"hello world", "aGVsbG8gd29ybGQ="},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
result := unary(tc.input)
|
||||
if types.IsError(result) {
|
||||
t.Fatalf("base64_string(%q): unexpected error %v", tc.input, result)
|
||||
}
|
||||
got, ok := result.(types.String)
|
||||
if !ok {
|
||||
t.Fatalf("expected types.String, got %T", result)
|
||||
}
|
||||
if got != tc.want {
|
||||
t.Errorf("base64_string(%q) = %q, want %q", tc.input, got, tc.want)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("base64_string_wrong_type", func(t *testing.T) {
|
||||
result := overloads[idx["base64_string"]].Unary(types.Int(1))
|
||||
if !types.IsError(result) {
|
||||
t.Error("expected error for non-String input")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("base64_bytes", func(t *testing.T) {
|
||||
unary := overloads[idx["base64_bytes"]].Unary
|
||||
|
||||
tests := []struct {
|
||||
input types.Bytes
|
||||
want types.String
|
||||
}{
|
||||
{types.Bytes([]byte("hello")), "aGVsbG8="},
|
||||
{types.Bytes([]byte{}), ""},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
result := unary(tc.input)
|
||||
if types.IsError(result) {
|
||||
t.Fatalf("base64_bytes: unexpected error %v", result)
|
||||
}
|
||||
got, ok := result.(types.String)
|
||||
if !ok {
|
||||
t.Fatalf("expected types.String, got %T", result)
|
||||
}
|
||||
if got != tc.want {
|
||||
t.Errorf("base64_bytes(%v) = %q, want %q", []byte(tc.input), got, tc.want)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("base64_bytes_wrong_type", func(t *testing.T) {
|
||||
result := overloads[idx["base64_bytes"]].Unary(types.String("hello"))
|
||||
if !types.IsError(result) {
|
||||
t.Error("expected error for non-Bytes input")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("base64Decode_string", func(t *testing.T) {
|
||||
unary := overloads[idx["base64Decode_string"]].Unary
|
||||
|
||||
tests := []struct {
|
||||
input types.String
|
||||
want types.String
|
||||
}{
|
||||
{"aGVsbG8=", "hello"},
|
||||
{"", ""},
|
||||
{"aGVsbG8gd29ybGQ=", "hello world"},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
result := unary(tc.input)
|
||||
if types.IsError(result) {
|
||||
t.Fatalf("base64Decode_string(%q): unexpected error %v", tc.input, result)
|
||||
}
|
||||
got, ok := result.(types.String)
|
||||
if !ok {
|
||||
t.Fatalf("expected types.String, got %T", result)
|
||||
}
|
||||
if got != tc.want {
|
||||
t.Errorf("base64Decode_string(%q) = %q, want %q", tc.input, got, tc.want)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("base64Decode_string_invalid", func(t *testing.T) {
|
||||
result := overloads[idx["base64Decode_string"]].Unary(types.String("!!!"))
|
||||
if !types.IsError(result) {
|
||||
t.Error("expected error for invalid base64 input")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("base64Decode_string_wrong_type", func(t *testing.T) {
|
||||
result := overloads[idx["base64Decode_string"]].Unary(types.Bool(true))
|
||||
if !types.IsError(result) {
|
||||
t.Error("expected error for non-String input")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("base64Decode_bytes", func(t *testing.T) {
|
||||
unary := overloads[idx["base64Decode_bytes"]].Unary
|
||||
|
||||
result := unary(types.Bytes([]byte("aGVsbG8=")))
|
||||
if types.IsError(result) {
|
||||
t.Fatalf("unexpected error: %v", result)
|
||||
}
|
||||
got, ok := result.(types.String)
|
||||
if !ok {
|
||||
t.Fatalf("expected types.String, got %T", result)
|
||||
}
|
||||
if got != "hello" {
|
||||
t.Errorf("base64Decode_bytes = %q, want %q", got, "hello")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("base64Decode_bytes_invalid", func(t *testing.T) {
|
||||
result := overloads[idx["base64Decode_bytes"]].Unary(types.Bytes([]byte("!!!")))
|
||||
if !types.IsError(result) {
|
||||
t.Error("expected error for invalid base64 bytes")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("urlencode_string", func(t *testing.T) {
|
||||
unary := overloads[idx["urlencode_string"]].Unary
|
||||
|
||||
// url.QueryEscape: 空格 → "+"
|
||||
tests := []struct {
|
||||
input types.String
|
||||
want types.String
|
||||
}{
|
||||
{"hello world", "hello+world"},
|
||||
{"a=1&b=2", "a%3D1%26b%3D2"},
|
||||
{"", ""},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
result := unary(tc.input)
|
||||
if types.IsError(result) {
|
||||
t.Fatalf("urlencode_string(%q): unexpected error %v", tc.input, result)
|
||||
}
|
||||
got, ok := result.(types.String)
|
||||
if !ok {
|
||||
t.Fatalf("expected types.String, got %T", result)
|
||||
}
|
||||
if got != tc.want {
|
||||
t.Errorf("urlencode_string(%q) = %q, want %q", tc.input, got, tc.want)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("urlencode_string_wrong_type", func(t *testing.T) {
|
||||
result := overloads[idx["urlencode_string"]].Unary(types.Int(0))
|
||||
if !types.IsError(result) {
|
||||
t.Error("expected error for non-String input")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("urldecode_string", func(t *testing.T) {
|
||||
unary := overloads[idx["urldecode_string"]].Unary
|
||||
|
||||
tests := []struct {
|
||||
input types.String
|
||||
want types.String
|
||||
}{
|
||||
{"hello%20world", "hello world"},
|
||||
{"hello+world", "hello world"},
|
||||
{"", ""},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
result := unary(tc.input)
|
||||
if types.IsError(result) {
|
||||
t.Fatalf("urldecode_string(%q): unexpected error %v", tc.input, result)
|
||||
}
|
||||
got, ok := result.(types.String)
|
||||
if !ok {
|
||||
t.Fatalf("expected types.String, got %T", result)
|
||||
}
|
||||
if got != tc.want {
|
||||
t.Errorf("urldecode_string(%q) = %q, want %q", tc.input, got, tc.want)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("urldecode_string_invalid", func(t *testing.T) {
|
||||
// % 后跟非法字符
|
||||
result := overloads[idx["urldecode_string"]].Unary(types.String("hello%ZZ"))
|
||||
if !types.IsError(result) {
|
||||
t.Error("expected error for invalid percent-encoding")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("urldecode_string_wrong_type", func(t *testing.T) {
|
||||
result := overloads[idx["urldecode_string"]].Unary(types.Bool(false))
|
||||
if !types.IsError(result) {
|
||||
t.Error("expected error for non-String input")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("hexdecode", func(t *testing.T) {
|
||||
unary := overloads[idx["hexdecode"]].Unary
|
||||
|
||||
tests := []struct {
|
||||
input types.String
|
||||
want []byte
|
||||
}{
|
||||
{"48656c6c6f", []byte("Hello")},
|
||||
{"", []byte{}},
|
||||
{"deadbeef", []byte{0xde, 0xad, 0xbe, 0xef}},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
result := unary(tc.input)
|
||||
if types.IsError(result) {
|
||||
t.Fatalf("hexdecode(%q): unexpected error %v", tc.input, result)
|
||||
}
|
||||
got, ok := result.(types.Bytes)
|
||||
if !ok {
|
||||
t.Fatalf("expected types.Bytes, got %T", result)
|
||||
}
|
||||
if string(got) != string(tc.want) {
|
||||
t.Errorf("hexdecode(%q) = %v, want %v", tc.input, []byte(got), tc.want)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("hexdecode_invalid", func(t *testing.T) {
|
||||
result := overloads[idx["hexdecode"]].Unary(types.String("zz"))
|
||||
if !types.IsError(result) {
|
||||
t.Error("expected error for invalid hex input")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("hexdecode_wrong_type", func(t *testing.T) {
|
||||
result := overloads[idx["hexdecode"]].Unary(types.Int(99))
|
||||
if !types.IsError(result) {
|
||||
t.Error("expected error for non-String input")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"unicode"
|
||||
|
||||
"github.com/google/cel-go/common/types"
|
||||
)
|
||||
|
||||
func TestRegisterMiscImplementations_TongdaDate(t *testing.T) {
|
||||
overloads := registerMiscImplementations()
|
||||
|
||||
idx := make(map[string]int, len(overloads))
|
||||
for i, o := range overloads {
|
||||
idx[o.Operator] = i
|
||||
}
|
||||
|
||||
i, ok := idx["tongda_date"]
|
||||
if !ok {
|
||||
t.Fatal("overload tongda_date not found")
|
||||
}
|
||||
fn := overloads[i].Function
|
||||
if fn == nil {
|
||||
t.Fatal("tongda_date Function field is nil")
|
||||
}
|
||||
|
||||
result := fn()
|
||||
|
||||
if types.IsError(result) {
|
||||
t.Fatalf("unexpected error: %v", result)
|
||||
}
|
||||
|
||||
got, ok := result.(types.String)
|
||||
if !ok {
|
||||
t.Fatalf("expected types.String, got %T", result)
|
||||
}
|
||||
|
||||
s := string(got)
|
||||
|
||||
t.Run("length_is_4", func(t *testing.T) {
|
||||
if len(s) != 4 {
|
||||
t.Errorf("tongda_date returned %q, want 4-char string", s)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("all_digits", func(t *testing.T) {
|
||||
for _, r := range s {
|
||||
if !unicode.IsDigit(r) {
|
||||
t.Errorf("tongda_date returned %q, contains non-digit char %q", s, r)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"unicode"
|
||||
|
||||
"github.com/google/cel-go/common/types"
|
||||
)
|
||||
|
||||
func TestRegisterRandomImplementations(t *testing.T) {
|
||||
overloads := registerRandomImplementations()
|
||||
|
||||
idx := make(map[string]int, len(overloads))
|
||||
for i, o := range overloads {
|
||||
idx[o.Operator] = i
|
||||
}
|
||||
|
||||
t.Run("randomInt_int_int", func(t *testing.T) {
|
||||
i, ok := idx["randomInt_int_int"]
|
||||
if !ok {
|
||||
t.Fatal("overload randomInt_int_int not found")
|
||||
}
|
||||
binary := overloads[i].Binary
|
||||
|
||||
t.Run("returns_Int_type", func(t *testing.T) {
|
||||
result := binary(types.Int(0), types.Int(100))
|
||||
if types.IsError(result) {
|
||||
t.Fatalf("unexpected error: %v", result)
|
||||
}
|
||||
if _, ok := result.(types.Int); !ok {
|
||||
t.Errorf("expected types.Int, got %T", result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("value_in_range", func(t *testing.T) {
|
||||
min, max := types.Int(10), types.Int(20)
|
||||
for range 50 {
|
||||
result := binary(min, max)
|
||||
if types.IsError(result) {
|
||||
t.Fatalf("unexpected error: %v", result)
|
||||
}
|
||||
v := int64(result.(types.Int))
|
||||
if v < 10 || v >= 20 {
|
||||
t.Errorf("randomInt(10,20) = %d, out of [10,20)", v)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("max_le_min_returns_error", func(t *testing.T) {
|
||||
result := binary(types.Int(5), types.Int(5))
|
||||
if !types.IsError(result) {
|
||||
t.Errorf("expected error when max == min, got %v", result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("wrong_lhs_type", func(t *testing.T) {
|
||||
result := binary(types.String("x"), types.Int(10))
|
||||
if !types.IsError(result) {
|
||||
t.Error("expected error for non-Int lhs")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("wrong_rhs_type", func(t *testing.T) {
|
||||
result := binary(types.Int(0), types.String("x"))
|
||||
if !types.IsError(result) {
|
||||
t.Error("expected error for non-Int rhs")
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("randomLowercase_int", func(t *testing.T) {
|
||||
i, ok := idx["randomLowercase_int"]
|
||||
if !ok {
|
||||
t.Fatal("overload randomLowercase_int not found")
|
||||
}
|
||||
unary := overloads[i].Unary
|
||||
|
||||
t.Run("returns_String_type", func(t *testing.T) {
|
||||
result := unary(types.Int(8))
|
||||
if types.IsError(result) {
|
||||
t.Fatalf("unexpected error: %v", result)
|
||||
}
|
||||
if _, ok := result.(types.String); !ok {
|
||||
t.Errorf("expected types.String, got %T", result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("correct_length", func(t *testing.T) {
|
||||
for _, n := range []int{0, 1, 8, 16} {
|
||||
result := unary(types.Int(n))
|
||||
if types.IsError(result) {
|
||||
t.Fatalf("unexpected error for n=%d: %v", n, result)
|
||||
}
|
||||
got := string(result.(types.String))
|
||||
if len(got) != n {
|
||||
t.Errorf("randomLowercase(%d) returned length %d", n, len(got))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("all_lowercase", func(t *testing.T) {
|
||||
result := unary(types.Int(32))
|
||||
got := string(result.(types.String))
|
||||
for _, r := range got {
|
||||
if !unicode.IsLower(r) {
|
||||
t.Errorf("randomLowercase returned non-lowercase char %q in %q", r, got)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid_length_negative", func(t *testing.T) {
|
||||
result := unary(types.Int(-1))
|
||||
if !types.IsError(result) {
|
||||
t.Error("expected error for negative length")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid_length_too_large", func(t *testing.T) {
|
||||
result := unary(types.Int(maxRandomStringLength + 1))
|
||||
if !types.IsError(result) {
|
||||
t.Error("expected error for length > maxRandomStringLength")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("wrong_type", func(t *testing.T) {
|
||||
result := unary(types.String("x"))
|
||||
if !types.IsError(result) {
|
||||
t.Error("expected error for non-Int input")
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("randomUppercase_int", func(t *testing.T) {
|
||||
i, ok := idx["randomUppercase_int"]
|
||||
if !ok {
|
||||
t.Fatal("overload randomUppercase_int not found")
|
||||
}
|
||||
unary := overloads[i].Unary
|
||||
|
||||
t.Run("returns_String_type", func(t *testing.T) {
|
||||
result := unary(types.Int(8))
|
||||
if types.IsError(result) {
|
||||
t.Fatalf("unexpected error: %v", result)
|
||||
}
|
||||
if _, ok := result.(types.String); !ok {
|
||||
t.Errorf("expected types.String, got %T", result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("correct_length", func(t *testing.T) {
|
||||
for _, n := range []int{0, 1, 8, 16} {
|
||||
result := unary(types.Int(n))
|
||||
if types.IsError(result) {
|
||||
t.Fatalf("unexpected error for n=%d: %v", n, result)
|
||||
}
|
||||
got := string(result.(types.String))
|
||||
if len(got) != n {
|
||||
t.Errorf("randomUppercase(%d) returned length %d", n, len(got))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("all_uppercase", func(t *testing.T) {
|
||||
result := unary(types.Int(32))
|
||||
got := string(result.(types.String))
|
||||
for _, r := range got {
|
||||
if !unicode.IsUpper(r) {
|
||||
t.Errorf("randomUppercase returned non-uppercase char %q in %q", r, got)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("wrong_type", func(t *testing.T) {
|
||||
result := unary(types.String("x"))
|
||||
if !types.IsError(result) {
|
||||
t.Error("expected error for non-Int input")
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("randomString_int", func(t *testing.T) {
|
||||
i, ok := idx["randomString_int"]
|
||||
if !ok {
|
||||
t.Fatal("overload randomString_int not found")
|
||||
}
|
||||
unary := overloads[i].Unary
|
||||
|
||||
t.Run("returns_String_type", func(t *testing.T) {
|
||||
result := unary(types.Int(8))
|
||||
if types.IsError(result) {
|
||||
t.Fatalf("unexpected error: %v", result)
|
||||
}
|
||||
if _, ok := result.(types.String); !ok {
|
||||
t.Errorf("expected types.String, got %T", result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("correct_length", func(t *testing.T) {
|
||||
for _, n := range []int{0, 1, 8, 16} {
|
||||
result := unary(types.Int(n))
|
||||
if types.IsError(result) {
|
||||
t.Fatalf("unexpected error for n=%d: %v", n, result)
|
||||
}
|
||||
got := string(result.(types.String))
|
||||
if len(got) != n {
|
||||
t.Errorf("randomString(%d) returned length %d", n, len(got))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("wrong_type", func(t *testing.T) {
|
||||
result := unary(types.String("x"))
|
||||
if !types.IsError(result) {
|
||||
t.Error("expected error for non-Int input")
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRandomIntSpan(t *testing.T) {
|
||||
t.Run("normal_range", func(t *testing.T) {
|
||||
span, err := randomIntSpan(10, 20)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if span != 10 {
|
||||
t.Errorf("randomIntSpan(10,20) = %d, want 10", span)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("min_zero", func(t *testing.T) {
|
||||
span, err := randomIntSpan(0, 100)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if span != 100 {
|
||||
t.Errorf("randomIntSpan(0,100) = %d, want 100", span)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("negative_min", func(t *testing.T) {
|
||||
span, err := randomIntSpan(-5, 5)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if span != 10 {
|
||||
t.Errorf("randomIntSpan(-5,5) = %d, want 10", span)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("max_eq_min_returns_error", func(t *testing.T) {
|
||||
_, err := randomIntSpan(7, 7)
|
||||
if err == nil {
|
||||
t.Error("expected error when max == min")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("max_lt_min_returns_error", func(t *testing.T) {
|
||||
_, err := randomIntSpan(10, 5)
|
||||
if err == nil {
|
||||
t.Error("expected error when max < min")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,337 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/cel-go/common/types"
|
||||
"github.com/google/cel-go/common/types/ref"
|
||||
)
|
||||
|
||||
func TestRegisterStringImplementations(t *testing.T) {
|
||||
overloads := registerStringImplementations()
|
||||
|
||||
idx := make(map[string]int, len(overloads))
|
||||
for i, o := range overloads {
|
||||
idx[o.Operator] = i
|
||||
}
|
||||
|
||||
t.Run("bytes_bcontains_bytes", func(t *testing.T) {
|
||||
binary := overloads[idx["bytes_bcontains_bytes"]].Binary
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
lhs types.Bytes
|
||||
rhs types.Bytes
|
||||
want types.Bool
|
||||
}{
|
||||
{"contains", types.Bytes([]byte("hello world")), types.Bytes([]byte("world")), true},
|
||||
{"not_contains", types.Bytes([]byte("hello world")), types.Bytes([]byte("xyz")), false},
|
||||
{"empty_needle", types.Bytes([]byte("hello")), types.Bytes([]byte{}), true},
|
||||
{"both_empty", types.Bytes([]byte{}), types.Bytes([]byte{}), true},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := binary(tc.lhs, tc.rhs)
|
||||
if types.IsError(result) {
|
||||
t.Fatalf("unexpected error: %v", result)
|
||||
}
|
||||
got, ok := result.(types.Bool)
|
||||
if !ok {
|
||||
t.Fatalf("expected types.Bool, got %T", result)
|
||||
}
|
||||
if got != tc.want {
|
||||
t.Errorf("bcontains = %v, want %v", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("bytes_bcontains_bytes_wrong_lhs", func(t *testing.T) {
|
||||
result := overloads[idx["bytes_bcontains_bytes"]].Binary(types.String("hello"), types.Bytes([]byte("x")))
|
||||
if !types.IsError(result) {
|
||||
t.Error("expected error for non-Bytes lhs")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("bytes_bcontains_bytes_wrong_rhs", func(t *testing.T) {
|
||||
result := overloads[idx["bytes_bcontains_bytes"]].Binary(types.Bytes([]byte("hello")), types.String("x"))
|
||||
if !types.IsError(result) {
|
||||
t.Error("expected error for non-Bytes rhs")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("string_bmatches_bytes", func(t *testing.T) {
|
||||
binary := overloads[idx["string_bmatches_bytes"]].Binary
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
pattern types.String
|
||||
input types.Bytes
|
||||
want types.Bool
|
||||
}{
|
||||
{"digits_match", `\d+`, types.Bytes([]byte("abc123")), true},
|
||||
{"digits_no_match", `\d+`, types.Bytes([]byte("abc")), false},
|
||||
{"any", `.*`, types.Bytes([]byte("hello")), true},
|
||||
{"empty_pattern", ``, types.Bytes([]byte("hello")), true},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := binary(tc.pattern, tc.input)
|
||||
if types.IsError(result) {
|
||||
t.Fatalf("unexpected error: %v", result)
|
||||
}
|
||||
got, ok := result.(types.Bool)
|
||||
if !ok {
|
||||
t.Fatalf("expected types.Bool, got %T", result)
|
||||
}
|
||||
if got != tc.want {
|
||||
t.Errorf("bmatches(%q, %q) = %v, want %v", tc.pattern, tc.input, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("string_bmatches_bytes_invalid_regex", func(t *testing.T) {
|
||||
result := overloads[idx["string_bmatches_bytes"]].Binary(types.String(`[invalid`), types.Bytes([]byte("hello")))
|
||||
if !types.IsError(result) {
|
||||
t.Error("expected error for invalid regex pattern")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("string_bmatches_bytes_wrong_lhs", func(t *testing.T) {
|
||||
result := overloads[idx["string_bmatches_bytes"]].Binary(types.Int(0), types.Bytes([]byte("hello")))
|
||||
if !types.IsError(result) {
|
||||
t.Error("expected error for non-String lhs")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("string_bmatches_bytes_wrong_rhs", func(t *testing.T) {
|
||||
result := overloads[idx["string_bmatches_bytes"]].Binary(types.String(`\d+`), types.String("123"))
|
||||
if !types.IsError(result) {
|
||||
t.Error("expected error for non-Bytes rhs")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("icontains_string", func(t *testing.T) {
|
||||
binary := overloads[idx["icontains_string"]].Binary
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
lhs types.String
|
||||
rhs types.String
|
||||
want types.Bool
|
||||
}{
|
||||
{"case_insensitive_match", "Hello World", "hello", true},
|
||||
{"exact_match", "Hello World", "Hello", true},
|
||||
{"upper_needle", "hello world", "WORLD", true},
|
||||
{"not_contains", "hello world", "xyz", false},
|
||||
{"empty_needle", "hello", "", true},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := binary(tc.lhs, tc.rhs)
|
||||
if types.IsError(result) {
|
||||
t.Fatalf("unexpected error: %v", result)
|
||||
}
|
||||
got, ok := result.(types.Bool)
|
||||
if !ok {
|
||||
t.Fatalf("expected types.Bool, got %T", result)
|
||||
}
|
||||
if got != tc.want {
|
||||
t.Errorf("icontains(%q, %q) = %v, want %v", tc.lhs, tc.rhs, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("icontains_string_wrong_lhs", func(t *testing.T) {
|
||||
result := overloads[idx["icontains_string"]].Binary(types.Bool(true), types.String("x"))
|
||||
if !types.IsError(result) {
|
||||
t.Error("expected error for non-String lhs")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("icontains_string_wrong_rhs", func(t *testing.T) {
|
||||
result := overloads[idx["icontains_string"]].Binary(types.String("hello"), types.Int(1))
|
||||
if !types.IsError(result) {
|
||||
t.Error("expected error for non-String rhs")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("substr_string_int_int", func(t *testing.T) {
|
||||
fn := overloads[idx["substr_string_int_int"]].Function
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
str types.String
|
||||
start types.Int
|
||||
length types.Int
|
||||
want types.String
|
||||
}{
|
||||
{"basic", "hello world", 0, 5, "hello"},
|
||||
{"middle", "hello world", 6, 5, "world"},
|
||||
{"single_char", "hello", 1, 1, "e"},
|
||||
{"full", "hello", 0, 5, "hello"},
|
||||
{"zero_length", "hello", 2, 0, ""},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := fn(tc.str, tc.start, tc.length)
|
||||
if types.IsError(result) {
|
||||
t.Fatalf("substr(%q, %d, %d): unexpected error %v", tc.str, tc.start, tc.length, result)
|
||||
}
|
||||
got, ok := result.(types.String)
|
||||
if !ok {
|
||||
t.Fatalf("expected types.String, got %T", result)
|
||||
}
|
||||
if got != tc.want {
|
||||
t.Errorf("substr(%q, %d, %d) = %q, want %q", tc.str, tc.start, tc.length, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("substr_out_of_bounds", func(t *testing.T) {
|
||||
fn := overloads[idx["substr_string_int_int"]].Function
|
||||
|
||||
oob := []struct {
|
||||
name string
|
||||
str types.String
|
||||
start types.Int
|
||||
length types.Int
|
||||
}{
|
||||
{"negative_start", "hello", -1, 2},
|
||||
{"negative_length", "hello", 0, -1},
|
||||
{"start_too_large", "hello", 10, 1},
|
||||
{"length_overflow", "hello", 3, 10},
|
||||
}
|
||||
for _, tc := range oob {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := fn(tc.str, tc.start, tc.length)
|
||||
if !types.IsError(result) {
|
||||
t.Errorf("expected error for substr(%q, %d, %d), got %v", tc.str, tc.start, tc.length, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("substr_wrong_arg_count", func(t *testing.T) {
|
||||
fn := overloads[idx["substr_string_int_int"]].Function
|
||||
result := fn(types.String("hello"), types.Int(0))
|
||||
if !types.IsError(result) {
|
||||
t.Error("expected error for wrong argument count")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("substr_wrong_types", func(t *testing.T) {
|
||||
fn := overloads[idx["substr_string_int_int"]].Function
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
args []ref.Val
|
||||
}{
|
||||
{"wrong_str", []ref.Val{types.Int(0), types.Int(0), types.Int(1)}},
|
||||
{"wrong_start", []ref.Val{types.String("hello"), types.String("x"), types.Int(1)}},
|
||||
{"wrong_length", []ref.Val{types.String("hello"), types.Int(0), types.String("x")}},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := fn(tc.args...)
|
||||
if !types.IsError(result) {
|
||||
t.Errorf("expected error, got %v", result)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("startsWith_bytes", func(t *testing.T) {
|
||||
binary := overloads[idx["startsWith_bytes"]].Binary
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
lhs types.Bytes
|
||||
rhs types.Bytes
|
||||
want types.Bool
|
||||
}{
|
||||
{"match", types.Bytes([]byte("hello world")), types.Bytes([]byte("hello")), true},
|
||||
{"no_match", types.Bytes([]byte("hello world")), types.Bytes([]byte("world")), false},
|
||||
{"empty_prefix", types.Bytes([]byte("hello")), types.Bytes([]byte{}), true},
|
||||
{"exact", types.Bytes([]byte("hello")), types.Bytes([]byte("hello")), true},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := binary(tc.lhs, tc.rhs)
|
||||
if types.IsError(result) {
|
||||
t.Fatalf("unexpected error: %v", result)
|
||||
}
|
||||
got, ok := result.(types.Bool)
|
||||
if !ok {
|
||||
t.Fatalf("expected types.Bool, got %T", result)
|
||||
}
|
||||
if got != tc.want {
|
||||
t.Errorf("startsWith_bytes = %v, want %v", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("startsWith_bytes_wrong_lhs", func(t *testing.T) {
|
||||
result := overloads[idx["startsWith_bytes"]].Binary(types.String("hello"), types.Bytes([]byte("h")))
|
||||
if !types.IsError(result) {
|
||||
t.Error("expected error for non-Bytes lhs")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("startsWith_bytes_wrong_rhs", func(t *testing.T) {
|
||||
result := overloads[idx["startsWith_bytes"]].Binary(types.Bytes([]byte("hello")), types.String("h"))
|
||||
if !types.IsError(result) {
|
||||
t.Error("expected error for non-Bytes rhs")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("startsWith_string", func(t *testing.T) {
|
||||
binary := overloads[idx["startsWith_string"]].Binary
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
lhs types.String
|
||||
rhs types.String
|
||||
want types.Bool
|
||||
}{
|
||||
{"case_insensitive_match", "Hello World", "hello", true},
|
||||
{"upper_prefix", "hello world", "HELLO", true},
|
||||
{"no_match", "hello world", "world", false},
|
||||
{"empty_prefix", "hello", "", true},
|
||||
{"exact", "Hello", "Hello", true},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := binary(tc.lhs, tc.rhs)
|
||||
if types.IsError(result) {
|
||||
t.Fatalf("unexpected error: %v", result)
|
||||
}
|
||||
got, ok := result.(types.Bool)
|
||||
if !ok {
|
||||
t.Fatalf("expected types.Bool, got %T", result)
|
||||
}
|
||||
if got != tc.want {
|
||||
t.Errorf("startsWith_string(%q, %q) = %v, want %v", tc.lhs, tc.rhs, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("startsWith_string_wrong_lhs", func(t *testing.T) {
|
||||
result := overloads[idx["startsWith_string"]].Binary(types.Int(0), types.String("h"))
|
||||
if !types.IsError(result) {
|
||||
t.Error("expected error for non-String lhs")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("startsWith_string_wrong_rhs", func(t *testing.T) {
|
||||
result := overloads[idx["startsWith_string"]].Binary(types.String("hello"), types.Bool(true))
|
||||
if !types.IsError(result) {
|
||||
t.Error("expected error for non-String rhs")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1342,3 +1342,54 @@ func TestRandomStrRejectsNegativeLength(t *testing.T) {
|
||||
t.Fatalf("RandomStr negative length = %q, want empty", got)
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MakeVarDecl 测试
|
||||
// =============================================================================
|
||||
|
||||
func TestMakeVarDecl(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
key string
|
||||
value string
|
||||
wantIdent string // 期望 Decl.Name
|
||||
wantKind string // "int" / "string" / "object"
|
||||
}{
|
||||
{"randomInt 前缀 -> Int", "myrand", "randomInt(1,100)", "myrand", "int"},
|
||||
{"newReverse 前缀 -> Object", "myrev", "newReverse()", "myrev", "object"},
|
||||
{"普通字符串 -> String", "myvar", "somevalue", "myvar", "string"},
|
||||
{"空值 -> String", "empty", "", "empty", "string"},
|
||||
{"randomIntExtra -> Int", "n", "randomInt(0, 65535)", "n", "int"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
decl := MakeVarDecl(tt.key, tt.value)
|
||||
if decl == nil {
|
||||
t.Fatal("MakeVarDecl() returned nil")
|
||||
}
|
||||
if decl.Name != tt.wantIdent {
|
||||
t.Errorf("Decl.Name = %q, want %q", decl.Name, tt.wantIdent)
|
||||
}
|
||||
// 通过 Type 字段判断类型种类
|
||||
tp := decl.GetIdent().GetType()
|
||||
if tp == nil {
|
||||
t.Fatal("Decl.GetIdent().GetType() == nil")
|
||||
}
|
||||
switch tt.wantKind {
|
||||
case "int":
|
||||
if tp.GetPrimitive().String() != "INT64" {
|
||||
t.Errorf("type = %v, want INT64", tp)
|
||||
}
|
||||
case "string":
|
||||
if tp.GetPrimitive().String() != "STRING" {
|
||||
t.Errorf("type = %v, want STRING", tp)
|
||||
}
|
||||
case "object":
|
||||
if tp.GetMessageType() == "" {
|
||||
t.Errorf("type = %v, want MessageType", tp)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,11 @@ package lib
|
||||
|
||||
import (
|
||||
"crypto/md5" //nolint:gosec // G501: MD5用于POC规则去重,非加密用途
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand" //nolint:gosec // G404: math/rand用于生成测试数据,非加密用途
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -236,6 +238,9 @@ func executeRules(oReq *http.Request, p *Poc, variableMap map[string]interface{}
|
||||
resp, err := DoRequest(newRequest, rule.FollowRedirects, session)
|
||||
newRequest = nil
|
||||
if err != nil {
|
||||
if isTransportError(err) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
@@ -793,6 +798,9 @@ func clustersend(oReq *http.Request, variableMap map[string]interface{}, req *Re
|
||||
// 发送请求
|
||||
resp, err := DoRequest(newRequest, rule.FollowRedirects, session)
|
||||
if err != nil {
|
||||
if isTransportError(err) {
|
||||
return false, nil
|
||||
}
|
||||
return false, fmt.Errorf("%s: %w", i18n.GetText("webscan_request_send_error"), err)
|
||||
}
|
||||
|
||||
@@ -936,3 +944,20 @@ func GetHeader(header map[string]string) string {
|
||||
builder.WriteString("\r\n")
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
func isTransportError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
var netErr *net.OpError
|
||||
if errors.As(err, &netErr) {
|
||||
return true
|
||||
}
|
||||
s := err.Error()
|
||||
return strings.Contains(s, "malformed HTTP") ||
|
||||
strings.Contains(s, "transport connection broken") ||
|
||||
strings.Contains(s, "connection reset") ||
|
||||
strings.Contains(s, "connection refused") ||
|
||||
strings.Contains(s, "i/o timeout") ||
|
||||
strings.Contains(s, "EOF")
|
||||
}
|
||||
|
||||
@@ -517,6 +517,74 @@ func TestPocExecutorPureHelpers(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// isPlainLiteral 测试
|
||||
// =============================================================================
|
||||
|
||||
func TestIsPlainLiteral_EmptyString(t *testing.T) {
|
||||
if isPlainLiteral("", nil) {
|
||||
t.Error("空字符串不是字面量")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsPlainLiteral_PlainWord(t *testing.T) {
|
||||
if !isPlainLiteral("database", nil) {
|
||||
t.Error("纯单词 'database' 应视为字面量")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsPlainLiteral_WithParens(t *testing.T) {
|
||||
if isPlainLiteral("func()", nil) {
|
||||
t.Error("含括号的表达式不是字面量")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsPlainLiteral_WithOperator(t *testing.T) {
|
||||
for _, expr := range []string{"a+b", "a*b", "a==b", "a!=b", "a<b", "a>b", "a&&b", "a||b"} {
|
||||
if isPlainLiteral(expr, nil) {
|
||||
t.Errorf("含运算符的表达式 %q 不是字面量", expr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsPlainLiteral_WithQuotes(t *testing.T) {
|
||||
if isPlainLiteral(`"hello"`, nil) {
|
||||
t.Error("含引号的表达式不是字面量")
|
||||
}
|
||||
if isPlainLiteral("'hello'", nil) {
|
||||
t.Error("含单引号的表达式不是字面量")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsPlainLiteral_VariableRef(t *testing.T) {
|
||||
// 如果 expr 是已声明变量的名字,应走 CEL 求值
|
||||
varMap := map[string]interface{}{"token": "abc123"}
|
||||
if isPlainLiteral("token", varMap) {
|
||||
t.Error("已声明变量不应被视为字面量")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsPlainLiteral_UndeclaredVariable(t *testing.T) {
|
||||
varMap := map[string]interface{}{"token": "abc123"}
|
||||
// 未声明的变量名且无特殊字符 -> 字面量
|
||||
if !isPlainLiteral("sql", varMap) {
|
||||
t.Error("未声明的纯单词 'sql' 应视为字面量")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsPlainLiteral_WithBracket(t *testing.T) {
|
||||
if isPlainLiteral("arr[0]", nil) {
|
||||
t.Error("含方括号的表达式不是字面量")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsPlainLiteral_PathLike(t *testing.T) {
|
||||
// 路径中可能含 /,但 / 不在排除字符中,视为字面量
|
||||
if !isPlainLiteral("admin", nil) {
|
||||
t.Error("纯字母字符串应为字面量")
|
||||
}
|
||||
}
|
||||
|
||||
func stringMatrixEqual(a, b [][]string) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
@@ -533,3 +601,267 @@ func stringMatrixEqual(a, b [][]string) bool {
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// buildVulnDetails 测试
|
||||
// =============================================================================
|
||||
|
||||
func TestBuildVulnDetails(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pocDef *Poc
|
||||
vulName string
|
||||
params StrMap
|
||||
wantKeys []string
|
||||
wantNoKeys []string
|
||||
wantVulnType string
|
||||
wantVulnName string
|
||||
wantParamVal string
|
||||
wantParamKey string
|
||||
}{
|
||||
{
|
||||
name: "最小Poc只有Name",
|
||||
pocDef: &Poc{Name: "poc-yaml-test"},
|
||||
vulName: "poc-yaml-test",
|
||||
params: nil,
|
||||
wantKeys: []string{"vulnerability_type", "vulnerability_name"},
|
||||
wantNoKeys: []string{"author", "references", "description", "parameters"},
|
||||
wantVulnType: "poc-yaml-test",
|
||||
wantVulnName: "poc-yaml-test",
|
||||
},
|
||||
{
|
||||
name: "完整Poc含Author+Links+Description",
|
||||
pocDef: &Poc{
|
||||
Name: "poc-yaml-full",
|
||||
Detail: Detail{
|
||||
Author: "kei",
|
||||
Links: []string{"https://example.com"},
|
||||
Description: "test vuln",
|
||||
},
|
||||
},
|
||||
vulName: "Full Vuln",
|
||||
params: nil,
|
||||
wantKeys: []string{"vulnerability_type", "vulnerability_name", "author", "references", "description"},
|
||||
wantNoKeys: []string{"parameters"},
|
||||
wantVulnType: "poc-yaml-full",
|
||||
wantVulnName: "Full Vuln",
|
||||
},
|
||||
{
|
||||
name: "有params则details含parameters字段",
|
||||
pocDef: &Poc{Name: "poc-yaml-params"},
|
||||
vulName: "Params Vuln",
|
||||
params: StrMap{
|
||||
{Key: "user", Value: "admin"},
|
||||
{Key: "pass", Value: "123456"},
|
||||
},
|
||||
wantKeys: []string{"vulnerability_type", "vulnerability_name", "parameters"},
|
||||
wantNoKeys: []string{"author"},
|
||||
wantParamKey: "user",
|
||||
wantParamVal: "admin",
|
||||
},
|
||||
{
|
||||
name: "空params不含parameters字段",
|
||||
pocDef: &Poc{Name: "poc-yaml-empty-params"},
|
||||
vulName: "Empty Params",
|
||||
params: StrMap{},
|
||||
wantKeys: []string{"vulnerability_type", "vulnerability_name"},
|
||||
wantNoKeys: []string{"parameters"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
details := buildVulnDetails(tt.pocDef, tt.vulName, tt.params)
|
||||
|
||||
for _, k := range tt.wantKeys {
|
||||
if _, ok := details[k]; !ok {
|
||||
t.Errorf("details 缺少字段 %q", k)
|
||||
}
|
||||
}
|
||||
for _, k := range tt.wantNoKeys {
|
||||
if _, ok := details[k]; ok {
|
||||
t.Errorf("details 不应含字段 %q", k)
|
||||
}
|
||||
}
|
||||
if tt.wantVulnType != "" {
|
||||
if got, _ := details["vulnerability_type"].(string); got != tt.wantVulnType {
|
||||
t.Errorf("vulnerability_type = %q, want %q", got, tt.wantVulnType)
|
||||
}
|
||||
}
|
||||
if tt.wantVulnName != "" {
|
||||
if got, _ := details["vulnerability_name"].(string); got != tt.wantVulnName {
|
||||
t.Errorf("vulnerability_name = %q, want %q", got, tt.wantVulnName)
|
||||
}
|
||||
}
|
||||
if tt.wantParamKey != "" {
|
||||
pm, ok := details["parameters"].(map[string]string)
|
||||
if !ok {
|
||||
t.Fatalf("parameters 类型错误,实际 %T", details["parameters"])
|
||||
}
|
||||
if got := pm[tt.wantParamKey]; got != tt.wantParamVal {
|
||||
t.Errorf("parameters[%q] = %q, want %q", tt.wantParamKey, got, tt.wantParamVal)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// buildVulnLogMsg 测试
|
||||
// =============================================================================
|
||||
|
||||
func TestBuildVulnLogMsg(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
targetURL string
|
||||
pocDef *Poc
|
||||
vulName string
|
||||
params StrMap
|
||||
}{
|
||||
{
|
||||
name: "backup-file名称走特殊模板",
|
||||
targetURL: "http://example.com",
|
||||
pocDef: &Poc{Name: "poc-yaml-backup-file"},
|
||||
vulName: "poc-yaml-backup-file",
|
||||
params: nil,
|
||||
},
|
||||
{
|
||||
name: "sql-file名称走特殊模板",
|
||||
targetURL: "http://example.com",
|
||||
pocDef: &Poc{Name: "poc-yaml-sql-file"},
|
||||
vulName: "poc-yaml-sql-file",
|
||||
params: nil,
|
||||
},
|
||||
{
|
||||
name: "有params走params模板",
|
||||
targetURL: "http://example.com",
|
||||
pocDef: &Poc{Name: "poc-yaml-rce"},
|
||||
vulName: "RCE",
|
||||
params: StrMap{{Key: "cmd", Value: "id"}},
|
||||
},
|
||||
{
|
||||
name: "无params走detail_header模板",
|
||||
targetURL: "http://example.com",
|
||||
pocDef: &Poc{
|
||||
Name: "poc-yaml-sqli",
|
||||
Detail: Detail{
|
||||
Author: "kei",
|
||||
Links: []string{"https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-0001"},
|
||||
Description: "SQL injection",
|
||||
},
|
||||
},
|
||||
vulName: "SQLi",
|
||||
params: nil,
|
||||
},
|
||||
{
|
||||
name: "无params无detail只走header",
|
||||
targetURL: "http://example.com",
|
||||
pocDef: &Poc{Name: "poc-yaml-generic"},
|
||||
vulName: "Generic",
|
||||
params: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
msg := buildVulnLogMsg(tt.targetURL, tt.pocDef, tt.vulName, tt.params)
|
||||
if msg == "" {
|
||||
t.Errorf("buildVulnLogMsg() 返回空字符串")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// collectVarDeclarations 测试
|
||||
// =============================================================================
|
||||
|
||||
func TestCollectVarDeclarations(t *testing.T) {
|
||||
t.Run("空 POC 返回空切片", func(t *testing.T) {
|
||||
p := &Poc{}
|
||||
decls := collectVarDeclarations(p)
|
||||
if len(decls) != 0 {
|
||||
t.Fatalf("len = %d, want 0", len(decls))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("仅 Set 字段", func(t *testing.T) {
|
||||
p := &Poc{
|
||||
Set: StrMap{
|
||||
{Key: "token", Value: "randomLowercase(8)"},
|
||||
{Key: "port", Value: "randomInt(1000, 9000)"},
|
||||
},
|
||||
}
|
||||
decls := collectVarDeclarations(p)
|
||||
if len(decls) != 2 {
|
||||
t.Fatalf("len = %d, want 2", len(decls))
|
||||
}
|
||||
if decls[0].Name != "token" {
|
||||
t.Errorf("decls[0].Name = %q, want token", decls[0].Name)
|
||||
}
|
||||
if decls[1].Name != "port" {
|
||||
t.Errorf("decls[1].Name = %q, want port", decls[1].Name)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("仅 Sets 字段", func(t *testing.T) {
|
||||
p := &Poc{
|
||||
Sets: ListMap{
|
||||
{Key: "user", Value: []string{"admin", "root"}},
|
||||
},
|
||||
}
|
||||
decls := collectVarDeclarations(p)
|
||||
if len(decls) != 1 {
|
||||
t.Fatalf("len = %d, want 1", len(decls))
|
||||
}
|
||||
if decls[0].Name != "user" {
|
||||
t.Errorf("decls[0].Name = %q, want user", decls[0].Name)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Sets 空值列表不 panic", func(t *testing.T) {
|
||||
p := &Poc{
|
||||
Sets: ListMap{
|
||||
{Key: "empty", Value: []string{}},
|
||||
},
|
||||
}
|
||||
decls := collectVarDeclarations(p)
|
||||
if len(decls) != 1 {
|
||||
t.Fatalf("len = %d, want 1", len(decls))
|
||||
}
|
||||
if decls[0].Name != "empty" {
|
||||
t.Errorf("decls[0].Name = %q, want empty", decls[0].Name)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Set 和 Sets 合并", func(t *testing.T) {
|
||||
p := &Poc{
|
||||
Set: StrMap{
|
||||
{Key: "a", Value: "x"},
|
||||
},
|
||||
Sets: ListMap{
|
||||
{Key: "b", Value: []string{"y"}},
|
||||
},
|
||||
}
|
||||
decls := collectVarDeclarations(p)
|
||||
if len(decls) != 2 {
|
||||
t.Fatalf("len = %d, want 2", len(decls))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("newReverse 前缀推断 Object 类型", func(t *testing.T) {
|
||||
p := &Poc{
|
||||
Set: StrMap{
|
||||
{Key: "rev", Value: "newReverse()"},
|
||||
},
|
||||
}
|
||||
decls := collectVarDeclarations(p)
|
||||
if len(decls) != 1 {
|
||||
t.Fatalf("len = %d, want 1", len(decls))
|
||||
}
|
||||
tp := decls[0].GetIdent().GetType()
|
||||
if tp == nil || tp.GetMessageType() == "" {
|
||||
t.Errorf("期望 Object 类型,实际 %v", tp)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// Padding 测试
|
||||
// =============================================================================
|
||||
|
||||
func TestPadding_BasicBlockAlignment(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input []byte
|
||||
blockSize int
|
||||
wantLen int // 期望长度
|
||||
}{
|
||||
{
|
||||
name: "空输入填充整个块",
|
||||
input: []byte{},
|
||||
blockSize: 16,
|
||||
wantLen: 16,
|
||||
},
|
||||
{
|
||||
name: "15字节填充1字节",
|
||||
input: make([]byte, 15),
|
||||
blockSize: 16,
|
||||
wantLen: 16,
|
||||
},
|
||||
{
|
||||
name: "整块对齐追加完整块",
|
||||
input: make([]byte, 16),
|
||||
blockSize: 16,
|
||||
wantLen: 32,
|
||||
},
|
||||
{
|
||||
name: "1字节填充15字节",
|
||||
input: []byte{0x01},
|
||||
blockSize: 16,
|
||||
wantLen: 16,
|
||||
},
|
||||
{
|
||||
name: "blockSize=8时的对齐",
|
||||
input: make([]byte, 5),
|
||||
blockSize: 8,
|
||||
wantLen: 8,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := Padding(tt.input, tt.blockSize)
|
||||
if len(result) != tt.wantLen {
|
||||
t.Errorf("Padding() len=%d, want %d", len(result), tt.wantLen)
|
||||
}
|
||||
// 验证填充字节值符合 PKCS7 规范
|
||||
if len(result) > 0 {
|
||||
padLen := int(result[len(result)-1])
|
||||
if padLen == 0 || padLen > tt.blockSize {
|
||||
t.Errorf("填充字节值 %d 超出 blockSize=%d", padLen, tt.blockSize)
|
||||
}
|
||||
// 验证所有填充字节相同
|
||||
for i := len(result) - padLen; i < len(result); i++ {
|
||||
if result[i] != byte(padLen) {
|
||||
t.Errorf("填充字节[%d]=%d 不等于 padLen=%d", i, result[i], padLen)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPadding_ResultLength(t *testing.T) {
|
||||
// 任意长度输入,结果都应该是 blockSize 的整数倍
|
||||
blockSize := 16
|
||||
for inputLen := 0; inputLen < 50; inputLen++ {
|
||||
input := make([]byte, inputLen)
|
||||
result := Padding(input, blockSize)
|
||||
if len(result)%blockSize != 0 {
|
||||
t.Errorf("输入长度 %d: 填充后长度 %d 不是 %d 的倍数", inputLen, len(result), blockSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// AESCBCEncrypt 测试
|
||||
// =============================================================================
|
||||
|
||||
func TestAESCBCEncrypt_ValidKey128(t *testing.T) {
|
||||
// 128-bit AES key (16 bytes)
|
||||
key := base64.StdEncoding.EncodeToString(make([]byte, 16))
|
||||
result := AESCBCEncrypt(key)
|
||||
if result == "" {
|
||||
t.Error("有效的128位密钥应返回非空结果")
|
||||
}
|
||||
// 结果应为有效的 base64
|
||||
_, err := base64.StdEncoding.DecodeString(result)
|
||||
if err != nil {
|
||||
t.Errorf("AESCBCEncrypt 结果应为有效 base64: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAESCBCEncrypt_ValidKey256(t *testing.T) {
|
||||
// 256-bit AES key (32 bytes)
|
||||
key := base64.StdEncoding.EncodeToString(make([]byte, 32))
|
||||
result := AESCBCEncrypt(key)
|
||||
if result == "" {
|
||||
t.Error("有效的256位密钥应返回非空结果")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAESCBCEncrypt_InvalidBase64Key(t *testing.T) {
|
||||
result := AESCBCEncrypt("!!!not-valid-base64!!!")
|
||||
if result != "" {
|
||||
t.Error("无效 base64 密钥应返回空字符串")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAESCBCEncrypt_InvalidKeySize(t *testing.T) {
|
||||
// AES 要求密钥为 16/24/32 字节,10 字节无效
|
||||
key := base64.StdEncoding.EncodeToString(make([]byte, 10))
|
||||
result := AESCBCEncrypt(key)
|
||||
if result != "" {
|
||||
t.Error("无效密钥长度应返回空字符串")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAESCBCEncrypt_NonDeterministic(t *testing.T) {
|
||||
// 因为 IV 是随机的,两次加密结果应不同
|
||||
key := base64.StdEncoding.EncodeToString(make([]byte, 16))
|
||||
r1 := AESCBCEncrypt(key)
|
||||
r2 := AESCBCEncrypt(key)
|
||||
if r1 == r2 {
|
||||
// 极小概率相同,记录即可
|
||||
t.Log("两次加密结果相同(极低概率事件)")
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// AESGCMEncrypt 测试
|
||||
// =============================================================================
|
||||
|
||||
func TestAESGCMEncrypt_ValidKey128(t *testing.T) {
|
||||
key := base64.StdEncoding.EncodeToString(make([]byte, 16))
|
||||
result := AESGCMEncrypt(key)
|
||||
if result == "" {
|
||||
t.Error("有效的128位密钥应返回非空结果")
|
||||
}
|
||||
_, err := base64.StdEncoding.DecodeString(result)
|
||||
if err != nil {
|
||||
t.Errorf("AESGCMEncrypt 结果应为有效 base64: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAESGCMEncrypt_InvalidKey(t *testing.T) {
|
||||
result := AESGCMEncrypt("invalid-base64!!!")
|
||||
if result != "" {
|
||||
t.Error("无效 base64 密钥应返回空字符串")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAESGCMEncrypt_NonDeterministic(t *testing.T) {
|
||||
key := base64.StdEncoding.EncodeToString(make([]byte, 16))
|
||||
r1 := AESGCMEncrypt(key)
|
||||
r2 := AESGCMEncrypt(key)
|
||||
// GCM nonce 随机,结果不应相同
|
||||
if r1 == r2 {
|
||||
t.Log("两次 GCM 加密结果相同(极低概率事件)")
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// GetShrioCookie 测试
|
||||
// =============================================================================
|
||||
|
||||
func TestGetShrioCookie_CBCMode(t *testing.T) {
|
||||
key := base64.StdEncoding.EncodeToString(make([]byte, 16))
|
||||
result := GetShrioCookie(key, "cbc")
|
||||
if result == "" {
|
||||
t.Error("CBC 模式应返回非空 cookie")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetShrioCookie_GCMMode(t *testing.T) {
|
||||
key := base64.StdEncoding.EncodeToString(make([]byte, 16))
|
||||
result := GetShrioCookie(key, "gcm")
|
||||
if result == "" {
|
||||
t.Error("GCM 模式应返回非空 cookie")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetShrioCookie_DefaultMode(t *testing.T) {
|
||||
// 非 gcm 模式走 CBC
|
||||
key := base64.StdEncoding.EncodeToString(make([]byte, 16))
|
||||
result := GetShrioCookie(key, "other")
|
||||
cbcResult := AESCBCEncrypt(key)
|
||||
// 两个结果都应为非空 base64,但由于随机 IV 不一定相同
|
||||
if result == "" {
|
||||
t.Error("默认(非gcm)模式应使用 CBC 加密并返回非空结果")
|
||||
}
|
||||
_ = cbcResult
|
||||
}
|
||||
|
||||
func TestGetShrioCookie_RealShiroKey(t *testing.T) {
|
||||
// 使用真实的 Shiro 默认密钥
|
||||
shiroDefaultKey := "kPH+bIxk5D2deZiIxcaaaA=="
|
||||
result := GetShrioCookie(shiroDefaultKey, "cbc")
|
||||
if result == "" {
|
||||
t.Error("使用默认 Shiro 密钥应能生成有效 cookie")
|
||||
}
|
||||
// 验证结果是 base64 编码
|
||||
decoded, err := base64.StdEncoding.DecodeString(result)
|
||||
if err != nil {
|
||||
t.Errorf("结果应为有效 base64: %v", err)
|
||||
}
|
||||
// CBC 模式:IV(16字节) + 密文,结果至少 32 字节
|
||||
if len(decoded) < 32 {
|
||||
t.Errorf("CBC 加密结果太短: %d 字节", len(decoded))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetShrioCookie_ResultIsBase64(t *testing.T) {
|
||||
key := base64.StdEncoding.EncodeToString(make([]byte, 16))
|
||||
for _, mode := range []string{"cbc", "gcm"} {
|
||||
result := GetShrioCookie(key, mode)
|
||||
if result == "" {
|
||||
t.Errorf("mode=%s: 结果不应为空", mode)
|
||||
continue
|
||||
}
|
||||
// base64 只含 [A-Za-z0-9+/=]
|
||||
if strings.ContainsAny(result, " \t\n\r") {
|
||||
t.Errorf("mode=%s: base64 结果不应含空白字符", mode)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
id: test-nuclei-example
|
||||
info:
|
||||
name: Test Nuclei Example Template
|
||||
author: fscan-dev
|
||||
severity: info
|
||||
description: |
|
||||
This is a test template to demonstrate Nuclei format support in fscan.
|
||||
It will be automatically converted to fscan format during loading.
|
||||
reference:
|
||||
- https://github.com/shadow1ng/fscan
|
||||
|
||||
http:
|
||||
- method: GET
|
||||
path:
|
||||
- "{{BaseURL}}/robots.txt"
|
||||
|
||||
matchers:
|
||||
- type: word
|
||||
words:
|
||||
- "User-agent"
|
||||
- "Disallow"
|
||||
condition: and
|
||||
|
||||
- type: status
|
||||
status:
|
||||
- 200
|
||||
+14
-1
@@ -97,7 +97,11 @@ func WebScan(ctx context.Context, info *common.HostInfo, cfg *common.Config, ses
|
||||
func buildTargetURL(info *common.HostInfo) (string, error) {
|
||||
// 自动构建URL
|
||||
if info.URL == "" {
|
||||
info.URL = protocolHTTP + net.JoinHostPort(info.Host, fmt.Sprint(info.Port))
|
||||
protocol := protocolHTTP
|
||||
if isTLSPort(info.Port) {
|
||||
protocol = protocolHTTPS
|
||||
}
|
||||
info.URL = protocol + net.JoinHostPort(info.Host, fmt.Sprint(info.Port))
|
||||
} else if !hasProtocolPrefix(info.URL) {
|
||||
info.URL = protocolHTTP + normalizeSchemelessWebTarget(info.URL)
|
||||
}
|
||||
@@ -132,6 +136,15 @@ func hasProtocolPrefix(urlStr string) bool {
|
||||
return strings.HasPrefix(urlStr, protocolHTTP) || strings.HasPrefix(urlStr, protocolHTTPS)
|
||||
}
|
||||
|
||||
func isTLSPort(port int) bool {
|
||||
switch port {
|
||||
case 443, 8443, 4443, 9443:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeSchemelessWebTarget(rawURL string) string {
|
||||
authority := rawURL
|
||||
suffix := ""
|
||||
|
||||
Reference in New Issue
Block a user