From 4c592619a38a9b128de49a0e43fd2152703b3463 Mon Sep 17 00:00:00 2001 From: Garfield Lee Freeman Date: Tue, 2 Apr 2024 16:59:03 +0200 Subject: [PATCH] add uuid service support --- policies/rules/security/entry.go | 750 ++++++++++++++++ policies/rules/security/interfaces.go | 7 + policies/rules/security/location.go | 134 +++ policies/rules/security/service.go | 1187 +++++++++++++++++++++++++ 4 files changed, 2078 insertions(+) create mode 100644 policies/rules/security/entry.go create mode 100644 policies/rules/security/interfaces.go create mode 100644 policies/rules/security/location.go create mode 100644 policies/rules/security/service.go diff --git a/policies/rules/security/entry.go b/policies/rules/security/entry.go new file mode 100644 index 0000000..2627960 --- /dev/null +++ b/policies/rules/security/entry.go @@ -0,0 +1,750 @@ +package security + +import ( + "encoding/xml" + "fmt" + "strings" + + "github.com/PaloAltoNetworks/pango/filtering" + "github.com/PaloAltoNetworks/pango/generic" + "github.com/PaloAltoNetworks/pango/util" + "github.com/PaloAltoNetworks/pango/version" +) + +var ( + _ filtering.Fielder = &Entry{} +) + +var ( + Suffix = []string{"security", "rules"} +) + +// Entry is the normalized object. +// +// Targets is a map where the key is the serial number of the target device and +// the value is a list of specific vsys on that device. The list of vsys is +// nil if all vsys on that device should be included or if the device is a +// virtual firewall (and thus only has vsys1). +type Entry struct { + Name string + Uuid string + SourceZones []string // unordered + DestinationZones []string // unordered + SourceAddresses []string // unordered + SourceUsers []string // unordered + DestinationAddresses []string // unordered + Services []string // unordered + Categories []string // unordered + Applications []string // unordered + SourceDevices []string + DestinationDevices []string + Schedule *string + Tags []string // ordered + NegateSource *bool + NegateDestination *bool + Disabled *bool + Description *string + GroupTag *string + Action string + IcmpUnreachable *bool + Type *string + DisableServerResponseInspection *bool + LogSetting *string + LogStart *bool + LogEnd *bool + ProfileSettings *ProfileSettingsObject + Qos *QosObject + Targets map[string][]string + NegateTarget *bool + DisableInspect *bool // 10.2.4, 10.2.0? : disable-inspect + + Misc map[string][]generic.Xml +} + +type ProfileSettingsObject struct { + Groups []string + Profiles *ProfilesObject +} + +type ProfilesObject struct { + UrlFilteringProfiles []string + DataFilteringProfiles []string + FileBlockingProfiles []string + WildfireAnalysisProfiles []string + AntiVirusProfiles []string + AntiSpywareProfiles []string + VulnerabilityProfiles []string +} + +type QosObject struct { + IpDscp *string + IpPrecedence *string + FollowClientToServerFlow any +} + +func (e *Entry) CopyMiscFrom(v *Entry) { + if v == nil || len(v.Misc) == 0 { + return + } + + e.Misc = make(map[string][]generic.Xml) + for key := range v.Misc { + e.Misc[key] = append([]generic.Xml(nil), v.Misc[key]...) + } +} + +func (e *Entry) Field(value string) (any, error) { + obj := e + v := value + + if v == "name" || v == "Name" { + return obj.Name, nil + } + if v == "uuid" || v == "Uuid" { + return obj.Uuid, nil + } + if v == "source_zones" || v == "SourceZones" { + return obj.SourceZones, nil + } + if v == "source_zones|LENGTH" || v == "SourceZones|LENGTH" { + return len(obj.SourceZones), nil + } + if v == "destination_zones" || v == "DestinationZones" { + return obj.DestinationZones, nil + } + if v == "destination_zones|LENGTH" || v == "DestinationZones|LENGTH" { + return len(obj.DestinationZones), nil + } + if v == "source_addresses" || v == "SourceAddresses" { + return obj.SourceAddresses, nil + } + if v == "source_addresses|LENGTH" || v == "SourceAddresses|LENGTH" { + return len(obj.SourceAddresses), nil + } + if v == "source_users" || v == "SourceUsers" { + return obj.SourceUsers, nil + } + if v == "source_users|LENGTH" || v == "SourceUsers|LENGTH" { + return len(obj.SourceUsers), nil + } + if v == "destination_addresses" || v == "DestinationAddresses" { + return obj.DestinationAddresses, nil + } + if v == "destination_addresses|LENGTH" || v == "DestinationAddresses|LENGTH" { + return len(obj.DestinationAddresses), nil + } + if v == "services" || v == "Services" { + return obj.Services, nil + } + if v == "services|LENGTH" || v == "Services|LENGTH" { + return len(obj.Services), nil + } + if v == "categories" || v == "Categories" { + return obj.Categories, nil + } + if v == "categories|LENGTH" || v == "Categories|LENGTH" { + return len(obj.Categories), nil + } + if v == "applications" || v == "Applications" { + return obj.Applications, nil + } + if v == "applications|LENGTH" || v == "Applications|LENGTH" { + return len(obj.Applications), nil + } + if v == "source_devices" || v == "SourceDevices" { + return obj.SourceDevices, nil + } + if v == "source_devices|LENGTH" || v == "SourceDevices|LENGTH" { + return len(obj.SourceDevices), nil + } + if v == "destination_devices" || v == "DestinationDevices" { + return obj.DestinationDevices, nil + } + if v == "destination_devices|LENGTH" || v == "DestinationDevices|LENGTH" { + return len(obj.DestinationDevices), nil + } + if v == "schedule" || v == "Schedule" { + return obj.Schedule, nil + } + if v == "tags" || v == "Tags" { + return obj.Tags, nil + } + if v == "tags|LENGTH" || v == "Tags|LENGTH" { + return len(obj.Tags), nil + } + if v == "negate_source" || v == "NegateSource" { + return obj.NegateSource, nil + } + if v == "negate_destination" || v == "NegateDestination" { + return obj.NegateDestination, nil + } + if v == "disabled" || v == "Disabled" { + return obj.Disabled, nil + } + if v == "description" || v == "Description" { + return obj.Description, nil + } + if v == "group_tag" || v == "GroupTag" { + return obj.GroupTag, nil + } + if v == "action" || v == "Action" { + return obj.Action, nil + } + if v == "icmp_unreachable" || v == "IcmpUnreachable" { + return obj.IcmpUnreachable, nil + } + if v == "type" || v == "Type" { + return obj.Type, nil + } + if v == "disable_server_response_inspection" || v == "DisableServerResponseInspection" { + return obj.DisableServerResponseInspection, nil + } + if v == "log_setting" || v == "LogSetting" { + return obj.LogSetting, nil + } + if v == "log_start" || v == "LogStart" { + return obj.LogStart, nil + } + if v == "log_end" || v == "LogEnd" { + return obj.LogEnd, nil + } + if v == "profile_settings" || v == "ProfileSettings" { + return obj.ProfileSettings != nil, nil + } + if strings.HasPrefix(v, "profile_settings.") || strings.HasPrefix(v, "ProfileSettings.") { + if obj.ProfileSettings == nil { + return nil, fmt.Errorf("profile_settings is nil") + } + obj := obj.ProfileSettings + for _, chk := range []string{"profile_settings.", "ProfileSettings."} { + if strings.HasPrefix(v, chk) { + v = v[len(chk):] + break + } + } + + if v == "groups" || v == "Groups" { + return obj.Groups, nil + } + if v == "groups|LENGTH" || v == "Groups|LENGTH" { + return len(obj.Groups), nil + } + if v == "profiles" || v == "Profiles" { + return obj.Profiles != nil, nil + } + if strings.HasPrefix(v, "profiles.") || strings.HasPrefix(v, "Profiles.") { + if obj.Profiles == nil { + return nil, fmt.Errorf("profiles is nil") + } + obj := obj.Profiles + for _, chk := range []string{"profiles.", "Profiles."} { + if strings.HasPrefix(v, chk) { + v = v[len(chk):] + break + } + } + + if v == "url_filtering_profiles" || v == "UrlFilteringProfiles" { + return obj.UrlFilteringProfiles, nil + } + if v == "url_filtering_profiles|LENGTH" || v == "UrlFilteringProfiles|LENGTH" { + return len(obj.UrlFilteringProfiles), nil + } + if v == "data_filtering_profiles" || v == "DataFilteringProfiles" { + return obj.DataFilteringProfiles, nil + } + if v == "data_filtering_profiles|LENGTH" || v == "DataFilteringProfiles|LENGTH" { + return len(obj.DataFilteringProfiles), nil + } + if v == "file_blocking_profiles" || v == "FileBlockingProfiles" { + return obj.FileBlockingProfiles, nil + } + if v == "file_blocking_profiles|LENGTH" || v == "FileBlockingProfiles|LENGTH" { + return len(obj.FileBlockingProfiles), nil + } + if v == "wildfire_analysis_profiles" || v == "WildfireAnalysisProfiles" { + return obj.WildfireAnalysisProfiles, nil + } + if v == "wildfire_analysis_profiles|LENGTH" || v == "WildfireAnalysisProfiles|LENGTH" { + return len(obj.WildfireAnalysisProfiles), nil + } + if v == "anti_virus_profiles" || v == "AntiVirusProfiles" { + return obj.AntiVirusProfiles, nil + } + if v == "anti_virus_profiles|LENGTH" || v == "AntiVirusProfiles|LENGTH" { + return len(obj.AntiVirusProfiles), nil + } + if v == "anti_spyware_profiles" || v == "AntiSpywareProfiles" { + return obj.AntiSpywareProfiles, nil + } + if v == "anti_spyware_profiles|LENGTH" || v == "AntiSpywareProfiles|LENGTH" { + return len(obj.AntiSpywareProfiles), nil + } + if v == "vulnerability_profiles" || v == "VulnerabilityProfiles" { + return obj.VulnerabilityProfiles, nil + } + if v == "vulnerability_profiles|LENGTH" || v == "VulnerabilityProfiles|LENGTH" { + return len(obj.VulnerabilityProfiles), nil + } + } + } + if v == "qos" || v == "Qos" { + return obj.Qos != nil, nil + } + if strings.HasPrefix(v, "qos.") || strings.HasPrefix(v, "Qos.") { + if obj.Qos == nil { + return nil, fmt.Errorf("qos is nil") + } + obj := obj.Qos + for _, chk := range []string{"qos.", "Qos."} { + if strings.HasPrefix(v, chk) { + v = v[len(chk):] + break + } + } + + if v == "ip_dscp" || v == "IpDscp" { + return obj.IpDscp, nil + } + if v == "ip_precedence" || v == "IpPrecedence" { + return obj.IpPrecedence, nil + } + if v == "follow_client_to_server_flow" || v == "FollowClientToServerFlow" { + return obj.FollowClientToServerFlow != nil, nil + } + } + if v == "negate_target" || v == "NegateTarget" { + return e.NegateTarget, nil + } + if v == "disable_inspect" || v == "DisableInspect" { + return e.DisableInspect, nil + } + + return nil, fmt.Errorf("unknown field: %s", value) +} + +func Versioning(vn version.Number) (Specifier, Normalizer, error) { + /* + if vn.Gte(version.Number{10, 2, 0, ""}) { + return Entry2Specify, &Entry2Container{}, nil + } + */ + + return Entry1Specify, &Entry1Container{}, nil +} + +func Entry1Specify(o Entry) (any, error) { + ans := Entry1{Misc: o.Misc["Entry"]} + ans.Name = o.Name + ans.Uuid = o.Uuid + ans.SourceZones = util.StrToMem(o.SourceZones) + ans.DestinationZones = util.StrToMem(o.DestinationZones) + ans.SourceAddresses = util.StrToMem(o.SourceAddresses) + ans.SourceUsers = util.StrToMem(o.SourceUsers) + ans.DestinationAddresses = util.StrToMem(o.DestinationAddresses) + ans.Services = util.StrToMem(o.Services) + ans.Categories = util.StrToMem(o.Categories) + ans.Applications = util.StrToMem(o.Applications) + ans.SourceDevices = util.StrToMem(o.SourceDevices) + ans.DestinationDevices = util.StrToMem(o.DestinationDevices) + ans.Schedule = o.Schedule + ans.Tags = util.StrToMem(o.Tags) + if o.NegateSource != nil { + *ans.NegateSource = util.YesNo(*o.NegateSource) + } + if o.NegateDestination != nil { + *ans.NegateDestination = util.YesNo(*o.NegateDestination) + } + if o.Disabled != nil { + *ans.Disabled = util.YesNo(*o.Disabled) + } + ans.Description = o.Description + ans.GroupTag = o.GroupTag + ans.Action = o.Action + if o.IcmpUnreachable != nil { + *ans.IcmpUnreachable = util.YesNo(*o.IcmpUnreachable) + } + ans.Type = o.Type + if o.DisableServerResponseInspection != nil { + ans.Options = &secOptions{Misc: o.Misc["secOptions"]} + *ans.Options.DisableServerResponseInspection = util.YesNo(*o.DisableServerResponseInspection) + } + ans.LogSetting = o.LogSetting + if o.LogStart != nil { + *ans.LogStart = util.YesNo(*o.LogStart) + } + if o.LogEnd != nil { + *ans.LogEnd = util.YesNo(*o.LogEnd) + } + if o.ProfileSettings != nil { + ans.ProfileSettings = &profileSettings{Misc: o.Misc["profileSettings"]} + ans.ProfileSettings.Groups = util.StrToMem(o.ProfileSettings.Groups) + if o.ProfileSettings.Profiles != nil { + ans.ProfileSettings.Profiles = &profileSettingsProfiles{Misc: o.Misc["profileSettingsProfiles"]} + ans.ProfileSettings.Profiles.UrlFilteringProfiles = util.StrToMem(o.ProfileSettings.Profiles.UrlFilteringProfiles) + ans.ProfileSettings.Profiles.DataFilteringProfiles = util.StrToMem(o.ProfileSettings.Profiles.DataFilteringProfiles) + ans.ProfileSettings.Profiles.FileBlockingProfiles = util.StrToMem(o.ProfileSettings.Profiles.FileBlockingProfiles) + ans.ProfileSettings.Profiles.WildfireAnalysisProfiles = util.StrToMem(o.ProfileSettings.Profiles.WildfireAnalysisProfiles) + ans.ProfileSettings.Profiles.AntiVirusProfiles = util.StrToMem(o.ProfileSettings.Profiles.AntiVirusProfiles) + ans.ProfileSettings.Profiles.AntiSpywareProfiles = util.StrToMem(o.ProfileSettings.Profiles.AntiSpywareProfiles) + ans.ProfileSettings.Profiles.VulnerabilityProfiles = util.StrToMem(o.ProfileSettings.Profiles.VulnerabilityProfiles) + } + } + if o.Qos != nil { + ans.Qos = &qos{Misc: o.Misc["qos"]} + ans.Qos.Marking = &qosMarking{Misc: o.Misc["qosMarking"]} + ans.Qos.Marking.IpDscp = o.Qos.IpDscp + ans.Qos.Marking.IpPrecedence = o.Qos.IpPrecedence + if o.Qos.FollowClientToServerFlow != nil { + ans.Qos.Marking.FollowClientToServerFlow = "" + } + } + + return ans, nil +} + +type Entry1Container struct { + Answer []Entry1 `xml:"entry"` +} + +func (c *Entry1Container) Normalize() ([]Entry, error) { + ans := make([]Entry, 0, len(c.Answer)) + for _, var0 := range c.Answer { + var1 := Entry{ + Misc: make(map[string][]generic.Xml), + } + var1.Misc["Entry"] = var0.Misc + var1.Name = var0.Name + var1.Uuid = var0.Uuid + var1.SourceZones = util.MemToStr(var0.SourceZones) + var1.SourceUsers = util.MemToStr(var0.SourceUsers) + var1.DestinationAddresses = util.MemToStr(var0.DestinationAddresses) + var1.Services = util.MemToStr(var0.Services) + var1.Categories = util.MemToStr(var0.Categories) + var1.Applications = util.MemToStr(var0.Applications) + var1.SourceDevices = util.MemToStr(var0.SourceDevices) + var1.DestinationDevices = util.MemToStr(var0.DestinationDevices) + var1.Schedule = var0.Schedule + var1.Tags = util.MemToStr(var0.Tags) + if var0.NegateSource != nil { + var5 := util.AsBool(*var0.NegateSource) + var1.NegateSource = &var5 + } + if var0.NegateDestination != nil { + var5 := util.AsBool(*var0.NegateDestination) + var1.NegateDestination = &var5 + } + if var0.Disabled != nil { + var5 := util.AsBool(*var0.Disabled) + var1.Disabled = &var5 + } + var1.Description = var0.Description + var1.GroupTag = var0.GroupTag + var1.Action = var0.Action + if var0.IcmpUnreachable != nil { + var5 := util.AsBool(*var0.IcmpUnreachable) + var1.IcmpUnreachable = &var5 + } + var1.Type = var0.Type + if var0.Options != nil { + var1.Misc["secOptions"] = var0.Options.Misc + if var0.Options.DisableServerResponseInspection != nil { + var5 := util.AsBool(*var0.Options.DisableServerResponseInspection) + var1.DisableServerResponseInspection = &var5 + } + } + var1.LogSetting = var0.LogSetting + if var0.LogStart != nil { + var5 := util.AsBool(*var0.LogStart) + var1.LogStart = &var5 + } + if var0.LogEnd != nil { + var5 := util.AsBool(*var0.LogEnd) + var1.LogEnd = &var5 + } + if var0.ProfileSettings != nil { + var1.ProfileSettings = &ProfileSettingsObject{} + var1.Misc["profileSettings"] = var0.ProfileSettings.Misc + var1.ProfileSettings.Groups = util.MemToStr(var0.ProfileSettings.Groups) + if var0.ProfileSettings.Profiles != nil { + var1.ProfileSettings.Profiles = &ProfilesObject{} + var1.Misc["profileSettingsProfiles"] = var0.ProfileSettings.Profiles.Misc + var1.ProfileSettings.Profiles.UrlFilteringProfiles = util.MemToStr(var0.ProfileSettings.Profiles.UrlFilteringProfiles) + var1.ProfileSettings.Profiles.DataFilteringProfiles = util.MemToStr(var0.ProfileSettings.Profiles.DataFilteringProfiles) + var1.ProfileSettings.Profiles.FileBlockingProfiles = util.MemToStr(var0.ProfileSettings.Profiles.FileBlockingProfiles) + var1.ProfileSettings.Profiles.WildfireAnalysisProfiles = util.MemToStr(var0.ProfileSettings.Profiles.WildfireAnalysisProfiles) + var1.ProfileSettings.Profiles.AntiVirusProfiles = util.MemToStr(var0.ProfileSettings.Profiles.AntiVirusProfiles) + var1.ProfileSettings.Profiles.AntiSpywareProfiles = util.MemToStr(var0.ProfileSettings.Profiles.AntiSpywareProfiles) + var1.ProfileSettings.Profiles.VulnerabilityProfiles = util.MemToStr(var0.ProfileSettings.Profiles.VulnerabilityProfiles) + } + } + // qos + if var0.Qos != nil { + var1.Misc["qos"] = var0.Qos.Misc + if var0.Qos.Marking != nil { + var1.Qos = &QosObject{} + var1.Misc["qosMarking"] = var0.Qos.Marking.Misc + var1.Qos.IpDscp = var0.Qos.Marking.IpDscp + var1.Qos.IpPrecedence = var0.Qos.Marking.IpPrecedence + var1.Qos.FollowClientToServerFlow = var0.Qos.Marking.FollowClientToServerFlow + } + } + + ans = append(ans, var1) + } + + return ans, nil +} + +type Entry1 struct { + XMLName xml.Name `xml:"entry"` + Name string `xml:"name,attr"` + Uuid string `xml:"uuid,attr,omitempty"` + SourceZones *util.MemberType `xml:"from"` + DestinationZones *util.MemberType `xml:"to"` + SourceAddresses *util.MemberType `xml:"source"` + SourceUsers *util.MemberType `xml:"source-user"` + DestinationAddresses *util.MemberType `xml:"destination"` + Services *util.MemberType `xml:"service"` + Categories *util.MemberType `xml:"category"` + Applications *util.MemberType `xml:"application"` + SourceDevices *util.MemberType `xml:"source-hip"` + DestinationDevices *util.MemberType `xml:"destination-hip"` + Schedule *string `xml:"schedule,omitempty"` + Tags *util.MemberType `xml:"tag"` + NegateSource *string `xml:"negate-source,omitempty"` // bool + NegateDestination *string `xml:"negate-destination,omitempty"` // bool + Disabled *string `xml:"disabled,omitempty"` // bool + Description *string `xml:"description,omitempty"` + GroupTag *string `xml:"group-tag,omitempty"` + Action string `xml:"action"` + IcmpUnreachable *string `xml:"icmp-unreachable,omitempty"` // bool + Type *string `xml:"rule-type,omitempty"` + Options *secOptions `xml:"option"` + LogSetting *string `xml:"log-setting,omitempty"` + LogStart *string `xml:"log-start,omitempty"` // bool + LogEnd *string `xml:"log-end,omitempty"` // bool, default is true + ProfileSettings *profileSettings `xml:"profile-setting,omitempty"` + Qos *qos `xml:"qos,omitempty"` + + Misc []generic.Xml `xml:",any"` +} + +func (e *Entry1) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + type local Entry1 + yes := util.YesNo(true) + ans := local{ + LogEnd: &yes, + } + if err := d.DecodeElement(&ans, &start); err != nil { + return err + } + *e = Entry1(ans) + return nil +} + +type secOptions struct { + DisableServerResponseInspection *string `xml:"disable-server-response-inspection,omitempty"` // bool + + Misc []generic.Xml `xml:",any"` +} + +type profileSettings struct { // One of. + Groups *util.MemberType `xml:"group,omitempty"` + Profiles *profileSettingsProfiles `xml:"profiles,omitempty"` + + Misc []generic.Xml `xml:",any"` +} + +type profileSettingsProfiles struct { + UrlFilteringProfiles *util.MemberType `xml:"url-filtering,omitempty"` + DataFilteringProfiles *util.MemberType `xml:"data-filtering,omitempty"` + FileBlockingProfiles *util.MemberType `xml:"file-blocking,omitempty"` + WildfireAnalysisProfiles *util.MemberType `xml:"wildfire-analysis,omitempty"` + AntiVirusProfiles *util.MemberType `xml:"virus,omitempty"` + AntiSpywareProfiles *util.MemberType `xml:"spyware,omitempty"` + VulnerabilityProfiles *util.MemberType `xml:"vulnerability,omitempty"` + + Misc []generic.Xml `xml:",any"` +} + +type qos struct { + Marking *qosMarking `xml:"marking,omitempty"` + + Misc []generic.Xml `xml:",any"` +} + +type qosMarking struct { // One of. + IpDscp *string `xml:"ip-dscp,omitempty"` + IpPrecedence *string `xml:"ip-precedence,omitempty"` + FollowClientToServerFlow any `xml:"follow-c2s-flow,omitempty"` // exist bool + + Misc []generic.Xml `xml:",any"` +} + +func SpecMatches(a, b *Entry) bool { + if a == nil && b == nil { + return true + } else if a == nil || b == nil { + return false + } + + // omit compare: name + // omit compare: uuid + + if !util.UnorderedListsMatch(a.SourceZones, b.SourceZones) { + return false + } + + if !util.UnorderedListsMatch(a.DestinationZones, b.DestinationZones) { + return false + } + + if !util.UnorderedListsMatch(a.SourceAddresses, b.SourceAddresses) { + return false + } + + if !util.UnorderedListsMatch(a.SourceUsers, b.SourceUsers) { + return false + } + + if !util.UnorderedListsMatch(a.DestinationAddresses, b.DestinationAddresses) { + return false + } + + if !util.UnorderedListsMatch(a.Services, b.Services) { + return false + } + + if !util.UnorderedListsMatch(a.Categories, b.Categories) { + return false + } + + if !util.UnorderedListsMatch(a.Applications, b.Applications) { + return false + } + + if !util.UnorderedListsMatch(a.SourceDevices, b.SourceDevices) { + return false + } + + if !util.UnorderedListsMatch(a.DestinationDevices, b.DestinationDevices) { + return false + } + + if !util.StringsMatch(a.Schedule, b.Schedule) { + return false + } + + if !util.OrderedListsMatch(a.Tags, b.Tags) { + return false + } + + if !util.BoolsMatch(a.NegateSource, b.NegateSource) { + return false + } + + if !util.BoolsMatch(a.NegateDestination, b.NegateDestination) { + return false + } + + if !util.BoolsMatch(a.Disabled, b.Disabled) { + return false + } + + if !util.StringsMatch(a.Description, b.Description) { + return false + } + + if !util.StringsMatch(a.GroupTag, b.GroupTag) { + return false + } + + if a.Action != b.Action { + return false + } + + if !util.BoolsMatch(a.IcmpUnreachable, b.IcmpUnreachable) { + return false + } + + if !util.StringsMatch(a.Type, b.Type) { + return false + } + + if !util.BoolsMatch(a.DisableServerResponseInspection, b.DisableServerResponseInspection) { + return false + } + + if !util.BoolsMatch(a.LogStart, b.LogStart) { + return false + } + + if !util.BoolsMatch(a.LogEnd, b.LogEnd) { + return false + } + + // profile settings + if a.ProfileSettings == nil && b.ProfileSettings == nil { + } else if a.ProfileSettings == nil || b.ProfileSettings == nil { + return false + } else { + if !util.OrderedListsMatch(a.ProfileSettings.Groups, b.ProfileSettings.Groups) { + return false + } + + if a.ProfileSettings.Profiles == nil && b.ProfileSettings.Profiles == nil { + } else if a.ProfileSettings.Profiles == nil || b.ProfileSettings.Profiles == nil { + return false + } else { + if !util.OrderedListsMatch(a.ProfileSettings.Profiles.UrlFilteringProfiles, b.ProfileSettings.Profiles.UrlFilteringProfiles) { + return false + } + + if !util.OrderedListsMatch(a.ProfileSettings.Profiles.DataFilteringProfiles, b.ProfileSettings.Profiles.DataFilteringProfiles) { + return false + } + + if !util.OrderedListsMatch(a.ProfileSettings.Profiles.FileBlockingProfiles, b.ProfileSettings.Profiles.FileBlockingProfiles) { + return false + } + + if !util.OrderedListsMatch(a.ProfileSettings.Profiles.WildfireAnalysisProfiles, b.ProfileSettings.Profiles.WildfireAnalysisProfiles) { + return false + } + + if !util.OrderedListsMatch(a.ProfileSettings.Profiles.AntiVirusProfiles, b.ProfileSettings.Profiles.AntiVirusProfiles) { + return false + } + + if !util.OrderedListsMatch(a.ProfileSettings.Profiles.AntiSpywareProfiles, b.ProfileSettings.Profiles.AntiSpywareProfiles) { + return false + } + + if !util.OrderedListsMatch(a.ProfileSettings.Profiles.VulnerabilityProfiles, b.ProfileSettings.Profiles.VulnerabilityProfiles) { + return false + } + } + } + + if a.Qos == nil && b.Qos == nil { + } else if a.Qos == nil || b.Qos == nil { + return false + } else { + if !util.StringsMatch(a.Qos.IpDscp, b.Qos.IpDscp) { + return false + } + if !util.StringsMatch(a.Qos.IpPrecedence, b.Qos.IpPrecedence) { + return false + } + if !util.AnysMatch(a.Qos.FollowClientToServerFlow, b.Qos.FollowClientToServerFlow) { + return false + } + } + + if !util.BoolsMatch(a.DisableInspect, b.DisableInspect) { + return false + } + + return true +} diff --git a/policies/rules/security/interfaces.go b/policies/rules/security/interfaces.go new file mode 100644 index 0000000..73d2fcf --- /dev/null +++ b/policies/rules/security/interfaces.go @@ -0,0 +1,7 @@ +package security + +type Specifier func(Entry) (any, error) + +type Normalizer interface { + Normalize() ([]Entry, error) +} diff --git a/policies/rules/security/location.go b/policies/rules/security/location.go new file mode 100644 index 0000000..eb7969a --- /dev/null +++ b/policies/rules/security/location.go @@ -0,0 +1,134 @@ +package security + +import ( + "fmt" + + "github.com/PaloAltoNetworks/pango/errors" + "github.com/PaloAltoNetworks/pango/util" + "github.com/PaloAltoNetworks/pango/version" +) + +type Location struct { + Vsys *VsysLocation `json:"vsys,omitempty"` + Shared *SharedLocation `json:"shared"` + DeviceGroup *DeviceGroupLocation `json:"device_group,omitempty"` +} + +func (o Location) IsValid() error { + count := 0 + + if o.Vsys != nil { + if o.Vsys.Name == "" { + return fmt.Errorf("vsys.name is unspecified") + } + if o.Vsys.NgfwDevice == "" { + return fmt.Errorf("vsys.ngfw_device is unspecified") + } + count++ + } + + if o.Shared != nil { + if o.Shared.Rulebase == "" { + return fmt.Errorf("shared.rulebase is unspecified") + } + count++ + } + + if o.DeviceGroup != nil { + if o.DeviceGroup.Name == "" { + return fmt.Errorf("device_group.name is unspecified") + } + if o.DeviceGroup.PanoramaDevice == "" { + return fmt.Errorf("device_group.panorama_device is unspecified") + } + if o.DeviceGroup.Rulebase == "" { + return fmt.Errorf("device_group.rulebase is unspecified") + } + count++ + } + + if count == 0 { + return fmt.Errorf("no path specified") + } + + if count > 1 { + return fmt.Errorf("multiple paths specified: only one should be specified") + } + + return nil +} + +func (o Location) Xpath(vn version.Number, name, uuid string) ([]string, error) { + var ans []string + + switch { + case o.Vsys != nil: + if o.Vsys.NgfwDevice == "" { + return nil, fmt.Errorf("NgfwDevice is unspecified") + } + if o.Vsys.Name == "" { + return nil, fmt.Errorf("Name is unspecified") + } + ans = []string{ + "config", + "devices", + util.AsEntryXpath([]string{o.Vsys.NgfwDevice}), + "vsys", + util.AsEntryXpath([]string{o.Vsys.Name}), + "rulebase", + } + case o.Shared != nil: + if o.Shared.Rulebase == "" { + return nil, fmt.Errorf("Rulebase is unspecified") + } + ans = []string{ + "config", + "shared", + o.Shared.Rulebase, + } + case o.DeviceGroup != nil: + if o.DeviceGroup.PanoramaDevice == "" { + return nil, fmt.Errorf("PanoramaDevice is unspecified") + } + if o.DeviceGroup.Name == "" { + return nil, fmt.Errorf("Name is unspecified") + } + if o.DeviceGroup.Rulebase == "" { + return nil, fmt.Errorf("Rulebase is unspecified") + } + ans = []string{ + "config", + "devices", + util.AsEntryXpath([]string{o.DeviceGroup.PanoramaDevice}), + "device-group", + util.AsEntryXpath([]string{o.DeviceGroup.Name}), + o.DeviceGroup.Rulebase, + } + default: + return nil, errors.NoLocationSpecifiedError + } + + ans = append(ans, Suffix...) + if uuid != "" { + ans = append(ans, util.AsUuidXpath(uuid)) + } else { + ans = append(ans, util.AsEntryXpath([]string{name})) + } + + return ans, nil +} + +type VsysLocation struct { + NgfwDevice string `json:"ngfw_device"` + Name string `json:"name"` +} + +type SharedLocation struct { + Rulebase string `json:"rulebase"` +} + +type DeviceGroupLocation struct { + PanoramaDevice string `json:"panorama_device"` + Rulebase string `json:"rulebase"` + Name string `json:"name"` +} diff --git a/policies/rules/security/service.go b/policies/rules/security/service.go new file mode 100644 index 0000000..9b23a34 --- /dev/null +++ b/policies/rules/security/service.go @@ -0,0 +1,1187 @@ +package security + +import ( + "context" + "fmt" + "log" + + "github.com/PaloAltoNetworks/pango/audit" + "github.com/PaloAltoNetworks/pango/errors" + "github.com/PaloAltoNetworks/pango/filtering" + "github.com/PaloAltoNetworks/pango/rule" + "github.com/PaloAltoNetworks/pango/util" + "github.com/PaloAltoNetworks/pango/xmlapi" +) + +type Service struct { + client util.PangoClient +} + +func NewService(client util.PangoClient) *Service { + return &Service{ + client: client, + } +} + +// Create creates the given config object. +func (s *Service) Create(ctx context.Context, loc Location, entry Entry) (*Entry, error) { + if entry.Name == "" { + return nil, errors.NameNotSpecifiedError + } + + vn := s.client.Versioning() + + // Get versioning stuff. + specifier, _, err := Versioning(vn) + if err != nil { + return nil, err + } + + // Get the xpath. + path, err := loc.Xpath(vn, entry.Name, "") + if err != nil { + return nil, err + } + + createSpec, err := specifier(entry) + if err != nil { + return nil, err + } + + cmd := &xmlapi.Config{ + Action: "set", + Xpath: util.AsXpath(path[:len(path)-1]), + Element: createSpec, + Target: s.client.GetTarget(), + } + + // Perform the set. + if _, _, err = s.client.Communicate(ctx, cmd, false, nil); err != nil { + return nil, err + } + + // Return the Read results. + return s.Read(ctx, loc, entry.Name, "get") +} + +// Read returns the given config object, using the specified action. +// +// Param action should be either "get" or "show". +func (s *Service) Read(ctx context.Context, loc Location, name, action string) (*Entry, error) { + if name == "" { + return nil, errors.NameNotSpecifiedError + } + + vn := s.client.Versioning() + _, normalizer, err := Versioning(vn) + if err != nil { + return nil, err + } + + path, err := loc.Xpath(vn, name, "") + if err != nil { + return nil, err + } + + cmd := &xmlapi.Config{ + Action: action, + Xpath: util.AsXpath(path), + Target: s.client.GetTarget(), + } + + if _, _, err = s.client.Communicate(ctx, cmd, true, normalizer); err != nil { + // action=show returns empty config like this + if err.Error() == "No such node" && action == "show" { + return nil, errors.ObjectNotFound() + } + return nil, err + } + + list, err := normalizer.Normalize() + if err != nil { + return nil, err + } else if len(list) != 1 { + return nil, fmt.Errorf("expected to %q 1 entry, got %d", action, len(list)) + } + + return &list[0], nil +} + +// ReadById returns the given config object for the given uuid, using the specified action. +// +// Param action should be either "get" or "show". +func (s *Service) ReadById(ctx context.Context, loc Location, uuid, action string) (*Entry, error) { + if uuid == "" { + return nil, errors.UuidNotSpecifiedError + } + + vn := s.client.Versioning() + _, normalizer, err := Versioning(vn) + if err != nil { + return nil, err + } + + path, err := loc.Xpath(vn, "", uuid) + if err != nil { + return nil, err + } + + cmd := &xmlapi.Config{ + Action: action, + Xpath: util.AsXpath(path), + Target: s.client.GetTarget(), + } + + if _, _, err = s.client.Communicate(ctx, cmd, true, normalizer); err != nil { + // action=show returns empty config like this + if err.Error() == "No such node" && action == "show" { + return nil, errors.ObjectNotFound() + } + return nil, err + } + + list, err := normalizer.Normalize() + if err != nil { + return nil, err + } else if len(list) != 1 { + return nil, fmt.Errorf("expected to %q 1 entry, got %d", action, len(list)) + } + + return &list[0], nil +} + +// ReadFromConfig returns the given config object from the loaded XML config. +// +// Requires that client.LoadPanosConfig() has been invoked. +func (s *Service) ReadFromConfig(ctx context.Context, loc Location, name string) (*Entry, error) { + if name == "" { + return nil, errors.NameNotSpecifiedError + } + + vn := s.client.Versioning() + _, normalizer, err := Versioning(vn) + if err != nil { + return nil, err + } + + path, err := loc.Xpath(vn, name, "") + if err != nil { + return nil, err + } + + if _, err = s.client.ReadFromConfig(ctx, path, true, normalizer); err != nil { + return nil, err + } + + list, err := normalizer.Normalize() + if err != nil { + return nil, err + } else if len(list) != 1 { + return nil, fmt.Errorf("expected to find 1 entry, got %d", len(list)) + } + + return &list[0], nil +} + +// ReadFromConfigById returns the given config object for the given UUID from the +// loaded XML config. +// +// Requires that client.LoadPanosConfig() has been invoked. +func (s *Service) ReadFromConfigById(ctx context.Context, loc Location, uuid string) (*Entry, error) { + if uuid == "" { + return nil, errors.NameNotSpecifiedError + } + + vn := s.client.Versioning() + _, normalizer, err := Versioning(vn) + if err != nil { + return nil, err + } + + path, err := loc.Xpath(vn, "", uuid) + if err != nil { + return nil, err + } + + if _, err = s.client.ReadFromConfig(ctx, path, true, normalizer); err != nil { + return nil, err + } + + list, err := normalizer.Normalize() + if err != nil { + return nil, err + } else if len(list) != 1 { + return nil, fmt.Errorf("expected to find 1 entry, got %d", len(list)) + } + + return &list[0], nil +} + +// Update updates the given config object, then returns the result. +func (s *Service) Update(ctx context.Context, loc Location, entry Entry, oldName string) (*Entry, error) { + if entry.Name == "" { + return nil, errors.NameNotSpecifiedError + } + + vn := s.client.Versioning() + updates := xmlapi.NewMultiConfig(2) + specifier, _, err := Versioning(vn) + if err != nil { + return nil, err + } + + // Get the old config. + var old *Entry + if oldName != "" && oldName != entry.Name { + // Action needed: rename. + path, err := loc.Xpath(vn, oldName, "") + if err != nil { + return nil, err + } + + old, err = s.Read(ctx, loc, oldName, "get") + + updates.Add(&xmlapi.Config{ + Action: "rename", + Xpath: util.AsXpath(path), + NewName: entry.Name, + Target: s.client.GetTarget(), + }) + } else { + old, err = s.Read(ctx, loc, entry.Name, "get") + } + if err != nil { + return nil, err + } + + if !SpecMatches(&entry, old) { + // Action needed: edit. + path, err := loc.Xpath(vn, entry.Name, "") + if err != nil { + return nil, err + } + + // Copy over the misc stuff. + entry.CopyMiscFrom(old) + + updateSpec, err := specifier(entry) + if err != nil { + return nil, err + } + + updates.Add(&xmlapi.Config{ + Action: "edit", + Xpath: util.AsXpath(path), + Element: updateSpec, + Target: s.client.GetTarget(), + }) + } + + // Do the updates we've built up. + if len(updates.Operations) != 0 { + if _, _, _, err = s.client.MultiConfig(ctx, updates, false, nil); err != nil { + return nil, err + } + } + + // Return the read results. + return s.Read(ctx, loc, entry.Name, "get") +} + +// UpdateById updates the given config object by uuid, then returns the result. +func (s *Service) UpdateById(ctx context.Context, loc Location, entry Entry, uuid string) (*Entry, error) { + if entry.Name == "" { + return nil, errors.NameNotSpecifiedError + } else if uuid == "" { + return nil, errors.UuidNotSpecifiedError + } + + vn := s.client.Versioning() + updates := xmlapi.NewMultiConfig(2) + specifier, _, err := Versioning(vn) + if err != nil { + return nil, err + } + + // Get the old config. + old, err := s.ReadById(ctx, loc, uuid, "get") + if err != nil { + return nil, err + } + + if old.Name != entry.Name { + // Action needed: rename. + path, err := loc.Xpath(vn, old.Name, "") + if err != nil { + return nil, err + } + + updates.Add(&xmlapi.Config{ + Action: "rename", + Xpath: util.AsXpath(path), + NewName: entry.Name, + Target: s.client.GetTarget(), + }) + } + + if !SpecMatches(&entry, old) { + // Action needed: edit. + path, err := loc.Xpath(vn, entry.Name, "") + if err != nil { + return nil, err + } + + // Copy over the misc stuff. + entry.CopyMiscFrom(old) + + updateSpec, err := specifier(entry) + if err != nil { + return nil, err + } + + updates.Add(&xmlapi.Config{ + Action: "edit", + Xpath: util.AsXpath(path), + Element: updateSpec, + Target: s.client.GetTarget(), + }) + } + + // Do the updates we've built up. + if len(updates.Operations) != 0 { + if _, _, _, err = s.client.MultiConfig(ctx, updates, false, nil); err != nil { + return nil, err + } + } + + // Return the read results. + return s.Read(ctx, loc, entry.Name, "get") +} + +// Delete deletes the given item. +func (s *Service) Delete(ctx context.Context, loc Location, name string) error { + if name == "" { + return errors.NameNotSpecifiedError + } + + vn := s.client.Versioning() + + path, err := loc.Xpath(vn, name, "") + if err != nil { + return err + } + + cmd := &xmlapi.Config{ + Action: "delete", + Xpath: util.AsXpath(path), + Target: s.client.GetTarget(), + } + + _, _, err = s.client.Communicate(ctx, cmd, false, nil) + + return err +} + +// DeleteById deletes the given item using the uuid. +func (s *Service) DeleteById(ctx context.Context, loc Location, uuid string) error { + if uuid == "" { + return errors.UuidNotSpecifiedError + } + + vn := s.client.Versioning() + + path, err := loc.Xpath(vn, "", uuid) + if err != nil { + return err + } + + cmd := &xmlapi.Config{ + Action: "delete", + Xpath: util.AsXpath(path), + Target: s.client.GetTarget(), + } + + _, _, err = s.client.Communicate(ctx, cmd, false, nil) + + return err +} + +// List returns a list of service objects using the given action. +// +// Param action should be either "get" or "show". +// +// Params filter and quote are for client side filtering. +func (s *Service) List(ctx context.Context, loc Location, action, filter, quote string) ([]Entry, error) { + var err error + + var logic *filtering.Group + if filter != "" { + logic, err = filtering.Parse(filter, quote) + if err != nil { + return nil, err + } + } + + vn := s.client.Versioning() + + _, normalizer, err := Versioning(vn) + if err != nil { + return nil, err + } + + path, err := loc.Xpath(vn, "", "") + if err != nil { + return nil, err + } + + cmd := &xmlapi.Config{ + Action: action, + Xpath: util.AsXpath(path), + Target: s.client.GetTarget(), + } + + if _, _, err = s.client.Communicate(ctx, cmd, true, normalizer); err != nil { + // action=show returns empty config like this, it is not an error. + if err.Error() == "No such node" && action == "show" { + return nil, nil + } + return nil, err + } + + listing, err := normalizer.Normalize() + if err != nil || logic == nil { + return listing, err + } + + filtered := make([]Entry, 0, len(listing)) + for _, x := range listing { + ok, err := logic.Matches(&x) + if err != nil { + return nil, err + } + if ok { + filtered = append(filtered, x) + } + } + + return filtered, nil +} + +// ListFromConfig returns a list of objects at the given location. +// +// Requires that client.LoadPanosConfig() has been invoked. +// +// Params filter and quote are for client side filtering. +func (s *Service) ListFromConfig(ctx context.Context, loc Location, filter, quote string) ([]Entry, error) { + var err error + + var logic *filtering.Group + if filter != "" { + logic, err = filtering.Parse(filter, quote) + if err != nil { + return nil, err + } + } + + vn := s.client.Versioning() + + _, normalizer, err := Versioning(vn) + if err != nil { + return nil, err + } + + path, err := loc.Xpath(vn, "", "") + if err != nil { + return nil, err + } + path = path[:len(path)-1] + + if _, err = s.client.ReadFromConfig(ctx, path, false, normalizer); err != nil { + return nil, err + } + + listing, err := normalizer.Normalize() + if err != nil || logic == nil { + return listing, err + } + + filtered := make([]Entry, 0, len(listing)) + for _, x := range listing { + ok, err := logic.Matches(&x) + if err != nil { + return nil, err + } + if ok { + filtered = append(filtered, x) + } + } + + return filtered, nil +} + +// ConfigureGroup performs all necessary edit, rename, and delete commands to ensure that +// the objects are configured as specified. +// +// If removeEverythingElse is true, then any rule not in the entries param is deleted. +func (s *Service) ConfigureGroup(ctx context.Context, loc Location, position rule.Position, entries []Entry, uuids, auditComments map[string]string, removeEverythingElse bool) ([]Entry, map[string]string, bool, bool, error) { + if len(entries) == 0 { + return nil, nil, false, false, fmt.Errorf("no rules given") + } + + var err error + + listing, err := s.List(ctx, loc, "get", "", "") + if err != nil { + return nil, nil, false, false, err + } + + vn := s.client.Versioning() + updates := xmlapi.NewMultiConfig(len(entries) + len(listing)) + comments := make([]audit.SetComment, 0, len(entries)) + specifier, _, err := Versioning(vn) + if err != nil { + return nil, nil, false, false, err + } + + newOrRename := make([]Entry, 0, len(entries)) + uuidIsUsed := make(map[string]bool) + + // First pass on the rules given. + for _, entry := range entries { + var id string + for uuid, name := range uuids { + if name == entry.Name { + id = uuid + break + } + } + + // If the rule name does not have a previously managed UUID, then this is + // either a brand new rule or a renamed rule. In either case, save it for + // later. + if id == "" { + newOrRename = append(newOrRename, entry) + continue + } + + // There was a UUID, so see if it still exists. + for _, live := range listing { + if live.Uuid != id { + continue + } + + // The UUID is still there, so continue using it. + uuidIsUsed[id] = true + + // Check if the name needs to be updated due to out-of-band modification. + if live.Name != entry.Name { + path, err := loc.Xpath(vn, live.Name, "") + if err != nil { + return nil, nil, false, false, err + } + updates.Add(&xmlapi.Config{ + Action: "rename", + Xpath: util.AsXpath(path), + NewName: entry.Name, + Target: s.client.GetTarget(), + }) + } + + // Check if the rule spec matches or not. + if !SpecMatches(&entry, &live) { + path, err := loc.Xpath(vn, entry.Name, "") + if err != nil { + return nil, nil, false, false, err + } + + entry.CopyMiscFrom(&live) + elm, err := specifier(entry) + if err != nil { + return nil, nil, false, false, err + } + + updates.Add(&xmlapi.Config{ + Action: "edit", + Xpath: util.AsXpath(path), + Element: elm, + Target: s.client.GetTarget(), + }) + + if auditComments[entry.Name] != "" { + comments = append(comments, audit.SetComment{ + Xpath: util.AsXpath(path), + Comment: auditComments[entry.Name], + }) + } + } + + break + } + + if !uuidIsUsed[id] { + newOrRename = append(newOrRename, entry) + } + } + + // At this point, we only have new or renamed rules to deal with. + // A renamed rule will be a rule that exactly matches one and only one other + // rule, and that rule also has a uuid that is not currently being used. + for _, entry := range newOrRename { + matches := make([]Entry, 0, len(listing)) + for _, live := range listing { + if live.Uuid == "" || uuidIsUsed[live.Uuid] { + continue + } + + if SpecMatches(&entry, &live) { + matches = append(matches, live) + } + } + + if len(matches) == 1 && !uuidIsUsed[matches[0].Uuid] { + path, err := loc.Xpath(vn, matches[0].Name, "") + if err != nil { + return nil, nil, false, false, err + } + + updates.Add(&xmlapi.Config{ + Action: "rename", + Xpath: util.AsXpath(path), + NewName: entry.Name, + Target: s.client.GetTarget(), + }) + + uuidIsUsed[matches[0].Uuid] = true + } else { + // This is a new rule. + path, err := loc.Xpath(vn, entry.Name, "") + if err != nil { + return nil, nil, false, false, err + } + + elm, err := specifier(entry) + if err != nil { + return nil, nil, false, false, err + } + + updates.Add(&xmlapi.Config{ + Action: "edit", + Xpath: util.AsXpath(path), + Element: elm, + Target: s.client.GetTarget(), + }) + + if auditComments[entry.Name] != "" { + comments = append(comments, audit.SetComment{ + Xpath: util.AsXpath(path), + Comment: auditComments[entry.Name], + }) + } + } + } + + // Rule deletion. We're either deleting everything not specified or old + // rules that aren't in use anymore. + if removeEverythingElse { + for _, live := range listing { + if live.Uuid != "" && !uuidIsUsed[live.Uuid] { + path, err := loc.Xpath(vn, "", live.Uuid) + if err != nil { + return nil, nil, false, false, err + } + + updates.Add(&xmlapi.Config{ + Action: "delete", + Xpath: util.AsXpath(path), + Target: s.client.GetTarget(), + }) + } + } + } else { + // Delete unused security rules from the previous group. + for uuid := range uuids { + if !uuidIsUsed[uuid] { + path, err := loc.Xpath(vn, "", uuid) + if err != nil { + return nil, nil, false, false, err + } + + updates.Add(&xmlapi.Config{ + Action: "delete", + Xpath: util.AsXpath(path), + Target: s.client.GetTarget(), + }) + } + } + } + + // It's possible the rules were fine, so don't run an empty multi-config. + if len(updates.Operations) > 0 { + if false { + log.Printf("%d operations, doing multi-config", len(updates.Operations)) + } + if _, _, _, err = s.client.MultiConfig(ctx, updates, false, nil); err != nil { + return nil, nil, false, false, err + } + + // Update audit comments. + if false { + log.Printf("%d audit comments to be applied", len(comments)) + } + for _, ac := range comments { + cmd := &xmlapi.Op{ + Command: ac, + Target: s.client.GetTarget(), + } + if _, _, err = s.client.Communicate(ctx, cmd, false, nil); err != nil { + return nil, nil, false, false, err + } + } + + // Get an updated listing in preparation to move all rules into place. + listing, err = s.List(ctx, loc, "get", "", "") + if err != nil { + return nil, nil, false, false, err + } else if len(listing) == 0 { + return nil, nil, false, false, fmt.Errorf("no rules present before move operations") + } + } + + // Rule placement mapping. + rp := make(map[string]int) + for idx, live := range listing { + rp[live.Name] = idx + } + + // TODO: Now we need to verify positioning of the rules. + updates = xmlapi.NewMultiConfig(len(entries)) + + var ok, topDown bool + var otherIndex int + baseIndex := -1 + switch { + case position.First != nil && *position.First: + topDown = true + target := entries[0] + + baseIndex, ok = rp[target.Name] + if !ok { + return nil, nil, false, false, fmt.Errorf("could not find rule %q for first positioning", target.Name) + } + + if baseIndex != 0 { + path, err := loc.Xpath(vn, target.Name, "") + if err != nil { + return nil, nil, false, false, err + } + + for name, val := range rp { + switch { + case name == entries[0].Name: + rp[name] = 0 + case val < baseIndex: + rp[name] = val + 1 + } + } + + updates.Add(&xmlapi.Config{ + Action: "move", + Xpath: util.AsXpath(path), + Where: "top", + Target: s.client.GetTarget(), + }) + + baseIndex = 0 + } + case position.Last != nil && *position.Last: + target := entries[len(entries)-1] + + baseIndex, ok = rp[target.Name] + if !ok { + return nil, nil, false, false, fmt.Errorf("could not find rule %q for last positioning", target.Name) + } + + if baseIndex != len(listing)-1 { + path, err := loc.Xpath(vn, target.Name, "") + if err != nil { + return nil, nil, false, false, err + } + + for name, val := range rp { + switch { + case name == target.Name: + rp[name] = len(listing) - 1 + case val > baseIndex: + rp[name] = val - 1 + } + } + + updates.Add(&xmlapi.Config{ + Action: "move", + Xpath: util.AsXpath(path), + Where: "bottom", + Target: s.client.GetTarget(), + }) + + baseIndex = len(listing) - 1 + } + case position.SomewhereAfter != nil && *position.SomewhereAfter != "": + topDown = true + target := entries[0] + + baseIndex, ok = rp[target.Name] + if !ok { + return nil, nil, false, false, fmt.Errorf("could not find rule %q for initial positioning", target.Name) + } + + otherIndex, ok = rp[*position.SomewhereAfter] + if !ok { + return nil, nil, false, false, fmt.Errorf("could not find referenced rule %q for initial positioning", *position.SomewhereAfter) + } + + if baseIndex < otherIndex { + path, err := loc.Xpath(vn, target.Name, "") + if err != nil { + return nil, nil, false, false, err + } + + for name, val := range rp { + switch { + case name == target.Name: + rp[name] = otherIndex + case val > baseIndex && val <= otherIndex: + rp[name] = otherIndex - 1 + } + } + + updates.Add(&xmlapi.Config{ + Action: "move", + Xpath: util.AsXpath(path), + Where: "after", + Destination: *position.SomewhereAfter, + Target: s.client.GetTarget(), + }) + + baseIndex = otherIndex + } + case position.SomewhereBefore != nil && *position.SomewhereBefore != "": + target := entries[len(entries)-1] + + baseIndex, ok = rp[target.Name] + if !ok { + return nil, nil, false, false, fmt.Errorf("could not find rule %q for initial positioning", target.Name) + } + + otherIndex, ok = rp[*position.SomewhereBefore] + if !ok { + return nil, nil, false, false, fmt.Errorf("could not find referenced rule %q", *position.SomewhereBefore) + } + + if baseIndex > otherIndex { + path, err := loc.Xpath(vn, target.Name, "") + if err != nil { + return nil, nil, false, false, err + } + + for name, val := range rp { + switch { + case name == target.Name: + rp[name] = otherIndex + case val < baseIndex && val >= otherIndex: + rp[name] = val + 1 + } + } + + updates.Add(&xmlapi.Config{ + Action: "move", + Xpath: util.AsXpath(path), + Where: "before", + Destination: *position.SomewhereBefore, + Target: s.client.GetTarget(), + }) + + baseIndex = otherIndex + } + case position.DirectlyAfter != nil && *position.DirectlyAfter != "": + topDown = true + target := entries[0] + + baseIndex, ok = rp[target.Name] + if !ok { + return nil, nil, false, false, fmt.Errorf("could not find rule %q for initial positioning", target.Name) + } + + otherIndex, ok = rp[*position.DirectlyAfter] + if !ok { + return nil, nil, false, false, fmt.Errorf("could not find referenced rule %q for initial positioning", *position.DirectlyAfter) + } + + if baseIndex != otherIndex+1 { + path, err := loc.Xpath(vn, target.Name, "") + if err != nil { + return nil, nil, false, false, err + } + + for name, val := range rp { + switch { + case name == target.Name: + rp[name] = otherIndex + case val > baseIndex && val <= otherIndex: + rp[name] = otherIndex - 1 + } + } + + updates.Add(&xmlapi.Config{ + Action: "move", + Xpath: util.AsXpath(path), + Where: "after", + Destination: *position.DirectlyAfter, + Target: s.client.GetTarget(), + }) + + baseIndex = otherIndex + } + case position.DirectlyBefore != nil && *position.DirectlyBefore != "": + target := entries[len(entries)-1] + + baseIndex, ok = rp[target.Name] + if !ok { + return nil, nil, false, false, fmt.Errorf("could not find rule %q for initial positioning", target.Name) + } + + otherIndex, ok = rp[*position.DirectlyBefore] + if !ok { + return nil, nil, false, false, fmt.Errorf("could not find referenced rule %q", *position.DirectlyBefore) + } + + if baseIndex+1 != otherIndex { + path, err := loc.Xpath(vn, target.Name, "") + if err != nil { + return nil, nil, false, false, err + } + + for name, val := range rp { + switch { + case name == target.Name: + rp[name] = otherIndex + case val < baseIndex && val >= otherIndex: + rp[name] = val + 1 + } + } + + updates.Add(&xmlapi.Config{ + Action: "move", + Xpath: util.AsXpath(path), + Where: "before", + Destination: *position.DirectlyBefore, + Target: s.client.GetTarget(), + }) + + baseIndex = otherIndex + } + default: + topDown = true + target := entries[0] + + baseIndex, ok = rp[target.Name] + if !ok { + return nil, nil, false, false, fmt.Errorf("could not find rule %q for first positioning", target.Name) + } + + } + + var prevName, where string + if topDown { + prevName = entries[0].Name + where = "after" + } else { + prevName = entries[len(entries)-1].Name + where = "before" + } + + // Move the rest of the rules if necessary. + for i := 1; i < len(entries); i++ { + var target Entry + var desiredIndex int + if topDown { + target = entries[i] + desiredIndex = baseIndex + i + } else { + target = entries[len(entries)-1-i] + desiredIndex = baseIndex - i + } + + idx, ok := rp[target.Name] + if !ok { + return nil, nil, false, false, fmt.Errorf("rule %q not present", target.Name) + } + + if idx != desiredIndex { + path, err := loc.Xpath(vn, target.Name, "") + if err != nil { + return nil, nil, false, false, err + } + + if idx < desiredIndex { + for name, val := range rp { + if val > idx && val <= desiredIndex { + rp[name] = val - 1 + } + } + } else { + for name, val := range rp { + if val < idx && val >= desiredIndex { + rp[name] = val + 1 + } + } + } + rp[target.Name] = desiredIndex + + updates.Add(&xmlapi.Config{ + Action: "move", + Xpath: util.AsXpath(path), + Where: where, + Destination: prevName, + Target: s.client.GetTarget(), + }) + } + + prevName = target.Name + } + + if len(updates.Operations) > 0 { + if false { + log.Printf("%d operations, doing multi-config", len(updates.Operations)) + } + if _, _, _, err = s.client.MultiConfig(ctx, updates, false, nil); err != nil { + return nil, nil, false, false, err + } + } + + // Build out the uuid map and return ReadGroup. + uuidList := make([]string, 0, len(entries)) + for _, entry := range entries { + for _, live := range listing { + if live.Name == entry.Name { + uuidList = append(uuidList, live.Uuid) + break + } + } + } + + // Done. + return s.ReadGroup(ctx, loc, position, uuidList) +} + +// ReadGroup returns the config for the given rule UUIDs. +// +// The first boolean returned is true if the UUIDs are contiguous. +// The second boolean returned returns if the given position matches. +func (s *Service) ReadGroup(ctx context.Context, loc Location, position rule.Position, uuids []string) ([]Entry, map[string]string, bool, bool, error) { + objs, err := s.List(ctx, loc, "get", "", "") + if err != nil { + return nil, nil, false, false, err + } + + contiguous := true + listing := make([]Entry, 0, len(uuids)) + + firstIndex, prev := -1, -1 + uuidMap := make(map[string]string, len(uuids)) + for _, uuid := range uuids { + for num, entry := range objs { + if entry.Uuid == uuid { + uuidMap[entry.Uuid] = entry.Name + if contiguous && prev != -1 { + contiguous = num == prev+1 + } + listing = append(listing, entry) + prev = num + break + } + } + + if firstIndex == -1 && prev != -1 { + firstIndex = prev + } + } + + var positionOk, found bool + if len(objs) > 0 && len(listing) > 0 && firstIndex != -1 { + switch { + case position.First != nil && *position.First: + found = true + positionOk = objs[0].Uuid == listing[0].Uuid + case position.Last != nil && *position.Last: + found = true + positionOk = contiguous && firstIndex+len(listing) == len(objs) + case position.SomewhereBefore != nil && *position.SomewhereBefore != "": + for otherIndex, entry := range objs { + if entry.Name == *position.SomewhereBefore { + found = true + positionOk = firstIndex < otherIndex + break + } + } + case position.DirectlyBefore != nil && *position.DirectlyBefore != "": + for otherIndex, entry := range objs { + if entry.Name == *position.DirectlyBefore { + found = true + positionOk = firstIndex+len(listing) == otherIndex + break + } + } + case position.SomewhereAfter != nil && *position.SomewhereAfter != "": + for otherIndex, entry := range objs { + if entry.Name == *position.SomewhereAfter { + found = true + positionOk = firstIndex > otherIndex + break + } + } + case position.DirectlyAfter != nil && *position.DirectlyAfter != "": + for otherIndex, entry := range objs { + if entry.Name == *position.DirectlyAfter { + found = true + positionOk = firstIndex-1 == otherIndex + break + } + } + default: + found, positionOk = true, true + } + + if !found { + return listing, uuidMap, contiguous, false, fmt.Errorf("referenced rule does not exist") + } + } + + return listing, uuidMap, contiguous, positionOk, nil +} + +// DeleteGroup deletes the given uuids. +func (s *Service) DeleteGroup(ctx context.Context, loc Location, uuids map[string]string) error { + if len(uuids) == 0 { + return errors.UuidNotSpecifiedError + } + + vn := s.client.Versioning() + updates := xmlapi.NewMultiConfig(len(uuids)) + + for uuid := range uuids { + path, err := loc.Xpath(vn, "", uuid) + if err != nil { + return err + } + + updates.Add(&xmlapi.Config{ + Action: "delete", + Xpath: util.AsXpath(path), + Target: s.client.GetTarget(), + }) + } + + _, _, _, err := s.client.MultiConfig(ctx, updates, false, nil) + + return err +}