From a0ffbc1d975782fcfe1bc30dcabc8057809f890f Mon Sep 17 00:00:00 2001 From: Evgeniy Belyi Date: Wed, 11 Feb 2026 22:29:13 -0600 Subject: [PATCH 1/7] Implement verifier receiver --- .../elastic-components/manifest.yaml | 2 + receiver/verifierreceiver/Makefile | 1 + receiver/verifierreceiver/README.md | 366 +++++++++++ receiver/verifierreceiver/config.go | 359 +++++++++++ receiver/verifierreceiver/config_test.go | 340 ++++++++++ receiver/verifierreceiver/doc.go | 69 +++ receiver/verifierreceiver/factory.go | 62 ++ receiver/verifierreceiver/factory_test.go | 94 +++ .../generated_package_test.go | 35 ++ receiver/verifierreceiver/go.mod | 77 +++ receiver/verifierreceiver/go.sum | 201 ++++++ .../internal/metadata/generated_status.go | 33 + .../internal/verifier/aws_verifier.go | 580 ++++++++++++++++++ .../internal/verifier/verifier.go | 338 ++++++++++ receiver/verifierreceiver/metadata.yaml | 13 + receiver/verifierreceiver/receiver.go | 524 ++++++++++++++++ receiver/verifierreceiver/receiver_test.go | 424 +++++++++++++ receiver/verifierreceiver/registry.go | 540 ++++++++++++++++ receiver/verifierreceiver/testdata/TESTING.md | 333 ++++++++++ .../testdata/agent-simulation.yaml | 74 +++ .../verifierreceiver/testdata/config.yaml | 103 ++++ .../testdata/expected-logs.yaml | 190 ++++++ .../testdata/otel-config.yaml | 27 + .../testdata/standalone-test.yaml | 90 +++ .../testdata/templates/verifier.yaml | 33 + .../testdata/test-csp-profile.yaml | 36 ++ 26 files changed, 4944 insertions(+) create mode 100644 receiver/verifierreceiver/Makefile create mode 100644 receiver/verifierreceiver/README.md create mode 100644 receiver/verifierreceiver/config.go create mode 100644 receiver/verifierreceiver/config_test.go create mode 100644 receiver/verifierreceiver/doc.go create mode 100644 receiver/verifierreceiver/factory.go create mode 100644 receiver/verifierreceiver/factory_test.go create mode 100644 receiver/verifierreceiver/generated_package_test.go create mode 100644 receiver/verifierreceiver/go.mod create mode 100644 receiver/verifierreceiver/go.sum create mode 100644 receiver/verifierreceiver/internal/metadata/generated_status.go create mode 100644 receiver/verifierreceiver/internal/verifier/aws_verifier.go create mode 100644 receiver/verifierreceiver/internal/verifier/verifier.go create mode 100644 receiver/verifierreceiver/metadata.yaml create mode 100644 receiver/verifierreceiver/receiver.go create mode 100644 receiver/verifierreceiver/receiver_test.go create mode 100644 receiver/verifierreceiver/registry.go create mode 100644 receiver/verifierreceiver/testdata/TESTING.md create mode 100644 receiver/verifierreceiver/testdata/agent-simulation.yaml create mode 100644 receiver/verifierreceiver/testdata/config.yaml create mode 100644 receiver/verifierreceiver/testdata/expected-logs.yaml create mode 100644 receiver/verifierreceiver/testdata/otel-config.yaml create mode 100644 receiver/verifierreceiver/testdata/standalone-test.yaml create mode 100644 receiver/verifierreceiver/testdata/templates/verifier.yaml create mode 100644 receiver/verifierreceiver/testdata/test-csp-profile.yaml diff --git a/distributions/elastic-components/manifest.yaml b/distributions/elastic-components/manifest.yaml index fed107700..aca083aeb 100644 --- a/distributions/elastic-components/manifest.yaml +++ b/distributions/elastic-components/manifest.yaml @@ -39,6 +39,7 @@ receivers: - gomod: github.com/elastic/opentelemetry-collector-components/receiver/integrationreceiver v0.0.0 - gomod: github.com/elastic/opentelemetry-collector-components/receiver/loadgenreceiver v0.20.0 - gomod: github.com/elastic/opentelemetry-collector-components/receiver/elasticapmintakereceiver v0.20.0 + - gomod: github.com/elastic/opentelemetry-collector-components/receiver/verifierreceiver v0.0.0 processors: - gomod: go.opentelemetry.io/collector/processor/memorylimiterprocessor v0.144.0 @@ -89,5 +90,6 @@ replaces: - github.com/elastic/opentelemetry-collector-components/connector/dynamicroutingconnector => ../connector/dynamicroutingconnector - github.com/elastic/opentelemetry-collector-components/receiver/elasticapmintakereceiver => ../receiver/elasticapmintakereceiver - github.com/elastic/opentelemetry-collector-components/receiver/integrationreceiver => ../receiver/integrationreceiver + - github.com/elastic/opentelemetry-collector-components/receiver/verifierreceiver => ../receiver/verifierreceiver - github.com/elastic/opentelemetry-collector-components/processor/elastictraceprocessor => ../processor/elastictraceprocessor - github.com/elastic/opentelemetry-collector-components/internal/elasticattr => ../internal/elasticattr diff --git a/receiver/verifierreceiver/Makefile b/receiver/verifierreceiver/Makefile new file mode 100644 index 000000000..ded7a3609 --- /dev/null +++ b/receiver/verifierreceiver/Makefile @@ -0,0 +1 @@ +include ../../Makefile.Common diff --git a/receiver/verifierreceiver/README.md b/receiver/verifierreceiver/README.md new file mode 100644 index 000000000..46b508b37 --- /dev/null +++ b/receiver/verifierreceiver/README.md @@ -0,0 +1,366 @@ +# Verifier Receiver + +## Overview + +The Verifier Receiver is a custom EDOT (Elastic Distribution of OpenTelemetry) collector component that verifies permissions for cloud connector based integrations and reports results as OTEL logs to Elasticsearch. + +## Features + +- **Multi-Provider Architecture**: Extensible design supporting AWS, Azure, GCP, Okta, and other providers +- **Permission Registry**: Internal mapping of integration types to their required permissions +- **Active Verification**: Makes actual API calls to verify permissions (granted/denied) +- **On-demand verification**: Proactively check all permissions for attached integrations +- **Structured reporting**: Output OTEL logs with full policy/integration context to Elasticsearch +- **Policy-aware**: Results are grouped by Cloud Connector, policy, and integration for clear remediation +- **Verification Methods**: Supports `api_call` (minimal API calls) and `dry_run` (EC2-style DryRun parameter) + +## Supported Providers + +| Provider | Status | Integrations | +|----------|--------|--------------| +| **AWS** | Active | CloudTrail, GuardDuty, Security Hub, S3, EC2, VPC Flow Logs, WAF, Route53, ELB, CloudFront | +| **Azure** | Planned | Activity Logs, Audit Logs, Blob Storage | +| **GCP** | Planned | Audit Logs, Cloud Storage, Pub/Sub | +| **Okta** | Planned | System Logs, User Events | + +## Configuration + +The receiver configuration follows the RFC structure for Cloud Connector Permission Verification: + +```yaml +receivers: + verifier: + # Cloud Connector identification + cloud_connector_id: "cc-12345" + cloud_connector_name: "Production Connector" + + # Verification session + verification_id: "verify-abc123" + verification_type: "on_demand" # or "scheduled" + + # Provider credentials + providers: + # AWS Authentication - Cloud Connector STS AssumeRole + aws: + credentials: + role_arn: "arn:aws:iam::123456789012:role/ElasticAgentRole" + external_id: "elastic-external-id-from-setup" + default_region: "us-east-1" + + # Azure Authentication (future) + # azure: + # credentials: + # tenant_id: "your-tenant-id" + # client_id: "your-client-id" + # client_secret: "your-client-secret" + + # GCP Authentication (future) + # gcp: + # credentials: + # project_id: "your-project-id" + # use_default_credentials: true + + # Okta Authentication (future) + # okta: + # credentials: + # domain: "dev-123456.okta.com" + # api_token: "your-api-token" + + # Policy context from Fleet API (no permissions specified!) + policies: + - policy_id: "policy-1" + policy_name: "AWS Security Monitoring" + integrations: + - integration_id: "int-cloudtrail-001" + integration_type: "aws_cloudtrail" + integration_name: "AWS CloudTrail" + config: + account_id: "123456789012" + region: "us-east-1" + - integration_id: "int-guardduty-001" + integration_type: "aws_guardduty" + integration_name: "AWS GuardDuty" + config: + account_id: "123456789012" + region: "us-east-1" + + - policy_id: "policy-2" + policy_name: "AWS Infrastructure" + integrations: + - integration_id: "int-ec2-001" + integration_type: "aws_ec2" + integration_name: "AWS EC2 Metrics" + config: + account_id: "123456789012" +``` + +### Configuration Options + +| Option | Type | Required | Default | Description | +|--------|------|----------|---------|-------------| +| `cloud_connector_id` | `string` | Yes | - | Unique identifier for the Cloud Connector | +| `cloud_connector_name` | `string` | No | - | Human-readable name of the Cloud Connector | +| `verification_id` | `string` | Yes | - | Unique identifier for this verification session | +| `verification_type` | `string` | No | `on_demand` | Type of verification (`on_demand` or `scheduled`) | +| `providers` | `ProvidersConfig` | No | - | Provider credentials for AWS, Azure, GCP, Okta | +| `policies` | `[]PolicyConfig` | Yes | - | List of policies to verify | + +### Provider Credentials + +#### AWS (`providers.aws.credentials`) + +| Option | Type | Required | Default | Description | +|--------|------|----------|---------|-------------| +| `role_arn` | `string` | Yes* | - | ARN of the IAM role to assume | +| `external_id` | `string` | Yes* | - | External ID for confused deputy protection | +| `default_region` | `string` | No | `us-east-1` | Default AWS region for API calls | +| `use_default_credentials` | `bool` | No | `false` | Use AWS SDK default credential chain (for testing) | + +*Required when using Cloud Connector authentication. Not required if `use_default_credentials` is `true`. + +#### Azure (`providers.azure.credentials`) - Future + +| Option | Type | Required | Description | +|--------|------|----------|-------------| +| `tenant_id` | `string` | Yes* | Azure AD tenant ID | +| `client_id` | `string` | Yes* | Azure AD application client ID | +| `client_secret` | `string` | Yes* | Azure AD application secret | +| `subscription_id` | `string` | No | Azure subscription ID | +| `use_managed_identity` | `bool` | No | Use Azure Managed Identity | + +#### GCP (`providers.gcp.credentials`) - Future + +| Option | Type | Required | Description | +|--------|------|----------|-------------| +| `project_id` | `string` | No | GCP project ID | +| `service_account_key` | `string` | No | Service account JSON key | +| `use_default_credentials` | `bool` | No | Use application default credentials | +| `impersonate_service_account` | `string` | No | Service account to impersonate | + +#### Okta (`providers.okta.credentials`) - Future + +| Option | Type | Required | Description | +|--------|------|----------|-------------| +| `domain` | `string` | Yes | Okta domain (e.g., `dev-123456.okta.com`) | +| `api_token` | `string` | Yes* | Okta API token | +| `client_id` | `string` | No | OAuth 2.0 client ID | +| `private_key` | `string` | No | Private key for OAuth authentication | + +### PolicyConfig + +| Option | Type | Required | Description | +|--------|------|----------|-------------| +| `policy_id` | `string` | Yes | Unique identifier for the policy | +| `policy_name` | `string` | No | Human-readable name of the policy | +| `integrations` | `[]IntegrationConfig` | Yes | List of integrations within this policy | + +### IntegrationConfig + +| Option | Type | Required | Description | +|--------|------|----------|-------------| +| `integration_id` | `string` | No | Unique identifier for the integration instance | +| `integration_type` | `string` | Yes | Package/integration type (e.g., `aws_cloudtrail`) | +| `integration_name` | `string` | No | Human-readable name of the integration | +| `config` | `map[string]interface{}` | No | Provider-specific configuration | + +## Supported Integration Types + +### AWS Integrations + +| Integration Type | Permissions Verified | +|-----------------|---------------------| +| `aws_cloudtrail` | `cloudtrail:LookupEvents`, `cloudtrail:DescribeTrails`, `s3:GetObject`, `s3:ListBucket`, `sqs:ReceiveMessage` | +| `aws_guardduty` | `guardduty:ListDetectors`, `guardduty:GetFindings`, `guardduty:ListFindings` | +| `aws_securityhub` | `securityhub:GetFindings`, `securityhub:DescribeHub` | +| `aws_s3` | `s3:ListBucket`, `s3:GetObject`, `s3:GetBucketLocation` | +| `aws_ec2` | `ec2:DescribeInstances`, `ec2:DescribeRegions`, `cloudwatch:GetMetricData` | +| `aws_vpcflow` | `logs:FilterLogEvents`, `logs:DescribeLogGroups`, `ec2:DescribeFlowLogs` | +| `aws_waf` | `wafv2:GetWebACL`, `wafv2:ListWebACLs`, `s3:GetObject` | +| `aws_route53` | `logs:FilterLogEvents`, `logs:DescribeLogGroups`, `route53:ListHostedZones` | +| `aws_elb` | `s3:GetObject`, `s3:ListBucket`, `elasticloadbalancing:DescribeLoadBalancers` | +| `aws_cloudfront` | `s3:GetObject`, `s3:ListBucket`, `cloudfront:ListDistributions` | + +### Azure Integrations (Planned) + +| Integration Type | Permissions Verified | +|-----------------|---------------------| +| `azure_activitylogs` | `Microsoft.Insights/eventtypes/values/Read` | +| `azure_auditlogs` | `Microsoft.Insights/eventtypes/values/Read` | +| `azure_blob_storage` | `Microsoft.Storage/storageAccounts/blobServices/containers/read` | + +### GCP Integrations (Planned) + +| Integration Type | Permissions Verified | +|-----------------|---------------------| +| `gcp_audit` | `logging.logEntries.list` | +| `gcp_storage` | `storage.objects.get`, `storage.objects.list` | +| `gcp_pubsub` | `pubsub.subscriptions.consume` | + +### Okta Integrations (Planned) + +| Integration Type | Permissions Verified | +|-----------------|---------------------| +| `okta_system` | `okta.logs.read` | +| `okta_users` | `okta.users.read` | + +## Output + +The receiver emits OTEL logs following the RFC structure. Each log record represents a single permission verification result. + +### Resource Attributes + +| Attribute | Description | +|-----------|-------------| +| `cloud_connector.id` | Cloud Connector identifier | +| `cloud_connector.name` | Cloud Connector name | +| `verification.id` | Verification session ID | +| `verification.timestamp` | When verification started | +| `verification.type` | `on_demand` or `scheduled` | +| `service.name` | Always `permission-verifier` | +| `service.version` | Receiver version | + +### Scope + +| Attribute | Value | +|-----------|-------| +| `name` | `elastic.permission_verification` | +| `version` | `0.0.0` | + +### Log Record Attributes + +| Attribute | Description | +|-----------|-------------| +| `policy.id` | Policy identifier | +| `policy.name` | Policy name | +| `integration.id` | Integration instance identifier | +| `integration.name` | Integration name | +| `integration.type` | Integration type (e.g., `aws_cloudtrail`) | +| `provider.type` | Provider type (`aws`, `azure`, `gcp`, `okta`) | +| `provider.account` | Account identifier (if available) | +| `provider.region` | Region (if available) | +| `permission.action` | Permission being checked (e.g., `cloudtrail:LookupEvents`) | +| `permission.category` | Category (`data_access`, `management`) | +| `permission.status` | Result (`granted`, `denied`, `error`, `skipped`) | +| `permission.required` | Whether this permission is required | +| `permission.error_code` | Error code from provider (if status is `denied` or `error`) | +| `permission.error_message` | Error message from provider (if status is `denied` or `error`) | +| `verification.method` | Method used (`api_call`, `dry_run`, `http_probe`) | +| `verification.endpoint` | API endpoint called for verification | +| `verification.duration_ms` | Time taken for verification in milliseconds | + +## Architecture + +The receiver uses a registry-based architecture for extensibility: + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Verifier Receiver │ +├─────────────────────────────────────────────────────────────────┤ +│ ┌───────────────────┐ ┌───────────────────────────────┐ │ +│ │ Permission │ │ Verifier Registry │ │ +│ │ Registry │ │ │ │ +│ │ │ │ ┌─────────────────────────┐ │ │ +│ │ aws_cloudtrail │ │ │ AWS Verifier (active) │ │ │ +│ │ aws_guardduty │ │ └─────────────────────────┘ │ │ +│ │ azure_activitylogs│ │ ┌─────────────────────────┐ │ │ +│ │ gcp_audit │ │ │ Azure Verifier (future) │ │ │ +│ │ okta_system │ │ └─────────────────────────┘ │ │ +│ │ ... │ │ ┌─────────────────────────┐ │ │ +│ └───────────────────┘ │ │ GCP Verifier (future) │ │ │ +│ │ └─────────────────────────┘ │ │ +│ │ ┌─────────────────────────┐ │ │ +│ │ │ Okta Verifier (future) │ │ │ +│ │ └─────────────────────────┘ │ │ +│ └───────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### Adding a New Provider + +1. Create a verifier in `internal/verifier/` implementing the `Verifier` interface +2. Create a factory function (e.g., `NewAzureVerifierFactory()`) +3. Register the factory in `receiver.go` +4. Add integration mappings in `registry.go` + +## AWS Authentication + +### Cloud Connector Authentication (Recommended) + +When deploying as part of a Cloud Connector, the receiver uses STS AssumeRole with an external ID: + +```yaml +providers: + aws: + credentials: + role_arn: "arn:aws:iam::123456789012:role/ElasticAgentRole" + external_id: "elastic-unique-external-id" + default_region: "us-east-1" +``` + +### Local Development + +For local testing, use the default credential chain: + +```yaml +providers: + aws: + credentials: + use_default_credentials: true + default_region: "us-east-1" +``` + +Run with: `AWS_PROFILE=your-profile ./_build/elastic-collector-components --config ./receiver/verifierreceiver/testdata/test-csp-profile.yaml` + +## Example Pipeline + +```yaml +receivers: + verifier: + cloud_connector_id: "${CLOUD_CONNECTOR_ID}" + verification_id: "${VERIFICATION_ID}" + + providers: + aws: + credentials: + role_arn: "${AWS_ROLE_ARN}" + external_id: "${AWS_EXTERNAL_ID}" + default_region: "us-east-1" + + policies: + - policy_id: "policy-1" + policy_name: "AWS Security Monitoring" + integrations: + - integration_id: "int-cloudtrail-001" + integration_type: "aws_cloudtrail" + integration_name: "AWS CloudTrail" + config: + region: "us-east-1" + +exporters: + elasticsearch: + endpoints: ["${ES_ENDPOINT}"] + api_key: "${ES_API_KEY}" + logs_index: "logs-cloud_connector.permission_verification-default" + +service: + pipelines: + logs: + receivers: [verifier] + exporters: [elasticsearch] +``` + +## Development Status + +This receiver is currently in **development** stability level. + +### Planned +- [ ] Azure verifier implementation +- [ ] GCP verifier implementation +- [ ] Okta verifier implementation +- [ ] Fleet API integration for triggering verification + +## Related + +- [RFC: OTEL Permission Verifier Receiver](https://docs.google.com/document/d/...) +- [GitHub Issue #15628](https://github.com/elastic/security-team/issues/15628) +- [Integration Package](https://github.com/elastic/integrations/tree/main/packages/verifier_otel) diff --git a/receiver/verifierreceiver/config.go b/receiver/verifierreceiver/config.go new file mode 100644 index 000000000..5c14dee22 --- /dev/null +++ b/receiver/verifierreceiver/config.go @@ -0,0 +1,359 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package verifierreceiver // import "github.com/elastic/opentelemetry-collector-components/receiver/verifierreceiver" + +import ( + "errors" + "fmt" + + "github.com/elastic/opentelemetry-collector-components/receiver/verifierreceiver/internal/verifier" +) + +// Config defines configuration for the permission verifier receiver. +// The receiver owns the mapping between integrations and their required permissions. +// Fleet API provides the policy/integration context; the receiver determines what +// permissions each integration needs and how to verify them. +type Config struct { + // CloudConnectorID identifies the Cloud Connector being verified. + CloudConnectorID string `mapstructure:"cloud_connector_id"` + + // CloudConnectorName is the human-readable name of the Cloud Connector. + CloudConnectorName string `mapstructure:"cloud_connector_name"` + + // VerificationID is a unique identifier for this verification session. + VerificationID string `mapstructure:"verification_id"` + + // VerificationType indicates the type of verification: "on_demand" or "scheduled". + VerificationType string `mapstructure:"verification_type"` + + // Providers contains authentication configuration for each cloud/identity provider. + Providers ProvidersConfig `mapstructure:"providers"` + + // Policies is the list of agent policies to verify. + // Each policy contains integrations that need permission verification. + Policies []PolicyConfig `mapstructure:"policies"` +} + +// ProvidersConfig contains authentication configuration for all supported providers. +type ProvidersConfig struct { + // AWS contains AWS-specific authentication configuration. + AWS AWSProviderConfig `mapstructure:"aws"` + + // Azure contains Azure-specific authentication configuration. + Azure AzureProviderConfig `mapstructure:"azure"` + + // GCP contains GCP-specific authentication configuration. + GCP GCPProviderConfig `mapstructure:"gcp"` + + // Okta contains Okta-specific authentication configuration. + Okta OktaProviderConfig `mapstructure:"okta"` +} + +// AWSProviderConfig contains AWS authentication configuration. +type AWSProviderConfig struct { + // Credentials contains the Cloud Connector authentication credentials. + Credentials AWSCredentials `mapstructure:"credentials"` +} + +// AWSCredentials contains the AWS credentials for Cloud Connector mode. +type AWSCredentials struct { + // RoleARN is the ARN of the IAM role to assume in the customer's AWS account. + RoleARN string `mapstructure:"role_arn"` + + // ExternalID is used to prevent confused deputy attacks. + ExternalID string `mapstructure:"external_id"` + + // DefaultRegion is the default AWS region to use for API calls. + DefaultRegion string `mapstructure:"default_region"` + + // UseDefaultCredentials enables using default AWS credentials (for testing). + UseDefaultCredentials bool `mapstructure:"use_default_credentials"` +} + +// Validate validates the AWS credentials. +func (cfg *AWSCredentials) Validate() error { + // If UseDefaultCredentials is set, no other fields are required + if cfg.UseDefaultCredentials { + return nil + } + // If completely empty, that's valid (AWS auth is optional) + if cfg.RoleARN == "" && cfg.ExternalID == "" { + return nil + } + // If partially configured, that's an error + if cfg.RoleARN == "" { + return errors.New("role_arn must be specified when external_id is set") + } + if cfg.ExternalID == "" { + return errors.New("external_id must be specified when role_arn is set") + } + return nil +} + +// IsConfigured returns true if AWS credentials are configured. +func (cfg *AWSCredentials) IsConfigured() bool { + return (cfg.RoleARN != "" && cfg.ExternalID != "") || cfg.UseDefaultCredentials +} + +// ToAuthConfig converts the config to a verifier.AWSAuthConfig. +func (cfg *AWSCredentials) ToAuthConfig() verifier.AWSAuthConfig { + return verifier.AWSAuthConfig{ + RoleARN: cfg.RoleARN, + ExternalID: cfg.ExternalID, + DefaultRegion: cfg.DefaultRegion, + UseDefaultCredentials: cfg.UseDefaultCredentials, + } +} + +// AzureProviderConfig contains Azure authentication configuration. +type AzureProviderConfig struct { + // Credentials contains the Azure authentication credentials. + Credentials AzureCredentials `mapstructure:"credentials"` +} + +// AzureCredentials contains the Azure credentials. +type AzureCredentials struct { + // TenantID is the Azure AD tenant ID. + TenantID string `mapstructure:"tenant_id"` + + // ClientID is the Azure AD application (client) ID. + ClientID string `mapstructure:"client_id"` + + // ClientSecret is the Azure AD application secret. + ClientSecret string `mapstructure:"client_secret"` + + // SubscriptionID is the Azure subscription ID. + SubscriptionID string `mapstructure:"subscription_id"` + + // UseManagedIdentity uses Azure managed identity for authentication. + UseManagedIdentity bool `mapstructure:"use_managed_identity"` +} + +// Validate validates the Azure credentials. +func (cfg *AzureCredentials) Validate() error { + if cfg.UseManagedIdentity { + return nil + } + if cfg.TenantID == "" && cfg.ClientID == "" && cfg.ClientSecret == "" { + return nil // Not configured + } + if cfg.TenantID == "" { + return errors.New("tenant_id must be specified") + } + if cfg.ClientID == "" { + return errors.New("client_id must be specified") + } + if cfg.ClientSecret == "" { + return errors.New("client_secret must be specified") + } + return nil +} + +// IsConfigured returns true if Azure credentials are configured. +func (cfg *AzureCredentials) IsConfigured() bool { + return cfg.UseManagedIdentity || (cfg.TenantID != "" && cfg.ClientID != "" && cfg.ClientSecret != "") +} + +// ToAuthConfig converts the config to a verifier.AzureAuthConfig. +func (cfg *AzureCredentials) ToAuthConfig() verifier.AzureAuthConfig { + return verifier.AzureAuthConfig{ + TenantID: cfg.TenantID, + ClientID: cfg.ClientID, + ClientSecret: cfg.ClientSecret, + SubscriptionID: cfg.SubscriptionID, + UseManagedIdentity: cfg.UseManagedIdentity, + } +} + +// GCPProviderConfig contains GCP authentication configuration. +type GCPProviderConfig struct { + // Credentials contains the GCP authentication credentials. + Credentials GCPCredentials `mapstructure:"credentials"` +} + +// GCPCredentials contains the GCP credentials. +type GCPCredentials struct { + // ProjectID is the GCP project ID. + ProjectID string `mapstructure:"project_id"` + + // ServiceAccountKey is the JSON key for the service account. + ServiceAccountKey string `mapstructure:"service_account_key"` + + // UseDefaultCredentials uses application default credentials. + UseDefaultCredentials bool `mapstructure:"use_default_credentials"` + + // ImpersonateServiceAccount is the service account to impersonate. + ImpersonateServiceAccount string `mapstructure:"impersonate_service_account"` +} + +// Validate validates the GCP credentials. +func (cfg *GCPCredentials) Validate() error { + if cfg.UseDefaultCredentials || cfg.ServiceAccountKey != "" || cfg.ImpersonateServiceAccount != "" { + return nil + } + // Not configured is valid + return nil +} + +// IsConfigured returns true if GCP credentials are configured. +func (cfg *GCPCredentials) IsConfigured() bool { + return cfg.UseDefaultCredentials || cfg.ServiceAccountKey != "" || cfg.ImpersonateServiceAccount != "" +} + +// ToAuthConfig converts the config to a verifier.GCPAuthConfig. +func (cfg *GCPCredentials) ToAuthConfig() verifier.GCPAuthConfig { + return verifier.GCPAuthConfig{ + ProjectID: cfg.ProjectID, + ServiceAccountKey: cfg.ServiceAccountKey, + UseDefaultCredentials: cfg.UseDefaultCredentials, + ImpersonateServiceAccount: cfg.ImpersonateServiceAccount, + } +} + +// OktaProviderConfig contains Okta authentication configuration. +type OktaProviderConfig struct { + // Credentials contains the Okta authentication credentials. + Credentials OktaCredentials `mapstructure:"credentials"` +} + +// OktaCredentials contains the Okta credentials. +type OktaCredentials struct { + // Domain is the Okta domain (e.g., dev-123456.okta.com). + Domain string `mapstructure:"domain"` + + // APIToken is the Okta API token. + APIToken string `mapstructure:"api_token"` + + // ClientID is the OAuth 2.0 client ID (for OAuth authentication). + ClientID string `mapstructure:"client_id"` + + // PrivateKey is the private key for OAuth authentication. + PrivateKey string `mapstructure:"private_key"` +} + +// Validate validates the Okta credentials. +func (cfg *OktaCredentials) Validate() error { + if cfg.Domain == "" && cfg.APIToken == "" && cfg.ClientID == "" { + return nil // Not configured + } + if cfg.Domain == "" { + return errors.New("domain must be specified") + } + if cfg.APIToken == "" && cfg.ClientID == "" { + return errors.New("either api_token or client_id must be specified") + } + if cfg.ClientID != "" && cfg.PrivateKey == "" { + return errors.New("private_key must be specified when using client_id") + } + return nil +} + +// IsConfigured returns true if Okta credentials are configured. +func (cfg *OktaCredentials) IsConfigured() bool { + return cfg.Domain != "" && (cfg.APIToken != "" || (cfg.ClientID != "" && cfg.PrivateKey != "")) +} + +// ToAuthConfig converts the config to a verifier.OktaAuthConfig. +func (cfg *OktaCredentials) ToAuthConfig() verifier.OktaAuthConfig { + return verifier.OktaAuthConfig{ + Domain: cfg.Domain, + APIToken: cfg.APIToken, + ClientID: cfg.ClientID, + PrivateKey: cfg.PrivateKey, + } +} + +// PolicyConfig represents an agent policy with its integrations. +type PolicyConfig struct { + // PolicyID is the unique identifier for the policy. + PolicyID string `mapstructure:"policy_id"` + + // PolicyName is the human-readable name of the policy. + PolicyName string `mapstructure:"policy_name"` + + // Integrations is the list of integrations within this policy. + Integrations []IntegrationConfig `mapstructure:"integrations"` +} + +// IntegrationConfig represents an integration within a policy. +type IntegrationConfig struct { + // IntegrationID is the unique identifier for the integration instance. + IntegrationID string `mapstructure:"integration_id"` + + // IntegrationType is the package/integration type (e.g., "aws_cloudtrail", "okta"). + // This is used to look up required permissions from the registry. + IntegrationType string `mapstructure:"integration_type"` + + // IntegrationName is the human-readable name of the integration. + IntegrationName string `mapstructure:"integration_name"` + + // Config contains provider-specific configuration. + // For AWS: may include regions, account_id, etc. + Config map[string]interface{} `mapstructure:"config"` +} + +// Validate validates the configuration. +func (cfg *Config) Validate() error { + if cfg.CloudConnectorID == "" { + return errors.New("cloud_connector_id must be specified") + } + if len(cfg.Policies) == 0 { + return errors.New("at least one policy must be specified") + } + + for i, policy := range cfg.Policies { + if policy.PolicyID == "" { + return fmt.Errorf("policies[%d]: policy_id must be specified", i) + } + if len(policy.Integrations) == 0 { + return fmt.Errorf("policies[%d]: at least one integration must be specified", i) + } + for j, integration := range policy.Integrations { + if integration.IntegrationType == "" { + return fmt.Errorf("policies[%d].integrations[%d]: integration_type must be specified", i, j) + } + } + } + + // Provider credentials validation is handled by their respective Validate() methods + // which are called automatically by the OTel framework. + + return nil +} + +// GetProviderForIntegration returns the provider type for a given integration type. +func GetProviderForIntegration(integrationType string) verifier.ProviderType { + // AWS integrations start with "aws_" + if len(integrationType) > 4 && integrationType[:4] == "aws_" { + return verifier.ProviderAWS + } + // Azure integrations start with "azure_" + if len(integrationType) > 6 && integrationType[:6] == "azure_" { + return verifier.ProviderAzure + } + // GCP integrations start with "gcp_" + if len(integrationType) > 4 && integrationType[:4] == "gcp_" { + return verifier.ProviderGCP + } + // Okta integrations + if len(integrationType) >= 4 && integrationType[:4] == "okta" { + return verifier.ProviderOkta + } + // Unknown provider + return "" +} diff --git a/receiver/verifierreceiver/config_test.go b/receiver/verifierreceiver/config_test.go new file mode 100644 index 000000000..a6bc1b70c --- /dev/null +++ b/receiver/verifierreceiver/config_test.go @@ -0,0 +1,340 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package verifierreceiver + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/elastic/opentelemetry-collector-components/receiver/verifierreceiver/internal/verifier" +) + +func TestConfig_Validate(t *testing.T) { + tests := []struct { + name string + config Config + wantErr string + }{ + { + name: "valid config with AWS credentials", + config: Config{ + CloudConnectorID: "cc-12345", + CloudConnectorName: "Production Connector", + VerificationID: "verify-abc123", + VerificationType: "on_demand", + Providers: ProvidersConfig{ + AWS: AWSProviderConfig{ + Credentials: AWSCredentials{ + RoleARN: "arn:aws:iam::123456789012:role/ElasticAgentRole", + ExternalID: "elastic-external-id-12345", + DefaultRegion: "us-east-1", + }, + }, + }, + Policies: []PolicyConfig{ + { + PolicyID: "policy-1", + PolicyName: "AWS Security Monitoring", + Integrations: []IntegrationConfig{ + { + IntegrationID: "int-cloudtrail-001", + IntegrationType: "aws_cloudtrail", + IntegrationName: "AWS CloudTrail", + }, + }, + }, + }, + }, + wantErr: "", + }, + { + name: "valid config without AWS credentials (non-AWS integrations)", + config: Config{ + CloudConnectorID: "cc-12345", + VerificationID: "verify-abc123", + Policies: []PolicyConfig{ + { + PolicyID: "policy-1", + Integrations: []IntegrationConfig{ + {IntegrationType: "okta_system"}, + }, + }, + }, + }, + wantErr: "", + }, + { + name: "valid config with AWS integration but no credentials (credentials optional at config level)", + config: Config{ + CloudConnectorID: "cc-12345", + VerificationID: "verify-abc123", + Policies: []PolicyConfig{ + { + PolicyID: "policy-1", + Integrations: []IntegrationConfig{ + {IntegrationType: "aws_cloudtrail"}, + }, + }, + }, + }, + wantErr: "", + }, + // Note: Provider credentials validation is handled by their respective Validate() methods. + { + name: "invalid config - missing cloud_connector_id", + config: Config{ + VerificationID: "verify-abc123", + Policies: []PolicyConfig{ + { + PolicyID: "policy-1", + Integrations: []IntegrationConfig{ + {IntegrationType: "aws_cloudtrail"}, + }, + }, + }, + }, + wantErr: "cloud_connector_id must be specified", + }, + { + name: "invalid config - no policies", + config: Config{ + CloudConnectorID: "cc-12345", + VerificationID: "verify-abc123", + Policies: []PolicyConfig{}, + }, + wantErr: "at least one policy must be specified", + }, + { + name: "invalid config - policy without policy_id", + config: Config{ + CloudConnectorID: "cc-12345", + VerificationID: "verify-abc123", + Policies: []PolicyConfig{ + { + Integrations: []IntegrationConfig{ + {IntegrationType: "aws_cloudtrail"}, + }, + }, + }, + }, + wantErr: "policies[0]: policy_id must be specified", + }, + { + name: "invalid config - policy without integrations", + config: Config{ + CloudConnectorID: "cc-12345", + VerificationID: "verify-abc123", + Policies: []PolicyConfig{ + { + PolicyID: "policy-1", + Integrations: []IntegrationConfig{}, + }, + }, + }, + wantErr: "policies[0]: at least one integration must be specified", + }, + { + name: "invalid config - integration without type", + config: Config{ + CloudConnectorID: "cc-12345", + VerificationID: "verify-abc123", + Policies: []PolicyConfig{ + { + PolicyID: "policy-1", + Integrations: []IntegrationConfig{ + {IntegrationName: "Some Integration"}, + }, + }, + }, + }, + wantErr: "policies[0].integrations[0]: integration_type must be specified", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.config.Validate() + if tt.wantErr != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.wantErr) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestAWSCredentials_Validate(t *testing.T) { + tests := []struct { + name string + credentials AWSCredentials + wantErr string + }{ + { + name: "valid - fully configured", + credentials: AWSCredentials{ + RoleARN: "arn:aws:iam::123456789012:role/ElasticAgentRole", + ExternalID: "test-external-id", + DefaultRegion: "us-east-1", + }, + wantErr: "", + }, + { + name: "valid - empty (not configured)", + credentials: AWSCredentials{}, + wantErr: "", + }, + { + name: "valid - only default_region (considered empty)", + credentials: AWSCredentials{ + DefaultRegion: "us-east-1", + }, + wantErr: "", + }, + { + name: "valid - use_default_credentials", + credentials: AWSCredentials{ + UseDefaultCredentials: true, + }, + wantErr: "", + }, + { + name: "invalid - role_arn without external_id", + credentials: AWSCredentials{ + RoleARN: "arn:aws:iam::123456789012:role/ElasticAgentRole", + }, + wantErr: "external_id must be specified when role_arn is set", + }, + { + name: "invalid - external_id without role_arn", + credentials: AWSCredentials{ + ExternalID: "test-external-id", + }, + wantErr: "role_arn must be specified when external_id is set", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.credentials.Validate() + if tt.wantErr != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.wantErr) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestAWSCredentials_IsConfigured(t *testing.T) { + tests := []struct { + name string + credentials AWSCredentials + want bool + }{ + { + name: "fully configured", + credentials: AWSCredentials{ + RoleARN: "arn:aws:iam::123456789012:role/ElasticAgentRole", + ExternalID: "test-external-id", + }, + want: true, + }, + { + name: "use_default_credentials", + credentials: AWSCredentials{ + UseDefaultCredentials: true, + }, + want: true, + }, + { + name: "missing external_id", + credentials: AWSCredentials{ + RoleARN: "arn:aws:iam::123456789012:role/ElasticAgentRole", + }, + want: false, + }, + { + name: "missing role_arn", + credentials: AWSCredentials{ + ExternalID: "test-external-id", + }, + want: false, + }, + { + name: "empty", + credentials: AWSCredentials{}, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, tt.credentials.IsConfigured()) + }) + } +} + +func TestGetProviderForIntegration(t *testing.T) { + tests := []struct { + name string + integrationType string + want verifier.ProviderType + }{ + { + name: "AWS CloudTrail", + integrationType: "aws_cloudtrail", + want: verifier.ProviderAWS, + }, + { + name: "AWS GuardDuty", + integrationType: "aws_guardduty", + want: verifier.ProviderAWS, + }, + { + name: "Azure Activity Logs", + integrationType: "azure_activitylogs", + want: verifier.ProviderAzure, + }, + { + name: "GCP Audit", + integrationType: "gcp_audit", + want: verifier.ProviderGCP, + }, + { + name: "Okta System", + integrationType: "okta_system", + want: verifier.ProviderOkta, + }, + { + name: "Unknown", + integrationType: "unknown_integration", + want: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := GetProviderForIntegration(tt.integrationType) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/receiver/verifierreceiver/doc.go b/receiver/verifierreceiver/doc.go new file mode 100644 index 000000000..f86db6aff --- /dev/null +++ b/receiver/verifierreceiver/doc.go @@ -0,0 +1,69 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +//go:generate mdatagen metadata.yaml + +// Package verifierreceiver provides an OTEL receiver that verifies +// permissions for cloud integrations and reports the results as OTEL logs. +// +// # Overview +// +// The receiver uses a registry-based architecture to support multiple +// cloud and identity providers: +// - AWS (active): CloudTrail, GuardDuty, Security Hub, S3, EC2, etc. +// - Azure (planned): Activity Logs, Audit Logs, Blob Storage +// - GCP (planned): Audit Logs, Cloud Storage, Pub/Sub +// - Okta (planned): System Logs, User Events +// +// # Architecture +// +// The receiver consists of two main registries: +// - Permission Registry: Maps integration types to required permissions +// - Verifier Registry: Manages provider-specific verifiers (AWS, Azure, etc.) +// +// Each verifier implements the Verifier interface and is responsible for +// making API calls to verify that permissions are granted. +// +// # Configuration +// +// The receiver is configured with: +// - Cloud Connector identification (ID, name) +// - Verification session (ID, type) +// - Provider credentials (AWS, Azure, GCP, Okta) +// - Policies containing integrations to verify +// +// Example: +// +// receivers: +// verifier: +// cloud_connector_id: "cc-12345" +// verification_id: "verify-001" +// providers: +// aws: +// credentials: +// role_arn: "arn:aws:iam::123456789012:role/Role" +// external_id: "external-id" +// policies: +// - policy_id: "policy-1" +// integrations: +// - integration_type: "aws_cloudtrail" +// +// # Output +// +// The receiver emits OTEL logs with structured attributes following the +// RFC specification for Cloud Connector Permission Verification. +package verifierreceiver // import "github.com/elastic/opentelemetry-collector-components/receiver/verifierreceiver" diff --git a/receiver/verifierreceiver/factory.go b/receiver/verifierreceiver/factory.go new file mode 100644 index 000000000..0c953e207 --- /dev/null +++ b/receiver/verifierreceiver/factory.go @@ -0,0 +1,62 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package verifierreceiver // import "github.com/elastic/opentelemetry-collector-components/receiver/verifierreceiver" + +import ( + "context" + + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/consumer" + "go.opentelemetry.io/collector/receiver" + + "github.com/elastic/opentelemetry-collector-components/receiver/verifierreceiver/internal/metadata" +) + +// NewFactory creates a new factory for the verifier receiver. +// The verifier receiver supports multiple cloud providers (AWS, Azure, GCP, Okta) +// and verifies permissions for configured integrations. +func NewFactory() receiver.Factory { + return receiver.NewFactory( + metadata.Type, + createDefaultConfig, + receiver.WithLogs(createLogsReceiver, metadata.LogsStability), + ) +} + +// createDefaultConfig creates the default configuration for the receiver. +// Provider credentials are optional and can be configured per-provider. +func createDefaultConfig() component.Config { + return &Config{ + VerificationType: "on_demand", + Providers: ProvidersConfig{}, + Policies: []PolicyConfig{}, + } +} + +// createLogsReceiver creates a new logs receiver instance. +// The receiver initializes verifiers for configured providers and +// verifies permissions based on the configured policies. +func createLogsReceiver( + _ context.Context, + params receiver.Settings, + cfg component.Config, + consumer consumer.Logs, +) (receiver.Logs, error) { + config := cfg.(*Config) + return newVerifierReceiver(params, config, consumer), nil +} diff --git a/receiver/verifierreceiver/factory_test.go b/receiver/verifierreceiver/factory_test.go new file mode 100644 index 000000000..e22d5c974 --- /dev/null +++ b/receiver/verifierreceiver/factory_test.go @@ -0,0 +1,94 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package verifierreceiver + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/component/componenttest" + "go.opentelemetry.io/collector/consumer/consumertest" + "go.opentelemetry.io/collector/receiver/receivertest" + + "github.com/elastic/opentelemetry-collector-components/receiver/verifierreceiver/internal/metadata" +) + +func TestNewFactory(t *testing.T) { + factory := NewFactory() + require.NotNil(t, factory) + + assert.Equal(t, "verifier", factory.Type().String()) +} + +func TestCreateDefaultConfig(t *testing.T) { + factory := NewFactory() + cfg := factory.CreateDefaultConfig() + + require.NotNil(t, cfg) + config, ok := cfg.(*Config) + require.True(t, ok) + + assert.Empty(t, config.Policies) + assert.Equal(t, "on_demand", config.VerificationType) +} + +func TestCreateLogsReceiver(t *testing.T) { + factory := NewFactory() + cfg := factory.CreateDefaultConfig().(*Config) + + // Set up required configuration per RFC structure + cfg.CloudConnectorID = "cc-test-001" + cfg.VerificationID = "verify-test-001" + cfg.Policies = []PolicyConfig{ + { + PolicyID: "policy-1", + PolicyName: "Test Policy", + Integrations: []IntegrationConfig{ + { + IntegrationID: "int-001", + IntegrationType: "aws_cloudtrail", + IntegrationName: "AWS CloudTrail", + Config: map[string]interface{}{ + "account_id": "123456789012", + "region": "us-east-1", + }, + }, + }, + }, + } + + consumer := consumertest.NewNop() + receiver, err := factory.CreateLogs( + context.Background(), + receivertest.NewNopSettings(metadata.Type), + cfg, + consumer, + ) + + require.NoError(t, err) + require.NotNil(t, receiver) + + // Verify it can start and shutdown + err = receiver.Start(context.Background(), componenttest.NewNopHost()) + require.NoError(t, err) + + err = receiver.Shutdown(context.Background()) + require.NoError(t, err) +} diff --git a/receiver/verifierreceiver/generated_package_test.go b/receiver/verifierreceiver/generated_package_test.go new file mode 100644 index 000000000..8fceb3e8f --- /dev/null +++ b/receiver/verifierreceiver/generated_package_test.go @@ -0,0 +1,35 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package verifierreceiver + +import ( + "testing" + + "go.uber.org/goleak" +) + +func TestMain(m *testing.M) { + // Ignore goroutines from AWS SDK HTTP client connections + goleak.VerifyTestMain(m, + goleak.IgnoreTopFunction("net/http.(*persistConn).writeLoop"), + goleak.IgnoreTopFunction("net/http.(*persistConn).readLoop"), + goleak.IgnoreTopFunction("net/http.(*persistConn).addTLS"), + goleak.IgnoreTopFunction("net/http.(*persistConn).roundTrip"), + goleak.IgnoreTopFunction("internal/poll.runtime_pollWait"), + ) +} diff --git a/receiver/verifierreceiver/go.mod b/receiver/verifierreceiver/go.mod new file mode 100644 index 000000000..1d0a5d63e --- /dev/null +++ b/receiver/verifierreceiver/go.mod @@ -0,0 +1,77 @@ +module github.com/elastic/opentelemetry-collector-components/receiver/verifierreceiver + +go 1.24.0 + +require ( + github.com/aws/aws-sdk-go-v2 v1.31.0 + github.com/aws/aws-sdk-go-v2/config v1.27.33 + github.com/aws/aws-sdk-go-v2/credentials v1.17.32 + github.com/aws/aws-sdk-go-v2/service/cloudtrail v1.43.2 + github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.40.7 + github.com/aws/aws-sdk-go-v2/service/ec2 v1.177.2 + github.com/aws/aws-sdk-go-v2/service/guardduty v1.48.2 + github.com/aws/aws-sdk-go-v2/service/s3 v1.61.2 + github.com/aws/aws-sdk-go-v2/service/securityhub v1.52.2 + github.com/aws/aws-sdk-go-v2/service/sqs v1.34.7 + github.com/aws/aws-sdk-go-v2/service/sts v1.30.7 + github.com/aws/smithy-go v1.21.0 + github.com/stretchr/testify v1.11.1 + go.opentelemetry.io/collector/component v1.44.0 + go.opentelemetry.io/collector/component/componenttest v0.137.0 + go.opentelemetry.io/collector/consumer v1.44.0 + go.opentelemetry.io/collector/consumer/consumertest v0.138.0 + go.opentelemetry.io/collector/pdata v1.44.0 + go.opentelemetry.io/collector/receiver v1.44.0 + go.opentelemetry.io/collector/receiver/receivertest v0.137.0 + go.uber.org/goleak v1.3.0 + go.uber.org/zap v1.27.0 +) + +require ( + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.4 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.18 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.18 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.17 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.19 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.17 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.22.7 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.7 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/hashicorp/go-version v1.7.0 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/collector/consumer/consumererror v0.137.0 // indirect + go.opentelemetry.io/collector/consumer/xconsumer v0.138.0 // indirect + go.opentelemetry.io/collector/featuregate v1.44.0 // indirect + go.opentelemetry.io/collector/internal/telemetry v0.138.0 // indirect + go.opentelemetry.io/collector/pdata/pprofile v0.138.0 // indirect + go.opentelemetry.io/collector/pipeline v1.44.0 // indirect + go.opentelemetry.io/collector/receiver/xreceiver v0.137.0 // indirect + go.opentelemetry.io/contrib/bridges/otelzap v0.13.0 // indirect + go.opentelemetry.io/otel v1.38.0 // indirect + go.opentelemetry.io/otel/log v0.14.0 // indirect + go.opentelemetry.io/otel/metric v1.38.0 // indirect + go.opentelemetry.io/otel/sdk v1.38.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect + go.opentelemetry.io/otel/trace v1.38.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/net v0.42.0 // indirect + golang.org/x/sys v0.35.0 // indirect + golang.org/x/text v0.27.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect + google.golang.org/grpc v1.76.0 // indirect + google.golang.org/protobuf v1.36.10 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/receiver/verifierreceiver/go.sum b/receiver/verifierreceiver/go.sum new file mode 100644 index 000000000..65472ed7a --- /dev/null +++ b/receiver/verifierreceiver/go.sum @@ -0,0 +1,201 @@ +github.com/aws/aws-sdk-go-v2 v1.31.0 h1:3V05LbxTSItI5kUqNwhJrrrY1BAXxXt0sN0l72QmG5U= +github.com/aws/aws-sdk-go-v2 v1.31.0/go.mod h1:ztolYtaEUtdpf9Wftr31CJfLVjOnD/CVRkKOOYgF8hA= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.4 h1:70PVAiL15/aBMh5LThwgXdSQorVr91L127ttckI9QQU= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.4/go.mod h1:/MQxMqci8tlqDH+pjmoLu1i0tbWCUP1hhyMRuFxpQCw= +github.com/aws/aws-sdk-go-v2/config v1.27.33 h1:Nof9o/MsmH4oa0s2q9a0k7tMz5x/Yj5k06lDODWz3BU= +github.com/aws/aws-sdk-go-v2/config v1.27.33/go.mod h1:kEqdYzRb8dd8Sy2pOdEbExTTF5v7ozEXX0McgPE7xks= +github.com/aws/aws-sdk-go-v2/credentials v1.17.32 h1:7Cxhp/BnT2RcGy4VisJ9miUPecY+lyE9I8JvcZofn9I= +github.com/aws/aws-sdk-go-v2/credentials v1.17.32/go.mod h1:P5/QMF3/DCHbXGEGkdbilXHsyTBX5D3HSwcrSc9p20I= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13 h1:pfQ2sqNpMVK6xz2RbqLEL0GH87JOwSxPV2rzm8Zsb74= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13/go.mod h1:NG7RXPUlqfsCLLFfi0+IpKN4sCB9D9fw/qTaSB+xRoU= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.18 h1:kYQ3H1u0ANr9KEKlGs/jTLrBFPo8P8NaH/w7A01NeeM= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.18/go.mod h1:r506HmK5JDUh9+Mw4CfGJGSSoqIiLCndAuqXuhbv67Y= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.18 h1:Z7IdFUONvTcvS7YuhtVxN99v2cCoHRXOS4mTr0B/pUc= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.18/go.mod h1:DkKMmksZVVyat+Y+r1dEOgJEfUeA7UngIHWeKsi0yNc= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.17 h1:Roo69qTpfu8OlJ2Tb7pAYVuF0CpuUMB0IYWwYP/4DZM= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.17/go.mod h1:NcWPxQzGM1USQggaTVwz6VpqMZPX1CvDJLDh6jnOCa4= +github.com/aws/aws-sdk-go-v2/service/cloudtrail v1.43.2 h1:sLoUkwhrhogwbnQ2/nsc1MT3dia7krZHHwCMbFyYGbo= +github.com/aws/aws-sdk-go-v2/service/cloudtrail v1.43.2/go.mod h1:ODEcuhq+MDaWP9fpgCPcYMKE12pyK5g5W2U0z0nHEiI= +github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.40.7 h1:G8JC8KCrNiQiyK61CYyzRDixCb+XNktVcaQzlG95yJI= +github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.40.7/go.mod h1:HeDvLYJALo05N6wCx3Ufa1rHGL1mz9ON312O2yVclIs= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.177.2 h1:QUUvxEs9q1DsYCaWaRrV8i7n82Adm34jrHb6OPjXPqc= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.177.2/go.mod h1:TFSALWR7Xs7+KyMM87ZAYxncKFBvzEt2rpK/BJCH2ps= +github.com/aws/aws-sdk-go-v2/service/guardduty v1.48.2 h1:F7iPMAIiEX5xqUEhbeflkREaforxmuIkobZi9apGFKc= +github.com/aws/aws-sdk-go-v2/service/guardduty v1.48.2/go.mod h1:yL5DOvh8huFx2ZwB9kj20TnZ5DQJjnoCYUkFitas/2k= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 h1:KypMCbLPPHEmf9DgMGw51jMj77VfGPAN2Kv4cfhlfgI= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4/go.mod h1:Vz1JQXliGcQktFTN/LN6uGppAIRoLBR2bMvIMP0gOjc= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.19 h1:FLMkfEiRjhgeDTCjjLoc3URo/TBkgeQbocA78lfkzSI= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.19/go.mod h1:Vx+GucNSsdhaxs3aZIKfSUjKVGsxN25nX2SRcdhuw08= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19 h1:rfprUlsdzgl7ZL2KlXiUAoJnI/VxfHCvDFr2QDFj6u4= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19/go.mod h1:SCWkEdRq8/7EK60NcvvQ6NXKuTcchAD4ROAsC37VEZE= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.17 h1:u+EfGmksnJc/x5tq3A+OD7LrMbSSR/5TrKLvkdy/fhY= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.17/go.mod h1:VaMx6302JHax2vHJWgRo+5n9zvbacs3bLU/23DNQrTY= +github.com/aws/aws-sdk-go-v2/service/s3 v1.61.2 h1:Kp6PWAlXwP1UvIflkIP6MFZYBNDCa4mFCGtxrpICVOg= +github.com/aws/aws-sdk-go-v2/service/s3 v1.61.2/go.mod h1:5FmD/Dqq57gP+XwaUnd5WFPipAuzrf0HmupX27Gvjvc= +github.com/aws/aws-sdk-go-v2/service/securityhub v1.52.2 h1:sO8Z9YGxpvPtXsVF0UBBgNOMeEZq2H/GRBdZxTBfEbE= +github.com/aws/aws-sdk-go-v2/service/securityhub v1.52.2/go.mod h1:TccpGcVXrED4xcLhtYFs5qHJEzL8qXCCoQj+TDosCxQ= +github.com/aws/aws-sdk-go-v2/service/sqs v1.34.7 h1:RxETYGXhRlRxL96mtab1lQ9fPVPIJFXuOI3uRL/MuHI= +github.com/aws/aws-sdk-go-v2/service/sqs v1.34.7/go.mod h1:zn0Oy7oNni7XIGoAd6bHBTVtX06OrnpvT1kww8jxyi8= +github.com/aws/aws-sdk-go-v2/service/sso v1.22.7 h1:pIaGg+08llrP7Q5aiz9ICWbY8cqhTkyy+0SHvfzQpTc= +github.com/aws/aws-sdk-go-v2/service/sso v1.22.7/go.mod h1:eEygMHnTKH/3kNp9Jr1n3PdejuSNcgwLe1dWgQtO0VQ= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.7 h1:/Cfdu0XV3mONYKaOt1Gr0k1KvQzkzPyiKUdlWJqy+J4= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.7/go.mod h1:bCbAxKDqNvkHxRaIMnyVPXPo+OaPRwvmgzMxbz1VKSA= +github.com/aws/aws-sdk-go-v2/service/sts v1.30.7 h1:NKTa1eqZYw8tiHSRGpP0VtTdub/8KNk8sDkNPFaOKDE= +github.com/aws/aws-sdk-go-v2/service/sts v1.30.7/go.mod h1:NXi1dIAGteSaRLqYgarlhP/Ij0cFT+qmCwiJqWh/U5o= +github.com/aws/smithy-go v1.21.0 h1:H7L8dtDRk0P1Qm6y0ji7MCYMQObJ5R9CRpyPhRUkLYA= +github.com/aws/smithy-go v1.21.0/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= +github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/collector/component v1.44.0 h1:SX5UO/gSDm+1zyvHVRFgpf8J1WP6U3y/SLUXiVEghbE= +go.opentelemetry.io/collector/component v1.44.0/go.mod h1:geKbCTNoQfu55tOPiDuxLzNZsoO9//HRRg10/8WusWk= +go.opentelemetry.io/collector/component/componenttest v0.137.0 h1:QC9MZsYyzQqN9qMlleJb78wf7FeCjbr4jLeCuNlKHLU= +go.opentelemetry.io/collector/component/componenttest v0.137.0/go.mod h1:JuiX9pv7qE5G8keihhjM66LeidryEnziPND0sXuK9PQ= +go.opentelemetry.io/collector/consumer v1.44.0 h1:vkKJTfQYBQNuKas0P1zv1zxJjHvmMa/n7d6GiSHT0aw= +go.opentelemetry.io/collector/consumer v1.44.0/go.mod h1:t6u5+0FBUtyZLVFhVPgFabd4Iph7rP+b9VkxaY8dqXU= +go.opentelemetry.io/collector/consumer/consumererror v0.137.0 h1:4HgYX6vVmaF17RRRtJDpR8EuWmLAv6JdKYG8slDDa+g= +go.opentelemetry.io/collector/consumer/consumererror v0.137.0/go.mod h1:muYN3UZ/43YHpDpQRVvCj0Rhpt/YjoPAF/BO63cPSwk= +go.opentelemetry.io/collector/consumer/consumertest v0.138.0 h1:1PwWhjQ3msYhcml/YeeSegjUAVC4nlA8+LY5uKqJbHk= +go.opentelemetry.io/collector/consumer/consumertest v0.138.0/go.mod h1:2XBKvZKVcF/7ts1Y+PxTgrQiBhXAnzMfT+1VKtzoDpQ= +go.opentelemetry.io/collector/consumer/xconsumer v0.138.0 h1:peQ59TyBmt30lv4YH8gfBbTSJPuPIZW0kpFTfk45rVk= +go.opentelemetry.io/collector/consumer/xconsumer v0.138.0/go.mod h1:ivpzDlwQowx8RTOZBPa281/4NvNBvhabm7JmeAbsGIU= +go.opentelemetry.io/collector/featuregate v1.44.0 h1:/GeGhTD8f+FNWS7C4w1Dj0Ui9Jp4v2WAdlXyW1p3uG8= +go.opentelemetry.io/collector/featuregate v1.44.0/go.mod h1:d0tiRzVYrytB6LkcYgz2ESFTv7OktRPQe0QEQcPt1L4= +go.opentelemetry.io/collector/internal/telemetry v0.138.0 h1:xHHYlPh1vVvr+ip0ct288l1joc4bsEeHh0rcY3WVXJo= +go.opentelemetry.io/collector/internal/telemetry v0.138.0/go.mod h1:evqf71fdIMXdQEofbs1bVnBUzfF6zysLMLR9bEAS9Xw= +go.opentelemetry.io/collector/pdata v1.44.0 h1:q/EfWDDKrSaf4hjTIzyPeg1ZcCRg1Uj7VTFnGfNVdk8= +go.opentelemetry.io/collector/pdata v1.44.0/go.mod h1:LnsjYysFc3AwMVh6KGNlkGKJUF2ReuWxtD9Hb3lSMZk= +go.opentelemetry.io/collector/pdata/pprofile v0.138.0 h1:ElnIPJK8jVzHYSnzbIVjg/v2Yq8iVLUKf7kB00zUFlE= +go.opentelemetry.io/collector/pdata/pprofile v0.138.0/go.mod h1:M7/5+Q4LohEkEB38kHhFu3S3XCA1eGSGz5uSXvNyMlM= +go.opentelemetry.io/collector/pdata/testdata v0.138.0 h1:6geeGQ4Rsb88OARLcACKn09PVIbhExaNJ1aC9OVLZaw= +go.opentelemetry.io/collector/pdata/testdata v0.138.0/go.mod h1:4wvgY+KTP7ohJVd1/pb8UIKb2TA/girsZbGTKqM5e20= +go.opentelemetry.io/collector/pipeline v1.44.0 h1:EFdFBg3Wm2BlMtQbUeork5a4KFpS6haInSr+u/dk8rg= +go.opentelemetry.io/collector/pipeline v1.44.0/go.mod h1:xUrAqiebzYbrgxyoXSkk6/Y3oi5Sy3im2iCA51LwUAI= +go.opentelemetry.io/collector/receiver v1.44.0 h1:oPgHg7u+aqplnVTLyC3FapTsAE7BiGdTtDceE1BuTJg= +go.opentelemetry.io/collector/receiver v1.44.0/go.mod h1:NzkrGOIoWigOG54eF92ZGfJ8oSWhqGHTT0ZCGaH5NMc= +go.opentelemetry.io/collector/receiver/receivertest v0.137.0 h1:LqlFKtThf07dFjYGLMfI2J4aio60S03gocm8CL6jOd4= +go.opentelemetry.io/collector/receiver/receivertest v0.137.0/go.mod h1:bg4wfd9uq3jZfarMcqanHhQDlwbByp3GHCY7I6YO/QY= +go.opentelemetry.io/collector/receiver/xreceiver v0.137.0 h1:30h6o1hI03PSc0upgwWMFRZYaVrqLaruA6r/jI1Kk/4= +go.opentelemetry.io/collector/receiver/xreceiver v0.137.0/go.mod h1:kvydfp3S8PKBVXH5OgPsTSneXQ92HGyi30hSrKy1fe4= +go.opentelemetry.io/contrib/bridges/otelzap v0.13.0 h1:aBKdhLVieqvwWe9A79UHI/0vgp2t/s2euY8X59pGRlw= +go.opentelemetry.io/contrib/bridges/otelzap v0.13.0/go.mod h1:SYqtxLQE7iINgh6WFuVi2AI70148B8EI35DSk0Wr8m4= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/log v0.14.0 h1:2rzJ+pOAZ8qmZ3DDHg73NEKzSZkhkGIua9gXtxNGgrM= +go.opentelemetry.io/otel/log v0.14.0/go.mod h1:5jRG92fEAgx0SU/vFPxmJvhIuDU9E1SUnEQrMlJpOno= +go.opentelemetry.io/otel/log/logtest v0.14.0 h1:BGTqNeluJDK2uIHAY8lRqxjVAYfqgcaTbVk1n3MWe5A= +go.opentelemetry.io/otel/log/logtest v0.14.0/go.mod h1:IuguGt8XVP4XA4d2oEEDMVDBBCesMg8/tSGWDjuKfoA= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/proto/slim/otlp v1.8.0 h1:afcLwp2XOeCbGrjufT1qWyruFt+6C9g5SOuymrSPUXQ= +go.opentelemetry.io/proto/slim/otlp v1.8.0/go.mod h1:Yaa5fjYm1SMCq0hG0x/87wV1MP9H5xDuG/1+AhvBcsI= +go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.1.0 h1:Uc+elixz922LHx5colXGi1ORbsW8DTIGM+gg+D9V7HE= +go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.1.0/go.mod h1:VyU6dTWBWv6h9w/+DYgSZAPMabWbPTFTuxp25sM8+s0= +go.opentelemetry.io/proto/slim/otlp/profiles/v1development v0.1.0 h1:i8YpvWGm/Uq1koL//bnbJ/26eV3OrKWm09+rDYo7keU= +go.opentelemetry.io/proto/slim/otlp/profiles/v1development v0.1.0/go.mod h1:pQ70xHY/ZVxNUBPn+qUWPl8nwai87eWdqL3M37lNi9A= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= +golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= +golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b h1:zPKJod4w6F1+nRGDI9ubnXYhU9NSWoFAijkHkUXeTK8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= +google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/receiver/verifierreceiver/internal/metadata/generated_status.go b/receiver/verifierreceiver/internal/metadata/generated_status.go new file mode 100644 index 000000000..f5cd7d1b0 --- /dev/null +++ b/receiver/verifierreceiver/internal/metadata/generated_status.go @@ -0,0 +1,33 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +// Code generated by mdatagen. DO NOT EDIT. + +package metadata + +import ( + "go.opentelemetry.io/collector/component" +) + +var ( + Type = component.MustNewType("verifier") + ScopeName = "github.com/elastic/opentelemetry-collector-components/receiver/verifierreceiver" +) + +const ( + LogsStability = component.StabilityLevelDevelopment +) diff --git a/receiver/verifierreceiver/internal/verifier/aws_verifier.go b/receiver/verifierreceiver/internal/verifier/aws_verifier.go new file mode 100644 index 000000000..de5c33a55 --- /dev/null +++ b/receiver/verifierreceiver/internal/verifier/aws_verifier.go @@ -0,0 +1,580 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package verifier + +import ( + "context" + "errors" + "strings" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/credentials/stscreds" + "github.com/aws/aws-sdk-go-v2/service/cloudtrail" + "github.com/aws/aws-sdk-go-v2/service/cloudwatch" + "github.com/aws/aws-sdk-go-v2/service/ec2" + "github.com/aws/aws-sdk-go-v2/service/guardduty" + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/aws/aws-sdk-go-v2/service/securityhub" + "github.com/aws/aws-sdk-go-v2/service/sqs" + "github.com/aws/aws-sdk-go-v2/service/sts" + "github.com/aws/smithy-go" + "go.uber.org/zap" +) + +const ( + defaultSessionName = "verifier-receiver" + defaultAssumeRoleDuration = 15 * time.Minute +) + +// AWSVerifier implements permission verification for AWS. +type AWSVerifier struct { + logger *zap.Logger + baseConfig aws.Config + configured bool + authConfig AWSAuthConfig + defaultRegion string +} + +// Ensure AWSVerifier implements Verifier interface. +var _ Verifier = (*AWSVerifier)(nil) + +// NewAWSVerifierFactory returns a factory function for creating AWS verifiers. +// This factory should be registered with the verifier Registry. +func NewAWSVerifierFactory() VerifierFactory { + return func(ctx context.Context, logger *zap.Logger, authConfig AuthConfig) (Verifier, error) { + awsConfig, ok := authConfig.(AWSAuthConfig) + if !ok { + return nil, errors.New("invalid auth config type for AWS verifier") + } + return NewAWSVerifier(ctx, logger, awsConfig) + } +} + +// NewAWSVerifier creates a new AWS verifier with Cloud Connector authentication. +// It uses STS AssumeRole with the provided role ARN and external ID. +func NewAWSVerifier(ctx context.Context, logger *zap.Logger, authConfig AWSAuthConfig) (*AWSVerifier, error) { + // Start with loading default config (for base credentials from IRSA, instance profile, etc.) + baseCfg, err := config.LoadDefaultConfig(ctx) + if err != nil { + logger.Warn("Failed to load default AWS config", zap.Error(err)) + return &AWSVerifier{ + logger: logger, + configured: false, + }, nil + } + + // Set default region if specified + if authConfig.DefaultRegion != "" { + baseCfg.Region = authConfig.DefaultRegion + } + + // If role ARN is provided, configure STS AssumeRole with external ID + if authConfig.RoleARN != "" { + logger.Info("Configuring AWS STS AssumeRole", + zap.String("role_arn", authConfig.RoleARN), + zap.Bool("has_external_id", authConfig.ExternalID != ""), + ) + + // Create STS client using base credentials + stsClient := sts.NewFromConfig(baseCfg) + + // Configure assume role options + sessionName := authConfig.SessionName + if sessionName == "" { + sessionName = defaultSessionName + } + + duration := authConfig.AssumeRoleDuration + if duration == 0 { + duration = defaultAssumeRoleDuration + } + + // Create assume role provider with external ID + assumeRoleProvider := stscreds.NewAssumeRoleProvider(stsClient, authConfig.RoleARN, + func(options *stscreds.AssumeRoleOptions) { + options.RoleSessionName = sessionName + options.Duration = duration + if authConfig.ExternalID != "" { + options.ExternalID = aws.String(authConfig.ExternalID) + } + }, + ) + + // Wrap with credentials cache for automatic refresh + baseCfg.Credentials = aws.NewCredentialsCache(assumeRoleProvider) + + logger.Info("AWS STS AssumeRole configured successfully", + zap.String("session_name", sessionName), + zap.Duration("duration", duration), + ) + } else { + logger.Info("Using default AWS credentials (no role assumption)") + } + + return &AWSVerifier{ + logger: logger, + baseConfig: baseCfg, + configured: true, + authConfig: authConfig, + defaultRegion: authConfig.DefaultRegion, + }, nil +} + +// ProviderType returns the provider type. +func (v *AWSVerifier) ProviderType() ProviderType { + return ProviderAWS +} + +// Close releases resources. +func (v *AWSVerifier) Close() error { + return nil +} + +// Verify checks if an AWS permission is granted. +func (v *AWSVerifier) Verify(ctx context.Context, permission Permission, providerCfg ProviderConfig) Result { + start := time.Now() + + if !v.configured { + return Result{ + Status: StatusError, + ErrorCode: "ConfigurationError", + ErrorMessage: "AWS credentials not configured", + Duration: time.Since(start), + } + } + + // Create region-specific config + cfg := v.baseConfig.Copy() + if providerCfg.Region != "" { + cfg.Region = providerCfg.Region + } + + // Parse the action to determine service and operation + parts := strings.SplitN(permission.Action, ":", 2) + if len(parts) != 2 { + return Result{ + Status: StatusError, + ErrorCode: "InvalidAction", + ErrorMessage: "Invalid action format: " + permission.Action, + Duration: time.Since(start), + } + } + + service := strings.ToLower(parts[0]) + operation := parts[1] + + v.logger.Debug("Verifying AWS permission", + zap.String("service", service), + zap.String("operation", operation), + zap.String("region", cfg.Region), + zap.String("method", string(permission.Method)), + ) + + var result Result + switch service { + case "cloudtrail": + result = v.verifyCloudTrail(ctx, cfg, operation) + case "guardduty": + result = v.verifyGuardDuty(ctx, cfg, operation) + case "securityhub": + result = v.verifySecurityHub(ctx, cfg, operation) + case "s3": + result = v.verifyS3(ctx, cfg, operation) + case "ec2": + result = v.verifyEC2(ctx, cfg, operation, permission.Method) + case "cloudwatch": + result = v.verifyCloudWatch(ctx, cfg, operation) + case "sqs": + result = v.verifySQS(ctx, cfg, operation) + case "logs": + result = v.verifyCloudWatchLogs(ctx, cfg, operation) + default: + result = Result{ + Status: StatusSkipped, + ErrorMessage: "Unsupported AWS service: " + service, + } + } + + result.Duration = time.Since(start) + return result +} + +// verifyCloudTrail verifies CloudTrail permissions. +func (v *AWSVerifier) verifyCloudTrail(ctx context.Context, cfg aws.Config, operation string) Result { + client := cloudtrail.NewFromConfig(cfg) + + switch operation { + case "LookupEvents": + // Make a minimal API call to check permission + _, err := client.LookupEvents(ctx, &cloudtrail.LookupEventsInput{ + MaxResults: aws.Int32(1), + }) + return v.handleAWSError(err, "cloudtrail:LookupEvents") + + case "DescribeTrails": + _, err := client.DescribeTrails(ctx, &cloudtrail.DescribeTrailsInput{}) + return v.handleAWSError(err, "cloudtrail:DescribeTrails") + + case "GetTrailStatus": + // Need a trail name - try listing first + trails, err := client.DescribeTrails(ctx, &cloudtrail.DescribeTrailsInput{}) + if err != nil { + return v.handleAWSError(err, "cloudtrail:GetTrailStatus") + } + if len(trails.TrailList) == 0 { + return Result{ + Status: StatusGranted, + Endpoint: "cloudtrail:GetTrailStatus (no trails to check)", + } + } + _, err = client.GetTrailStatus(ctx, &cloudtrail.GetTrailStatusInput{ + Name: trails.TrailList[0].Name, + }) + return v.handleAWSError(err, "cloudtrail:GetTrailStatus") + + default: + return Result{ + Status: StatusSkipped, + ErrorMessage: "Unsupported CloudTrail operation: " + operation, + } + } +} + +// verifyGuardDuty verifies GuardDuty permissions. +func (v *AWSVerifier) verifyGuardDuty(ctx context.Context, cfg aws.Config, operation string) Result { + client := guardduty.NewFromConfig(cfg) + + switch operation { + case "ListDetectors": + _, err := client.ListDetectors(ctx, &guardduty.ListDetectorsInput{ + MaxResults: aws.Int32(1), + }) + return v.handleAWSError(err, "guardduty:ListDetectors") + + case "GetFindings", "ListFindings": + // First get a detector ID + detectors, err := client.ListDetectors(ctx, &guardduty.ListDetectorsInput{ + MaxResults: aws.Int32(1), + }) + if err != nil { + return v.handleAWSError(err, "guardduty:"+operation) + } + if len(detectors.DetectorIds) == 0 { + return Result{ + Status: StatusGranted, + Endpoint: "guardduty:" + operation + " (no detectors configured)", + } + } + + if operation == "ListFindings" { + _, err = client.ListFindings(ctx, &guardduty.ListFindingsInput{ + DetectorId: aws.String(detectors.DetectorIds[0]), + MaxResults: aws.Int32(1), + }) + } else { + // GetFindings requires finding IDs, so we'll use ListFindings as proxy + _, err = client.ListFindings(ctx, &guardduty.ListFindingsInput{ + DetectorId: aws.String(detectors.DetectorIds[0]), + MaxResults: aws.Int32(1), + }) + } + return v.handleAWSError(err, "guardduty:"+operation) + + default: + return Result{ + Status: StatusSkipped, + ErrorMessage: "Unsupported GuardDuty operation: " + operation, + } + } +} + +// verifySecurityHub verifies Security Hub permissions. +func (v *AWSVerifier) verifySecurityHub(ctx context.Context, cfg aws.Config, operation string) Result { + client := securityhub.NewFromConfig(cfg) + + switch operation { + case "GetFindings": + _, err := client.GetFindings(ctx, &securityhub.GetFindingsInput{ + MaxResults: aws.Int32(1), + }) + return v.handleAWSError(err, "securityhub:GetFindings") + + case "DescribeHub": + _, err := client.DescribeHub(ctx, &securityhub.DescribeHubInput{}) + return v.handleAWSError(err, "securityhub:DescribeHub") + + case "BatchGetSecurityControls": + // This requires control IDs, skip if we don't have them + return Result{ + Status: StatusSkipped, + Endpoint: "securityhub:BatchGetSecurityControls (requires control IDs)", + } + + default: + return Result{ + Status: StatusSkipped, + ErrorMessage: "Unsupported Security Hub operation: " + operation, + } + } +} + +// verifyS3 verifies S3 permissions. +func (v *AWSVerifier) verifyS3(ctx context.Context, cfg aws.Config, operation string) Result { + client := s3.NewFromConfig(cfg) + + switch operation { + case "ListBucket": + // ListBuckets is a simpler permission check + _, err := client.ListBuckets(ctx, &s3.ListBucketsInput{}) + return v.handleAWSError(err, "s3:ListBucket") + + case "GetObject": + // GetObject requires a bucket and key - use ListBuckets as proxy + _, err := client.ListBuckets(ctx, &s3.ListBucketsInput{}) + if err != nil { + return v.handleAWSError(err, "s3:GetObject") + } + // If we can list buckets, we have basic S3 access + // The actual GetObject permission is bucket-specific + return Result{ + Status: StatusGranted, + Endpoint: "s3:GetObject (verified via ListBuckets)", + } + + case "GetBucketLocation": + buckets, err := client.ListBuckets(ctx, &s3.ListBucketsInput{}) + if err != nil { + return v.handleAWSError(err, "s3:GetBucketLocation") + } + if len(buckets.Buckets) == 0 { + return Result{ + Status: StatusGranted, + Endpoint: "s3:GetBucketLocation (no buckets to check)", + } + } + _, err = client.GetBucketLocation(ctx, &s3.GetBucketLocationInput{ + Bucket: buckets.Buckets[0].Name, + }) + return v.handleAWSError(err, "s3:GetBucketLocation") + + default: + return Result{ + Status: StatusSkipped, + ErrorMessage: "Unsupported S3 operation: " + operation, + } + } +} + +// verifyEC2 verifies EC2 permissions, using DryRun where appropriate. +func (v *AWSVerifier) verifyEC2(ctx context.Context, cfg aws.Config, operation string, method VerificationMethod) Result { + client := ec2.NewFromConfig(cfg) + + switch operation { + case "DescribeInstances": + if method == MethodDryRun { + // Use DryRun to check permission without actually running + _, err := client.DescribeInstances(ctx, &ec2.DescribeInstancesInput{ + DryRun: aws.Bool(true), + MaxResults: aws.Int32(5), + }) + return v.handleEC2DryRunError(err, "ec2:DescribeInstances") + } + _, err := client.DescribeInstances(ctx, &ec2.DescribeInstancesInput{ + MaxResults: aws.Int32(5), + }) + return v.handleAWSError(err, "ec2:DescribeInstances") + + case "DescribeRegions": + _, err := client.DescribeRegions(ctx, &ec2.DescribeRegionsInput{}) + return v.handleAWSError(err, "ec2:DescribeRegions") + + case "DescribeFlowLogs": + _, err := client.DescribeFlowLogs(ctx, &ec2.DescribeFlowLogsInput{ + MaxResults: aws.Int32(5), + }) + return v.handleAWSError(err, "ec2:DescribeFlowLogs") + + default: + return Result{ + Status: StatusSkipped, + ErrorMessage: "Unsupported EC2 operation: " + operation, + } + } +} + +// verifyCloudWatch verifies CloudWatch permissions. +func (v *AWSVerifier) verifyCloudWatch(ctx context.Context, cfg aws.Config, operation string) Result { + client := cloudwatch.NewFromConfig(cfg) + + switch operation { + case "GetMetricData": + // GetMetricData requires metric queries - use ListMetrics as proxy + _, err := client.ListMetrics(ctx, &cloudwatch.ListMetricsInput{}) + return v.handleAWSError(err, "cloudwatch:GetMetricData") + + default: + return Result{ + Status: StatusSkipped, + ErrorMessage: "Unsupported CloudWatch operation: " + operation, + } + } +} + +// verifySQS verifies SQS permissions. +func (v *AWSVerifier) verifySQS(ctx context.Context, cfg aws.Config, operation string) Result { + client := sqs.NewFromConfig(cfg) + + switch operation { + case "ReceiveMessage", "DeleteMessage": + // These require a queue URL - use ListQueues as proxy + _, err := client.ListQueues(ctx, &sqs.ListQueuesInput{ + MaxResults: aws.Int32(1), + }) + return v.handleAWSError(err, "sqs:"+operation) + + default: + return Result{ + Status: StatusSkipped, + ErrorMessage: "Unsupported SQS operation: " + operation, + } + } +} + +// verifyCloudWatchLogs verifies CloudWatch Logs permissions. +func (v *AWSVerifier) verifyCloudWatchLogs(ctx context.Context, cfg aws.Config, operation string) Result { + // CloudWatch Logs uses the same SDK client pattern + // For now, skip - would need to add cloudwatchlogs client + return Result{ + Status: StatusSkipped, + ErrorMessage: "CloudWatch Logs verification not yet implemented", + } +} + +// handleAWSError converts an AWS error to a verification result. +func (v *AWSVerifier) handleAWSError(err error, endpoint string) Result { + if err == nil { + return Result{ + Status: StatusGranted, + Endpoint: endpoint, + } + } + + var apiErr smithy.APIError + if errors.As(err, &apiErr) { + code := apiErr.ErrorCode() + + // Check for access denied errors + if isAccessDeniedError(code) { + return Result{ + Status: StatusDenied, + ErrorCode: code, + ErrorMessage: apiErr.ErrorMessage(), + Endpoint: endpoint, + } + } + + // Other errors are treated as errors, not denials + return Result{ + Status: StatusError, + ErrorCode: code, + ErrorMessage: apiErr.ErrorMessage(), + Endpoint: endpoint, + } + } + + // Non-API errors + return Result{ + Status: StatusError, + ErrorMessage: err.Error(), + Endpoint: endpoint, + } +} + +// handleEC2DryRunError handles EC2 DryRun responses. +// DryRun returns an error even on success - we need to check the error type. +func (v *AWSVerifier) handleEC2DryRunError(err error, endpoint string) Result { + if err == nil { + // Unexpected - DryRun should always return an error + return Result{ + Status: StatusGranted, + Endpoint: endpoint, + } + } + + var apiErr smithy.APIError + if errors.As(err, &apiErr) { + code := apiErr.ErrorCode() + + // DryRunOperation means the permission check passed + if code == "DryRunOperation" { + return Result{ + Status: StatusGranted, + Endpoint: endpoint + " (DryRun)", + } + } + + // UnauthorizedOperation means access denied + if code == "UnauthorizedOperation" || isAccessDeniedError(code) { + return Result{ + Status: StatusDenied, + ErrorCode: code, + ErrorMessage: apiErr.ErrorMessage(), + Endpoint: endpoint + " (DryRun)", + } + } + + // Other errors + return Result{ + Status: StatusError, + ErrorCode: code, + ErrorMessage: apiErr.ErrorMessage(), + Endpoint: endpoint, + } + } + + return Result{ + Status: StatusError, + ErrorMessage: err.Error(), + Endpoint: endpoint, + } +} + +// isAccessDeniedError checks if an error code indicates access denied. +func isAccessDeniedError(code string) bool { + accessDeniedCodes := []string{ + "AccessDenied", + "AccessDeniedException", + "UnauthorizedAccess", + "UnauthorizedOperation", + "AuthorizationError", + "Forbidden", + "InvalidAccessKeyId", + "SignatureDoesNotMatch", + "ExpiredToken", + "ExpiredTokenException", + } + + for _, c := range accessDeniedCodes { + if code == c { + return true + } + } + return false +} diff --git a/receiver/verifierreceiver/internal/verifier/verifier.go b/receiver/verifierreceiver/internal/verifier/verifier.go new file mode 100644 index 000000000..5b5153526 --- /dev/null +++ b/receiver/verifierreceiver/internal/verifier/verifier.go @@ -0,0 +1,338 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +// Package verifier provides permission verification for cloud providers. +// It defines interfaces and types for verifying permissions across different +// cloud providers (AWS, Azure, GCP) and identity providers (Okta, etc.). +package verifier + +import ( + "context" + "fmt" + "sync" + "time" + + "go.uber.org/zap" +) + +// ProviderType represents the type of cloud/identity provider. +type ProviderType string + +const ( + ProviderAWS ProviderType = "aws" + ProviderAzure ProviderType = "azure" + ProviderGCP ProviderType = "gcp" + ProviderOkta ProviderType = "okta" +) + +// Result represents the result of a permission verification. +type Result struct { + // Status is the verification result status. + Status Status + + // ErrorCode is the error code returned by the provider (if any). + ErrorCode string + + // ErrorMessage is the error message returned by the provider (if any). + ErrorMessage string + + // Duration is how long the verification took. + Duration time.Duration + + // Endpoint is the API endpoint that was called (if applicable). + Endpoint string +} + +// Status represents the result status of a permission verification. +type Status string + +const ( + StatusGranted Status = "granted" + StatusDenied Status = "denied" + StatusError Status = "error" + StatusSkipped Status = "skipped" +) + +// VerificationMethod indicates how a permission should be verified. +type VerificationMethod string + +const ( + MethodAPICall VerificationMethod = "api_call" + MethodDryRun VerificationMethod = "dry_run" + MethodHTTPProbe VerificationMethod = "http_probe" +) + +// Permission represents a permission to verify. +type Permission struct { + Action string + Method VerificationMethod + Required bool + Category string +} + +// ProviderConfig contains provider-specific configuration passed during verification. +type ProviderConfig struct { + // AWS configuration + Region string + AccountID string + + // Azure configuration + SubscriptionID string + ResourceGroup string + TenantID string + + // GCP configuration + ProjectID string + + // Okta configuration + OktaDomain string + + // Generic configuration + Endpoint string +} + +// AuthConfig is the interface for provider-specific authentication configuration. +// Each provider implements its own auth config struct. +type AuthConfig interface { + // ProviderType returns the provider type this auth config is for. + ProviderType() ProviderType + // IsConfigured returns true if the auth config has the required fields. + IsConfigured() bool +} + +// AWSAuthConfig contains AWS authentication configuration for Cloud Connector. +// This enables assuming a role in the customer's AWS account using STS AssumeRole. +type AWSAuthConfig struct { + // RoleARN is the ARN of the IAM role to assume in the customer's AWS account. + RoleARN string + + // ExternalID is used to prevent confused deputy attacks. + ExternalID string + + // SessionName is an optional name for the assumed role session. + SessionName string + + // AssumeRoleDuration is the duration for the assumed role credentials. + AssumeRoleDuration time.Duration + + // DefaultRegion is the default AWS region to use for API calls. + DefaultRegion string + + // UseDefaultCredentials uses default AWS credentials (for testing). + UseDefaultCredentials bool +} + +// ProviderType implements AuthConfig. +func (c AWSAuthConfig) ProviderType() ProviderType { return ProviderAWS } + +// IsConfigured implements AuthConfig. +func (c AWSAuthConfig) IsConfigured() bool { + return (c.RoleARN != "" && c.ExternalID != "") || c.UseDefaultCredentials +} + +// AzureAuthConfig contains Azure authentication configuration. +type AzureAuthConfig struct { + // TenantID is the Azure AD tenant ID. + TenantID string + + // ClientID is the Azure AD application (client) ID. + ClientID string + + // ClientSecret is the Azure AD application secret. + ClientSecret string + + // SubscriptionID is the Azure subscription ID. + SubscriptionID string + + // UseManagedIdentity uses Azure managed identity for authentication. + UseManagedIdentity bool +} + +// ProviderType implements AuthConfig. +func (c AzureAuthConfig) ProviderType() ProviderType { return ProviderAzure } + +// IsConfigured implements AuthConfig. +func (c AzureAuthConfig) IsConfigured() bool { + return c.UseManagedIdentity || (c.TenantID != "" && c.ClientID != "" && c.ClientSecret != "") +} + +// GCPAuthConfig contains GCP authentication configuration. +type GCPAuthConfig struct { + // ProjectID is the GCP project ID. + ProjectID string + + // ServiceAccountKey is the JSON key for the service account. + ServiceAccountKey string + + // UseDefaultCredentials uses application default credentials. + UseDefaultCredentials bool + + // ImpersonateServiceAccount is the service account to impersonate. + ImpersonateServiceAccount string +} + +// ProviderType implements AuthConfig. +func (c GCPAuthConfig) ProviderType() ProviderType { return ProviderGCP } + +// IsConfigured implements AuthConfig. +func (c GCPAuthConfig) IsConfigured() bool { + return c.UseDefaultCredentials || c.ServiceAccountKey != "" || c.ImpersonateServiceAccount != "" +} + +// OktaAuthConfig contains Okta authentication configuration. +type OktaAuthConfig struct { + // Domain is the Okta domain (e.g., dev-123456.okta.com). + Domain string + + // APIToken is the Okta API token. + APIToken string + + // ClientID is the OAuth 2.0 client ID (for OAuth authentication). + ClientID string + + // PrivateKey is the private key for OAuth authentication. + PrivateKey string +} + +// ProviderType implements AuthConfig. +func (c OktaAuthConfig) ProviderType() ProviderType { return ProviderOkta } + +// IsConfigured implements AuthConfig. +func (c OktaAuthConfig) IsConfigured() bool { + return c.Domain != "" && (c.APIToken != "" || (c.ClientID != "" && c.PrivateKey != "")) +} + +// Verifier is the interface for permission verifiers. +// Each cloud/identity provider implements this interface. +type Verifier interface { + // Verify checks if a permission is granted. + Verify(ctx context.Context, permission Permission, config ProviderConfig) Result + + // ProviderType returns the provider type this verifier handles. + ProviderType() ProviderType + + // Close releases any resources held by the verifier. + Close() error +} + +// VerifierFactory is a function that creates a new Verifier instance. +type VerifierFactory func(ctx context.Context, logger *zap.Logger, authConfig AuthConfig) (Verifier, error) + +// Registry manages verifier factories and instances. +// It allows registration of new verifier types and creation of verifier instances. +type Registry struct { + mu sync.RWMutex + factories map[ProviderType]VerifierFactory + verifiers map[ProviderType]Verifier + logger *zap.Logger +} + +// NewRegistry creates a new verifier registry. +func NewRegistry(logger *zap.Logger) *Registry { + return &Registry{ + factories: make(map[ProviderType]VerifierFactory), + verifiers: make(map[ProviderType]Verifier), + logger: logger, + } +} + +// RegisterFactory registers a verifier factory for a provider type. +func (r *Registry) RegisterFactory(providerType ProviderType, factory VerifierFactory) { + r.mu.Lock() + defer r.mu.Unlock() + r.factories[providerType] = factory + r.logger.Debug("Registered verifier factory", zap.String("provider", string(providerType))) +} + +// InitializeVerifier creates and stores a verifier for the given provider type and auth config. +func (r *Registry) InitializeVerifier(ctx context.Context, authConfig AuthConfig) error { + r.mu.Lock() + defer r.mu.Unlock() + + providerType := authConfig.ProviderType() + factory, ok := r.factories[providerType] + if !ok { + return fmt.Errorf("no factory registered for provider type: %s", providerType) + } + + verifier, err := factory(ctx, r.logger, authConfig) + if err != nil { + return fmt.Errorf("failed to create verifier for %s: %w", providerType, err) + } + + r.verifiers[providerType] = verifier + r.logger.Info("Initialized verifier", zap.String("provider", string(providerType))) + return nil +} + +// GetVerifier returns the verifier for a provider type, or nil if not initialized. +func (r *Registry) GetVerifier(providerType ProviderType) Verifier { + r.mu.RLock() + defer r.mu.RUnlock() + return r.verifiers[providerType] +} + +// HasVerifier returns true if a verifier is initialized for the provider type. +func (r *Registry) HasVerifier(providerType ProviderType) bool { + r.mu.RLock() + defer r.mu.RUnlock() + _, ok := r.verifiers[providerType] + return ok +} + +// Close closes all initialized verifiers. +func (r *Registry) Close() error { + r.mu.Lock() + defer r.mu.Unlock() + + var errs []error + for providerType, verifier := range r.verifiers { + if err := verifier.Close(); err != nil { + errs = append(errs, fmt.Errorf("failed to close %s verifier: %w", providerType, err)) + } + } + r.verifiers = make(map[ProviderType]Verifier) + + if len(errs) > 0 { + return fmt.Errorf("errors closing verifiers: %v", errs) + } + return nil +} + +// RegisteredProviders returns the list of provider types with registered factories. +func (r *Registry) RegisteredProviders() []ProviderType { + r.mu.RLock() + defer r.mu.RUnlock() + + providers := make([]ProviderType, 0, len(r.factories)) + for p := range r.factories { + providers = append(providers, p) + } + return providers +} + +// InitializedProviders returns the list of provider types with initialized verifiers. +func (r *Registry) InitializedProviders() []ProviderType { + r.mu.RLock() + defer r.mu.RUnlock() + + providers := make([]ProviderType, 0, len(r.verifiers)) + for p := range r.verifiers { + providers = append(providers, p) + } + return providers +} diff --git a/receiver/verifierreceiver/metadata.yaml b/receiver/verifierreceiver/metadata.yaml new file mode 100644 index 000000000..68659bca2 --- /dev/null +++ b/receiver/verifierreceiver/metadata.yaml @@ -0,0 +1,13 @@ +type: verifier +github_project: elastic/opentelemetry-collector-components + +status: + class: receiver + stability: + development: [logs] + distributions: [] + codeowners: + active: [jeniawhite] + +tests: + skip_lifecycle: true # Receiver uses integration finders diff --git a/receiver/verifierreceiver/receiver.go b/receiver/verifierreceiver/receiver.go new file mode 100644 index 000000000..34892b3e8 --- /dev/null +++ b/receiver/verifierreceiver/receiver.go @@ -0,0 +1,524 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package verifierreceiver // import "github.com/elastic/opentelemetry-collector-components/receiver/verifierreceiver" + +import ( + "context" + "fmt" + "sync" + "time" + + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/consumer" + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/plog" + "go.opentelemetry.io/collector/receiver" + "go.uber.org/zap" + + "github.com/elastic/opentelemetry-collector-components/receiver/verifierreceiver/internal/verifier" +) + +const ( + scopeName = "elastic.permission_verification" + scopeVersion = "1.0.0" + serviceName = "permission-verifier" +) + +// verifierReceiver implements the receiver.Logs interface. +// It verifies permissions for cloud integrations and reports results as OTEL logs. +// The receiver owns the mapping between integrations and their required permissions. +type verifierReceiver struct { + params receiver.Settings + config *Config + consumer consumer.Logs + logger *zap.Logger + permissionRegistry *PermissionRegistry + + // Verifier registry manages verifiers for all cloud/identity providers + verifierRegistry *verifier.Registry + + cancelFn context.CancelFunc + wg sync.WaitGroup +} + +// newVerifierReceiver creates a new verifier receiver. +func newVerifierReceiver( + params receiver.Settings, + config *Config, + consumer consumer.Logs, +) *verifierReceiver { + // Create verifier registry and register available factories + verifierRegistry := verifier.NewRegistry(params.Logger) + + // Register AWS verifier factory (always available) + verifierRegistry.RegisterFactory(verifier.ProviderAWS, verifier.NewAWSVerifierFactory()) + + // Future: Register other provider factories here + // verifierRegistry.RegisterFactory(verifier.ProviderAzure, verifier.NewAzureVerifierFactory()) + // verifierRegistry.RegisterFactory(verifier.ProviderGCP, verifier.NewGCPVerifierFactory()) + // verifierRegistry.RegisterFactory(verifier.ProviderOkta, verifier.NewOktaVerifierFactory()) + + return &verifierReceiver{ + params: params, + config: config, + consumer: consumer, + logger: params.Logger, + permissionRegistry: NewPermissionRegistry(), + verifierRegistry: verifierRegistry, + } +} + +// Start begins the permission verification process. +func (r *verifierReceiver) Start(ctx context.Context, _ component.Host) error { + r.logger.Info("Starting verifier receiver", + zap.String("cloud_connector_id", r.config.CloudConnectorID), + zap.String("verification_id", r.config.VerificationID), + zap.Int("policy_count", len(r.config.Policies)), + ) + + // Initialize verifiers for configured providers + r.initializeVerifiers(ctx) + + startCtx, cancelFn := context.WithCancel(ctx) + r.cancelFn = cancelFn + + // Run verification + r.wg.Add(1) + go func() { + defer r.wg.Done() + r.runVerification(startCtx) + }() + + return nil +} + +// initializeVerifiers initializes verifiers for all configured providers. +func (r *verifierReceiver) initializeVerifiers(ctx context.Context) { + // Initialize AWS verifier if configured + if r.config.Providers.AWS.Credentials.IsConfigured() { + creds := r.config.Providers.AWS.Credentials + if creds.UseDefaultCredentials { + r.logger.Info("Initializing AWS verifier with default credentials (AWS_PROFILE or environment)", + zap.String("default_region", creds.DefaultRegion), + ) + } else { + r.logger.Info("Initializing AWS verifier with Cloud Connector authentication", + zap.String("role_arn", creds.RoleARN), + zap.Bool("has_external_id", creds.ExternalID != ""), + zap.String("default_region", creds.DefaultRegion), + ) + } + + if err := r.verifierRegistry.InitializeVerifier(ctx, creds.ToAuthConfig()); err != nil { + r.logger.Warn("Failed to initialize AWS verifier", zap.Error(err)) + } else { + r.logger.Info("AWS verifier initialized successfully") + } + } else { + r.logger.Debug("AWS credentials not configured") + } + + // Initialize Azure verifier if configured + if r.config.Providers.Azure.Credentials.IsConfigured() { + r.logger.Info("Initializing Azure verifier", + zap.String("tenant_id", r.config.Providers.Azure.Credentials.TenantID), + zap.Bool("use_managed_identity", r.config.Providers.Azure.Credentials.UseManagedIdentity), + ) + + if err := r.verifierRegistry.InitializeVerifier(ctx, r.config.Providers.Azure.Credentials.ToAuthConfig()); err != nil { + r.logger.Warn("Failed to initialize Azure verifier", zap.Error(err)) + } else { + r.logger.Info("Azure verifier initialized successfully") + } + } else { + r.logger.Debug("Azure credentials not configured") + } + + // Initialize GCP verifier if configured + if r.config.Providers.GCP.Credentials.IsConfigured() { + r.logger.Info("Initializing GCP verifier", + zap.String("project_id", r.config.Providers.GCP.Credentials.ProjectID), + zap.Bool("use_default_credentials", r.config.Providers.GCP.Credentials.UseDefaultCredentials), + ) + + if err := r.verifierRegistry.InitializeVerifier(ctx, r.config.Providers.GCP.Credentials.ToAuthConfig()); err != nil { + r.logger.Warn("Failed to initialize GCP verifier", zap.Error(err)) + } else { + r.logger.Info("GCP verifier initialized successfully") + } + } else { + r.logger.Debug("GCP credentials not configured") + } + + // Initialize Okta verifier if configured + if r.config.Providers.Okta.Credentials.IsConfigured() { + r.logger.Info("Initializing Okta verifier", + zap.String("domain", r.config.Providers.Okta.Credentials.Domain), + ) + + if err := r.verifierRegistry.InitializeVerifier(ctx, r.config.Providers.Okta.Credentials.ToAuthConfig()); err != nil { + r.logger.Warn("Failed to initialize Okta verifier", zap.Error(err)) + } else { + r.logger.Info("Okta verifier initialized successfully") + } + } else { + r.logger.Debug("Okta credentials not configured") + } + + // Log summary of initialized verifiers + initialized := r.verifierRegistry.InitializedProviders() + if len(initialized) > 0 { + providers := make([]string, len(initialized)) + for i, p := range initialized { + providers[i] = string(p) + } + r.logger.Info("Verifiers initialized", zap.Strings("providers", providers)) + } else { + r.logger.Warn("No verifiers initialized - permission verification will be limited") + } +} + +// Shutdown stops the permission verification process. +func (r *verifierReceiver) Shutdown(ctx context.Context) error { + r.logger.Info("Shutting down verifier receiver") + if r.cancelFn != nil { + r.cancelFn() + } + r.wg.Wait() + + // Close all verifiers + if err := r.verifierRegistry.Close(); err != nil { + r.logger.Warn("Error closing verifiers", zap.Error(err)) + } + + return nil +} + +// runVerification runs the permission verification for all configured policies. +func (r *verifierReceiver) runVerification(ctx context.Context) { + if err := r.verifyPermissions(ctx); err != nil { + r.logger.Error("Failed to verify permissions", zap.Error(err)) + } +} + +// verifyPermissions performs permission verification for all policies and integrations. +// For each integration, it looks up required permissions from the registry and emits +// OTEL log records with structured results. +func (r *verifierReceiver) verifyPermissions(ctx context.Context) error { + r.logger.Info("Starting permission verification", + zap.String("cloud_connector_id", r.config.CloudConnectorID), + zap.String("verification_id", r.config.VerificationID), + zap.Int("policy_count", len(r.config.Policies)), + ) + + now := time.Now() + timestamp := pcommon.NewTimestampFromTime(now) + verificationTimestamp := now.UTC().Format(time.RFC3339) + + logs := plog.NewLogs() + resourceLogs := logs.ResourceLogs().AppendEmpty() + + // Set resource attributes per RFC specification + resource := resourceLogs.Resource() + resource.Attributes().PutStr("cloud_connector.id", r.config.CloudConnectorID) + if r.config.CloudConnectorName != "" { + resource.Attributes().PutStr("cloud_connector.name", r.config.CloudConnectorName) + } + resource.Attributes().PutStr("verification.id", r.config.VerificationID) + resource.Attributes().PutStr("verification.timestamp", verificationTimestamp) + verificationType := r.config.VerificationType + if verificationType == "" { + verificationType = "on_demand" + } + resource.Attributes().PutStr("verification.type", verificationType) + resource.Attributes().PutStr("service.name", serviceName) + resource.Attributes().PutStr("service.version", scopeVersion) + + scopeLogs := resourceLogs.ScopeLogs().AppendEmpty() + scopeLogs.Scope().SetName(scopeName) + scopeLogs.Scope().SetVersion(scopeVersion) + + // Iterate through all policies and their integrations + for _, policy := range r.config.Policies { + r.logger.Debug("Processing policy", + zap.String("policy_id", policy.PolicyID), + zap.String("policy_name", policy.PolicyName), + zap.Int("integration_count", len(policy.Integrations)), + ) + + for _, integration := range policy.Integrations { + r.logger.Debug("Processing integration", + zap.String("integration_id", integration.IntegrationID), + zap.String("integration_type", integration.IntegrationType), + zap.String("integration_name", integration.IntegrationName), + ) + + // Look up required permissions from registry + integrationPerms := r.permissionRegistry.GetPermissions(integration.IntegrationType) + if integrationPerms == nil { + // Unknown integration type - emit a warning log + r.emitUnsupportedIntegrationLog( + scopeLogs, + timestamp, + policy, + integration, + ) + continue + } + + // Verify and emit a log record for each permission + for _, perm := range integrationPerms.Permissions { + result := r.verifyPermission(ctx, integrationPerms.Provider, perm, integration) + r.emitPermissionCheckLog( + scopeLogs, + timestamp, + policy, + integration, + integrationPerms.Provider, + perm, + result, + ) + } + } + } + + // Send logs to the consumer + if scopeLogs.LogRecords().Len() > 0 { + if err := r.consumer.ConsumeLogs(ctx, logs); err != nil { + return fmt.Errorf("failed to consume logs: %w", err) + } + r.logger.Info("Permission verification logs emitted", + zap.Int("log_count", scopeLogs.LogRecords().Len()), + ) + } + + return nil +} + +// verifyPermission verifies a single permission using the appropriate provider verifier. +func (r *verifierReceiver) verifyPermission( + ctx context.Context, + provider verifier.ProviderType, + perm Permission, + integration IntegrationConfig, +) verifier.Result { + // Build provider config from integration config + providerCfg := verifier.ProviderConfig{} + + // AWS-specific config + if region, ok := integration.Config["region"].(string); ok { + providerCfg.Region = region + } + if accountID, ok := integration.Config["account_id"].(string); ok { + providerCfg.AccountID = accountID + } + + // Azure-specific config + if subscriptionID, ok := integration.Config["subscription_id"].(string); ok { + providerCfg.SubscriptionID = subscriptionID + } + if resourceGroup, ok := integration.Config["resource_group"].(string); ok { + providerCfg.ResourceGroup = resourceGroup + } + if tenantID, ok := integration.Config["tenant_id"].(string); ok { + providerCfg.TenantID = tenantID + } + + // GCP-specific config + if projectID, ok := integration.Config["project_id"].(string); ok { + providerCfg.ProjectID = projectID + } + + // Okta-specific config + if domain, ok := integration.Config["domain"].(string); ok { + providerCfg.OktaDomain = domain + } + + // Get the verifier for this provider + v := r.verifierRegistry.GetVerifier(provider) + if v == nil { + return verifier.Result{ + Status: verifier.StatusError, + ErrorCode: "VerifierNotInitialized", + ErrorMessage: fmt.Sprintf("%s verifier not initialized - credentials not configured", provider), + } + } + + return v.Verify(ctx, verifier.Permission{ + Action: perm.Action, + Method: verifier.VerificationMethod(perm.Method), + Required: perm.Required, + Category: perm.Category, + }, providerCfg) +} + +// emitPermissionCheckLog emits a log record for a single permission check. +// The log record follows the RFC structure with all required attributes. +func (r *verifierReceiver) emitPermissionCheckLog( + scopeLogs plog.ScopeLogs, + timestamp pcommon.Timestamp, + policy PolicyConfig, + integration IntegrationConfig, + provider verifier.ProviderType, + perm Permission, + result verifier.Result, +) { + logRecord := scopeLogs.LogRecords().AppendEmpty() + logRecord.SetTimestamp(timestamp) + logRecord.SetObservedTimestamp(timestamp) + + // Determine severity based on verification result + var severityNumber plog.SeverityNumber + var severityText string + var status PermissionStatus + + switch result.Status { + case verifier.StatusGranted: + severityNumber = plog.SeverityNumberInfo + severityText = "INFO" + status = StatusGranted + case verifier.StatusDenied: + if perm.Required { + severityNumber = plog.SeverityNumberError + severityText = "ERROR" + } else { + severityNumber = plog.SeverityNumberWarn + severityText = "WARN" + } + status = StatusDenied + case verifier.StatusError: + severityNumber = plog.SeverityNumberError + severityText = "ERROR" + status = StatusError + case verifier.StatusSkipped: + severityNumber = plog.SeverityNumberInfo + severityText = "INFO" + status = StatusSkipped + default: + severityNumber = plog.SeverityNumberInfo + severityText = "INFO" + status = StatusPending + } + + logRecord.SetSeverityNumber(severityNumber) + logRecord.SetSeverityText(severityText) + + // Set the log body with human-readable summary + body := fmt.Sprintf("Permission check: %s/%s - %s", provider, perm.Action, status) + logRecord.Body().SetStr(body) + + // Set log attributes per RFC specification + attrs := logRecord.Attributes() + + // Policy context + attrs.PutStr("policy.id", policy.PolicyID) + if policy.PolicyName != "" { + attrs.PutStr("policy.name", policy.PolicyName) + } + + // Integration context + if integration.IntegrationID != "" { + attrs.PutStr("integration.id", integration.IntegrationID) + } + if integration.IntegrationName != "" { + attrs.PutStr("integration.name", integration.IntegrationName) + } + attrs.PutStr("integration.type", integration.IntegrationType) + + // Provider context + attrs.PutStr("provider.type", string(provider)) + if accountID, ok := integration.Config["account_id"].(string); ok && accountID != "" { + attrs.PutStr("provider.account", accountID) + } + if region, ok := integration.Config["region"].(string); ok && region != "" { + attrs.PutStr("provider.region", region) + } + if subscriptionID, ok := integration.Config["subscription_id"].(string); ok && subscriptionID != "" { + attrs.PutStr("provider.subscription_id", subscriptionID) + } + if projectID, ok := integration.Config["project_id"].(string); ok && projectID != "" { + attrs.PutStr("provider.project_id", projectID) + } + + // Permission details + attrs.PutStr("permission.action", perm.Action) + if perm.Category != "" { + attrs.PutStr("permission.category", perm.Category) + } + attrs.PutStr("permission.status", string(status)) + attrs.PutBool("permission.required", perm.Required) + + // Error details (if any) + if result.ErrorCode != "" { + attrs.PutStr("permission.error_code", result.ErrorCode) + } + if result.ErrorMessage != "" { + attrs.PutStr("permission.error_message", result.ErrorMessage) + } + + // Verification metadata + attrs.PutStr("verification.method", string(perm.Method)) + if result.Endpoint != "" { + attrs.PutStr("verification.endpoint", result.Endpoint) + } + attrs.PutInt("verification.duration_ms", result.Duration.Milliseconds()) + + r.logger.Debug("Emitted permission check log", + zap.String("policy_id", policy.PolicyID), + zap.String("integration_type", integration.IntegrationType), + zap.String("permission", perm.Action), + zap.String("status", string(status)), + zap.Duration("duration", result.Duration), + ) +} + +// emitUnsupportedIntegrationLog emits a warning log for unsupported integration types. +func (r *verifierReceiver) emitUnsupportedIntegrationLog( + scopeLogs plog.ScopeLogs, + timestamp pcommon.Timestamp, + policy PolicyConfig, + integration IntegrationConfig, +) { + logRecord := scopeLogs.LogRecords().AppendEmpty() + logRecord.SetTimestamp(timestamp) + logRecord.SetObservedTimestamp(timestamp) + logRecord.SetSeverityNumber(plog.SeverityNumberWarn) + logRecord.SetSeverityText("WARN") + + body := fmt.Sprintf("Unsupported integration type: %s - skipping permission verification", integration.IntegrationType) + logRecord.Body().SetStr(body) + + attrs := logRecord.Attributes() + attrs.PutStr("policy.id", policy.PolicyID) + if policy.PolicyName != "" { + attrs.PutStr("policy.name", policy.PolicyName) + } + if integration.IntegrationID != "" { + attrs.PutStr("integration.id", integration.IntegrationID) + } + if integration.IntegrationName != "" { + attrs.PutStr("integration.name", integration.IntegrationName) + } + attrs.PutStr("integration.type", integration.IntegrationType) + attrs.PutStr("permission.status", string(StatusSkipped)) + + r.logger.Warn("Unsupported integration type", + zap.String("integration_type", integration.IntegrationType), + zap.String("policy_id", policy.PolicyID), + ) +} diff --git a/receiver/verifierreceiver/receiver_test.go b/receiver/verifierreceiver/receiver_test.go new file mode 100644 index 000000000..08021157f --- /dev/null +++ b/receiver/verifierreceiver/receiver_test.go @@ -0,0 +1,424 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package verifierreceiver + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/consumer/consumertest" + "go.opentelemetry.io/collector/receiver/receivertest" + + "github.com/elastic/opentelemetry-collector-components/receiver/verifierreceiver/internal/metadata" + "github.com/elastic/opentelemetry-collector-components/receiver/verifierreceiver/internal/verifier" +) + +func TestReceiver_StartShutdown(t *testing.T) { + config := &Config{ + CloudConnectorID: "cc-12345", + CloudConnectorName: "Test Connector", + VerificationID: "verify-test-001", + VerificationType: "on_demand", + Providers: ProvidersConfig{ + AWS: AWSProviderConfig{ + Credentials: AWSCredentials{ + RoleARN: "arn:aws:iam::123456789012:role/ElasticAgentRole", + ExternalID: "elastic-test-external-id", + DefaultRegion: "us-east-1", + }, + }, + }, + Policies: []PolicyConfig{ + { + PolicyID: "policy-1", + PolicyName: "AWS Security Monitoring", + Integrations: []IntegrationConfig{ + { + IntegrationID: "int-cloudtrail-001", + IntegrationType: "aws_cloudtrail", + IntegrationName: "AWS CloudTrail", + Config: map[string]interface{}{ + "account_id": "123456789012", + "region": "us-east-1", + }, + }, + }, + }, + }, + } + + consumer := &consumertest.LogsSink{} + receiver := newVerifierReceiver( + receivertest.NewNopSettings(metadata.Type), + config, + consumer, + ) + + ctx := context.Background() + + // Start the receiver + err := receiver.Start(ctx, nil) + require.NoError(t, err) + + // Give it time to run the verification + time.Sleep(100 * time.Millisecond) + + // Shutdown the receiver + err = receiver.Shutdown(ctx) + require.NoError(t, err) + + // Verify logs were emitted + logs := consumer.AllLogs() + require.NotEmpty(t, logs, "expected logs to be emitted") + + // Check the first log batch + firstLog := logs[0] + require.Equal(t, 1, firstLog.ResourceLogs().Len()) + + resourceLog := firstLog.ResourceLogs().At(0) + + // Verify resource attributes per RFC specification + attrs := resourceLog.Resource().Attributes() + serviceName, ok := attrs.Get("service.name") + require.True(t, ok) + assert.Equal(t, "permission-verifier", serviceName.Str()) + + cloudConnectorID, ok := attrs.Get("cloud_connector.id") + require.True(t, ok) + assert.Equal(t, "cc-12345", cloudConnectorID.Str()) + + verificationID, ok := attrs.Get("verification.id") + require.True(t, ok) + assert.Equal(t, "verify-test-001", verificationID.Str()) + + // Verify log records + scopeLogs := resourceLog.ScopeLogs() + require.Equal(t, 1, scopeLogs.Len()) + + // Check scope name per RFC + assert.Equal(t, "elastic.permission_verification", scopeLogs.At(0).Scope().Name()) + + logRecords := scopeLogs.At(0).LogRecords() + assert.GreaterOrEqual(t, logRecords.Len(), 1, "expected log records for permissions") + + // Verify first log record attributes + record := logRecords.At(0) + + // Policy context + policyID, ok := record.Attributes().Get("policy.id") + require.True(t, ok) + assert.Equal(t, "policy-1", policyID.Str()) + + // Integration context + integrationType, ok := record.Attributes().Get("integration.type") + require.True(t, ok) + assert.Equal(t, "aws_cloudtrail", integrationType.Str()) + + // Provider context + providerType, ok := record.Attributes().Get("provider.type") + require.True(t, ok) + assert.Equal(t, "aws", providerType.Str()) + + // Permission status + status, ok := record.Attributes().Get("permission.status") + require.True(t, ok) + assert.Contains(t, []string{"pending", "granted", "denied", "error", "skipped"}, status.Str()) +} + +func TestReceiver_WithoutAWSCredentials(t *testing.T) { + config := &Config{ + CloudConnectorID: "cc-12345", + VerificationID: "verify-test-002", + // No provider credentials configured + Policies: []PolicyConfig{ + { + PolicyID: "policy-1", + Integrations: []IntegrationConfig{ + { + IntegrationType: "aws_cloudtrail", + IntegrationName: "CloudTrail", + }, + }, + }, + }, + } + + consumer := &consumertest.LogsSink{} + receiver := newVerifierReceiver( + receivertest.NewNopSettings(metadata.Type), + config, + consumer, + ) + + ctx := context.Background() + + err := receiver.Start(ctx, nil) + require.NoError(t, err) + + time.Sleep(100 * time.Millisecond) + + err = receiver.Shutdown(ctx) + require.NoError(t, err) + + // Should still emit logs but with error status + logs := consumer.AllLogs() + require.NotEmpty(t, logs) + + logRecords := logs[0].ResourceLogs().At(0).ScopeLogs().At(0).LogRecords() + assert.GreaterOrEqual(t, logRecords.Len(), 1) + + // First record should have error status since no credentials + record := logRecords.At(0) + status, ok := record.Attributes().Get("permission.status") + require.True(t, ok) + assert.Equal(t, "error", status.Str()) + + errorCode, ok := record.Attributes().Get("permission.error_code") + require.True(t, ok) + assert.Equal(t, "VerifierNotInitialized", errorCode.Str()) +} + +func TestReceiver_UnsupportedIntegration(t *testing.T) { + config := &Config{ + CloudConnectorID: "cc-12345", + VerificationID: "verify-test-003", + Policies: []PolicyConfig{ + { + PolicyID: "policy-1", + Integrations: []IntegrationConfig{ + { + IntegrationType: "unknown_integration", + IntegrationName: "Unknown Integration", + }, + }, + }, + }, + } + + consumer := &consumertest.LogsSink{} + receiver := newVerifierReceiver( + receivertest.NewNopSettings(metadata.Type), + config, + consumer, + ) + + ctx := context.Background() + + err := receiver.Start(ctx, nil) + require.NoError(t, err) + + time.Sleep(100 * time.Millisecond) + + err = receiver.Shutdown(ctx) + require.NoError(t, err) + + logs := consumer.AllLogs() + require.NotEmpty(t, logs) + + logRecords := logs[0].ResourceLogs().At(0).ScopeLogs().At(0).LogRecords() + require.Equal(t, 1, logRecords.Len()) + + record := logRecords.At(0) + assert.Equal(t, "WARN", record.SeverityText()) + assert.Contains(t, record.Body().Str(), "Unsupported integration type") + + status, ok := record.Attributes().Get("permission.status") + require.True(t, ok) + assert.Equal(t, "skipped", status.Str()) +} + +func TestReceiver_MultipleIntegrations(t *testing.T) { + config := &Config{ + CloudConnectorID: "cc-12345", + VerificationID: "verify-test-004", + Providers: ProvidersConfig{ + AWS: AWSProviderConfig{ + Credentials: AWSCredentials{ + RoleARN: "arn:aws:iam::123456789012:role/ElasticAgentRole", + ExternalID: "elastic-test-external-id", + DefaultRegion: "us-east-1", + }, + }, + }, + Policies: []PolicyConfig{ + { + PolicyID: "policy-1", + PolicyName: "AWS Security", + Integrations: []IntegrationConfig{ + {IntegrationType: "aws_cloudtrail"}, + {IntegrationType: "aws_guardduty"}, + }, + }, + { + PolicyID: "policy-2", + PolicyName: "AWS Storage", + Integrations: []IntegrationConfig{ + {IntegrationType: "aws_s3"}, + }, + }, + }, + } + + consumer := &consumertest.LogsSink{} + receiver := newVerifierReceiver( + receivertest.NewNopSettings(metadata.Type), + config, + consumer, + ) + + ctx := context.Background() + + err := receiver.Start(ctx, nil) + require.NoError(t, err) + + time.Sleep(100 * time.Millisecond) + + err = receiver.Shutdown(ctx) + require.NoError(t, err) + + logs := consumer.AllLogs() + require.NotEmpty(t, logs) + + logRecords := logs[0].ResourceLogs().At(0).ScopeLogs().At(0).LogRecords() + assert.GreaterOrEqual(t, logRecords.Len(), 10, "expected log records for all integration permissions") + + // Collect unique policy IDs and integration types + policyIDs := make(map[string]bool) + integrationTypes := make(map[string]bool) + + for i := 0; i < logRecords.Len(); i++ { + record := logRecords.At(i) + if policyID, ok := record.Attributes().Get("policy.id"); ok { + policyIDs[policyID.Str()] = true + } + if intType, ok := record.Attributes().Get("integration.type"); ok { + integrationTypes[intType.Str()] = true + } + } + + assert.True(t, policyIDs["policy-1"]) + assert.True(t, policyIDs["policy-2"]) + assert.True(t, integrationTypes["aws_cloudtrail"]) + assert.True(t, integrationTypes["aws_guardduty"]) + assert.True(t, integrationTypes["aws_s3"]) +} + +func TestPermissionRegistry(t *testing.T) { + registry := NewPermissionRegistry() + + t.Run("supported integration", func(t *testing.T) { + perms := registry.GetPermissions("aws_cloudtrail") + require.NotNil(t, perms) + assert.Equal(t, verifier.ProviderAWS, perms.Provider) + assert.NotEmpty(t, perms.Permissions) + + actionFound := false + for _, p := range perms.Permissions { + if p.Action == "cloudtrail:LookupEvents" { + actionFound = true + assert.True(t, p.Required) + assert.Equal(t, MethodAPICall, p.Method) + break + } + } + assert.True(t, actionFound, "expected cloudtrail:LookupEvents permission") + }) + + t.Run("unsupported integration", func(t *testing.T) { + perms := registry.GetPermissions("unknown_integration") + assert.Nil(t, perms) + assert.False(t, registry.IsSupported("unknown_integration")) + }) + + t.Run("all AWS integrations registered", func(t *testing.T) { + awsIntegrations := []string{ + "aws_cloudtrail", + "aws_guardduty", + "aws_securityhub", + "aws_s3", + "aws_ec2", + "aws_vpcflow", + "aws_waf", + "aws_route53", + "aws_elb", + "aws_cloudfront", + } + + for _, integration := range awsIntegrations { + assert.True(t, registry.IsSupported(integration), "expected %s to be supported", integration) + perms := registry.GetPermissions(integration) + require.NotNil(t, perms, "expected permissions for %s", integration) + assert.Equal(t, verifier.ProviderAWS, perms.Provider, "expected AWS provider for %s", integration) + } + }) + + t.Run("Azure integrations registered", func(t *testing.T) { + azureIntegrations := []string{ + "azure_activitylogs", + "azure_auditlogs", + "azure_blob_storage", + } + + for _, integration := range azureIntegrations { + assert.True(t, registry.IsSupported(integration), "expected %s to be supported", integration) + perms := registry.GetPermissions(integration) + require.NotNil(t, perms, "expected permissions for %s", integration) + assert.Equal(t, verifier.ProviderAzure, perms.Provider, "expected Azure provider for %s", integration) + } + }) + + t.Run("GCP integrations registered", func(t *testing.T) { + gcpIntegrations := []string{ + "gcp_audit", + "gcp_storage", + "gcp_pubsub", + } + + for _, integration := range gcpIntegrations { + assert.True(t, registry.IsSupported(integration), "expected %s to be supported", integration) + perms := registry.GetPermissions(integration) + require.NotNil(t, perms, "expected permissions for %s", integration) + assert.Equal(t, verifier.ProviderGCP, perms.Provider, "expected GCP provider for %s", integration) + } + }) + + t.Run("Okta integrations registered", func(t *testing.T) { + oktaIntegrations := []string{ + "okta_system", + "okta_users", + } + + for _, integration := range oktaIntegrations { + assert.True(t, registry.IsSupported(integration), "expected %s to be supported", integration) + perms := registry.GetPermissions(integration) + require.NotNil(t, perms, "expected permissions for %s", integration) + assert.Equal(t, verifier.ProviderOkta, perms.Provider, "expected Okta provider for %s", integration) + } + }) + + t.Run("supported integrations by provider", func(t *testing.T) { + byProvider := registry.SupportedIntegrationsByProvider() + assert.NotEmpty(t, byProvider[verifier.ProviderAWS]) + assert.NotEmpty(t, byProvider[verifier.ProviderAzure]) + assert.NotEmpty(t, byProvider[verifier.ProviderGCP]) + assert.NotEmpty(t, byProvider[verifier.ProviderOkta]) + }) +} diff --git a/receiver/verifierreceiver/registry.go b/receiver/verifierreceiver/registry.go new file mode 100644 index 000000000..d3994b9b3 --- /dev/null +++ b/receiver/verifierreceiver/registry.go @@ -0,0 +1,540 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package verifierreceiver // import "github.com/elastic/opentelemetry-collector-components/receiver/verifierreceiver" + +import ( + "github.com/elastic/opentelemetry-collector-components/receiver/verifierreceiver/internal/verifier" +) + +// VerificationMethod indicates how a permission should be verified. +type VerificationMethod string + +const ( + // MethodAPICall makes an actual API call with minimal scope. + MethodAPICall VerificationMethod = "api_call" + // MethodDryRun uses provider's DryRun parameter where supported (e.g., AWS EC2). + MethodDryRun VerificationMethod = "dry_run" + // MethodHTTPProbe uses HTTP HEAD/GET request to check connectivity. + MethodHTTPProbe VerificationMethod = "http_probe" + // MethodGraphQL uses GraphQL introspection or minimal query. + MethodGraphQL VerificationMethod = "graphql_query" +) + +// PermissionStatus represents the result of a permission verification. +type PermissionStatus string + +const ( + StatusGranted PermissionStatus = "granted" + StatusDenied PermissionStatus = "denied" + StatusError PermissionStatus = "error" + StatusSkipped PermissionStatus = "skipped" + StatusPending PermissionStatus = "pending" +) + +// Permission represents a single permission to verify. +type Permission struct { + // Action is the permission action (e.g., "cloudtrail:LookupEvents", "s3:GetObject"). + Action string + + // Required indicates if this permission is required for the integration to function. + Required bool + + // Method is the verification method to use. + Method VerificationMethod + + // APIEndpoint is the API endpoint to call (for http_probe, graphql_query methods). + APIEndpoint string + + // Category is an optional categorization (e.g., "data_access", "management"). + Category string +} + +// IntegrationPermissions defines the permissions required by an integration type. +type IntegrationPermissions struct { + // Provider identifies the cloud/service provider. + Provider verifier.ProviderType + + // Permissions is the list of permissions required by this integration. + Permissions []Permission +} + +// PermissionRegistry maintains the mapping of integration types to their required permissions. +// The receiver owns this mapping - Fleet API only provides the integration context. +type PermissionRegistry struct { + integrations map[string]IntegrationPermissions +} + +// NewPermissionRegistry creates a new permission registry with default mappings. +func NewPermissionRegistry() *PermissionRegistry { + registry := &PermissionRegistry{ + integrations: make(map[string]IntegrationPermissions), + } + + // Register all provider integrations + registry.registerAWSIntegrations() + registry.registerAzureIntegrations() + registry.registerGCPIntegrations() + registry.registerOktaIntegrations() + + return registry +} + +// GetPermissions returns the permissions required for an integration type. +// Returns nil if the integration type is not registered. +func (r *PermissionRegistry) GetPermissions(integrationType string) *IntegrationPermissions { + if perms, ok := r.integrations[integrationType]; ok { + return &perms + } + return nil +} + +// IsSupported returns true if the integration type is registered in the registry. +func (r *PermissionRegistry) IsSupported(integrationType string) bool { + _, ok := r.integrations[integrationType] + return ok +} + +// SupportedIntegrations returns a list of all supported integration types. +func (r *PermissionRegistry) SupportedIntegrations() []string { + integrations := make([]string, 0, len(r.integrations)) + for k := range r.integrations { + integrations = append(integrations, k) + } + return integrations +} + +// SupportedIntegrationsByProvider returns integration types grouped by provider. +func (r *PermissionRegistry) SupportedIntegrationsByProvider() map[verifier.ProviderType][]string { + byProvider := make(map[verifier.ProviderType][]string) + for integrationType, perms := range r.integrations { + byProvider[perms.Provider] = append(byProvider[perms.Provider], integrationType) + } + return byProvider +} + +// registerAWSIntegrations registers all AWS-based integrations. +func (r *PermissionRegistry) registerAWSIntegrations() { + // AWS CloudTrail - commonly used for security auditing + // https://www.elastic.co/docs/current/integrations/aws/cloudtrail + r.integrations["aws_cloudtrail"] = IntegrationPermissions{ + Provider: verifier.ProviderAWS, + Permissions: []Permission{ + { + Action: "cloudtrail:LookupEvents", + Required: true, + Method: MethodAPICall, + Category: "data_access", + }, + { + Action: "cloudtrail:DescribeTrails", + Required: true, + Method: MethodAPICall, + Category: "management", + }, + { + Action: "cloudtrail:GetTrailStatus", + Required: false, + Method: MethodAPICall, + Category: "management", + }, + { + Action: "s3:GetObject", + Required: true, + Method: MethodAPICall, + Category: "data_access", + }, + { + Action: "s3:ListBucket", + Required: true, + Method: MethodAPICall, + Category: "data_access", + }, + { + Action: "sqs:ReceiveMessage", + Required: false, + Method: MethodAPICall, + Category: "data_access", + }, + { + Action: "sqs:DeleteMessage", + Required: false, + Method: MethodAPICall, + Category: "data_access", + }, + }, + } + + // AWS GuardDuty - threat detection service + r.integrations["aws_guardduty"] = IntegrationPermissions{ + Provider: verifier.ProviderAWS, + Permissions: []Permission{ + { + Action: "guardduty:ListDetectors", + Required: true, + Method: MethodAPICall, + Category: "management", + }, + { + Action: "guardduty:GetFindings", + Required: true, + Method: MethodAPICall, + Category: "data_access", + }, + { + Action: "guardduty:ListFindings", + Required: true, + Method: MethodAPICall, + Category: "data_access", + }, + }, + } + + // AWS Security Hub - security findings aggregation + r.integrations["aws_securityhub"] = IntegrationPermissions{ + Provider: verifier.ProviderAWS, + Permissions: []Permission{ + { + Action: "securityhub:GetFindings", + Required: true, + Method: MethodAPICall, + Category: "data_access", + }, + { + Action: "securityhub:BatchGetSecurityControls", + Required: false, + Method: MethodAPICall, + Category: "data_access", + }, + { + Action: "securityhub:DescribeHub", + Required: true, + Method: MethodAPICall, + Category: "management", + }, + }, + } + + // AWS S3 - storage access logs + r.integrations["aws_s3"] = IntegrationPermissions{ + Provider: verifier.ProviderAWS, + Permissions: []Permission{ + { + Action: "s3:ListBucket", + Required: true, + Method: MethodAPICall, + Category: "data_access", + }, + { + Action: "s3:GetObject", + Required: true, + Method: MethodAPICall, + Category: "data_access", + }, + { + Action: "s3:GetBucketLocation", + Required: false, + Method: MethodAPICall, + Category: "management", + }, + }, + } + + // AWS EC2 - compute instance metrics + r.integrations["aws_ec2"] = IntegrationPermissions{ + Provider: verifier.ProviderAWS, + Permissions: []Permission{ + { + Action: "ec2:DescribeInstances", + Required: true, + Method: MethodDryRun, + Category: "data_access", + }, + { + Action: "ec2:DescribeRegions", + Required: true, + Method: MethodAPICall, + Category: "management", + }, + { + Action: "cloudwatch:GetMetricData", + Required: true, + Method: MethodAPICall, + Category: "data_access", + }, + }, + } + + // AWS VPC Flow Logs + r.integrations["aws_vpcflow"] = IntegrationPermissions{ + Provider: verifier.ProviderAWS, + Permissions: []Permission{ + { + Action: "logs:FilterLogEvents", + Required: true, + Method: MethodAPICall, + Category: "data_access", + }, + { + Action: "logs:DescribeLogGroups", + Required: true, + Method: MethodAPICall, + Category: "management", + }, + { + Action: "logs:DescribeLogStreams", + Required: true, + Method: MethodAPICall, + Category: "management", + }, + { + Action: "ec2:DescribeFlowLogs", + Required: false, + Method: MethodAPICall, + Category: "management", + }, + }, + } + + // AWS WAF - Web Application Firewall logs + r.integrations["aws_waf"] = IntegrationPermissions{ + Provider: verifier.ProviderAWS, + Permissions: []Permission{ + { + Action: "wafv2:GetWebACL", + Required: true, + Method: MethodAPICall, + Category: "management", + }, + { + Action: "wafv2:ListWebACLs", + Required: true, + Method: MethodAPICall, + Category: "management", + }, + { + Action: "s3:GetObject", + Required: true, + Method: MethodAPICall, + Category: "data_access", + }, + { + Action: "s3:ListBucket", + Required: true, + Method: MethodAPICall, + Category: "data_access", + }, + }, + } + + // AWS Route53 - DNS query logs + r.integrations["aws_route53"] = IntegrationPermissions{ + Provider: verifier.ProviderAWS, + Permissions: []Permission{ + { + Action: "logs:FilterLogEvents", + Required: true, + Method: MethodAPICall, + Category: "data_access", + }, + { + Action: "logs:DescribeLogGroups", + Required: true, + Method: MethodAPICall, + Category: "management", + }, + { + Action: "route53:ListHostedZones", + Required: false, + Method: MethodAPICall, + Category: "management", + }, + }, + } + + // AWS ELB - Elastic Load Balancer access logs + r.integrations["aws_elb"] = IntegrationPermissions{ + Provider: verifier.ProviderAWS, + Permissions: []Permission{ + { + Action: "s3:GetObject", + Required: true, + Method: MethodAPICall, + Category: "data_access", + }, + { + Action: "s3:ListBucket", + Required: true, + Method: MethodAPICall, + Category: "data_access", + }, + { + Action: "elasticloadbalancing:DescribeLoadBalancers", + Required: false, + Method: MethodAPICall, + Category: "management", + }, + }, + } + + // AWS CloudFront - CDN access logs + r.integrations["aws_cloudfront"] = IntegrationPermissions{ + Provider: verifier.ProviderAWS, + Permissions: []Permission{ + { + Action: "s3:GetObject", + Required: true, + Method: MethodAPICall, + Category: "data_access", + }, + { + Action: "s3:ListBucket", + Required: true, + Method: MethodAPICall, + Category: "data_access", + }, + { + Action: "cloudfront:ListDistributions", + Required: false, + Method: MethodAPICall, + Category: "management", + }, + }, + } +} + +// registerAzureIntegrations registers all Azure-based integrations. +// TODO: Implement Azure verifier and add actual permission mappings. +func (r *PermissionRegistry) registerAzureIntegrations() { + // Azure Activity Logs + r.integrations["azure_activitylogs"] = IntegrationPermissions{ + Provider: verifier.ProviderAzure, + Permissions: []Permission{ + { + Action: "Microsoft.Insights/eventtypes/values/Read", + Required: true, + Method: MethodAPICall, + Category: "data_access", + }, + }, + } + + // Azure Audit Logs + r.integrations["azure_auditlogs"] = IntegrationPermissions{ + Provider: verifier.ProviderAzure, + Permissions: []Permission{ + { + Action: "Microsoft.Insights/eventtypes/values/Read", + Required: true, + Method: MethodAPICall, + Category: "data_access", + }, + }, + } + + // Azure Blob Storage + r.integrations["azure_blob_storage"] = IntegrationPermissions{ + Provider: verifier.ProviderAzure, + Permissions: []Permission{ + { + Action: "Microsoft.Storage/storageAccounts/blobServices/containers/read", + Required: true, + Method: MethodAPICall, + Category: "data_access", + }, + }, + } +} + +// registerGCPIntegrations registers all GCP-based integrations. +// TODO: Implement GCP verifier and add actual permission mappings. +func (r *PermissionRegistry) registerGCPIntegrations() { + // GCP Audit Logs + r.integrations["gcp_audit"] = IntegrationPermissions{ + Provider: verifier.ProviderGCP, + Permissions: []Permission{ + { + Action: "logging.logEntries.list", + Required: true, + Method: MethodAPICall, + Category: "data_access", + }, + }, + } + + // GCP Cloud Storage + r.integrations["gcp_storage"] = IntegrationPermissions{ + Provider: verifier.ProviderGCP, + Permissions: []Permission{ + { + Action: "storage.objects.get", + Required: true, + Method: MethodAPICall, + Category: "data_access", + }, + { + Action: "storage.objects.list", + Required: true, + Method: MethodAPICall, + Category: "data_access", + }, + }, + } + + // GCP Pub/Sub + r.integrations["gcp_pubsub"] = IntegrationPermissions{ + Provider: verifier.ProviderGCP, + Permissions: []Permission{ + { + Action: "pubsub.subscriptions.consume", + Required: true, + Method: MethodAPICall, + Category: "data_access", + }, + }, + } +} + +// registerOktaIntegrations registers all Okta-based integrations. +// TODO: Implement Okta verifier and add actual permission mappings. +func (r *PermissionRegistry) registerOktaIntegrations() { + // Okta System Logs + r.integrations["okta_system"] = IntegrationPermissions{ + Provider: verifier.ProviderOkta, + Permissions: []Permission{ + { + Action: "okta.logs.read", + Required: true, + Method: MethodAPICall, + Category: "data_access", + }, + }, + } + + // Okta User Events + r.integrations["okta_users"] = IntegrationPermissions{ + Provider: verifier.ProviderOkta, + Permissions: []Permission{ + { + Action: "okta.users.read", + Required: true, + Method: MethodAPICall, + Category: "data_access", + }, + }, + } +} diff --git a/receiver/verifierreceiver/testdata/TESTING.md b/receiver/verifierreceiver/testdata/TESTING.md new file mode 100644 index 000000000..c504f165f --- /dev/null +++ b/receiver/verifierreceiver/testdata/TESTING.md @@ -0,0 +1,333 @@ +# Testing the Verifier Receiver + +The verifier receiver supports permission verification for multiple cloud and identity providers: +- **AWS** - CloudTrail, GuardDuty, S3, EC2, etc. +- **Azure** - Activity Logs, Audit Logs, Blob Storage (future implementation) +- **GCP** - Audit Logs, Cloud Storage, Pub/Sub (future implementation) +- **Okta** - System Logs, User Events (future implementation) + +## 1. Unit Tests + +```bash +cd receiver/verifierreceiver +go test ./... -v +``` + +## 2. Build the OTEL Distribution + +Build the elastic-components distribution that includes the verifier receiver: + +```bash +cd opentelemetry-collector-components + +# Install the builder if needed +go install go.opentelemetry.io/collector/cmd/builder@latest + +# Build the collector (uses Makefile) +make genelasticcol +``` + +This creates `./_build/elastic-collector-components`. + +## 3. Run Standalone Test (No Credentials) + +Quick smoke test without provider credentials - will show "VerifierNotInitialized" errors: + +```bash +./_build/elastic-collector-components --config ./receiver/verifierreceiver/testdata/otel-config.yaml +``` + +Expected output includes: +``` +info Starting verifier receiver {"cloud_connector_id": "cc-test-minimal", ...} +debug AWS credentials not configured +debug Azure credentials not configured +debug GCP credentials not configured +debug Okta credentials not configured +warn No verifiers initialized - permission verification will be limited +... +LogsExporter {"logs": {"resourceLogs":[{...}]}} +``` + +## 4. Run Standalone Test with AWS Cloud Connector Auth + +Edit `testdata/standalone-test.yaml` and uncomment/set your AWS credentials: + +```yaml +providers: + aws: + credentials: + role_arn: "arn:aws:iam::YOUR_ACCOUNT:role/YOUR_ROLE" + external_id: "your-external-id" + default_region: "us-east-1" +``` + +Then run: + +```bash +./_build/elastic-collector-components --config ./receiver/verifierreceiver/testdata/standalone-test.yaml +``` + +Expected output with valid credentials: +``` +info Starting verifier receiver {"cloud_connector_id": "cc-test-12345", ...} +info Initializing AWS verifier with Cloud Connector authentication +info AWS verifier initialized successfully +info Verifiers initialized {"providers": ["aws"]} +... +Permission check: aws/cloudtrail:DescribeTrails - granted +Permission check: aws/cloudtrail:GetEventSelectors - granted +... +``` + +## 5. Test with AWS Default Credentials (AWS_PROFILE) + +For local testing with an AWS profile, use the test-csp-profile.yaml config: + +```bash +# Replace 'csp' with your AWS profile name +AWS_PROFILE=csp ./_build/elastic-collector-components --config ./receiver/verifierreceiver/testdata/test-csp-profile.yaml +``` + +The config uses `use_default_credentials: true`: + +```yaml +providers: + aws: + credentials: + use_default_credentials: true + default_region: "us-east-1" +``` + +## 6. Multi-Provider Configuration + +Configure multiple providers in a single receiver: + +```yaml +receivers: + verifier: + cloud_connector_id: "cc-multi-provider" + verification_id: "verify-multi-001" + + providers: + # AWS Cloud Connector authentication + aws: + credentials: + role_arn: "arn:aws:iam::123456789012:role/ElasticAgentRole" + external_id: "your-external-id" + default_region: "us-east-1" + + # Azure (future - not yet implemented) + azure: + credentials: + tenant_id: "your-tenant-id" + client_id: "your-client-id" + client_secret: "your-client-secret" + + # GCP (future - not yet implemented) + gcp: + credentials: + project_id: "your-project-id" + use_default_credentials: true + + # Okta (future - not yet implemented) + okta: + credentials: + domain: "dev-123456.okta.com" + api_token: "your-api-token" + + policies: + - policy_id: "multi-cloud-policy" + policy_name: "Multi-Cloud Monitoring" + integrations: + - integration_type: "aws_cloudtrail" + integration_name: "CloudTrail" + - integration_type: "azure_activitylogs" + integration_name: "Azure Activity" + - integration_type: "gcp_audit" + integration_name: "GCP Audit" + - integration_type: "okta_system" + integration_name: "Okta System Logs" +``` + +## 7. Test with Elastic Agent + +### Build elastic-agent with the new receiver: + +```bash +cd elastic-agent + +# Update go.mod to point to local opentelemetry-collector-components +go mod edit -replace github.com/elastic/opentelemetry-collector-components/receiver/verifierreceiver=../opentelemetry-collector-components/receiver/verifierreceiver + +go mod tidy +mage build +``` + +### Create a test OTEL config for the agent: + +```yaml +# otel.yml +receivers: + verifier: + cloud_connector_id: "cc-agent-test" + verification_id: "verify-agent-001" + + providers: + aws: + credentials: + role_arn: "arn:aws:iam::123456789012:role/ElasticAgentRole" + external_id: "your-external-id" + default_region: "us-east-1" + + policies: + - policy_id: "agent-test-policy" + policy_name: "Agent Test Policy" + integrations: + - integration_type: "aws_cloudtrail" + integration_name: "CloudTrail" + +exporters: + debug: + verbosity: detailed + +service: + pipelines: + logs: + receivers: [verifier] + exporters: [debug] +``` + +### Run the agent in OTEL mode: + +```bash +./elastic-agent otel --config otel.yml +``` + +## 8. Test the Fleet Integration Package + +### Validate the integration package: + +```bash +cd integrations +elastic-package build --packages verifier_otel +elastic-package lint --packages verifier_otel +``` + +### Test the template rendering: + +```bash +elastic-package test policy --packages verifier_otel +``` + +### Run system tests (requires running stack): + +```bash +elastic-package stack up -d +elastic-package test system --packages verifier_otel +``` + +## 9. End-to-End Test with Elasticsearch + +Create a config that exports to Elasticsearch: + +```yaml +receivers: + verifier: + cloud_connector_id: "cc-e2e-test" + verification_id: "verify-e2e-001" + + providers: + aws: + credentials: + role_arn: "arn:aws:iam::123456789012:role/ElasticAgentRole" + external_id: "your-external-id" + default_region: "us-east-1" + + policies: + - policy_id: "e2e-test-policy" + policy_name: "E2E Test Policy" + integrations: + - integration_type: "aws_cloudtrail" + integration_name: "CloudTrail E2E" + +processors: + batch: + +exporters: + elasticsearch: + endpoints: ["http://localhost:9200"] + logs_index: "logs-cloud_connector.permission_verification-default" + mapping: + mode: ecs + +service: + pipelines: + logs: + receivers: [verifier] + processors: [batch] + exporters: [elasticsearch] +``` + +Then query Elasticsearch: + +```bash +curl -X GET "localhost:9200/logs-cloud_connector.permission_verification-default/_search?pretty" -H 'Content-Type: application/json' -d' +{ + "query": { + "match": { + "integration.type": "aws_cloudtrail" + } + } +}' +``` + +## 10. Quick Smoke Test + +For a quick verification that everything compiles: + +```bash +# In opentelemetry-collector-components +cd receiver/verifierreceiver +go build ./... +go test ./... -short + +# Build the full distribution +cd ../.. +make genelasticcol +``` + +## Architecture Overview + +The verifier receiver uses a **registry pattern** for extensibility: + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Verifier Receiver │ +├─────────────────────────────────────────────────────────────────┤ +│ ┌───────────────────┐ ┌───────────────────────────────┐ │ +│ │ Permission │ │ Verifier Registry │ │ +│ │ Registry │ │ │ │ +│ │ │ │ ┌─────────────────────────┐ │ │ +│ │ aws_cloudtrail │ │ │ AWS Verifier (active) │ │ │ +│ │ aws_guardduty │ │ └─────────────────────────┘ │ │ +│ │ azure_activitylogs│ │ ┌─────────────────────────┐ │ │ +│ │ gcp_audit │ │ │ Azure Verifier (future) │ │ │ +│ │ okta_system │ │ └─────────────────────────┘ │ │ +│ │ ... │ │ ┌─────────────────────────┐ │ │ +│ └───────────────────┘ │ │ GCP Verifier (future) │ │ │ +│ │ └─────────────────────────┘ │ │ +│ │ ┌─────────────────────────┐ │ │ +│ │ │ Okta Verifier (future) │ │ │ +│ │ └─────────────────────────┘ │ │ +│ └───────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +To add a new provider verifier: + +1. Create a new verifier in `internal/verifier/` (e.g., `azure_verifier.go`) +2. Implement the `Verifier` interface +3. Create a factory function: `NewAzureVerifierFactory()` +4. Register the factory in `newVerifierReceiver()` in `receiver.go` +5. Add integration mappings in `registry.go` diff --git a/receiver/verifierreceiver/testdata/agent-simulation.yaml b/receiver/verifierreceiver/testdata/agent-simulation.yaml new file mode 100644 index 000000000..02e2078ee --- /dev/null +++ b/receiver/verifierreceiver/testdata/agent-simulation.yaml @@ -0,0 +1,74 @@ +# Simulates the Elastic Agent OTEL mode configuration +# This tests the same receiver that would run inside elastic-agent +# +# Run with: +# AWS_PROFILE=csp ./_build/elastic-collector-components --config ./receiver/verifierreceiver/testdata/agent-simulation.yaml + +receivers: + verifier: + # Cloud Connector Identification (would come from Fleet) + cloud_connector_id: "cc-agent-simulation-001" + cloud_connector_name: "Agent Simulation Test" + + # Verification Session (would be generated per run) + verification_id: "verify-sim-001" + verification_type: "on_demand" + + # Provider Credentials + providers: + aws: + credentials: + # Using default credentials (AWS_PROFILE) for testing + use_default_credentials: true + default_region: "us-east-1" + + # Policies and Integrations (would come from Fleet policy) + policies: + - policy_id: "policy-aws-security" + policy_name: "AWS Security Monitoring" + integrations: + - integration_id: "int-cloudtrail-001" + integration_type: "aws_cloudtrail" + integration_name: "AWS CloudTrail" + config: + region: "us-east-1" + - integration_id: "int-guardduty-001" + integration_type: "aws_guardduty" + integration_name: "AWS GuardDuty" + config: + region: "us-east-1" + + - policy_id: "policy-aws-infra" + policy_name: "AWS Infrastructure" + integrations: + - integration_id: "int-s3-001" + integration_type: "aws_s3" + integration_name: "AWS S3 Logs" + config: + region: "us-west-2" + - integration_id: "int-ec2-001" + integration_type: "aws_ec2" + integration_name: "AWS EC2 Metrics" + config: + region: "us-east-1" + +exporters: + # Debug exporter shows detailed output in console + debug: + verbosity: detailed + +processors: + # Batch processor for efficiency (like production) + batch: + timeout: 5s + send_batch_size: 100 + +service: + telemetry: + logs: + level: info + pipelines: + logs: + receivers: [verifier] + processors: [batch] + exporters: [debug] diff --git a/receiver/verifierreceiver/testdata/config.yaml b/receiver/verifierreceiver/testdata/config.yaml new file mode 100644 index 000000000..6f517fdba --- /dev/null +++ b/receiver/verifierreceiver/testdata/config.yaml @@ -0,0 +1,103 @@ +# Test configuration for verifier receiver +# This file demonstrates the OTel Collector configuration format for the verifier receiver + +receivers: + # Default verifier configuration + verifier: + cloud_connector_id: "cc-12345" + cloud_connector_name: "Production Connector" + verification_id: "verify-abc123" + verification_type: "on_demand" + # AWS Authentication - Cloud Connector STS AssumeRole + providers: + aws: + credentials: + role_arn: "arn:aws:iam::123456789012:role/ElasticAgentRole" + external_id: "elastic-external-id-12345" + default_region: "us-east-1" + policies: + - policy_id: "policy-1" + policy_name: "AWS Security Monitoring" + integrations: + - integration_id: "int-cloudtrail-001" + integration_type: "aws_cloudtrail" + integration_name: "AWS CloudTrail" + config: + account_id: "123456789012" + region: "us-east-1" + - integration_id: "int-guardduty-001" + integration_type: "aws_guardduty" + integration_name: "AWS GuardDuty" + config: + account_id: "123456789012" + region: "us-east-1" + - policy_id: "policy-2" + policy_name: "AWS Infrastructure" + integrations: + - integration_id: "int-ec2-001" + integration_type: "aws_ec2" + integration_name: "AWS EC2 Metrics" + config: + account_id: "123456789012" + region: "us-west-2" + + # Named verifier instance for single integration testing + verifier/single-integration: + cloud_connector_id: "cc-single" + cloud_connector_name: "Single Integration Test Connector" + verification_id: "verify-single-001" + verification_type: "on_demand" + providers: + aws: + credentials: + role_arn: "arn:aws:iam::123456789012:role/ElasticAgentRole" + external_id: "elastic-external-id-single" + default_region: "us-east-1" + policies: + - policy_id: "policy-single" + policy_name: "Single Integration Test" + integrations: + - integration_id: "int-s3-001" + integration_type: "aws_s3" + integration_name: "AWS S3 Logs" + config: + account_id: "123456789012" + + # Named verifier instance for Security Hub testing + verifier/security-hub: + cloud_connector_id: "cc-securityhub" + cloud_connector_name: "Security Hub Connector" + verification_id: "verify-sechub-001" + verification_type: "on_demand" + providers: + aws: + credentials: + role_arn: "arn:aws:iam::123456789012:role/ElasticAgentRole" + external_id: "elastic-external-id-sechub" + default_region: "us-east-1" + policies: + - policy_id: "policy-security" + policy_name: "Security Hub Policy" + integrations: + - integration_id: "int-securityhub-001" + integration_type: "aws_securityhub" + integration_name: "AWS Security Hub" + config: + account_id: "123456789012" + region: "us-east-1" + +exporters: + debug: + verbosity: detailed + +service: + pipelines: + logs: + receivers: [verifier] + exporters: [debug] + logs/single: + receivers: [verifier/single-integration] + exporters: [debug] + logs/security: + receivers: [verifier/security-hub] + exporters: [debug] diff --git a/receiver/verifierreceiver/testdata/expected-logs.yaml b/receiver/verifierreceiver/testdata/expected-logs.yaml new file mode 100644 index 000000000..ec1593844 --- /dev/null +++ b/receiver/verifierreceiver/testdata/expected-logs.yaml @@ -0,0 +1,190 @@ +# Expected log output from the verifier receiver +# This file documents the expected OTLP log format for permission verification results + +resourceLogs: + - resource: + attributes: + - key: cloud_connector.id + value: + stringValue: cc-12345 + - key: cloud_connector.name + value: + stringValue: Production Connector + - key: verification.id + value: + stringValue: verify-abc123 + - key: verification.timestamp + value: + stringValue: "2024-01-15T10:30:00Z" + - key: verification.type + value: + stringValue: on_demand + - key: service.name + value: + stringValue: permission-verifier + - key: service.version + value: + stringValue: "1.0.0" + scopeLogs: + - scope: + name: elastic.permission_verification + version: "1.0.0" + logRecords: + # Permission granted example + - severityNumber: 9 + severityText: INFO + body: + stringValue: "Permission check: aws/cloudtrail:LookupEvents - granted" + attributes: + - key: policy.id + value: + stringValue: policy-1 + - key: policy.name + value: + stringValue: AWS Security Monitoring + - key: integration.id + value: + stringValue: int-cloudtrail-001 + - key: integration.name + value: + stringValue: AWS CloudTrail + - key: integration.type + value: + stringValue: aws_cloudtrail + - key: provider.type + value: + stringValue: aws + - key: provider.region + value: + stringValue: us-east-1 + - key: permission.action + value: + stringValue: cloudtrail:LookupEvents + - key: permission.category + value: + stringValue: data_access + - key: permission.status + value: + stringValue: granted + - key: permission.required + value: + boolValue: true + - key: verification.method + value: + stringValue: api_call + - key: verification.endpoint + value: + stringValue: cloudtrail:LookupEvents + - key: verification.duration_ms + value: + intValue: 450 + + # Permission denied example + - severityNumber: 13 + severityText: WARN + body: + stringValue: "Permission check: aws/guardduty:ListDetectors - denied" + attributes: + - key: policy.id + value: + stringValue: policy-1 + - key: policy.name + value: + stringValue: AWS Security Monitoring + - key: integration.id + value: + stringValue: int-guardduty-001 + - key: integration.name + value: + stringValue: AWS GuardDuty + - key: integration.type + value: + stringValue: aws_guardduty + - key: provider.type + value: + stringValue: aws + - key: provider.region + value: + stringValue: us-east-1 + - key: permission.action + value: + stringValue: guardduty:ListDetectors + - key: permission.category + value: + stringValue: data_access + - key: permission.status + value: + stringValue: denied + - key: permission.required + value: + boolValue: true + - key: permission.error_code + value: + stringValue: AccessDenied + - key: permission.error_message + value: + stringValue: "User is not authorized to perform guardduty:ListDetectors" + - key: verification.method + value: + stringValue: api_call + - key: verification.endpoint + value: + stringValue: guardduty:ListDetectors + - key: verification.duration_ms + value: + intValue: 320 + + # Permission error example + - severityNumber: 17 + severityText: ERROR + body: + stringValue: "Permission check: aws/cloudtrail:GetTrailStatus - error" + attributes: + - key: policy.id + value: + stringValue: policy-1 + - key: policy.name + value: + stringValue: AWS Security Monitoring + - key: integration.id + value: + stringValue: int-cloudtrail-001 + - key: integration.name + value: + stringValue: AWS CloudTrail + - key: integration.type + value: + stringValue: aws_cloudtrail + - key: provider.type + value: + stringValue: aws + - key: provider.region + value: + stringValue: us-east-1 + - key: permission.action + value: + stringValue: cloudtrail:GetTrailStatus + - key: permission.category + value: + stringValue: management + - key: permission.status + value: + stringValue: error + - key: permission.required + value: + boolValue: false + - key: permission.error_code + value: + stringValue: TrailNotFoundException + - key: permission.error_message + value: + stringValue: "Trail not found" + - key: verification.method + value: + stringValue: api_call + - key: verification.endpoint + value: + stringValue: cloudtrail:GetTrailStatus + - key: verification.duration_ms + value: + intValue: 280 diff --git a/receiver/verifierreceiver/testdata/otel-config.yaml b/receiver/verifierreceiver/testdata/otel-config.yaml new file mode 100644 index 000000000..d15584d78 --- /dev/null +++ b/receiver/verifierreceiver/testdata/otel-config.yaml @@ -0,0 +1,27 @@ +# Minimal OTEL Collector test configuration for quick smoke tests +# Run with: ./_build/elastic-collector-components --config=./receiver/verifierreceiver/testdata/otel-config.yaml +# +# This config uses minimal settings - no AWS credentials configured. +# The receiver will emit logs with "error" status showing "VerifierNotInitialized". + +receivers: + verifier: + cloud_connector_id: "cc-test-minimal" + verification_id: "verify-minimal-001" + + policies: + - policy_id: "test-policy-local" + policy_name: "Minimal Test Policy" + integrations: + - integration_type: "aws_cloudtrail" + integration_name: "Test CloudTrail" + +exporters: + debug: + verbosity: detailed + +service: + pipelines: + logs: + receivers: [verifier] + exporters: [debug] diff --git a/receiver/verifierreceiver/testdata/standalone-test.yaml b/receiver/verifierreceiver/testdata/standalone-test.yaml new file mode 100644 index 000000000..1fdf2b07c --- /dev/null +++ b/receiver/verifierreceiver/testdata/standalone-test.yaml @@ -0,0 +1,90 @@ +# Standalone test configuration for the verifier receiver +# +# Run with: +# cd opentelemetry-collector-components +# make genelasticcol +# ./_build/elastic-collector-components --config ./receiver/verifierreceiver/testdata/standalone-test.yaml +# +# For AWS Cloud Connector authentication: +# 1. Set the role_arn to the IAM role ARN in the target AWS account +# 2. Set the external_id to the value from Cloud Connector setup +# +# Without provider credentials, the receiver will still run but report "error" status +# with "VerifierNotInitialized" for permissions requiring those providers. + +receivers: + verifier: + # Cloud Connector Identification + cloud_connector_id: "cc-test-12345" + cloud_connector_name: "Local Test Connector" + + # Verification Session + verification_id: "verify-local-001" + verification_type: "on_demand" + + # Provider Credentials + # Uncomment and configure with real values for production testing + providers: + # AWS Authentication - Cloud Connector STS AssumeRole + aws: + credentials: + # role_arn: "arn:aws:iam::123456789012:role/ElasticAgentRole" + # external_id: "your-external-id-from-elastic" + default_region: "us-east-1" + + # Azure Authentication (future) + # azure: + # credentials: + # tenant_id: "your-tenant-id" + # client_id: "your-client-id" + # client_secret: "your-client-secret" + # subscription_id: "your-subscription-id" + + # GCP Authentication (future) + # gcp: + # credentials: + # project_id: "your-project-id" + # use_default_credentials: true + + # Okta Authentication (future) + # okta: + # credentials: + # domain: "dev-123456.okta.com" + # api_token: "your-api-token" + + # Policies and Integrations + policies: + - policy_id: "policy-aws-security" + policy_name: "AWS Security Monitoring" + integrations: + - integration_id: "int-cloudtrail-001" + integration_type: "aws_cloudtrail" + integration_name: "AWS CloudTrail" + config: + account_id: "123456789012" + region: "us-east-1" + - integration_id: "int-guardduty-001" + integration_type: "aws_guardduty" + integration_name: "AWS GuardDuty" + config: + account_id: "123456789012" + region: "us-east-1" + - policy_id: "policy-aws-infra" + policy_name: "AWS Infrastructure" + integrations: + - integration_id: "int-s3-001" + integration_type: "aws_s3" + integration_name: "AWS S3 Logs" + config: + account_id: "123456789012" + region: "us-west-2" + +exporters: + debug: + verbosity: detailed + +service: + pipelines: + logs: + receivers: [verifier] + exporters: [debug] diff --git a/receiver/verifierreceiver/testdata/templates/verifier.yaml b/receiver/verifierreceiver/testdata/templates/verifier.yaml new file mode 100644 index 000000000..0c6355745 --- /dev/null +++ b/receiver/verifierreceiver/testdata/templates/verifier.yaml @@ -0,0 +1,33 @@ +# Verifier Integration Template +# This template defines how the verifier receiver should be configured +# when used as a Fleet integration. +# +# Variables are substituted by Fleet/Elastic Agent when the policy is applied: +# - ${var:cloud_connector_id} - Unique identifier for the Cloud Connector +# - ${var:verification_id} - Unique verification session ID +# - ${var:providers.aws.credentials.*} - AWS credentials for verification +# - ${var:policies} - List of policies with integrations to verify + +receivers: + verifier: + cloud_connector_id: ${var:cloud_connector_id} + cloud_connector_name: ${var:cloud_connector_name} + verification_id: ${var:verification_id} + verification_type: ${var:verification_type} + providers: + aws: + credentials: + role_arn: ${var:providers.aws.credentials.role_arn} + external_id: ${var:providers.aws.credentials.external_id} + default_region: ${var:providers.aws.credentials.default_region} + policies: ${var:policies} + +exporters: + debug: + verbosity: detailed + +service: + pipelines: + logs/verifier: + receivers: [verifier] + exporters: [debug] diff --git a/receiver/verifierreceiver/testdata/test-csp-profile.yaml b/receiver/verifierreceiver/testdata/test-csp-profile.yaml new file mode 100644 index 000000000..69d7174cc --- /dev/null +++ b/receiver/verifierreceiver/testdata/test-csp-profile.yaml @@ -0,0 +1,36 @@ +# Test configuration using AWS CSP profile +# Run with: AWS_PROFILE=csp ./_build/elastic-collector-components --config ./receiver/verifierreceiver/testdata/test-csp-profile.yaml + +receivers: + verifier: + cloud_connector_id: "cc-test-csp" + cloud_connector_name: "CSP Profile Test" + verification_id: "verify-csp-001" + verification_type: "on_demand" + + # Provider credentials - using default AWS credentials from environment/profile + providers: + aws: + credentials: + use_default_credentials: true + default_region: "us-east-1" + + policies: + - policy_id: "policy-aws-test" + policy_name: "AWS Test Policy" + integrations: + - integration_id: "int-cloudtrail-001" + integration_type: "aws_cloudtrail" + integration_name: "AWS CloudTrail" + config: + region: "us-east-1" + +exporters: + debug: + verbosity: detailed + +service: + pipelines: + logs: + receivers: [verifier] + exporters: [debug] From f221d8dc08703d76e6109ed78cb3539226e4a9e0 Mon Sep 17 00:00:00 2001 From: Evgeniy Belyi Date: Thu, 12 Feb 2026 09:37:14 -0600 Subject: [PATCH 2/7] proto gen --- receiver/verifierreceiver/internal/verifier/aws_verifier.go | 2 +- receiver/verifierreceiver/internal/verifier/verifier.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/receiver/verifierreceiver/internal/verifier/aws_verifier.go b/receiver/verifierreceiver/internal/verifier/aws_verifier.go index de5c33a55..e33e05bd0 100644 --- a/receiver/verifierreceiver/internal/verifier/aws_verifier.go +++ b/receiver/verifierreceiver/internal/verifier/aws_verifier.go @@ -15,7 +15,7 @@ // specific language governing permissions and limitations // under the License. -package verifier +package verifier // import "github.com/elastic/opentelemetry-collector-components/receiver/verifierreceiver/internal/verifier" import ( "context" diff --git a/receiver/verifierreceiver/internal/verifier/verifier.go b/receiver/verifierreceiver/internal/verifier/verifier.go index 5b5153526..0b8b5ba4d 100644 --- a/receiver/verifierreceiver/internal/verifier/verifier.go +++ b/receiver/verifierreceiver/internal/verifier/verifier.go @@ -18,7 +18,7 @@ // Package verifier provides permission verification for cloud providers. // It defines interfaces and types for verifying permissions across different // cloud providers (AWS, Azure, GCP) and identity providers (Okta, etc.). -package verifier +package verifier // import "github.com/elastic/opentelemetry-collector-components/receiver/verifierreceiver/internal/verifier" import ( "context" From f11b09532cdd772e26eeedb30b17540b2d0cb258 Mon Sep 17 00:00:00 2001 From: Evgeniy Belyi Date: Thu, 12 Feb 2026 13:51:44 -0600 Subject: [PATCH 3/7] versioning for integrations --- receiver/verifierreceiver/README.md | 24 ++ receiver/verifierreceiver/config.go | 5 + receiver/verifierreceiver/config_test.go | 19 ++ receiver/verifierreceiver/doc.go | 8 +- .../generated_component_test.go | 76 ++++++ .../generated_package_test.go | 11 +- receiver/verifierreceiver/go.mod | 1 + receiver/verifierreceiver/go.sum | 2 + .../internal/metadata/generated_logs.go | 109 +++++++++ .../internal/metadata/generated_logs_test.go | 82 +++++++ receiver/verifierreceiver/receiver.go | 36 ++- receiver/verifierreceiver/receiver_test.go | 89 ++++++- receiver/verifierreceiver/registry.go | 222 ++++++++++++++---- receiver/verifierreceiver/testdata/TESTING.md | 41 +++- .../testdata/agent-simulation.yaml | 4 + .../verifierreceiver/testdata/config.yaml | 3 + .../testdata/expected-logs.yaml | 9 + .../testdata/standalone-test.yaml | 3 + .../testdata/test-csp-profile.yaml | 1 + 19 files changed, 676 insertions(+), 69 deletions(-) create mode 100644 receiver/verifierreceiver/generated_component_test.go create mode 100644 receiver/verifierreceiver/internal/metadata/generated_logs.go create mode 100644 receiver/verifierreceiver/internal/metadata/generated_logs_test.go diff --git a/receiver/verifierreceiver/README.md b/receiver/verifierreceiver/README.md index 46b508b37..088cbd84f 100644 --- a/receiver/verifierreceiver/README.md +++ b/receiver/verifierreceiver/README.md @@ -74,12 +74,14 @@ receivers: - integration_id: "int-cloudtrail-001" integration_type: "aws_cloudtrail" integration_name: "AWS CloudTrail" + integration_version: "2.17.0" # Version-aware permissions config: account_id: "123456789012" region: "us-east-1" - integration_id: "int-guardduty-001" integration_type: "aws_guardduty" integration_name: "AWS GuardDuty" + integration_version: "1.5.0" config: account_id: "123456789012" region: "us-east-1" @@ -90,6 +92,7 @@ receivers: - integration_id: "int-ec2-001" integration_type: "aws_ec2" integration_name: "AWS EC2 Metrics" + # No integration_version - uses latest permission set config: account_id: "123456789012" ``` @@ -161,6 +164,7 @@ receivers: | `integration_id` | `string` | No | Unique identifier for the integration instance | | `integration_type` | `string` | Yes | Package/integration type (e.g., `aws_cloudtrail`) | | `integration_name` | `string` | No | Human-readable name of the integration | +| `integration_version` | `string` | No | Semantic version of the integration package (e.g., `2.17.0`). Different versions may require different permissions. When empty, the latest registered permission set is used. | | `config` | `map[string]interface{}` | No | Provider-specific configuration | ## Supported Integration Types @@ -235,6 +239,7 @@ The receiver emits OTEL logs following the RFC structure. Each log record repres | `integration.id` | Integration instance identifier | | `integration.name` | Integration name | | `integration.type` | Integration type (e.g., `aws_cloudtrail`) | +| `integration.version` | Integration package version (e.g., `2.17.0`) or `unspecified` | | `provider.type` | Provider type (`aws`, `azure`, `gcp`, `okta`) | | `provider.account` | Account identifier (if available) | | `provider.region` | Region (if available) | @@ -248,6 +253,24 @@ The receiver emits OTEL logs following the RFC structure. Each log record repres | `verification.endpoint` | API endpoint called for verification | | `verification.duration_ms` | Time taken for verification in milliseconds | +## Version-Aware Permissions + +The permission registry supports versioned permission sets per integration type. Different versions of an integration package may require different permissions (for example, a new version might add a required SQS permission for queue-based ingestion). + +### How It Works + +- Each integration type is registered with one or more semver constraints (e.g., `>=2.0.0`, `>=1.0.0,<2.0.0`) +- When `integration_version` is provided, the registry matches it against the constraints and returns the appropriate permission set +- When `integration_version` is omitted, the latest (first registered) permission set is used +- If the version does not match any constraint, a warning log with `permission.error_code: UnsupportedVersion` is emitted + +### Example: AWS CloudTrail Version Differences + +| Version Range | Change | +|---------------|--------| +| `>=2.0.0` | `sqs:ReceiveMessage` and `sqs:DeleteMessage` became **required** (queue-based ingestion is the default) | +| `>=1.0.0,<2.0.0` | `sqs:ReceiveMessage` and `sqs:DeleteMessage` are **optional** (direct S3 polling was the default) | + ## Architecture The receiver uses a registry-based architecture for extensibility: @@ -333,6 +356,7 @@ receivers: - integration_id: "int-cloudtrail-001" integration_type: "aws_cloudtrail" integration_name: "AWS CloudTrail" + integration_version: "2.17.0" config: region: "us-east-1" diff --git a/receiver/verifierreceiver/config.go b/receiver/verifierreceiver/config.go index 5c14dee22..eac263dcf 100644 --- a/receiver/verifierreceiver/config.go +++ b/receiver/verifierreceiver/config.go @@ -302,6 +302,11 @@ type IntegrationConfig struct { // IntegrationName is the human-readable name of the integration. IntegrationName string `mapstructure:"integration_name"` + // IntegrationVersion is the semantic version of the integration package (e.g., "2.17.0"). + // Different versions may require different permissions. When empty, the latest + // registered permission set is used. + IntegrationVersion string `mapstructure:"integration_version"` + // Config contains provider-specific configuration. // For AWS: may include regions, account_id, etc. Config map[string]interface{} `mapstructure:"config"` diff --git a/receiver/verifierreceiver/config_test.go b/receiver/verifierreceiver/config_test.go index a6bc1b70c..53d7babd3 100644 --- a/receiver/verifierreceiver/config_test.go +++ b/receiver/verifierreceiver/config_test.go @@ -64,6 +64,25 @@ func TestConfig_Validate(t *testing.T) { }, wantErr: "", }, + { + name: "valid config with integration version", + config: Config{ + CloudConnectorID: "cc-12345", + VerificationID: "verify-abc123", + Policies: []PolicyConfig{ + { + PolicyID: "policy-1", + Integrations: []IntegrationConfig{ + { + IntegrationType: "aws_cloudtrail", + IntegrationVersion: "2.17.0", + }, + }, + }, + }, + }, + wantErr: "", + }, { name: "valid config without AWS credentials (non-AWS integrations)", config: Config{ diff --git a/receiver/verifierreceiver/doc.go b/receiver/verifierreceiver/doc.go index f86db6aff..3b2d9a5d3 100644 --- a/receiver/verifierreceiver/doc.go +++ b/receiver/verifierreceiver/doc.go @@ -32,7 +32,9 @@ // # Architecture // // The receiver consists of two main registries: -// - Permission Registry: Maps integration types to required permissions +// - Permission Registry: Maps integration types and versions to required permissions. +// Each integration type can have multiple versioned permission sets matched via +// semver constraints (e.g., ">=2.0.0", ">=1.0.0,<2.0.0"). // - Verifier Registry: Manages provider-specific verifiers (AWS, Azure, etc.) // // Each verifier implements the Verifier interface and is responsible for @@ -44,7 +46,8 @@ // - Cloud Connector identification (ID, name) // - Verification session (ID, type) // - Provider credentials (AWS, Azure, GCP, Okta) -// - Policies containing integrations to verify +// - Policies containing integrations to verify, each with an optional +// integration_version for version-aware permission lookup // // Example: // @@ -61,6 +64,7 @@ // - policy_id: "policy-1" // integrations: // - integration_type: "aws_cloudtrail" +// integration_version: "2.17.0" // // # Output // diff --git a/receiver/verifierreceiver/generated_component_test.go b/receiver/verifierreceiver/generated_component_test.go new file mode 100644 index 000000000..0718d7e0e --- /dev/null +++ b/receiver/verifierreceiver/generated_component_test.go @@ -0,0 +1,76 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +// Code generated by mdatagen. DO NOT EDIT. + +package verifierreceiver + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/component/componenttest" + "go.opentelemetry.io/collector/confmap/confmaptest" + "go.opentelemetry.io/collector/consumer/consumertest" + "go.opentelemetry.io/collector/receiver" + "go.opentelemetry.io/collector/receiver/receivertest" +) + +var typ = component.MustNewType("verifier") + +func TestComponentFactoryType(t *testing.T) { + require.Equal(t, typ, NewFactory().Type()) +} + +func TestComponentConfigStruct(t *testing.T) { + require.NoError(t, componenttest.CheckConfigStruct(NewFactory().CreateDefaultConfig())) +} + +func TestComponentLifecycle(t *testing.T) { + factory := NewFactory() + + tests := []struct { + createFn func(ctx context.Context, set receiver.Settings, cfg component.Config) (component.Component, error) + name string + }{ + + { + name: "logs", + createFn: func(ctx context.Context, set receiver.Settings, cfg component.Config) (component.Component, error) { + return factory.CreateLogs(ctx, set, cfg, consumertest.NewNop()) + }, + }, + } + + cm, err := confmaptest.LoadConf("metadata.yaml") + require.NoError(t, err) + cfg := factory.CreateDefaultConfig() + sub, err := cm.Sub("tests::config") + require.NoError(t, err) + require.NoError(t, sub.Unmarshal(&cfg)) + + for _, tt := range tests { + t.Run(tt.name+"-shutdown", func(t *testing.T) { + c, err := tt.createFn(context.Background(), receivertest.NewNopSettings(typ), cfg) + require.NoError(t, err) + err = c.Shutdown(context.Background()) + require.NoError(t, err) + }) + } +} diff --git a/receiver/verifierreceiver/generated_package_test.go b/receiver/verifierreceiver/generated_package_test.go index 8fceb3e8f..ff9de353b 100644 --- a/receiver/verifierreceiver/generated_package_test.go +++ b/receiver/verifierreceiver/generated_package_test.go @@ -15,6 +15,8 @@ // specific language governing permissions and limitations // under the License. +// Code generated by mdatagen. DO NOT EDIT. + package verifierreceiver import ( @@ -24,12 +26,5 @@ import ( ) func TestMain(m *testing.M) { - // Ignore goroutines from AWS SDK HTTP client connections - goleak.VerifyTestMain(m, - goleak.IgnoreTopFunction("net/http.(*persistConn).writeLoop"), - goleak.IgnoreTopFunction("net/http.(*persistConn).readLoop"), - goleak.IgnoreTopFunction("net/http.(*persistConn).addTLS"), - goleak.IgnoreTopFunction("net/http.(*persistConn).roundTrip"), - goleak.IgnoreTopFunction("internal/poll.runtime_pollWait"), - ) + goleak.VerifyTestMain(m) } diff --git a/receiver/verifierreceiver/go.mod b/receiver/verifierreceiver/go.mod index 1d0a5d63e..b03f8324f 100644 --- a/receiver/verifierreceiver/go.mod +++ b/receiver/verifierreceiver/go.mod @@ -3,6 +3,7 @@ module github.com/elastic/opentelemetry-collector-components/receiver/verifierre go 1.24.0 require ( + github.com/Masterminds/semver/v3 v3.4.0 github.com/aws/aws-sdk-go-v2 v1.31.0 github.com/aws/aws-sdk-go-v2/config v1.27.33 github.com/aws/aws-sdk-go-v2/credentials v1.17.32 diff --git a/receiver/verifierreceiver/go.sum b/receiver/verifierreceiver/go.sum index 65472ed7a..cf5c2c32b 100644 --- a/receiver/verifierreceiver/go.sum +++ b/receiver/verifierreceiver/go.sum @@ -1,3 +1,5 @@ +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/aws/aws-sdk-go-v2 v1.31.0 h1:3V05LbxTSItI5kUqNwhJrrrY1BAXxXt0sN0l72QmG5U= github.com/aws/aws-sdk-go-v2 v1.31.0/go.mod h1:ztolYtaEUtdpf9Wftr31CJfLVjOnD/CVRkKOOYgF8hA= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.4 h1:70PVAiL15/aBMh5LThwgXdSQorVr91L127ttckI9QQU= diff --git a/receiver/verifierreceiver/internal/metadata/generated_logs.go b/receiver/verifierreceiver/internal/metadata/generated_logs.go new file mode 100644 index 000000000..3332753aa --- /dev/null +++ b/receiver/verifierreceiver/internal/metadata/generated_logs.go @@ -0,0 +1,109 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +// Code generated by mdatagen. DO NOT EDIT. + +package metadata + +import ( + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/plog" + "go.opentelemetry.io/collector/receiver" +) + +// LogsBuilder provides an interface for scrapers to report logs while taking care of all the transformations +// required to produce log representation defined in metadata and user config. +type LogsBuilder struct { + logsBuffer plog.Logs + logRecordsBuffer plog.LogRecordSlice + buildInfo component.BuildInfo // contains version information. +} + +// LogBuilderOption applies changes to default logs builder. +type LogBuilderOption interface { + apply(*LogsBuilder) +} + +func NewLogsBuilder(settings receiver.Settings) *LogsBuilder { + lb := &LogsBuilder{ + logsBuffer: plog.NewLogs(), + logRecordsBuffer: plog.NewLogRecordSlice(), + buildInfo: settings.BuildInfo, + } + + return lb +} + +// ResourceLogsOption applies changes to provided resource logs. +type ResourceLogsOption interface { + apply(plog.ResourceLogs) +} + +type resourceLogsOptionFunc func(plog.ResourceLogs) + +func (rlof resourceLogsOptionFunc) apply(rl plog.ResourceLogs) { + rlof(rl) +} + +// WithLogsResource sets the provided resource on the emitted ResourceLogs. +// It's recommended to use ResourceBuilder to create the resource. +func WithLogsResource(res pcommon.Resource) ResourceLogsOption { + return resourceLogsOptionFunc(func(rl plog.ResourceLogs) { + res.CopyTo(rl.Resource()) + }) +} + +// AppendLogRecord adds a log record to the logs builder. +func (lb *LogsBuilder) AppendLogRecord(lr plog.LogRecord) { + lr.MoveTo(lb.logRecordsBuffer.AppendEmpty()) +} + +// EmitForResource saves all the generated logs under a new resource and updates the internal state to be ready for +// recording another set of log records as part of another resource. This function can be helpful when one scraper +// needs to emit logs from several resources. Otherwise calling this function is not required, +// just `Emit` function can be called instead. +// Resource attributes should be provided as ResourceLogsOption arguments. +func (lb *LogsBuilder) EmitForResource(options ...ResourceLogsOption) { + rl := plog.NewResourceLogs() + ils := rl.ScopeLogs().AppendEmpty() + ils.Scope().SetName(ScopeName) + ils.Scope().SetVersion(lb.buildInfo.Version) + + for _, op := range options { + op.apply(rl) + } + + if lb.logRecordsBuffer.Len() > 0 { + lb.logRecordsBuffer.MoveAndAppendTo(ils.LogRecords()) + lb.logRecordsBuffer = plog.NewLogRecordSlice() + } + + if ils.LogRecords().Len() > 0 { + rl.MoveTo(lb.logsBuffer.ResourceLogs().AppendEmpty()) + } +} + +// Emit returns all the logs accumulated by the logs builder and updates the internal state to be ready for +// recording another set of logs. This function will be responsible for applying all the transformations required to +// produce logs representation defined in metadata and user config. +func (lb *LogsBuilder) Emit(options ...ResourceLogsOption) plog.Logs { + lb.EmitForResource(options...) + logs := lb.logsBuffer + lb.logsBuffer = plog.NewLogs() + return logs +} diff --git a/receiver/verifierreceiver/internal/metadata/generated_logs_test.go b/receiver/verifierreceiver/internal/metadata/generated_logs_test.go new file mode 100644 index 000000000..dfbf77a47 --- /dev/null +++ b/receiver/verifierreceiver/internal/metadata/generated_logs_test.go @@ -0,0 +1,82 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +// Code generated by mdatagen. DO NOT EDIT. + +package metadata + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/plog" + "go.opentelemetry.io/collector/receiver/receivertest" + "go.uber.org/zap" + "go.uber.org/zap/zaptest/observer" +) + +func TestLogsBuilderAppendLogRecord(t *testing.T) { + observedZapCore, _ := observer.New(zap.WarnLevel) + settings := receivertest.NewNopSettings(receivertest.NopType) + settings.Logger = zap.New(observedZapCore) + lb := NewLogsBuilder(settings) + + res := pcommon.NewResource() + + // append the first log record + lr := plog.NewLogRecord() + lr.SetTimestamp(pcommon.NewTimestampFromTime(time.Now())) + lr.Attributes().PutStr("type", "log") + lr.Body().SetStr("the first log record") + + // append the second log record + lr2 := plog.NewLogRecord() + lr2.SetTimestamp(pcommon.NewTimestampFromTime(time.Now())) + lr2.Attributes().PutStr("type", "event") + lr2.Body().SetStr("the second log record") + + lb.AppendLogRecord(lr) + lb.AppendLogRecord(lr2) + + logs := lb.Emit(WithLogsResource(res)) + assert.Equal(t, 1, logs.ResourceLogs().Len()) + + rl := logs.ResourceLogs().At(0) + assert.Equal(t, 1, rl.ScopeLogs().Len()) + + sl := rl.ScopeLogs().At(0) + assert.Equal(t, ScopeName, sl.Scope().Name()) + assert.Equal(t, lb.buildInfo.Version, sl.Scope().Version()) + + assert.Equal(t, 2, sl.LogRecords().Len()) + + attrVal, ok := sl.LogRecords().At(0).Attributes().Get("type") + assert.True(t, ok) + assert.Equal(t, "log", attrVal.Str()) + + assert.Equal(t, pcommon.ValueTypeStr, sl.LogRecords().At(0).Body().Type()) + assert.Equal(t, "the first log record", sl.LogRecords().At(0).Body().Str()) + + attrVal, ok = sl.LogRecords().At(1).Attributes().Get("type") + assert.True(t, ok) + assert.Equal(t, "event", attrVal.Str()) + + assert.Equal(t, pcommon.ValueTypeStr, sl.LogRecords().At(1).Body().Type()) + assert.Equal(t, "the second log record", sl.LogRecords().At(1).Body().Str()) +} diff --git a/receiver/verifierreceiver/receiver.go b/receiver/verifierreceiver/receiver.go index 34892b3e8..91ba93da7 100644 --- a/receiver/verifierreceiver/receiver.go +++ b/receiver/verifierreceiver/receiver.go @@ -266,12 +266,13 @@ func (r *verifierReceiver) verifyPermissions(ctx context.Context) error { zap.String("integration_id", integration.IntegrationID), zap.String("integration_type", integration.IntegrationType), zap.String("integration_name", integration.IntegrationName), + zap.String("integration_version", integration.IntegrationVersion), ) - // Look up required permissions from registry - integrationPerms := r.permissionRegistry.GetPermissions(integration.IntegrationType) + // Look up required permissions from registry (version-aware) + integrationPerms := r.permissionRegistry.GetPermissions(integration.IntegrationType, integration.IntegrationVersion) if integrationPerms == nil { - // Unknown integration type - emit a warning log + // Unknown integration type or unsupported version - emit a warning log r.emitUnsupportedIntegrationLog( scopeLogs, timestamp, @@ -439,6 +440,11 @@ func (r *verifierReceiver) emitPermissionCheckLog( attrs.PutStr("integration.name", integration.IntegrationName) } attrs.PutStr("integration.type", integration.IntegrationType) + if integration.IntegrationVersion != "" { + attrs.PutStr("integration.version", integration.IntegrationVersion) + } else { + attrs.PutStr("integration.version", "unspecified") + } // Provider context attrs.PutStr("provider.type", string(provider)) @@ -500,7 +506,19 @@ func (r *verifierReceiver) emitUnsupportedIntegrationLog( logRecord.SetSeverityNumber(plog.SeverityNumberWarn) logRecord.SetSeverityText("WARN") - body := fmt.Sprintf("Unsupported integration type: %s - skipping permission verification", integration.IntegrationType) + // Distinguish between unsupported type and unsupported version + var body string + var errorCode string + if r.permissionRegistry.IsSupported(integration.IntegrationType) { + // Type is known but version doesn't match any constraint + body = fmt.Sprintf("Unsupported integration version: %s@%s - skipping permission verification", + integration.IntegrationType, integration.IntegrationVersion) + errorCode = "UnsupportedVersion" + } else { + body = fmt.Sprintf("Unsupported integration type: %s - skipping permission verification", + integration.IntegrationType) + errorCode = "UnsupportedIntegration" + } logRecord.Body().SetStr(body) attrs := logRecord.Attributes() @@ -515,10 +533,18 @@ func (r *verifierReceiver) emitUnsupportedIntegrationLog( attrs.PutStr("integration.name", integration.IntegrationName) } attrs.PutStr("integration.type", integration.IntegrationType) + if integration.IntegrationVersion != "" { + attrs.PutStr("integration.version", integration.IntegrationVersion) + } else { + attrs.PutStr("integration.version", "unspecified") + } attrs.PutStr("permission.status", string(StatusSkipped)) + attrs.PutStr("permission.error_code", errorCode) - r.logger.Warn("Unsupported integration type", + r.logger.Warn("Unsupported integration", zap.String("integration_type", integration.IntegrationType), + zap.String("integration_version", integration.IntegrationVersion), + zap.String("error_code", errorCode), zap.String("policy_id", policy.PolicyID), ) } diff --git a/receiver/verifierreceiver/receiver_test.go b/receiver/verifierreceiver/receiver_test.go index 08021157f..127e48517 100644 --- a/receiver/verifierreceiver/receiver_test.go +++ b/receiver/verifierreceiver/receiver_test.go @@ -324,8 +324,8 @@ func TestReceiver_MultipleIntegrations(t *testing.T) { func TestPermissionRegistry(t *testing.T) { registry := NewPermissionRegistry() - t.Run("supported integration", func(t *testing.T) { - perms := registry.GetPermissions("aws_cloudtrail") + t.Run("supported integration - no version (latest)", func(t *testing.T) { + perms := registry.GetPermissions("aws_cloudtrail", "") require.NotNil(t, perms) assert.Equal(t, verifier.ProviderAWS, perms.Provider) assert.NotEmpty(t, perms.Permissions) @@ -343,7 +343,7 @@ func TestPermissionRegistry(t *testing.T) { }) t.Run("unsupported integration", func(t *testing.T) { - perms := registry.GetPermissions("unknown_integration") + perms := registry.GetPermissions("unknown_integration", "") assert.Nil(t, perms) assert.False(t, registry.IsSupported("unknown_integration")) }) @@ -364,7 +364,7 @@ func TestPermissionRegistry(t *testing.T) { for _, integration := range awsIntegrations { assert.True(t, registry.IsSupported(integration), "expected %s to be supported", integration) - perms := registry.GetPermissions(integration) + perms := registry.GetPermissions(integration, "") require.NotNil(t, perms, "expected permissions for %s", integration) assert.Equal(t, verifier.ProviderAWS, perms.Provider, "expected AWS provider for %s", integration) } @@ -379,7 +379,7 @@ func TestPermissionRegistry(t *testing.T) { for _, integration := range azureIntegrations { assert.True(t, registry.IsSupported(integration), "expected %s to be supported", integration) - perms := registry.GetPermissions(integration) + perms := registry.GetPermissions(integration, "") require.NotNil(t, perms, "expected permissions for %s", integration) assert.Equal(t, verifier.ProviderAzure, perms.Provider, "expected Azure provider for %s", integration) } @@ -394,7 +394,7 @@ func TestPermissionRegistry(t *testing.T) { for _, integration := range gcpIntegrations { assert.True(t, registry.IsSupported(integration), "expected %s to be supported", integration) - perms := registry.GetPermissions(integration) + perms := registry.GetPermissions(integration, "") require.NotNil(t, perms, "expected permissions for %s", integration) assert.Equal(t, verifier.ProviderGCP, perms.Provider, "expected GCP provider for %s", integration) } @@ -408,7 +408,7 @@ func TestPermissionRegistry(t *testing.T) { for _, integration := range oktaIntegrations { assert.True(t, registry.IsSupported(integration), "expected %s to be supported", integration) - perms := registry.GetPermissions(integration) + perms := registry.GetPermissions(integration, "") require.NotNil(t, perms, "expected permissions for %s", integration) assert.Equal(t, verifier.ProviderOkta, perms.Provider, "expected Okta provider for %s", integration) } @@ -421,4 +421,79 @@ func TestPermissionRegistry(t *testing.T) { assert.NotEmpty(t, byProvider[verifier.ProviderGCP]) assert.NotEmpty(t, byProvider[verifier.ProviderOkta]) }) + + // Version-aware permission lookup tests + t.Run("cloudtrail v2 - SQS permissions required", func(t *testing.T) { + perms := registry.GetPermissions("aws_cloudtrail", "2.17.0") + require.NotNil(t, perms) + assert.Equal(t, verifier.ProviderAWS, perms.Provider) + + // In v2+, sqs:ReceiveMessage and sqs:DeleteMessage should be required + for _, p := range perms.Permissions { + if p.Action == "sqs:ReceiveMessage" { + assert.True(t, p.Required, "sqs:ReceiveMessage should be required in v2+") + } + if p.Action == "sqs:DeleteMessage" { + assert.True(t, p.Required, "sqs:DeleteMessage should be required in v2+") + } + } + }) + + t.Run("cloudtrail v1 - SQS permissions optional", func(t *testing.T) { + perms := registry.GetPermissions("aws_cloudtrail", "1.5.0") + require.NotNil(t, perms) + assert.Equal(t, verifier.ProviderAWS, perms.Provider) + + // In v1.x, sqs:ReceiveMessage and sqs:DeleteMessage should be optional + for _, p := range perms.Permissions { + if p.Action == "sqs:ReceiveMessage" { + assert.False(t, p.Required, "sqs:ReceiveMessage should be optional in v1.x") + } + if p.Action == "sqs:DeleteMessage" { + assert.False(t, p.Required, "sqs:DeleteMessage should be optional in v1.x") + } + } + }) + + t.Run("cloudtrail no version - defaults to latest (v2+)", func(t *testing.T) { + perms := registry.GetPermissions("aws_cloudtrail", "") + require.NotNil(t, perms) + + // Should get v2+ permissions (latest) + for _, p := range perms.Permissions { + if p.Action == "sqs:ReceiveMessage" { + assert.True(t, p.Required, "default (latest) should have sqs:ReceiveMessage required") + } + } + }) + + t.Run("cloudtrail invalid version - falls back to latest", func(t *testing.T) { + perms := registry.GetPermissions("aws_cloudtrail", "not-a-version") + require.NotNil(t, perms) + // Should fall back to the first (latest) entry + for _, p := range perms.Permissions { + if p.Action == "sqs:ReceiveMessage" { + assert.True(t, p.Required, "invalid version should fall back to latest") + } + } + }) + + t.Run("guardduty with version - matches >=0.0.0", func(t *testing.T) { + perms := registry.GetPermissions("aws_guardduty", "3.0.0") + require.NotNil(t, perms) + assert.Equal(t, verifier.ProviderAWS, perms.Provider) + }) + + t.Run("version constraints are returned", func(t *testing.T) { + constraints := registry.GetVersionConstraints("aws_cloudtrail") + require.NotNil(t, constraints) + assert.Len(t, constraints, 2) + assert.Equal(t, ">=2.0.0", constraints[0]) + assert.Equal(t, ">=1.0.0,<2.0.0", constraints[1]) + }) + + t.Run("version constraints for unknown integration", func(t *testing.T) { + constraints := registry.GetVersionConstraints("unknown_integration") + assert.Nil(t, constraints) + }) } diff --git a/receiver/verifierreceiver/registry.go b/receiver/verifierreceiver/registry.go index d3994b9b3..26b69f554 100644 --- a/receiver/verifierreceiver/registry.go +++ b/receiver/verifierreceiver/registry.go @@ -18,6 +18,10 @@ package verifierreceiver // import "github.com/elastic/opentelemetry-collector-components/receiver/verifierreceiver" import ( + "fmt" + + "github.com/Masterminds/semver/v3" + "github.com/elastic/opentelemetry-collector-components/receiver/verifierreceiver/internal/verifier" ) @@ -73,16 +77,33 @@ type IntegrationPermissions struct { Permissions []Permission } +// VersionedPermissions associates a semver constraint with a set of integration permissions. +// The constraint string follows semver syntax (e.g., ">=2.0.0", ">=1.0.0,<2.0.0"). +type VersionedPermissions struct { + // ConstraintStr is the raw semver constraint string for display/logging. + ConstraintStr string + + // Constraint is the parsed semver constraint used for matching. + Constraint *semver.Constraints + + // Permissions is the permission set for integrations matching this constraint. + Permissions IntegrationPermissions +} + // PermissionRegistry maintains the mapping of integration types to their required permissions. // The receiver owns this mapping - Fleet API only provides the integration context. +// +// Each integration type can have multiple versioned permission sets. When looking up +// permissions, the registry matches the provided integration version against the +// registered semver constraints and returns the first match (newest-first order). type PermissionRegistry struct { - integrations map[string]IntegrationPermissions + integrations map[string][]VersionedPermissions } // NewPermissionRegistry creates a new permission registry with default mappings. func NewPermissionRegistry() *PermissionRegistry { registry := &PermissionRegistry{ - integrations: make(map[string]IntegrationPermissions), + integrations: make(map[string][]VersionedPermissions), } // Register all provider integrations @@ -94,19 +115,62 @@ func NewPermissionRegistry() *PermissionRegistry { return registry } -// GetPermissions returns the permissions required for an integration type. -// Returns nil if the integration type is not registered. -func (r *PermissionRegistry) GetPermissions(integrationType string) *IntegrationPermissions { - if perms, ok := r.integrations[integrationType]; ok { +// register adds a versioned permission set for an integration type. +// Entries should be registered newest-first so that the first entry serves as the +// default when no version is specified. The constraint string follows semver syntax +// (e.g., ">=2.0.0", ">=1.0.0,<2.0.0", ">=0.0.0"). +func (r *PermissionRegistry) register(integrationType string, constraintStr string, perms IntegrationPermissions) { + constraint, err := semver.NewConstraint(constraintStr) + if err != nil { + panic(fmt.Sprintf("invalid semver constraint %q for integration %q: %v", constraintStr, integrationType, err)) + } + + r.integrations[integrationType] = append(r.integrations[integrationType], VersionedPermissions{ + ConstraintStr: constraintStr, + Constraint: constraint, + Permissions: perms, + }) +} + +// GetPermissions returns the permissions required for an integration type and version. +// If version is empty, the first (latest) registered permission set is returned. +// If no constraint matches the version, nil is returned. +func (r *PermissionRegistry) GetPermissions(integrationType string, version string) *IntegrationPermissions { + entries, ok := r.integrations[integrationType] + if !ok || len(entries) == 0 { + return nil + } + + // If no version specified, return the first (latest) entry + if version == "" { + perms := entries[0].Permissions + return &perms + } + + // Parse the provided version + v, err := semver.NewVersion(version) + if err != nil { + // If the version string is not valid semver, fall back to the latest entry + perms := entries[0].Permissions return &perms } + + // Find the first matching constraint + for i := range entries { + if entries[i].Constraint.Check(v) { + perms := entries[i].Permissions + return &perms + } + } + + // No matching constraint found return nil } // IsSupported returns true if the integration type is registered in the registry. func (r *PermissionRegistry) IsSupported(integrationType string) bool { - _, ok := r.integrations[integrationType] - return ok + entries, ok := r.integrations[integrationType] + return ok && len(entries) > 0 } // SupportedIntegrations returns a list of all supported integration types. @@ -121,17 +185,83 @@ func (r *PermissionRegistry) SupportedIntegrations() []string { // SupportedIntegrationsByProvider returns integration types grouped by provider. func (r *PermissionRegistry) SupportedIntegrationsByProvider() map[verifier.ProviderType][]string { byProvider := make(map[verifier.ProviderType][]string) - for integrationType, perms := range r.integrations { - byProvider[perms.Provider] = append(byProvider[perms.Provider], integrationType) + for integrationType, entries := range r.integrations { + if len(entries) > 0 { + byProvider[entries[0].Permissions.Provider] = append(byProvider[entries[0].Permissions.Provider], integrationType) + } } return byProvider } +// GetVersionConstraints returns the version constraints registered for an integration type. +// Returns nil if the integration type is not registered. +func (r *PermissionRegistry) GetVersionConstraints(integrationType string) []string { + entries, ok := r.integrations[integrationType] + if !ok { + return nil + } + constraints := make([]string, len(entries)) + for i, entry := range entries { + constraints[i] = entry.ConstraintStr + } + return constraints +} + // registerAWSIntegrations registers all AWS-based integrations. func (r *PermissionRegistry) registerAWSIntegrations() { // AWS CloudTrail - commonly used for security auditing // https://www.elastic.co/docs/current/integrations/aws/cloudtrail - r.integrations["aws_cloudtrail"] = IntegrationPermissions{ + // + // v2.0.0+: Added sqs:DeleteMessage as required (queue-based ingestion became default) + r.register("aws_cloudtrail", ">=2.0.0", IntegrationPermissions{ + Provider: verifier.ProviderAWS, + Permissions: []Permission{ + { + Action: "cloudtrail:LookupEvents", + Required: true, + Method: MethodAPICall, + Category: "data_access", + }, + { + Action: "cloudtrail:DescribeTrails", + Required: true, + Method: MethodAPICall, + Category: "management", + }, + { + Action: "cloudtrail:GetTrailStatus", + Required: false, + Method: MethodAPICall, + Category: "management", + }, + { + Action: "s3:GetObject", + Required: true, + Method: MethodAPICall, + Category: "data_access", + }, + { + Action: "s3:ListBucket", + Required: true, + Method: MethodAPICall, + Category: "data_access", + }, + { + Action: "sqs:ReceiveMessage", + Required: true, + Method: MethodAPICall, + Category: "data_access", + }, + { + Action: "sqs:DeleteMessage", + Required: true, + Method: MethodAPICall, + Category: "data_access", + }, + }, + }) + // v1.x: Original permission set (SQS optional) + r.register("aws_cloudtrail", ">=1.0.0,<2.0.0", IntegrationPermissions{ Provider: verifier.ProviderAWS, Permissions: []Permission{ { @@ -177,10 +307,10 @@ func (r *PermissionRegistry) registerAWSIntegrations() { Category: "data_access", }, }, - } + }) // AWS GuardDuty - threat detection service - r.integrations["aws_guardduty"] = IntegrationPermissions{ + r.register("aws_guardduty", ">=0.0.0", IntegrationPermissions{ Provider: verifier.ProviderAWS, Permissions: []Permission{ { @@ -202,10 +332,10 @@ func (r *PermissionRegistry) registerAWSIntegrations() { Category: "data_access", }, }, - } + }) // AWS Security Hub - security findings aggregation - r.integrations["aws_securityhub"] = IntegrationPermissions{ + r.register("aws_securityhub", ">=0.0.0", IntegrationPermissions{ Provider: verifier.ProviderAWS, Permissions: []Permission{ { @@ -227,10 +357,10 @@ func (r *PermissionRegistry) registerAWSIntegrations() { Category: "management", }, }, - } + }) // AWS S3 - storage access logs - r.integrations["aws_s3"] = IntegrationPermissions{ + r.register("aws_s3", ">=0.0.0", IntegrationPermissions{ Provider: verifier.ProviderAWS, Permissions: []Permission{ { @@ -252,10 +382,10 @@ func (r *PermissionRegistry) registerAWSIntegrations() { Category: "management", }, }, - } + }) // AWS EC2 - compute instance metrics - r.integrations["aws_ec2"] = IntegrationPermissions{ + r.register("aws_ec2", ">=0.0.0", IntegrationPermissions{ Provider: verifier.ProviderAWS, Permissions: []Permission{ { @@ -277,10 +407,10 @@ func (r *PermissionRegistry) registerAWSIntegrations() { Category: "data_access", }, }, - } + }) // AWS VPC Flow Logs - r.integrations["aws_vpcflow"] = IntegrationPermissions{ + r.register("aws_vpcflow", ">=0.0.0", IntegrationPermissions{ Provider: verifier.ProviderAWS, Permissions: []Permission{ { @@ -308,10 +438,10 @@ func (r *PermissionRegistry) registerAWSIntegrations() { Category: "management", }, }, - } + }) // AWS WAF - Web Application Firewall logs - r.integrations["aws_waf"] = IntegrationPermissions{ + r.register("aws_waf", ">=0.0.0", IntegrationPermissions{ Provider: verifier.ProviderAWS, Permissions: []Permission{ { @@ -339,10 +469,10 @@ func (r *PermissionRegistry) registerAWSIntegrations() { Category: "data_access", }, }, - } + }) // AWS Route53 - DNS query logs - r.integrations["aws_route53"] = IntegrationPermissions{ + r.register("aws_route53", ">=0.0.0", IntegrationPermissions{ Provider: verifier.ProviderAWS, Permissions: []Permission{ { @@ -364,10 +494,10 @@ func (r *PermissionRegistry) registerAWSIntegrations() { Category: "management", }, }, - } + }) // AWS ELB - Elastic Load Balancer access logs - r.integrations["aws_elb"] = IntegrationPermissions{ + r.register("aws_elb", ">=0.0.0", IntegrationPermissions{ Provider: verifier.ProviderAWS, Permissions: []Permission{ { @@ -389,10 +519,10 @@ func (r *PermissionRegistry) registerAWSIntegrations() { Category: "management", }, }, - } + }) // AWS CloudFront - CDN access logs - r.integrations["aws_cloudfront"] = IntegrationPermissions{ + r.register("aws_cloudfront", ">=0.0.0", IntegrationPermissions{ Provider: verifier.ProviderAWS, Permissions: []Permission{ { @@ -414,14 +544,14 @@ func (r *PermissionRegistry) registerAWSIntegrations() { Category: "management", }, }, - } + }) } // registerAzureIntegrations registers all Azure-based integrations. // TODO: Implement Azure verifier and add actual permission mappings. func (r *PermissionRegistry) registerAzureIntegrations() { // Azure Activity Logs - r.integrations["azure_activitylogs"] = IntegrationPermissions{ + r.register("azure_activitylogs", ">=0.0.0", IntegrationPermissions{ Provider: verifier.ProviderAzure, Permissions: []Permission{ { @@ -431,10 +561,10 @@ func (r *PermissionRegistry) registerAzureIntegrations() { Category: "data_access", }, }, - } + }) // Azure Audit Logs - r.integrations["azure_auditlogs"] = IntegrationPermissions{ + r.register("azure_auditlogs", ">=0.0.0", IntegrationPermissions{ Provider: verifier.ProviderAzure, Permissions: []Permission{ { @@ -444,10 +574,10 @@ func (r *PermissionRegistry) registerAzureIntegrations() { Category: "data_access", }, }, - } + }) // Azure Blob Storage - r.integrations["azure_blob_storage"] = IntegrationPermissions{ + r.register("azure_blob_storage", ">=0.0.0", IntegrationPermissions{ Provider: verifier.ProviderAzure, Permissions: []Permission{ { @@ -457,14 +587,14 @@ func (r *PermissionRegistry) registerAzureIntegrations() { Category: "data_access", }, }, - } + }) } // registerGCPIntegrations registers all GCP-based integrations. // TODO: Implement GCP verifier and add actual permission mappings. func (r *PermissionRegistry) registerGCPIntegrations() { // GCP Audit Logs - r.integrations["gcp_audit"] = IntegrationPermissions{ + r.register("gcp_audit", ">=0.0.0", IntegrationPermissions{ Provider: verifier.ProviderGCP, Permissions: []Permission{ { @@ -474,10 +604,10 @@ func (r *PermissionRegistry) registerGCPIntegrations() { Category: "data_access", }, }, - } + }) // GCP Cloud Storage - r.integrations["gcp_storage"] = IntegrationPermissions{ + r.register("gcp_storage", ">=0.0.0", IntegrationPermissions{ Provider: verifier.ProviderGCP, Permissions: []Permission{ { @@ -493,10 +623,10 @@ func (r *PermissionRegistry) registerGCPIntegrations() { Category: "data_access", }, }, - } + }) // GCP Pub/Sub - r.integrations["gcp_pubsub"] = IntegrationPermissions{ + r.register("gcp_pubsub", ">=0.0.0", IntegrationPermissions{ Provider: verifier.ProviderGCP, Permissions: []Permission{ { @@ -506,14 +636,14 @@ func (r *PermissionRegistry) registerGCPIntegrations() { Category: "data_access", }, }, - } + }) } // registerOktaIntegrations registers all Okta-based integrations. // TODO: Implement Okta verifier and add actual permission mappings. func (r *PermissionRegistry) registerOktaIntegrations() { // Okta System Logs - r.integrations["okta_system"] = IntegrationPermissions{ + r.register("okta_system", ">=0.0.0", IntegrationPermissions{ Provider: verifier.ProviderOkta, Permissions: []Permission{ { @@ -523,10 +653,10 @@ func (r *PermissionRegistry) registerOktaIntegrations() { Category: "data_access", }, }, - } + }) // Okta User Events - r.integrations["okta_users"] = IntegrationPermissions{ + r.register("okta_users", ">=0.0.0", IntegrationPermissions{ Provider: verifier.ProviderOkta, Permissions: []Permission{ { @@ -536,5 +666,5 @@ func (r *PermissionRegistry) registerOktaIntegrations() { Category: "data_access", }, }, - } + }) } diff --git a/receiver/verifierreceiver/testdata/TESTING.md b/receiver/verifierreceiver/testdata/TESTING.md index c504f165f..129e24002 100644 --- a/receiver/verifierreceiver/testdata/TESTING.md +++ b/receiver/verifierreceiver/testdata/TESTING.md @@ -142,8 +142,10 @@ receivers: integrations: - integration_type: "aws_cloudtrail" integration_name: "CloudTrail" + integration_version: "2.17.0" - integration_type: "azure_activitylogs" integration_name: "Azure Activity" + integration_version: "1.0.0" - integration_type: "gcp_audit" integration_name: "GCP Audit" - integration_type: "okta_system" @@ -282,7 +284,44 @@ curl -X GET "localhost:9200/logs-cloud_connector.permission_verification-default }' ``` -## 10. Quick Smoke Test +## 10. Version-Aware Permission Testing + +The permission registry supports versioned permission sets. Different integration versions may require different permissions. + +### Test with a specific version: + +```yaml +policies: + - policy_id: "version-test" + policy_name: "Version Test" + integrations: + - integration_type: "aws_cloudtrail" + integration_version: "2.17.0" # v2+ requires SQS permissions + - integration_type: "aws_cloudtrail" + integration_version: "1.5.0" # v1.x has SQS as optional +``` + +### Expected behavior: +- `aws_cloudtrail` v2.17.0: `sqs:ReceiveMessage` and `sqs:DeleteMessage` are **required** +- `aws_cloudtrail` v1.5.0: `sqs:ReceiveMessage` and `sqs:DeleteMessage` are **optional** +- No version specified: uses the latest (v2+) permission set +- Invalid version string: falls back to the latest permission set +- Version that matches no constraint: emits a warning with `permission.error_code: UnsupportedVersion` + +### Unit tests for versioning: + +```bash +go test ./... -run TestPermissionRegistry -v +``` + +This runs all version-aware test cases including: +- `cloudtrail_v2_-_SQS_permissions_required` +- `cloudtrail_v1_-_SQS_permissions_optional` +- `cloudtrail_no_version_-_defaults_to_latest` +- `cloudtrail_invalid_version_-_falls_back_to_latest` +- `version_constraints_are_returned` + +## 11. Quick Smoke Test For a quick verification that everything compiles: diff --git a/receiver/verifierreceiver/testdata/agent-simulation.yaml b/receiver/verifierreceiver/testdata/agent-simulation.yaml index 02e2078ee..60caa8c38 100644 --- a/receiver/verifierreceiver/testdata/agent-simulation.yaml +++ b/receiver/verifierreceiver/testdata/agent-simulation.yaml @@ -30,11 +30,13 @@ receivers: - integration_id: "int-cloudtrail-001" integration_type: "aws_cloudtrail" integration_name: "AWS CloudTrail" + integration_version: "2.17.0" config: region: "us-east-1" - integration_id: "int-guardduty-001" integration_type: "aws_guardduty" integration_name: "AWS GuardDuty" + integration_version: "1.5.0" config: region: "us-east-1" @@ -44,11 +46,13 @@ receivers: - integration_id: "int-s3-001" integration_type: "aws_s3" integration_name: "AWS S3 Logs" + integration_version: "1.0.0" config: region: "us-west-2" - integration_id: "int-ec2-001" integration_type: "aws_ec2" integration_name: "AWS EC2 Metrics" + integration_version: "2.0.0" config: region: "us-east-1" diff --git a/receiver/verifierreceiver/testdata/config.yaml b/receiver/verifierreceiver/testdata/config.yaml index 6f517fdba..a45c0f2c4 100644 --- a/receiver/verifierreceiver/testdata/config.yaml +++ b/receiver/verifierreceiver/testdata/config.yaml @@ -22,12 +22,14 @@ receivers: - integration_id: "int-cloudtrail-001" integration_type: "aws_cloudtrail" integration_name: "AWS CloudTrail" + integration_version: "2.17.0" config: account_id: "123456789012" region: "us-east-1" - integration_id: "int-guardduty-001" integration_type: "aws_guardduty" integration_name: "AWS GuardDuty" + integration_version: "1.5.0" config: account_id: "123456789012" region: "us-east-1" @@ -37,6 +39,7 @@ receivers: - integration_id: "int-ec2-001" integration_type: "aws_ec2" integration_name: "AWS EC2 Metrics" + # No integration_version - will use latest permission set config: account_id: "123456789012" region: "us-west-2" diff --git a/receiver/verifierreceiver/testdata/expected-logs.yaml b/receiver/verifierreceiver/testdata/expected-logs.yaml index ec1593844..d89cab042 100644 --- a/receiver/verifierreceiver/testdata/expected-logs.yaml +++ b/receiver/verifierreceiver/testdata/expected-logs.yaml @@ -51,6 +51,9 @@ resourceLogs: - key: integration.type value: stringValue: aws_cloudtrail + - key: integration.version + value: + stringValue: "2.17.0" - key: provider.type value: stringValue: aws @@ -100,6 +103,9 @@ resourceLogs: - key: integration.type value: stringValue: aws_guardduty + - key: integration.version + value: + stringValue: "1.5.0" - key: provider.type value: stringValue: aws @@ -155,6 +161,9 @@ resourceLogs: - key: integration.type value: stringValue: aws_cloudtrail + - key: integration.version + value: + stringValue: "2.17.0" - key: provider.type value: stringValue: aws diff --git a/receiver/verifierreceiver/testdata/standalone-test.yaml b/receiver/verifierreceiver/testdata/standalone-test.yaml index 1fdf2b07c..474aac691 100644 --- a/receiver/verifierreceiver/testdata/standalone-test.yaml +++ b/receiver/verifierreceiver/testdata/standalone-test.yaml @@ -60,12 +60,14 @@ receivers: - integration_id: "int-cloudtrail-001" integration_type: "aws_cloudtrail" integration_name: "AWS CloudTrail" + integration_version: "2.17.0" config: account_id: "123456789012" region: "us-east-1" - integration_id: "int-guardduty-001" integration_type: "aws_guardduty" integration_name: "AWS GuardDuty" + integration_version: "1.5.0" config: account_id: "123456789012" region: "us-east-1" @@ -75,6 +77,7 @@ receivers: - integration_id: "int-s3-001" integration_type: "aws_s3" integration_name: "AWS S3 Logs" + integration_version: "1.0.0" config: account_id: "123456789012" region: "us-west-2" diff --git a/receiver/verifierreceiver/testdata/test-csp-profile.yaml b/receiver/verifierreceiver/testdata/test-csp-profile.yaml index 69d7174cc..b73c9ba22 100644 --- a/receiver/verifierreceiver/testdata/test-csp-profile.yaml +++ b/receiver/verifierreceiver/testdata/test-csp-profile.yaml @@ -22,6 +22,7 @@ receivers: - integration_id: "int-cloudtrail-001" integration_type: "aws_cloudtrail" integration_name: "AWS CloudTrail" + integration_version: "2.17.0" config: region: "us-east-1" From 0d7d34c9bc37afa9cbe2802084077c7809379a6f Mon Sep 17 00:00:00 2001 From: Evgeniy Belyi Date: Thu, 12 Feb 2026 14:14:52 -0600 Subject: [PATCH 4/7] update go.mod --- receiver/verifierreceiver/go.mod | 15 ++++++++++++--- receiver/verifierreceiver/go.sum | 30 ++++++++++++++++++++++++------ 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/receiver/verifierreceiver/go.mod b/receiver/verifierreceiver/go.mod index b03f8324f..dd3d55c5c 100644 --- a/receiver/verifierreceiver/go.mod +++ b/receiver/verifierreceiver/go.mod @@ -19,13 +19,14 @@ require ( github.com/stretchr/testify v1.11.1 go.opentelemetry.io/collector/component v1.44.0 go.opentelemetry.io/collector/component/componenttest v0.137.0 + go.opentelemetry.io/collector/confmap v1.51.0 go.opentelemetry.io/collector/consumer v1.44.0 go.opentelemetry.io/collector/consumer/consumertest v0.138.0 go.opentelemetry.io/collector/pdata v1.44.0 go.opentelemetry.io/collector/receiver v1.44.0 go.opentelemetry.io/collector/receiver/receivertest v0.137.0 go.uber.org/goleak v1.3.0 - go.uber.org/zap v1.27.0 + go.uber.org/zap v1.27.1 ) require ( @@ -44,18 +45,25 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-viper/mapstructure/v2 v2.5.0 // indirect + github.com/gobwas/glob v0.2.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/hashicorp/go-version v1.7.0 // indirect + github.com/hashicorp/go-version v1.8.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/knadh/koanf/maps v0.1.2 // indirect + github.com/knadh/koanf/providers/confmap v1.0.0 // indirect + github.com/knadh/koanf/v2 v2.3.2 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/pmezard/go-difflib v1.0.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/collector/consumer/consumererror v0.137.0 // indirect go.opentelemetry.io/collector/consumer/xconsumer v0.138.0 // indirect - go.opentelemetry.io/collector/featuregate v1.44.0 // indirect + go.opentelemetry.io/collector/featuregate v1.51.0 // indirect go.opentelemetry.io/collector/internal/telemetry v0.138.0 // indirect go.opentelemetry.io/collector/pdata/pprofile v0.138.0 // indirect go.opentelemetry.io/collector/pipeline v1.44.0 // indirect @@ -68,6 +76,7 @@ require ( go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect go.opentelemetry.io/otel/trace v1.38.0 // indirect go.uber.org/multierr v1.11.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/net v0.42.0 // indirect golang.org/x/sys v0.35.0 // indirect golang.org/x/text v0.27.0 // indirect diff --git a/receiver/verifierreceiver/go.sum b/receiver/verifierreceiver/go.sum index cf5c2c32b..f4b89ecd3 100644 --- a/receiver/verifierreceiver/go.sum +++ b/receiver/verifierreceiver/go.sum @@ -56,6 +56,10 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= @@ -65,8 +69,8 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= -github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4= +github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= @@ -75,10 +79,20 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo= +github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= +github.com/knadh/koanf/providers/confmap v1.0.0 h1:mHKLJTE7iXEys6deO5p6olAiZdG5zwp8Aebir+/EaRE= +github.com/knadh/koanf/providers/confmap v1.0.0/go.mod h1:txHYHiI2hAtF0/0sCmcuol4IDcuQbKTybiB1nOcUo1A= +github.com/knadh/koanf/v2 v2.3.2 h1:Ee6tuzQYFwcZXQpc2MiVeC6qHMandf5SMUJJNoFp/c4= +github.com/knadh/koanf/v2 v2.3.2/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -101,6 +115,8 @@ go.opentelemetry.io/collector/component v1.44.0 h1:SX5UO/gSDm+1zyvHVRFgpf8J1WP6U go.opentelemetry.io/collector/component v1.44.0/go.mod h1:geKbCTNoQfu55tOPiDuxLzNZsoO9//HRRg10/8WusWk= go.opentelemetry.io/collector/component/componenttest v0.137.0 h1:QC9MZsYyzQqN9qMlleJb78wf7FeCjbr4jLeCuNlKHLU= go.opentelemetry.io/collector/component/componenttest v0.137.0/go.mod h1:JuiX9pv7qE5G8keihhjM66LeidryEnziPND0sXuK9PQ= +go.opentelemetry.io/collector/confmap v1.51.0 h1:C9YlMNkIgzuauLpUz2F7DLlWwqAmkQKNcKj1XATVWuE= +go.opentelemetry.io/collector/confmap v1.51.0/go.mod h1:uWi4b9lHfvEC2poJ2I2vXwGUREVEQTcdUguOpfqdcHM= go.opentelemetry.io/collector/consumer v1.44.0 h1:vkKJTfQYBQNuKas0P1zv1zxJjHvmMa/n7d6GiSHT0aw= go.opentelemetry.io/collector/consumer v1.44.0/go.mod h1:t6u5+0FBUtyZLVFhVPgFabd4Iph7rP+b9VkxaY8dqXU= go.opentelemetry.io/collector/consumer/consumererror v0.137.0 h1:4HgYX6vVmaF17RRRtJDpR8EuWmLAv6JdKYG8slDDa+g= @@ -109,8 +125,8 @@ go.opentelemetry.io/collector/consumer/consumertest v0.138.0 h1:1PwWhjQ3msYhcml/ go.opentelemetry.io/collector/consumer/consumertest v0.138.0/go.mod h1:2XBKvZKVcF/7ts1Y+PxTgrQiBhXAnzMfT+1VKtzoDpQ= go.opentelemetry.io/collector/consumer/xconsumer v0.138.0 h1:peQ59TyBmt30lv4YH8gfBbTSJPuPIZW0kpFTfk45rVk= go.opentelemetry.io/collector/consumer/xconsumer v0.138.0/go.mod h1:ivpzDlwQowx8RTOZBPa281/4NvNBvhabm7JmeAbsGIU= -go.opentelemetry.io/collector/featuregate v1.44.0 h1:/GeGhTD8f+FNWS7C4w1Dj0Ui9Jp4v2WAdlXyW1p3uG8= -go.opentelemetry.io/collector/featuregate v1.44.0/go.mod h1:d0tiRzVYrytB6LkcYgz2ESFTv7OktRPQe0QEQcPt1L4= +go.opentelemetry.io/collector/featuregate v1.51.0 h1:dxJuv/3T84dhNKp7fz5+8srHz1dhquGzDpLW4OZTFBw= +go.opentelemetry.io/collector/featuregate v1.51.0/go.mod h1:/1bclXgP91pISaEeNulRxzzmzMTm4I5Xih2SnI4HRSo= go.opentelemetry.io/collector/internal/telemetry v0.138.0 h1:xHHYlPh1vVvr+ip0ct288l1joc4bsEeHh0rcY3WVXJo= go.opentelemetry.io/collector/internal/telemetry v0.138.0/go.mod h1:evqf71fdIMXdQEofbs1bVnBUzfF6zysLMLR9bEAS9Xw= go.opentelemetry.io/collector/pdata v1.44.0 h1:q/EfWDDKrSaf4hjTIzyPeg1ZcCRg1Uj7VTFnGfNVdk8= @@ -153,8 +169,10 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= From 2ab3c3f38e31f7dfb37731cd89599ab0c753e84d Mon Sep 17 00:00:00 2001 From: Evgeniy Belyi Date: Tue, 17 Feb 2026 16:49:12 -0600 Subject: [PATCH 5/7] Update aws_verifier.go --- .../internal/verifier/aws_verifier.go | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/receiver/verifierreceiver/internal/verifier/aws_verifier.go b/receiver/verifierreceiver/internal/verifier/aws_verifier.go index e33e05bd0..c2455134b 100644 --- a/receiver/verifierreceiver/internal/verifier/aws_verifier.go +++ b/receiver/verifierreceiver/internal/verifier/aws_verifier.go @@ -20,6 +20,7 @@ package verifier // import "github.com/elastic/opentelemetry-collector-component import ( "context" "errors" + "net/http" "strings" "time" @@ -50,6 +51,7 @@ type AWSVerifier struct { configured bool authConfig AWSAuthConfig defaultRegion string + httpClient *http.Client } // Ensure AWSVerifier implements Verifier interface. @@ -70,10 +72,19 @@ func NewAWSVerifierFactory() VerifierFactory { // NewAWSVerifier creates a new AWS verifier with Cloud Connector authentication. // It uses STS AssumeRole with the provided role ARN and external ID. func NewAWSVerifier(ctx context.Context, logger *zap.Logger, authConfig AWSAuthConfig) (*AWSVerifier, error) { + // Create a dedicated HTTP client so we can close idle connections on shutdown, + // preventing goroutine leaks from persistent HTTP connections. + httpClient := &http.Client{ + Transport: http.DefaultTransport.(*http.Transport).Clone(), + } + // Start with loading default config (for base credentials from IRSA, instance profile, etc.) - baseCfg, err := config.LoadDefaultConfig(ctx) + baseCfg, err := config.LoadDefaultConfig(ctx, + config.WithHTTPClient(httpClient), + ) if err != nil { logger.Warn("Failed to load default AWS config", zap.Error(err)) + httpClient.CloseIdleConnections() return &AWSVerifier{ logger: logger, configured: false, @@ -134,6 +145,7 @@ func NewAWSVerifier(ctx context.Context, logger *zap.Logger, authConfig AWSAuthC configured: true, authConfig: authConfig, defaultRegion: authConfig.DefaultRegion, + httpClient: httpClient, }, nil } @@ -142,8 +154,11 @@ func (v *AWSVerifier) ProviderType() ProviderType { return ProviderAWS } -// Close releases resources. +// Close releases resources, including closing idle HTTP connections. func (v *AWSVerifier) Close() error { + if v.httpClient != nil { + v.httpClient.CloseIdleConnections() + } return nil } From 8f047d8ea6215462c45b0582bf503391aa6273f0 Mon Sep 17 00:00:00 2001 From: Evgeniy Belyi Date: Tue, 17 Feb 2026 20:47:38 -0600 Subject: [PATCH 6/7] Context handling and refactoring --- receiver/verifierreceiver/config.go | 17 +- receiver/verifierreceiver/config_test.go | 20 ++ receiver/verifierreceiver/go.mod | 15 +- receiver/verifierreceiver/go.sum | 30 ++- .../internal/verifier/aws_verifier.go | 255 ++++++++++++++++-- .../internal/verifier/verifier.go | 1 + receiver/verifierreceiver/receiver.go | 9 +- receiver/verifierreceiver/receiver_test.go | 13 +- receiver/verifierreceiver/registry.go | 30 +-- 9 files changed, 313 insertions(+), 77 deletions(-) diff --git a/receiver/verifierreceiver/config.go b/receiver/verifierreceiver/config.go index eac263dcf..54734f198 100644 --- a/receiver/verifierreceiver/config.go +++ b/receiver/verifierreceiver/config.go @@ -20,6 +20,7 @@ package verifierreceiver // import "github.com/elastic/opentelemetry-collector-c import ( "errors" "fmt" + "strings" "github.com/elastic/opentelemetry-collector-components/receiver/verifierreceiver/internal/verifier" ) @@ -317,6 +318,9 @@ func (cfg *Config) Validate() error { if cfg.CloudConnectorID == "" { return errors.New("cloud_connector_id must be specified") } + if cfg.VerificationID == "" { + return errors.New("verification_id must be specified") + } if len(cfg.Policies) == 0 { return errors.New("at least one policy must be specified") } @@ -343,22 +347,17 @@ func (cfg *Config) Validate() error { // GetProviderForIntegration returns the provider type for a given integration type. func GetProviderForIntegration(integrationType string) verifier.ProviderType { - // AWS integrations start with "aws_" - if len(integrationType) > 4 && integrationType[:4] == "aws_" { + if strings.HasPrefix(integrationType, "aws_") { return verifier.ProviderAWS } - // Azure integrations start with "azure_" - if len(integrationType) > 6 && integrationType[:6] == "azure_" { + if strings.HasPrefix(integrationType, "azure_") { return verifier.ProviderAzure } - // GCP integrations start with "gcp_" - if len(integrationType) > 4 && integrationType[:4] == "gcp_" { + if strings.HasPrefix(integrationType, "gcp_") { return verifier.ProviderGCP } - // Okta integrations - if len(integrationType) >= 4 && integrationType[:4] == "okta" { + if strings.HasPrefix(integrationType, "okta_") { return verifier.ProviderOkta } - // Unknown provider return "" } diff --git a/receiver/verifierreceiver/config_test.go b/receiver/verifierreceiver/config_test.go index 53d7babd3..fd668ff57 100644 --- a/receiver/verifierreceiver/config_test.go +++ b/receiver/verifierreceiver/config_test.go @@ -131,6 +131,21 @@ func TestConfig_Validate(t *testing.T) { }, wantErr: "cloud_connector_id must be specified", }, + { + name: "invalid config - missing verification_id", + config: Config{ + CloudConnectorID: "cc-12345", + Policies: []PolicyConfig{ + { + PolicyID: "policy-1", + Integrations: []IntegrationConfig{ + {IntegrationType: "aws_cloudtrail"}, + }, + }, + }, + }, + wantErr: "verification_id must be specified", + }, { name: "invalid config - no policies", config: Config{ @@ -343,6 +358,11 @@ func TestGetProviderForIntegration(t *testing.T) { integrationType: "okta_system", want: verifier.ProviderOkta, }, + { + name: "Okta bare prefix does not match", + integrationType: "okta", + want: "", + }, { name: "Unknown", integrationType: "unknown_integration", diff --git a/receiver/verifierreceiver/go.mod b/receiver/verifierreceiver/go.mod index dd3d55c5c..4df80dcdd 100644 --- a/receiver/verifierreceiver/go.mod +++ b/receiver/verifierreceiver/go.mod @@ -4,18 +4,23 @@ go 1.24.0 require ( github.com/Masterminds/semver/v3 v3.4.0 - github.com/aws/aws-sdk-go-v2 v1.31.0 + github.com/aws/aws-sdk-go-v2 v1.41.1 github.com/aws/aws-sdk-go-v2/config v1.27.33 github.com/aws/aws-sdk-go-v2/credentials v1.17.32 + github.com/aws/aws-sdk-go-v2/service/cloudfront v1.60.0 github.com/aws/aws-sdk-go-v2/service/cloudtrail v1.43.2 github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.40.7 + github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.63.1 github.com/aws/aws-sdk-go-v2/service/ec2 v1.177.2 + github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.54.6 github.com/aws/aws-sdk-go-v2/service/guardduty v1.48.2 + github.com/aws/aws-sdk-go-v2/service/route53 v1.62.1 github.com/aws/aws-sdk-go-v2/service/s3 v1.61.2 github.com/aws/aws-sdk-go-v2/service/securityhub v1.52.2 github.com/aws/aws-sdk-go-v2/service/sqs v1.34.7 github.com/aws/aws-sdk-go-v2/service/sts v1.30.7 - github.com/aws/smithy-go v1.21.0 + github.com/aws/aws-sdk-go-v2/service/wafv2 v1.70.7 + github.com/aws/smithy-go v1.24.0 github.com/stretchr/testify v1.11.1 go.opentelemetry.io/collector/component v1.44.0 go.opentelemetry.io/collector/component/componenttest v0.137.0 @@ -30,10 +35,10 @@ require ( ) require ( - github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.4 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.18 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.18 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.17 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 // indirect diff --git a/receiver/verifierreceiver/go.sum b/receiver/verifierreceiver/go.sum index f4b89ecd3..6894af6af 100644 --- a/receiver/verifierreceiver/go.sum +++ b/receiver/verifierreceiver/go.sum @@ -1,29 +1,35 @@ github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= -github.com/aws/aws-sdk-go-v2 v1.31.0 h1:3V05LbxTSItI5kUqNwhJrrrY1BAXxXt0sN0l72QmG5U= -github.com/aws/aws-sdk-go-v2 v1.31.0/go.mod h1:ztolYtaEUtdpf9Wftr31CJfLVjOnD/CVRkKOOYgF8hA= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.4 h1:70PVAiL15/aBMh5LThwgXdSQorVr91L127ttckI9QQU= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.4/go.mod h1:/MQxMqci8tlqDH+pjmoLu1i0tbWCUP1hhyMRuFxpQCw= +github.com/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU= +github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4= github.com/aws/aws-sdk-go-v2/config v1.27.33 h1:Nof9o/MsmH4oa0s2q9a0k7tMz5x/Yj5k06lDODWz3BU= github.com/aws/aws-sdk-go-v2/config v1.27.33/go.mod h1:kEqdYzRb8dd8Sy2pOdEbExTTF5v7ozEXX0McgPE7xks= github.com/aws/aws-sdk-go-v2/credentials v1.17.32 h1:7Cxhp/BnT2RcGy4VisJ9miUPecY+lyE9I8JvcZofn9I= github.com/aws/aws-sdk-go-v2/credentials v1.17.32/go.mod h1:P5/QMF3/DCHbXGEGkdbilXHsyTBX5D3HSwcrSc9p20I= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13 h1:pfQ2sqNpMVK6xz2RbqLEL0GH87JOwSxPV2rzm8Zsb74= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13/go.mod h1:NG7RXPUlqfsCLLFfi0+IpKN4sCB9D9fw/qTaSB+xRoU= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.18 h1:kYQ3H1u0ANr9KEKlGs/jTLrBFPo8P8NaH/w7A01NeeM= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.18/go.mod h1:r506HmK5JDUh9+Mw4CfGJGSSoqIiLCndAuqXuhbv67Y= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.18 h1:Z7IdFUONvTcvS7YuhtVxN99v2cCoHRXOS4mTr0B/pUc= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.18/go.mod h1:DkKMmksZVVyat+Y+r1dEOgJEfUeA7UngIHWeKsi0yNc= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 h1:xOLELNKGp2vsiteLsvLPwxC+mYmO6OZ8PYgiuPJzF8U= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17/go.mod h1:5M5CI3D12dNOtH3/mk6minaRwI2/37ifCURZISxA/IQ= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 h1:WWLqlh79iO48yLkj1v3ISRNiv+3KdQoZ6JWyfcsyQik= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17/go.mod h1:EhG22vHRrvF8oXSTYStZhJc1aUgKtnJe+aOiFEV90cM= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.17 h1:Roo69qTpfu8OlJ2Tb7pAYVuF0CpuUMB0IYWwYP/4DZM= github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.17/go.mod h1:NcWPxQzGM1USQggaTVwz6VpqMZPX1CvDJLDh6jnOCa4= +github.com/aws/aws-sdk-go-v2/service/cloudfront v1.60.0 h1:RUQqU9L1LnFJ+9t5hsSB7GI6dVvJDCnG4WgRlDeHK6E= +github.com/aws/aws-sdk-go-v2/service/cloudfront v1.60.0/go.mod h1:9Hd/cqshF4zl13KGLkWtRfITbvKR6m6FZHwhL2BYDSY= github.com/aws/aws-sdk-go-v2/service/cloudtrail v1.43.2 h1:sLoUkwhrhogwbnQ2/nsc1MT3dia7krZHHwCMbFyYGbo= github.com/aws/aws-sdk-go-v2/service/cloudtrail v1.43.2/go.mod h1:ODEcuhq+MDaWP9fpgCPcYMKE12pyK5g5W2U0z0nHEiI= github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.40.7 h1:G8JC8KCrNiQiyK61CYyzRDixCb+XNktVcaQzlG95yJI= github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.40.7/go.mod h1:HeDvLYJALo05N6wCx3Ufa1rHGL1mz9ON312O2yVclIs= +github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.63.1 h1:l65dmgr7tO26EcHe6WMdseRnFLoJ2nqdkPz1nJdXfaw= +github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.63.1/go.mod h1:wvnXh1w1pGS2UpEvPTKSjXYuxiXhuvob/IMaK2AWvek= github.com/aws/aws-sdk-go-v2/service/ec2 v1.177.2 h1:QUUvxEs9q1DsYCaWaRrV8i7n82Adm34jrHb6OPjXPqc= github.com/aws/aws-sdk-go-v2/service/ec2 v1.177.2/go.mod h1:TFSALWR7Xs7+KyMM87ZAYxncKFBvzEt2rpK/BJCH2ps= +github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.54.6 h1:fQR1aeZKaiPkNPya0JMy2nhsoqoSgIWc3/QTiTiL1K0= +github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.54.6/go.mod h1:oJRLDix51wqBDlP9dv+blFkvvf7HESolQz5cdhdmV4A= github.com/aws/aws-sdk-go-v2/service/guardduty v1.48.2 h1:F7iPMAIiEX5xqUEhbeflkREaforxmuIkobZi9apGFKc= github.com/aws/aws-sdk-go-v2/service/guardduty v1.48.2/go.mod h1:yL5DOvh8huFx2ZwB9kj20TnZ5DQJjnoCYUkFitas/2k= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 h1:KypMCbLPPHEmf9DgMGw51jMj77VfGPAN2Kv4cfhlfgI= @@ -34,6 +40,8 @@ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19 h1:rfprUlsd github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19/go.mod h1:SCWkEdRq8/7EK60NcvvQ6NXKuTcchAD4ROAsC37VEZE= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.17 h1:u+EfGmksnJc/x5tq3A+OD7LrMbSSR/5TrKLvkdy/fhY= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.17/go.mod h1:VaMx6302JHax2vHJWgRo+5n9zvbacs3bLU/23DNQrTY= +github.com/aws/aws-sdk-go-v2/service/route53 v1.62.1 h1:1jIdwWOulae7bBLIgB36OZ0DINACb1wxM6wdGlx4eHE= +github.com/aws/aws-sdk-go-v2/service/route53 v1.62.1/go.mod h1:tE2zGlMIlxWv+7Otap7ctRp3qeKqtnja7DZguj3Vu/Y= github.com/aws/aws-sdk-go-v2/service/s3 v1.61.2 h1:Kp6PWAlXwP1UvIflkIP6MFZYBNDCa4mFCGtxrpICVOg= github.com/aws/aws-sdk-go-v2/service/s3 v1.61.2/go.mod h1:5FmD/Dqq57gP+XwaUnd5WFPipAuzrf0HmupX27Gvjvc= github.com/aws/aws-sdk-go-v2/service/securityhub v1.52.2 h1:sO8Z9YGxpvPtXsVF0UBBgNOMeEZq2H/GRBdZxTBfEbE= @@ -46,8 +54,10 @@ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.7 h1:/Cfdu0XV3mONYKaOt1Gr0k1K github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.7/go.mod h1:bCbAxKDqNvkHxRaIMnyVPXPo+OaPRwvmgzMxbz1VKSA= github.com/aws/aws-sdk-go-v2/service/sts v1.30.7 h1:NKTa1eqZYw8tiHSRGpP0VtTdub/8KNk8sDkNPFaOKDE= github.com/aws/aws-sdk-go-v2/service/sts v1.30.7/go.mod h1:NXi1dIAGteSaRLqYgarlhP/Ij0cFT+qmCwiJqWh/U5o= -github.com/aws/smithy-go v1.21.0 h1:H7L8dtDRk0P1Qm6y0ji7MCYMQObJ5R9CRpyPhRUkLYA= -github.com/aws/smithy-go v1.21.0/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= +github.com/aws/aws-sdk-go-v2/service/wafv2 v1.70.7 h1:WXGcHbw0n/WGrp2mLxDImYsPeQFdrd3wUk1dNI8d5QI= +github.com/aws/aws-sdk-go-v2/service/wafv2 v1.70.7/go.mod h1:5M/5JdJM11qAE+yQSPlDzcoDpjckAkWTf4cl6INnOE8= +github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk= +github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/receiver/verifierreceiver/internal/verifier/aws_verifier.go b/receiver/verifierreceiver/internal/verifier/aws_verifier.go index c2455134b..06852f686 100644 --- a/receiver/verifierreceiver/internal/verifier/aws_verifier.go +++ b/receiver/verifierreceiver/internal/verifier/aws_verifier.go @@ -27,14 +27,20 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/credentials/stscreds" + "github.com/aws/aws-sdk-go-v2/service/cloudfront" "github.com/aws/aws-sdk-go-v2/service/cloudtrail" "github.com/aws/aws-sdk-go-v2/service/cloudwatch" + "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs" "github.com/aws/aws-sdk-go-v2/service/ec2" + "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" "github.com/aws/aws-sdk-go-v2/service/guardduty" + "github.com/aws/aws-sdk-go-v2/service/route53" "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/aws/aws-sdk-go-v2/service/securityhub" "github.com/aws/aws-sdk-go-v2/service/sqs" "github.com/aws/aws-sdk-go-v2/service/sts" + "github.com/aws/aws-sdk-go-v2/service/wafv2" + wafv2types "github.com/aws/aws-sdk-go-v2/service/wafv2/types" "github.com/aws/smithy-go" "go.uber.org/zap" ) @@ -220,6 +226,14 @@ func (v *AWSVerifier) Verify(ctx context.Context, permission Permission, provide result = v.verifySQS(ctx, cfg, operation) case "logs": result = v.verifyCloudWatchLogs(ctx, cfg, operation) + case "wafv2": + result = v.verifyWAFv2(ctx, cfg, operation) + case "route53": + result = v.verifyRoute53(ctx, cfg, operation) + case "elasticloadbalancing": + result = v.verifyELB(ctx, cfg, operation) + case "cloudfront": + result = v.verifyCloudFront(ctx, cfg, operation) default: result = Result{ Status: StatusSkipped, @@ -283,34 +297,46 @@ func (v *AWSVerifier) verifyGuardDuty(ctx context.Context, cfg aws.Config, opera }) return v.handleAWSError(err, "guardduty:ListDetectors") - case "GetFindings", "ListFindings": - // First get a detector ID + case "GetFindings": detectors, err := client.ListDetectors(ctx, &guardduty.ListDetectorsInput{ MaxResults: aws.Int32(1), }) if err != nil { - return v.handleAWSError(err, "guardduty:"+operation) + return v.handleAWSError(err, "guardduty:GetFindings") } if len(detectors.DetectorIds) == 0 { return Result{ Status: StatusGranted, - Endpoint: "guardduty:" + operation + " (no detectors configured)", + Endpoint: "guardduty:GetFindings (no detectors configured)", } } + // Call GetFindings with an empty finding IDs list. This exercises the + // guardduty:GetFindings IAM permission and returns an empty result set + // rather than an error. + _, err = client.GetFindings(ctx, &guardduty.GetFindingsInput{ + DetectorId: aws.String(detectors.DetectorIds[0]), + FindingIds: []string{}, + }) + return v.handleAWSError(err, "guardduty:GetFindings") - if operation == "ListFindings" { - _, err = client.ListFindings(ctx, &guardduty.ListFindingsInput{ - DetectorId: aws.String(detectors.DetectorIds[0]), - MaxResults: aws.Int32(1), - }) - } else { - // GetFindings requires finding IDs, so we'll use ListFindings as proxy - _, err = client.ListFindings(ctx, &guardduty.ListFindingsInput{ - DetectorId: aws.String(detectors.DetectorIds[0]), - MaxResults: aws.Int32(1), - }) + case "ListFindings": + detectors, err := client.ListDetectors(ctx, &guardduty.ListDetectorsInput{ + MaxResults: aws.Int32(1), + }) + if err != nil { + return v.handleAWSError(err, "guardduty:ListFindings") } - return v.handleAWSError(err, "guardduty:"+operation) + if len(detectors.DetectorIds) == 0 { + return Result{ + Status: StatusGranted, + Endpoint: "guardduty:ListFindings (no detectors configured)", + } + } + _, err = client.ListFindings(ctx, &guardduty.ListFindingsInput{ + DetectorId: aws.String(detectors.DetectorIds[0]), + MaxResults: aws.Int32(1), + }) + return v.handleAWSError(err, "guardduty:ListFindings") default: return Result{ @@ -356,21 +382,47 @@ func (v *AWSVerifier) verifyS3(ctx context.Context, cfg aws.Config, operation st switch operation { case "ListBucket": - // ListBuckets is a simpler permission check - _, err := client.ListBuckets(ctx, &s3.ListBucketsInput{}) + // Use HeadBucket on a known bucket to verify s3:ListBucket. + // ListBuckets checks s3:ListAllMyBuckets which is a different permission. + // If no specific bucket is available, fall back to ListBuckets as a basic connectivity check. + buckets, err := client.ListBuckets(ctx, &s3.ListBucketsInput{}) + if err != nil { + return v.handleAWSError(err, "s3:ListBucket") + } + if len(buckets.Buckets) == 0 { + return Result{ + Status: StatusGranted, + Endpoint: "s3:ListBucket (no buckets to check, ListBuckets succeeded)", + } + } + _, err = client.HeadBucket(ctx, &s3.HeadBucketInput{ + Bucket: buckets.Buckets[0].Name, + }) return v.handleAWSError(err, "s3:ListBucket") case "GetObject": - // GetObject requires a bucket and key - use ListBuckets as proxy - _, err := client.ListBuckets(ctx, &s3.ListBucketsInput{}) + // s3:GetObject is bucket/key-specific and cannot be fully verified without + // a target object. Use HeadBucket as a proxy to confirm the role has some + // level of S3 access to the account's buckets. + buckets, err := client.ListBuckets(ctx, &s3.ListBucketsInput{}) + if err != nil { + return v.handleAWSError(err, "s3:GetObject") + } + if len(buckets.Buckets) == 0 { + return Result{ + Status: StatusSkipped, + Endpoint: "s3:GetObject (no buckets available for verification)", + } + } + _, err = client.HeadBucket(ctx, &s3.HeadBucketInput{ + Bucket: buckets.Buckets[0].Name, + }) if err != nil { return v.handleAWSError(err, "s3:GetObject") } - // If we can list buckets, we have basic S3 access - // The actual GetObject permission is bucket-specific return Result{ Status: StatusGranted, - Endpoint: "s3:GetObject (verified via ListBuckets)", + Endpoint: "s3:GetObject (verified via HeadBucket - full verification requires bucket/key)", } case "GetBucketLocation": @@ -474,11 +526,158 @@ func (v *AWSVerifier) verifySQS(ctx context.Context, cfg aws.Config, operation s // verifyCloudWatchLogs verifies CloudWatch Logs permissions. func (v *AWSVerifier) verifyCloudWatchLogs(ctx context.Context, cfg aws.Config, operation string) Result { - // CloudWatch Logs uses the same SDK client pattern - // For now, skip - would need to add cloudwatchlogs client - return Result{ - Status: StatusSkipped, - ErrorMessage: "CloudWatch Logs verification not yet implemented", + client := cloudwatchlogs.NewFromConfig(cfg) + + switch operation { + case "FilterLogEvents": + // FilterLogEvents requires a log group; use DescribeLogGroups to find one. + groups, err := client.DescribeLogGroups(ctx, &cloudwatchlogs.DescribeLogGroupsInput{ + Limit: aws.Int32(1), + }) + if err != nil { + return v.handleAWSError(err, "logs:FilterLogEvents") + } + if len(groups.LogGroups) == 0 { + return Result{ + Status: StatusGranted, + Endpoint: "logs:FilterLogEvents (no log groups to check)", + } + } + _, err = client.FilterLogEvents(ctx, &cloudwatchlogs.FilterLogEventsInput{ + LogGroupName: groups.LogGroups[0].LogGroupName, + Limit: aws.Int32(1), + }) + return v.handleAWSError(err, "logs:FilterLogEvents") + + case "DescribeLogGroups": + _, err := client.DescribeLogGroups(ctx, &cloudwatchlogs.DescribeLogGroupsInput{ + Limit: aws.Int32(1), + }) + return v.handleAWSError(err, "logs:DescribeLogGroups") + + case "DescribeLogStreams": + groups, err := client.DescribeLogGroups(ctx, &cloudwatchlogs.DescribeLogGroupsInput{ + Limit: aws.Int32(1), + }) + if err != nil { + return v.handleAWSError(err, "logs:DescribeLogStreams") + } + if len(groups.LogGroups) == 0 { + return Result{ + Status: StatusGranted, + Endpoint: "logs:DescribeLogStreams (no log groups to check)", + } + } + _, err = client.DescribeLogStreams(ctx, &cloudwatchlogs.DescribeLogStreamsInput{ + LogGroupName: groups.LogGroups[0].LogGroupName, + Limit: aws.Int32(1), + }) + return v.handleAWSError(err, "logs:DescribeLogStreams") + + default: + return Result{ + Status: StatusSkipped, + ErrorMessage: "Unsupported CloudWatch Logs operation: " + operation, + } + } +} + +// verifyWAFv2 verifies WAFv2 permissions. +func (v *AWSVerifier) verifyWAFv2(ctx context.Context, cfg aws.Config, operation string) Result { + client := wafv2.NewFromConfig(cfg) + + switch operation { + case "ListWebACLs": + _, err := client.ListWebACLs(ctx, &wafv2.ListWebACLsInput{ + Scope: wafv2types.ScopeRegional, + Limit: aws.Int32(1), + }) + return v.handleAWSError(err, "wafv2:ListWebACLs") + + case "GetWebACL": + // GetWebACL requires a WebACL ID; list first to find one. + acls, err := client.ListWebACLs(ctx, &wafv2.ListWebACLsInput{ + Scope: wafv2types.ScopeRegional, + Limit: aws.Int32(1), + }) + if err != nil { + return v.handleAWSError(err, "wafv2:GetWebACL") + } + if len(acls.WebACLs) == 0 { + return Result{ + Status: StatusGranted, + Endpoint: "wafv2:GetWebACL (no WebACLs to check)", + } + } + _, err = client.GetWebACL(ctx, &wafv2.GetWebACLInput{ + Name: acls.WebACLs[0].Name, + Id: acls.WebACLs[0].Id, + Scope: wafv2types.ScopeRegional, + }) + return v.handleAWSError(err, "wafv2:GetWebACL") + + default: + return Result{ + Status: StatusSkipped, + ErrorMessage: "Unsupported WAFv2 operation: " + operation, + } + } +} + +// verifyRoute53 verifies Route 53 permissions. +func (v *AWSVerifier) verifyRoute53(ctx context.Context, cfg aws.Config, operation string) Result { + client := route53.NewFromConfig(cfg) + + switch operation { + case "ListHostedZones": + _, err := client.ListHostedZones(ctx, &route53.ListHostedZonesInput{ + MaxItems: aws.Int32(1), + }) + return v.handleAWSError(err, "route53:ListHostedZones") + + default: + return Result{ + Status: StatusSkipped, + ErrorMessage: "Unsupported Route53 operation: " + operation, + } + } +} + +// verifyELB verifies Elastic Load Balancing permissions. +func (v *AWSVerifier) verifyELB(ctx context.Context, cfg aws.Config, operation string) Result { + client := elasticloadbalancingv2.NewFromConfig(cfg) + + switch operation { + case "DescribeLoadBalancers": + _, err := client.DescribeLoadBalancers(ctx, &elasticloadbalancingv2.DescribeLoadBalancersInput{ + PageSize: aws.Int32(1), + }) + return v.handleAWSError(err, "elasticloadbalancing:DescribeLoadBalancers") + + default: + return Result{ + Status: StatusSkipped, + ErrorMessage: "Unsupported ELB operation: " + operation, + } + } +} + +// verifyCloudFront verifies CloudFront permissions. +func (v *AWSVerifier) verifyCloudFront(ctx context.Context, cfg aws.Config, operation string) Result { + client := cloudfront.NewFromConfig(cfg) + + switch operation { + case "ListDistributions": + _, err := client.ListDistributions(ctx, &cloudfront.ListDistributionsInput{ + MaxItems: aws.Int32(1), + }) + return v.handleAWSError(err, "cloudfront:ListDistributions") + + default: + return Result{ + Status: StatusSkipped, + ErrorMessage: "Unsupported CloudFront operation: " + operation, + } } } diff --git a/receiver/verifierreceiver/internal/verifier/verifier.go b/receiver/verifierreceiver/internal/verifier/verifier.go index 0b8b5ba4d..f2362d0af 100644 --- a/receiver/verifierreceiver/internal/verifier/verifier.go +++ b/receiver/verifierreceiver/internal/verifier/verifier.go @@ -74,6 +74,7 @@ const ( MethodAPICall VerificationMethod = "api_call" MethodDryRun VerificationMethod = "dry_run" MethodHTTPProbe VerificationMethod = "http_probe" + MethodGraphQL VerificationMethod = "graphql_query" ) // Permission represents a permission to verify. diff --git a/receiver/verifierreceiver/receiver.go b/receiver/verifierreceiver/receiver.go index 91ba93da7..ac9502b7e 100644 --- a/receiver/verifierreceiver/receiver.go +++ b/receiver/verifierreceiver/receiver.go @@ -54,6 +54,9 @@ type verifierReceiver struct { cancelFn context.CancelFunc wg sync.WaitGroup + + // done is closed when verification completes (used for testing) + done chan struct{} } // newVerifierReceiver creates a new verifier receiver. @@ -80,6 +83,7 @@ func newVerifierReceiver( logger: params.Logger, permissionRegistry: NewPermissionRegistry(), verifierRegistry: verifierRegistry, + done: make(chan struct{}), } } @@ -94,7 +98,9 @@ func (r *verifierReceiver) Start(ctx context.Context, _ component.Host) error { // Initialize verifiers for configured providers r.initializeVerifiers(ctx) - startCtx, cancelFn := context.WithCancel(ctx) + // Use context.Background() as parent because the Start() ctx is startup-scoped + // and may be cancelled after Start returns, which would abort verification. + startCtx, cancelFn := context.WithCancel(context.Background()) r.cancelFn = cancelFn // Run verification @@ -211,6 +217,7 @@ func (r *verifierReceiver) Shutdown(ctx context.Context) error { // runVerification runs the permission verification for all configured policies. func (r *verifierReceiver) runVerification(ctx context.Context) { + defer close(r.done) if err := r.verifyPermissions(ctx); err != nil { r.logger.Error("Failed to verify permissions", zap.Error(err)) } diff --git a/receiver/verifierreceiver/receiver_test.go b/receiver/verifierreceiver/receiver_test.go index 127e48517..03ec2e4f1 100644 --- a/receiver/verifierreceiver/receiver_test.go +++ b/receiver/verifierreceiver/receiver_test.go @@ -20,7 +20,6 @@ package verifierreceiver import ( "context" "testing" - "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -74,14 +73,12 @@ func TestReceiver_StartShutdown(t *testing.T) { ctx := context.Background() - // Start the receiver err := receiver.Start(ctx, nil) require.NoError(t, err) - // Give it time to run the verification - time.Sleep(100 * time.Millisecond) + // Wait for verification to complete + <-receiver.done - // Shutdown the receiver err = receiver.Shutdown(ctx) require.NoError(t, err) @@ -173,7 +170,7 @@ func TestReceiver_WithoutAWSCredentials(t *testing.T) { err := receiver.Start(ctx, nil) require.NoError(t, err) - time.Sleep(100 * time.Millisecond) + <-receiver.done err = receiver.Shutdown(ctx) require.NoError(t, err) @@ -225,7 +222,7 @@ func TestReceiver_UnsupportedIntegration(t *testing.T) { err := receiver.Start(ctx, nil) require.NoError(t, err) - time.Sleep(100 * time.Millisecond) + <-receiver.done err = receiver.Shutdown(ctx) require.NoError(t, err) @@ -289,7 +286,7 @@ func TestReceiver_MultipleIntegrations(t *testing.T) { err := receiver.Start(ctx, nil) require.NoError(t, err) - time.Sleep(100 * time.Millisecond) + <-receiver.done err = receiver.Shutdown(ctx) require.NoError(t, err) diff --git a/receiver/verifierreceiver/registry.go b/receiver/verifierreceiver/registry.go index 26b69f554..71dac61c6 100644 --- a/receiver/verifierreceiver/registry.go +++ b/receiver/verifierreceiver/registry.go @@ -25,28 +25,26 @@ import ( "github.com/elastic/opentelemetry-collector-components/receiver/verifierreceiver/internal/verifier" ) -// VerificationMethod indicates how a permission should be verified. -type VerificationMethod string - -const ( - // MethodAPICall makes an actual API call with minimal scope. - MethodAPICall VerificationMethod = "api_call" - // MethodDryRun uses provider's DryRun parameter where supported (e.g., AWS EC2). - MethodDryRun VerificationMethod = "dry_run" - // MethodHTTPProbe uses HTTP HEAD/GET request to check connectivity. - MethodHTTPProbe VerificationMethod = "http_probe" - // MethodGraphQL uses GraphQL introspection or minimal query. - MethodGraphQL VerificationMethod = "graphql_query" +// Type aliases for verifier package types to avoid duplication. +// The canonical definitions live in the internal/verifier package. +type VerificationMethod = verifier.VerificationMethod + +// Re-export verification method constants for use by registry callers. +var ( + MethodAPICall = verifier.MethodAPICall + MethodDryRun = verifier.MethodDryRun + MethodHTTPProbe = verifier.MethodHTTPProbe + MethodGraphQL = verifier.MethodGraphQL ) // PermissionStatus represents the result of a permission verification. type PermissionStatus string const ( - StatusGranted PermissionStatus = "granted" - StatusDenied PermissionStatus = "denied" - StatusError PermissionStatus = "error" - StatusSkipped PermissionStatus = "skipped" + StatusGranted PermissionStatus = PermissionStatus(verifier.StatusGranted) + StatusDenied PermissionStatus = PermissionStatus(verifier.StatusDenied) + StatusError PermissionStatus = PermissionStatus(verifier.StatusError) + StatusSkipped PermissionStatus = PermissionStatus(verifier.StatusSkipped) StatusPending PermissionStatus = "pending" ) From 79e2ed79cb0b6d35d5918cf2c311ea857d3607ad Mon Sep 17 00:00:00 2001 From: Evgeniy Belyi Date: Thu, 19 Feb 2026 13:22:42 -0600 Subject: [PATCH 7/7] Update CODEOWNERS --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e2807912e..4526b4bd7 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -5,6 +5,7 @@ loadgen @elastic/obs-ds-intake-services @elastic/obs-ds-hosted-services @elastic/ingest-otel-data receiver/loadgenreceiver @elastic/obs-ds-intake-services @elastic/obs-ds-hosted-services @elastic/ingest-otel-data receiver/elasticapmintakereceiver @elastic/obs-ds-intake-services @elastic/obs-ds-hosted-services @elastic/ingest-otel-data +receiver/verifierreceiver @elastic/cloud-services processor/elasticinframetricsprocessor @elastic/obs-infraobs-integrations @elastic/ingest-otel-data processor/elasticapmprocessor @elastic/obs-ds-intake-services @elastic/obs-ds-hosted-services @elastic/ingest-otel-data processor/lsmintervalprocessor @elastic/obs-ds-intake-services @elastic/obs-ds-hosted-services @elastic/ingest-otel-data