diff --git a/.github/ISSUE_TEMPLATE/BUG.md b/.github/ISSUE_TEMPLATE/BUG.md new file mode 100644 index 0000000..272649d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/BUG.md @@ -0,0 +1,24 @@ +--- +name: Bug Report +about: Report a Bug + +--- + +**Describe the bug** +What issue did you experience? More detail the better! + +**Reproducing the Bug** +Steps to reproduce the behavior: +1. Using terraform version x.y.z +2. With module version a.b.c +3. Provider versions... 1,2,3 +4. Error looks like: + +**Expected Behavior** +Describe the expected behavior. + +**Screenshots** +If applicable, add screenshots, obfuscated tf state files, etc... to help explain your issue. + +**Any Additional Context** +Add any other context about the bug that will help us troubleshoot the issue. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/FEATURE.md b/.github/ISSUE_TEMPLATE/FEATURE.md new file mode 100644 index 0000000..449a622 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/FEATURE.md @@ -0,0 +1,18 @@ +--- +name: Feature Request +about: Suggest an feature for this project + +--- + +**Is your feature request related to an ongoing bug?** +Please provide a clear description of the challenge you're facing. + +**Propose a solution** +Outline a solution you may have to the challenge and any tests / evidence that may help us better +decide to take it on! + +**What alternatives have you tried or considered?** +Outline any alternatives to the solution you're proposing in a clear and concise way. + +**Any additional context** +Add any other context or screenshots about the feature request here. \ No newline at end of file diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..1a3d752 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,19 @@ +# Description + +Include an overview of the change and which issue it addresses. Please also include relevant +motivation and context. List any dependencies that are required for this change. + +Associated it with an existing issue, i.e. - "Fixes issue #12345" + +## Type of change + +Please delete options that are not relevant. + +- [ ] Bug Fix +- [ ] New Feature +- [ ] This change requires a documentation update + +# How Has This Been Tested? + +Describe the tests that you ran to verify your changes. Provide instructions so they can be +reproduced. Please also list any relevant details for your test configuration diff --git a/.github/workflows/fmt:check.yml b/.github/workflows/fmt:check.yml new file mode 100644 index 0000000..981961d --- /dev/null +++ b/.github/workflows/fmt:check.yml @@ -0,0 +1,25 @@ +--- +name: Terraform Validation + +on: + pull_request: + branches: + - main + +jobs: + terraform: + name: Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install Task + uses: arduino/setup-task@v2 + with: + version: 3.x + repo-token: ${{ secrets.GITHUB_TOKEN }} + - name: Install Terraform + uses: hashicorp/setup-terraform@v3 + with: + terraform_version: "1.8.2" + - name: Terraform fmt + run: task fmt:check \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b65e6c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.idea** +**/.terraform/** +*.tfstate.* +*.tfstate +**.terraform* +**tfplan** \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a42d1c7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2024 Corelight, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index 426db06..897cdcb 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,44 @@ # terraform-azure-sensor + Terraform for Corelight's Azure Cloud Sensor Deployment. + +overview + +## Usage +```hcl +resource "sensor" { + source = "github.com/corelight/terraform-azure-sensor" + + license_key = "" + location = "" + resource_group_name = "" + virtual_network_name = "" + virtual_network_resource_group = "" + virtual_network_address_space = "" + corelight_sensor_image_id = "" + sensor_api_password = "" + sensor_ssh_public_key = "" + + # (Optional) Cloud Enrichment Variables + enrichment_storage_account_name = "" + enrichment_storage_container_name = "" + tags = { + foo: bar, + terraform: true, + purpose: Corelight + } +} +``` + +### Deployment + +The variables for this module all have default values that can be overwritten +to meet your naming and compliance standards. + +Deployment examples can be found [here](examples). + +## License + +The project is licensed under the [MIT][] license. + +[MIT]: LICENSE diff --git a/Taskfile.yml b/Taskfile.yml new file mode 100644 index 0000000..1981726 --- /dev/null +++ b/Taskfile.yml @@ -0,0 +1,13 @@ +--- +version: "3" + +tasks: + fmt: + desc: Reformat your configuration in the standard style + cmds: + - terraform fmt -recursive . + + fmt:check: + desc: Check if the input is formatted + cmds: + - terraform fmt -recursive -check -diff . \ No newline at end of file diff --git a/data.tf b/data.tf new file mode 100644 index 0000000..d94f9be --- /dev/null +++ b/data.tf @@ -0,0 +1,37 @@ +data "cloudinit_config" "config" { + gzip = true + base64_encode = true + + part { + content_type = "text/cloud-config" + content = templatefile("${path.module}/templates/sensor_init.tpl", + { + api_password = var.sensor_api_password + sensor_license = var.license_key + mgmt_int = "eth0" + mon_int = "eth1" + } + ) + filename = "sensor-build.yaml" + } +} + +data "cloudinit_config" "config_with_enrichment" { + gzip = true + base64_encode = true + + part { + content_type = "text/cloud-config" + content = templatefile("${path.module}/templates/sensor_init_with_enrichment.tpl", + { + api_password = var.sensor_api_password + sensor_license = var.license_key + mgmt_int = "eth0" + mon_int = "eth1" + container_name = var.enrichment_storage_container_name + storage_account_name = var.enrichment_storage_account_name + } + ) + filename = "sensor-build.yaml" + } +} \ No newline at end of file diff --git a/docs/overview.svg b/docs/overview.svg new file mode 100644 index 0000000..f5a663d --- /dev/null +++ b/docs/overview.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/deployment/main.tf b/examples/deployment/main.tf new file mode 100644 index 0000000..dfac70d --- /dev/null +++ b/examples/deployment/main.tf @@ -0,0 +1,59 @@ +locals { + subscription_id = "" + resource_group_name = "corelight" + location = "eastus" + license = "" + tags = { + terraform : true, + purpose : "Corelight" + } +} + +#################################################################################################### +# Create a resource group for the corelight resources +#################################################################################################### +resource "azurerm_resource_group" "sensor_rg" { + location = local.location + name = local.resource_group_name + + tags = local.tags +} + +#################################################################################################### +# Get data on the existing vnet and create a subnet in that vnet for the sensor +#################################################################################################### +data "azurerm_virtual_network" "existing_vnet" { + name = "" + resource_group_name = "" +} + +#################################################################################################### +# Deploy the Sensor +#################################################################################################### +module "sensor" { + source = "../.." + + license_key = local.license + location = local.location + resource_group_name = azurerm_resource_group.sensor_rg.name + virtual_network_name = data.azurerm_virtual_network.existing_vnet.name + virtual_network_resource_group = "" + virtual_network_address_space = "" + corelight_sensor_image_id = "" + sensor_api_password = "" + sensor_ssh_public_key = "" + + # (Optional) Cloud Enrichment Variables + enrichment_storage_account_name = "" + enrichment_storage_container_name = "" + tags = local.tags +} + +#################################################################################################### +# (Optional) Assign the VMSS identity access to the enrichment bucket if enabled +#################################################################################################### +resource "azurerm_role_assignment" "enrichment_data_access" { + principal_id = module.sensor.sensor_identity_principal_id + scope = "" + role_definition_name = "Storage Blob Data Reader" +} diff --git a/examples/deployment/versions.tf b/examples/deployment/versions.tf new file mode 100644 index 0000000..7a8b113 --- /dev/null +++ b/examples/deployment/versions.tf @@ -0,0 +1,16 @@ +terraform { + required_version = ">=1.3.2" + + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = ">=3.97.1" + } + } +} + +provider "azurerm" { + features {} + subscription_id = local.subscription_id +} + diff --git a/load_balancer.tf b/load_balancer.tf new file mode 100644 index 0000000..5408677 --- /dev/null +++ b/load_balancer.tf @@ -0,0 +1,82 @@ +resource "azurerm_lb" "scale_set_lb" { + location = var.location + name = var.load_balancer_name + resource_group_name = var.resource_group_name + sku = "Standard" + + frontend_ip_configuration { + name = var.lb_frontend_ip_config_name + subnet_id = azurerm_subnet.subnet.id + } + + tags = var.tags +} + +resource "azurerm_lb_backend_address_pool" "management_pool" { + loadbalancer_id = azurerm_lb.scale_set_lb.id + name = var.lb_mgmt_backend_address_pool_name +} + +resource "azurerm_lb_backend_address_pool" "monitoring_pool" { + loadbalancer_id = azurerm_lb.scale_set_lb.id + name = var.lb_mon_backend_address_pool_name +} + +resource "azurerm_lb_probe" "sensor_health_check_probe" { + loadbalancer_id = azurerm_lb.scale_set_lb.id + name = var.lb_health_check_probe_name + port = 443 + request_path = "/api/system/healthcheck/" + protocol = "Https" + interval_in_seconds = 30 + probe_threshold = 3 +} + +resource "azurerm_lb_rule" "monitoring_vxlan_lb_rule" { + loadbalancer_id = azurerm_lb.scale_set_lb.id + name = var.lb_vxlan_rule_name + protocol = "Udp" + backend_port = 4789 + frontend_port = 4789 + frontend_ip_configuration_name = azurerm_lb.scale_set_lb.frontend_ip_configuration[0].name + backend_address_pool_ids = [ + azurerm_lb_backend_address_pool.monitoring_pool.id + ] +} + +resource "azurerm_lb_rule" "monitoring_geneve_lb_rule" { + name = var.lb_geneve_rule_name + loadbalancer_id = azurerm_lb.scale_set_lb.id + protocol = "Udp" + backend_port = 6081 + frontend_port = 6081 + frontend_ip_configuration_name = azurerm_lb.scale_set_lb.frontend_ip_configuration[0].name + backend_address_pool_ids = [ + azurerm_lb_backend_address_pool.monitoring_pool.id + ] +} + +resource "azurerm_lb_rule" "monitoring_health_check_rule" { + name = var.lb_health_check_rule_name + loadbalancer_id = azurerm_lb.scale_set_lb.id + protocol = "Tcp" + backend_port = 443 + frontend_port = 443 + frontend_ip_configuration_name = azurerm_lb.scale_set_lb.frontend_ip_configuration[0].name + backend_address_pool_ids = [ + azurerm_lb_backend_address_pool.management_pool.id + ] + probe_id = azurerm_lb_probe.sensor_health_check_probe.id +} + +resource "azurerm_lb_rule" "management_lb_rule" { + name = var.lb_ssh_rule_name + loadbalancer_id = azurerm_lb.scale_set_lb.id + frontend_ip_configuration_name = azurerm_lb.scale_set_lb.frontend_ip_configuration[0].name + protocol = "Tcp" + backend_port = 22 + frontend_port = 22 + backend_address_pool_ids = [ + azurerm_lb_backend_address_pool.management_pool.id + ] +} diff --git a/nat_gateway.tf b/nat_gateway.tf new file mode 100644 index 0000000..72d1bc2 --- /dev/null +++ b/nat_gateway.tf @@ -0,0 +1,27 @@ +resource "azurerm_public_ip" "nat_gw_ip" { + name = var.nat_gateway_ip_name + location = var.location + resource_group_name = var.resource_group_name + allocation_method = "Static" + sku = "Standard" + + tags = var.tags +} + +resource "azurerm_nat_gateway" "lb_nat_gw" { + name = var.nat_gateway_name + location = var.location + resource_group_name = var.resource_group_name + + tags = var.tags +} + +resource "azurerm_subnet_nat_gateway_association" "nat_gw_association" { + subnet_id = azurerm_subnet.subnet.id + nat_gateway_id = azurerm_nat_gateway.lb_nat_gw.id +} + +resource "azurerm_nat_gateway_public_ip_association" "public_ip_association" { + nat_gateway_id = azurerm_nat_gateway.lb_nat_gw.id + public_ip_address_id = azurerm_public_ip.nat_gw_ip.id +} \ No newline at end of file diff --git a/outputs.tf b/outputs.tf new file mode 100644 index 0000000..3ab2492 --- /dev/null +++ b/outputs.tf @@ -0,0 +1,23 @@ +output "internal_load_balancer_name" { + value = azurerm_lb.scale_set_lb.name +} + +output "nat_gateway_public_ip_name" { + value = azurerm_public_ip.nat_gw_ip.name +} + +output "nat_gateway_name" { + value = azurerm_nat_gateway.lb_nat_gw.name +} + +output "sensor_identity_principal_id" { + value = azurerm_linux_virtual_machine_scale_set.sensor_scale_set.identity[0].principal_id +} + +output "sensor_scale_set_name" { + value = azurerm_linux_virtual_machine_scale_set.sensor_scale_set.name +} + +output "sensor_scale_set_subnet_name" { + value = azurerm_subnet.subnet.name +} \ No newline at end of file diff --git a/scale_set.tf b/scale_set.tf new file mode 100644 index 0000000..6948fb6 --- /dev/null +++ b/scale_set.tf @@ -0,0 +1,118 @@ +resource "azurerm_linux_virtual_machine_scale_set" "sensor_scale_set" { + admin_username = var.sensor_admin_username + admin_ssh_key { + public_key = var.sensor_ssh_public_key + username = var.sensor_admin_username + } + location = var.location + name = var.scale_set_name + resource_group_name = var.resource_group_name + sku = var.virtual_machine_size + instances = 1 + custom_data = var.enrichment_storage_account_name == "" ? data.cloudinit_config.config.rendered : data.cloudinit_config.config_with_enrichment.rendered + + source_image_id = var.corelight_sensor_image_id + + identity { + type = "SystemAssigned" + } + + os_disk { + caching = "ReadWrite" + storage_account_type = "StandardSSD_LRS" + disk_size_gb = var.virtual_machine_os_disk_size + } + + health_probe_id = azurerm_lb_probe.sensor_health_check_probe.id + network_interface { + name = "management-nic" + primary = true + + ip_configuration { + primary = false + name = "management-nic-ip-cfg" + subnet_id = azurerm_subnet.subnet.id + load_balancer_backend_address_pool_ids = [ + azurerm_lb_backend_address_pool.management_pool.id + ] + } + } + + network_interface { + name = "monitoring-nic" + ip_configuration { + primary = false + name = "monitoring-nic-ip-cfg" + subnet_id = azurerm_subnet.subnet.id + load_balancer_backend_address_pool_ids = [ + azurerm_lb_backend_address_pool.monitoring_pool.id + ] + } + } + + tags = var.tags +} + +resource "azurerm_monitor_autoscale_setting" "auto_scale_config" { + location = var.location + name = var.autoscale_setting_name + resource_group_name = var.resource_group_name + target_resource_id = azurerm_linux_virtual_machine_scale_set.sensor_scale_set.id + + profile { + name = "autoscale" + capacity { + default = 1 + minimum = 1 + maximum = 5 + } + + rule { + metric_trigger { + metric_name = "Percentage CPU" + metric_namespace = "microsoft.compute/virtualmachinescalesets" + metric_resource_id = azurerm_linux_virtual_machine_scale_set.sensor_scale_set.id + operator = "GreaterThan" + statistic = "Average" + threshold = 70 + time_aggregation = "Average" + time_grain = "PT1M" + time_window = "PT5M" + } + + scale_action { + cooldown = "PT1M" + direction = "Increase" + type = "ChangeCount" + value = 1 + } + } + + rule { + metric_trigger { + metric_name = "Percentage CPU" + metric_namespace = "microsoft.compute/virtualmachinescalesets" + metric_resource_id = azurerm_linux_virtual_machine_scale_set.sensor_scale_set.id + time_grain = "PT1M" + statistic = "Average" + time_window = "PT5M" + time_aggregation = "Average" + operator = "LessThan" + threshold = 25 + } + + scale_action { + direction = "Decrease" + type = "ChangeCount" + value = "1" + cooldown = "PT1M" + } + } + } + + tags = var.tags + + depends_on = [ + azurerm_lb_probe.sensor_health_check_probe + ] +} \ No newline at end of file diff --git a/subnet.tf b/subnet.tf new file mode 100644 index 0000000..f86aacf --- /dev/null +++ b/subnet.tf @@ -0,0 +1,8 @@ +resource "azurerm_subnet" "subnet" { + name = var.sensor_subnet_name + virtual_network_name = var.virtual_network_name + resource_group_name = var.virtual_network_resource_group + address_prefixes = [ + cidrsubnet(var.virtual_network_address_space, 8, 1) + ] +} \ No newline at end of file diff --git a/templates/sensor_init.tpl b/templates/sensor_init.tpl new file mode 100644 index 0000000..c62ed3a --- /dev/null +++ b/templates/sensor_init.tpl @@ -0,0 +1,30 @@ +#cloud-config + +write_files: + - content: | + sensor: + api: + password: ${api_password} + license_key: ${sensor_license} + management_interface: + name: ${mgmt_int} + wait: true + monitoring_interface: + name: ${mon_int} + wait: true + kubernetes: + allow_ports: + - protocol: tcp + port: 80 + net: 0.0.0.0/0 + - protocol: tcp + port: 443 + net: 0.0.0.0/0 + owner: root:root + path: /etc/corelight/corelightctl.yaml + permissions: '0644' + +runcmd: + - corelightctl sensor bootstrap -v + - corelightctl sensor deploy -v + diff --git a/templates/sensor_init_with_enrichment.tpl b/templates/sensor_init_with_enrichment.tpl new file mode 100644 index 0000000..8dc9f59 --- /dev/null +++ b/templates/sensor_init_with_enrichment.tpl @@ -0,0 +1,33 @@ +#cloud-config + +write_files: + - content: | + sensor: + api: + password: ${api_password} + license_key: ${sensor_license} + management_interface: + name: ${mgmt_int} + wait: true + monitoring_interface: + name: ${mon_int} + wait: true + kubernetes: + allow_ports: + - protocol: tcp + port: 80 + net: 0.0.0.0/0 + - protocol: tcp + port: 443 + net: 0.0.0.0/0 + owner: root:root + path: /etc/corelight/corelightctl.yaml + permissions: '0644' + +runcmd: + - [ corelightctl, sensor, bootstrap, -v ] + - [ corelightctl, sensor, deploy, -v ] + - | + echo '{"cloud_enrichment.enable": "true", "cloud_enrichment.cloud_provider": "azure","cloud_enrichment.bucket_name": "${container_name}", "cloud_enrichment.azure_storage_account": "${storage_account_name}"}' | corelightctl sensor cfg put + + diff --git a/variables.tf b/variables.tf new file mode 100644 index 0000000..024aa2b --- /dev/null +++ b/variables.tf @@ -0,0 +1,167 @@ +variable "location" { + description = "The Azure location where resources will be deployed" + type = string +} + +variable "resource_group_name" { + description = "The name of the resource group where corelight resources will be deployed" + type = string +} + +variable "license_key" { + description = "Your Corelight sensor license key" + type = string +} + +variable "virtual_network_name" { + description = "The name of the virtual network the sensor will be deployed in" + type = string +} + +variable "virtual_network_address_space" { + description = "The address space of the virtual network the sensor be deployed in" + type = string +} + +variable "virtual_network_resource_group" { + description = "The resource group where the virtual network is deployed" + type = string +} + +variable "corelight_sensor_image_id" { + description = "The resource id of Corelight sensor image" + type = string +} + +variable "sensor_api_password" { + description = "The password that should be used for the Corelight sensor API" + sensitive = true + type = string +} + +variable "sensor_ssh_public_key" { + description = "The SSH public key which will be added to all sensors in the scale set" + type = string +} + +## Variables with defaults +variable "sensor_subnet_name" { + description = "The name of the subnet the VMSS will scale sensors in" + type = string + default = "cl-sensor-subnet" +} +variable "sensor_admin_username" { + description = "The name of the admin user on the corelight sensor VM in the VMSS" + type = string + default = "corelight" +} + +variable "nat_gateway_ip_name" { + description = "The resource name of the VMSS NAT Gateway public IP resource" + type = string + default = "cl-nat-gw-ip" +} + +variable "nat_gateway_name" { + description = "The resource name of the VMSS NAT Gateway resource" + type = string + default = "cl-sensor-nat-gw" +} + +variable "autoscale_setting_name" { + description = "The VMSS autoscale monitor name" + type = string + default = "corelight-scale-set-autoscale-cfg" +} + +variable "load_balancer_name" { + description = "The nane of the internal load balancer that sends traffic to the VMSS" + type = string + default = "corelight-sensor-lb" +} + +variable "scale_set_name" { + description = "Name of the Corelight VMSS of sensors" + type = string + default = "vmss-sensor" +} + +variable "virtual_machine_size" { + description = "The VMSS VM size" + type = string + default = "Standard_D4s_v3" +} + +variable "virtual_machine_os_disk_size" { + description = "The amount of OS disk to attach to the VMSS instances" + type = number + default = 500 +} + +variable "enrichment_storage_account_name" { + description = "(optional) the azure storage account where enrichment data is stored" + type = string + default = "" +} + +variable "enrichment_storage_container_name" { + description = "(optional) the container where enrichment data is stored" + type = string + default = "" +} + +variable "lb_frontend_ip_config_name" { + description = "Name of the internal load balancer frontend ip configuration" + type = string + default = "corelight-sensor-lb-ip" +} + +variable "lb_mgmt_backend_address_pool_name" { + description = "Name of the load balancer management backend address pool" + type = string + default = "management-pool" +} + +variable "lb_mon_backend_address_pool_name" { + description = "Name of the load balancer monitoring backend address pool" + type = string + default = "monitoring-pool" +} + +variable "lb_health_check_probe_name" { + description = "Name of the load balancer health check probe that check the sensor healthcheck API" + type = string + default = "health-check" +} + +variable "lb_vxlan_rule_name" { + description = "Name of the load balancer rule for VXLAN traffic" + type = string + default = "vxlan-lb-rule" +} + +variable "lb_geneve_rule_name" { + description = "Name of the load balancer rule for Geneve traffic" + type = string + default = "geneve-lb-rule" +} + +variable "lb_health_check_rule_name" { + description = "Name of the load balancer rule for health check traffic" + type = string + default = "healthcheck-lb-rule" +} + +variable "lb_ssh_rule_name" { + description = "Name of the load balancer rule for SSH traffic" + type = string + default = "management-ssh-lb-rule" +} + + + +variable "tags" { + description = "Any tags that should be applied to resources deployed by the module" + type = object({}) + default = {} +} \ No newline at end of file diff --git a/versions.tf b/versions.tf new file mode 100644 index 0000000..1ff2328 --- /dev/null +++ b/versions.tf @@ -0,0 +1,12 @@ +terraform { + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = ">=3.97.1" + } + cloudinit = { + source = "hashicorp/cloudinit" + version = ">=2.3.4" + } + } +}