diff --git a/.github/workflows/fmt-check.yml b/.github/workflows/fmt-check.yml index ff32f34..96b9763 100644 --- a/.github/workflows/fmt-check.yml +++ b/.github/workflows/fmt-check.yml @@ -8,7 +8,7 @@ on: jobs: terraform: - name: Lint + name: Lint and Test runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -23,4 +23,8 @@ jobs: terraform_version: "1.8.2" - name: Terraform fmt run: task fmt:check + - name: Terraform Init + run: terraform init + - name: Terraform Test + run: task test diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..6cd9eb2 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,30 @@ +--- +name: Terraform Tests + +on: + pull_request: + branches: + - main + push: + branches: + - main + +jobs: + test: + name: Unit Tests + 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 Init + run: terraform init + - name: Terraform Test + run: task test:verbose diff --git a/README.md b/README.md index d21b4f3..5863ca5 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,468 @@ # terraform-config-sensor -Terraform for Corelight's Sensor Configuration. +[![Terraform Tests](https://github.com/corelight/terraform-config-sensor/actions/workflows/test.yml/badge.svg)](https://github.com/corelight/terraform-config-sensor/actions/workflows/test.yml) +[![Terraform Validation](https://github.com/corelight/terraform-config-sensor/actions/workflows/fmt-check.yml/badge.svg)](https://github.com/corelight/terraform-config-sensor/actions/workflows/fmt-check.yml) +[![Trivy Security Scan](https://github.com/corelight/terraform-config-sensor/actions/workflows/scan-trivy.yml/badge.svg)](https://github.com/corelight/terraform-config-sensor/actions/workflows/scan-trivy.yml) + +Terraform module for generating Corelight Sensor configuration using cloud-init. This module creates a cloud-config user data file that configures Corelight Sensors for deployment in cloud environments. + +## Features + +- **Template-based Configuration**: Generates `corelightctl.yaml` configuration via cloud-init +- **Fleet Manager Integration**: Optional pairing with Corelight Fleet Manager +- **Health Check Support**: Configurable HTTP health checks for cloud load balancers +- **Prometheus Metrics**: Optional Prometheus endpoint enablement +- **FedRAMP Mode**: Optional FedRAMP compliance mode +- **Flexible Output**: Optional gzip and base64 encoding for cloud providers +- **Comprehensive Testing**: 24 unit tests with 100% pass rate +- **Automated Validation**: CI/CD pipelines for formatting, testing, and security scanning + +## Requirements + +| Name | Version | +|------|---------| +| [Terraform](https://www.terraform.io/downloads.html) | >= 1.3.2 | +| [cloudinit provider](https://registry.terraform.io/providers/hashicorp/cloudinit/latest) | >= 2.3.0, < 3.0.0 | + +**Note**: Tests require Terraform >= 1.6.0 for native testing framework support. ## Usage +### Basic Example + ```terraform module "sensor_config" { source = "github.com/corelight/terraform-config-sensor" - fleet_community_string = "" - sensor_license = "" - sensor_management_interface_name = "" - sensor_monitoring_interface_name = "" - sensor_health_check_probe_source_ranges_cidr = "" - subnetwork_monitoring_cidr = "" - subnetwork_monitoring_gateway = "" - - # Optional - Fleet Manager - fleet_token = "b1cd099ff22ed8a41abc63929d1db126" - fleet_url = "https://fleet.example.com:1443/fleet/v1/internal/softsensor/websocket" - fleet_server_sslname = "foo.example.com" + # Required: Core sensor configuration + fleet_community_string = "your-fleet-community-string" + sensor_license = "your-sensor-license-key" + sensor_management_interface_name = "eth0" + sensor_monitoring_interface_name = "eth1" + + # Optional: Health check configuration (both must be set together) + sensor_health_check_probe_source_ranges_cidr = ["35.191.0.0/16", "130.211.0.0/22"] + subnetwork_monitoring_cidr = "10.0.1.0/24" + subnetwork_monitoring_gateway = "10.0.1.1" } ``` -## Deployment +**Note**: While health check configuration is shown above, only the four core sensor variables are strictly required. Health check configuration requires both `subnetwork_monitoring_cidr` and `subnetwork_monitoring_gateway` to be set together. + +### With Fleet Manager Pairing + +```terraform +module "sensor_config" { + source = "github.com/corelight/terraform-config-sensor" + + # Required configuration + fleet_community_string = "your-fleet-community-string" + sensor_license = "your-sensor-license-key" + sensor_management_interface_name = "eth0" + sensor_monitoring_interface_name = "eth1" + + # Health checks + sensor_health_check_probe_source_ranges_cidr = ["35.191.0.0/16"] + subnetwork_monitoring_cidr = "10.0.1.0/24" + subnetwork_monitoring_gateway = "10.0.1.1" + + # Fleet Manager pairing (all three required together) + fleet_token = "b1cd099ff22ed8a41abc63929d1db126" + fleet_url = "https://fleet.example.com:1443/fleet/v1/internal/softsensor/websocket" + fleet_server_sslname = "fleet.example.com" + + # Optional: Fleet proxy configuration + fleet_http_proxy = "http://proxy.example.com:8080" + fleet_https_proxy = "https://proxy.example.com:8443" + fleet_no_proxy = "localhost,127.0.0.1,.example.com" +} +``` + +### With Advanced Features + +```terraform +module "sensor_config" { + source = "github.com/corelight/terraform-config-sensor" + + # Required configuration + fleet_community_string = "your-fleet-community-string" + sensor_license = "your-sensor-license-key" + sensor_management_interface_name = "eth0" + sensor_monitoring_interface_name = "eth1" + + # Health checks + sensor_health_check_probe_source_ranges_cidr = ["35.191.0.0/16"] + subnetwork_monitoring_cidr = "10.0.1.0/24" + subnetwork_monitoring_gateway = "10.0.1.1" + + # Feature flags + prometheus_enabled = true + fedramp_mode_enabled = true + + # Output encoding (useful for some cloud providers) + gzip_config = true + base64_encode_config = true +} +``` + +## Inputs + +| Name | Description | Type | Default | Required | +|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------|----------------|-----------|----------| +| [fleet_community_string](#input_fleet_community_string) | The Fleet Manager community string (API string) | `string` | n/a | yes | +| [sensor_license](#input_sensor_license) | The Corelight sensor license key string | `string` | n/a | yes | +| [sensor_management_interface_name](#input_sensor_management_interface_name) | The sensor's management interface name (e.g., eth0, ens5) | `string` | n/a | yes | +| [sensor_monitoring_interface_name](#input_sensor_monitoring_interface_name) | The sensor's monitoring interface name (e.g., eth1, ens6) | `string` | n/a | yes | +| [base64_encode_config](#input_base64_encode_config) | Whether to base64 encode the configuration output | `bool` | `false` | no | +| [fedramp_mode_enabled](#input_fedramp_mode_enabled) | Enable FedRAMP compliance mode on the sensor | `bool` | `false` | no | +| [fleet_http_proxy](#input_fleet_http_proxy) | HTTP proxy URL for Fleet Manager traffic (e.g., http://proxy.example.com:8080) | `string` | `""` | no | +| [fleet_https_proxy](#input_fleet_https_proxy) | HTTPS proxy URL for Fleet Manager traffic (e.g., https://proxy.example.com:8443) | `string` | `""` | no | +| [fleet_no_proxy](#input_fleet_no_proxy) | Comma-separated list of hosts/domains to bypass proxy for Fleet traffic | `string` | `""` | no | +| [fleet_server_sslname](#input_fleet_server_sslname) | SSL hostname for the Fleet Manager server. Must be set together with fleet_token and fleet_url | `string` | `""` | no | +| [fleet_token](#input_fleet_token) | Fleet Manager pairing token from Fleet UI. Must be set together with fleet_url and fleet_server_sslname | `string` | `""` | no | +| [fleet_url](#input_fleet_url) | Fleet Manager WebSocket URL. Must be set together with fleet_token and fleet_server_sslname | `string` | `""` | no | +| [gzip_config](#input_gzip_config) | Whether to gzip the configuration output (useful for large configs) | `bool` | `false` | no | +| [prometheus_enabled](#input_prometheus_enabled) | Enable Prometheus metrics endpoint on the sensor | `bool` | `false` | no | +| [sensor_health_check_http_port](#input_sensor_health_check_http_port) | HTTP port number for health check endpoint | `string` | `"41080"` | no | +| [sensor_health_check_probe_source_ranges_cidr](#input_sensor_health_check_probe_source_ranges_cidr) | List of CIDR ranges allowed to access the health check endpoint | `list(string)` | `[""]` | no | +| [subnetwork_monitoring_cidr](#input_subnetwork_monitoring_cidr) | Monitoring subnet in CIDR notation (e.g., 10.0.1.0/24). Must be valid CIDR or empty. Required with subnetwork_monitoring_gateway for health checks | `string` | `""` | no | +| [subnetwork_monitoring_gateway](#input_subnetwork_monitoring_gateway) | Monitoring subnet gateway IP address (e.g., 10.0.1.1). Required with subnetwork_monitoring_cidr for health checks | `string` | `""` | no | + +### Input Notes + +- **Fleet Manager Pairing**: `fleet_token`, `fleet_url`, and `fleet_server_sslname` must all be set together or all be empty. The module enforces this validation rule - providing only one or two will result in an error. +- **Health Checks**: To enable health check configuration in the generated config, both `subnetwork_monitoring_cidr` and `subnetwork_monitoring_gateway` must be provided. If either is empty, the health check section will be omitted from the cloud-init output. +- **CIDR Validation**: The `subnetwork_monitoring_cidr` variable validates that the value is either empty or a valid CIDR block (e.g., `10.0.1.0/24`). +- **Output Encoding**: Use `gzip_config` and `base64_encode_config` based on your cloud provider's requirements (e.g., AWS EC2 user data supports gzip, and some providers require base64 encoding). + +## Outputs + +| Name | Description | +|------|-------------| +| [cloudinit_config](#output_cloudinit_config) | The complete cloudinit_config data source object. Use `.rendered` for the final user data string. | + +### Output Usage + +The module outputs the entire `cloudinit_config` data source, which provides multiple attributes: + +```terraform +# Get the rendered user data +user_data = module.sensor_config.cloudinit_config.rendered + +# Access the gzipped version (if gzip_config = true) +user_data_gzipped = module.sensor_config.cloudinit_config.gzip + +# Access the base64 encoded version (if base64_encode_config = true) +user_data_base64 = module.sensor_config.cloudinit_config.base64_encode +``` + +## Configuration Flow + +This module generates cloud-init user data through the following process: + +1. **Variable Input**: User provides configuration variables to the module +2. **Template Processing**: The `data.tf` file processes the cloud-init template (`cloud-config/init.tpl`) with the provided variables +3. **Conditional Rendering**: The template conditionally includes configuration sections based on variable presence: + - Health checks (requires both `subnetwork_monitoring_cidr` and `subnetwork_monitoring_gateway`) + - Fleet Manager pairing (requires `fleet_token`, `fleet_url`, and `fleet_server_sslname`) + - Prometheus metrics (requires `prometheus_enabled = true`) + - FedRAMP mode (requires `fedramp_mode_enabled = true`) +4. **Output Generation**: The module outputs the cloudinit_config data source with optional gzip/base64 encoding +5. **Cloud-Init Execution**: When the cloud instance boots with the generated user data, cloud-init: + - Writes `/etc/corelight/corelightctl.yaml` with the sensor configuration + - Executes `corelightctl sensor deploy -v` to initialize the sensor +6. **Sensor Initialization**: The Corelight sensor starts with the configured settings + +## Development + +This project uses [Task](https://taskfile.dev/) for common operations: + +```bash +# Format Terraform files +task fmt + +# Check if Terraform files are properly formatted +task fmt:check + +# Run Terraform unit tests +task test + +# Run Terraform unit tests with verbose output +task test:verbose +``` + +Manual Terraform commands: + +```bash +# Format recursively +terraform fmt -recursive . + +# Check formatting with diff +terraform fmt -recursive -check -diff . +``` + +## Testing + +This module includes comprehensive unit tests using Terraform's native testing framework. + +**Requirements**: Terraform >= 1.6.0 for testing support + +### Running Tests + +Using Task (recommended): + +```bash +# Run all tests +task test + +# Run tests with verbose output +task test:verbose +``` + +Using Terraform directly: + +```bash +# Run all tests +terraform test + +# Run tests with verbose output +terraform test -verbose + +# Run a specific test file +terraform test -filter=tests/basic_configuration.tftest.hcl +``` + +### Test Results + +All 24 tests pass successfully: +- 3 basic configuration tests +- 5 feature flag tests +- 3 Fleet Manager tests +- 2 integration tests +- 5 output encoding tests +- 6 validation tests + +### Test Coverage + +The test suite validates all module functionality: + +| Test File | Purpose | Test Count | +|----------------------------------|----------------------------------------------------------------------|------------| +| `basic_configuration.tftest.hcl` | Basic sensor config, minimal config, health check rendering | 3 | +| `fleet_manager.tftest.hcl` | Fleet Manager pairing, proxy configuration, non-Fleet scenarios | 3 | +| `feature_flags.tftest.hcl` | Prometheus and FedRAMP mode enablement | 5 | +| `output_encoding.tftest.hcl` | Gzip and base64 encoding options | 5 | +| `validation.tftest.hcl` | Input validation rules (Fleet Manager dependencies, CIDR validation) | 6 | +| `integration.tftest.hcl` | Full feature integration and minimal configuration | 2 | + +### Key Test Scenarios + +- **Required Variables**: Validates all required variables are properly rendered +- **Conditional Features**: Verifies optional sections only appear when configured +- **Fleet Manager Validation**: Ensures `fleet_token`, `fleet_url`, and `fleet_server_sslname` must all be set together +- **Health Check Logic**: Confirms health checks require both CIDR and gateway +- **Output Encoding**: Tests gzip and base64 encoding independently and together +- **CIDR Validation**: Validates `subnetwork_monitoring_cidr` accepts valid CIDR or empty string +- **Integration**: Tests realistic configurations with multiple features enabled + +### Writing New Tests + +Tests follow the `.tftest.hcl` format in the `tests/` directory: + +```hcl +run "descriptive_test_name" { + command = plan + + variables { + fleet_community_string = "test-value" + sensor_license = "test-license" + # ... other required variables + } + + assert { + condition = can(regex("expected-pattern", output.cloudinit_config.rendered)) + error_message = "Descriptive error message explaining what should be present" + } +} +``` + +For validation tests, use `expect_failures`: + +```hcl +run "validation_test_name" { + command = plan + + variables { + # ... configuration that should fail + } + + expect_failures = [ + var.variable_name, # The variable with validation rule + ] +} +``` + +## Continuous Integration + +This module uses GitHub Actions for comprehensive automated validation. All workflows use Terraform 1.8.2. + +### CI/CD Workflows + +| Workflow | Trigger | Purpose | Status | +|--------------------------|---------------------------------|---------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **Terraform Validation** | Pull requests to `main` | Format checking and unit tests | [![Terraform Validation](https://github.com/corelight/terraform-config-sensor/actions/workflows/fmt-check.yml/badge.svg)](https://github.com/corelight/terraform-config-sensor/actions/workflows/fmt-check.yml) | +| **Terraform Tests** | PRs and pushes to `main` | Verbose test execution | [![Terraform Tests](https://github.com/corelight/terraform-config-sensor/actions/workflows/test.yml/badge.svg)](https://github.com/corelight/terraform-config-sensor/actions/workflows/test.yml) | +| **Trivy Security Scan** | PRs, daily at 03:00 UTC, manual | Security vulnerability scanning | [![Trivy Security Scan](https://github.com/corelight/terraform-config-sensor/actions/workflows/scan-trivy.yml/badge.svg)](https://github.com/corelight/terraform-config-sensor/actions/workflows/scan-trivy.yml) | +| **Version Bump** | Merged PRs to `main` | Automatic version tagging | [![Bump version](https://github.com/corelight/terraform-config-sensor/actions/workflows/tag-bump.yml/badge.svg)](https://github.com/corelight/terraform-config-sensor/actions/workflows/tag-bump.yml) | + +### Workflow Details + +**Terraform Validation** (`.github/workflows/fmt-check.yml`) +- Validates Terraform formatting with `task fmt:check` +- Runs `terraform init` to ensure provider dependencies resolve +- Executes unit tests with `task test` +- Must pass before PRs can be merged + +**Terraform Tests** (`.github/workflows/test.yml`) +- Runs complete test suite with verbose output (`task test:verbose`) +- Provides detailed test results for each test file +- Executes on both PRs and commits to main branch + +**Trivy Security Scan** (`.github/workflows/scan-trivy.yml`) +- Scans Terraform code for security vulnerabilities and misconfigurations +- Runs automatically on PRs to catch issues early +- Scheduled nightly scans for ongoing security monitoring +- Can be triggered manually via workflow_dispatch + +**Version Bump** (`.github/workflows/tag-bump.yml`) +- Automatically increments semantic version tags when PRs are merged +- Uses commit messages to determine version bump type +- Creates git tags with `v` prefix (e.g., `v1.2.3`) + +### Quality Gates + +All pull requests must pass the following before merging: +- Terraform formatting validation +- All 24 unit tests +- Security scanning (Trivy) + +## Module Design + +### Architecture Decisions + +This module follows Terraform best practices and Corelight-specific patterns: + +- **Data Source Only**: Generates configuration data without creating infrastructure resources, making it composable with any cloud provider module +- **Template-Based**: Uses a single cloud-init template (`cloud-config/init.tpl`) for flexibility, maintainability, and easier testing +- **Fleet Integration**: Fleet Manager pairing is always configured via the cloud-init template, ensuring consistent, automated sensor enrollment (no manual `corelightctl` commands required) +- **Conditional Rendering**: Template sections are rendered only when required variables are provided, keeping generated configs clean and minimal +- **Validation at Input**: Input validation rules catch configuration errors early, before deployment +- **No Sensitive Marking**: Variables are intentionally not marked as sensitive since this module only generates configuration strings (secrets should be protected at the consumption layer) + +### Module Structure + +``` +. +├── cloud-config/ +│ └── init.tpl # Cloud-init template for corelightctl.yaml +├── tests/ # Terraform native test suite (24 tests) +│ ├── basic_configuration.tftest.hcl +│ ├── feature_flags.tftest.hcl +│ ├── fleet_manager.tftest.hcl +│ ├── integration.tftest.hcl +│ ├── output_encoding.tftest.hcl +│ └── validation.tftest.hcl +├── .github/workflows/ # CI/CD pipelines +│ ├── fmt-check.yml # Format validation and tests +│ ├── test.yml # Verbose test execution +│ ├── scan-trivy.yml # Security scanning +│ └── tag-bump.yml # Automated versioning +├── data.tf # cloudinit_config data source +├── variables.tf # Input variables with validation rules +├── outputs.tf # Module outputs +├── versions.tf # Provider version constraints +├── Taskfile.yml # Task runner commands +├── CLAUDE.md # AI assistant guidance +└── README.md # This file +``` + +### Key Files + +- **`variables.tf`**: Defines 17 input variables with type constraints, defaults, descriptions, and validation rules +- **`data.tf`**: Configures the `cloudinit_config` data source, processing the template with user variables +- **`outputs.tf`**: Exposes the complete cloudinit_config object for use by consuming modules +- **`versions.tf`**: Specifies Terraform >= 1.3.2 and cloudinit provider >= 2.3.0, < 3.0.0 +- **`cloud-config/init.tpl`**: Jinja-style template that generates YAML configuration for `corelightctl` +- **`tests/`**: Comprehensive test suite with 24 tests covering all functionality and edge cases + +## Best Practices + +### Security Considerations + +1. **Protect Sensitive Variables**: While this module doesn't mark variables as sensitive (since it only generates config strings), you should protect secrets when using the module: + ```terraform + locals { + # Mark sensitive values in your consuming module + fleet_community_string = sensitive(var.fleet_community_string) + sensor_license = sensitive(var.sensor_license) + } + ``` + +2. **Use Terraform Cloud/Enterprise**: For production deployments, store sensitive values in Terraform Cloud/Enterprise variable sets with "Sensitive" enabled. + +3. **Secrets Management Integration**: Consider integrating with secrets managers: + ```terraform + data "aws_secretsmanager_secret_version" "sensor_config" { + secret_id = "corelight/sensor/config" + } + + locals { + secrets = jsondecode(data.aws_secretsmanager_secret_version.sensor_config.secret_string) + } + + module "sensor_config" { + source = "github.com/corelight/terraform-config-sensor" + + fleet_community_string = local.secrets.community_string + sensor_license = local.secrets.license + # ... + } + ``` + +### Module Versioning + +Pin to a specific version tag for production use: + +```terraform +module "sensor_config" { + source = "github.com/corelight/terraform-config-sensor?ref=v1.2.3" + # ... +} +``` + +Check available versions: https://github.com/corelight/terraform-config-sensor/tags + +## Contributing + +Contributions are welcome! Please: + +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Run `task fmt` to format code +5. Run `task test` to ensure all tests pass +6. Submit a pull request + +All PRs must pass CI checks (formatting, tests, security scan) before merging. -The variables for this module all have default values that can be overwritten -to meet your naming and compliance standards. +## Support -Deployment examples can be found [here](examples). +For issues and questions: +- **GitHub Issues**: https://github.com/corelight/terraform-config-sensor/issues +- **Corelight Support**: https://www.corelight.com/support ## License -The project is licensed under the MIT license. +Copyright (c) 2025 Corelight, Inc. -[MIT]: LICENSE +This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. diff --git a/Taskfile.yml b/Taskfile.yml index 1684a3a..afa3c42 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -11,3 +11,13 @@ tasks: desc: Check if the input is formatted cmds: - terraform fmt -recursive -check -diff . + + test: + desc: Run Terraform unit tests + cmds: + - terraform test + + test:verbose: + desc: Run Terraform unit tests with verbose output + cmds: + - terraform test -verbose diff --git a/data.tf b/data.tf index 30cf6e1..3c6e2a6 100644 --- a/data.tf +++ b/data.tf @@ -25,4 +25,11 @@ data "cloudinit_config" "config" { }) filename = "sensor-build.yaml" } + + lifecycle { + precondition { + condition = (var.fleet_token == "" && var.fleet_url == "" && var.fleet_server_sslname == "") || (var.fleet_token != "" && var.fleet_url != "" && var.fleet_server_sslname != "") + error_message = "Fleet Manager pairing requires all three variables to be set together: fleet_token, fleet_url, and fleet_server_sslname. Either set all three or leave all three empty." + } + } } diff --git a/examples/deployment/main.tf b/examples/deployment/main.tf deleted file mode 100644 index f728849..0000000 --- a/examples/deployment/main.tf +++ /dev/null @@ -1,28 +0,0 @@ -locals { - community_string = "abc123" - license = file("~/corelight-license.txt") - mgmt_interface = "eth0" - mon_interface = "eth1" - probe_source_ranges_cidr = ["130.211.0.0/22", "35.191.0.0/16"] - mon_cidr = "10.3.0.0/24" - mon_gateway = "10.3.0.1" - fleet_token = "b1cd099ff22ed8a41abc63929d1db126" - fleet_url = "https://fleet.example.com:1443/fleet/v1/internal/softsensor/websocket" - -} - -module "sensor_config" { - source = "../../" - - fleet_community_string = local.community_string - fleet_token = local.fleet_token - fleet_url = local.fleet_url - - sensor_license = local.license - sensor_management_interface_name = local.mgmt_interface - sensor_monitoring_interface_name = local.mon_interface - sensor_health_check_probe_source_ranges_cidr = local.probe_source_ranges_cidr - subnetwork_monitoring_cidr = local.mon_cidr - subnetwork_monitoring_gateway = local.mon_gateway - -} diff --git a/examples/deployment/versions.tf b/examples/deployment/versions.tf deleted file mode 100644 index deaa9dd..0000000 --- a/examples/deployment/versions.tf +++ /dev/null @@ -1,3 +0,0 @@ -terraform { - required_version = ">=1.3.2" -} diff --git a/outputs.tf b/outputs.tf index bffa87c..6f06f4b 100644 --- a/outputs.tf +++ b/outputs.tf @@ -1,3 +1,5 @@ output "cloudinit_config" { - value = data.cloudinit_config.config + value = data.cloudinit_config.config + sensitive = true + description = "The complete cloudinit_config data source object. Use .rendered for the final user data string." } diff --git a/tests/basic_configuration.tftest.hcl b/tests/basic_configuration.tftest.hcl new file mode 100644 index 0000000..5c67a20 --- /dev/null +++ b/tests/basic_configuration.tftest.hcl @@ -0,0 +1,91 @@ +# Test basic sensor configuration without optional features +run "basic_sensor_config" { + command = plan + + variables { + fleet_community_string = "test-community-string" + sensor_license = "test-license-key" + sensor_management_interface_name = "eth0" + sensor_monitoring_interface_name = "eth1" + sensor_health_check_probe_source_ranges_cidr = ["35.191.0.0/16", "130.211.0.0/22"] + subnetwork_monitoring_cidr = "10.0.1.0/24" + subnetwork_monitoring_gateway = "10.0.1.1" + } + + assert { + condition = output.cloudinit_config.rendered != null + error_message = "cloudinit_config output should not be null" + } + + assert { + condition = can(regex("password: test-community-string", output.cloudinit_config.rendered)) + error_message = "Community string should be present in rendered config" + } + + assert { + condition = can(regex("license_key: test-license-key", output.cloudinit_config.rendered)) + error_message = "License should be present in rendered config" + } + + assert { + condition = can(regex("name: eth0", output.cloudinit_config.rendered)) + error_message = "Management interface name should be present in rendered config" + } + + assert { + condition = can(regex("name: eth1", output.cloudinit_config.rendered)) + error_message = "Monitoring interface name should be present in rendered config" + } +} + +# Test minimal configuration without health checks +run "minimal_config_without_health_checks" { + command = plan + + variables { + fleet_community_string = "test-community-string" + sensor_license = "test-license-key" + sensor_management_interface_name = "eth0" + sensor_monitoring_interface_name = "eth1" + } + + assert { + condition = output.cloudinit_config.rendered != null + error_message = "cloudinit_config output should not be null" + } + + assert { + condition = can(regex("password: test-community-string", output.cloudinit_config.rendered)) + error_message = "Community string should be present in rendered config" + } +} + +# Test that configuration includes health check when both CIDR and gateway are provided +run "health_check_present_when_configured" { + command = plan + + variables { + fleet_community_string = "test-community-string" + sensor_license = "test-license-key" + sensor_management_interface_name = "eth0" + sensor_monitoring_interface_name = "eth1" + subnetwork_monitoring_cidr = "10.0.1.0/24" + subnetwork_monitoring_gateway = "10.0.1.1" + sensor_health_check_probe_source_ranges_cidr = ["35.191.0.0/16"] + } + + assert { + condition = can(regex("health_check:", output.cloudinit_config.rendered)) + error_message = "Health check section should be present when CIDR and gateway are configured" + } + + assert { + condition = can(regex("10.0.1.0/24", output.cloudinit_config.rendered)) + error_message = "Monitoring CIDR should be present in health check config" + } + + assert { + condition = can(regex("10.0.1.1", output.cloudinit_config.rendered)) + error_message = "Monitoring gateway should be present in health check config" + } +} diff --git a/tests/feature_flags.tftest.hcl b/tests/feature_flags.tftest.hcl new file mode 100644 index 0000000..0849f6f --- /dev/null +++ b/tests/feature_flags.tftest.hcl @@ -0,0 +1,115 @@ +# Test Prometheus feature flag +run "prometheus_enabled" { + command = plan + + variables { + fleet_community_string = "test-community-string" + sensor_license = "test-license-key" + sensor_management_interface_name = "eth0" + sensor_monitoring_interface_name = "eth1" + prometheus_enabled = true + } + + assert { + condition = output.cloudinit_config.rendered != null + error_message = "cloudinit_config output should not be null" + } + + assert { + condition = can(regex("prometheus:", output.cloudinit_config.rendered)) + error_message = "Prometheus section should be present when enabled" + } +} + +# Test Prometheus disabled (default) +run "prometheus_disabled" { + command = plan + + variables { + fleet_community_string = "test-community-string" + sensor_license = "test-license-key" + sensor_management_interface_name = "eth0" + sensor_monitoring_interface_name = "eth1" + prometheus_enabled = false + } + + assert { + condition = output.cloudinit_config.rendered != null + error_message = "cloudinit_config output should not be null" + } + + assert { + condition = !can(regex("prometheus:", output.cloudinit_config.rendered)) + error_message = "Prometheus section should not be present when disabled" + } +} + +# Test FedRAMP mode enabled +run "fedramp_enabled" { + command = plan + + variables { + fleet_community_string = "test-community-string" + sensor_license = "test-license-key" + sensor_management_interface_name = "eth0" + sensor_monitoring_interface_name = "eth1" + fedramp_mode_enabled = true + } + + assert { + condition = output.cloudinit_config.rendered != null + error_message = "cloudinit_config output should not be null" + } + + assert { + condition = can(regex("fedramp_mode:", output.cloudinit_config.rendered)) + error_message = "FedRAMP mode section should be present when enabled" + } +} + +# Test FedRAMP mode disabled (default) +run "fedramp_disabled" { + command = plan + + variables { + fleet_community_string = "test-community-string" + sensor_license = "test-license-key" + sensor_management_interface_name = "eth0" + sensor_monitoring_interface_name = "eth1" + fedramp_mode_enabled = false + } + + assert { + condition = output.cloudinit_config.rendered != null + error_message = "cloudinit_config output should not be null" + } + + assert { + condition = !can(regex("fedramp_mode:", output.cloudinit_config.rendered)) + error_message = "FedRAMP mode section should not be present when disabled" + } +} + +# Test both features enabled together +run "multiple_features_enabled" { + command = plan + + variables { + fleet_community_string = "test-community-string" + sensor_license = "test-license-key" + sensor_management_interface_name = "eth0" + sensor_monitoring_interface_name = "eth1" + prometheus_enabled = true + fedramp_mode_enabled = true + } + + assert { + condition = can(regex("prometheus:", output.cloudinit_config.rendered)) + error_message = "Prometheus section should be present when enabled" + } + + assert { + condition = can(regex("fedramp_mode:", output.cloudinit_config.rendered)) + error_message = "FedRAMP mode section should be present when enabled" + } +} diff --git a/tests/fleet_manager.tftest.hcl b/tests/fleet_manager.tftest.hcl new file mode 100644 index 0000000..603f24e --- /dev/null +++ b/tests/fleet_manager.tftest.hcl @@ -0,0 +1,93 @@ +# Test Fleet Manager pairing configuration +run "fleet_manager_pairing_complete" { + command = plan + + variables { + fleet_community_string = "test-community-string" + sensor_license = "test-license-key" + sensor_management_interface_name = "eth0" + sensor_monitoring_interface_name = "eth1" + + # Fleet Manager configuration + fleet_token = "b1cd099ff22ed8a41abc63929d1db126" + fleet_url = "https://fleet.example.com:1443/fleet/v1/internal/softsensor/websocket" + fleet_server_sslname = "fleet.example.com" + } + + assert { + condition = output.cloudinit_config.rendered != null + error_message = "cloudinit_config output should not be null" + } + + assert { + condition = can(regex("token: b1cd099ff22ed8a41abc63929d1db126", output.cloudinit_config.rendered)) + error_message = "Fleet token should be present in rendered config" + } + + assert { + condition = can(regex("url: https://fleet.example.com:1443/fleet/v1/internal/softsensor/websocket", output.cloudinit_config.rendered)) + error_message = "Fleet URL should be present in rendered config" + } + + assert { + condition = can(regex("server_sslname: fleet.example.com", output.cloudinit_config.rendered)) + error_message = "Fleet server SSL name should be present in rendered config" + } +} + +# Test Fleet Manager with proxy configuration +run "fleet_manager_with_proxy" { + command = plan + + variables { + fleet_community_string = "test-community-string" + sensor_license = "test-license-key" + sensor_management_interface_name = "eth0" + sensor_monitoring_interface_name = "eth1" + + # Fleet Manager configuration with proxy + fleet_token = "test-token" + fleet_url = "https://fleet.example.com:1443/fleet/v1/internal/softsensor/websocket" + fleet_server_sslname = "fleet.example.com" + fleet_http_proxy = "http://proxy.example.com:8080" + fleet_https_proxy = "https://proxy.example.com:8443" + fleet_no_proxy = "localhost,127.0.0.1,.example.com" + } + + assert { + condition = can(regex("http_proxy: http://proxy.example.com:8080", output.cloudinit_config.rendered)) + error_message = "HTTP proxy should be present in rendered config" + } + + assert { + condition = can(regex("https_proxy: https://proxy.example.com:8443", output.cloudinit_config.rendered)) + error_message = "HTTPS proxy should be present in rendered config" + } + + assert { + condition = can(regex("no_proxy: localhost,127.0.0.1,.example.com", output.cloudinit_config.rendered)) + error_message = "No proxy should be present in rendered config" + } +} + +# Test that configuration works without Fleet Manager +run "no_fleet_manager" { + command = plan + + variables { + fleet_community_string = "test-community-string" + sensor_license = "test-license-key" + sensor_management_interface_name = "eth0" + sensor_monitoring_interface_name = "eth1" + } + + assert { + condition = output.cloudinit_config.rendered != null + error_message = "cloudinit_config output should not be null without Fleet Manager" + } + + assert { + condition = !can(regex("fleet_token:", output.cloudinit_config.rendered)) + error_message = "Fleet token should not be present when not configured" + } +} diff --git a/tests/integration.tftest.hcl b/tests/integration.tftest.hcl new file mode 100644 index 0000000..2f47810 --- /dev/null +++ b/tests/integration.tftest.hcl @@ -0,0 +1,109 @@ +# Integration test with all features enabled +run "full_configuration" { + command = plan + + variables { + # Core configuration + fleet_community_string = "test-community-string" + sensor_license = "test-license-key" + sensor_management_interface_name = "eth0" + sensor_monitoring_interface_name = "eth1" + + # Health check configuration + sensor_health_check_http_port = "41080" + sensor_health_check_probe_source_ranges_cidr = ["35.191.0.0/16", "130.211.0.0/22"] + subnetwork_monitoring_cidr = "10.0.1.0/24" + subnetwork_monitoring_gateway = "10.0.1.1" + + # Fleet Manager configuration + fleet_token = "b1cd099ff22ed8a41abc63929d1db126" + fleet_url = "https://fleet.example.com:1443/fleet/v1/internal/softsensor/websocket" + fleet_server_sslname = "fleet.example.com" + fleet_http_proxy = "http://proxy.example.com:8080" + fleet_https_proxy = "https://proxy.example.com:8443" + fleet_no_proxy = "localhost,127.0.0.1" + + # Feature flags + prometheus_enabled = true + fedramp_mode_enabled = true + + # Output encoding + gzip_config = true + base64_encode_config = true + } + + # Note: Output is gzipped and base64 encoded, so we can't regex match content + # Content validation is covered by other tests without encoding + + assert { + condition = output.cloudinit_config.rendered != null + error_message = "cloudinit_config output should not be null" + } + + # Verify output encoding is enabled + assert { + condition = output.cloudinit_config.gzip == true + error_message = "gzip should be enabled" + } + + assert { + condition = output.cloudinit_config.base64_encode == true + error_message = "base64_encode should be enabled" + } + + # Verify rendered output is base64-encoded (starts with expected base64 chars) + assert { + condition = can(regex("^[A-Za-z0-9+/]+=*$", output.cloudinit_config.rendered)) + error_message = "rendered output should be base64 encoded" + } +} + +# Integration test with minimal configuration (only required variables) +run "minimal_configuration" { + command = plan + + variables { + fleet_community_string = "test-community-string" + sensor_license = "test-license-key" + sensor_management_interface_name = "eth0" + sensor_monitoring_interface_name = "eth1" + } + + assert { + condition = output.cloudinit_config.rendered != null + error_message = "cloudinit_config output should not be null with minimal config" + } + + # Verify core configuration is present + assert { + condition = can(regex("password:", output.cloudinit_config.rendered)) + error_message = "Community string should be present" + } + + # Verify optional features are not present + assert { + condition = !can(regex("fleet_token:", output.cloudinit_config.rendered)) + error_message = "Fleet token should not be present in minimal config" + } + + assert { + condition = !can(regex("prometheus:", output.cloudinit_config.rendered)) + error_message = "Prometheus should not be present in minimal config" + } + + assert { + condition = !can(regex("fedramp_mode:", output.cloudinit_config.rendered)) + error_message = "FedRAMP mode should not be present in minimal config" + } + + # Verify default encoding + assert { + condition = output.cloudinit_config.gzip == false + error_message = "gzip should be disabled by default" + } + + assert { + condition = output.cloudinit_config.base64_encode == false + error_message = "base64_encode should be disabled by default" + } +} diff --git a/tests/output_encoding.tftest.hcl b/tests/output_encoding.tftest.hcl new file mode 100644 index 0000000..4a302f0 --- /dev/null +++ b/tests/output_encoding.tftest.hcl @@ -0,0 +1,111 @@ +# Test gzip encoding enabled (requires base64 to also be enabled) +run "gzip_enabled" { + command = plan + + variables { + fleet_community_string = "test-community-string" + sensor_license = "test-license-key" + sensor_management_interface_name = "eth0" + sensor_monitoring_interface_name = "eth1" + gzip_config = true + base64_encode_config = true + } + + assert { + condition = output.cloudinit_config.rendered != null + error_message = "cloudinit_config output should not be null" + } + + assert { + condition = output.cloudinit_config.gzip == true + error_message = "gzip should be true when gzip_config is enabled" + } + + assert { + condition = output.cloudinit_config.base64_encode == true + error_message = "base64_encode must be true when gzip is enabled" + } +} + +# Test gzip encoding disabled (default) +run "gzip_disabled" { + command = plan + + variables { + fleet_community_string = "test-community-string" + sensor_license = "test-license-key" + sensor_management_interface_name = "eth0" + sensor_monitoring_interface_name = "eth1" + gzip_config = false + } + + assert { + condition = output.cloudinit_config.gzip == false + error_message = "gzip should be false when gzip_config is disabled" + } +} + +# Test base64 encoding enabled +run "base64_enabled" { + command = plan + + variables { + fleet_community_string = "test-community-string" + sensor_license = "test-license-key" + sensor_management_interface_name = "eth0" + sensor_monitoring_interface_name = "eth1" + base64_encode_config = true + } + + assert { + condition = output.cloudinit_config.base64_encode == true + error_message = "base64_encode should be true when base64_encode_config is enabled" + } +} + +# Test base64 encoding disabled (default) +run "base64_disabled" { + command = plan + + variables { + fleet_community_string = "test-community-string" + sensor_license = "test-license-key" + sensor_management_interface_name = "eth0" + sensor_monitoring_interface_name = "eth1" + base64_encode_config = false + } + + assert { + condition = output.cloudinit_config.base64_encode == false + error_message = "base64_encode should be false when base64_encode_config is disabled" + } +} + +# Test both encodings enabled +run "both_encodings_enabled" { + command = plan + + variables { + fleet_community_string = "test-community-string" + sensor_license = "test-license-key" + sensor_management_interface_name = "eth0" + sensor_monitoring_interface_name = "eth1" + gzip_config = true + base64_encode_config = true + } + + assert { + condition = output.cloudinit_config.gzip == true + error_message = "gzip should be true when gzip_config is enabled" + } + + assert { + condition = output.cloudinit_config.base64_encode == true + error_message = "base64_encode should be true when base64_encode_config is enabled" + } + + assert { + condition = output.cloudinit_config.rendered != null + error_message = "cloudinit_config rendered output should not be null" + } +} diff --git a/tests/validation.tftest.hcl b/tests/validation.tftest.hcl new file mode 100644 index 0000000..59186e9 --- /dev/null +++ b/tests/validation.tftest.hcl @@ -0,0 +1,110 @@ +# Test Fleet Manager validation - token without URL should fail +run "fleet_token_without_url_fails" { + command = plan + + variables { + fleet_community_string = "test-community-string" + sensor_license = "test-license-key" + sensor_management_interface_name = "eth0" + sensor_monitoring_interface_name = "eth1" + fleet_token = "test-token" + # fleet_url and fleet_server_sslname intentionally omitted + } + + expect_failures = [ + data.cloudinit_config.config, + ] +} + +# Test Fleet Manager validation - URL without token should fail +run "fleet_url_without_token_fails" { + command = plan + + variables { + fleet_community_string = "test-community-string" + sensor_license = "test-license-key" + sensor_management_interface_name = "eth0" + sensor_monitoring_interface_name = "eth1" + fleet_url = "https://fleet.example.com:1443/fleet/v1/internal/softsensor/websocket" + # fleet_token and fleet_server_sslname intentionally omitted + } + + expect_failures = [ + data.cloudinit_config.config, + ] +} + +# Test Fleet Manager validation - token and URL without SSL name should fail +run "fleet_without_sslname_fails" { + command = plan + + variables { + fleet_community_string = "test-community-string" + sensor_license = "test-license-key" + sensor_management_interface_name = "eth0" + sensor_monitoring_interface_name = "eth1" + fleet_token = "test-token" + fleet_url = "https://fleet.example.com:1443/fleet/v1/internal/softsensor/websocket" + # fleet_server_sslname intentionally omitted + } + + expect_failures = [ + data.cloudinit_config.config, + ] +} + +# Test Fleet Manager validation - SSL name without token and URL should fail +run "fleet_sslname_without_token_url_fails" { + command = plan + + variables { + fleet_community_string = "test-community-string" + sensor_license = "test-license-key" + sensor_management_interface_name = "eth0" + sensor_monitoring_interface_name = "eth1" + fleet_server_sslname = "fleet.example.com" + # fleet_token and fleet_url intentionally omitted + } + + expect_failures = [ + data.cloudinit_config.config, + ] +} + +# Test that all three Fleet variables together passes validation +run "fleet_all_three_variables_succeeds" { + command = plan + + variables { + fleet_community_string = "test-community-string" + sensor_license = "test-license-key" + sensor_management_interface_name = "eth0" + sensor_monitoring_interface_name = "eth1" + fleet_token = "test-token" + fleet_url = "https://fleet.example.com:1443/fleet/v1/internal/softsensor/websocket" + fleet_server_sslname = "fleet.example.com" + } + + assert { + condition = output.cloudinit_config.rendered != null + error_message = "cloudinit_config should be valid when all Fleet variables are provided" + } +} + +# Test that none of the Fleet variables also passes validation +run "fleet_no_variables_succeeds" { + command = plan + + variables { + fleet_community_string = "test-community-string" + sensor_license = "test-license-key" + sensor_management_interface_name = "eth0" + sensor_monitoring_interface_name = "eth1" + # All fleet_* variables intentionally omitted (using defaults) + } + + assert { + condition = output.cloudinit_config.rendered != null + error_message = "cloudinit_config should be valid when no Fleet variables are provided" + } +} diff --git a/variables.tf b/variables.tf index ba73fc5..9000f53 100644 --- a/variables.tf +++ b/variables.tf @@ -6,7 +6,6 @@ variable "fleet_community_string" { variable "sensor_license" { type = string - sensitive = true description = "path to the Corelight sensor license file" } @@ -48,6 +47,11 @@ variable "subnetwork_monitoring_cidr" { type = string default = "" description = "(optional) the monitoring subnet for the sensor(s), leaving this empty will result in no sensor.monitoring_interface.health_check section being rendered into user data" + + validation { + condition = var.subnetwork_monitoring_cidr == "" || can(cidrhost(var.subnetwork_monitoring_cidr, 0)) + error_message = "subnetwork_monitoring_cidr must be a valid CIDR block (e.g., '10.0.0.0/24') or empty." + } } variable "subnetwork_monitoring_gateway" { diff --git a/versions.tf b/versions.tf new file mode 100644 index 0000000..b2276c1 --- /dev/null +++ b/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.3.2" + + required_providers { + cloudinit = { + source = "hashicorp/cloudinit" + version = ">= 2.3.0, < 3.0.0" + } + } +} \ No newline at end of file