package WebScan import ( "context" "testing" "github.com/shadow1ng/fscan/common" "github.com/shadow1ng/fscan/common/config" "github.com/shadow1ng/fscan/webscan/lib" ) func TestBuildTargetURL(t *testing.T) { tests := []struct { name string hostInfo *common.HostInfo expected string expectError bool }{ { name: "empty url builds from host and port", hostInfo: &common.HostInfo{ Host: "192.168.1.1", Port: 8080, URL: "", }, expected: "http://192.168.1.1:8080", expectError: false, }, { name: "url without protocol gets http prefix", hostInfo: &common.HostInfo{ Host: "example.com", Port: 80, URL: "example.com", }, expected: "http://example.com", expectError: false, }, { name: "url with http protocol", hostInfo: &common.HostInfo{ Host: "example.com", Port: 80, URL: "http://example.com", }, expected: "http://example.com", expectError: false, }, { name: "url with https protocol", hostInfo: &common.HostInfo{ Host: "example.com", Port: 443, URL: "https://example.com", }, expected: "https://example.com", expectError: false, }, { name: "url with port", hostInfo: &common.HostInfo{ Host: "example.com", Port: 8443, URL: "https://example.com:8443", }, expected: "https://example.com:8443", expectError: false, }, { name: "url with path gets stripped", hostInfo: &common.HostInfo{ Host: "example.com", Port: 80, URL: "http://example.com/admin/login", }, expected: "http://example.com", expectError: false, }, { name: "url with query string gets stripped", hostInfo: &common.HostInfo{ Host: "example.com", Port: 80, URL: "http://example.com?foo=bar", }, expected: "http://example.com", expectError: false, }, { name: "localhost with port", hostInfo: &common.HostInfo{ Host: "localhost", Port: 3000, URL: "", }, expected: "http://localhost:3000", expectError: false, }, { name: "ipv4 with port", hostInfo: &common.HostInfo{ Host: "10.0.0.1", Port: 8888, URL: "", }, expected: "http://10.0.0.1:8888", expectError: false, }, { name: "domain without port builds from hostinfo", hostInfo: &common.HostInfo{ Host: "test.example.com", Port: 9090, URL: "", }, expected: "http://test.example.com:9090", expectError: false, }, { name: "ipv6 builds bracketed host and port", hostInfo: &common.HostInfo{ Host: "2001:db8::1", Port: 8080, URL: "", }, expected: "http://[2001:db8::1]:8080", expectError: false, }, { name: "ipv6 url without protocol keeps brackets", hostInfo: &common.HostInfo{ Host: "2001:db8::1", Port: 443, URL: "[2001:db8::1]:443/admin", }, expected: "http://[2001:db8::1]:443", expectError: false, }, { name: "bare ipv6 url without protocol gets brackets", hostInfo: &common.HostInfo{ Host: "2001:db8::1", Port: 80, URL: "2001:db8::1/admin", }, expected: "http://[2001:db8::1]", expectError: false, }, { name: "bare ipv6 url with protocol gets brackets", hostInfo: &common.HostInfo{ Host: "2001:db8::1", Port: 80, URL: "http://2001:db8::1/admin", }, expected: "http://[2001:db8::1]", expectError: false, }, { name: "empty host is rejected", hostInfo: &common.HostInfo{ Port: 80, URL: "http://", }, expectError: true, }, { name: "invalid port is rejected", hostInfo: &common.HostInfo{ Host: "example.com", Port: 80, URL: "http://example.com:bad", }, expectError: true, }, { name: "empty explicit port is rejected", hostInfo: &common.HostInfo{ Host: "example.com", Port: 80, URL: "http://example.com:", }, expectError: true, }, { name: "out of range port is rejected", hostInfo: &common.HostInfo{ Host: "example.com", Port: 80, URL: "http://example.com:70000", }, expectError: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := buildTargetURL(tt.hostInfo) if tt.expectError { if err == nil { t.Errorf("expected error but got nil") } return } if err != nil { t.Errorf("unexpected error: %v", err) return } if result != tt.expected { t.Errorf("buildTargetURL() = %q, want %q", result, tt.expected) } }) } } func TestBuildTargetURLErrors(t *testing.T) { tests := []struct { name string hostInfo *common.HostInfo }{ { name: "invalid url with special chars", hostInfo: &common.HostInfo{ Host: "example.com", Port: 80, URL: "http://exam ple.com", // 空格在 URL 中无效 }, }, { name: "url with invalid scheme", hostInfo: &common.HostInfo{ Host: "example.com", Port: 80, URL: "ht!tp://example.com", }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := buildTargetURL(tt.hostInfo) // 某些无效 URL 可能仍被解析,我们主要检查函数不会 panic _ = result _ = err }) } } func TestHasProtocolPrefix(t *testing.T) { tests := []struct { name string input string expected bool }{ // 正常情况 {"http prefix", "http://example.com", true}, {"https prefix", "https://example.com", true}, {"no prefix domain", "example.com", false}, {"no prefix ip", "192.168.1.1", false}, {"no prefix with port", "example.com:8080", false}, // 边界情况 {"empty string", "", false}, {"only http", "http://", true}, {"only https", "https://", true}, {"http in middle", "example.http://com", false}, {"uppercase HTTP", "HTTP://example.com", true}, {"uppercase HTTPS", "HTTPS://example.com", true}, {"ftp protocol", "ftp://example.com", false}, {"http no slashes", "http:example.com", false}, {"partial prefix", "http:/example.com", false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := hasProtocolPrefix(tt.input) if result != tt.expected { t.Errorf("hasProtocolPrefix(%q) = %v, want %v", tt.input, result, tt.expected) } }) } } func TestIsPocFile(t *testing.T) { tests := []struct { name string filename string expected bool }{ // 正常情况 {"yaml extension", "test.yaml", true}, {"yml extension", "test.yml", true}, {"uppercase YAML", "test.YAML", true}, {"uppercase YML", "test.YML", true}, {"mixed case Yaml", "test.Yaml", true}, {"mixed case Yml", "test.Yml", true}, // 非POC文件 {"go file", "test.go", false}, {"txt file", "test.txt", false}, {"no extension", "test", false}, {"json file", "test.json", false}, // 边界情况 {"empty string", "", false}, {"only .yaml", ".yaml", true}, {"only .yml", ".yml", true}, {"multiple dots", "test.poc.yaml", true}, {"yaml in name", "yaml.txt", false}, {"yml in name", "yml.go", false}, {"ends with yaml no dot", "testyaml", false}, {"yaml with path", "pocs/test.yaml", true}, {"yml with path", "/tmp/pocs/test.yml", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := isPocFile(tt.filename) if result != tt.expected { t.Errorf("isPocFile(%q) = %v, want %v", tt.filename, result, tt.expected) } }) } } func TestFilterPocs(t *testing.T) { // 创建测试 POC 数据 testPocs := []*lib.Poc{ {Name: "Apache-Struts2-CVE-2017-5638"}, {Name: "WebLogic-CVE-2020-14882"}, {Name: "Tomcat-CVE-2020-1938"}, {Name: "Apache-Log4j-CVE-2021-44228"}, {Name: "Spring-CVE-2022-22965"}, nil, // 测试 nil 处理 {Name: "Nginx-Path-Traversal"}, } // 保存原始 allPocs 并在测试后恢复 origAllPocs := allPocs defer func() { allPocs = origAllPocs }() allPocs = testPocs tests := []struct { name string pocName string expectedCount int expectedNames []string }{ { name: "empty poc name returns all", pocName: "", expectedCount: 7, // 包括 nil(实际行为) expectedNames: nil, }, { name: "match apache case insensitive", pocName: "apache", expectedCount: 2, expectedNames: []string{"Apache-Struts2-CVE-2017-5638", "Apache-Log4j-CVE-2021-44228"}, }, { name: "match APACHE uppercase", pocName: "APACHE", expectedCount: 2, expectedNames: []string{"Apache-Struts2-CVE-2017-5638", "Apache-Log4j-CVE-2021-44228"}, }, { name: "match cve", pocName: "cve", expectedCount: 5, expectedNames: nil, }, { name: "match weblogic", pocName: "weblogic", expectedCount: 1, expectedNames: []string{"WebLogic-CVE-2020-14882"}, }, { name: "match 2020", pocName: "2020", expectedCount: 2, expectedNames: []string{"WebLogic-CVE-2020-14882", "Tomcat-CVE-2020-1938"}, }, { name: "no match", pocName: "nonexistent", expectedCount: 0, expectedNames: []string{}, }, { name: "partial match spring", pocName: "spring", expectedCount: 1, expectedNames: []string{"Spring-CVE-2022-22965"}, }, { name: "match with special chars", pocName: "log4j", expectedCount: 1, expectedNames: []string{"Apache-Log4j-CVE-2021-44228"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := filterPocs(tt.pocName) if len(result) != tt.expectedCount { t.Errorf("filterPocs(%q) returned %d pocs, want %d", tt.pocName, len(result), tt.expectedCount) } // 如果指定了期望的名称,验证它们 if tt.expectedNames != nil { if len(result) != len(tt.expectedNames) { t.Errorf("filterPocs(%q) returned %d pocs, want %d", tt.pocName, len(result), len(tt.expectedNames)) return } for i, poc := range result { found := false for _, expectedName := range tt.expectedNames { if poc.Name == expectedName { found = true break } } if !found { t.Errorf("filterPocs(%q) result[%d].Name = %q, not in expected names %v", tt.pocName, i, poc.Name, tt.expectedNames) } } } // 只有在非空 pocName 时才验证没有 nil(因为有过滤) if tt.pocName != "" { for i, poc := range result { if poc == nil { t.Errorf("filterPocs(%q) result[%d] is nil", tt.pocName, i) } } } }) } } func TestFilterPocsNilSafety(t *testing.T) { // 测试全是 nil 的情况 origAllPocs := allPocs defer func() { allPocs = origAllPocs }() allPocs = []*lib.Poc{nil, nil, nil} result := filterPocs("test") if len(result) != 0 { t.Errorf("filterPocs with all nil should return empty slice, got %d items", len(result)) } // 空 pocName 返回所有 POCs(包括 nil) result = filterPocs("") if len(result) != 3 { t.Errorf("filterPocs with empty name should return all pocs (including nil), got %d items, want 3", len(result)) } } func TestCreateBaseRequestHeaders(t *testing.T) { cfg := common.NewConfig() cfg.HTTP.UserAgent = "fscan-test-agent" cfg.HTTP.Accept = "application/json" cfg.HTTP.Cookie = "sid=abc" req, err := createBaseRequest(context.Background(), "http://example.com/path", cfg) if err != nil { t.Fatalf("createBaseRequest error = %v", err) } if req.Method != "GET" { t.Fatalf("method = %q, want GET", req.Method) } if got := req.Header.Get("User-agent"); got != "fscan-test-agent" { t.Fatalf("User-agent = %q", got) } if got := req.Header.Get("Accept"); got != "application/json" { t.Fatalf("Accept = %q", got) } if got := req.Header.Get("Cookie"); got != "sid=abc" { t.Fatalf("Cookie = %q", got) } if got := req.Header.Get("Accept-Language"); got == "" { t.Fatal("Accept-Language should be set") } } func TestExecutePOCsEarlyReturns(t *testing.T) { cfg := common.NewConfig() session := common.NewScanSession(cfg, common.NewState(), &common.FlagVars{}) previous := allPocs allPocs = nil t.Cleanup(func() { allPocs = previous }) executePOCs(context.Background(), config.PocInfo{}, cfg, session) executePOCs(context.Background(), config.PocInfo{Target: "http://example.com", PocName: "missing"}, cfg, session) } func TestDirectoryExists(t *testing.T) { tests := []struct { name string path string expected bool }{ // 这些测试取决于文件系统状态,仅作为示例 {"current directory", ".", true}, {"nonexistent", "/nonexistent/path/12345", false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := directoryExists(tt.path) if result != tt.expected { t.Errorf("directoryExists(%q) = %v, want %v", tt.path, result, tt.expected) } }) } }