diff --git a/common/blackList/blacklist.go b/common/blackList/blacklist.go new file mode 100644 index 0000000..ed4f159 --- /dev/null +++ b/common/blackList/blacklist.go @@ -0,0 +1,55 @@ +package blackList + +import ( + "fmt" + "os" + "regexp" + "strings" + + "gemini-grc/config" + "gemini-grc/errors" + "gemini-grc/logging" +) + +var Blacklist []regexp.Regexp //nolint:gochecknoglobals + +func LoadBlacklist() error { + if config.CONFIG.BlacklistPath == "" { + return nil + } + if Blacklist == nil { + data, err := os.ReadFile(config.CONFIG.BlacklistPath) + if err != nil { + Blacklist = []regexp.Regexp{} + return errors.NewError(fmt.Errorf("could not load Blacklist file: %w", err)) + } + + lines := strings.Split(string(data), "\n") + + for _, line := range lines { + if line == "" || strings.HasPrefix(line, "#") { + continue + } + regex, err := regexp.Compile(line) + if err != nil { + return errors.NewError(fmt.Errorf("could not compile Blacklist line %s: %w", line, err)) + } + Blacklist = append(Blacklist, *regex) + + } + + if len(lines) > 0 { + logging.LogInfo("Loaded %d blacklist entries", len(Blacklist)) + } + } + return nil +} + +func IsBlacklisted(u string) bool { + for _, v := range Blacklist { + if v.MatchString(u) { + return true + } + } + return false +} diff --git a/common/blackList/blacklist_test.go b/common/blackList/blacklist_test.go new file mode 100644 index 0000000..55a6252 --- /dev/null +++ b/common/blackList/blacklist_test.go @@ -0,0 +1,295 @@ +package blackList + +import ( + "os" + "regexp" + "testing" + + "gemini-grc/config" +) + +func TestIsBlacklisted(t *testing.T) { + // Save original blacklist to restore after test + originalBlacklist := Blacklist + defer func() { + Blacklist = originalBlacklist + }() + + tests := []struct { + name string + setup func() + url string + expected bool + }{ + { + name: "empty blacklist", + setup: func() { + Blacklist = []regexp.Regexp{} + }, + url: "https://example.com", + expected: false, + }, + { + name: "exact hostname match", + setup: func() { + regex, _ := regexp.Compile(`example\.com`) + Blacklist = []regexp.Regexp{*regex} + }, + url: "example.com", + expected: true, + }, + { + name: "hostname in URL match", + setup: func() { + regex, _ := regexp.Compile(`example\.com`) + Blacklist = []regexp.Regexp{*regex} + }, + url: "https://example.com/path", + expected: true, + }, + { + name: "partial hostname match", + setup: func() { + regex, _ := regexp.Compile(`example\.com`) + Blacklist = []regexp.Regexp{*regex} + }, + url: "https://safe-example.com", + expected: true, + }, + { + name: "full URL match", + setup: func() { + regex, _ := regexp.Compile(`https://example\.com/bad-path`) + Blacklist = []regexp.Regexp{*regex} + }, + url: "https://example.com/bad-path", + expected: true, + }, + { + name: "path match", + setup: func() { + regex, _ := regexp.Compile("/malicious-path") + Blacklist = []regexp.Regexp{*regex} + }, + url: "https://example.com/malicious-path", + expected: true, + }, + { + name: "subdomain match with word boundary", + setup: func() { + regex, _ := regexp.Compile(`bad\.example\.com`) + Blacklist = []regexp.Regexp{*regex} + }, + url: "https://bad.example.com/path", + expected: true, + }, + { + name: "multiple patterns, one match", + setup: func() { + regex1, _ := regexp.Compile(`badsite\.com`) + regex2, _ := regexp.Compile(`malicious\.org`) + regex3, _ := regexp.Compile(`example\.com/sensitive`) + Blacklist = []regexp.Regexp{*regex1, *regex2, *regex3} + }, + url: "https://example.com/sensitive/data", + expected: true, + }, + { + name: "multiple patterns, no match", + setup: func() { + regex1, _ := regexp.Compile(`badsite\.com`) + regex2, _ := regexp.Compile(`malicious\.org`) + regex3, _ := regexp.Compile(`example\.com/sensitive`) + Blacklist = []regexp.Regexp{*regex1, *regex2, *regex3} + }, + url: "https://example.com/safe/data", + expected: false, + }, + { + name: "pattern with wildcard", + setup: func() { + regex, _ := regexp.Compile(`.*\.evil\.com`) + Blacklist = []regexp.Regexp{*regex} + }, + url: "https://subdomain.evil.com/path", + expected: true, + }, + { + name: "pattern with special characters", + setup: func() { + regex, _ := regexp.Compile(`example\.com/path\?id=[0-9]+`) + Blacklist = []regexp.Regexp{*regex} + }, + url: "https://example.com/path?id=12345", + expected: true, + }, + { + name: "unicode character support", + setup: func() { + regex, _ := regexp.Compile(`example\.com/[\p{L}]+`) + Blacklist = []regexp.Regexp{*regex} + }, + url: "https://example.com/café", + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.setup() + result := IsBlacklisted(tt.url) + if result != tt.expected { + t.Errorf("IsBlacklisted(%q) = %v, want %v", tt.url, result, tt.expected) + } + }) + } +} + +func TestLoadBlacklist(t *testing.T) { + // Save original blacklist to restore after test + originalBlacklist := Blacklist + originalConfigPath := config.CONFIG.BlacklistPath + defer func() { + Blacklist = originalBlacklist + config.CONFIG.BlacklistPath = originalConfigPath + }() + + // Create a temporary blacklist file for testing + tmpFile, err := os.CreateTemp("", "blacklist-*.txt") + if err != nil { + t.Fatalf("Failed to create temporary file: %v", err) + } + defer os.Remove(tmpFile.Name()) + + // Test cases for LoadBlacklist + tests := []struct { + name string + blacklistLines []string + configPath string + wantErr bool + expectedLen int + }{ + { + name: "empty path", + blacklistLines: []string{}, + configPath: "", + wantErr: false, + expectedLen: 0, + }, + { + name: "valid blacklist with comments", + blacklistLines: []string{"example\\.com", "# This is a comment", "malicious\\.org"}, + configPath: tmpFile.Name(), + wantErr: false, + expectedLen: 2, + }, + { + name: "invalid regex", + blacklistLines: []string{"example\\.com", "[invalid regex"}, + configPath: tmpFile.Name(), + wantErr: true, + expectedLen: 0, + }, + { + name: "nonexistent file", + blacklistLines: []string{}, + configPath: "nonexistent-file.txt", + wantErr: true, + expectedLen: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Reset blacklist + Blacklist = nil + + // Set config path + config.CONFIG.BlacklistPath = tt.configPath + + // Write test data to file if needed + if tt.configPath == tmpFile.Name() { + content := "" + for _, line := range tt.blacklistLines { + content += line + "\n" + } + if err := os.WriteFile(tmpFile.Name(), []byte(content), 0o644); err != nil { + t.Fatalf("Failed to write to temporary file: %v", err) + } + } + + // Call the function + err := LoadBlacklist() + + // Check results + if (err != nil) != tt.wantErr { + t.Errorf("LoadBlacklist() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if !tt.wantErr && len(Blacklist) != tt.expectedLen { + t.Errorf("LoadBlacklist() loaded %d entries, want %d", len(Blacklist), tt.expectedLen) + } + }) + } +} + +// TestIsBlacklistedIntegration tests the integration between LoadBlacklist and IsBlacklisted +func TestIsBlacklistedIntegration(t *testing.T) { + // Save original blacklist to restore after test + originalBlacklist := Blacklist + originalConfigPath := config.CONFIG.BlacklistPath + defer func() { + Blacklist = originalBlacklist + config.CONFIG.BlacklistPath = originalConfigPath + }() + + // Create a temporary blacklist file for testing + tmpFile, err := os.CreateTemp("", "blacklist-*.txt") + if err != nil { + t.Fatalf("Failed to create temporary file: %v", err) + } + defer os.Remove(tmpFile.Name()) + + // Write test patterns to the blacklist file + blacklistContent := `# Test blacklist file +example\.com +malicious\.org +/phishing +.*\.evil\.com +\w+@spam\.com +` + if err := os.WriteFile(tmpFile.Name(), []byte(blacklistContent), 0o644); err != nil { + t.Fatalf("Failed to write to temporary file: %v", err) + } + + // Set up the test + Blacklist = nil + config.CONFIG.BlacklistPath = tmpFile.Name() + + // Load the blacklist + if err := LoadBlacklist(); err != nil { + t.Fatalf("LoadBlacklist() failed: %v", err) + } + + // Test URLs against the loaded blacklist + tests := []struct { + url string + expected bool + }{ + {"https://example.com", true}, + {"https://safe-site.com", false}, + {"https://malicious.org/path", true}, + {"https://example.org/phishing", true}, + {"https://subdomain.evil.com", true}, + {"https://safe-site.com/safe-path", false}, + {"mailto:user@spam.com", true}, + } + + for _, tt := range tests { + result := IsBlacklisted(tt.url) + if result != tt.expected { + t.Errorf("IsBlacklisted(%q) = %v, want %v", tt.url, result, tt.expected) + } + } +} diff --git a/gemini/blacklist.go b/gemini/blacklist.go deleted file mode 100644 index 5fa9ad5..0000000 --- a/gemini/blacklist.go +++ /dev/null @@ -1,55 +0,0 @@ -package gemini - -import ( - "fmt" - "gemini-grc/common" - "os" - "strings" - - "gemini-grc/config" - "gemini-grc/logging" -) - -var Blacklist *[]string //nolint:gochecknoglobals - -func LoadBlacklist() { - if Blacklist == nil { - data, err := os.ReadFile(config.CONFIG.BlacklistPath) - if err != nil { - Blacklist = &[]string{} - logging.LogWarn("Could not load Blacklist file: %v", err) - return - } - lines := strings.Split(string(data), "\n") - - // Ignore lines starting with '#' (comments) - filteredLines := func() []string { - out := make([]string, 0, len(lines)) - for _, line := range lines { - if !strings.HasPrefix(line, "#") { - out = append(out, line) - } - } - return out - }() - - if len(lines) > 0 { - Blacklist = &filteredLines - logging.LogInfo("Blacklist has %d entries", len(*Blacklist)) - } - } -} - -func IsBlacklisted(u string) bool { - url, err := common.ParseURL(u, "") - if err != nil { - return false - } - hostWithPort := fmt.Sprintf("%s:%d", url.Hostname, url.Port) - for _, v := range *Blacklist { - if v == url.Hostname || v == hostWithPort { - return true - } - } - return false -}