diff --git a/profile/endpoints/endpoint-any.go b/profile/endpoints/endpoint-any.go new file mode 100644 index 00000000..34fd65b4 --- /dev/null +++ b/profile/endpoints/endpoint-any.go @@ -0,0 +1,25 @@ +package endpoints + +import "github.com/safing/portmaster/intel" + +// EndpointAny matches anything. +type EndpointAny struct { + EndpointBase +} + +// Matches checks whether the given entity matches this endpoint definition. +func (ep *EndpointAny) Matches(entity *intel.Entity) (result EPResult, reason string) { + return ep.matchesPPP(entity), "matches *" +} + +func (ep *EndpointAny) String() string { + return ep.renderPPP("*") +} + +func parseTypeAny(fields []string) (Endpoint, error) { + if fields[1] == "*" { + ep := &EndpointAny{} + return ep.parsePPP(ep, fields) + } + return nil, nil +} diff --git a/profile/endpoints/endpoint-asn.go b/profile/endpoints/endpoint-asn.go new file mode 100644 index 00000000..dee94f2d --- /dev/null +++ b/profile/endpoints/endpoint-asn.go @@ -0,0 +1,58 @@ +package endpoints + +import ( + "fmt" + "regexp" + "strconv" + + "github.com/safing/portmaster/intel" +) + +var ( + asnRegex = regexp.MustCompile("^(AS)?[0-9]+$") +) + +// EndpointASN matches ASNs. +type EndpointASN struct { + EndpointBase + + ASN uint + Reason string +} + +// Matches checks whether the given entity matches this endpoint definition. +func (ep *EndpointASN) Matches(entity *intel.Entity) (result EPResult, reason string) { + if entity.IP == nil { + return Undeterminable, "" + } + + asn, ok := entity.GetASN() + if !ok { + return Undeterminable, "" + } + if asn == ep.ASN { + return ep.matchesPPP(entity), ep.Reason + } + return NoMatch, "" +} + +func (ep *EndpointASN) String() string { + return ep.renderPPP("AS" + strconv.FormatInt(int64(ep.ASN), 10)) +} + +func parseTypeASN(fields []string) (Endpoint, error) { + if asnRegex.MatchString(fields[1]) { + asn, err := strconv.ParseUint(fields[1][2:], 10, 64) + if err != nil { + return nil, fmt.Errorf("failed to parse AS number %s", fields[1]) + } + + ep := &EndpointASN{ + ASN: uint(asn), + Reason: "IP is part of AS" + strconv.FormatInt(int64(asn), 10), + } + return ep.parsePPP(ep, fields) + } + + return nil, nil +} diff --git a/profile/endpoints/endpoint-country.go b/profile/endpoints/endpoint-country.go new file mode 100644 index 00000000..63d21bfc --- /dev/null +++ b/profile/endpoints/endpoint-country.go @@ -0,0 +1,50 @@ +package endpoints + +import ( + "regexp" + "strings" + + "github.com/safing/portmaster/intel" +) + +var ( + countryRegex = regexp.MustCompile(`^[A-Z]{2}$`) +) + +// EndpointCountry matches countries. +type EndpointCountry struct { + EndpointBase + + Country string +} + +// Matches checks whether the given entity matches this endpoint definition. +func (ep *EndpointCountry) Matches(entity *intel.Entity) (result EPResult, reason string) { + if entity.IP == nil { + return Undeterminable, "" + } + + country, ok := entity.GetCountry() + if !ok { + return Undeterminable, "" + } + if country == ep.Country { + return ep.matchesPPP(entity), "IP is located in " + country + } + return NoMatch, "" +} + +func (ep *EndpointCountry) String() string { + return ep.renderPPP(ep.Country) +} + +func parseTypeCountry(fields []string) (Endpoint, error) { + if countryRegex.MatchString(fields[1]) { + ep := &EndpointCountry{ + Country: strings.ToUpper(fields[1]), + } + return ep.parsePPP(ep, fields) + } + + return nil, nil +} diff --git a/profile/endpoints/endpoint-domain.go b/profile/endpoints/endpoint-domain.go new file mode 100644 index 00000000..225a4b5d --- /dev/null +++ b/profile/endpoints/endpoint-domain.go @@ -0,0 +1,123 @@ +package endpoints + +import ( + "regexp" + "strings" + + "github.com/safing/portmaster/intel" +) + +const ( + domainMatchTypeExact uint8 = iota + domainMatchTypeZone + domainMatchTypeSuffix + domainMatchTypePrefix + domainMatchTypeContains +) + +var ( + domainRegex = regexp.MustCompile(`^\*?(([a-z0-9][a-z0-9-]{0,61}[a-z0-9])?\.)*[a-z]{2,}\.?$`) + altDomainRegex = regexp.MustCompile(`^\*?[a-z0-9\.-]+\*$`) +) + +// EndpointDomain matches domains. +type EndpointDomain struct { + EndpointBase + + OriginalValue string + Domain string + DomainZone string + MatchType uint8 + Reason string +} + +// Matches checks whether the given entity matches this endpoint definition. +func (ep *EndpointDomain) Matches(entity *intel.Entity) (result EPResult, reason string) { + if entity.Domain == "" { + return NoMatch, "" + } + + switch ep.MatchType { + case domainMatchTypeExact: + if entity.Domain == ep.Domain { + return ep.matchesPPP(entity), ep.Reason + } + case domainMatchTypeZone: + if entity.Domain == ep.Domain { + return ep.matchesPPP(entity), ep.Reason + } + if strings.HasSuffix(entity.Domain, ep.DomainZone) { + return ep.matchesPPP(entity), ep.Reason + } + case domainMatchTypeSuffix: + if strings.HasSuffix(entity.Domain, ep.Domain) { + return ep.matchesPPP(entity), ep.Reason + } + case domainMatchTypePrefix: + if strings.HasPrefix(entity.Domain, ep.Domain) { + return ep.matchesPPP(entity), ep.Reason + } + case domainMatchTypeContains: + if strings.Contains(entity.Domain, ep.Domain) { + return ep.matchesPPP(entity), ep.Reason + } + } + + return NoMatch, "" +} + +func (ep *EndpointDomain) String() string { + return ep.renderPPP(ep.OriginalValue) +} + +func parseTypeDomain(fields []string) (Endpoint, error) { + domain := fields[1] + + if domainRegex.MatchString(domain) || altDomainRegex.MatchString(domain) { + ep := &EndpointDomain{ + OriginalValue: domain, + Reason: "domain matches " + domain, + } + + // fix domain ending + switch domain[len(domain)-1] { + case '.': + case '*': + default: + domain += "." + } + + // fix domain case + domain = strings.ToLower(domain) + + switch { + case strings.HasPrefix(domain, "*") && strings.HasSuffix(domain, "*"): + ep.MatchType = domainMatchTypeContains + ep.Domain = strings.Trim(domain, "*") + return ep.parsePPP(ep, fields) + + case strings.HasSuffix(domain, "*"): + ep.MatchType = domainMatchTypePrefix + ep.Domain = strings.Trim(domain, "*") + return ep.parsePPP(ep, fields) + + case strings.HasPrefix(domain, "*"): + ep.MatchType = domainMatchTypeSuffix + ep.Domain = strings.Trim(domain, "*") + return ep.parsePPP(ep, fields) + + case strings.HasPrefix(domain, "."): + ep.MatchType = domainMatchTypeZone + ep.Domain = strings.TrimLeft(domain, ".") + ep.DomainZone = "." + ep.Domain + return ep.parsePPP(ep, fields) + + default: + ep.MatchType = domainMatchTypeExact + ep.Domain = domain + return ep.parsePPP(ep, fields) + } + } + + return nil, nil +} diff --git a/profile/endpoints/endpoint-ip.go b/profile/endpoints/endpoint-ip.go new file mode 100644 index 00000000..6fa742c0 --- /dev/null +++ b/profile/endpoints/endpoint-ip.go @@ -0,0 +1,42 @@ +package endpoints + +import ( + "net" + + "github.com/safing/portmaster/intel" +) + +// EndpointIP matches IPs. +type EndpointIP struct { + EndpointBase + + IP net.IP + Reason string +} + +// Matches checks whether the given entity matches this endpoint definition. +func (ep *EndpointIP) Matches(entity *intel.Entity) (result EPResult, reason string) { + if entity.IP == nil { + return Undeterminable, "" + } + if ep.IP.Equal(entity.IP) { + return ep.matchesPPP(entity), ep.Reason + } + return NoMatch, "" +} + +func (ep *EndpointIP) String() string { + return ep.renderPPP(ep.IP.String()) +} + +func parseTypeIP(fields []string) (Endpoint, error) { + ip := net.ParseIP(fields[1]) + if ip != nil { + ep := &EndpointIP{ + IP: ip, + Reason: "IP is " + ip.String(), + } + return ep.parsePPP(ep, fields) + } + return nil, nil +} diff --git a/profile/endpoints/endpoint-iprange.go b/profile/endpoints/endpoint-iprange.go new file mode 100644 index 00000000..16afa4bb --- /dev/null +++ b/profile/endpoints/endpoint-iprange.go @@ -0,0 +1,42 @@ +package endpoints + +import ( + "net" + + "github.com/safing/portmaster/intel" +) + +// EndpointIPRange matches IP ranges. +type EndpointIPRange struct { + EndpointBase + + Net *net.IPNet + Reason string +} + +// Matches checks whether the given entity matches this endpoint definition. +func (ep *EndpointIPRange) Matches(entity *intel.Entity) (result EPResult, reason string) { + if entity.IP == nil { + return Undeterminable, "" + } + if ep.Net.Contains(entity.IP) { + return ep.matchesPPP(entity), ep.Reason + } + return NoMatch, "" +} + +func (ep *EndpointIPRange) String() string { + return ep.renderPPP(ep.Net.String()) +} + +func parseTypeIPRange(fields []string) (Endpoint, error) { + _, net, err := net.ParseCIDR(fields[1]) + if err == nil { + ep := &EndpointIPRange{ + Net: net, + Reason: "IP is part of " + net.String(), + } + return ep.parsePPP(ep, fields) + } + return nil, nil +} diff --git a/profile/endpoints/endpoint-lists.go b/profile/endpoints/endpoint-lists.go new file mode 100644 index 00000000..fe151347 --- /dev/null +++ b/profile/endpoints/endpoint-lists.go @@ -0,0 +1,46 @@ +package endpoints + +import ( + "strings" + + "github.com/safing/portmaster/intel" +) + +// EndpointLists matches endpoint lists. +type EndpointLists struct { + EndpointBase + + ListSet *intel.ListSet + Lists string + Reason string +} + +// Matches checks whether the given entity matches this endpoint definition. +func (ep *EndpointLists) Matches(entity *intel.Entity) (result EPResult, reason string) { + lists, ok := entity.GetLists() + if !ok { + return Undeterminable, "" + } + matched := ep.ListSet.MatchSet(lists) + if len(matched) > 0 { + return ep.matchesPPP(entity), ep.Reason + } + return NoMatch, "" +} + +func (ep *EndpointLists) String() string { + return ep.renderPPP(ep.Lists) +} + +func parseTypeList(fields []string) (Endpoint, error) { + if strings.HasPrefix(fields[1], "L:") { + lists := strings.Split(strings.TrimPrefix(fields[1], "L:"), ",") + ep := &EndpointLists{ + ListSet: intel.NewListSet(lists), + Lists: "L:" + strings.Join(lists, ","), + Reason: "matched lists " + strings.Join(lists, ","), + } + return ep.parsePPP(ep, fields) + } + return nil, nil +} diff --git a/profile/endpoints/endpoint.go b/profile/endpoints/endpoint.go new file mode 100644 index 00000000..687834cd --- /dev/null +++ b/profile/endpoints/endpoint.go @@ -0,0 +1,211 @@ +package endpoints + +import ( + "fmt" + "strconv" + "strings" + + "github.com/safing/portmaster/intel" + "github.com/safing/portmaster/network/reference" +) + +// Endpoint describes an Endpoint Matcher +type Endpoint interface { + Matches(entity *intel.Entity) (result EPResult, reason string) + String() string +} + +// EndpointBase provides general functions for implementing an Endpoint to reduce boilerplate. +type EndpointBase struct { //nolint:maligned // TODO + Protocol uint8 + StartPort uint16 + EndPort uint16 + + Permitted bool +} + +func (ep *EndpointBase) matchesPPP(entity *intel.Entity) (result EPResult) { + // only check if protocol is defined + if ep.Protocol > 0 { + // if protocol is unknown, return Undeterminable + if entity.Protocol == 0 { + return Undeterminable + } + // if protocol does not match, return NoMatch + if entity.Protocol != ep.Protocol { + return NoMatch + } + } + + // only check if port is defined + if ep.StartPort > 0 { + // if port is unknown, return Undeterminable + if entity.Port == 0 { + return Undeterminable + } + // if port does not match, return NoMatch + if entity.Port < ep.StartPort || entity.Port > ep.EndPort { + return NoMatch + } + } + + // protocol and port matched or were defined as any + if ep.Permitted { + return Permitted + } + return Denied +} + +func (ep *EndpointBase) renderPPP(s string) string { + var rendered string + if ep.Permitted { + rendered = "+ " + s + } else { + rendered = "- " + s + } + + if ep.Protocol > 0 || ep.StartPort > 0 { + if ep.Protocol > 0 { + rendered += " " + reference.GetProtocolName(ep.Protocol) + } else { + rendered += " *" + } + + if ep.StartPort > 0 { + if ep.StartPort == ep.EndPort { + rendered += "/" + reference.GetPortName(ep.StartPort) + } else { + rendered += "/" + strconv.Itoa(int(ep.StartPort)) + "-" + strconv.Itoa(int(ep.EndPort)) + } + } + } + + return rendered +} + +func (ep *EndpointBase) parsePPP(typedEp Endpoint, fields []string) (Endpoint, error) { //nolint:gocognit // TODO + switch len(fields) { + case 2: + // nothing else to do here + case 3: + // parse protocol and port(s) + var ok bool + splitted := strings.Split(fields[2], "/") + if len(splitted) > 2 { + return nil, invalidDefinitionError(fields, "protocol and port must be in format /") + } + // protocol + switch splitted[0] { + case "": + return nil, invalidDefinitionError(fields, "protocol can't be empty") + case "*": + // any protocol that supports ports + default: + n, err := strconv.ParseUint(splitted[0], 10, 8) + n8 := uint8(n) + if err != nil { + // maybe it's a name? + n8, ok = reference.GetProtocolNumber(splitted[0]) + if !ok { + return nil, invalidDefinitionError(fields, "protocol number parsing error") + } + } + ep.Protocol = n8 + } + // port(s) + if len(splitted) > 1 { + switch splitted[1] { + case "", "*": + return nil, invalidDefinitionError(fields, "omit port if should match any") + default: + portSplitted := strings.Split(splitted[1], "-") + if len(portSplitted) > 2 { + return nil, invalidDefinitionError(fields, "ports must be in format from-to") + } + // parse start port + n, err := strconv.ParseUint(portSplitted[0], 10, 16) + n16 := uint16(n) + if err != nil { + // maybe it's a name? + n16, ok = reference.GetPortNumber(portSplitted[0]) + if !ok { + return nil, invalidDefinitionError(fields, "port number parsing error") + } + } + ep.StartPort = n16 + // parse end port + if len(portSplitted) > 1 { + n, err = strconv.ParseUint(portSplitted[1], 10, 16) + n16 = uint16(n) + if err != nil { + // maybe it's a name? + n16, ok = reference.GetPortNumber(portSplitted[1]) + if !ok { + return nil, invalidDefinitionError(fields, "port number parsing error") + } + } + } + ep.EndPort = n16 + } + } + // check if anything was parsed + if ep.Protocol == 0 && ep.StartPort == 0 { + return nil, invalidDefinitionError(fields, "omit protocol/port if should match any") + } + default: + return nil, invalidDefinitionError(fields, "there should be only 2 or 3 segments") + } + + switch fields[0] { + case "+": + ep.Permitted = true + case "-": + ep.Permitted = false + default: + return nil, invalidDefinitionError(fields, "invalid permission prefix") + } + + return typedEp, nil +} + +func invalidDefinitionError(fields []string, msg string) error { + return fmt.Errorf(`invalid endpoint definition: "%s" - %s`, strings.Join(fields, " "), msg) +} + +func parseEndpoint(value string) (endpoint Endpoint, err error) { + fields := strings.Fields(value) + if len(fields) < 2 { + return nil, fmt.Errorf(`invalid endpoint definition: "%s"`, value) + } + + // any + if endpoint, err = parseTypeAny(fields); endpoint != nil || err != nil { + return + } + // ip + if endpoint, err = parseTypeIP(fields); endpoint != nil || err != nil { + return + } + // ip range + if endpoint, err = parseTypeIPRange(fields); endpoint != nil || err != nil { + return + } + // domain + if endpoint, err = parseTypeDomain(fields); endpoint != nil || err != nil { + return + } + // country + if endpoint, err = parseTypeCountry(fields); endpoint != nil || err != nil { + return + } + // asn + if endpoint, err = parseTypeASN(fields); endpoint != nil || err != nil { + return + } + // lists + if endpoint, err = parseTypeList(fields); endpoint != nil || err != nil { + return + } + + return nil, fmt.Errorf(`unknown endpoint definition: "%s"`, value) +} diff --git a/profile/endpoints/endpoint_test.go b/profile/endpoints/endpoint_test.go new file mode 100644 index 00000000..d8aabee8 --- /dev/null +++ b/profile/endpoints/endpoint_test.go @@ -0,0 +1,83 @@ +package endpoints + +import ( + "strings" + "testing" +) + +func TestEndpointParsing(t *testing.T) { + // any (basics) + testParsing(t, "- *") + testParsing(t, "+ *") + + // domain + testDomainParsing(t, "- *bad*", domainMatchTypeContains, "bad") + testDomainParsing(t, "- bad*", domainMatchTypePrefix, "bad") + testDomainParsing(t, "- *bad.com", domainMatchTypeSuffix, "bad.com.") + testDomainParsing(t, "- .bad.com", domainMatchTypeZone, "bad.com.") + testDomainParsing(t, "- bad.com", domainMatchTypeExact, "bad.com.") + testDomainParsing(t, "- www.bad.com.", domainMatchTypeExact, "www.bad.com.") + testDomainParsing(t, "- www.bad.com", domainMatchTypeExact, "www.bad.com.") + + // ip + testParsing(t, "+ 127.0.0.1") + testParsing(t, "+ 192.168.0.1") + testParsing(t, "+ ::1") + testParsing(t, "+ 2606:4700:4700::1111") + + // ip + testParsing(t, "+ 127.0.0.0/8") + testParsing(t, "+ 192.168.0.0/24") + testParsing(t, "+ 2606:4700:4700::/48") + + // country + testParsing(t, "+ DE") + testParsing(t, "+ AT") + testParsing(t, "+ CH") + testParsing(t, "+ AS") + + // asn + testParsing(t, "+ AS1") + testParsing(t, "+ AS12") + testParsing(t, "+ AS123") + testParsing(t, "+ AS1234") + testParsing(t, "+ AS12345") + + // protocol and ports + testParsing(t, "+ * TCP/1-1024") + testParsing(t, "+ * */DNS") + testParsing(t, "+ * ICMP") + testParsing(t, "+ * 127") + testParsing(t, "+ * UDP/1234") + testParsing(t, "+ * TCP/HTTP") + testParsing(t, "+ * TCP/80-443") +} + +func testParsing(t *testing.T, value string) { + ep, err := parseEndpoint(value) + if err != nil { + t.Error(err) + return + } + if value != ep.String() { + t.Errorf(`stringified endpoint mismatch: original was "%s", parsed is "%s"`, value, ep.String()) + } +} + +func testDomainParsing(t *testing.T, value string, matchType uint8, matchValue string) { + testParsing(t, value) + + epGeneric, err := parseTypeDomain(strings.Fields(value)) + if err != nil { + t.Error(err) + return + } + ep := epGeneric.(*EndpointDomain) + + if ep.MatchType != matchType { + t.Errorf(`error parsing domain endpoint "%s": match type should be %d, was %d`, value, matchType, ep.MatchType) + } + if ep.Domain != matchValue { + t.Errorf(`error parsing domain endpoint "%s": match domain value should be %s, was %s`, value, matchValue, ep.Domain) + } +} diff --git a/profile/endpoints/endpoints.go b/profile/endpoints/endpoints.go new file mode 100644 index 00000000..7a49a0c8 --- /dev/null +++ b/profile/endpoints/endpoints.go @@ -0,0 +1,93 @@ +package endpoints + +import ( + "fmt" + "strings" + + "github.com/safing/portmaster/intel" +) + +// Endpoints is a list of permitted or denied endpoints. +type Endpoints []Endpoint + +// EPResult represents the result of a check against an EndpointPermission +type EPResult uint8 + +// Endpoint matching return values +const ( + NoMatch EPResult = iota + Undeterminable + Denied + Permitted +) + +// ParseEndpoints parses a list of endpoints and returns a list of Endpoints for matching. +func ParseEndpoints(entries []string) (Endpoints, error) { + var firstErr error + var errCnt int + endpoints := make(Endpoints, 0, len(entries)) + +entriesLoop: + for _, entry := range entries { + ep, err := parseEndpoint(entry) + if err != nil { + errCnt++ + if firstErr == nil { + firstErr = err + } + continue entriesLoop + } + + endpoints = append(endpoints, ep) + } + + if firstErr != nil { + if errCnt > 0 { + return endpoints, fmt.Errorf("encountered %d errors, first was: %s", errCnt, firstErr) + } + return endpoints, firstErr + } + + return endpoints, nil +} + +// IsSet returns whether the Endpoints object is "set". +func (e Endpoints) IsSet() bool { + return len(e) > 0 +} + +// Match checks whether the given entity matches any of the endpoint definitions in the list. +func (e Endpoints) Match(entity *intel.Entity) (result EPResult, reason string) { + for _, entry := range e { + if entry != nil { + if result, reason = entry.Matches(entity); result != NoMatch { + return + } + } + } + + return NoMatch, "" +} + +func (e Endpoints) String() string { + s := make([]string, 0, len(e)) + for _, entry := range e { + s = append(s, entry.String()) + } + return fmt.Sprintf("[%s]", strings.Join(s, ", ")) +} + +func (epr EPResult) String() string { + switch epr { + case NoMatch: + return "No Match" + case Undeterminable: + return "Undeterminable" + case Denied: + return "Denied" + case Permitted: + return "Permitted" + default: + return "Unknown" + } +} diff --git a/profile/endpoints/endpoints_test.go b/profile/endpoints/endpoints_test.go new file mode 100644 index 00000000..147e6bf4 --- /dev/null +++ b/profile/endpoints/endpoints_test.go @@ -0,0 +1,353 @@ +package endpoints + +import ( + "net" + "runtime" + "testing" + + "github.com/safing/portmaster/core/pmtesting" + "github.com/safing/portmaster/intel" +) + +func TestMain(m *testing.M) { + pmtesting.TestMain(m) +} + +func testEndpointMatch(t *testing.T, ep Endpoint, entity *intel.Entity, expectedResult EPResult) { + result, _ := ep.Matches(entity) + if result != expectedResult { + t.Errorf( + "line %d: unexpected result for endpoint %s and entity %+v: result=%s, expected=%s", + getLineNumberOfCaller(1), + ep, + entity, + result, + expectedResult, + ) + } +} + +func TestEndpointMatching(t *testing.T) { + // ANY + + ep, err := parseEndpoint("+ *") + if err != nil { + t.Fatal(err) + } + + testEndpointMatch(t, ep, (&intel.Entity{ + Domain: "example.com.", + }).Init(), Permitted) + testEndpointMatch(t, ep, (&intel.Entity{ + Domain: "example.com.", + IP: net.ParseIP("10.2.3.4"), + Protocol: 6, + Port: 443, + }).Init(), Permitted) + + // DOMAIN + + // wildcard domains + ep, err = parseEndpoint("+ *example.com") + if err != nil { + t.Fatal(err) + } + + testEndpointMatch(t, ep, (&intel.Entity{ + Domain: "example.com.", + }).Init(), Permitted) + testEndpointMatch(t, ep, (&intel.Entity{ + Domain: "abc.example.com.", + }).Init(), Permitted) + testEndpointMatch(t, ep, (&intel.Entity{ + Domain: "abc-example.com.", + }).Init(), Permitted) + testEndpointMatch(t, ep, (&intel.Entity{ + Domain: "example.com.", + IP: net.ParseIP("10.2.3.4"), + Protocol: 6, + Port: 443, + }).Init(), Permitted) + testEndpointMatch(t, ep, (&intel.Entity{ + Domain: "abc.example.com.", + IP: net.ParseIP("10.2.3.4"), + Protocol: 6, + Port: 443, + }).Init(), Permitted) + testEndpointMatch(t, ep, (&intel.Entity{ + Domain: "abc-example.com.", + IP: net.ParseIP("10.2.3.4"), + Protocol: 6, + Port: 443, + }).Init(), Permitted) + + ep, err = parseEndpoint("+ *.example.com") + if err != nil { + t.Fatal(err) + } + + testEndpointMatch(t, ep, (&intel.Entity{ + Domain: "example.com.", + }).Init(), NoMatch) + testEndpointMatch(t, ep, (&intel.Entity{ + Domain: "abc.example.com.", + }).Init(), Permitted) + testEndpointMatch(t, ep, (&intel.Entity{ + Domain: "abc-example.com.", + }).Init(), NoMatch) + testEndpointMatch(t, ep, (&intel.Entity{ + Domain: "example.com.", + IP: net.ParseIP("10.2.3.4"), + Protocol: 6, + Port: 443, + }).Init(), NoMatch) + testEndpointMatch(t, ep, (&intel.Entity{ + Domain: "abc.example.com.", + IP: net.ParseIP("10.2.3.4"), + Protocol: 6, + Port: 443, + }).Init(), Permitted) + testEndpointMatch(t, ep, (&intel.Entity{ + Domain: "abc-example.com.", + IP: net.ParseIP("10.2.3.4"), + Protocol: 6, + Port: 443, + }).Init(), NoMatch) + + ep, err = parseEndpoint("+ .example.com") + if err != nil { + t.Fatal(err) + } + + testEndpointMatch(t, ep, (&intel.Entity{ + Domain: "example.com.", + }).Init(), Permitted) + testEndpointMatch(t, ep, (&intel.Entity{ + Domain: "abc.example.com.", + }).Init(), Permitted) + testEndpointMatch(t, ep, (&intel.Entity{ + Domain: "abc-example.com.", + }).Init(), NoMatch) + testEndpointMatch(t, ep, (&intel.Entity{ + Domain: "example.com.", + IP: net.ParseIP("10.2.3.4"), + Protocol: 6, + Port: 443, + }).Init(), Permitted) + testEndpointMatch(t, ep, (&intel.Entity{ + Domain: "abc.example.com.", + IP: net.ParseIP("10.2.3.4"), + Protocol: 6, + Port: 443, + }).Init(), Permitted) + testEndpointMatch(t, ep, (&intel.Entity{ + Domain: "abc-example.com.", + IP: net.ParseIP("10.2.3.4"), + Protocol: 6, + Port: 443, + }).Init(), NoMatch) + + ep, err = parseEndpoint("+ example.*") + if err != nil { + t.Fatal(err) + } + + testEndpointMatch(t, ep, (&intel.Entity{ + Domain: "example.com.", + }).Init(), Permitted) + testEndpointMatch(t, ep, (&intel.Entity{ + Domain: "abc.example.com.", + }).Init(), NoMatch) + testEndpointMatch(t, ep, (&intel.Entity{ + Domain: "example.com.", + IP: net.ParseIP("10.2.3.4"), + Protocol: 6, + Port: 443, + }).Init(), Permitted) + testEndpointMatch(t, ep, (&intel.Entity{ + Domain: "abc.example.com.", + IP: net.ParseIP("10.2.3.4"), + Protocol: 6, + Port: 443, + }).Init(), NoMatch) + + ep, err = parseEndpoint("+ *.exampl*") + if err != nil { + t.Fatal(err) + } + + testEndpointMatch(t, ep, (&intel.Entity{ + Domain: "example.com.", + }).Init(), NoMatch) + testEndpointMatch(t, ep, (&intel.Entity{ + Domain: "abc.example.com.", + }).Init(), Permitted) + testEndpointMatch(t, ep, (&intel.Entity{ + Domain: "example.com.", + IP: net.ParseIP("10.2.3.4"), + Protocol: 6, + Port: 443, + }).Init(), NoMatch) + testEndpointMatch(t, ep, (&intel.Entity{ + Domain: "abc.example.com.", + IP: net.ParseIP("10.2.3.4"), + Protocol: 6, + Port: 443, + }).Init(), Permitted) + + ep, err = parseEndpoint("+ *.com.") + if err != nil { + t.Fatal(err) + } + + testEndpointMatch(t, ep, (&intel.Entity{ + Domain: "example.com.", + }).Init(), Permitted) + testEndpointMatch(t, ep, (&intel.Entity{ + Domain: "example.org.", + }).Init(), NoMatch) + + // protocol + + ep, err = parseEndpoint("+ example.com UDP") + if err != nil { + t.Fatal(err) + } + + testEndpointMatch(t, ep, (&intel.Entity{ + Domain: "example.com.", + IP: net.ParseIP("10.2.3.4"), + Protocol: 6, + Port: 443, + }).Init(), NoMatch) + testEndpointMatch(t, ep, (&intel.Entity{ + Domain: "example.com.", + IP: net.ParseIP("10.2.3.4"), + Protocol: 17, + Port: 443, + }).Init(), Permitted) + testEndpointMatch(t, ep, (&intel.Entity{ + Domain: "example.com.", + }).Init(), Undeterminable) + + // ports + + ep, err = parseEndpoint("+ example.com 17/442-444") + if err != nil { + t.Fatal(err) + } + + entity := (&intel.Entity{ + Domain: "example.com.", + IP: net.ParseIP("10.2.3.4"), + Protocol: 17, + Port: 441, + }).Init() + testEndpointMatch(t, ep, entity, NoMatch) + + entity.Port = 442 + testEndpointMatch(t, ep, entity, Permitted) + + entity.Port = 443 + testEndpointMatch(t, ep, entity, Permitted) + + entity.Port = 444 + testEndpointMatch(t, ep, entity, Permitted) + + entity.Port = 445 + testEndpointMatch(t, ep, entity, NoMatch) + + testEndpointMatch(t, ep, (&intel.Entity{ + Domain: "example.com.", + }).Init(), Undeterminable) + + // IP + + ep, err = parseEndpoint("+ 10.2.3.4") + if err != nil { + t.Fatal(err) + } + + testEndpointMatch(t, ep, (&intel.Entity{ + Domain: "", + IP: net.ParseIP("10.2.3.4"), + Protocol: 6, + Port: 443, + }).Init(), Permitted) + testEndpointMatch(t, ep, (&intel.Entity{ + Domain: "example.com.", + IP: net.ParseIP("10.2.3.4"), + Protocol: 17, + Port: 443, + }).Init(), Permitted) + + testEndpointMatch(t, ep, (&intel.Entity{ + Domain: "", + IP: net.ParseIP("10.2.3.3"), + Protocol: 6, + Port: 443, + }).Init(), NoMatch) + testEndpointMatch(t, ep, (&intel.Entity{ + Domain: "example.com.", + IP: net.ParseIP("10.2.3.5"), + Protocol: 17, + Port: 443, + }).Init(), NoMatch) + + testEndpointMatch(t, ep, (&intel.Entity{ + Domain: "example.com.", + }).Init(), Undeterminable) + + // IP Range + + ep, err = parseEndpoint("+ 10.2.3.0/24") + if err != nil { + t.Fatal(err) + } + testEndpointMatch(t, ep, (&intel.Entity{ + IP: net.ParseIP("10.2.2.4"), + }).Init(), NoMatch) + testEndpointMatch(t, ep, (&intel.Entity{ + IP: net.ParseIP("10.2.3.4"), + }).Init(), Permitted) + testEndpointMatch(t, ep, (&intel.Entity{ + IP: net.ParseIP("10.2.4.4"), + }).Init(), NoMatch) + + // ASN + + ep, err = parseEndpoint("+ AS13335") + if err != nil { + t.Fatal(err) + } + + testEndpointMatch(t, ep, (&intel.Entity{ + IP: net.ParseIP("1.1.1.1"), + }).Init(), Permitted) + testEndpointMatch(t, ep, (&intel.Entity{ + IP: net.ParseIP("8.8.8.8"), + }).Init(), NoMatch) + + // Country + + ep, err = parseEndpoint("+ AT") + if err != nil { + t.Fatal(err) + } + + testEndpointMatch(t, ep, (&intel.Entity{ + IP: net.ParseIP("194.232.104.1"), // orf.at + }).Init(), Permitted) + testEndpointMatch(t, ep, (&intel.Entity{ + IP: net.ParseIP("151.101.1.164"), // nytimes.com + }).Init(), NoMatch) + + // Lists + // TODO: write test for lists matcher + +} + +func getLineNumberOfCaller(levels int) int { + _, _, line, _ := runtime.Caller(levels + 1) //nolint:dogsled + return line +}