Skip to content

Commit

Permalink
Add cross account trust rule for ES and OpenSearch domains (#200)
Browse files Browse the repository at this point in the history
* upgrade to pycfmodel 0.13.0 in order to have ES and OpenSearch domains

* add cross account trust rules for ES and OpenSearch domains

* add tests and test files

* bump version and update changelog

Co-authored-by: Ramon <[email protected]>
  • Loading branch information
w0rmr1d3r and Ramon committed Jan 17, 2022
1 parent 5c388f8 commit 19dfee4
Show file tree
Hide file tree
Showing 13 changed files with 371 additions and 5 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
# Changelog
All notable changes to this project will be documented in this file.

## [1.3.0] - 2022-1-17
### Improvements
- Add `ElasticsearchDomainCrossAccountTrustRule` and `OpenSearchDomainCrossAccountTrustRule`
- Bump `pycfmodel` to `0.13.0`

## [1.2.2] - 2022-1-07
### Improvements
- Bump `pycfmodel` to `0.11.1`
Expand Down
2 changes: 1 addition & 1 deletion cfripper/__version__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
VERSION = (1, 2, 2)
VERSION = (1, 3, 0)

__version__ = ".".join(map(str, VERSION))
4 changes: 4 additions & 0 deletions cfripper/rules/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
from cfripper.rules.cross_account_trust import (
CrossAccountCheckingRule,
CrossAccountTrustRule,
ElasticsearchDomainCrossAccountTrustRule,
KMSKeyCrossAccountTrustRule,
OpenSearchDomainCrossAccountTrustRule,
S3CrossAccountTrustRule,
)
from cfripper.rules.ebs_volume_has_sse import EBSVolumeHasSSERule
Expand Down Expand Up @@ -51,6 +53,7 @@
EC2SecurityGroupIngressOpenToWorldRule,
EC2SecurityGroupMissingEgressRule,
EC2SecurityGroupOpenToWorldRule,
ElasticsearchDomainCrossAccountTrustRule,
FullWildcardPrincipalRule,
HardcodedRDSPasswordRule,
IAMRolesOverprivilegedRule,
Expand All @@ -59,6 +62,7 @@
KMSKeyWildcardPrincipalRule,
KMSKeyEnabledKeyRotation,
ManagedPolicyOnUserRule,
OpenSearchDomainCrossAccountTrustRule,
PartialWildcardPrincipalRule,
PolicyOnUserRule,
PrivilegeEscalationRule,
Expand Down
60 changes: 60 additions & 0 deletions cfripper/rules/cross_account_trust.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
__all__ = [
"CrossAccountCheckingRule",
"CrossAccountTrustRule",
"ElasticsearchDomainCrossAccountTrustRule",
"KMSKeyCrossAccountTrustRule",
"OpenSearchDomainCrossAccountTrustRule",
"S3CrossAccountTrustRule",
]

Expand All @@ -10,8 +12,10 @@
from typing import Dict, Optional, Set

from pycfmodel.model.cf_model import CFModel
from pycfmodel.model.resources.es_domain import ESDomain
from pycfmodel.model.resources.iam_role import IAMRole
from pycfmodel.model.resources.kms_key import KMSKey
from pycfmodel.model.resources.opensearch_domain import OpenSearchDomain
from pycfmodel.model.resources.properties.statement import Statement
from pycfmodel.model.resources.resource import Resource
from pycfmodel.model.resources.s3_bucket_policy import S3BucketPolicy
Expand Down Expand Up @@ -185,3 +189,59 @@ class KMSKeyCrossAccountTrustRule(CrossAccountCheckingRule):
REASON = "{} has forbidden cross-account policy allow with {} for an KMS Key Policy"
RESOURCE_TYPE = KMSKey
PROPERTY_WITH_POLICYDOCUMENT = "KeyPolicy"


class ElasticsearchDomainCrossAccountTrustRule(CrossAccountCheckingRule):
"""
Checks for Elasticsearch domains that allow cross-account principals to get access.
Risk:
It might allow other AWS identities to read/modify data.
Fix:
If cross account permissions are required for ES domains, the stack should be added to the allowlist for this rule.
Otherwise, the access should be removed from the CloudFormation definition.
Filters context:
| Parameter | Type | Description |
|:-----------:|:-----------:|:--------------------------------------------------------------:|
|`config` | str | `config` variable available inside the rule |
|`extras` | str | `extras` variable available inside the rule |
|`logical_id` | str | ID used in Cloudformation to refer the resource being analysed |
|`resource` | `ESDomain` | Resource that is being addressed |
|`statement` | `Statement` | Statement being checked found in the Resource |
|`principal` | `str` | AWS Principal being checked found in the statement |
|`account_id` | `str` | Account ID found in the principal |
"""

REASON = "{} has forbidden cross-account policy allow with {} for an ES domain policy."
RESOURCE_TYPE = ESDomain
PROPERTY_WITH_POLICYDOCUMENT = "AccessPolicies"


class OpenSearchDomainCrossAccountTrustRule(CrossAccountCheckingRule):
"""
Checks for OpenSearch domains that allow cross-account principals to get access.
Risk:
It might allow other AWS identities to read/modify data.
Fix:
If cross account permissions are required for OpenSearch domains, the stack should be added to the allowlist for this rule.
Otherwise, the access should be removed from the CloudFormation definition.
Filters context:
| Parameter | Type | Description |
|:-----------:|:------------------:|:--------------------------------------------------------------:|
|`config` | str | `config` variable available inside the rule |
|`extras` | str | `extras` variable available inside the rule |
|`logical_id` | str | ID used in Cloudformation to refer the resource being analysed |
|`resource` | `OpenSearchDomain` | Resource that is being addressed |
|`statement` | `Statement` | Statement being checked found in the Resource |
|`principal` | `str` | AWS Principal being checked found in the statement |
|`account_id` | `str` | Account ID found in the principal |
"""

REASON = "{} has forbidden cross-account policy allow with {} for an OpenSearch domain policy."
RESOURCE_TYPE = OpenSearchDomain
PROPERTY_WITH_POLICYDOCUMENT = "AccessPolicies"
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ cfn-flip==1.2.3
click==7.1.2
jmespath==0.10.0
pluggy==0.13.1
pycfmodel==0.11.1
pycfmodel==0.13.0
pydantic==1.8.2
pydash==4.7.6
python-dateutil==2.8.1
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"cfn_flip>=1.2.0",
"click~=7.1.1",
"pluggy~=0.13.1",
"pycfmodel>=0.11.1",
"pycfmodel>=0.13.0",
"pydash~=4.7.6",
"PyYAML>=4.2b1",
]
Expand Down
183 changes: 181 additions & 2 deletions tests/rules/test_CrossAccountTrustRule.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,13 @@
from cfripper.model.enums import RuleGranularity, RuleMode, RuleRisk
from cfripper.model.result import Failure
from cfripper.rule_processor import RuleProcessor
from cfripper.rules import DEFAULT_RULES, KMSKeyCrossAccountTrustRule
from cfripper.rules.cross_account_trust import CrossAccountTrustRule
from cfripper.rules import DEFAULT_RULES
from cfripper.rules.cross_account_trust import (
CrossAccountTrustRule,
ElasticsearchDomainCrossAccountTrustRule,
KMSKeyCrossAccountTrustRule,
OpenSearchDomainCrossAccountTrustRule,
)
from tests.utils import compare_lists_of_failures, get_cfmodel_from


Expand Down Expand Up @@ -35,11 +40,31 @@ def template_valid_with_sts():
return get_cfmodel_from("rules/CrossAccountTrustRule/valid_with_sts.yml").resolve()


@pytest.fixture()
def template_valid_with_sts_es_domain():
return get_cfmodel_from("rules/CrossAccountTrustRule/valid_with_sts_es_domain.yml").resolve()


@pytest.fixture()
def template_valid_with_sts_opensearch_domain():
return get_cfmodel_from("rules/CrossAccountTrustRule/valid_with_sts_opensearch_domain.yml").resolve()


@pytest.fixture()
def template_invalid_with_sts():
return get_cfmodel_from("rules/CrossAccountTrustRule/invalid_with_sts.yml").resolve()


@pytest.fixture()
def template_invalid_with_sts_es_domain():
return get_cfmodel_from("rules/CrossAccountTrustRule/invalid_with_sts_es_domain.yml").resolve()


@pytest.fixture()
def template_invalid_with_sts_opensearch_domain():
return get_cfmodel_from("rules/CrossAccountTrustRule/invalid_with_sts_opensearch_domain.yml").resolve()


@pytest.fixture()
def expected_result_two_roles():
return [
Expand Down Expand Up @@ -263,3 +288,157 @@ def test_sts_failure(template_invalid_with_sts):
)
],
)


@pytest.mark.parametrize(
"principal",
[
"arn:aws:iam::999999999:root",
"arn:aws:iam::*:root",
"arn:aws:iam::*:*",
"arn:aws:iam::*:root*",
"arn:aws:iam::*:not-root*",
"*",
],
)
def test_es_domain_cross_account_failure(principal):
rule = ElasticsearchDomainCrossAccountTrustRule(Config(aws_account_id="123456789", aws_principals=["999999999"]))
model = get_cfmodel_from("rules/CrossAccountTrustRule/es_domain_basic.yml").resolve(
extra_params={"Principal": principal}
)
result = rule.invoke(model)
assert not result.valid
assert compare_lists_of_failures(
result.failures,
[
Failure(
granularity=RuleGranularity.RESOURCE,
reason=f"TestDomain has forbidden cross-account policy allow with {principal} for an ES domain policy.",
risk_value=RuleRisk.MEDIUM,
rule="ElasticsearchDomainCrossAccountTrustRule",
rule_mode=RuleMode.BLOCKING,
actions=None,
resource_ids={"TestDomain"},
)
],
)


@pytest.mark.parametrize(
"principal", ["arn:aws:iam::123456789:root", "arn:aws:iam::123456789:not-root", "arn:aws:iam::123456789:not-root*"],
)
def test_es_domain_cross_account_success(principal):
rule = ElasticsearchDomainCrossAccountTrustRule(Config(aws_account_id="123456789", aws_principals=["999999999"]))
model = get_cfmodel_from("rules/CrossAccountTrustRule/es_domain_basic.yml").resolve(
extra_params={"Principal": principal}
)
result = rule.invoke(model)

assert result.valid
assert compare_lists_of_failures(result.failures, [])


def test_sts_valid_es_domain(template_valid_with_sts_es_domain):
rule = ElasticsearchDomainCrossAccountTrustRule(Config(aws_account_id="123456789", aws_principals=["999999999"]))
result = rule.invoke(template_valid_with_sts_es_domain)

assert result.valid
assert compare_lists_of_failures(result.failures, [])


def test_sts_failure_es_domain(template_invalid_with_sts_es_domain):
rule = ElasticsearchDomainCrossAccountTrustRule(Config(aws_account_id="123456789", aws_principals=["999999999"]))
result = rule.invoke(template_invalid_with_sts_es_domain)

assert not result.valid
assert compare_lists_of_failures(
result.failures,
[
Failure(
granularity=RuleGranularity.RESOURCE,
reason="TestDomain has forbidden cross-account policy allow with arn:aws:sts::999999999:assumed-role/test-role/session for an ES domain policy.",
risk_value=RuleRisk.MEDIUM,
rule="ElasticsearchDomainCrossAccountTrustRule",
rule_mode=RuleMode.BLOCKING,
actions=None,
resource_ids={"TestDomain"},
)
],
)


@pytest.mark.parametrize(
"principal",
[
"arn:aws:iam::999999999:root",
"arn:aws:iam::*:root",
"arn:aws:iam::*:*",
"arn:aws:iam::*:root*",
"arn:aws:iam::*:not-root*",
"*",
],
)
def test_opensearch_domain_cross_account_failure(principal):
rule = OpenSearchDomainCrossAccountTrustRule(Config(aws_account_id="123456789", aws_principals=["999999999"]))
model = get_cfmodel_from("rules/CrossAccountTrustRule/opensearch_domain_basic.yml").resolve(
extra_params={"Principal": principal}
)
result = rule.invoke(model)
assert not result.valid
assert compare_lists_of_failures(
result.failures,
[
Failure(
granularity=RuleGranularity.RESOURCE,
reason=f"TestDomain has forbidden cross-account policy allow with {principal} for an OpenSearch domain policy.",
risk_value=RuleRisk.MEDIUM,
rule="OpenSearchDomainCrossAccountTrustRule",
rule_mode=RuleMode.BLOCKING,
actions=None,
resource_ids={"TestDomain"},
)
],
)


@pytest.mark.parametrize(
"principal", ["arn:aws:iam::123456789:root", "arn:aws:iam::123456789:not-root", "arn:aws:iam::123456789:not-root*"],
)
def test_opensearch_domain_cross_account_success(principal):
rule = OpenSearchDomainCrossAccountTrustRule(Config(aws_account_id="123456789", aws_principals=["999999999"]))
model = get_cfmodel_from("rules/CrossAccountTrustRule/opensearch_domain_basic.yml").resolve(
extra_params={"Principal": principal}
)
result = rule.invoke(model)

assert result.valid
assert compare_lists_of_failures(result.failures, [])


def test_sts_valid_opensearch_domain(template_valid_with_sts_opensearch_domain):
rule = OpenSearchDomainCrossAccountTrustRule(Config(aws_account_id="123456789", aws_principals=["999999999"]))
result = rule.invoke(template_valid_with_sts_opensearch_domain)

assert result.valid
assert compare_lists_of_failures(result.failures, [])


def test_sts_failure_opensearch_domain(template_invalid_with_sts_opensearch_domain):
rule = OpenSearchDomainCrossAccountTrustRule(Config(aws_account_id="123456789", aws_principals=["999999999"]))
result = rule.invoke(template_invalid_with_sts_opensearch_domain)

assert not result.valid
assert compare_lists_of_failures(
result.failures,
[
Failure(
granularity=RuleGranularity.RESOURCE,
reason="TestDomain has forbidden cross-account policy allow with arn:aws:sts::999999999:assumed-role/test-role/session for an OpenSearch domain policy.",
risk_value=RuleRisk.MEDIUM,
rule="OpenSearchDomainCrossAccountTrustRule",
rule_mode=RuleMode.BLOCKING,
actions=None,
resource_ids={"TestDomain"},
)
],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
AWSTemplateFormatVersion: "2010-09-09"
Description: Testing ES Domain

Parameters:
Principal:
Type: String

Resources:
TestDomain:
Type: AWS::Elasticsearch::Domain
Properties:
DomainName: "test"
AccessPolicies:
Version: "2012-10-17"
Statement:
- Sid: "Allow full access of the domain"
Effect: "Allow"
Principal:
AWS: !Ref Principal
Action: "es:*"
Resource: "arn:aws:es:*:123456789012:domain/test/*"
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
AWSTemplateFormatVersion: "2010-09-09"
Description: Testing ES Domain

Resources:
TestDomain:
Type: AWS::Elasticsearch::Domain
Properties:
DomainName: "test"
AccessPolicies:
Version: "2012-10-17"
Statement:
- Sid: "Allow full access of the domain"
Effect: "Allow"
Principal:
AWS:
- "arn:aws:iam::123456789:role/test-role"
- "arn:aws:sts::999999999:assumed-role/test-role/session"
Action: "es:*"
Resource: "arn:aws:es:*:123456789012:domain/test/*"
Loading

0 comments on commit 19dfee4

Please sign in to comment.