From 19658dec619835b6b033f7d6563ac38eda007db7 Mon Sep 17 00:00:00 2001 From: Tuomas Hietanen Date: Sat, 8 Nov 2025 14:52:34 +0000 Subject: [PATCH] Add comprehensive Azure security capabilities to achieve enterprise production-readiness --- .../resources/container-registry.md | 93 +++- .../resources/ddos-protection-plan.md | 101 +++++ .../resources/defender-for-cloud.md | 214 ++++++++++ .../api-overview/resources/network-watcher.md | 282 +++++++++++++ docs/content/api-overview/resources/policy.md | 397 ++++++++++++++++++ .../resources/recovery-services-vault.md | 358 ++++++++++++++++ .../api-overview/resources/sentinel.md | 138 ++++++ src/Farmer/Arm/ContainerRegistry.fs | 39 +- src/Farmer/Arm/Network.fs | 82 ++++ src/Farmer/Arm/Policy.fs | 144 +++++++ src/Farmer/Arm/RecoveryServices.fs | 118 ++++++ src/Farmer/Arm/Security.fs | 64 +++ src/Farmer/Arm/SecurityInsights.fs | 26 ++ .../Builders/Builders.ContainerRegistry.fs | 69 ++- .../Builders/Builders.DdosProtectionPlan.fs | 40 ++ .../Builders/Builders.DefenderForCloud.fs | 45 ++ .../Builders/Builders.NetworkWatcher.fs | 132 ++++++ src/Farmer/Builders/Builders.Policy.fs | 232 ++++++++++ .../Builders/Builders.RecoveryServices.fs | 158 +++++++ src/Farmer/Builders/Builders.Sentinel.fs | 61 +++ src/Farmer/Common.fs | 21 +- src/Farmer/Farmer.fsproj | 12 +- src/Tests/AllTests.fs | 6 + src/Tests/ContainerRegistry.fs | 46 +- src/Tests/DdosProtectionPlan.fs | 49 +++ src/Tests/DefenderForCloud.fs | 76 ++++ src/Tests/NetworkWatcher.fs | 94 +++++ src/Tests/Policy.fs | 273 ++++++++++++ src/Tests/RecoveryServices.fs | 139 ++++++ src/Tests/Sentinel.fs | 59 +++ src/Tests/Tests.fsproj | 6 + 31 files changed, 3556 insertions(+), 18 deletions(-) create mode 100644 docs/content/api-overview/resources/ddos-protection-plan.md create mode 100644 docs/content/api-overview/resources/defender-for-cloud.md create mode 100644 docs/content/api-overview/resources/network-watcher.md create mode 100644 docs/content/api-overview/resources/policy.md create mode 100644 docs/content/api-overview/resources/recovery-services-vault.md create mode 100644 docs/content/api-overview/resources/sentinel.md create mode 100644 src/Farmer/Arm/Policy.fs create mode 100644 src/Farmer/Arm/RecoveryServices.fs create mode 100644 src/Farmer/Arm/Security.fs create mode 100644 src/Farmer/Arm/SecurityInsights.fs create mode 100644 src/Farmer/Builders/Builders.DdosProtectionPlan.fs create mode 100644 src/Farmer/Builders/Builders.DefenderForCloud.fs create mode 100644 src/Farmer/Builders/Builders.NetworkWatcher.fs create mode 100644 src/Farmer/Builders/Builders.Policy.fs create mode 100644 src/Farmer/Builders/Builders.RecoveryServices.fs create mode 100644 src/Farmer/Builders/Builders.Sentinel.fs create mode 100644 src/Tests/DdosProtectionPlan.fs create mode 100644 src/Tests/DefenderForCloud.fs create mode 100644 src/Tests/NetworkWatcher.fs create mode 100644 src/Tests/Policy.fs create mode 100644 src/Tests/RecoveryServices.fs create mode 100644 src/Tests/Sentinel.fs diff --git a/docs/content/api-overview/resources/container-registry.md b/docs/content/api-overview/resources/container-registry.md index 69f0902c1..ab8648d18 100644 --- a/docs/content/api-overview/resources/container-registry.md +++ b/docs/content/api-overview/resources/container-registry.md @@ -15,7 +15,11 @@ The Container Registry builder is used to create Azure Container Registry (ACR) |-|-| | name | Sets the name of the Container Registry instance. | | sku | Sets the SKU of the instance. Defaults to Basic. | -| enable_admin_user | The value that indicates whether the admin user is enabled. | +| enable_admin_user | Enables the admin user (not recommended for production). | +| enable_public_network_access | Explicitly enables public network access. | +| disable_public_network_access | Disables public network access (Premium SKU only, recommended for security). | +| add_ip_rule | Adds an IP address or CIDR range to the allow list (Premium SKU only). | +| add_ip_rules | Adds multiple IP addresses or CIDR ranges to the allow list (Premium SKU only). | #### Configuration Members @@ -25,7 +29,7 @@ The Container Registry builder is used to create Azure Container Registry (ACR) | Password2 | Gets the ARM expression path to the second admin password of this container registry if the admin user was enabled. | | Username | Gets the ARM expression path to the admin username of this container registry if the admin user was enabled. | -#### Example +#### Basic Example ```fsharp open Farmer open Farmer.Builders @@ -36,3 +40,88 @@ let myRegistry = containerRegistry { enable_admin_user } ``` + +#### Secure Example with Network Restrictions (Premium SKU) +```fsharp +open Farmer +open Farmer.Builders +open Farmer.ContainerRegistry + +let secureRegistry = containerRegistry { + name "mySecureRegistry" + sku Premium + // Disable public network access - use private endpoints only + disable_public_network_access +} + +let restrictedRegistry = containerRegistry { + name "myRestrictedRegistry" + sku Premium + // Allow access only from specific IP addresses/ranges + add_ip_rules [ + "203.0.113.0/24" // Corporate network + "198.51.100.5" // Build server + ] +} +``` + +#### Security Best Practices + +1. **Disable Admin User**: The admin user provides a single account with full access to the registry. For production, use Azure AD authentication with managed identities instead. + +2. **Use Premium SKU for Production**: Only the Premium SKU supports: + - Network restrictions (IP rules and private endpoints) + - Disabling public network access + - Content trust and image signing + - Customer-managed keys + +3. **Restrict Network Access**: Use one of these strategies: + - **Disable Public Access**: Use `disable_public_network_access` and access only through private endpoints (most secure) + - **IP Restrictions**: Use `add_ip_rules` to limit access to known IP addresses + +4. **Use Managed Identities**: Instead of admin credentials, authenticate using Azure managed identities from services like AKS, Azure DevOps, or GitHub Actions. + +#### Network Security Notes + +- **IP Rules require Premium SKU**: Network restrictions are only available with the Premium tier +- **Default Deny**: When you add IP rules, all other IPs are denied by default +- **CIDR Notation**: IP rules support both individual IPs (`203.0.113.5`) and CIDR ranges (`203.0.113.0/24`) +- **Private Endpoints**: For complete isolation, use `disable_public_network_access` and connect via private endpoints + +#### Cost Considerations + +Azure Container Registry pricing varies significantly by SKU tier: + +| SKU | Approx. Monthly Cost* | Storage (Included) | Network Features | +|-----|----------------------|-------------------|------------------| +| **Basic** | ~$5 USD | 10 GB | Public access only | +| **Standard** | ~$20 USD | 100 GB | Public access only | +| **Premium** | ~$500 USD | 500 GB | IP rules, private endpoints, geo-replication | + +*Approximate costs as of 2025. Additional charges apply for: +- Storage beyond included amounts: ~$0.10/GB per day +- Build tasks and image pulls +- Geo-replication (Premium only) +- Data egress + +**Cost Optimization Tips:** +1. **Start with Basic**: Use Basic SKU for development/testing environments +2. **Standard for Production**: Standard SKU provides better performance and storage for most production workloads +3. **Premium When Needed**: Only upgrade to Premium when you specifically need: + - Network restrictions (IP rules, private endpoints) + - Geo-replication for global deployments + - Enhanced security features (content trust, customer-managed keys) +4. **Clean Up Old Images**: Regularly delete unused images to minimize storage costs +5. **Use Retention Policies**: Premium SKU supports automated image cleanup policies + +**Security vs. Cost Tradeoff:** +- Network restrictions (IP rules, private endpoints) require Premium SKU (~$500/month) +- For sensitive workloads, this cost is justified by the enhanced security +- For less sensitive workloads, consider Basic/Standard with Azure AD authentication and proper RBAC + +#### Compliance + +Container Registry with network restrictions helps meet security requirements from: +- **NIST 800-53**: AC-3 (Access Enforcement), SC-7 (Boundary Protection) +- **CIS Benchmarks**: 6.10 (Restrict container registry network access) +- **SOC 2**: CC6.6 (Logical and Physical Access Controls) diff --git a/docs/content/api-overview/resources/ddos-protection-plan.md b/docs/content/api-overview/resources/ddos-protection-plan.md new file mode 100644 index 000000000..b614b2f49 --- /dev/null +++ b/docs/content/api-overview/resources/ddos-protection-plan.md @@ -0,0 +1,101 @@ +--- +title: "DDoS Protection Plan" +date: 2025-11-08 +chapter: false +weight: 14 +--- + +#### Overview +The DDoS Protection Plan builder creates Azure DDoS Protection Plans that provide enhanced DDoS mitigation capabilities for virtual networks. + +* DDoS Protection Plan (`Microsoft.Network/ddosProtectionPlans`) + +> DDoS Protection Plans provide always-on traffic monitoring and automatic mitigation of DDoS attacks. They can be shared across multiple virtual networks in the same subscription or across subscriptions in the same Azure AD tenant, providing cost-effective protection at scale. + +#### Builder Keywords + +| Keyword | Purpose | +|-|-| +| name | Sets the name of the DDoS Protection Plan | +| add_tag | Adds a tag to the DDoS Protection Plan | +| add_tags | Adds multiple tags to the DDoS Protection Plan | + +#### Example + +```fsharp +open Farmer +open Farmer.Builders + +let myDdosPlan = ddosProtectionPlan { + name "my-ddos-protection-plan" + add_tags [ + "environment", "production" + "cost-center", "security" + ] +} + +let deployment = arm { + location Location.EastUS + add_resource myDdosPlan +} +``` + +#### Cost Considerations + +DDoS Protection Plan is a premium service with a fixed monthly cost (~$3,000 USD/month) plus data transfer charges. However, a single DDoS Protection Plan can be: + +* Shared across all virtual networks in a subscription +* Shared across subscriptions in the same Azure AD tenant + +This makes it cost-effective to deploy a single DDoS Protection Plan at the tenant or management group level and share it across all virtual networks. + +#### Best Practices + +1. **Centralized Deployment**: Create one DDoS Protection Plan per tenant and share it across all virtual networks +2. **Tagging**: Use tags to track the cost center and ownership +3. **Virtual Network Association**: After creating a DDoS Protection Plan, associate it with virtual networks using the `link_to_ddos_protection_plan` operation on virtual networks + +#### Linking to Virtual Networks + +Once a DDoS Protection Plan is created, it must be linked to virtual networks to provide protection: + +```fsharp +let ddosPlan = ddosProtectionPlan { + name "shared-ddos-plan" +} + +let vnet = vnet { + name "my-vnet" + add_address_spaces [ "10.0.0.0/16" ] + link_to_ddos_protection_plan ddosPlan +} + +let deployment = arm { + location Location.EastUS + add_resources [ ddosPlan; vnet ] +} +``` + +> Note: The `link_to_ddos_protection_plan` operation for virtual networks will be available in a future Farmer release. + +#### Security Benefits + +DDoS Protection Plan provides: + +* **Always-on traffic monitoring**: Continuous monitoring of application traffic patterns +* **Automatic attack mitigation**: Instant attack detection and mitigation without user intervention +* **Attack analytics**: Detailed metrics and diagnostics during and after attacks +* **Adaptive tuning**: Machine learning-based traffic profiling for more accurate detection +* **Protection for Azure resources**: Covers public IP addresses, Application Gateways, and Azure Load Balancers +* **DDoS rapid response support**: Access to DDoS experts during an active attack +* **Cost protection**: Service credits for scale-out costs during documented attacks + +#### Compliance + +DDoS Protection Plans help meet compliance requirements from security frameworks including: + +* **NIST Cybersecurity Framework**: SC-5 (Denial of Service Protection) +* **ISO 27001**: A.14.1.2 (Securing application services) +* **PCI DSS**: Requirement 5 (Protect all systems against malware and regularly update anti-virus software) +* **SOC 2**: CC7.2 (System monitoring) + diff --git a/docs/content/api-overview/resources/defender-for-cloud.md b/docs/content/api-overview/resources/defender-for-cloud.md new file mode 100644 index 000000000..4e387cc22 --- /dev/null +++ b/docs/content/api-overview/resources/defender-for-cloud.md @@ -0,0 +1,214 @@ +--- +title: "Defender for Cloud" +date: 2025-11-08 +chapter: false +weight: 19 +--- + +#### Overview +The Defender for Cloud builder enables Microsoft Defender plans for continuous security posture management and threat protection. + +* Defender Pricing (`Microsoft.Security/pricings`) + +> Microsoft Defender for Cloud (formerly Azure Security Center) provides unified security management and advanced threat protection across hybrid cloud workloads. It helps strengthen security posture, protect against threats, and meet compliance requirements. + +#### Builder Keywords + +| Keyword | Purpose | +|-|-| +| plan | Sets the Defender plan to enable (VirtualMachines, SqlServers, AppServices, etc.) | +| tier | Sets the pricing tier (Standard for enabled, Free for disabled). Default is Standard | +| enable | Explicitly enables the plan (sets tier to Standard) | +| disable | Disables the plan (sets tier to Free) | + +#### Available Defender Plans + +| Plan | Protects | Monthly Cost* | +|------|----------|---------------| +| **VirtualMachines** | Azure VMs | **$15/VM** | +| **AppServices** | Web Apps, Functions | **$15/instance** | +| **SqlServers** | Azure SQL, SQL MI | **$15/server** | +| **SqlServerVirtualMachines** | SQL on VMs | **$15/VM** | +| **StorageAccounts** | Blob, Files | **$10/million transactions** | +| **KubernetesService** | AKS clusters | **$7/vCore/month** | +| **ContainerRegistry** | ACR images | **$0.29/image** | +| **KeyVaults** | Key Vaults | **$0.02/10K transactions** | +| **Dns** | DNS queries | **$0.70/million queries** | +| **Arm** | ARM operations | **FREE** | +| **Containers** | Container security | **$7/vCore/month** | +| **OpenSourceRelationalDatabases** | PostgreSQL, MySQL | **$15/server** | +| **CosmosDbs** | Cosmos DB | **$0.0012/100 RU/s/hour** | +| **CloudPosture** | CSPM | **FREE** | + +*Approximate costs as of 2025. Actual costs vary. + +#### Examples + +##### Enable Defender for VMs + +```fsharp +open Farmer +open Farmer.Builders +open Farmer.Arm.Security + +let vmDefender = defenderForCloud { + plan DefenderPlan.VirtualMachines +} + +let deployment = arm { + add_resource vmDefender +} +``` + +##### Enable Multiple Defender Plans + +```fsharp +open Farmer +open Farmer.Builders +open Farmer.Arm.Security + +let vmDefender = defenderForCloud { plan DefenderPlan.VirtualMachines } +let sqlDefender = defenderForCloud { plan DefenderPlan.SqlServers } +let storageDefender = defenderForCloud { plan DefenderPlan.StorageAccounts } +let aksDefender = defenderForCloud { plan DefenderPlan.KubernetesService } + +let deployment = arm { + add_resources [ + vmDefender + sqlDefender + storageDefender + aksDefender + ] +} +``` + +##### Enable Free Cloud Posture Management + +```fsharp +open Farmer +open Farmer.Builders +open Farmer.Arm.Security + +// Cloud Security Posture Management (CSPM) is FREE +let cspm = defenderForCloud { + plan DefenderPlan.CloudPosture +} + +let deployment = arm { + add_resource cspm +} +``` + +##### Disable a Defender Plan + +```fsharp +open Farmer +open Farmer.Builders +open Farmer.Arm.Security + +let disableVmDefender = defenderForCloud { + plan DefenderPlan.VirtualMachines + disable // Sets tier to Free +} + +let deployment = arm { + add_resource disableVmDefender +} +``` + +#### Cost Optimization + +**Free Features:** +- Cloud Security Posture Management (CSPM) +- Azure Resource Manager (ARM) protection +- Secure Score +- Security recommendations +- Regulatory compliance dashboard + +**Recommended Plans by Priority:** + +| Priority | Plan | Why | +|----------|------|-----| +| **Critical** | VirtualMachines | Most attack surface | +| **Critical** | SqlServers | Data protection | +| **High** | AppServices | Public-facing apps | +| **High** | KubernetesService | Container security | +| **Medium** | StorageAccounts | Data protection | +| **Medium** | KeyVaults | Secrets protection | +| **Low** | ContainerRegistry | Image scanning | + +**Start Small:** +- Enable FREE Cloud Posture first +- Add VM Defender for production VMs +- Expand to other services as needed + +#### Security Benefits + +Defender for Cloud provides: + +* **Continuous Assessment**: Real-time security posture evaluation +* **Secure Score**: Quantified security posture with actionable recommendations +* **Threat Protection**: Advanced threat detection using Microsoft threat intelligence +* **Just-In-Time VM Access**: Reduce attack surface with temporary VM access +* **Adaptive Application Controls**: Whitelist applications on VMs +* **File Integrity Monitoring**: Detect unauthorized changes +* **Vulnerability Assessment**: Built-in scanner for VMs and containers +* **Compliance Dashboard**: Track compliance with standards (PCI, HIPAA, ISO, etc.) +* **Security Alerts**: Real-time alerts for detected threats +* **Automated Response**: Integration with Logic Apps for automation + +#### Best Practices + +1. **Enable Cloud Posture First**: It's free and provides immediate value +2. **Start with Critical Resources**: VMs and databases first +3. **Review Recommendations Daily**: Act on high-severity findings +4. **Enable JIT Access**: Reduce VM attack surface +5. **Configure Email Alerts**: Get notified of security incidents +6. **Integrate with Sentinel**: Send alerts to SIEM for investigation +7. **Regular Compliance Reviews**: Track regulatory compliance monthly +8. **Test Response Procedures**: Simulate security incidents quarterly + +#### Compliance + +Defender for Cloud helps meet requirements from: + +* **PCI DSS**: Multiple requirements (vulnerability management, monitoring) +* **HIPAA**: §164.308(a)(1) (Security management), §164.308(a)(5) (Security awareness) +* **ISO 27001**: A.12.6 (Technical vulnerability management), A.18.2 (Compliance reviews) +* **NIST CSF**: ID.RA (Risk assessment), DE.CM (Continuous monitoring) +* **SOC 2**: CC7 (System monitoring), CC9 (Risk mitigation) +* **CIS Controls**: Control 3 (Continuous vulnerability management) + +#### What You Get + +**All Plans Include:** +- Security alerts for detected threats +- Integration with Sentinel +- Compliance dashboard +- Secure Score recommendations +- Integration with Azure Policy + +**VM Defender Adds:** +- Just-In-Time VM access +- Adaptive application controls +- File integrity monitoring +- Vulnerability scanner + +**Container Defender Adds:** +- Image vulnerability scanning +- Runtime threat protection +- Kubernetes workload protection + +#### Next Steps + +After enabling Defender: + +1. **Review Secure Score**: Check security posture +2. **Act on Recommendations**: Fix high-severity issues +3. **Enable JIT Access**: Configure for production VMs +4. **Set Up Alerts**: Configure email notifications +5. **Integrate with Sentinel**: Send alerts to SIEM +6. **Review Compliance**: Check regulatory compliance status +7. **Test Incident Response**: Simulate and respond to alerts + +**Note:** This builder enables Defender plans. Additional configuration (JIT policies, alert rules, etc.) must be done through Azure Portal or CLI. diff --git a/docs/content/api-overview/resources/network-watcher.md b/docs/content/api-overview/resources/network-watcher.md new file mode 100644 index 000000000..01b0a406f --- /dev/null +++ b/docs/content/api-overview/resources/network-watcher.md @@ -0,0 +1,282 @@ +--- +title: "Network Watcher" +date: 2025-11-08 +chapter: false +weight: 16 +--- + +#### Overview +The Network Watcher builder creates Azure Network Watcher instances and Flow Logs for network monitoring and diagnostics. + +* Network Watcher (`Microsoft.Network/networkWatchers`) +* Flow Log (`Microsoft.Network/networkWatchers/flowLogs`) + +> Network Watcher provides tools to monitor, diagnose, view metrics, and enable or disable logs for resources in an Azure virtual network. Flow Logs capture information about IP traffic flowing through Network Security Groups (NSGs), enabling security analysis, compliance auditing, and traffic pattern monitoring. + +#### Builder Keywords + +##### Network Watcher + +| Keyword | Purpose | +|-|-| +| name | Sets the name of the Network Watcher | +| add_tag | Adds a tag to the Network Watcher | +| add_tags | Adds multiple tags to the Network Watcher | + +##### Flow Log + +| Keyword | Purpose | +|-|-| +| name | Sets the name of the flow log | +| link_to_network_watcher | Links to a Network Watcher instance (required) | +| link_to_nsg | Links to the NSG to monitor (required) | +| link_to_storage_account | Links to the Storage Account for storing logs (required) | +| retention_days | Sets log retention period in days (0 = unlimited, default 7) | +| enable_traffic_analytics | Enables Traffic Analytics with Log Analytics Workspace | + +#### Examples + +##### Basic Network Watcher + +```fsharp +open Farmer +open Farmer.Builders + +let watcher = networkWatcher { + name "network-watcher-eastus" + add_tags [ + "environment", "production" + "cost-center", "network-operations" + ] +} + +let deployment = arm { + location Location.EastUS + add_resource watcher +} +``` + +##### Flow Log with Storage + +```fsharp +open Farmer +open Farmer.Builders + +let storageAccount = storageAccount { + name "flowlogsstorage" + sku Storage.Standard_LRS +} + +let nsg = nsg { + name "web-tier-nsg" + add_rules [ + securityRule { + name "allow-https" + services [ NetworkService ("https", 443) ] + add_source_tag NetworkSecurity.Tag.Internet + add_destination_network "10.0.1.0/24" + } + ] +} + +let watcher = networkWatcher { + name "network-watcher-eastus" +} + +let flowLog = flowLog { + name "web-tier-flow-logs" + link_to_network_watcher watcher + link_to_nsg nsg.ResourceId + link_to_storage_account storageAccount.ResourceId + retention_days 30 +} + +let deployment = arm { + location Location.EastUS + add_resources [ storageAccount; nsg; watcher; flowLog ] +} +``` + +##### Flow Log with Traffic Analytics + +```fsharp +open Farmer +open Farmer.Builders + +let logAnalytics = logAnalytics { + name "security-workspace" + sku LogAnalytics.PerGB2018 +} + +let storageAccount = storageAccount { + name "flowlogsstorage" + sku Storage.Standard_LRS +} + +let nsg = nsg { + name "app-tier-nsg" +} + +let watcher = networkWatcher { + name "network-watcher-eastus" +} + +let flowLogWithAnalytics = flowLog { + name "app-tier-flow-logs-analytics" + link_to_network_watcher watcher + link_to_nsg nsg.ResourceId + link_to_storage_account storageAccount.ResourceId + retention_days 90 + enable_traffic_analytics logAnalytics.ResourceId +} + +let deployment = arm { + location Location.EastUS + add_resources [ logAnalytics; storageAccount; nsg; watcher; flowLogWithAnalytics ] +} +``` + +#### Cost Considerations + +**Network Watcher Costs:** + +| Component | Cost Model | Approx. Cost* | +|-----------|-----------|---------------| +| **Network Watcher Instance** | Per region | **FREE** (automatically deployed) | +| **Flow Logs** | Per GB processed | **$0.50 per GB** | +| **Storage (Flow Logs)** | Standard storage rates | **~$0.02 per GB/month** (LRS) | +| **Traffic Analytics** | Per GB analyzed | **$0.125 per GB** | +| **Log Analytics Ingestion** | Per GB ingested | **$2.30 per GB** (Pay-as-you-go) | + +*Approximate costs as of 2025. Actual costs vary by region and usage. + +**Example Monthly Cost Calculations:** + +| Scenario | Flow Log Volume | Storage | Traffic Analytics | Log Analytics | **Total/Month** | +|----------|----------------|---------|-------------------|---------------|-----------------| +| **Small (Dev/Test)** | 10 GB | $0.20 | $1.25 | $23 | **~$30** | +| **Medium (Production)** | 100 GB | $2 | $12.50 | $230 | **~$295** | +| **Large (Enterprise)** | 1 TB | $20 | $125 | $2,300 | **~$2,950** | + +**Cost Optimization Strategies:** + +1. **Selective Monitoring**: Only enable Flow Logs on critical NSGs (not every NSG in your environment) +2. **Shorter Retention**: Use 7-30 day retention instead of 90+ days for most workloads +3. **Storage Tiers**: + - Use **Cool tier** for archived logs (access infrequent, ~40% cheaper storage) + - Use **Hot tier** only for recent logs requiring frequent access +4. **Sampling**: For very high-volume environments, consider sampling traffic instead of logging every flow +5. **Traffic Analytics**: Only enable on critical workloads where you need deep insights +6. **Log Analytics**: + - Use **Commitment Tiers** if ingesting >100 GB/day (up to 30% savings) + - Set **data retention** to minimum required (31 days default, first 31 days free) +7. **Regional Placement**: Use same region for Network Watcher, NSG, and Storage to avoid data transfer costs + +**When to Use Each Feature:** + +| Feature | Use For | Skip If | +|---------|---------|---------| +| **Basic Flow Logs** | Compliance requirements, security investigations, troubleshooting | Cost-sensitive dev/test environments | +| **Traffic Analytics** | Threat detection, capacity planning, usage analysis | Basic compliance logging only | +| **Long Retention** | Regulatory compliance (e.g., 90+ days) | No specific retention requirements | + +**Cost vs. Security Tradeoff:** +- Flow Logs are essential for security incident investigation and compliance +- For production workloads handling sensitive data, the cost (~$30-500/month per environment) is typically justified +- Development/test environments may skip Flow Logs to reduce costs + +#### Network Watcher Per Region + +Azure automatically deploys one Network Watcher instance per region when you enable it. You typically don't need to create Network Watcher instances manually unless: + +1. You're using Infrastructure as Code and want explicit control +2. You need to set specific tags for cost tracking +3. You're deploying to a new region for the first time + +#### Flow Log Retention + +Retention settings control how long flow logs are kept in storage: + +- **0 days**: Unlimited retention (data kept until manually deleted) +- **1-365 days**: Logs automatically deleted after specified period +- **Default**: 7 days +- **Compliance**: Set based on regulatory requirements (e.g., PCI DSS requires 90 days) + +#### Traffic Analytics Benefits + +Traffic Analytics provides advanced insights beyond basic Flow Logs: + +1. **Security Threat Detection**: Identify suspicious traffic patterns, malicious IPs +2. **Compliance Reporting**: Generate reports on traffic flows for audits +3. **Capacity Planning**: Identify bandwidth hotspots and underutilized resources +4. **Application Mapping**: Visualize which applications are communicating +5. **Geo-mapping**: See traffic patterns across regions and countries +6. **Top Talkers**: Identify VMs generating most traffic + +**When to Enable:** +- Production environments requiring active threat monitoring +- Environments subject to compliance audits +- Complex multi-tier applications where traffic patterns aren't obvious + +**When to Skip:** +- Simple dev/test environments +- Cost-sensitive deployments +- When basic flow logs suffice for compliance + +#### Best Practices + +1. **Regional Deployment**: Deploy one Network Watcher per Azure region you use +2. **Storage Account Location**: Place storage account in same region as NSG to avoid egress charges +3. **Retention Policy**: Set retention based on compliance requirements, not "just in case" +4. **Centralized Storage**: Use one storage account per region for all flow logs +5. **Monitoring**: Set up alerts for unusual traffic patterns in Log Analytics +6. **Cost Alerts**: Configure Azure Cost Management alerts when flow log costs exceed budget +7. **Tagging**: Tag Network Watchers and Flow Logs for cost allocation and tracking +8. **Regular Review**: Periodically review which NSGs have flow logs enabled and disable unnecessary ones + +#### Security Benefits + +Network Watcher and Flow Logs provide essential security capabilities: + +* **Threat Detection**: Identify anomalous traffic patterns and potential attacks +* **Forensic Analysis**: Investigate security incidents with historical traffic data +* **Compliance Auditing**: Demonstrate network traffic logging for regulatory requirements +* **Baseline Monitoring**: Establish normal traffic patterns to detect deviations +* **Incident Response**: Quickly understand network activity during security events +* **Lateral Movement Detection**: Identify unauthorized internal network traversal +* **Data Exfiltration Detection**: Detect unusual outbound traffic patterns + +#### Compliance + +Network Watcher Flow Logs help meet requirements from major security frameworks: + +* **NIST SP 800-53**: AU-2 (Audit Events), AU-6 (Audit Review), SI-4 (Information System Monitoring) +* **ISO 27001**: A.12.4.1 (Event logging), A.12.4.2 (Protection of log information) +* **PCI DSS**: Requirement 10 (Track and monitor all access to network resources) +* **HIPAA**: §164.312(b) (Audit controls) +* **SOC 2**: CC7.2 (System monitoring), CC7.3 (Logging and monitoring) +* **CIS Azure Foundations**: 6.5 (Ensure that Network Watcher is 'Enabled') +* **FedRAMP**: AU-2 (Audit Events), SI-4 (Information System Monitoring) + +Flow Logs are particularly important for demonstrating: +- Continuous monitoring of network traffic +- Audit trails for security investigations +- Compliance with data residency requirements (logs stored in specific regions) + +#### Integration with Azure Security Center + +Network Watcher integrates with Microsoft Defender for Cloud to provide: + +* Automatic threat detection based on flow log analysis +* Security recommendations for network configuration +* Network map visualization +* Just-in-time VM access monitoring + +#### Limitations and Considerations + +* Flow Logs only capture NSG-level traffic (not individual packet content) +* Processing delay: Flow logs typically appear within 5-15 minutes +* Storage costs can accumulate quickly in high-traffic environments +* Traffic Analytics requires Log Analytics workspace (additional cost) +* Flow Logs v2 is the current version (v1 is being deprecated) +* Maximum retention: 365 days (use storage lifecycle policies for longer retention) diff --git a/docs/content/api-overview/resources/policy.md b/docs/content/api-overview/resources/policy.md new file mode 100644 index 000000000..dbcd7f0ff --- /dev/null +++ b/docs/content/api-overview/resources/policy.md @@ -0,0 +1,397 @@ +--- +title: "Azure Policy" +date: 2025-11-08 +chapter: false +weight: 15 +--- + +#### Overview +The Azure Policy builder creates policy definitions and policy assignments that help enforce organizational standards and assess compliance at scale. + +* Policy Definition (`Microsoft.Authorization/policyDefinitions`) +* Policy Assignment (`Microsoft.Authorization/policyAssignments`) + +> Azure Policy helps enforce organizational standards and assess compliance at-scale. Through its compliance dashboard, it provides an aggregated view to evaluate the overall state of the environment, with the ability to drill down to per-resource, per-policy granularity. + +#### Builder Keywords + +##### Policy Definition + +| Keyword | Purpose | +|-|-| +| name | Sets the name of the policy definition | +| display_name | Sets the display name of the policy definition | +| description | Sets the description of the policy definition | +| mode | Sets the policy mode (PolicyMode.All or PolicyMode.Indexed) | +| policy_rule | Sets the policy rule as a JSON string | +| parameters | Sets the parameters for the policy definition | + +##### Policy Assignment + +| Keyword | Purpose | +|-|-| +| name | Sets the name of the policy assignment | +| display_name | Sets the display name of the policy assignment | +| description | Sets the description of the policy assignment | +| link_to_policy | Links to a policy definition config built in this deployment | +| link_to_policy_id | Links to an existing policy definition by resource ID | +| enforcement_mode | Sets the enforcement mode (EnforcementMode.Default or EnforcementMode.DoNotEnforce) | +| parameters | Sets the parameters for the policy assignment | +| scope | Sets the scope for the policy assignment | +| not_scopes | Adds resource scopes to exclude from this policy assignment | +| add_dependency | Adds a dependency to this policy assignment | + +#### Examples + +##### Creating a Policy Definition + +```fsharp +open Farmer +open Farmer.Builders +open Farmer.Arm.Policy + +// Define a policy that restricts deployment locations +let locationPolicy = policyDefinition { + name "allowed-locations-policy" + display_name "Allowed Azure Regions" + description "This policy restricts resource deployments to specific Azure regions" + mode PolicyMode.All + policy_rule """{ + "if": { + "not": { + "field": "location", + "in": ["eastus", "westus", "northeurope"] + } + }, + "then": { + "effect": "deny" + } + }""" +} + +let deployment = arm { + location Location.EastUS + add_resource locationPolicy +} +``` + +##### Creating a Policy Assignment + +```fsharp +open Farmer +open Farmer.Builders +open Farmer.Arm.Policy + +// Define and assign a policy to audit storage accounts without HTTPS +let storagePolicy = policyDefinition { + name "require-https-storage-policy" + display_name "Require HTTPS for Storage Accounts" + description "This policy audits storage accounts that don't enforce HTTPS" + mode PolicyMode.All + policy_rule """{ + "if": { + "allOf": [ + { + "field": "type", + "equals": "Microsoft.Storage/storageAccounts" + }, + { + "field": "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly", + "notEquals": "true" + } + ] + }, + "then": { + "effect": "audit" + } + }""" +} + +let storageAssignment = policyAssignment { + name "require-https-storage-assignment" + display_name "Audit Storage HTTPS Compliance" + description "Audits all storage accounts for HTTPS enforcement" + link_to_policy storagePolicy + enforcement_mode EnforcementMode.Default +} + +let deployment = arm { + location Location.EastUS + add_resources [ storagePolicy; storageAssignment ] +} +``` + +##### Testing a Policy (DoNotEnforce Mode) + +```fsharp +open Farmer +open Farmer.Builders +open Farmer.Arm.Policy + +// Create a policy in test mode to evaluate impact without enforcement +let tagPolicy = policyDefinition { + name "require-environment-tag-policy" + display_name "Require Environment Tag" + description "This policy requires all resources to have an environment tag" + mode PolicyMode.Indexed + policy_rule """{ + "if": { + "field": "tags.environment", + "exists": "false" + }, + "then": { + "effect": "deny" + } + }""" +} + +let tagAssignment = policyAssignment { + name "require-env-tag-test" + display_name "Environment Tag Test Assignment" + link_to_policy tagPolicy + enforcement_mode EnforcementMode.DoNotEnforce // Test mode - logs violations but doesn't block +} + +let deployment = arm { + location Location.EastUS + add_resources [ tagPolicy; tagAssignment ] +} +``` + +#### Policy Modes + +Azure Policy supports two modes: + +* **PolicyMode.All**: Evaluates all resource types including resource groups and subscriptions +* **PolicyMode.Indexed**: Only evaluates resource types that support tags and location (default for most policies) + +Use `All` mode when your policy needs to evaluate resource groups or subscription-level properties. Use `Indexed` mode for policies focused on individual resources. + +#### Policy Effects + +Common policy effects you can use in your policy rules: + +* **Audit**: Creates a warning event in the activity log but doesn't stop the request +* **Deny**: Blocks non-compliant resource creation or updates +* **DeployIfNotExists**: Deploys additional resources if a condition isn't met +* **Modify**: Adds, updates, or removes tags or properties on resources +* **Append**: Adds additional fields to the resource during creation +* **Disabled**: Useful for testing or temporarily disabling a policy + +#### Best Practices + +1. **Start with Audit**: Begin with `audit` effect to understand impact before enforcing with `deny` +2. **Use DoNotEnforce Mode**: Test policy assignments in `DoNotEnforce` mode before enabling enforcement +3. **Descriptive Names**: Use clear display names and descriptions for easier management in Azure Portal +4. **Scope Carefully**: Apply policies at the appropriate scope (management group, subscription, or resource group) +5. **Tag Policies**: Use tags on policy definitions to organize and track them +6. **Exemptions**: Use `not_scopes` to exclude specific resources from policy enforcement when needed +7. **Built-in Policies**: Consider using Azure's built-in policies before creating custom ones + +#### Common Policy Patterns + +##### Enforce Resource Naming Convention + +```fsharp +let namingPolicy = policyDefinition { + name "enforce-naming-convention" + display_name "Enforce Resource Naming Convention" + mode PolicyMode.Indexed + policy_rule """{ + "if": { + "not": { + "field": "name", + "match": "[parameters('namePattern')]" + } + }, + "then": { + "effect": "deny" + } + }""" + parameters ( + Map.ofList [ + "namePattern", box {| + type = "String" + metadata = {| displayName = "Name Pattern"; description = "Pattern for resource names" |} + |} + ] + ) +} +``` + +##### Require Specific Tags + +```fsharp +let tagRequirementPolicy = policyDefinition { + name "require-cost-center-tag" + display_name "Require Cost Center Tag" + mode PolicyMode.Indexed + policy_rule """{ + "if": { + "field": "tags['cost-center']", + "exists": "false" + }, + "then": { + "effect": "deny" + } + }""" +} +``` + +##### Enforce Network Security + +```fsharp +let nsgPolicy = policyDefinition { + name "deny-rdp-from-internet" + display_name "Deny RDP from Internet" + description "Denies NSG rules that allow RDP (port 3389) from the Internet" + mode PolicyMode.All + policy_rule """{ + "if": { + "allOf": [ + { + "field": "type", + "equals": "Microsoft.Network/networkSecurityGroups/securityRules" + }, + { + "field": "Microsoft.Network/networkSecurityGroups/securityRules/access", + "equals": "Allow" + }, + { + "field": "Microsoft.Network/networkSecurityGroups/securityRules/direction", + "equals": "Inbound" + }, + { + "anyOf": [ + { + "field": "Microsoft.Network/networkSecurityGroups/securityRules/destinationPortRange", + "equals": "3389" + }, + { + "field": "Microsoft.Network/networkSecurityGroups/securityRules/destinationPortRange", + "equals": "*" + } + ] + }, + { + "anyOf": [ + { + "field": "Microsoft.Network/networkSecurityGroups/securityRules/sourceAddressPrefix", + "equals": "*" + }, + { + "field": "Microsoft.Network/networkSecurityGroups/securityRules/sourceAddressPrefix", + "equals": "Internet" + } + ] + } + ] + }, + "then": { + "effect": "deny" + } + }""" +} +``` + +#### Cost Considerations + +**Azure Policy is FREE!** + +Azure Policy has no direct costs: + +| Feature | Cost | +|---------|------| +| **Policy Definitions** | FREE (unlimited) | +| **Policy Assignments** | FREE (unlimited) | +| **Compliance Evaluation** | FREE | +| **Remediation Tasks** | FREE | +| **Guest Configuration** | FREE* | + +*Guest Configuration (policies that audit/configure VM settings) is free for Azure VMs. For Arc-enabled servers (on-premises or other clouds), standard Arc billing applies. + +**Indirect Costs to Consider:** + +While Azure Policy itself is free, be aware of these related costs: + +1. **Remediation Resources**: + - If you use `DeployIfNotExists` or `Modify` effects, the deployed resources have their own costs + - Example: A policy that deploys Azure Monitor agents to VMs incurs Log Analytics costs + - **Cost**: Varies by resource type + +2. **Activity Log Storage**: + - Policy compliance events are logged to Azure Activity Log + - Activity Log is retained for 90 days free, longer retention requires Log Analytics + - **Cost**: $2.30/GB for Log Analytics if extended retention is needed + +3. **Evaluation Performance**: + - Complex policies with many assignments may slightly slow deployment times + - **Cost**: Negligible, but can impact deployment duration + +4. **Management Overhead**: + - Staff time to develop, test, and maintain policies + - **Cost**: Operational/labor cost + +**Cost Optimization Tips:** + +1. **Test Before Enforcing**: Use `DoNotEnforce` mode to validate policies before applying them (avoids deployment failures) +2. **Use Audit First**: Start with `audit` effect to understand impact before switching to `deny` +3. **Leverage Built-in Policies**: Azure provides 1000+ built-in policies that are pre-tested and maintained +4. **Scope Carefully**: Apply policies at appropriate scope (management group vs. subscription vs. resource group) to avoid redundant evaluations +5. **Avoid Over-Remediation**: Be selective with `DeployIfNotExists` policies to avoid deploying unnecessary resources + +**ROI and Value:** + +Despite being free, Azure Policy provides immense value: + +* **Prevents Costly Mistakes**: Blocks insecure configurations before resources are deployed +* **Reduces Audit Costs**: Automates compliance checking that would otherwise require manual audits +* **Improves Security Posture**: Enforces security standards consistently, reducing breach risk +* **Accelerates Compliance**: Makes it easier to demonstrate compliance during audits +* **Operational Efficiency**: Automates governance that would require manual processes + +**Typical organizational savings from Azure Policy:** +- **Small Organization** (10-50 resources): 5-10 hours/month of manual compliance checking = ~$500-1000/month saved +- **Medium Organization** (100-500 resources): 20-40 hours/month = ~$2,000-4,000/month saved +- **Large Enterprise** (1000+ resources): 100+ hours/month = ~$10,000+/month saved + +#### Security Benefits + +Azure Policy provides critical security governance capabilities: + +* **Preventive Controls**: Block non-compliant resources before they're created +* **Detective Controls**: Audit existing resources for compliance violations +* **Automated Remediation**: Automatically fix non-compliant resources with DeployIfNotExists and Modify effects +* **Compliance Reporting**: Dashboard showing compliance state across your environment +* **Defense in Depth**: Works alongside RBAC to provide comprehensive access control +* **Consistent Enforcement**: Policies apply consistently across all resources in scope + +#### Compliance + +Azure Policy is essential for meeting compliance requirements from major security frameworks: + +* **NIST SP 800-53**: AC-1 (Access Control Policy), CM-2 (Baseline Configuration) +* **ISO 27001**: A.8.1.1 (Inventory of assets), A.12.1.1 (Documented operating procedures) +* **PCI DSS**: Requirement 2.2 (Develop configuration standards) +* **CIS Azure Foundations Benchmark**: Multiple controls across all sections +* **HIPAA**: Administrative Safeguards (Security Management Process) +* **SOC 2**: CC7.2 (System monitoring), CC8.1 (Change management) +* **FedRAMP**: CM-2 (Baseline Configuration), CM-6 (Configuration Settings) + +Azure Policy helps demonstrate continuous compliance and can generate evidence for audit purposes. + +#### Integration with Azure Security Center + +Policy assignments automatically integrate with Microsoft Defender for Cloud (formerly Azure Security Center), providing: + +* Centralized compliance dashboard +* Security score calculation +* Automated remediation recommendations +* Integration with Azure Security Benchmark + +#### Limitations + +* Policy evaluation can take up to 30 minutes for new or updated policy assignments +* Some Azure services may not be fully supported by Azure Policy +* Complex policies may impact deployment performance +* Policy effects like DeployIfNotExists require managed identity with appropriate permissions diff --git a/docs/content/api-overview/resources/recovery-services-vault.md b/docs/content/api-overview/resources/recovery-services-vault.md new file mode 100644 index 000000000..fb69c6c1e --- /dev/null +++ b/docs/content/api-overview/resources/recovery-services-vault.md @@ -0,0 +1,358 @@ +--- +title: "Recovery Services Vault" +date: 2025-11-08 +chapter: false +weight: 17 +--- + +#### Overview +The Recovery Services Vault builder creates Azure Recovery Services Vaults for backup and disaster recovery, along with backup policies for virtual machines. + +* Recovery Services Vault (`Microsoft.RecoveryServices/vaults`) +* VM Backup Policy (`Microsoft.RecoveryServices/vaults/backupPolicies`) + +> Recovery Services Vaults provide centralized backup and disaster recovery for Azure resources. They support Azure VMs, SQL databases, file shares, and on-premises workloads, with features like cross-region restore, soft delete, and ransomware protection. + +#### Builder Keywords + +##### Recovery Services Vault + +| Keyword | Purpose | +|-|-| +| name | Sets the name of the Recovery Services Vault | +| sku | Sets the SKU (RS0 for free tier, Standard for production). Default is Standard | +| add_tag | Adds a tag to the vault | +| add_tags | Adds multiple tags to the vault | + +##### VM Backup Policy + +| Keyword | Purpose | +|-|-| +| name | Sets the name of the backup policy | +| link_to_vault | Links to a Recovery Services Vault config | +| vault_name | Sets the vault name directly (for existing vaults) | +| schedule_frequency | Sets backup frequency (Daily or Weekly). Default is Daily | +| schedule_time | Sets backup time in ISO format. Default is 3 AM UTC | +| retention_days | Sets daily retention (7-9999 days). Default is 30 days | +| weekly_retention_weeks | Sets weekly retention (1-5163 weeks) | +| monthly_retention_months | Sets monthly retention (1-1188 months) | +| add_dependency | Adds a dependency to the backup policy | + +#### Examples + +##### Basic Recovery Services Vault + +```fsharp +open Farmer +open Farmer.Builders + +let vault = recoveryServicesVault { + name "production-backup-vault" + add_tags [ + "environment", "production" + "backup-tier", "critical" + ] +} + +let deployment = arm { + location Location.EastUS + add_resource vault +} +``` + +##### VM Backup Policy with Daily Backups + +```fsharp +open Farmer +open Farmer.Builders +open Farmer.Arm.RecoveryServices + +let vault = recoveryServicesVault { + name "backup-vault" +} + +let dailyBackup = vmBackupPolicy { + name "daily-vm-backup" + link_to_vault vault + schedule_frequency BackupScheduleFrequency.Daily + schedule_time "2023-01-01T02:00:00Z" // 2 AM UTC + retention_days 30 +} + +let deployment = arm { + location Location.EastUS + add_resources [ vault; dailyBackup ] +} +``` + +##### Comprehensive Backup Policy (Daily + Weekly + Monthly) + +```fsharp +open Farmer +open Farmer.Builders +open Farmer.Arm.RecoveryServices + +let vault = recoveryServicesVault { + name "enterprise-backup-vault" + add_tags [ "compliance", "required"; "retention", "long-term" ] +} + +let comprehensivePolicy = vmBackupPolicy { + name "comprehensive-backup" + link_to_vault vault + schedule_frequency BackupScheduleFrequency.Daily + schedule_time "2023-01-01T03:00:00Z" + retention_days 30 // Keep daily backups for 30 days + weekly_retention_weeks 52 // Keep weekly backups for 1 year + monthly_retention_months 24 // Keep monthly backups for 2 years +} + +let deployment = arm { + location Location.EastUS + add_resources [ vault; comprehensivePolicy ] +} +``` + +##### Multiple Backup Policies for Different Workloads + +```fsharp +open Farmer +open Farmer.Builders +open Farmer.Arm.RecoveryServices + +let vault = recoveryServicesVault { name "shared-backup-vault" } + +// Production VMs: Long retention +let productionPolicy = vmBackupPolicy { + name "production-backup" + link_to_vault vault + retention_days 90 + weekly_retention_weeks 52 + monthly_retention_months 12 +} + +// Development VMs: Short retention +let devPolicy = vmBackupPolicy { + name "dev-backup" + link_to_vault vault + retention_days 7 +} + +let deployment = arm { + location Location.EastUS + add_resources [ vault; productionPolicy; devPolicy ] +} +``` + +#### Cost Considerations + +**Recovery Services Vault Costs:** + +| Component | Cost Model | Approx. Cost* | +|-----------|-----------|---------------| +| **Vault (Standard SKU)** | FREE | **$0** | +| **Protected Instance** | Per VM/month | **~$5-10/VM** (depends on size) | +| **Backup Storage (LRS)** | Per GB/month | **$0.05/GB** (first 50 GB free per vault) | +| **Backup Storage (GRS)** | Per GB/month | **$0.10/GB** (geo-redundant) | +| **Snapshot Storage** | Per GB/month | **$0.05/GB** | +| **Restore** | Per GB | **Free** (data transfer may apply) | +| **Cross-Region Restore** | Data transfer | **$0.02/GB** (outbound) | + +*Approximate costs as of 2025. Actual costs vary by region. + +**Example Monthly Cost Calculations:** + +| Scenario | VMs | Avg VM Size | Storage (GRS) | **Total/Month** | +|----------|-----|-------------|---------------|-----------------| +| **Small (Dev/Test)** | 5 VMs | 50 GB each | 250 GB | **$25-75** | +| **Medium (Production)** | 20 VMs | 100 GB each | 2 TB | **$300-500** | +| **Large (Enterprise)** | 100 VMs | 150 GB each | 15 TB | **$2,000-3,500** | + +**Free Tier:** +- First 50 GB of backup storage per vault is **FREE** (Standard SKU) +- Good for small dev/test environments + +**Cost Optimization Strategies:** + +1. **Right-Size Retention**: + - Dev/test: 7 days ($minimal) + - Production: 30 days ($moderate) + - Compliance: 90+ days ($higher) + - Don't over-retain "just in case" + +2. **Use LRS for Non-Critical Workloads**: + - LRS: $0.05/GB (single region) + - GRS: $0.10/GB (geo-redundant) + - 50% savings for workloads that don't need geo-redundancy + +3. **Instant Restore vs. Standard**: + - Instant Restore (snapshot-based): Faster but costs more + - Standard (vault-based): Slower but cheaper + - Keep instant restore snapshots for 1-5 days only + +4. **Policy Segregation**: + - Create separate policies for different workload tiers + - Critical VMs: Long retention + - Non-critical VMs: Short retention + - Development VMs: Minimal retention + +5. **Backup Compression**: + - Azure automatically compresses backups (saves ~50% storage) + - No configuration needed + +6. **Monitor Unused Backups**: + - Delete backups for decommissioned VMs + - Use soft-delete (14-day retention) to prevent accidental deletion + +**Cost vs. Risk Tradeoff:** + +| Retention Period | Cost Level | Suitable For | Compliance | +|-----------------|------------|--------------|------------| +| **7 days** | Very Low (~$10/VM/mo) | Dev/test | None | +| **30 days** | Low (~$15/VM/mo) | Production | Basic | +| **90 days** | Medium (~$30/VM/mo) | Critical | PCI DSS, HIPAA | +| **365 days** | High (~$100/VM/mo) | Compliance | SOX, regulatory | + +**When to Use Each SKU:** + +| SKU | Cost | When to Use | +|-----|------|-------------| +| **Standard** | Backup costs only | Production (recommended) | +| **RS0** | FREE (deprecated) | Legacy only, use Standard instead | + +**ROI Analysis:** + +Backup costs are typically **1-5% of infrastructure costs** but prevent: +- Ransomware recovery costs ($100K-$1M+) +- Data loss from accidental deletions +- Compliance fines ($10K-$1M+) +- Business downtime ($5K-$500K per hour) + +**Cost is minimal compared to risk avoided.** + +#### Retention Policies + +Azure Backup supports multiple retention tiers: + +| Tier | Purpose | Max Duration | Typical Use | +|------|---------|--------------|-------------| +| **Daily** | Short-term recovery | 9999 days | All backups | +| **Weekly** | Medium-term recovery | 5163 weeks (~99 years) | Weekly checkpoints | +| **Monthly** | Long-term archival | 1188 months (~99 years) | Monthly archives | +| **Yearly** | Compliance archival | 99 years | Regulatory requirements | + +**Compliance Retention Requirements:** + +| Framework | Minimum Retention | Recommendation | +|-----------|------------------|----------------| +| **PCI DSS** | 90 days | 90 days daily + 1 year weekly | +| **HIPAA** | 6 years | 30 days daily + 7 years monthly | +| **SOX** | 7 years | 30 days daily + 7 years monthly | +| **GDPR** | Varies | 30 days (right to be forgotten applies) | +| **ISO 27001** | Define in policy | 90 days minimum recommended | + +#### Backup Schedule + +**Schedule Frequency:** +- **Daily**: Most common, recommended for production +- **Weekly**: For non-critical workloads + +**Schedule Time:** +- Use ISO 8601 format: `"2023-01-01T03:00:00Z"` +- Choose off-peak hours to minimize performance impact +- Typical production time: 2-4 AM local time +- All times in UTC + +**Backup Windows:** +- Daily backups take 30 minutes - 2 hours (depending on VM size and change rate) +- First backup (full) takes longer than incremental backups +- Plan for 2-4 hour backup window + +#### Best Practices + +1. **Use Standard SKU**: RS0 is deprecated, always use Standard +2. **Enable Soft Delete**: 14-day recovery window for accidentally deleted backups (enabled by default) +3. **Tag Everything**: Use tags for cost tracking and compliance reporting +4. **Separate Vaults by Environment**: Dev, test, prod in different vaults +5. **Regional Placement**: Place vault in same region as VMs to avoid data transfer costs +6. **Geo-Redundancy for Production**: Use GRS for critical workloads +7. **Test Restores**: Regularly test restore procedures (monthly recommended) +8. **Monitor Backup Jobs**: Set up alerts for failed backups +9. **Document Retention Policies**: Tie retention to business requirements +10. **Use Azure Policy**: Enforce backup on all production VMs + +#### Security Benefits + +Recovery Services Vaults provide critical business continuity and security capabilities: + +* **Ransomware Protection**: Immutable backups protected from encryption attacks +* **Soft Delete**: 14-day recovery window for deleted backups +* **Role-Based Access Control**: Granular permissions for backup operators +* **Encryption**: All data encrypted at rest and in transit +* **Air-Gapped Backups**: Isolated from production environment +* **Cross-Region Restore**: Disaster recovery to secondary region +* **Audit Logs**: Full backup/restore activity tracking +* **Point-in-Time Recovery**: Restore VMs to specific backup points + +#### Compliance + +Recovery Services Vaults help meet backup requirements from major frameworks: + +* **PCI DSS**: Requirement 3.1 (Keep cardholder data retention to minimum), 9.5 (Backup data) +* **HIPAA**: §164.308(a)(7) (Contingency plan), §164.310(d) (Data backup) +* **SOX**: Section 404 (Data retention and recoverability) +* **ISO 27001**: A.12.3.1 (Information backup) +* **NIST SP 800-53**: CP-9 (Information System Backup) +* **SOC 2**: CC7.5 (System availability) +* **GDPR**: Article 32 (Security of processing - resilience) + +**Audit Evidence:** +- Backup success/failure reports +- Restore test results +- Retention policy documentation +- Recovery time objective (RTO) testing + +#### Integration with Azure Services + +Recovery Services Vaults integrate with: + +* **Azure VMs**: Full VM backup and restore +* **Azure Policy**: Enforce backup on resources +* **Azure Monitor**: Backup job monitoring and alerts +* **Log Analytics**: Centralized backup reporting +* **Azure Security Center**: Backup recommendations +* **Azure Site Recovery**: Combined backup + DR strategy + +#### Limitations and Considerations + +* **Backup Frequency**: Maximum once per day (24-hour minimum interval) +* **Instant Restore**: Snapshots retained for maximum 5 days +* **Vault Location**: Must be in same Azure geography as backup source +* **First Backup**: Takes longer (full backup), subsequent backups are incremental +* **Bandwidth**: Initial backups may take time depending on VM size +* **Restore Time**: Depends on data size (typically 2-6 hours for full VM) +* **Cross-Region Restore**: Only available with GRS vaults +* **Soft Delete**: Cannot be disabled during first 14 days after enabling + +#### What's Not Covered (Out of Scope) + +This builder focuses on **VM backup only**. For complete backup coverage, you'll also need: + +- SQL/PostgreSQL database backups (built into database builders) +- File share backups (separate Azure Backup for Files) +- On-premises backup (requires Azure Backup Server) +- Azure Site Recovery (disaster recovery orchestration) + +These require additional configuration beyond this builder. + +#### Next Steps + +After creating a vault and policy: + +1. **Associate VMs with Policy**: Use Azure Portal or CLI to assign policy to VMs +2. **Run Initial Backup**: Trigger first backup manually to verify configuration +3. **Test Restore**: Perform test restore to secondary location +4. **Set Up Monitoring**: Configure backup job alerts in Azure Monitor +5. **Document RTO/RPO**: Define recovery time objectives for business continuity plan + +**Note:** Farmer creates the vault and policies. Associating specific VMs with backup policies is typically done through Azure Portal, CLI, or Azure Policy enforcement. diff --git a/docs/content/api-overview/resources/sentinel.md b/docs/content/api-overview/resources/sentinel.md new file mode 100644 index 000000000..1226df34f --- /dev/null +++ b/docs/content/api-overview/resources/sentinel.md @@ -0,0 +1,138 @@ +--- +title: "Azure Sentinel" +date: 2025-11-08 +chapter: false +weight: 18 +--- + +#### Overview +The Sentinel builder enables Azure Sentinel (cloud-native SIEM) on a Log Analytics Workspace. + +* Sentinel Onboarding (`Microsoft.SecurityInsights/onboardingStates`) + +> Azure Sentinel is Microsoft's cloud-native SIEM and SOAR solution. It provides intelligent security analytics and threat intelligence across the enterprise, providing a single solution for alert detection, threat visibility, proactive hunting, and threat response. + +#### Builder Keywords + +| Keyword | Purpose | +|-|-| +| link_to_workspace | Links to a Log Analytics Workspace to enable Sentinel on | +| workspace_name | Sets the workspace name directly (for existing workspaces) | +| add_dependency | Adds a dependency to Sentinel onboarding | + +#### Examples + +##### Enable Sentinel on New Workspace + +```fsharp +open Farmer +open Farmer.Builders + +let workspace = logAnalytics { + name "security-operations-workspace" + sku LogAnalytics.PerGB2018 +} + +let siem = sentinel { + link_to_workspace workspace +} + +let deployment = arm { + location Location.EastUS + add_resources [ workspace; siem ] +} +``` + +##### Enable Sentinel on Existing Workspace + +```fsharp +open Farmer +open Farmer.Builders + +let siem = sentinel { + workspace_name "existing-security-workspace" +} + +let deployment = arm { + location Location.EastUS + add_resource siem +} +``` + +#### Cost Considerations + +**Azure Sentinel Pricing** (Pay-as-you-go): + +| Tier | Included | Price per GB* | +|------|----------|---------------| +| **Pay-as-you-go** | None | **$2.30/GB** (same as Log Analytics) | +| **Commitment Tier** (100 GB/day) | 100 GB/day | **$2.00/GB** (~13% savings) | +| **Commitment Tier** (500 GB/day) | 500 GB/day | **$1.65/GB** (~28% savings) | + +*Approximate costs as of 2025. First 90 days free for new workspaces. + +**Example Monthly Costs:** + +| Scenario | Daily Ingestion | Commitment | **Monthly Cost** | +|----------|----------------|------------|------------------| +| **Small (Dev)** | 5 GB/day | Pay-as-you-go | **~$345/month** | +| **Medium** | 50 GB/day | Pay-as-you-go | **~$3,450/month** | +| **Large** | 100 GB/day | 100 GB tier | **~$6,000/month** | +| **Enterprise** | 500 GB/day | 500 GB tier | **~$24,750/month** | + +**Free Tier:** +- **First 90 days FREE** (up to 10 GB/day) for new Sentinel workspaces +- Good for POC and evaluation + +**What Sentinel provides for the cost:** +- Unlimited threat hunting queries +- Built-in analytics rules +- Automated threat response (SOAR) +- UEBA (User Entity Behavior Analytics) +- Threat intelligence integration +- Incident management + +**Cost Optimization:** + +1. **Use Basic Logs**: For high-volume, low-priority logs (~50% cheaper) +2. **Data Retention**: Keep 90 days in hot storage, archive rest +3. **Connector Selection**: Only enable needed data connectors +4. **Commitment Tiers**: Save 13-48% for predictable volumes +5. **Filter at Source**: Don't ingest unnecessary logs + +**Typical breakdown:** +- Security logs: 60% of ingestion +- Audit logs: 25% +- Diagnostic logs: 15% + +#### Security Benefits + +Azure Sentinel provides comprehensive SIEM/SOAR capabilities: + +* **Threat Detection**: AI-powered analytics detect threats across entire estate +* **Incident Response**: Automated playbooks respond to threats in real-time +* **Threat Hunting**: KQL-based hunting across petabytes of data +* **UEBA**: Detect insider threats and compromised accounts +* **Threat Intelligence**: Integration with Microsoft and third-party feeds +* **SOAR**: Security orchestration and automated response +* **Compliance**: Meet audit requirements for security monitoring + +#### Next Steps + +After enabling Sentinel: + +1. **Enable Data Connectors**: Azure AD, Office 365, Azure Activity, etc. +2. **Configure Analytics Rules**: Enable built-in detection rules +3. **Set Up Automation**: Create playbooks for automated response +4. **Configure UEBA**: Enable user and entity behavior analytics +5. **Integrate Threat Intelligence**: Connect threat feeds + +**Note:** This builder only enables Sentinel on the workspace. Data connectors, analytics rules, and playbooks must be configured separately. + +#### Compliance + +Azure Sentinel helps meet SOC requirements for: +- **NIST CSF**: DE (Detect), RS (Respond) +- **ISO 27001**: A.12.4 (Logging and monitoring), A.16.1 (Incident management) +- **PCI DSS**: Requirement 10 (Log monitoring), 11 (Security testing) +- **SOC 2**: CC7 (System monitoring) diff --git a/src/Farmer/Arm/ContainerRegistry.fs b/src/Farmer/Arm/ContainerRegistry.fs index 5785cd825..b9211174b 100644 --- a/src/Farmer/Arm/ContainerRegistry.fs +++ b/src/Farmer/Arm/ContainerRegistry.fs @@ -5,23 +5,46 @@ open Farmer open Farmer.ContainerRegistry let registries = - ResourceType("Microsoft.ContainerRegistry/registries", "2019-05-01") + ResourceType("Microsoft.ContainerRegistry/registries", "2023-07-01") type Registries = { Name: ResourceName Location: Location Sku: Sku AdminUserEnabled: bool + PublicNetworkAccess: ContainerRegistry.PublicNetworkAccess option + NetworkRuleSet: ContainerRegistry.NetworkRuleSet option Tags: Map } with interface IArmResource with member this.ResourceId = registries.resourceId this.Name - member this.JsonModel = {| - registries.Create(this.Name, this.Location, tags = this.Tags) with - sku = {| name = this.Sku.ToString() |} - properties = {| - adminUserEnabled = this.AdminUserEnabled - |} - |} \ No newline at end of file + member this.JsonModel = + let properties = {| + adminUserEnabled = this.AdminUserEnabled + publicNetworkAccess = + this.PublicNetworkAccess + |> Option.map (function + | ContainerRegistry.PublicNetworkAccess.Enabled -> "Enabled" :> obj + | ContainerRegistry.PublicNetworkAccess.Disabled -> "Disabled" :> obj) + |> Option.toObj + networkRuleSet = + this.NetworkRuleSet + |> Option.map (fun nrs -> + {| + defaultAction = + match nrs.DefaultAction with + | ContainerRegistry.NetworkRuleAction.Allow -> "Allow" + | ContainerRegistry.NetworkRuleAction.Deny -> "Deny" + ipRules = nrs.IpRules |> List.map (fun ip -> {| value = ip.Value; action = "Allow" |}) + |} + :> obj) + |> Option.toObj + |} + + {| + registries.Create(this.Name, this.Location, tags = this.Tags) with + sku = {| name = this.Sku.ToString() |} + properties = properties + |} \ No newline at end of file diff --git a/src/Farmer/Arm/Network.fs b/src/Farmer/Arm/Network.fs index 311b6e969..bf64a0880 100644 --- a/src/Farmer/Arm/Network.fs +++ b/src/Farmer/Arm/Network.fs @@ -48,6 +48,14 @@ let localNetworkGateways = let natGateways = ResourceType("Microsoft.Network/natGateways", "2024-07-01") +let ddosProtectionPlans = + ResourceType("Microsoft.Network/ddosProtectionPlans", "2024-05-01") + +let networkWatchers = ResourceType("Microsoft.Network/networkWatchers", "2024-05-01") + +let flowLogs = + ResourceType("Microsoft.Network/networkWatchers/flowLogs", "2024-05-01") + let privateEndpoints = ResourceType("Microsoft.Network/privateEndpoints", "2021-05-01") @@ -1060,4 +1068,78 @@ type NatGateway = { publicIpAddresses = this.PublicIpAddresses |> List.map LinkedResource.AsIdObject publicIpPrefixes = this.PublicIpPrefixes |> List.map LinkedResource.AsIdObject |} + |} + +type DdosProtectionPlan = { + Name: ResourceName + Location: Location + Tags: Map +} with + + interface IArmResource with + member this.ResourceId = ddosProtectionPlans.resourceId this.Name + + member this.JsonModel = + ddosProtectionPlans.Create(this.Name, this.Location, tags = this.Tags) + +type NetworkWatcher = { + Name: ResourceName + Location: Location + Tags: Map +} with + + interface IArmResource with + member this.ResourceId = networkWatchers.resourceId this.Name + + member this.JsonModel = networkWatchers.Create(this.Name, this.Location, tags = this.Tags) + +type FlowLog = { + Name: ResourceName + Location: Location + NetworkWatcher: ResourceName + TargetResourceId: ResourceId + StorageAccountId: ResourceId + Enabled: bool + RetentionDays: int + WorkspaceId: ResourceId option + Tags: Map +} with + + interface IArmResource with + member this.ResourceId = + flowLogs.resourceId (this.NetworkWatcher, this.Name) + + member this.JsonModel = + let dependencies = [ + this.TargetResourceId + this.StorageAccountId + yield! this.WorkspaceId |> Option.toList + ] + + {| + flowLogs.Create(this.NetworkWatcher / this.Name, this.Location, dependencies, this.Tags) with + properties = {| + targetResourceId = this.TargetResourceId.Eval() + storageId = this.StorageAccountId.Eval() + enabled = this.Enabled + retentionPolicy = {| + days = this.RetentionDays + enabled = this.RetentionDays > 0 + |} + format = {| ``type`` = "JSON"; version = 2 |} + flowAnalyticsConfiguration = + this.WorkspaceId + |> Option.map (fun workspaceId -> + {| + networkWatcherFlowAnalyticsConfiguration = {| + enabled = true + workspaceId = workspaceId.Eval() + workspaceRegion = this.Location.ArmValue + workspaceResourceId = workspaceId.Eval() + trafficAnalyticsInterval = 60 + |} + |} + :> obj) + |> Option.toObj + |} |} \ No newline at end of file diff --git a/src/Farmer/Arm/Policy.fs b/src/Farmer/Arm/Policy.fs new file mode 100644 index 000000000..deed27e6a --- /dev/null +++ b/src/Farmer/Arm/Policy.fs @@ -0,0 +1,144 @@ +[] +module Farmer.Arm.Policy + +open Farmer +open System.Text.Json + +let policyDefinitions = + ResourceType("Microsoft.Authorization/policyDefinitions", "2021-06-01") + +let policyAssignments = + ResourceType("Microsoft.Authorization/policyAssignments", "2024-04-01") + +[] +type PolicyMode = + | All + | Indexed + + member this.ArmValue = + match this with + | All -> "All" + | Indexed -> "Indexed" + +[] +type PolicyEffect = + | Audit + | AuditIfNotExists + | Deny + | DenyAction + | Disabled + | Modify + | Append + | DeployIfNotExists + + member this.ArmValue = + match this with + | Audit -> "Audit" + | AuditIfNotExists -> "AuditIfNotExists" + | Deny -> "Deny" + | DenyAction -> "DenyAction" + | Disabled -> "Disabled" + | Modify -> "Modify" + | Append -> "Append" + | DeployIfNotExists -> "DeployIfNotExists" + +[] +type EnforcementMode = + | Default + | DoNotEnforce + + member this.ArmValue = + match this with + | Default -> "Default" + | DoNotEnforce -> "DoNotEnforce" + +type PolicyDefinition = { + Name: ResourceName + DisplayName: string option + Description: string option + Mode: PolicyMode + PolicyRule: string + Parameters: Map option + Metadata: Map option +} with + + interface IArmResource with + member this.ResourceId = policyDefinitions.resourceId this.Name + + member this.JsonModel = + {| + policyDefinitions.Create(this.Name) with + properties = + {| + displayName = this.DisplayName |> Option.toObj + description = this.Description |> Option.toObj + mode = this.Mode.ArmValue + policyRule = JsonDocument.Parse(this.PolicyRule).RootElement + parameters = + match this.Parameters with + | Some p -> box p + | None -> null + metadata = + match this.Metadata with + | Some m -> box m + | None -> null + |} + :> obj + |} + +type PolicyAssignment = { + Name: ResourceName + DisplayName: string option + Description: string option + PolicyDefinitionId: ResourceId + EnforcementMode: EnforcementMode + Parameters: Map option + Scope: ResourceId option + NotScopes: string list + Location: Location option + Identity: Identity.ManagedIdentity option + Dependencies: ResourceId Set +} with + + interface IArmResource with + member this.ResourceId = policyAssignments.resourceId this.Name + + member this.JsonModel = + let dependencies = + this.Dependencies + + Set [ this.PolicyDefinitionId ] + + (match this.Identity with + | Some identity -> + identity.Dependencies + |> Set.ofList + |> Set.map (fun (rid: ResourceId) -> rid) + | None -> Set.empty) + + {| + policyAssignments.Create(this.Name, dependsOn = dependencies) with + location = + match this.Location with + | Some loc -> loc.ArmValue + | None -> null + identity = + match this.Identity with + | Some identity -> identity.ToArmJson :> obj + | None -> null + properties = + {| + displayName = this.DisplayName |> Option.toObj + description = this.Description |> Option.toObj + policyDefinitionId = this.PolicyDefinitionId.Eval() + enforcementMode = this.EnforcementMode.ArmValue + parameters = + match this.Parameters with + | Some p -> box p + | None -> null + notScopes = + match this.NotScopes with + | [] -> null + | scopes -> box scopes + |} + :> obj + scope = this.Scope |> Option.map (fun s -> s.Eval()) |> Option.toObj + |} diff --git a/src/Farmer/Arm/RecoveryServices.fs b/src/Farmer/Arm/RecoveryServices.fs new file mode 100644 index 000000000..4fc43566a --- /dev/null +++ b/src/Farmer/Arm/RecoveryServices.fs @@ -0,0 +1,118 @@ +module Farmer.Arm.RecoveryServices + +open Farmer + +module RecoveryServicesVaults = + let vaults = + ResourceType("Microsoft.RecoveryServices/vaults", "2024-04-01") + + let backupPolicies = + ResourceType("Microsoft.RecoveryServices/vaults/backupPolicies", "2024-04-01") + +[] +type SkuName = + | RS0 + | Standard + + member this.ArmValue = + match this with + | RS0 -> "RS0" + | Standard -> "Standard" + +[] +type BackupScheduleFrequency = + | Daily + | Weekly + + member this.ArmValue = + match this with + | Daily -> "Daily" + | Weekly -> "Weekly" + +type RecoveryServicesVault = { + Name: ResourceName + Location: Location + Sku: SkuName + Tags: Map +} with + + interface IArmResource with + member this.ResourceId = RecoveryServicesVaults.vaults.resourceId this.Name + + member this.JsonModel = + {| + RecoveryServicesVaults.vaults.Create(this.Name, this.Location, tags = this.Tags) with + sku = {| name = this.Sku.ArmValue |} + properties = {||} + |} + +type VmBackupPolicy = { + Name: ResourceName + VaultName: ResourceName + ScheduleFrequency: BackupScheduleFrequency + ScheduleTime: string + RetentionDays: int + WeeklyRetentionWeeks: int option + MonthlyRetentionMonths: int option + Dependencies: ResourceId Set +} with + + interface IArmResource with + member this.ResourceId = + RecoveryServicesVaults.backupPolicies.resourceId (this.VaultName, this.Name) + + member this.JsonModel = + let dependencies = this.Dependencies + Set [ RecoveryServicesVaults.vaults.resourceId this.VaultName ] + + {| + RecoveryServicesVaults.backupPolicies.Create(this.VaultName / this.Name, dependsOn = dependencies) with + properties = {| + backupManagementType = "AzureIaasVM" + schedulePolicy = {| + schedulePolicyType = "SimpleSchedulePolicy" + scheduleRunFrequency = this.ScheduleFrequency.ArmValue + scheduleRunTimes = [ this.ScheduleTime ] + |} + retentionPolicy = {| + retentionPolicyType = "LongTermRetentionPolicy" + dailySchedule = {| + retentionTimes = [ this.ScheduleTime ] + retentionDuration = {| + count = this.RetentionDays + durationType = "Days" + |} + |} + weeklySchedule = + match this.WeeklyRetentionWeeks with + | Some weeks -> + {| + daysOfTheWeek = [ "Sunday" ] + retentionTimes = [ this.ScheduleTime ] + retentionDuration = {| + count = weeks + durationType = "Weeks" + |} + |} + :> obj + | None -> null + monthlySchedule = + match this.MonthlyRetentionMonths with + | Some months -> + {| + retentionScheduleFormatType = "Weekly" + retentionScheduleWeekly = {| + daysOfTheWeek = [ "Sunday" ] + weeksOfTheMonth = [ "First" ] + |} + retentionTimes = [ this.ScheduleTime ] + retentionDuration = {| + count = months + durationType = "Months" + |} + |} + :> obj + | None -> null + |} + timeZone = "UTC" + |} + |} diff --git a/src/Farmer/Arm/Security.fs b/src/Farmer/Arm/Security.fs new file mode 100644 index 000000000..023087d6f --- /dev/null +++ b/src/Farmer/Arm/Security.fs @@ -0,0 +1,64 @@ +[] +module Farmer.Arm.Security + +open Farmer + +let pricings = ResourceType("Microsoft.Security/pricings", "2024-01-01") + +[] +type DefenderPlan = + | VirtualMachines + | SqlServers + | AppServices + | StorageAccounts + | SqlServerVirtualMachines + | KubernetesService + | ContainerRegistry + | KeyVaults + | Dns + | Arm + | OpenSourceRelationalDatabases + | Containers + | CosmosDbs + | CloudPosture + + member this.ArmValue = + match this with + | VirtualMachines -> "VirtualMachines" + | SqlServers -> "SqlServers" + | AppServices -> "AppServices" + | StorageAccounts -> "StorageAccounts" + | SqlServerVirtualMachines -> "SqlServerVirtualMachines" + | KubernetesService -> "KubernetesService" + | ContainerRegistry -> "ContainerRegistry" + | KeyVaults -> "KeyVaults" + | Dns -> "Dns" + | Arm -> "Arm" + | OpenSourceRelationalDatabases -> "OpenSourceRelationalDatabases" + | Containers -> "Containers" + | CosmosDbs -> "CosmosDbs" + | CloudPosture -> "CloudPosture" + +[] +type PricingTier = + | Free + | Standard + + member this.ArmValue = + match this with + | Free -> "Free" + | Standard -> "Standard" + +type DefenderPricing = { + Plan: DefenderPlan + Tier: PricingTier +} with + + interface IArmResource with + member this.ResourceId = pricings.resourceId (ResourceName this.Plan.ArmValue) + + member this.JsonModel = + {| + pricings.Create(ResourceName this.Plan.ArmValue) with + properties = {| pricingTier = this.Tier.ArmValue |} + |} diff --git a/src/Farmer/Arm/SecurityInsights.fs b/src/Farmer/Arm/SecurityInsights.fs new file mode 100644 index 000000000..ecfed450d --- /dev/null +++ b/src/Farmer/Arm/SecurityInsights.fs @@ -0,0 +1,26 @@ +[] +module Farmer.Arm.SecurityInsights + +open Farmer + +let onboardingStates = + ResourceType("Microsoft.SecurityInsights/onboardingStates", "2024-03-01") + +type SentinelOnboarding = { + WorkspaceName: ResourceName + Dependencies: ResourceId Set +} with + + interface IArmResource with + member this.ResourceId = + { onboardingStates.resourceId (this.WorkspaceName, ResourceName "default") with + Type = onboardingStates + } + + member this.JsonModel = + let dependencies = this.Dependencies + + {| + onboardingStates.Create(this.WorkspaceName / ResourceName "default", dependsOn = dependencies) with + properties = {||} + |} diff --git a/src/Farmer/Builders/Builders.ContainerRegistry.fs b/src/Farmer/Builders/Builders.ContainerRegistry.fs index d99add92a..428a92b70 100644 --- a/src/Farmer/Builders/Builders.ContainerRegistry.fs +++ b/src/Farmer/Builders/Builders.ContainerRegistry.fs @@ -9,26 +9,28 @@ type ContainerRegistryConfig = { Name: ResourceName Sku: Sku AdminUserEnabled: bool + PublicNetworkAccess: ContainerRegistry.PublicNetworkAccess option + NetworkRuleSet: ContainerRegistry.NetworkRuleSet option Tags: Map } with member this.LoginServer = - $"reference(resourceId('Microsoft.ContainerRegistry/registries', '{this.Name.Value}'),'2019-05-01').loginServer" + $"reference(resourceId('Microsoft.ContainerRegistry/registries', '{this.Name.Value}'),'2023-07-01').loginServer" |> ArmExpression.create /// Returns first Admin password if AdminUserEnabled member this.Password = - $"listCredentials(resourceId('Microsoft.ContainerRegistry/registries','{this.Name.Value}'),'2019-05-01').passwords[0].value" + $"listCredentials(resourceId('Microsoft.ContainerRegistry/registries','{this.Name.Value}'),'2023-07-01').passwords[0].value" |> ArmExpression.create /// Returns second Admin password if AdminUserEnabled member this.Password2 = - $"listCredentials(resourceId('Microsoft.ContainerRegistry/registries','{this.Name.Value}'),'2019-05-01').passwords[1].value" + $"listCredentials(resourceId('Microsoft.ContainerRegistry/registries','{this.Name.Value}'),'2023-07-01').passwords[1].value" |> ArmExpression.create /// Returns Admin username if AdminUserEnabled member this.Username = - $"listCredentials(resourceId('Microsoft.ContainerRegistry/registries','{this.Name.Value}'),'2019-05-01').username" + $"listCredentials(resourceId('Microsoft.ContainerRegistry/registries','{this.Name.Value}'),'2023-07-01').username" |> ArmExpression.create interface IBuilder with @@ -40,6 +42,8 @@ type ContainerRegistryConfig = { Location = location Sku = this.Sku AdminUserEnabled = this.AdminUserEnabled + PublicNetworkAccess = this.PublicNetworkAccess + NetworkRuleSet = this.NetworkRuleSet Tags = this.Tags } ] @@ -49,6 +53,8 @@ type ContainerRegistryBuilder() = Name = ResourceName.Empty Sku = Basic AdminUserEnabled = false + PublicNetworkAccess = None + NetworkRuleSet = None Tags = Map.empty } @@ -61,7 +67,6 @@ type ContainerRegistryBuilder() = member this.Name(state: ContainerRegistryConfig, name: string) = this.Name(state, ResourceName name) - [] /// Sets the name of the SKU/Tier for the Container Registry instance. member _.Sku(state: ContainerRegistryConfig, sku) = { state with Sku = sku } @@ -70,6 +75,60 @@ type ContainerRegistryBuilder() = /// Enables the admin user on the Azure Container Registry. member _.EnableAdminUser(state: ContainerRegistryConfig) = { state with AdminUserEnabled = true } + [] + /// Enables public network access to the registry. + member _.EnablePublicNetworkAccess(state: ContainerRegistryConfig) = { + state with + PublicNetworkAccess = Some ContainerRegistry.PublicNetworkAccess.Enabled + } + + [] + /// Disables public network access to the registry (Premium SKU only). + member _.DisablePublicNetworkAccess(state: ContainerRegistryConfig) = { + state with + PublicNetworkAccess = Some ContainerRegistry.PublicNetworkAccess.Disabled + } + + [] + /// Adds an IP address or CIDR range to the allow list (Premium SKU only). + member _.AddIpRule(state: ContainerRegistryConfig, ipAddressOrCidr: string) = + let currentRules = + state.NetworkRuleSet + |> Option.defaultValue { + DefaultAction = ContainerRegistry.NetworkRuleAction.Deny + IpRules = [] + } + + { + state with + NetworkRuleSet = + Some { + currentRules with + IpRules = currentRules.IpRules @ [ { Value = ipAddressOrCidr } ] + } + } + + [] + /// Adds multiple IP addresses or CIDR ranges to the allow list (Premium SKU only). + member _.AddIpRules(state: ContainerRegistryConfig, ipAddressesOrCidrs: string list) = + let currentRules = + state.NetworkRuleSet + |> Option.defaultValue { + DefaultAction = ContainerRegistry.NetworkRuleAction.Deny + IpRules = [] + } + + { + state with + NetworkRuleSet = + Some { + currentRules with + IpRules = + currentRules.IpRules + @ (ipAddressesOrCidrs |> List.map (fun ip -> { Value = ip })) + } + } + interface ITaggable with member _.Add state tags = { state with diff --git a/src/Farmer/Builders/Builders.DdosProtectionPlan.fs b/src/Farmer/Builders/Builders.DdosProtectionPlan.fs new file mode 100644 index 000000000..3aadca284 --- /dev/null +++ b/src/Farmer/Builders/Builders.DdosProtectionPlan.fs @@ -0,0 +1,40 @@ +[] +module Farmer.Builders.DdosProtectionPlan + +open Farmer +open Farmer.Arm.Network + +type DdosProtectionPlanConfig = { + Name: ResourceName + Tags: Map +} with + + interface IBuilder with + member this.ResourceId = ddosProtectionPlans.resourceId this.Name + + member this.BuildResources location = [ + { + DdosProtectionPlan.Name = this.Name + Location = location + Tags = this.Tags + } + ] + +type DdosProtectionPlanBuilder() = + member _.Yield _ = { + Name = ResourceName.Empty + Tags = Map.empty + } + + /// Sets the name of the DDoS Protection Plan. + [] + member _.Name(state: DdosProtectionPlanConfig, name: string) = { state with Name = ResourceName name } + + interface ITaggable with + member _.Add state tags = { + state with + Tags = state.Tags |> Map.merge tags + } + +/// Builds a DDoS Protection Plan resource. +let ddosProtectionPlan = DdosProtectionPlanBuilder() diff --git a/src/Farmer/Builders/Builders.DefenderForCloud.fs b/src/Farmer/Builders/Builders.DefenderForCloud.fs new file mode 100644 index 000000000..ed76ac0b6 --- /dev/null +++ b/src/Farmer/Builders/Builders.DefenderForCloud.fs @@ -0,0 +1,45 @@ +[] +module Farmer.Builders.DefenderForCloud + +open Farmer +open Farmer.Arm.Security + +type DefenderForCloudConfig = { + Plan: DefenderPlan + Tier: PricingTier +} with + + interface IBuilder with + member this.ResourceId = pricings.resourceId (ResourceName this.Plan.ArmValue) + + member this.BuildResources _ = [ + { + DefenderPricing.Plan = this.Plan + Tier = this.Tier + } + ] + +type DefenderForCloudBuilder() = + member _.Yield _ = { + Plan = DefenderPlan.VirtualMachines + Tier = PricingTier.Standard + } + + /// Sets the Defender plan to enable (VirtualMachines, SqlServers, AppServices, etc.). + [] + member _.Plan(state: DefenderForCloudConfig, plan: DefenderPlan) = { state with Plan = plan } + + /// Sets the pricing tier (Standard for enabled, Free for disabled). Default is Standard. + [] + member _.Tier(state: DefenderForCloudConfig, tier: PricingTier) = { state with Tier = tier } + + /// Enables the Defender plan (sets tier to Standard). + [] + member _.Enable(state: DefenderForCloudConfig) = { state with Tier = PricingTier.Standard } + + /// Disables the Defender plan (sets tier to Free). + [] + member _.Disable(state: DefenderForCloudConfig) = { state with Tier = PricingTier.Free } + +/// Enables Microsoft Defender for Cloud (formerly Security Center) plans. +let defenderForCloud = DefenderForCloudBuilder() diff --git a/src/Farmer/Builders/Builders.NetworkWatcher.fs b/src/Farmer/Builders/Builders.NetworkWatcher.fs new file mode 100644 index 000000000..0845e26b3 --- /dev/null +++ b/src/Farmer/Builders/Builders.NetworkWatcher.fs @@ -0,0 +1,132 @@ +[] +module Farmer.Builders.NetworkWatcher + +open Farmer +open Farmer.Arm.Network + +type NetworkWatcherConfig = { + Name: ResourceName + Tags: Map +} with + + interface IBuilder with + member this.ResourceId = networkWatchers.resourceId this.Name + + member this.BuildResources location = [ + { + NetworkWatcher.Name = this.Name + Location = location + Tags = this.Tags + } + ] + +type NetworkWatcherBuilder() = + member _.Yield _ = { + Name = ResourceName.Empty + Tags = Map.empty + } + + /// Sets the name of the Network Watcher. + [] + member _.Name(state: NetworkWatcherConfig, name: string) = { state with Name = ResourceName name } + + interface ITaggable with + member _.Add state tags = { + state with + Tags = state.Tags |> Map.merge tags + } + +type FlowLogConfig = { + Name: ResourceName + NetworkWatcher: ResourceName + TargetNsg: ResourceId option + StorageAccount: ResourceId option + RetentionDays: int + LogAnalytics: ResourceId option + Tags: Map +} with + + interface IBuilder with + member this.ResourceId = + match this.NetworkWatcher with + | name when name <> ResourceName.Empty -> flowLogs.resourceId (name, this.Name) + | _ -> raiseFarmer "Flow log must be linked to a Network Watcher" + + member this.BuildResources location = + match this.TargetNsg, this.StorageAccount with + | Some nsg, Some storage -> [ + { + FlowLog.Name = this.Name + Location = location + NetworkWatcher = this.NetworkWatcher + TargetResourceId = nsg + StorageAccountId = storage + Enabled = true + RetentionDays = this.RetentionDays + WorkspaceId = this.LogAnalytics + Tags = this.Tags + } + ] + | None, _ -> raiseFarmer "Flow log must have a target NSG (use link_to_nsg)" + | _, None -> raiseFarmer "Flow log must have a storage account (use link_to_storage_account)" + +type FlowLogBuilder() = + member _.Yield _ = { + Name = ResourceName.Empty + NetworkWatcher = ResourceName.Empty + TargetNsg = None + StorageAccount = None + RetentionDays = 7 + LogAnalytics = None + Tags = Map.empty + } + + /// Sets the name of the flow log. + [] + member _.Name(state: FlowLogConfig, name: string) = { state with Name = ResourceName name } + + /// Links the flow log to a Network Watcher. + [] + member _.LinkToNetworkWatcher(state: FlowLogConfig, networkWatcher: NetworkWatcherConfig) = { + state with + NetworkWatcher = (networkWatcher :> IBuilder).ResourceId.Name + } + + member _.LinkToNetworkWatcher(state: FlowLogConfig, networkWatcherName: string) = { + state with + NetworkWatcher = ResourceName networkWatcherName + } + + /// Links the flow log to an NSG. + [] + member _.LinkToNsg(state: FlowLogConfig, nsgId: ResourceId) = { state with TargetNsg = Some nsgId } + + /// Links the flow log to a storage account for storing logs. + [] + member _.LinkToStorageAccount(state: FlowLogConfig, storageId: ResourceId) = { + state with + StorageAccount = Some storageId + } + + /// Sets the retention period in days (0 = unlimited, default 7 days). + [] + member _.RetentionDays(state: FlowLogConfig, days: int) = { state with RetentionDays = days } + + /// Enables Traffic Analytics by linking to a Log Analytics Workspace. + [] + member _.EnableTrafficAnalytics(state: FlowLogConfig, workspaceId: ResourceId) = { + state with + LogAnalytics = Some workspaceId + } + + interface ITaggable with + member _.Add state tags = { + state with + Tags = state.Tags |> Map.merge tags + } + +/// Builds a Network Watcher resource. +let networkWatcher = NetworkWatcherBuilder() + +/// Builds a Flow Log resource for NSG monitoring. +let flowLog = FlowLogBuilder() diff --git a/src/Farmer/Builders/Builders.Policy.fs b/src/Farmer/Builders/Builders.Policy.fs new file mode 100644 index 000000000..b5c1ac452 --- /dev/null +++ b/src/Farmer/Builders/Builders.Policy.fs @@ -0,0 +1,232 @@ +[] +module Farmer.Builders.Policy + +open Farmer +open Farmer.Arm.Policy + +type PolicyDefinitionConfig = { + Name: ResourceName + DisplayName: string option + Description: string option + Mode: PolicyMode + PolicyRule: string + Parameters: Map option + Metadata: Map option +} with + + interface IBuilder with + member this.ResourceId = policyDefinitions.resourceId this.Name + + member this.BuildResources _ = [ + { + PolicyDefinition.Name = this.Name + DisplayName = this.DisplayName + Description = this.Description + Mode = this.Mode + PolicyRule = this.PolicyRule + Parameters = this.Parameters + Metadata = this.Metadata + } + ] + +type PolicyDefinitionBuilder() = + member _.Yield _ = { + Name = ResourceName.Empty + DisplayName = None + Description = None + Mode = PolicyMode.All + PolicyRule = "" + Parameters = None + Metadata = None + } + + /// Sets the name of the policy definition. + [] + member _.Name(state: PolicyDefinitionConfig, name: string) = { state with Name = ResourceName name } + + /// Sets the display name of the policy definition. + [] + member _.DisplayName(state: PolicyDefinitionConfig, displayName: string) = { + state with + DisplayName = Some displayName + } + + /// Sets the description of the policy definition. + [] + member _.Description(state: PolicyDefinitionConfig, description: string) = { + state with + Description = Some description + } + + /// Sets the mode of the policy definition (All or Indexed). Default is All. + [] + member _.Mode(state: PolicyDefinitionConfig, mode: PolicyMode) = { state with Mode = mode } + + /// Sets the policy rule as a JSON string. + [] + member _.PolicyRule(state: PolicyDefinitionConfig, rule: string) = { state with PolicyRule = rule } + + /// Sets the parameters for the policy definition. + [] + member _.Parameters(state: PolicyDefinitionConfig, parameters: Map) = { + state with + Parameters = Some parameters + } + + /// Sets metadata for the policy definition (category, version, etc.). + [] + member _.AddMetadata(state: PolicyDefinitionConfig, metadata: Map) = { + state with + Metadata = Some metadata + } + + /// Adds a single metadata field to the policy definition. + [] + member _.AddMetadataField(state: PolicyDefinitionConfig, key: string, value: string) = { + state with + Metadata = + match state.Metadata with + | Some existing -> Some(existing.Add(key, value)) + | None -> Some(Map.ofList [ key, value ]) + } + +type PolicyAssignmentConfig = { + Name: ResourceName + DisplayName: string option + Description: string option + PolicyDefinition: PolicyDefinitionConfig option + PolicyDefinitionId: ResourceId option + EnforcementMode: EnforcementMode + Parameters: Map option + Scope: ResourceId option + NotScopes: string list + Location: Location option + Identity: Identity.ManagedIdentity option + Dependencies: ResourceId Set +} with + + interface IBuilder with + member this.ResourceId = policyAssignments.resourceId this.Name + + member this.BuildResources location = + let policyDefId = + match this.PolicyDefinition, this.PolicyDefinitionId with + | Some def, _ -> (def :> IBuilder).ResourceId + | None, Some id -> id + | None, None -> raiseFarmer "Policy assignment must reference a policy definition" + + [ + { + PolicyAssignment.Name = this.Name + DisplayName = this.DisplayName + Description = this.Description + PolicyDefinitionId = policyDefId + EnforcementMode = this.EnforcementMode + Parameters = this.Parameters + Scope = this.Scope + NotScopes = this.NotScopes + Location = this.Location |> Option.orElse (Some location) + Identity = this.Identity + Dependencies = this.Dependencies + } + ] + +type PolicyAssignmentBuilder() = + member _.Yield _ = { + Name = ResourceName.Empty + DisplayName = None + Description = None + PolicyDefinition = None + PolicyDefinitionId = None + EnforcementMode = EnforcementMode.Default + Parameters = None + Scope = None + NotScopes = [] + Location = None + Identity = None + Dependencies = Set.empty + } + + /// Sets the name of the policy assignment. + [] + member _.Name(state: PolicyAssignmentConfig, name: string) = { state with Name = ResourceName name } + + /// Sets the display name of the policy assignment. + [] + member _.DisplayName(state: PolicyAssignmentConfig, displayName: string) = { + state with + DisplayName = Some displayName + } + + /// Sets the description of the policy assignment. + [] + member _.Description(state: PolicyAssignmentConfig, description: string) = { + state with + Description = Some description + } + + /// Links to a policy definition config built in this deployment. + [] + member _.LinkToPolicy(state: PolicyAssignmentConfig, policy: PolicyDefinitionConfig) = { + state with + PolicyDefinition = Some policy + } + + /// Links to an existing policy definition by resource ID. + [] + member _.LinkToPolicyId(state: PolicyAssignmentConfig, policyId: ResourceId) = { + state with + PolicyDefinitionId = Some policyId + } + + /// Sets the enforcement mode (Default or DoNotEnforce). + [] + member _.EnforcementMode(state: PolicyAssignmentConfig, mode: EnforcementMode) = { + state with + EnforcementMode = mode + } + + /// Sets the parameters for the policy assignment. + [] + member _.Parameters(state: PolicyAssignmentConfig, parameters: Map) = { + state with + Parameters = Some parameters + } + + /// Sets the scope for the policy assignment. + [] + member _.Scope(state: PolicyAssignmentConfig, scope: ResourceId) = { state with Scope = Some scope } + + /// Adds resource scopes to exclude from this policy assignment. + [] + member _.NotScopes(state: PolicyAssignmentConfig, notScopes: string list) = { + state with + NotScopes = notScopes + } + + /// Sets the location for the policy assignment (required for policies with managed identity). + [] + member _.Location(state: PolicyAssignmentConfig, location: Location) = { + state with + Location = Some location + } + + /// Assigns a system-assigned managed identity to the policy assignment (required for DeployIfNotExists and Modify effects). + [] + member _.SystemIdentity(state: PolicyAssignmentConfig) = { + state with + Identity = Some { SystemAssigned = Enabled; UserAssigned = [] } + } + + /// Adds a dependency to this policy assignment. + [] + member _.AddDependency(state: PolicyAssignmentConfig, dependency: ResourceId) = { + state with + Dependencies = state.Dependencies.Add dependency + } + +/// Builds a policy definition resource. +let policyDefinition = PolicyDefinitionBuilder() + +/// Builds a policy assignment resource. +let policyAssignment = PolicyAssignmentBuilder() diff --git a/src/Farmer/Builders/Builders.RecoveryServices.fs b/src/Farmer/Builders/Builders.RecoveryServices.fs new file mode 100644 index 000000000..ead2515a8 --- /dev/null +++ b/src/Farmer/Builders/Builders.RecoveryServices.fs @@ -0,0 +1,158 @@ +[] +module Farmer.Builders.RecoveryServices + +open Farmer +open Farmer.Arm.RecoveryServices +open Farmer.Arm.RecoveryServices.RecoveryServicesVaults + +type RecoveryServicesVaultConfig = { + Name: ResourceName + Sku: SkuName + Tags: Map +} with + + interface IBuilder with + member this.ResourceId = RecoveryServicesVaults.vaults.resourceId this.Name + + member this.BuildResources location = [ + { + RecoveryServicesVault.Name = this.Name + Location = location + Sku = this.Sku + Tags = this.Tags + } + ] + +type RecoveryServicesVaultBuilder() = + member _.Yield _ = { + Name = ResourceName.Empty + Sku = SkuName.Standard + Tags = Map.empty + } + + /// Sets the name of the Recovery Services Vault. + [] + member _.Name(state: RecoveryServicesVaultConfig, name: string) = { state with Name = ResourceName name } + + /// Sets the SKU (RS0 for free tier, Standard for production). Default is Standard. + [] + member _.Sku(state: RecoveryServicesVaultConfig, sku: SkuName) = { state with Sku = sku } + + interface ITaggable with + member _.Add state tags = { + state with + Tags = state.Tags |> Map.merge tags + } + +type VmBackupPolicyConfig = { + Name: ResourceName + VaultName: ResourceName + Vault: RecoveryServicesVaultConfig option + ScheduleFrequency: BackupScheduleFrequency + ScheduleTime: string + RetentionDays: int + WeeklyRetentionWeeks: int option + MonthlyRetentionMonths: int option + Dependencies: ResourceId Set +} with + + interface IBuilder with + member this.ResourceId = + let vaultName = + match this.Vault with + | Some vault -> vault.Name + | None -> this.VaultName + + RecoveryServicesVaults.backupPolicies.resourceId (vaultName, this.Name) + + member this.BuildResources _ = + let vaultName = + match this.Vault with + | Some vault -> vault.Name + | None -> this.VaultName + + [ + { + VmBackupPolicy.Name = this.Name + VaultName = vaultName + ScheduleFrequency = this.ScheduleFrequency + ScheduleTime = this.ScheduleTime + RetentionDays = this.RetentionDays + WeeklyRetentionWeeks = this.WeeklyRetentionWeeks + MonthlyRetentionMonths = this.MonthlyRetentionMonths + Dependencies = this.Dependencies + } + ] + +type VmBackupPolicyBuilder() = + member _.Yield _ = { + Name = ResourceName.Empty + VaultName = ResourceName.Empty + Vault = None + ScheduleFrequency = BackupScheduleFrequency.Daily + ScheduleTime = "2023-01-01T03:00:00Z" + RetentionDays = 30 + WeeklyRetentionWeeks = None + MonthlyRetentionMonths = None + Dependencies = Set.empty + } + + /// Sets the name of the backup policy. + [] + member _.Name(state: VmBackupPolicyConfig, name: string) = { state with Name = ResourceName name } + + /// Links to a Recovery Services Vault. + [] + member _.LinkToVault(state: VmBackupPolicyConfig, vault: RecoveryServicesVaultConfig) = { + state with + Vault = Some vault + } + + /// Sets the vault name directly (for existing vaults). + [] + member _.VaultName(state: VmBackupPolicyConfig, vaultName: string) = { + state with + VaultName = ResourceName vaultName + } + + /// Sets the backup schedule frequency (Daily or Weekly). Default is Daily. + [] + member _.ScheduleFrequency(state: VmBackupPolicyConfig, frequency: BackupScheduleFrequency) = { + state with + ScheduleFrequency = frequency + } + + /// Sets the backup schedule time in ISO format (e.g., "2023-01-01T03:00:00Z"). Default is 3 AM UTC. + [] + member _.ScheduleTime(state: VmBackupPolicyConfig, time: string) = { state with ScheduleTime = time } + + /// Sets daily retention in days (7-9999). Default is 30 days. + [] + member _.RetentionDays(state: VmBackupPolicyConfig, days: int) = { state with RetentionDays = days } + + /// Sets weekly retention in weeks (1-5163). + [] + member _.WeeklyRetentionWeeks(state: VmBackupPolicyConfig, weeks: int) = { + state with + WeeklyRetentionWeeks = Some weeks + } + + /// Sets monthly retention in months (1-1188). + [] + member _.MonthlyRetentionMonths(state: VmBackupPolicyConfig, months: int) = { + state with + MonthlyRetentionMonths = Some months + } + + /// Adds a dependency to this backup policy. + [] + member _.AddDependency(state: VmBackupPolicyConfig, dependency: ResourceId) = { + state with + Dependencies = state.Dependencies.Add dependency + } + +/// Builds a Recovery Services Vault. +let recoveryServicesVault = RecoveryServicesVaultBuilder() + +/// Builds a VM backup policy. +let vmBackupPolicy = VmBackupPolicyBuilder() diff --git a/src/Farmer/Builders/Builders.Sentinel.fs b/src/Farmer/Builders/Builders.Sentinel.fs new file mode 100644 index 000000000..a1ba854ce --- /dev/null +++ b/src/Farmer/Builders/Builders.Sentinel.fs @@ -0,0 +1,61 @@ +[] +module Farmer.Builders.Sentinel + +open Farmer +open Farmer.Arm.SecurityInsights +open Farmer.Arm.LogAnalytics + +type SentinelConfig = { + WorkspaceName: ResourceName + WorkspaceId: ResourceId option + Dependencies: ResourceId Set +} with + + interface IBuilder with + member this.ResourceId = + onboardingStates.resourceId (this.WorkspaceName, ResourceName "default") + + member this.BuildResources _ = + let dependencies = + match this.WorkspaceId with + | Some wsId -> this.Dependencies + Set [ wsId ] + | None -> this.Dependencies + + [ + { + SentinelOnboarding.WorkspaceName = this.WorkspaceName + Dependencies = dependencies + } + ] + +type SentinelBuilder() = + member _.Yield _ = { + WorkspaceName = ResourceName.Empty + WorkspaceId = None + Dependencies = Set.empty + } + + /// Links to a Log Analytics Workspace to enable Sentinel on. + [] + member _.LinkToWorkspace(state: SentinelConfig, workspace: IBuilder) = { + state with + WorkspaceName = workspace.ResourceId.Name + WorkspaceId = Some workspace.ResourceId + } + + /// Sets the workspace name directly (for existing workspaces). + [] + member _.WorkspaceName(state: SentinelConfig, workspaceName: string) = { + state with + WorkspaceName = ResourceName workspaceName + } + + /// Adds a dependency to Sentinel onboarding. + [] + member _.AddDependency(state: SentinelConfig, dependency: ResourceId) = { + state with + Dependencies = state.Dependencies.Add dependency + } + +/// Enables Azure Sentinel (SIEM) on a Log Analytics Workspace. +let sentinel = SentinelBuilder() diff --git a/src/Farmer/Common.fs b/src/Farmer/Common.fs index d82be4ae8..e8447201e 100644 --- a/src/Farmer/Common.fs +++ b/src/Farmer/Common.fs @@ -1,4 +1,4 @@ -namespace Farmer +namespace Farmer open System @@ -1926,6 +1926,25 @@ module ContainerRegistry = | Standard | Premium + /// Public network access option for Container Registry + type PublicNetworkAccess = + | Enabled + | Disabled + + /// Network rule action + type NetworkRuleAction = + | Allow + | Deny + + /// IP rule for Container Registry + type IPRule = { Value: string } + + /// Network rule set for Container Registry + type NetworkRuleSet = { + DefaultAction: NetworkRuleAction + IpRules: IPRule list + } + module ContainerRegistryValidation = open Validation diff --git a/src/Farmer/Farmer.fsproj b/src/Farmer/Farmer.fsproj index 7bbcc7fe0..c61e1a4a7 100644 --- a/src/Farmer/Farmer.fsproj +++ b/src/Farmer/Farmer.fsproj @@ -1,4 +1,4 @@ - + Farmer @@ -101,8 +101,12 @@ + + + + @@ -154,7 +158,9 @@ + + @@ -167,6 +173,7 @@ + @@ -187,9 +194,12 @@ + + + diff --git a/src/Tests/AllTests.fs b/src/Tests/AllTests.fs index 8aaed2260..2ed7b3798 100644 --- a/src/Tests/AllTests.fs +++ b/src/Tests/AllTests.fs @@ -41,7 +41,9 @@ let allTests = ContainerService.tests Cosmos.tests Databricks.tests + DdosProtectionPlan.tests DedicatedHosts.tests + DefenderForCloud.tests DeploymentScript.tests DiagnosticSettings.tests Disk.tests @@ -57,16 +59,20 @@ let allTests = JsonRegression.tests KeyVault.tests Network.tests + NetworkWatcher.tests LoadBalancer.tests LogAnalytics.tests LogicApps.tests Maps.tests NetworkSecurityGroup.tests OperationsManagement.tests + Policy.tests PostgreSQL.tests PrivateLink.tests + RecoveryServices.tests ResourceGroup.tests RoleAssignment.tests + Sentinel.tests ServiceBus.tests SignalR.tests Sql.tests diff --git a/src/Tests/ContainerRegistry.fs b/src/Tests/ContainerRegistry.fs index 0332eece2..cfd64f29b 100644 --- a/src/Tests/ContainerRegistry.fs +++ b/src/Tests/ContainerRegistry.fs @@ -63,6 +63,17 @@ let shouldHaveAdminUserDisabled (r: RegistryJson) = Expect.isFalse b "adminUserEnabled was expected to be disabled" r +let shouldHavePublicNetworkAccessDisabled (r: RegistryJson) = + let value = resource(r).properties.["publicNetworkAccess"].GetString() + Expect.equal value "Disabled" "publicNetworkAccess should be Disabled" + r + +let shouldHaveIpRules count (r: RegistryJson) = + let rules = resource(r).properties.["networkRuleSet"].GetProperty("ipRules") + let actualCount = rules.GetArrayLength() + Expect.equal actualCount count $"Expected {count} IP rules but found {actualCount}" + r + let tests = testList "Container Registry" [ test "Basic resource settings are written to template resource" { @@ -72,7 +83,7 @@ let tests = } |> whenWritten |> shouldHaveType "Microsoft.ContainerRegistry/registries" - |> shouldHaveApiVersion "2019-05-01" + |> shouldHaveApiVersion "2023-07-01" |> shouldHaveName "validContainerRegistryName" |> shouldHaveSku Premium |> shouldHaveALocation @@ -90,6 +101,39 @@ let tests = |> ignore } + test "Disabling public network access sets property correctly" { + containerRegistry { + name "secureRegistry" + sku Premium + disable_public_network_access + } + |> whenWritten + |> shouldHavePublicNetworkAccessDisabled + |> ignore + } + + test "Adding IP rules creates network rule set" { + containerRegistry { + name "restrictedRegistry" + sku Premium + add_ip_rules [ "203.0.113.0/24"; "198.51.100.5" ] + } + |> whenWritten + |> shouldHaveIpRules 2 + |> ignore + } + + test "Single IP rule can be added" { + containerRegistry { + name "singleIpRegistry" + sku Premium + add_ip_rule "203.0.113.10" + } + |> whenWritten + |> shouldHaveIpRules 1 + |> ignore + } + testList "Container Registry Name Validation tests" [ let invalidNameCases = [ "Empty Account", "", "cannot be empty", "Name too short" diff --git a/src/Tests/DdosProtectionPlan.fs b/src/Tests/DdosProtectionPlan.fs new file mode 100644 index 000000000..c97c544f2 --- /dev/null +++ b/src/Tests/DdosProtectionPlan.fs @@ -0,0 +1,49 @@ +module DdosProtectionPlan + +open Expecto +open Farmer +open Farmer.Builders +open Farmer.Arm.Network +open Newtonsoft.Json.Linq + +let tests = + testList "DDoS Protection Plan" [ + test "Creates a basic DDoS Protection Plan" { + let ddos = ddosProtectionPlan { name "my-ddos-plan" } + let deployment = arm { add_resources [ ddos ] } + + let jobj = deployment.Template |> Writer.toJson |> JToken.Parse + let ddosResource = jobj.SelectToken("resources[?(@.type=='Microsoft.Network/ddosProtectionPlans')]") + + Expect.isNotNull ddosResource "DDoS Protection Plan resource should exist" + Expect.equal (ddosResource.SelectToken("name").ToString()) "my-ddos-plan" "Name should be correct" + } + + test "DDoS Protection Plan can have tags" { + let ddos = + ddosProtectionPlan { + name "my-ddos-plan" + add_tags [ "environment", "production"; "cost-center", "security" ] + } + + let deployment = arm { add_resources [ ddos ] } + let jobj = deployment.Template |> Writer.toJson |> JToken.Parse + let ddosResource = jobj.SelectToken("resources[?(@.type=='Microsoft.Network/ddosProtectionPlans')]") + + let tags = ddosResource.SelectToken("tags") + Expect.isNotNull tags "Tags should exist" + Expect.equal (tags.SelectToken("environment").ToString()) "production" "Environment tag should be correct" + Expect.equal + (tags.SelectToken("cost-center").ToString()) + "security" + "Cost-center tag should be correct" + } + + test "DDoS Protection Plan has correct resource ID" { + let ddos = ddosProtectionPlan { name "test-plan" } + let resourceId = (ddos :> IBuilder).ResourceId + + Expect.equal resourceId.Type.Type "Microsoft.Network/ddosProtectionPlans" "Type should be correct" + Expect.equal resourceId.Name.Value "test-plan" "Name should be correct" + } + ] diff --git a/src/Tests/DefenderForCloud.fs b/src/Tests/DefenderForCloud.fs new file mode 100644 index 000000000..61089a6a1 --- /dev/null +++ b/src/Tests/DefenderForCloud.fs @@ -0,0 +1,76 @@ +module DefenderForCloud + +open Expecto +open Farmer +open Farmer.Builders +open Farmer.Arm.Security +open Newtonsoft.Json.Linq + +let tests = + testList "Defender for Cloud" [ + test "Enables Defender for Virtual Machines" { + let defender = defenderForCloud { plan DefenderPlan.VirtualMachines } + + let deployment = arm { add_resource defender } + let jobj = deployment.Template |> Writer.toJson |> JToken.Parse + + let defenderResource = jobj.SelectToken("resources[?(@.type=='Microsoft.Security/pricings')]") + + Expect.isNotNull defenderResource "Defender resource should exist" + Expect.equal (defenderResource.SelectToken("name").ToString()) "VirtualMachines" "Plan should be VirtualMachines" + + Expect.equal + (defenderResource.SelectToken("properties.pricingTier").ToString()) + "Standard" + "Tier should be Standard" + } + + test "Can enable multiple Defender plans" { + let vmDefender = defenderForCloud { plan DefenderPlan.VirtualMachines } + let sqlDefender = defenderForCloud { plan DefenderPlan.SqlServers } + let storageDefender = defenderForCloud { plan DefenderPlan.StorageAccounts } + + let deployment = arm { add_resources [ vmDefender; sqlDefender; storageDefender ] } + + let jobj = deployment.Template |> Writer.toJson |> JToken.Parse + let resources = jobj.SelectTokens("resources[?(@.type=='Microsoft.Security/pricings')]") + + Expect.equal (Seq.length resources) 3 "Should have 3 Defender plans" + } + + test "Can disable a Defender plan" { + let defender = + defenderForCloud { + plan DefenderPlan.AppServices + disable + } + + let deployment = arm { add_resource defender } + let jobj = deployment.Template |> Writer.toJson |> JToken.Parse + + let defenderResource = jobj.SelectToken("resources[?(@.type=='Microsoft.Security/pricings')]") + + Expect.equal + (defenderResource.SelectToken("properties.pricingTier").ToString()) + "Free" + "Tier should be Free when disabled" + } + + test "Can explicitly enable a Defender plan" { + let defender = + defenderForCloud { + plan DefenderPlan.Containers + enable + } + + let deployment = arm { add_resource defender } + let jobj = deployment.Template |> Writer.toJson |> JToken.Parse + + let defenderResource = jobj.SelectToken("resources[?(@.type=='Microsoft.Security/pricings')]") + + Expect.equal + (defenderResource.SelectToken("properties.pricingTier").ToString()) + "Standard" + "Tier should be Standard when enabled" + } + ] diff --git a/src/Tests/NetworkWatcher.fs b/src/Tests/NetworkWatcher.fs new file mode 100644 index 000000000..061d355fc --- /dev/null +++ b/src/Tests/NetworkWatcher.fs @@ -0,0 +1,94 @@ +module NetworkWatcher + +open Expecto +open Farmer +open Farmer.Builders +open Farmer.Arm.Network +open Newtonsoft.Json.Linq + +let tests = + testList "Network Watcher" [ + test "Creates a basic Network Watcher" { + let watcher = networkWatcher { name "my-network-watcher" } + + let deployment = arm { add_resources [ watcher ] } + let jobj = deployment.Template |> Writer.toJson |> JToken.Parse + let watcherResource = jobj.SelectToken("resources[?(@.type=='Microsoft.Network/networkWatchers')]") + + Expect.isNotNull watcherResource "Network Watcher resource should exist" + Expect.equal (watcherResource.SelectToken("name").ToString()) "my-network-watcher" "Name should be correct" + } + + test "Creates a flow log with NSG and storage" { + let nsg = nsg { name "my-nsg" } + let storage = storageAccount { name "flowlogsstorage" } + + let watcher = networkWatcher { name "my-watcher" } + + let flowlog = + flowLog { + name "my-flow-log" + link_to_network_watcher watcher + link_to_nsg (nsg :> IBuilder).ResourceId + link_to_storage_account (storage :> IBuilder).ResourceId + retention_days 30 + } + + let deployment = arm { + add_resources [ nsg; storage; watcher; flowlog ] + } + + let jobj = deployment.Template |> Writer.toJson |> JToken.Parse + let flowLogResource = + jobj.SelectToken("resources[?(@.type=='Microsoft.Network/networkWatchers/flowLogs')]") + + Expect.isNotNull flowLogResource "Flow log resource should exist" + Expect.equal + (flowLogResource.SelectToken("properties.retentionPolicy.days").ToObject()) + 30 + "Retention days should be 30" + } + + test "Flow log with Traffic Analytics" { + let nsg = nsg { name "my-nsg" } + let storage = storageAccount { name "flowlogsstorage" } + let workspace = logAnalytics { name "my-workspace" } + let watcher = networkWatcher { name "my-watcher" } + + let flowlog = + flowLog { + name "my-flow-log" + link_to_network_watcher watcher + link_to_nsg (nsg :> IBuilder).ResourceId + link_to_storage_account (storage :> IBuilder).ResourceId + enable_traffic_analytics (workspace :> IBuilder).ResourceId + } + + let deployment = arm { + add_resources [ nsg; storage; workspace; watcher; flowlog ] + } + + let jobj = deployment.Template |> Writer.toJson |> JToken.Parse + let flowLogResource = + jobj.SelectToken("resources[?(@.type=='Microsoft.Network/networkWatchers/flowLogs')]") + + let trafficAnalytics = + flowLogResource.SelectToken( + "properties.flowAnalyticsConfiguration.networkWatcherFlowAnalyticsConfiguration" + ) + + Expect.isNotNull trafficAnalytics "Traffic Analytics should be configured" + + Expect.isTrue + (trafficAnalytics.SelectToken("enabled").ToObject()) + "Traffic Analytics should be enabled" + } + + test "Network Watcher has correct resource ID" { + let watcher = networkWatcher { name "test-watcher" } + let resourceId = (watcher :> IBuilder).ResourceId + + Expect.equal resourceId.Type.Type "Microsoft.Network/networkWatchers" "Type should be correct" + Expect.equal resourceId.Name.Value "test-watcher" "Name should be correct" + } + ] diff --git a/src/Tests/Policy.fs b/src/Tests/Policy.fs new file mode 100644 index 000000000..90569419c --- /dev/null +++ b/src/Tests/Policy.fs @@ -0,0 +1,273 @@ +module Policy + +open Expecto +open Farmer +open Farmer.Builders +open Farmer.Arm.Policy +open Newtonsoft.Json.Linq + +let tests = + testList "Azure Policy" [ + test "Creates a basic policy definition" { + let policyRule = + """{ + "if": { + "field": "location", + "notIn": ["eastus", "westus"] + }, + "then": { + "effect": "deny" + } + }""" + + let policy = + policyDefinition { + name "location-restriction-policy" + display_name "Restrict deployment locations" + description "This policy restricts resource deployments to specific Azure regions" + mode PolicyMode.All + policy_rule policyRule + } + + let deployment = arm { add_resources [ policy ] } + let jobj = deployment.Template |> Writer.toJson |> JToken.Parse + + let policyResource = + jobj.SelectToken("resources[?(@.type=='Microsoft.Authorization/policyDefinitions')]") + + Expect.isNotNull policyResource "Policy definition resource should exist" + Expect.equal (policyResource.SelectToken("name").ToString()) "location-restriction-policy" "Name should be correct" + + Expect.equal + (policyResource.SelectToken("properties.displayName").ToString()) + "Restrict deployment locations" + "Display name should be correct" + + Expect.equal + (policyResource.SelectToken("properties.mode").ToString()) + "All" + "Mode should be correct" + + Expect.isNotNull (policyResource.SelectToken("properties.policyRule")) "Policy rule should exist" + } + + test "Creates a policy assignment" { + let policyRule = + """{ + "if": { + "field": "type", + "equals": "Microsoft.Storage/storageAccounts" + }, + "then": { + "effect": "audit" + } + }""" + + let policyDef = + policyDefinition { + name "audit-storage-policy" + display_name "Audit Storage Accounts" + policy_rule policyRule + } + + let assignment = + policyAssignment { + name "audit-storage-assignment" + display_name "Audit Storage Assignment" + link_to_policy policyDef + enforcement_mode EnforcementMode.Default + } + + let deployment = arm { add_resources [ policyDef; assignment ] } + let jobj = deployment.Template |> Writer.toJson |> JToken.Parse + + let assignmentResource = + jobj.SelectToken("resources[?(@.type=='Microsoft.Authorization/policyAssignments')]") + + Expect.isNotNull assignmentResource "Policy assignment resource should exist" + + Expect.equal + (assignmentResource.SelectToken("name").ToString()) + "audit-storage-assignment" + "Assignment name should be correct" + + Expect.equal + (assignmentResource.SelectToken("properties.enforcementMode").ToString()) + "Default" + "Enforcement mode should be correct" + + Expect.isNotNull + (assignmentResource.SelectToken("properties.policyDefinitionId")) + "Policy definition ID should be set" + } + + test "Policy assignment with DoNotEnforce mode" { + let policyRule = + """{ + "if": { + "field": "tags.environment", + "exists": "false" + }, + "then": { + "effect": "deny" + } + }""" + + let policyDef = + policyDefinition { + name "require-env-tag-policy" + policy_rule policyRule + } + + let assignment = + policyAssignment { + name "require-env-tag-test" + link_to_policy policyDef + enforcement_mode EnforcementMode.DoNotEnforce + } + + let deployment = arm { add_resources [ policyDef; assignment ] } + let jobj = deployment.Template |> Writer.toJson |> JToken.Parse + + let assignmentResource = + jobj.SelectToken("resources[?(@.type=='Microsoft.Authorization/policyAssignments')]") + + Expect.equal + (assignmentResource.SelectToken("properties.enforcementMode").ToString()) + "DoNotEnforce" + "Enforcement mode should be DoNotEnforce" + } + + test "Policy definition has correct resource ID" { + let policyRule = + """{ + "if": { + "field": "type", + "equals": "Microsoft.Compute/virtualMachines" + }, + "then": { + "effect": "audit" + } + }""" + + let policy = + policyDefinition { + name "test-policy" + policy_rule policyRule + } + + let resourceId = (policy :> IBuilder).ResourceId + + Expect.equal resourceId.Type.Type "Microsoft.Authorization/policyDefinitions" "Type should be correct" + Expect.equal resourceId.Name.Value "test-policy" "Name should be correct" + } + + test "Policy assignment depends on policy definition" { + let policyRule = + """{ + "if": { + "field": "type", + "equals": "Microsoft.Network/virtualNetworks" + }, + "then": { + "effect": "audit" + } + }""" + + let policyDef = + policyDefinition { + name "audit-vnets-policy" + policy_rule policyRule + } + + let assignment = + policyAssignment { + name "audit-vnets-assignment" + link_to_policy policyDef + } + + let deployment = arm { add_resources [ policyDef; assignment ] } + let jobj = deployment.Template |> Writer.toJson |> JToken.Parse + + let assignmentResource = + jobj.SelectToken("resources[?(@.type=='Microsoft.Authorization/policyAssignments')]") + + let dependsOn = assignmentResource.SelectToken("dependsOn") + Expect.isNotNull dependsOn "DependsOn should exist" + Expect.isTrue (dependsOn.ToString().Contains("audit-vnets-policy")) "Should depend on policy definition" + } + + test "Policy definition with metadata" { + let policyRule = + """{ + "if": { + "field": "type", + "equals": "Microsoft.Compute/virtualMachines" + }, + "then": { + "effect": "audit" + } + }""" + + let policy = + policyDefinition { + name "vm-audit-policy" + policy_rule policyRule + add_metadata + (Map.ofList + [ "category", "Compute" + "version", "1.0.0" ]) + } + + let deployment = arm { add_resources [ policy ] } + let jobj = deployment.Template |> Writer.toJson |> JToken.Parse + + let policyResource = + jobj.SelectToken("resources[?(@.type=='Microsoft.Authorization/policyDefinitions')]") + + let metadata = policyResource.SelectToken("properties.metadata") + Expect.isNotNull metadata "Metadata should exist" + Expect.equal (metadata.SelectToken("category").ToString()) "Compute" "Category should be correct" + Expect.equal (metadata.SelectToken("version").ToString()) "1.0.0" "Version should be correct" + } + + test "Policy assignment with system-assigned identity" { + let policyRule = + """{ + "if": { + "field": "type", + "equals": "Microsoft.Storage/storageAccounts" + }, + "then": { + "effect": "deployIfNotExists", + "details": { + "type": "Microsoft.Insights/diagnosticSettings", + "name": "setByPolicy" + } + } + }""" + + let policyDef = + policyDefinition { + name "deploy-storage-diagnostics" + policy_rule policyRule + } + + let assignment = + policyAssignment { + name "deploy-storage-diagnostics-assignment" + link_to_policy policyDef + system_identity + } + + let deployment = arm { add_resources [ policyDef; assignment ] } + let jobj = deployment.Template |> Writer.toJson |> JToken.Parse + + let assignmentResource = + jobj.SelectToken("resources[?(@.type=='Microsoft.Authorization/policyAssignments')]") + + let identity = assignmentResource.SelectToken("identity") + Expect.isNotNull identity "Identity should exist" + Expect.equal (identity.SelectToken("type").ToString()) "SystemAssigned" "Identity type should be SystemAssigned" + } + ] diff --git a/src/Tests/RecoveryServices.fs b/src/Tests/RecoveryServices.fs new file mode 100644 index 000000000..31cc29505 --- /dev/null +++ b/src/Tests/RecoveryServices.fs @@ -0,0 +1,139 @@ +module RecoveryServices + +open Expecto +open Farmer +open Farmer.Builders +open Farmer.Arm.RecoveryServices +open Newtonsoft.Json.Linq + +let tests = + testList "Recovery Services" [ + test "Creates a basic Recovery Services Vault" { + let vault = recoveryServicesVault { name "my-backup-vault" } + let deployment = arm { add_resources [ vault ] } + + let jobj = deployment.Template |> Writer.toJson |> JToken.Parse + + let vaultResource = + jobj.SelectToken("resources[?(@.type=='Microsoft.RecoveryServices/vaults')]") + + Expect.isNotNull vaultResource "Vault resource should exist" + Expect.equal (vaultResource.SelectToken("name").ToString()) "my-backup-vault" "Name should be correct" + Expect.equal (vaultResource.SelectToken("sku.name").ToString()) "Standard" "SKU should be Standard by default" + } + + test "Recovery Services Vault can have tags" { + let vault = + recoveryServicesVault { + name "tagged-vault" + add_tags [ "environment", "production"; "backup", "critical" ] + } + + let deployment = arm { add_resources [ vault ] } + let jobj = deployment.Template |> Writer.toJson |> JToken.Parse + let vaultResource = jobj.SelectToken("resources[?(@.type=='Microsoft.RecoveryServices/vaults')]") + + let tags = vaultResource.SelectToken("tags") + Expect.isNotNull tags "Tags should exist" + Expect.equal (tags.SelectToken("environment").ToString()) "production" "Environment tag should be correct" + Expect.equal (tags.SelectToken("backup").ToString()) "critical" "Backup tag should be correct" + } + + test "Creates a VM backup policy" { + let vault = recoveryServicesVault { name "backup-vault" } + + let policy = + vmBackupPolicy { + name "daily-vm-backup" + link_to_vault vault + schedule_frequency BackupScheduleFrequency.Daily + retention_days 30 + } + + let deployment = arm { add_resources [ vault; policy ] } + let jobj = deployment.Template |> Writer.toJson |> JToken.Parse + + let policyResource = + jobj.SelectToken("resources[?(@.type=='Microsoft.RecoveryServices/vaults/backupPolicies')]") + + Expect.isNotNull policyResource "Backup policy resource should exist" + Expect.equal (policyResource.SelectToken("name").ToString()) "backup-vault/daily-vm-backup" "Name should be correct" + + Expect.equal + (policyResource.SelectToken("properties.backupManagementType").ToString()) + "AzureIaasVM" + "Backup type should be VM" + + Expect.equal + (policyResource.SelectToken("properties.schedulePolicy.scheduleRunFrequency").ToString()) + "Daily" + "Schedule frequency should be Daily" + + Expect.equal + (policyResource.SelectToken("properties.retentionPolicy.dailySchedule.retentionDuration.count") + .ToString()) + "30" + "Retention days should be 30" + } + + test "VM backup policy with weekly and monthly retention" { + let vault = recoveryServicesVault { name "backup-vault" } + + let policy = + vmBackupPolicy { + name "comprehensive-backup" + link_to_vault vault + retention_days 30 + weekly_retention_weeks 12 + monthly_retention_months 6 + } + + let deployment = arm { add_resources [ vault; policy ] } + let jobj = deployment.Template |> Writer.toJson |> JToken.Parse + + let policyResource = + jobj.SelectToken("resources[?(@.type=='Microsoft.RecoveryServices/vaults/backupPolicies')]") + + Expect.equal + (policyResource + .SelectToken("properties.retentionPolicy.weeklySchedule.retentionDuration.count") + .ToString()) + "12" + "Weekly retention should be 12 weeks" + + Expect.equal + (policyResource + .SelectToken("properties.retentionPolicy.monthlySchedule.retentionDuration.count") + .ToString()) + "6" + "Monthly retention should be 6 months" + } + + test "Backup policy depends on vault" { + let vault = recoveryServicesVault { name "vault" } + + let policy = + vmBackupPolicy { + name "policy" + link_to_vault vault + } + + let deployment = arm { add_resources [ vault; policy ] } + let jobj = deployment.Template |> Writer.toJson |> JToken.Parse + + let policyResource = + jobj.SelectToken("resources[?(@.type=='Microsoft.RecoveryServices/vaults/backupPolicies')]") + + let dependsOn = policyResource.SelectToken("dependsOn") + Expect.isNotNull dependsOn "DependsOn should exist" + Expect.isTrue (dependsOn.ToString().Contains("vault")) "Should depend on vault" + } + + test "Recovery Services Vault has correct resource ID" { + let vault = recoveryServicesVault { name "test-vault" } + let resourceId = (vault :> IBuilder).ResourceId + + Expect.equal resourceId.Type.Type "Microsoft.RecoveryServices/vaults" "Type should be correct" + Expect.equal resourceId.Name.Value "test-vault" "Name should be correct" + } + ] diff --git a/src/Tests/Sentinel.fs b/src/Tests/Sentinel.fs new file mode 100644 index 000000000..216c50629 --- /dev/null +++ b/src/Tests/Sentinel.fs @@ -0,0 +1,59 @@ +module Sentinel + +open Expecto +open Farmer +open Farmer.Builders +open Newtonsoft.Json.Linq + +let tests = + testList "Azure Sentinel" [ + test "Enables Sentinel on a Log Analytics Workspace" { + let workspace = logAnalytics { name "security-workspace" } + + let sentinel = sentinel { link_to_workspace workspace } + + let deployment = arm { add_resources [ workspace; sentinel ] } + let jobj = deployment.Template |> Writer.toJson |> JToken.Parse + + let sentinelResource = + jobj.SelectToken("resources[?(@.type=='Microsoft.SecurityInsights/onboardingStates')]") + + Expect.isNotNull sentinelResource "Sentinel resource should exist" + + Expect.equal + (sentinelResource.SelectToken("name").ToString()) + "security-workspace/default" + "Name should be workspace/default" + } + + test "Sentinel depends on workspace" { + let workspace = logAnalytics { name "test-workspace" } + + let sentinel = sentinel { link_to_workspace workspace } + + let deployment = arm { add_resources [ workspace; sentinel ] } + let jobj = deployment.Template |> Writer.toJson |> JToken.Parse + + let sentinelResource = + jobj.SelectToken("resources[?(@.type=='Microsoft.SecurityInsights/onboardingStates')]") + + let dependsOn = sentinelResource.SelectToken("dependsOn") + Expect.isNotNull dependsOn "DependsOn should exist" + Expect.isTrue (dependsOn.ToString().Contains("test-workspace")) "Should depend on workspace" + } + + test "Sentinel can reference existing workspace by name" { + let sentinel = sentinel { workspace_name "existing-workspace" } + + let deployment = arm { add_resources [ sentinel ] } + let jobj = deployment.Template |> Writer.toJson |> JToken.Parse + + let sentinelResource = + jobj.SelectToken("resources[?(@.type=='Microsoft.SecurityInsights/onboardingStates')]") + + Expect.equal + (sentinelResource.SelectToken("name").ToString()) + "existing-workspace/default" + "Name should include existing workspace" + } + ] diff --git a/src/Tests/Tests.fsproj b/src/Tests/Tests.fsproj index 755acd197..6f31e1a44 100644 --- a/src/Tests/Tests.fsproj +++ b/src/Tests/Tests.fsproj @@ -26,6 +26,7 @@ + @@ -34,6 +35,7 @@ + @@ -41,13 +43,17 @@ + + + +