Repository: cloud-custodian/cloud-custodian

Issue:
aws - unused sg filter eni evaluation
### Describe the bug

When a subnet is shared cross account via RAM, ENI shows up in both the accounts i.e account that owns the subnet and the account that owns the ENI. The unused SG filter pulls down all the eni to evaluate the SGs and runs into an issue such that it doesn't contain all the metadata referenced in the code




### What did you expect to happen?

For the filter `unused` re: security-group resource type ENI should potentially only be evaluated in the parent account since SGs cannot be shared. 

### Cloud Provider

Amazon Web Services (AWS)

### Cloud Custodian version and dependency information

_No response_

### Policy

```shell
policies:
   - name: sg-unused
     resource: aws.security-group
     filters:
       - unused
```


### Relevant log/traceback output

```shell
Traceback (most recent call last):
  File "/usr/local/lib/python3.9/dist-packages/c7n/policy.py", line 331, in run
    resources = self.policy.resource_manager.resources()
  File "/usr/local/lib/python3.9/dist-packages/c7n/query.py", line 550, in resources
    resources = self.filter_resources(resources)
  File "/usr/local/lib/python3.9/dist-packages/c7n/manager.py", line 112, in filter_resources
    resources = f.process(resources, event)
  File "/usr/local/lib/python3.9/dist-packages/c7n/resources/vpc.py", line 952, in process
    enis = self._get_eni_attributes()
  File "/usr/local/lib/python3.9/dist-packages/c7n/resources/vpc.py", line 931, in _get_eni_attributes
    instance_owner_id = nic['Attachment']['InstanceOwnerId']
KeyError: 'InstanceOwnerId'
```


### Extra information or context

A describe command on network interface id in both the parent account and the shared account gets the following responses

aws ec2 describe-network-interfaces --network-interface-ids eni-0XXXXXXXXXXXXXXX

Producer account (owner of the vpc/subnet)
```
{
    "NetworkInterfaces": [
        {
            "Attachment": {
                "Status": "attached"
            },
            "AvailabilityZone": "us-east-1b",
            "Description": "AWS Lambda VPC",
            "Groups": [
                {
                    "GroupName": "test-1",
                    "GroupId": "sg-123"
                },
                {
                    "GroupName": "test-2",
                    "GroupId": "sg-456"
                },
                {
                    "GroupName": "test-2",
                    "GroupId": "sg-789"
                }
            ],
            "InterfaceType": "lambda",
            "Ipv6Addresses": [],
            "MacAddress": "XX:XX:XX:XX:XX:XX",
            "NetworkInterfaceId": "eni-0XXXXXXXXXXXXXXX",
            "OwnerId": "012345678910",
            "PrivateDnsName": "ip-X-X-X-X.ec2.internal",
            "PrivateIpAddress": "X.X.X.X",
            "PrivateIpAddresses": [
                {
                    "Primary": true,
                    "PrivateDnsName": "ip-X-X-X-X.ec2.internal",
                    "PrivateIpAddress": "X.X.X.X"
                }
            ],
            "RequesterManaged": false,
            "SourceDestCheck": true,
            "Status": "in-use",
            "SubnetId": "subnet-0xxxxxxxxxxxde4c0",
            "TagSet": [],
            "VpcId": "vpc-xxxxxx"
        }
    ]
}
```
Consumer account (owner of the ENI):
```
{
    "NetworkInterfaces": [
        {
            "Attachment": {
                "AttachmentId": ela-attach-xxxxxx,
                "DeleteOnTermination": false,
                "DeviceIndex": 1,
                "InstanceOwnerId": "amazon-aws",
                "Status": "attached"
            },
            "AvailabilityZone": "us-east-1a",
            "Description": "AWS Lambda VPC",
            "Groups": [
                {
                    "GroupName": "test-1",
                    "GroupId": "sg-123"
                },
                {
                    "GroupName": "test-2",
                    "GroupId": "sg-456"
                },
                {
                    "GroupName": "test-2",
                    "GroupId": "sg-789"
                }
            ],
            "InterfaceType": "lambda",
            "Ipv6Addresses": [],
            "MacAddress": "XX:XX:XX:XX:XX:XX",
            "NetworkInterfaceId": "eni-0XXXXXXXXXXXXXXX",
            "OwnerId": "012345678910",
            "PrivateDnsName": "ip-X-X-X-X.ec2.internal",
            "PrivateIpAddress": "X.X.X.X",
            "PrivateIpAddresses": [
                {
                    "Primary": true,
                    "PrivateDnsName": "ip-X-X-X-X.ec2.internal",
                    "PrivateIpAddress": "X.X.X.X"
                }
            ],
            "RequesterId": "AXXXXXXXXXXXXXX"
            "RequesterManaged": false,
            "SourceDestCheck": true,
            "Status": "in-use",
            "SubnetId": "subnet-0xxxxxxxxxxxde4c0",
            "TagSet": [],
            "VpcId": "vpc-xxxxxx"
        }
    ]
}
```
This [line](https://github.com/cloud-custodian/cloud-custodian/blob/main/c7n/resources/vpc.py#L1007) seems to be trying to reference a key that's not present in the metadata


Discussion:
hmmm.. in this context your running the policy in the ram "producer" account or the consumer account. 
Consumer account (subnet shared via RAM) owns the ENI, but it also shows up in the producer account. Correct @kapilt . In my context the policy is running in the producer account where the issue seems to be happening. The describe network interface id responses posted above is from both the producer account and the consumer account. I went ahead and edited the captions to make it more clear. Thanks

Resolving patch:
diff --git a/c7n/resources/vpc.py b/c7n/resources/vpc.py
--- a/c7n/resources/vpc.py
+++ b/c7n/resources/vpc.py
@@ -1000,24 +1000,21 @@ class UsedSecurityGroup(SGUsage):
     interface_resource_type_key = 'c7n:InterfaceResourceTypes'
 
     def _get_eni_attributes(self):
-        enis = []
+        group_enis = {}
         for nic in self.nics:
+            instance_owner_id, interface_resource_type = '', ''
             if nic['Status'] == 'in-use':
-                if nic.get('Attachment'):
+                if nic.get('Attachment') and 'InstanceOwnerId' in nic['Attachment']:
                     instance_owner_id = nic['Attachment']['InstanceOwnerId']
-                else:
-                    instance_owner_id = ''
                 interface_resource_type = get_eni_resource_type(nic)
-            else:
-                instance_owner_id = ''
-                interface_resource_type = ''
             interface_type = nic.get('InterfaceType')
             for g in nic['Groups']:
-                enis.append({'GroupId': g['GroupId'],
-                             'InstanceOwnerId': instance_owner_id,
-                             'InterfaceType': interface_type,
-                             'InterfaceResourceType': interface_resource_type})
-        return enis
+                group_enis.setdefault(g['GroupId'], []).append({
+                    'InstanceOwnerId': instance_owner_id,
+                    'InterfaceType': interface_type,
+                    'InterfaceResourceType': interface_resource_type
+                })
+        return group_enis
 
     def process(self, resources, event=None):
         used = self.scan_groups()
@@ -1025,19 +1022,15 @@ def process(self, resources, event=None):
             r for r in resources
             if r['GroupId'] not in used and 'VpcId' in r]
         unused = {g['GroupId'] for g in self.filter_peered_refs(unused)}
-        enis = self._get_eni_attributes()
+        group_enis = self._get_eni_attributes()
         for r in resources:
-            owner_ids = set()
-            interface_types = set()
-            interface_resource_types = set()
-            for eni in enis:
-                if r['GroupId'] == eni['GroupId']:
-                    owner_ids.add(eni['InstanceOwnerId'])
-                    interface_types.add(eni['InterfaceType'])
-                    interface_resource_types.add(eni['InterfaceResourceType'])
-            r[self.instance_owner_id_key] = list(filter(None, owner_ids))
-            r[self.interface_type_key] = list(filter(None, interface_types))
-            r[self.interface_resource_type_key] = list(filter(None, interface_resource_types))
+            enis = group_enis.get(r['GroupId'], ())
+            r[self.instance_owner_id_key] = list({
+                i['InstanceOwnerId'] for i in enis if i['InstanceOwnerId']})
+            r[self.interface_type_key] = list({
+                i['InterfaceType'] for i in enis if i['InterfaceType']})
+            r[self.interface_resource_type_key] = list({
+                i['InterfaceResourceType'] for i in enis if i['InterfaceResourceType']})
         return [r for r in resources if r['GroupId'] not in unused]
 
 
diff --git a/c7n/utils.py b/c7n/utils.py
--- a/c7n/utils.py
+++ b/c7n/utils.py
@@ -910,10 +910,12 @@ def get_eni_resource_type(eni):
         rtype = 'hsm'
     elif description.startswith('CloudHsm ENI'):
         rtype = 'hsmv2'
+    elif description.startswith('AWS Lambda VPC'):
+        rtype = 'lambda'
     elif description.startswith('AWS Lambda VPC ENI'):
         rtype = 'lambda'
     elif description.startswith('Interface for NAT Gateway'):
-        return 'nat'
+        rtype = 'nat'
     elif (description == 'RDSNetworkInterface' or
             description.startswith('Network interface for DBProxy')):
         rtype = 'rds'
@@ -923,6 +925,8 @@ def get_eni_resource_type(eni):
         rtype = 'tgw'
     elif description.startswith('VPC Endpoint Interface'):
         rtype = 'vpce'
+    elif description.startswith('aws-k8s-branch-eni'):
+        rtype = 'eks'
     else:
         rtype = 'unknown'
     return rtype


Test patch:
diff --git a/tests/data/ram-producer-view-consumer-eni.json b/tests/data/ram-producer-view-consumer-eni.json
new file mode 100644
--- /dev/null
+++ b/tests/data/ram-producer-view-consumer-eni.json
@@ -0,0 +1,45 @@
+{
+    "NetworkInterfaces": [
+        {
+            "Attachment": {
+                "Status": "attached"
+            },
+            "AvailabilityZone": "us-east-1b",
+            "Description": "AWS Lambda VPC",
+            "Groups": [
+                {
+                    "GroupName": "test-1",
+                    "GroupId": "sg-123"
+                },
+                {
+                    "GroupName": "test-2",
+                    "GroupId": "sg-456"
+                },
+                {
+                    "GroupName": "test-2",
+                    "GroupId": "sg-789"
+                }
+            ],
+            "InterfaceType": "lambda",
+            "Ipv6Addresses": [],
+            "MacAddress": "XX:XX:XX:XX:XX:XX",
+            "NetworkInterfaceId": "eni-0XXXXXXXXXXXXXXX",
+            "OwnerId": "012345678910",
+            "PrivateDnsName": "ip-X-X-X-X.ec2.internal",
+            "PrivateIpAddress": "X.X.X.X",
+            "PrivateIpAddresses": [
+                {
+                    "Primary": true,
+                    "PrivateDnsName": "ip-X-X-X-X.ec2.internal",
+                    "PrivateIpAddress": "X.X.X.X"
+                }
+            ],
+            "RequesterManaged": false,
+            "SourceDestCheck": true,
+            "Status": "in-use",
+            "SubnetId": "subnet-0xxxxxxxxxxxde4c0",
+            "TagSet": [],
+            "VpcId": "vpc-xxxxxx"
+        }
+    ]
+}
diff --git a/tests/test_vpc.py b/tests/test_vpc.py
--- a/tests/test_vpc.py
+++ b/tests/test_vpc.py
@@ -1,7 +1,7 @@
 # Copyright The Cloud Custodian Authors.
 # SPDX-License-Identifier: Apache-2.0
 import time
-from .common import BaseTest, functional, event_data
+from .common import BaseTest, functional, event_data, load_data
 from unittest.mock import MagicMock
 
 from botocore.exceptions import ClientError as BotoClientError
@@ -1419,6 +1419,16 @@ def test_stale(self):
             ],
         )
 
+    def test_used_consumer_eni_in_producer(self):
+        p = self.load_policy(
+            {"name": "sg-used", "resource": "security-group", "filters": ["used"]},
+        )
+        used = p.resource_manager.filters[0]
+        used.nics = load_data('ram-producer-view-consumer-eni.json')['NetworkInterfaces']
+
+        group_enis = used._get_eni_attributes()
+        assert set(group_enis) == {'sg-123', 'sg-456', 'sg-789'}
+
     def test_used(self):
         factory = self.replay_flight_data("test_security_group_used")
         p = self.load_policy(