Files
terraform-provider-unifi/internal/provider/settings/resource_setting_ips.go
2025-03-21 11:52:55 +01:00

867 lines
34 KiB
Go

package settings
import (
"context"
"github.com/filipowm/go-unifi/unifi"
"github.com/filipowm/go-unifi/unifi/features"
"github.com/filipowm/terraform-provider-unifi/internal/provider/base"
ut "github.com/filipowm/terraform-provider-unifi/internal/provider/types"
"github.com/filipowm/terraform-provider-unifi/internal/provider/utils"
"github.com/filipowm/terraform-provider-unifi/internal/provider/validators"
"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/objectplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
)
// DNS Filter model
type DNSFilterModel struct {
AllowedSites types.List `tfsdk:"allowed_sites"`
BlockedSites types.List `tfsdk:"blocked_sites"`
BlockedTld types.List `tfsdk:"blocked_tld"`
Description types.String `tfsdk:"description"`
Filter types.String `tfsdk:"filter"`
Name types.String `tfsdk:"name"`
NetworkID types.String `tfsdk:"network_id"`
}
func (m *DNSFilterModel) AttributeTypes() map[string]attr.Type {
return map[string]attr.Type{
"allowed_sites": types.ListType{
ElemType: types.StringType,
},
"blocked_sites": types.ListType{
ElemType: types.StringType,
},
"blocked_tld": types.ListType{
ElemType: types.StringType,
},
"description": types.StringType,
"filter": types.StringType,
"name": types.StringType,
"network_id": types.StringType,
}
}
// Honeypots model
type HoneypotModel struct {
IPAddress types.String `tfsdk:"ip_address"`
NetworkID types.String `tfsdk:"network_id"`
}
func (m *HoneypotModel) AttributeTypes() map[string]attr.Type {
return map[string]attr.Type{
"ip_address": types.StringType,
"network_id": types.StringType,
}
}
// Tracking model
type TrackingModel struct {
Direction types.String `tfsdk:"direction"`
Mode types.String `tfsdk:"mode"`
Value types.String `tfsdk:"value"`
}
func (m *TrackingModel) AttributeTypes() map[string]attr.Type {
return map[string]attr.Type{
"direction": types.StringType,
"mode": types.StringType,
"value": types.StringType,
}
}
// Alerts model
type AlertsModel struct {
Category types.String `tfsdk:"category"`
Signature types.String `tfsdk:"signature"`
Tracking types.List `tfsdk:"tracking"`
Type types.String `tfsdk:"type"`
}
func (m *AlertsModel) AttributeTypes() map[string]attr.Type {
return map[string]attr.Type{
"category": types.StringType,
"signature": types.StringType,
"tracking": types.ListType{
ElemType: types.ObjectType{
AttrTypes: (&TrackingModel{}).AttributeTypes(),
},
},
"type": types.StringType,
}
}
// Whitelist model
type WhitelistModel struct {
Direction types.String `tfsdk:"direction"`
Mode types.String `tfsdk:"mode"`
Value types.String `tfsdk:"value"`
}
func (m *WhitelistModel) AttributeTypes() map[string]attr.Type {
return map[string]attr.Type{
"direction": types.StringType,
"mode": types.StringType,
"value": types.StringType,
}
}
// Suppression model
type SuppressionModel struct {
Alerts types.List `tfsdk:"alerts"`
Whitelist types.List `tfsdk:"whitelist"`
}
func (m *SuppressionModel) AttributeTypes() map[string]attr.Type {
return map[string]attr.Type{
"alerts": types.ListType{
ElemType: types.ObjectType{
AttrTypes: (&AlertsModel{}).AttributeTypes(),
},
},
"whitelist": types.ListType{
ElemType: types.ObjectType{
AttrTypes: (&WhitelistModel{}).AttributeTypes(),
},
},
}
}
// Main IPS model
type ipsModel struct {
base.Model
AdBlockedNetworks types.List `tfsdk:"ad_blocked_networks"`
AdvancedFilteringPreference types.String `tfsdk:"advanced_filtering_preference"`
DNSFilters types.List `tfsdk:"dns_filters"`
EnabledCategories types.List `tfsdk:"enabled_categories"`
EnabledNetworks types.List `tfsdk:"enabled_networks"`
Honeypots types.List `tfsdk:"honeypots"`
Mode types.String `tfsdk:"ips_mode"`
MemoryOptimized types.Bool `tfsdk:"memory_optimized"`
RestrictTorrents types.Bool `tfsdk:"restrict_torrents"`
Suppression types.Object `tfsdk:"suppression"`
}
func (d *ipsModel) AsUnifiModel(ctx context.Context) (interface{}, diag.Diagnostics) {
diags := diag.Diagnostics{}
model := &unifi.SettingIps{
AdvancedFilteringPreference: d.AdvancedFilteringPreference.ValueString(),
IPsMode: d.Mode.ValueString(),
MemoryOptimized: d.MemoryOptimized.ValueBool(),
RestrictTorrents: d.RestrictTorrents.ValueBool(),
// Initialize empty slices for arrays to avoid null values in JSON
AdBlockingConfigurations: []unifi.SettingIpsAdBlockingConfigurations{},
DNSFilters: []unifi.SettingIpsDNSFilters{},
EnabledCategories: []string{},
EnabledNetworks: []string{},
Honeypot: []unifi.SettingIpsHoneypot{},
// Initialize suppression with empty arrays
Suppression: unifi.SettingIpsSuppression{
Alerts: []unifi.SettingIpsAlerts{},
Whitelist: []unifi.SettingIpsWhitelist{},
},
}
if model.AdvancedFilteringPreference == "" {
if model.IPsMode != "disabled" {
model.AdvancedFilteringPreference = "manual"
} else {
model.AdvancedFilteringPreference = "disabled"
}
}
var enabledCategories []string
diags.Append(ut.ListElementsAs(d.EnabledCategories, &enabledCategories)...)
if diags.HasError() {
return nil, diags
}
model.EnabledCategories = enabledCategories
var enabledNetworks []string
diags.Append(ut.ListElementsAs(d.EnabledNetworks, &enabledNetworks)...)
if diags.HasError() {
return nil, diags
}
model.EnabledNetworks = enabledNetworks
// Handle AdBlockedNetworks - if any networks are configured, set AdBlockingEnabled to true
if ut.IsDefined(d.AdBlockedNetworks) {
var adBlockedNetworks []string
diags.Append(ut.ListElementsAs(d.AdBlockedNetworks, &adBlockedNetworks)...)
if diags.HasError() {
return nil, diags
}
if len(adBlockedNetworks) > 0 {
model.AdBlockingEnabled = true
model.AdBlockingConfigurations = make([]unifi.SettingIpsAdBlockingConfigurations, 0, len(adBlockedNetworks))
for _, networkID := range adBlockedNetworks {
model.AdBlockingConfigurations = append(model.AdBlockingConfigurations, unifi.SettingIpsAdBlockingConfigurations{
NetworkID: networkID,
})
}
} else {
model.AdBlockingEnabled = false
model.AdBlockingConfigurations = []unifi.SettingIpsAdBlockingConfigurations{}
}
}
// Handle DNSFilters - if any filters are configured, set DNSFiltering to true
if ut.IsDefined(d.DNSFilters) {
var dnsFiltersObjects []DNSFilterModel
diags.Append(ut.ListElementsAs(d.DNSFilters, &dnsFiltersObjects)...)
if diags.HasError() {
return nil, diags
}
if len(dnsFiltersObjects) > 0 {
model.DNSFiltering = true
model.DNSFilters = make([]unifi.SettingIpsDNSFilters, 0, len(dnsFiltersObjects))
for _, filterObj := range dnsFiltersObjects {
version := "v4"
if utils.IsIPv6(filterObj.NetworkID.ValueString()) {
version = "v6"
}
filter := unifi.SettingIpsDNSFilters{
Description: filterObj.Description.ValueString(),
Filter: filterObj.Filter.ValueString(),
Name: filterObj.Name.ValueString(),
NetworkID: filterObj.NetworkID.ValueString(),
Version: version,
}
// Handle allowed sites
var allowedSites, blockedSites, blockedTlds []string
diags.Append(ut.ListElementsAs(filterObj.AllowedSites, &allowedSites)...)
diags.Append(ut.ListElementsAs(filterObj.BlockedSites, &blockedSites)...)
diags.Append(ut.ListElementsAs(filterObj.BlockedTld, &blockedTlds)...)
if diags.HasError() {
return nil, diags
}
filter.AllowedSites = allowedSites
filter.BlockedSites = blockedSites
filter.BlockedTld = blockedTlds
model.DNSFilters = append(model.DNSFilters, filter)
}
} else {
model.DNSFiltering = false
model.DNSFilters = []unifi.SettingIpsDNSFilters{}
}
}
// Handle honeypot
if ut.IsDefined(d.Honeypots) {
var honeypotObjects []HoneypotModel
diags.Append(ut.ListElementsAs(d.Honeypots, &honeypotObjects)...)
if diags.HasError() {
return nil, diags
}
model.Honeypot = make([]unifi.SettingIpsHoneypot, 0)
for _, honeypotObj := range honeypotObjects {
version := "v4"
if utils.IsIPv6(honeypotObj.IPAddress.ValueString()) {
version = "v6"
}
model.Honeypot = append(model.Honeypot, unifi.SettingIpsHoneypot{
IPAddress: honeypotObj.IPAddress.ValueString(),
NetworkID: honeypotObj.NetworkID.ValueString(),
Version: version,
})
}
}
if len(model.Honeypot) > 0 {
model.HoneypotEnabled = true
} else {
model.HoneypotEnabled = false
}
// Handle suppression
if ut.IsDefined(d.Suppression) {
var suppressionObj SuppressionModel
diags.Append(d.Suppression.As(ctx, &suppressionObj, basetypes.ObjectAsOptions{})...)
if diags.HasError() {
return nil, diags
}
var alerts []AlertsModel
diags.Append(ut.ListElementsAs(suppressionObj.Alerts, &alerts)...)
if diags.HasError() {
return nil, diags
}
model.Suppression.Alerts = make([]unifi.SettingIpsAlerts, 0)
for idx, alertObj := range alerts {
alert := unifi.SettingIpsAlerts{
Category: alertObj.Category.ValueString(),
Signature: alertObj.Signature.ValueString(),
Type: alertObj.Type.ValueString(),
ID: 100 + idx,
Gid: 200 + idx,
}
// Handle tracking
var trackings []TrackingModel
diags.Append(ut.ListElementsAs(alertObj.Tracking, &trackings)...)
if diags.HasError() {
return nil, diags
}
alert.Tracking = make([]unifi.SettingIpsTracking, 0)
for _, trackingObj := range trackings {
if ut.IsEmptyString(trackingObj.Direction) || ut.IsEmptyString(trackingObj.Mode) || ut.IsEmptyString(trackingObj.Value) {
continue
}
alert.Tracking = append(alert.Tracking, unifi.SettingIpsTracking{
Direction: trackingObj.Direction.ValueString(),
Mode: trackingObj.Mode.ValueString(),
Value: trackingObj.Value.ValueString()})
}
model.Suppression.Alerts = append(model.Suppression.Alerts, alert)
}
var whitelists []WhitelistModel
diags.Append(ut.ListElementsAs(suppressionObj.Whitelist, &whitelists)...)
if diags.HasError() {
return nil, diags
}
model.Suppression.Whitelist = make([]unifi.SettingIpsWhitelist, 0, len(whitelists))
for _, whitelistObj := range whitelists {
model.Suppression.Whitelist = append(model.Suppression.Whitelist, unifi.SettingIpsWhitelist{
Direction: whitelistObj.Direction.ValueString(),
Mode: whitelistObj.Mode.ValueString(),
Value: whitelistObj.Value.ValueString(),
})
}
}
return model, diags
}
func (d *ipsModel) Merge(ctx context.Context, other interface{}) diag.Diagnostics {
diags := diag.Diagnostics{}
model, ok := other.(*unifi.SettingIps)
if !ok {
diags.AddError("Invalid model type", "Expected *unifi.SettingIps")
return diags
}
d.ID = types.StringValue(model.ID)
// Only set values for fields that were explicitly set in the configuration
// or returned by the API with non-default values
// Set basic fields if they were defined in the plan
d.AdvancedFilteringPreference = types.StringValue(model.AdvancedFilteringPreference)
d.Mode = types.StringValue(model.IPsMode)
d.MemoryOptimized = types.BoolValue(model.MemoryOptimized)
d.RestrictTorrents = types.BoolValue(model.RestrictTorrents)
// Handle enabled categories
enabledCategoriesList, diags := types.ListValueFrom(ctx, types.StringType, model.EnabledCategories)
if diags.HasError() {
return diags
}
if ut.IsDefined(enabledCategoriesList) {
d.EnabledCategories = enabledCategoriesList
} else {
d.EnabledCategories = ut.EmptyList(types.StringType)
}
// Handle enabled networks
enabledNetworksList, diags := types.ListValueFrom(ctx, types.StringType, model.EnabledNetworks)
if diags.HasError() {
return diags
}
if ut.IsDefined(enabledNetworksList) {
d.EnabledNetworks = enabledNetworksList
} else {
d.EnabledNetworks = ut.EmptyList(types.StringType)
}
//Handle AdBlockedNetworks - extract network IDs from AdBlockingConfigurations
adBlockedNetworks := make([]string, 0, len(model.AdBlockingConfigurations))
for _, config := range model.AdBlockingConfigurations {
adBlockedNetworks = append(adBlockedNetworks, config.NetworkID)
}
adBlockedNetworksList, diags := types.ListValueFrom(ctx, types.StringType, adBlockedNetworks)
if diags.HasError() {
return diags
}
d.AdBlockedNetworks = adBlockedNetworksList
// Handle DNSFilters
dnsFilters := make([]DNSFilterModel, 0)
for _, filter := range model.DNSFilters {
dnsFilter := DNSFilterModel{
Description: types.StringValue(filter.Description),
Filter: types.StringValue(filter.Filter),
Name: types.StringValue(filter.Name),
NetworkID: types.StringValue(filter.NetworkID),
}
allowedSites, diags := types.ListValueFrom(ctx, types.StringType, filter.AllowedSites)
if diags.HasError() {
return diags
}
dnsFilter.AllowedSites = allowedSites
blockedSites, diags := types.ListValueFrom(ctx, types.StringType, filter.BlockedSites)
if diags.HasError() {
return diags
}
dnsFilter.BlockedSites = blockedSites
blockedTlds, diags := types.ListValueFrom(ctx, types.StringType, filter.BlockedTld)
if diags.HasError() {
return diags
}
dnsFilter.BlockedTld = blockedTlds
dnsFilters = append(dnsFilters, dnsFilter)
}
dnsFiltersList, diags := types.ListValueFrom(ctx, types.ObjectType{
AttrTypes: (&DNSFilterModel{}).AttributeTypes(),
}, dnsFilters)
if diags.HasError() {
return diags
}
d.DNSFilters = dnsFiltersList
// Handle honeypot
honeypotModels := make([]HoneypotModel, 0, len(model.Honeypot))
for _, honeypot := range model.Honeypot {
honeypotModels = append(honeypotModels, HoneypotModel{
IPAddress: types.StringValue(honeypot.IPAddress),
NetworkID: types.StringValue(honeypot.NetworkID),
})
}
honeypotList, diags := types.ListValueFrom(ctx, types.ObjectType{AttrTypes: (&HoneypotModel{}).AttributeTypes()}, honeypotModels)
if diags.HasError() {
return diags
}
d.Honeypots = honeypotList
// Handle suppression
suppression := SuppressionModel{}
// Handle alerts
alertModels := make([]AlertsModel, 0)
for _, alert := range model.Suppression.Alerts {
// Skip alerts with ID 0, because they may come as default values from the API
if alert.ID == 0 && alert.Category == "" && alert.Signature == "" && alert.Type == "" {
continue
}
alertModel := AlertsModel{
Category: types.StringValue(alert.Category),
Signature: types.StringValue(alert.Signature),
Type: types.StringValue(alert.Type),
}
// Handle tracking
trackingModels := make([]TrackingModel, 0)
for _, tracking := range alert.Tracking {
trackingModels = append(trackingModels, TrackingModel{
Direction: types.StringValue(tracking.Direction),
Mode: types.StringValue(tracking.Mode),
Value: types.StringValue(tracking.Value),
})
}
trackings, diags := types.ListValueFrom(ctx, types.ObjectType{AttrTypes: (&TrackingModel{}).AttributeTypes()}, trackingModels)
if diags.HasError() {
return diags
}
alertModel.Tracking = trackings
alertModels = append(alertModels, alertModel)
}
alerts, diags := types.ListValueFrom(ctx, types.ObjectType{AttrTypes: (&AlertsModel{}).AttributeTypes()}, alertModels)
if diags.HasError() {
return diags
}
suppression.Alerts = alerts
// Handle whitelist
whitelistModels := make([]WhitelistModel, 0)
for _, whitelist := range model.Suppression.Whitelist {
whitelistModels = append(whitelistModels, WhitelistModel{
Direction: types.StringValue(whitelist.Direction),
Mode: types.StringValue(whitelist.Mode),
Value: types.StringValue(whitelist.Value),
})
}
whitelist, diags := types.ListValueFrom(ctx, types.ObjectType{AttrTypes: (&WhitelistModel{}).AttributeTypes()}, whitelistModels)
if diags.HasError() {
return diags
}
suppression.Whitelist = whitelist
suppressionObj, diags := types.ObjectValueFrom(ctx, (&SuppressionModel{}).AttributeTypes(), suppression)
if diags.HasError() {
return diags
}
d.Suppression = suppressionObj
return diags
}
type ipsResource struct {
*base.GenericResource[*ipsModel]
}
func requiredTogetherIfString(ctx context.Context, config tfsdk.Config, attr, value, reqAttribute string) diag.Diagnostics {
v := validators.RequiredTogetherIf(path.MatchRoot(attr), types.StringValue(value), path.MatchRoot(reqAttribute))
return v.Validate(ctx, config)
}
func (r *ipsResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
resp.Diagnostics.Append(r.RequireMinVersion("7.4")...)
resp.Diagnostics.Append(r.RequireMinVersionForPath("7.5", path.Root("advanced_filtering_preference"), req.Config)...)
resp.Diagnostics.Append(r.RequireMinVersionForPath("8.0", path.Root("enabled_networks"), req.Config)...)
resp.Diagnostics.Append(r.RequireMinVersionForPath("9.0", path.Root("memory_optimized"), req.Config)...)
site, diags := r.GetClient().ResolveSiteFromConfig(ctx, req.Config)
if diags.HasError() {
resp.Diagnostics.Append(diags...)
return
}
resp.Diagnostics.Append(r.RequireFeaturesEnabled(ctx, site, features.Ips)...)
if r.GetClient().Version.GreaterThan(base.ControllerV8) {
diags.Append(requiredTogetherIfString(ctx, req.Config, "ips_mode", "ips", "enabled_networks")...)
diags.Append(requiredTogetherIfString(ctx, req.Config, "ips_mode", "ids", "enabled_networks")...)
diags.Append(requiredTogetherIfString(ctx, req.Config, "ips_mode", "ipsInline", "enabled_networks")...)
}
}
func (r *ipsResource) ConfigValidators(_ context.Context) []resource.ConfigValidator {
return []resource.ConfigValidator{}
}
func (r *ipsResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "The `unifi_setting_ips` resource allows you to configure the Intrusion Prevention System (IPS) settings for your UniFi network. IPS provides network threat protection by monitoring, detecting, and preventing malicious traffic based on configured rules and policies. Requires controller version 7.4 or later",
Attributes: map[string]schema.Attribute{
"id": ut.ID(),
"site": ut.SiteAttribute(),
"ad_blocked_networks": schema.ListAttribute{
MarkdownDescription: "List of network IDs to enable ad blocking for. If any networks are configured, ad blocking will be automatically enabled. Each entry should be a valid network ID from your UniFi configuration. Leave empty to disable ad blocking.",
ElementType: types.StringType,
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.List{
listplanmodifier.UseStateForUnknown(),
},
},
"advanced_filtering_preference": schema.StringAttribute{
MarkdownDescription: "The advanced filtering preference for IPS. Valid values are:\n" +
" * `disabled` - Advanced filtering is disabled\n" +
" * `manual` - Advanced filtering is enabled and manually configured",
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
Validators: []validator.String{
stringvalidator.OneOf("disabled", "manual"),
},
},
"dns_filters": schema.ListNestedAttribute{
MarkdownDescription: "DNS filters configuration. If any filters are configured, DNS filtering will be automatically enabled. Each filter can be applied to a specific network and provides content filtering capabilities.",
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.List{
listplanmodifier.UseStateForUnknown(),
},
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"allowed_sites": schema.ListAttribute{
MarkdownDescription: "List of allowed sites for this DNS filter. These domains will always be accessible regardless of other filtering rules. Each entry should be a valid domain name (e.g., `example.com`).",
ElementType: types.StringType,
Optional: true,
},
"blocked_sites": schema.ListAttribute{
MarkdownDescription: "List of blocked sites for this DNS filter. These domains will be blocked regardless of other filtering rules. Each entry should be a valid domain name (e.g., `example.com`).",
ElementType: types.StringType,
Optional: true,
},
"blocked_tld": schema.ListAttribute{
MarkdownDescription: "List of blocked top-level domains (TLDs) for this DNS filter. All domains with these TLDs will be blocked. Each entry should be a valid TLD without the dot prefix (e.g., `xyz`, `info`).",
ElementType: types.StringType,
Optional: true,
},
"description": schema.StringAttribute{
MarkdownDescription: "Description of the DNS filter. This is used for documentation purposes only and does not affect functionality.",
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"filter": schema.StringAttribute{
MarkdownDescription: "Filter type that determines the predefined filtering level. Valid values are:\n" +
" * `none` - No predefined filtering\n" +
" * `work` - Work-appropriate filtering that blocks adult content\n" +
" * `family` - Family-friendly filtering that blocks adult content and other inappropriate sites",
Required: true,
Validators: []validator.String{
stringvalidator.OneOf("none", "work", "family"),
},
},
"name": schema.StringAttribute{
MarkdownDescription: "Name of the DNS filter. This is used to identify the filter in the UniFi interface.",
Required: true,
},
"network_id": schema.StringAttribute{
MarkdownDescription: "Network ID this filter applies to. This should be a valid network ID from your UniFi configuration.",
Required: true,
},
},
},
},
"enabled_categories": schema.ListAttribute{
MarkdownDescription: "List of enabled IPS threat categories. Each entry enables detection and prevention for a specific type of threat. The list of valid categories includes common threats like malware, exploits, scanning, and policy violations. See the validator for the complete list of available categories.",
ElementType: types.StringType,
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.List{
listplanmodifier.UseStateForUnknown(),
},
//Default: utils.DefaultEmptyList(types.StringType),
Validators: []validator.List{
listvalidator.ValueStringsAre(stringvalidator.OneOf("emerging-activex", "emerging-attackresponse", "botcc", "emerging-chat", "ciarmy", "compromised", "emerging-dns", "emerging-dos", "dshield", "emerging-exploit", "emerging-ftp", "emerging-games", "emerging-icmp", "emerging-icmpinfo", "emerging-imap", "emerging-inappropriate", "emerging-info", "emerging-malware", "emerging-misc", "emerging-mobile", "emerging-netbios", "emerging-p2p", "emerging-policy", "emerging-pop3", "emerging-rpc", "emerging-scada", "emerging-scan", "emerging-shellcode", "emerging-smtp", "emerging-snmp", "emerging-sql", "emerging-telnet", "emerging-tftp", "tor", "emerging-useragent", "emerging-voip", "emerging-webapps", "emerging-webclient", "emerging-webserver", "emerging-worm", "exploit-kit", "adware-pup", "botcc-portgrouped", "phishing", "threatview-cs-c2", "3coresec", "chat", "coinminer", "current-events", "drop", "hunting", "icmp-info", "inappropriate", "info", "ja3", "policy", "scada", "dark-web-blocker-list", "malicious-hosts")),
},
},
"enabled_networks": schema.ListAttribute{
MarkdownDescription: "List of network IDs to enable IPS protection for. Each entry should be a valid network ID from your UniFi configuration. IPS will only monitor and protect traffic on these networks.",
ElementType: types.StringType,
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.List{
listplanmodifier.UseStateForUnknown(),
},
},
"honeypots": schema.ListNestedAttribute{
MarkdownDescription: "Honeypots configuration. Honeypots are decoy systems designed to detect, deflect, or study hacking attempts. They appear as legitimate parts of the network but are isolated and monitored.",
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.List{
listplanmodifier.UseStateForUnknown(),
},
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"ip_address": schema.StringAttribute{
MarkdownDescription: "IP address for the honeypot. This should be an unused IPv4 address within your network range that will be used as a decoy system.",
Required: true,
Validators: []validator.String{
stringvalidator.Any(validators.IPv4(), validators.IPv6()),
},
},
"network_id": schema.StringAttribute{
MarkdownDescription: "Network ID for the honeypot. This should be a valid network ID from your UniFi configuration where the honeypot will be deployed.",
Required: true,
},
},
},
},
"ips_mode": schema.StringAttribute{
MarkdownDescription: "The IPS operation mode. Valid values are:\n" +
" * `ids` - Intrusion Detection System mode (detect and log threats only)\n" +
" * `ips` - Intrusion Prevention System mode (detect and block threats)\n" +
" * `ipsInline` - Inline Intrusion Prevention System mode (more aggressive blocking)\n" +
" * `disabled` - IPS functionality is completely disabled",
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
Validators: []validator.String{
stringvalidator.OneOf("ids", "ips", "ipsInline", "disabled"),
},
},
"memory_optimized": schema.BoolAttribute{
MarkdownDescription: "Whether memory optimization is enabled for IPS. When set to `true`, the system will use less memory at the cost of potentially reduced detection capabilities. Useful for devices with limited resources. Defaults to `false`. Requires controller version 9.0 or later.",
Optional: true,
Computed: true,
Default: booldefault.StaticBool(false),
PlanModifiers: []planmodifier.Bool{
boolplanmodifier.UseStateForUnknown(),
},
},
"restrict_torrents": schema.BoolAttribute{
MarkdownDescription: "Whether to restrict BitTorrent and other peer-to-peer file sharing traffic. When set to `true`, the system will block P2P traffic across the network. Defaults to `false`.",
Optional: true,
Computed: true,
Default: booldefault.StaticBool(false),
PlanModifiers: []planmodifier.Bool{
boolplanmodifier.UseStateForUnknown(),
},
},
"suppression": schema.SingleNestedAttribute{
MarkdownDescription: "Suppression configuration for IPS. This allows you to customize which alerts are suppressed or tracked, and define whitelisted traffic that should never trigger IPS alerts.",
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.Object{
objectplanmodifier.UseStateForUnknown(),
},
Attributes: map[string]schema.Attribute{
"alerts": schema.ListNestedAttribute{
MarkdownDescription: "Alert suppressions. Each entry defines a specific IPS alert that should be suppressed or tracked differently from the default behavior.",
Optional: true,
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"category": schema.StringAttribute{
MarkdownDescription: "Category of the alert to suppress. This should match one of the categories from the enabled_categories list.",
Required: true,
},
//"gid": schema.Int64Attribute{
// MarkdownDescription: "Group ID of the alert to suppress. This is a numeric identifier for the alert group in the IPS ruleset.",
// Required: true,
//},
//"id": schema.Int64Attribute{
// MarkdownDescription: "ID of the alert to suppress. This is a numeric identifier for the specific alert in the IPS ruleset.",
// Required: true,
//},
"signature": schema.StringAttribute{
MarkdownDescription: "Signature name of the alert to suppress. This is a human-readable identifier for the alert in the IPS ruleset.",
Required: true,
},
"tracking": schema.ListNestedAttribute{
MarkdownDescription: "Tracking configuration for the alert. This defines how the system should track occurrences of this alert based on source/destination addresses.",
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.List{
listplanmodifier.UseStateForUnknown(),
},
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"direction": schema.StringAttribute{
MarkdownDescription: "Direction for tracking. Valid values are:\n" +
" * `src` - Track by source address\n" +
" * `dest` - Track by destination address\n" +
" * `both` - Track by both source and destination addresses",
Required: true,
Validators: []validator.String{
stringvalidator.OneOf("src", "dest", "both"),
},
},
"mode": schema.StringAttribute{
MarkdownDescription: "Mode for tracking. Valid values are:\n" +
" * `ip` - Track by individual IP address\n" +
" * `subnet` - Track by subnet\n" +
" * `network` - Track by network ID",
Required: true,
Validators: []validator.String{
stringvalidator.OneOf("ip", "subnet", "network"),
},
},
"value": schema.StringAttribute{
MarkdownDescription: "Value for tracking. The meaning depends on the mode:\n" +
" * For `ip` mode: An IP address (e.g., `192.168.1.100`)\n" +
" * For `subnet` mode: A CIDR notation subnet (e.g., `192.168.1.0/24`)\n" +
" * For `network` mode: A network ID from your UniFi configuration",
Required: true,
},
},
},
},
"type": schema.StringAttribute{
MarkdownDescription: "Type of suppression. Valid values are:\n" +
" * `all` - Suppress all occurrences of this alert\n" +
" * `track` - Only track this alert according to the tracking configuration",
Required: true,
Validators: []validator.String{
stringvalidator.OneOf("all", "track"),
},
},
},
},
},
"whitelist": schema.ListNestedAttribute{
MarkdownDescription: "Whitelist configuration. Each entry defines traffic that should never trigger IPS alerts, regardless of other rules.",
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.List{
listplanmodifier.UseStateForUnknown(),
},
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"direction": schema.StringAttribute{
MarkdownDescription: "Direction for whitelist. Valid values are:\n" +
" * `src` - Whitelist by source address\n" +
" * `dst` - Whitelist by destination address\n" +
" * `both` - Whitelist by both source and destination addresses",
Required: true,
Validators: []validator.String{
stringvalidator.OneOf("src", "dst", "both"),
},
},
"mode": schema.StringAttribute{
MarkdownDescription: "Mode for whitelist. Valid values are:\n" +
" * `ip` - Whitelist by individual IP address\n" +
" * `subnet` - Whitelist by subnet\n" +
" * `network` - Whitelist by network ID",
Required: true,
Validators: []validator.String{
stringvalidator.OneOf("ip", "subnet", "network"),
},
},
"value": schema.StringAttribute{
MarkdownDescription: "Value for whitelist. The meaning depends on the mode:\n" +
" * For `ip` mode: An IP address (e.g., `192.168.1.100`)\n" +
" * For `subnet` mode: A CIDR notation subnet (e.g., `192.168.1.0/24`)\n" +
" * For `network` mode: A network ID from your UniFi configuration",
Required: true,
},
},
},
},
},
},
},
}
}
func NewIpsResource() resource.Resource {
r := &ipsResource{}
r.GenericResource = NewSettingResource(
"unifi_setting_ips",
func() *ipsModel { return &ipsModel{} },
func(ctx context.Context, client *base.Client, site string) (interface{}, error) {
return client.GetSettingIps(ctx, site)
},
func(ctx context.Context, client *base.Client, site string, body interface{}) (interface{}, error) {
return client.UpdateSettingIps(ctx, site, body.(*unifi.SettingIps))
},
)
return r
}
var (
_ base.ResourceModel = &ipsModel{}
_ resource.Resource = &ipsResource{}
_ resource.ResourceWithConfigure = &ipsResource{}
_ resource.ResourceWithConfigValidators = &ipsResource{}
_ resource.ResourceWithModifyPlan = &ipsResource{}
)