Repository: cloud-custodian/cloud-custodian

Issue:
Cloud Custodian Not Handling DomainConfig.AccessPolicies.Options Properly For Elasticsearch
### Describe the bug

When trying to use the `cross-account` filter for elasticsearch ( https://cloudcustodian.io/docs/aws/resources/elasticsearch.html#aws-elasticsearch-filters-cross-account ), Cloud Custodian generates an exception when attempting to parse the value for `DomainConfig.AccessPolicies.Options`.

Cloud Custodian is expecting this value to be JSON - and it should be noted that for every other dict under `DomainConfig`, it is valid JSON. However, it seems AWS has made this specific field non-JSON; for my environment, it was set to a blank string. This causes `json.loads()` to fail [here](https://github.com/cloud-custodian/cloud-custodian/blob/main/c7n/resources/elasticsearch.py#L140-L142).

Based on [the AWS API info for the endpoint](https://awscli.amazonaws.com/v2/documentation/api/2.0.34/reference/es/describe-elasticsearch-domain-config.html#output) it seems to be intentional.

### What did you expect to happen?

Cloud Custodian should account for this incongruence in AWS's API and handle it appropriately.

### Cloud Provider

Amazon Web Services (AWS)

### Cloud Custodian version and dependency information

```shell
Custodian:   0.9.22
Python:      3.9.16 (main, Dec  7 2022, 10:15:43)
             [Clang 14.0.0 (clang-1400.0.29.202)]
Platform:    posix.uname_result(sysname='Darwin', nodename='jcarlson-pro', release='22.3.0', version='Darwin Kernel Version 22.3.0: Mon Jan 30 20:42:11 PST 2023; root:xnu-8792.81.3~2/RELEASE_X86_64', machine='x86_64')
Using venv:  True
Docker: False
Installed:

argcomplete==2.0.0
attrs==22.2.0
boto3==1.26.51
botocore==1.29.51
docutils==0.17.1
importlib-metadata==4.13.0
importlib-resources==5.10.2
jmespath==1.0.1
jsonschema==4.17.3
pkgutil-resolve-name==1.3.10
pyrsistent==0.19.3
python-dateutil==2.8.2
pyyaml==6.0
s3transfer==0.6.0
six==1.16.0
tabulate==0.8.10
typing-extensions==4.4.0
urllib3==1.26.14
zipp==3.11.0
```
```


### Policy

```shell
- name: remove-elasticsearch-access-policy_public-access
    resource: elasticsearch
    filters:
      - type: value
        key: VPCOptions
        value: absent
      - type: cross-account
        everyone_only: true
        whitelist_orgids: ["<REDACTED>"]
    actions:
      - type: remove-statements
        statement_ids: matched
```


### Relevant log/traceback output

```shell
cloud-custodian 2023-01-30 21:13:10,398: c7n_org:ERROR Exception running policy:remove-elasticsearch-access-policy_public-access account:XXX region:us-east-1 error:Expecting value: line 1 column 1 (char 0)
cloud-custodian 2023-01-30 21:13:11,118: c7n_org:ERROR Exception running policy:remove-elasticsearch-domain_global-access_no-auth account:XXX region:us-east-1 error:Expecting value: line 1 column 1 (char 0)


2023-02-10 09:03:36,422: c7n_org:ERROR Exception running policy:remove-elasticsearch-access-policy_public-access account:Brightcove Alive Stage region:us-east-1 error:Expecting value: line 1 column 1 (char 0)
Traceback (most recent call last):
  File "/Users/jcarlson/Code/cloud-custodian/venv/lib/python3.9/site-packages/c7n_org/cli.py", line 626, in run_account
    resources = p.run()
  File "/Users/jcarlson/Code/cloud-custodian/venv/lib/python3.9/site-packages/c7n/policy.py", line 1326, in __call__
    resources = PullMode(self).run()
  File "/Users/jcarlson/Code/cloud-custodian/venv/lib/python3.9/site-packages/c7n/policy.py", line 330, in run
    resources = self.policy.resource_manager.resources()
  File "/Users/jcarlson/Code/cloud-custodian/venv/lib/python3.9/site-packages/c7n/query.py", line 540, in resources
    resources = self.filter_resources(resources)
  File "/Users/jcarlson/Code/cloud-custodian/venv/lib/python3.9/site-packages/c7n/manager.py", line 111, in filter_resources
    resources = f.process(resources, event)
  File "/Users/jcarlson/Code/cloud-custodian/venv/lib/python3.9/site-packages/c7n/resources/elasticsearch.py", line 131, in process
    r[self.policy_attribute] = json.loads(
  File "/usr/local/Cellar/python@3.9/3.9.16/Frameworks/Python.framework/Versions/3.9/lib/python3.9/json/__init__.py", line 346, in loads
    return _default_decoder.decode(s)
  File "/usr/local/Cellar/python@3.9/3.9.16/Frameworks/Python.framework/Versions/3.9/lib/python3.9/json/decoder.py", line 337, in decode
    obj, end = self.raw_decode(s, idx=_w(s, 0).end())
  File "/usr/local/Cellar/python@3.9/3.9.16/Frameworks/Python.framework/Versions/3.9/lib/python3.9/json/decoder.py", line 355, in raw_decode
    raise JSONDecodeError("Expecting value", s, err.value) from None
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
> /usr/local/Cellar/python@3.9/3.9.16/Frameworks/Python.framework/Versions/3.9/lib/python3.9/json/decoder.py(355)raw_decode()
-> raise JSONDecodeError("Expecting value", s, err.value) from None
```


### Extra information or context

Value of `DomainConfig.AccessPolicies` in my environment:
```
jcarlson@jcarlson-pro ~ % aws --profile=xxx es describe-elasticsearch-domain-config --domain-name xxx | jq '.DomainConfig.AccessPolicies'
{
  "Options": "",
  "Status": {
    "CreationDate": 1678996095.084,
    "UpdateDate": 1678996095.084,
    "UpdateVersion": 23,
    "State": "Active",
    "PendingDeletion": false
  }
}
jcarlson@jcarlson-pro ~ %
```


Discussion:

Resolving patch:
diff --git a/c7n/resources/elasticsearch.py b/c7n/resources/elasticsearch.py
--- a/c7n/resources/elasticsearch.py
+++ b/c7n/resources/elasticsearch.py
@@ -137,9 +137,8 @@ def process(self, resources, event=None):
                     DomainName=r['DomainName'],
                     ignore_err_codes=('ResourceNotFoundException',))
                 if result:
-                    r[self.policy_attribute] = json.loads(
-                        result.get('DomainConfig').get('AccessPolicies').get('Options')
-                    )
+                    options = result.get('DomainConfig').get('AccessPolicies').get('Options')
+                    r[self.policy_attribute] = options and json.loads(options) or None
         return super().process(resources)
 
 


Test patch: