Repository: cloud-custodian/cloud-custodian

Issue:
azure - event grid mode needs to verify resource type from the event
Seems like today Event Grid mode policies don't verify event types and always handle events as-is (e.g. autotag for VM can tag Storage if there is an event grid subscription)


Discussion:
### ACCEPTANCE CRITERIA

1. Fail Azure functions provisioning if subscribe events are different from the policy resource.
Before implementing please provide design for how child resources will be handled.  e.g. if you get an event for a SQL Database but the policy targets a SQL server, can you truncate the resource ID and make it work? Should you?   Captured in #4617 

Resolving patch:
diff --git a/tools/c7n_azure/c7n_azure/policy.py b/tools/c7n_azure/c7n_azure/policy.py
--- a/tools/c7n_azure/c7n_azure/policy.py
+++ b/tools/c7n_azure/c7n_azure/policy.py
@@ -30,6 +30,7 @@
 
 from c7n import utils
 from c7n.actions import EventAction
+from c7n.exceptions import PolicyValidationError
 from c7n.policy import PullMode, ServerlessExecutionMode, execution
 from c7n.utils import local_session
 
@@ -351,6 +352,20 @@ class AzureEventGridMode(AzureFunctionMode):
                                required=['events'],
                                rinherit=AzureFunctionMode.schema)
 
+    def __init__(self, policy):
+        super(AzureEventGridMode, self).__init__(policy)
+        self.subscribed_events = AzureEvents.get_event_operations(
+            self.policy.data['mode'].get('events'))
+
+    def validate(self):
+        super(AzureEventGridMode, self).validate()
+        resource_type = self.policy.resource_manager.resource_type.resource_type
+        for event in self.subscribed_events:
+            if resource_type.lower() not in event.lower():
+                raise PolicyValidationError(
+                    'The policy resource, {}, can not be triggered by the event, {}.'.format(
+                        resource_type, event))
+
     def provision(self):
         super(AzureEventGridMode, self).provision()
         session = local_session(self.policy.session_factory)
@@ -391,9 +406,8 @@ def _create_event_subscription(self, storage_account, queue_name, session):
                                                                queue_name=queue_name)
 
         # filter specific events
-        subscribed_events = AzureEvents.get_event_operations(
-            self.policy.data['mode'].get('events'))
-        advance_filter = StringInAdvancedFilter(key='Data.OperationName', values=subscribed_events)
+        advance_filter = StringInAdvancedFilter(key='Data.OperationName',
+                                                values=self.subscribed_events)
         event_filter = EventSubscriptionFilter(advanced_filters=[advance_filter])
 
         for subscription_id in self.target_subscription_ids:


Test patch:
diff --git a/tools/c7n_azure/tests_azure/test_function_package.py b/tools/c7n_azure/tests_azure/test_function_package.py
--- a/tools/c7n_azure/tests_azure/test_function_package.py
+++ b/tools/c7n_azure/tests_azure/test_function_package.py
@@ -58,7 +58,7 @@ def test_add_function_config_events(self):
             'resource': 'azure.publicip',
             'mode':
                 {'type': FUNCTION_EVENT_TRIGGER_MODE,
-                 'events': ['VmWrite']},
+                 'events': ['PublicIpWrite']},
         })
 
         packer = FunctionPackage(p.data['name'])
@@ -76,7 +76,7 @@ def test_add_policy(self):
             'resource': 'azure.publicip',
             'mode':
                 {'type': FUNCTION_EVENT_TRIGGER_MODE,
-                 'events': ['VmWrite']},
+                 'events': ['PublicIpWrite']},
         })
 
         packer = FunctionPackage(p.data['name'])
@@ -87,7 +87,7 @@ def test_add_policy(self):
                          {u'resource': u'azure.publicip',
                           u'name': u'test-azure-public-ip',
                           u'mode': {u'type': u'azure-event-grid',
-                                    u'events': [u'VmWrite']}})
+                                    u'events': [u'PublicIpWrite']}})
 
     def test_zipped_files_have_modified_timestamp(self):
         t = time.gmtime(1577854800)
@@ -128,7 +128,7 @@ def test_event_package_files(self, session_mock):
             'resource': 'azure.resourcegroup',
             'mode':
                 {'type': FUNCTION_EVENT_TRIGGER_MODE,
-                 'events': ['VmWrite']},
+                 'events': ['ResourceGroupWrite']},
         })
 
         packer = FunctionPackage(p.data['name'])
@@ -196,7 +196,7 @@ def test_env_var_disables_cert_validation(self):
             'resource': 'azure.resourcegroup',
             'mode':
                 {'type': FUNCTION_EVENT_TRIGGER_MODE,
-                 'events': ['VmWrite']},
+                 'events': ['ResourceGroupWrite']},
         })
 
         with patch.dict(os.environ,
diff --git a/tools/c7n_azure/tests_azure/test_policy_mode.py b/tools/c7n_azure/tests_azure/test_policy_mode.py
--- a/tools/c7n_azure/tests_azure/test_policy_mode.py
+++ b/tools/c7n_azure/tests_azure/test_policy_mode.py
@@ -14,13 +14,15 @@
 from __future__ import absolute_import, division, print_function, unicode_literals
 
 from azure.mgmt.storage.models import StorageAccount
-from .azure_common import BaseTest, DEFAULT_SUBSCRIPTION_ID, arm_template, cassette_name
 from c7n_azure.constants import FUNCTION_EVENT_TRIGGER_MODE, FUNCTION_TIME_TRIGGER_MODE, \
     CONTAINER_EVENT_TRIGGER_MODE, CONTAINER_TIME_TRIGGER_MODE
 from c7n_azure.policy import AzureEventGridMode, AzureFunctionMode, AzureModeCommon
 from jsonschema import ValidationError
 from mock import mock, patch, Mock
 
+from c7n.exceptions import PolicyValidationError
+from .azure_common import BaseTest, DEFAULT_SUBSCRIPTION_ID, arm_template, cassette_name
+
 
 class AzurePolicyModeTest(BaseTest):
     def setUp(self):
@@ -59,15 +61,62 @@ def test_azure_function_event_mode_too_many_events_throws(self):
                         'type': FUNCTION_EVENT_TRIGGER_MODE,
                         'events': [
                             'VmWrite',
-                            'AppServicePlanWrite',
-                            'CognitiveServiceWrite',
+                            {
+                                'resourceProvider': 'Microsoft.Compute/virtualMachines/',
+                                'event': 'delete'
+                            },
+                            {
+                                'resourceProvider': 'Microsoft.Compute/virtualMachines/',
+                                'event': 'powerOff/action'
+                            },
+                            {
+                                'resourceProvider': 'Microsoft.Compute/virtualMachines/',
+                                'event': 'reimage/action'
+                            },
+                            {
+                                'resourceProvider': 'Microsoft.Compute/virtualMachines/',
+                                'event': 'redeploy/action'
+                            },
+                            {
+                                'resourceProvider': 'Microsoft.Compute/virtualMachines/',
+                                'event': 'start/action'
+                            }
+                        ]
+                    }
+                }, validate=True)
+
+    def test_azure_function_event_mode_incorrect_event_type(self):
+        with self.sign_out_patch():
+            with self.assertRaises(PolicyValidationError):
+                self.load_policy({
+                    'name': 'test-azure-serverless-mode',
+                    'resource': 'azure.vm',
+                    'mode': {
+                        'type': FUNCTION_EVENT_TRIGGER_MODE,
+                        'events': [
                             'CosmosDbWrite',
-                            'DataFactoryWrite',
-                            'DataLakeWrite'
                         ]
                     }
                 }, validate=True)
 
+    def test_azure_function_event_mode_child_event_type(self):
+        with self.sign_out_patch():
+            p = self.load_policy({
+                'name': 'test-azure-serverless-mode',
+                'resource': 'azure.networksecuritygroup',
+                'mode': {
+                    'type': FUNCTION_EVENT_TRIGGER_MODE,
+                    'events': [
+                        {
+                            'resourceProvider':
+                                'Microsoft.Network/networkSecurityGroups/securityRules',
+                            'event': 'write'
+                        }
+                    ]
+                }
+            }, validate=True)
+            self.assertTrue(p)
+
     def test_azure_function_periodic_mode_schema_validation(self):
         with self.sign_out_patch():
             p = self.load_policy({
@@ -364,10 +413,10 @@ def test_event_grid_mode_creates_advanced_filtered_subscription_with_multiple_ev
                 {'type': FUNCTION_EVENT_TRIGGER_MODE,
                  'events':
                      ['VmWrite',
-                        {
-                            'resourceProvider': 'Microsoft.Resources/subscriptions/resourceGroups',
-                            'event': 'write'
-                        }]},
+                      {
+                          'resourceProvider': 'Microsoft.Compute/virtualMachines',
+                          'event': 'powerOff/action'
+                      }]},
         })
 
         with mock.patch('c7n_azure.azure_events.AzureEventSubscription.create') as mock_create:
@@ -383,7 +432,7 @@ def test_event_grid_mode_creates_advanced_filtered_subscription_with_multiple_ev
             self.assertEqual(event_filter.key, 'Data.OperationName')
             self.assertEqual(event_filter.values,
                              ['Microsoft.Compute/virtualMachines/write',
-                              'Microsoft.Resources/subscriptions/resourceGroups/write'])
+                              'Microsoft.Compute/virtualMachines/powerOff/action'])
             self.assertEqual(event_filter.operator_type, 'StringIn')
 
     def test_extract_properties(self):