diff --git a/go.mod b/go.mod index c4ebcff6bd..3aa9d48e50 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresqlflexibleservers v1.1.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions v1.3.0 - github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/security/armsecurity v0.13.0 + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/security/armsecurity v0.14.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql v1.2.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.6.0 github.com/aquasecurity/go-dep-parser v0.0.0-20240606050805-1de9a375c629 diff --git a/go.sum b/go.sum index 69b47e354a..57367984f6 100644 --- a/go.sum +++ b/go.sum @@ -470,8 +470,8 @@ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1. github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0/go.mod h1:5kakwfW5CjC9KK+Q4wjXAg+ShuIm2mBMua0ZFj2C8PE= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions v1.3.0 h1:wxQx2Bt4xzPIKvW59WQf1tJNx/ZZKPfN+EhPX3Z6CYY= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions v1.3.0/go.mod h1:TpiwjwnW/khS0LKs4vW5UmmT9OWcxaveS8U7+tlknzo= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/security/armsecurity v0.13.0 h1:bvkjXDmjYA1qRJwqI+mmFYKioiLRUbR1eAOWsf4a+e4= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/security/armsecurity v0.13.0/go.mod h1:rVjowC1tCYv0Uw9/YHbrLzUjuTb8nMqih36SmasUhEo= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/security/armsecurity v0.14.0 h1:JfjIyBJvEvQNP/9MEUo1/6eoiPkiag2OZImw32xakcc= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/security/armsecurity v0.14.0/go.mod h1:HakuHOrWlp2G1WlFvkL7JApTZAbxRJnRiz+w4SYak5s= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql v1.2.0 h1:S087deZ0kP1RUg4pU7w9U9xpUedTCbOtz+mnd0+hrkQ= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql v1.2.0/go.mod h1:B4cEyXrWBmbfMDAPnpJ1di7MAt5DKP57jPEObAvZChg= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.6.0 h1:PiSrjRPpkQNjrM8H0WwKMnZUdu1RGMtd/LdGKUrOo+c= diff --git a/internal/resources/providers/azurelib/inventory/mock_security_client_wrapper.go b/internal/resources/providers/azurelib/inventory/mock_security_client_wrapper.go index e5c28d3d01..5c920db93d 100644 --- a/internal/resources/providers/azurelib/inventory/mock_security_client_wrapper.go +++ b/internal/resources/providers/azurelib/inventory/mock_security_client_wrapper.go @@ -96,18 +96,20 @@ func (_c *mockSecurityClientWrapper_ListAutoProvisioningSettings_Call) RunAndRet } // ListSecurityContacts provides a mock function with given fields: ctx, subID -func (_m *mockSecurityClientWrapper) ListSecurityContacts(ctx context.Context, subID string) (armsecurity.ContactsClientListResponse, error) { +func (_m *mockSecurityClientWrapper) ListSecurityContacts(ctx context.Context, subID string) ([]armsecurity.ContactsClientListResponse, error) { ret := _m.Called(ctx, subID) - var r0 armsecurity.ContactsClientListResponse + var r0 []armsecurity.ContactsClientListResponse var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string) (armsecurity.ContactsClientListResponse, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, string) ([]armsecurity.ContactsClientListResponse, error)); ok { return rf(ctx, subID) } - if rf, ok := ret.Get(0).(func(context.Context, string) armsecurity.ContactsClientListResponse); ok { + if rf, ok := ret.Get(0).(func(context.Context, string) []armsecurity.ContactsClientListResponse); ok { r0 = rf(ctx, subID) } else { - r0 = ret.Get(0).(armsecurity.ContactsClientListResponse) + if ret.Get(0) != nil { + r0 = ret.Get(0).([]armsecurity.ContactsClientListResponse) + } } if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { @@ -138,12 +140,12 @@ func (_c *mockSecurityClientWrapper_ListSecurityContacts_Call) Run(run func(ctx return _c } -func (_c *mockSecurityClientWrapper_ListSecurityContacts_Call) Return(_a0 armsecurity.ContactsClientListResponse, _a1 error) *mockSecurityClientWrapper_ListSecurityContacts_Call { +func (_c *mockSecurityClientWrapper_ListSecurityContacts_Call) Return(_a0 []armsecurity.ContactsClientListResponse, _a1 error) *mockSecurityClientWrapper_ListSecurityContacts_Call { _c.Call.Return(_a0, _a1) return _c } -func (_c *mockSecurityClientWrapper_ListSecurityContacts_Call) RunAndReturn(run func(context.Context, string) (armsecurity.ContactsClientListResponse, error)) *mockSecurityClientWrapper_ListSecurityContacts_Call { +func (_c *mockSecurityClientWrapper_ListSecurityContacts_Call) RunAndReturn(run func(context.Context, string) ([]armsecurity.ContactsClientListResponse, error)) *mockSecurityClientWrapper_ListSecurityContacts_Call { _c.Call.Return(run) return _c } diff --git a/internal/resources/providers/azurelib/inventory/security_provider.go b/internal/resources/providers/azurelib/inventory/security_provider.go index e3c1448f4a..ff31f9c7fe 100644 --- a/internal/resources/providers/azurelib/inventory/security_provider.go +++ b/internal/resources/providers/azurelib/inventory/security_provider.go @@ -19,16 +19,10 @@ package inventory import ( "context" - "errors" "fmt" - "net/http" - "net/url" "strings" "github.com/Azure/azure-sdk-for-go/sdk/azcore" - "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm" - "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" - "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/security/armsecurity" "github.com/elastic/elastic-agent-libs/logp" "github.com/samber/lo" @@ -38,7 +32,7 @@ import ( ) type securityClientWrapper interface { - ListSecurityContacts(ctx context.Context, subID string) (armsecurity.ContactsClientListResponse, error) + ListSecurityContacts(ctx context.Context, subID string) ([]armsecurity.ContactsClientListResponse, error) ListAutoProvisioningSettings(ctx context.Context, subID string) ([]armsecurity.AutoProvisioningSettingsClientListResponse, error) } @@ -47,76 +41,17 @@ type SecurityContactsProviderAPI interface { ListAutoProvisioningSettings(ctx context.Context, subscriptionID string) ([]AzureAsset, error) } -const listSecurityContactsURI = "/subscriptions/%s/providers/Microsoft.Security/securityContacts" - -// azureSecurityContactsClient is needed till this issue - -// https://github.com/Azure/azure-sdk-for-go/issues/19740 is fixed. -// Implementation code is similar to armsecurity.ContactsClient but it attempts to decode - -// the List response to two possible structs. -type azureSecurityContactsClient struct { - armClient *arm.Client -} - -// ListSecurityContacts implements the Security Contacts List Azure REST API call. -// https://learn.microsoft.com/en-us/rest/api/defenderforcloud/security-contacts/list -func (c *azureSecurityContactsClient) ListSecurityContacts(ctx context.Context, subID string) (armsecurity.ContactsClientListResponse, error) { - req, err := c.listSecurityContactsRequest(ctx, c.armClient.Endpoint(), subID) - if err != nil { - return armsecurity.ContactsClientListResponse{}, err - } - - httpResp, err := c.armClient.Pipeline().Do(req) - if err != nil { - return armsecurity.ContactsClientListResponse{}, err - } - - if !runtime.HasStatusCode(httpResp, http.StatusOK) { - err = runtime.NewResponseError(httpResp) - return armsecurity.ContactsClientListResponse{}, err - } - - return c.listSecurityContactsResponseDecode(httpResp) +type defaultSecurityClientWrapper struct { + credential azcore.TokenCredential } -// listCreateRequest creates the List request. -func (*azureSecurityContactsClient) listSecurityContactsRequest(ctx context.Context, endpoint, subID string) (*policy.Request, error) { - if subID == "" { - return nil, errors.New("parameter subscription id cannot be empty") - } - - urlPath := fmt.Sprintf(listSecurityContactsURI, url.PathEscape(subID)) - - req, err := runtime.NewRequest(ctx, http.MethodGet, runtime.JoinPaths(endpoint, urlPath)) +func (w *defaultSecurityClientWrapper) ListSecurityContacts(ctx context.Context, subID string) ([]armsecurity.ContactsClientListResponse, error) { + cl, err := armsecurity.NewContactsClient(subID, w.credential, nil) if err != nil { return nil, err } - reqQP := req.Raw().URL.Query() - reqQP.Set("api-version", "2020-01-01-preview") - req.Raw().URL.RawQuery = reqQP.Encode() - req.Raw().Header.Set("Accept", "application/json") - - return req, nil -} - -func (*azureSecurityContactsClient) listSecurityContactsResponseDecode(resp *http.Response) (armsecurity.ContactsClientListResponse, error) { - result := armsecurity.ContactsClientListResponse{} - if err := runtime.UnmarshalAsJSON(resp, &result.ContactList.Value); err != nil { - // fallback attempt to unmarshal - if secondErr := runtime.UnmarshalAsJSON(resp, &result); secondErr != nil { - return armsecurity.ContactsClientListResponse{}, err - } - } - return result, nil -} - -type defaultSecurityClientWrapper struct { - credential azcore.TokenCredential - azureSecurityContactsClient *azureSecurityContactsClient -} - -func (w *defaultSecurityClientWrapper) ListSecurityContacts(ctx context.Context, subID string) (armsecurity.ContactsClientListResponse, error) { - return w.azureSecurityContactsClient.ListSecurityContacts(ctx, subID) + return readPager(ctx, cl.NewListPager(nil)) } func (w *defaultSecurityClientWrapper) ListAutoProvisioningSettings(ctx context.Context, subID string) ([]armsecurity.AutoProvisioningSettingsClientListResponse, error) { @@ -132,14 +67,11 @@ type securityContactsProvider struct { log *logp.Logger //nolint:unused } -func NewSecurityContacts(log *logp.Logger, credential azcore.TokenCredential, armClient *arm.Client) SecurityContactsProviderAPI { +func NewSecurityContacts(log *logp.Logger, credential azcore.TokenCredential) SecurityContactsProviderAPI { return &securityContactsProvider{ log: log, client: &defaultSecurityClientWrapper{ credential: credential, - azureSecurityContactsClient: &azureSecurityContactsClient{ - armClient: armClient, - }, }, } } @@ -150,18 +82,22 @@ func (p *securityContactsProvider) ListSecurityContacts(ctx context.Context, sub return nil, fmt.Errorf("error while fetching security contacts: %w", err) } - return lo.FilterMap(res.Value, func(contract *armsecurity.Contact, _ int) (AzureAsset, bool) { - if contract == nil { - return AzureAsset{}, false - } - return p.transformSecurityContract(contract, subscriptionID), true - }), nil + return lo.Flatten( + lo.Map(res, func(listResponse armsecurity.ContactsClientListResponse, _ int) []AzureAsset { + return lo.FilterMap(listResponse.ContactList.Value, func(contact *armsecurity.Contact, _ int) (AzureAsset, bool) { + if contact == nil { + return AzureAsset{}, false + } + return p.transformSecurityContract(contact, subscriptionID), true + }) + }), + ), nil } func (p *securityContactsProvider) transformSecurityContract(contact *armsecurity.Contact, subscriptionID string) AzureAsset { properties := map[string]any{} - maps.AddIfNotNil(properties, "alertNotifications", contact.Properties.AlertNotifications) + maps.AddIfSliceNotEmpty(properties, "notificationsSources", contact.Properties.NotificationsSources) maps.AddIfNotNil(properties, "emails", contact.Properties.Emails) maps.AddIfNotNil(properties, "notificationsByRole", contact.Properties.NotificationsByRole) maps.AddIfNotNil(properties, "phone", contact.Properties.Phone) diff --git a/internal/resources/providers/azurelib/inventory/security_provider_test.go b/internal/resources/providers/azurelib/inventory/security_provider_test.go index 1a463bfb4b..e7c8d1ae08 100644 --- a/internal/resources/providers/azurelib/inventory/security_provider_test.go +++ b/internal/resources/providers/azurelib/inventory/security_provider_test.go @@ -33,24 +33,26 @@ func TestListSecurityContacts(t *testing.T) { notificationsByRole := func(roles []string, state string) *armsecurity.ContactPropertiesNotificationsByRole { n := &armsecurity.ContactPropertiesNotificationsByRole{} n.State = (*armsecurity.State)(&state) - n.Roles = make([]*armsecurity.Roles, 0, len(roles)) + n.Roles = make([]*armsecurity.SecurityContactRole, 0, len(roles)) for _, r := range roles { - n.Roles = append(n.Roles, (*armsecurity.Roles)(&r)) + n.Roles = append(n.Roles, (*armsecurity.SecurityContactRole)(&r)) } return n } - response := func(c ...*armsecurity.Contact) armsecurity.ContactsClientListResponse { - return armsecurity.ContactsClientListResponse{ - ContactList: armsecurity.ContactList{ - Value: c, + response := func(c ...*armsecurity.Contact) []armsecurity.ContactsClientListResponse { + return []armsecurity.ContactsClientListResponse{ + { + ContactList: armsecurity.ContactList{ + Value: c, + }, }, } } tests := map[string]struct { inputSubID string - mockReturn armsecurity.ContactsClientListResponse + mockReturn []armsecurity.ContactsClientListResponse mockReturnError error expected []AzureAsset expectError bool diff --git a/internal/resources/providers/azurelib/provider.go b/internal/resources/providers/azurelib/provider.go index f3872772ba..6e67287607 100644 --- a/internal/resources/providers/azurelib/provider.go +++ b/internal/resources/providers/azurelib/provider.go @@ -20,7 +20,6 @@ package azurelib import ( "fmt" - "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/monitor/armmonitor" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph" "github.com/elastic/elastic-agent-libs/logp" @@ -64,11 +63,6 @@ func (p *ProviderInitializer) Init(log *logp.Logger, azureConfig auth.AzureFacto return nil, fmt.Errorf("failed to init monitor client: %w", err) } - genericARMClient, err := arm.NewClient("armsecurity-custom", "v0.0.1", azureConfig.Credentials, nil) - if err != nil { - return nil, fmt.Errorf("failed to init generic arm client: %w", err) - } - resourceGraphProvider := inventory.NewResourceGraphProvider(log, resourceGraphClientFactory) return &provider{ AppServiceProviderAPI: inventory.NewAppServiceProvider(log, azureConfig.Credentials), @@ -78,7 +72,7 @@ func (p *ProviderInitializer) Init(log *logp.Logger, azureConfig auth.AzureFacto ProviderAPI: governance.NewProvider(log, resourceGraphProvider), ResourceGraphProviderAPI: resourceGraphProvider, SQLProviderAPI: inventory.NewSQLProvider(log, azureConfig.Credentials), - SecurityContactsProviderAPI: inventory.NewSecurityContacts(log, azureConfig.Credentials, genericARMClient), + SecurityContactsProviderAPI: inventory.NewSecurityContacts(log, azureConfig.Credentials), StorageAccountProviderAPI: inventory.NewStorageAccountProvider(log, diagnosticSettingsClient, azureConfig.Credentials), SubscriptionProviderAPI: inventory.NewSubscriptionProvider(log, azureConfig.Credentials), }, nil diff --git a/security-policies/bundle/compliance/cis_azure/rules/cis_2_1_20/rule.rego b/security-policies/bundle/compliance/cis_azure/rules/cis_2_1_20/rule.rego index 5d60d3f96b..4cf8c76089 100644 --- a/security-policies/bundle/compliance/cis_azure/rules/cis_2_1_20/rule.rego +++ b/security-policies/bundle/compliance/cis_azure/rules/cis_2_1_20/rule.rego @@ -19,10 +19,13 @@ finding = result if { default notification_alert_high = false notification_alert_high if { - # Ensure at least one Security Contact Settings exists and alertNotifications severity is set to high. + # Ensure at least one Security Contact Settings exists and alertNotifications severity is set to high, low, or medium. some security_contact in data_adapter.resource security_contact.name == "default" - lower(security_contact.properties.alertNotifications.state) == "on" - lower(security_contact.properties.alertNotifications.minimalSeverity) in ["low", "medium", "high"] + + some notification_source in security_contact.properties.notificationsSources + + lower(notification_source.sourceType) == "alert" + lower(notification_source.minimalSeverity) in ["low", "medium", "high"] } diff --git a/security-policies/bundle/compliance/cis_azure/rules/cis_2_1_20/test.rego b/security-policies/bundle/compliance/cis_azure/rules/cis_2_1_20/test.rego index a44856f1e2..0c7a2a2e08 100644 --- a/security-policies/bundle/compliance/cis_azure/rules/cis_2_1_20/test.rego +++ b/security-policies/bundle/compliance/cis_azure/rules/cis_2_1_20/test.rego @@ -8,31 +8,31 @@ import future.keywords.if test_violation if { eval_fail with input as test_data.generate_security_contacts([test_data.generate_single_security_contact( "default", - prop_alert_notifications({ - "state": "Off", + prop_notification_sources([{ "minimalSeverity": "Medium", - }), + "sourceType": "Another", + }]), )]) eval_fail with input as test_data.generate_security_contacts([test_data.generate_single_security_contact( "default", - prop_alert_notifications({"state": "On"}), + prop_notification_sources([{"sourceType": "Alert"}]), )]) eval_fail with input as test_data.generate_security_contacts([ test_data.generate_single_security_contact( "non-default", - prop_alert_notifications({ - "state": "On", + prop_notification_sources([{ "minimalSeverity": "High", - }), + "sourceType": "Alert", + }]), ), test_data.generate_single_security_contact( "default", - prop_alert_notifications({ - "state": "On", - "minimalSeverity": "Wrong Value", - }), + prop_notification_sources([{ + "minimalSeverity": "Wrong value", + "sourceType": "Alert", + }]), ), ]) } @@ -40,42 +40,42 @@ test_violation if { test_pass if { eval_pass with input as test_data.generate_security_contacts([test_data.generate_single_security_contact( "default", - prop_alert_notifications({ - "state": "On", + prop_notification_sources([{ "minimalSeverity": "High", - }), + "sourceType": "Alert", + }]), )]) eval_pass with input as test_data.generate_security_contacts([test_data.generate_single_security_contact( "default", - prop_alert_notifications({ - "state": "On", + prop_notification_sources([{ "minimalSeverity": "Medium", - }), + "sourceType": "Alert", + }]), )]) eval_pass with input as test_data.generate_security_contacts([test_data.generate_single_security_contact( "default", - prop_alert_notifications({ - "state": "On", + prop_notification_sources([{ "minimalSeverity": "Low", - }), + "sourceType": "Alert", + }]), )]) eval_pass with input as test_data.generate_security_contacts([ test_data.generate_single_security_contact( "non-default", - prop_alert_notifications({ - "state": "On", + prop_notification_sources([{ "minimalSeverity": "Low", - }), + "sourceType": "Alert", + }]), ), test_data.generate_single_security_contact( "default", - prop_alert_notifications({ - "state": "On", + prop_notification_sources([{ "minimalSeverity": "High", - }), + "sourceType": "Alert", + }]), ), ]) } @@ -96,4 +96,4 @@ not_eval if { not finding with data.benchmark_data_adapter as data_adapter } -prop_alert_notifications(alertNotifications) = {"alertNotifications": alertNotifications} +prop_notification_sources(notificationsSource) = {"notificationsSources": notificationsSource}