srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/internal/scope
diff options
context:
space:
mode:
Diffstat (limited to 'internal/scope')
-rw-r--r--internal/scope/scope.go61
-rw-r--r--internal/scope/scope_test.go96
2 files changed, 157 insertions, 0 deletions
diff --git a/internal/scope/scope.go b/internal/scope/scope.go
new file mode 100644
index 0000000..d8d2e80
--- /dev/null
+++ b/internal/scope/scope.go
@@ -0,0 +1,61 @@
+// Package scope filters which captured traffic gets recorded to
+// history - a target scope, in Burp's sense: out-of-scope requests
+// still proxy through completely normally (nothing is blocked), they
+// just aren't stored, so unrelated CDN/analytics/tracker noise doesn't
+// pollute history and search on a real engagement. Deliberately not an
+// access-control mechanism; that would be a materially different,
+// riskier feature (breaking a workflow by silently blocking traffic is
+// a much worse failure mode than a noisier history).
+package scope
+
+import (
+ "regexp"
+ "strings"
+)
+
+// Rule is one scope entry. A non-regex Pattern matches by substring
+// containment against the host (case-insensitive) - "example.com"
+// matches "example.com", "www.example.com", and "api.example.com"
+// alike, covering the common "this domain and its subdomains" case
+// without inventing a separate wildcard syntax. IsRegex switches to a
+// full regex match against the host, mirroring the same toggle
+// match-and-replace rules already use, for the same reason: one
+// consistent mental model across both rule types in this tool.
+type Rule struct {
+ ID int64
+ Enabled bool
+ Pattern string
+ IsRegex bool
+}
+
+// InScope reports whether host should be recorded, given rules.
+// An empty rule set (or one with nothing enabled) means "no scope
+// configured" - everything is in scope, matching this tool's behavior
+// before scope existed at all, so a fresh install or a user who never
+// opens the scope view keeps recording everything, not silently
+// nothing. Once at least one rule is enabled, only a host matching one
+// of them is in scope.
+func InScope(rules []Rule, host string) bool {
+ anyEnabled := false
+ for _, r := range rules {
+ if !r.Enabled {
+ continue
+ }
+ anyEnabled = true
+ if ruleMatches(r, host) {
+ return true
+ }
+ }
+ return !anyEnabled
+}
+
+func ruleMatches(r Rule, host string) bool {
+ if r.IsRegex {
+ re, err := regexp.Compile(r.Pattern)
+ if err != nil {
+ return false
+ }
+ return re.MatchString(host)
+ }
+ return strings.Contains(strings.ToLower(host), strings.ToLower(r.Pattern))
+}
diff --git a/internal/scope/scope_test.go b/internal/scope/scope_test.go
new file mode 100644
index 0000000..fee5d44
--- /dev/null
+++ b/internal/scope/scope_test.go
@@ -0,0 +1,96 @@
+package scope
+
+import "testing"
+
+func TestInScopeEmptyRulesMeansEverything(t *testing.T) {
+ if !InScope(nil, "example.com") {
+ t.Error("empty rule set should mean everything is in scope")
+ }
+ if !InScope([]Rule{}, "anything.at.all") {
+ t.Error("empty rule set should mean everything is in scope")
+ }
+}
+
+func TestInScopeAllDisabledMeansEverything(t *testing.T) {
+ rules := []Rule{{Enabled: false, Pattern: "example.com"}}
+ if !InScope(rules, "unrelated.org") {
+ t.Error("no enabled rules should mean everything is in scope")
+ }
+}
+
+func TestInScopeSubstringMatch(t *testing.T) {
+ rules := []Rule{{Enabled: true, Pattern: "example.com"}}
+ tests := []struct {
+ host string
+ want bool
+ }{
+ {"example.com", true},
+ {"www.example.com", true},
+ {"api.example.com", true},
+ {"example.com.evil.org", true}, // substring containment, deliberately simple
+ {"other.org", false},
+ }
+ for _, tt := range tests {
+ if got := InScope(rules, tt.host); got != tt.want {
+ t.Errorf("InScope(%q) = %v, want %v", tt.host, got, tt.want)
+ }
+ }
+}
+
+func TestInScopeCaseInsensitive(t *testing.T) {
+ rules := []Rule{{Enabled: true, Pattern: "Example.COM"}}
+ if !InScope(rules, "www.EXAMPLE.com") {
+ t.Error("substring match should be case-insensitive")
+ }
+}
+
+func TestInScopeRegex(t *testing.T) {
+ rules := []Rule{{Enabled: true, Pattern: `(^|\.)example\.com$`, IsRegex: true}}
+ tests := []struct {
+ host string
+ want bool
+ }{
+ {"example.com", true},
+ {"api.example.com", true},
+ {"notexample.com", false},
+ {"example.com.evil.org", false},
+ }
+ for _, tt := range tests {
+ if got := InScope(rules, tt.host); got != tt.want {
+ t.Errorf("InScope(%q) = %v, want %v", tt.host, got, tt.want)
+ }
+ }
+}
+
+func TestInScopeInvalidRegexNeverMatches(t *testing.T) {
+ rules := []Rule{{Enabled: true, Pattern: "(unclosed", IsRegex: true}}
+ if InScope(rules, "example.com") {
+ t.Error("an invalid regex rule should never match, not panic or false-positive")
+ }
+}
+
+func TestInScopeMultipleRulesAnyMatch(t *testing.T) {
+ rules := []Rule{
+ {Enabled: true, Pattern: "example.com"},
+ {Enabled: true, Pattern: "other.org"},
+ }
+ if !InScope(rules, "other.org") {
+ t.Error("should match the second rule")
+ }
+ if InScope(rules, "unrelated.net") {
+ t.Error("should not match either rule")
+ }
+}
+
+func TestInScopeDisabledRuleIgnored(t *testing.T) {
+ rules := []Rule{
+ {Enabled: false, Pattern: "example.com"},
+ {Enabled: true, Pattern: "other.org"},
+ }
+ if InScope(rules, "example.com") {
+ t.Error("a disabled rule should not match")
+ }
+ if !InScope(rules, "other.org") {
+ t.Error("the enabled rule should still match")
+ }
+}