diff --git a/.flake8 b/.flake8 deleted file mode 100644 index dc1e5391..00000000 --- a/.flake8 +++ /dev/null @@ -1,4 +0,0 @@ -[flake8] -max-line-length=120 -# E203 and E501 are ours, the others are the default from flake -ignore = E121,E123,E126,E226,E24,E704,W503,W504,E203,E501 \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..2e396efa --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,17 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "pip" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "monthly" + open-pull-requests-limit: 1 + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + open-pull-requests-limit: 1 diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..7f7c791c --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,7 @@ +# Description + + + +## Checklist + +- [ ] I have updated the CHANGELOG.md file accordingly diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 00000000..1318f25c --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,19 @@ +name-template: 'v$RESOLVED_VERSION' +tag-template: 'v$RESOLVED_VERSION' +change-template: '- $TITLE @$AUTHOR (#$NUMBER)' +change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks. +version-resolver: + major: + labels: + - 'major' + minor: + labels: + - 'minor' + patch: + labels: + - 'patch' + default: patch +template: | + ## Changes + + $CHANGES \ No newline at end of file diff --git a/.github/workflows/lint-and-test.yml b/.github/workflows/lint-and-test.yml index e5eae075..ac1aa702 100644 --- a/.github/workflows/lint-and-test.yml +++ b/.github/workflows/lint-and-test.yml @@ -9,15 +9,15 @@ jobs: strategy: fail-fast: false matrix: - python-version: ['3.7', '3.8', '3.9', '3.10'] + python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] name: Python ${{ matrix.python-version }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Setup python - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} diff --git a/.github/workflows/pypi-release.yml b/.github/workflows/pypi-release.yml index 69ebb89f..7e36ce6a 100644 --- a/.github/workflows/pypi-release.yml +++ b/.github/workflows/pypi-release.yml @@ -5,35 +5,33 @@ on: types: [published] jobs: - build: + pypi-publish: + name: Upload release to PyPI runs-on: ubuntu-latest - + environment: + name: pypi + url: https://pypi.org/p/cfripper + permissions: + id-token: write steps: - - uses: actions/checkout@v2 + - name: Checkout + uses: actions/checkout@v4 - name: Setup python - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: - python-version: '3.7' - - - run: pip install setuptools wheel + python-version: '3.9' - - run: make install + - name: Install dependencies + run: | + python -m pip install -U pip setuptools + python -m pip install -U twine build setuptools-scm - - name: Build a binary wheel - run: python setup.py sdist bdist_wheel - -# This doesn't add any value. It mostly passes, and if a release fails in the non-test PyPi -# this step then needs to be skipped. -# Leaving it commented until we find a cause to keep it that adds value to the project. -# - name: Publish distribution 📦 to Test PyPI -# uses: pypa/gh-action-pypi-publish@master -# with: -# password: ${{ secrets.test_pypi_password }} -# repository_url: https://test.pypi.org/legacy/ + - name: Build package + run: | + python -m setuptools_scm + python -m build + twine check --strict dist/* - name: Publish distribution 📦 to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 - with: - user: __token__ - password: ${{ secrets.PYPI_TOKEN }} + uses: pypa/gh-action-pypi-publish@release/v1 \ No newline at end of file diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml new file mode 100644 index 00000000..6da732b3 --- /dev/null +++ b/.github/workflows/release-drafter.yml @@ -0,0 +1,32 @@ +name: Release Drafter + +on: + push: + # branches to consider in the event; optional, defaults to all + branches: + - master + # pull_request event is required only for autolabeler + pull_request: + # Only following types are handled by the action, but one can default to all as well + types: [opened, reopened, synchronize] + # pull_request_target event is required for autolabeler to support PRs from forks + pull_request_target: + types: [opened, reopened, synchronize] + +permissions: + contents: read + +jobs: + update_release_draft: + permissions: + # write permission is required to create a github release + contents: write + # write permission is required for autolabeler + # otherwise, read permission is required at least + pull-requests: write + runs-on: ubuntu-latest + steps: + # Drafts your next Release notes as Pull Requests are merged into "master" + - uses: release-drafter/release-drafter@v6 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/test-docs.yml b/.github/workflows/test-docs.yml index d139947e..0ecf0089 100644 --- a/.github/workflows/test-docs.yml +++ b/.github/workflows/test-docs.yml @@ -9,10 +9,10 @@ jobs: name: Test Docs steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Setup python - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: 3.9 diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 69eb36d7..d209ee51 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -5,8 +5,12 @@ mkdocs: formats: all +build: + os: ubuntu-22.04 + tools: + python: "3.9" + python: - version: 3.7 install: - method: pip path: . diff --git a/CHANGELOG.md b/CHANGELOG.md index 34dd0e8f..07668c92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,63 @@ # Changelog All notable changes to this project will be documented in this file. +## [1.17.0] +### Additions +- Add support for python 3.13 +### Removals +- Remove support for python 3.8 + +## [1.16.0] +### Additions +- Added 2 new filter functions: `set` and `sorted` + +## [1.15.7] +### Updates +- Bumped pycfmodel to use pydantic v2 +### Other updates +- Add PR template @w0rmr1d3r (#279) + +## [1.15.6] +### Fixes +- Fix logo in pypi @ignaciobolonio (#274) +### Updates +- Update .readthedocs.yaml @jsoucheiron (#275) +### Bumps +- Bump actions/setup-python from 4 to 5 (#270) +- Bump cryptography from 42.0.3 to 42.0.4 (#272) + +## [1.15.5] +### Changes +- Migrate to pyproject.toml @jsoucheiron (#269) +- Add dependabot config @w0rmr1d3r (#257) + +## [1.15.4] +### Fixes +- Fix `KMSKeyWildcardPrincipalRule` to work without a KMS policy +- Fix release drafter template to show PR titles +### Updates +- Bumped minimum `pycfmodel` version to `0.22.0` + +## [1.15.3] +### Changes +- Update invalid_role_inline_policy_fn_if.json +- Improve logging for the exception when applying rule filters +- Add release drafter + +## [1.15.2] +### Fixes +- Fixes https://github.com/Skyscanner/cfripper/issues/260 + +## [1.15.1] +### Fixes +- Fix docs generation + +## [1.15.0] +### Additions +- New rules: `PublicELBCheckerRule`, `StackNameMatchesRegexRule`, and `StorageEncryptedRule` +- New regex: `REGEX_ALPHANUMERICAL_OR_HYPHEN` to check if stack name only consists of alphanumerical characters and hyphens. +- Config has a few extra methods that should make handling Filters easier + ## [1.14.0] ### Additions - `Config` includes a metrics logger, and it is called to register when a filter is used diff --git a/Makefile b/Makefile index fdd84caf..ebfda966 100644 --- a/Makefile +++ b/Makefile @@ -1,49 +1,50 @@ -SOURCE_DIRS = cfripper tests docs -SOURCE_FILES = setup.py -SOURCE_ALL = $(SOURCE_DIRS) $(SOURCE_FILES) +SOURCES = cfripper tests docs +PIP_COMMAND = pip install: - pip install -r requirements.txt + $(PIP_COMMAND) install -r requirements.txt -install-dev: install - pip install -e ".[dev]" +install-dev: + $(PIP_COMMAND) install -r requirements.txt -r requirements-dev.txt . install-docs: - pip install -e ".[dev,docs]" + $(PIP_COMMAND) install -r requirements.txt -r requirements-docs.txt . format: - isort --recursive $(SOURCE_ALL) - black $(SOURCE_ALL) + ruff format $(SOURCES) -lint: isort-lint black-lint flake8-lint - -isort-lint: - isort --check-only --recursive $(SOURCE_ALL) - -black-lint: - black --check $(SOURCE_ALL) - -flake8-lint: - flake8 $(SOURCE_ALL) +lint: + ruff check $(SOURCES) unit: pytest -svvv tests coverage: - coverage run --source=cfripper --branch -m pytest tests/ --junitxml=build/test.xml -v - coverage report - coverage xml -i -o build/coverage.xml - coverage html + pytest --cov cfripper test: lint unit test-docs: mkdocs build --strict -freeze: - CUSTOM_COMPILE_COMMAND="make freeze" pip-compile --no-emit-index-url --no-annotate --output-file requirements.txt setup.py - -freeze-upgrade: - CUSTOM_COMPILE_COMMAND="make freeze" pip-compile --no-emit-index-url --upgrade --no-annotate --output-file requirements.txt setup.py - -.PHONY: install install-dev install-docs format lint isort-lint black-lint flake8-lint unit coverage test freeze freeze-upgrade +FREEZE_COMMAND = CUSTOM_COMPILE_COMMAND="make freeze" uv pip compile +FREEZE_OPTIONS = --no-emit-index-url --no-annotate -v +freeze-base: pyproject.toml + $(FREEZE_COMMAND) $(FREEZE_OPTIONS) pyproject.toml --output-file requirements.txt +freeze-dev: pyproject.toml + $(FREEZE_COMMAND) $(FREEZE_OPTIONS) pyproject.toml --extra dev --output-file requirements-dev.txt +freeze-docs: pyproject.toml + $(FREEZE_COMMAND) $(FREEZE_OPTIONS) pyproject.toml --extra dev --extra docs --output-file requirements-docs.txt +freeze: freeze-base freeze-dev freeze-docs + +freeze-upgrade-base: + $(FREEZE_COMMAND) $(FREEZE_OPTIONS) pyproject.toml --upgrade --output-file requirements.txt +freeze-upgrade-dev: + $(FREEZE_COMMAND) $(FREEZE_OPTIONS) pyproject.toml --upgrade --extra dev --output-file requirements-dev.txt +freeze-upgrade-docs: + $(FREEZE_COMMAND) $(FREEZE_OPTIONS) pyproject.toml --upgrade --extra docs --extra dev --output-file requirements-docs.txt +freeze-upgrade: freeze-upgrade-base freeze-upgrade-dev freeze-upgrade-docs + + +.PHONY: install install-dev install-docs format lint unit coverage test freeze freeze-upgrade\ + freeze-base freeze-dev freeze-docs freeze-upgrade-base freeze-upgrade-dev freeze-upgrade-docs diff --git a/README.md b/README.md index 287364e6..90135173 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@
- +
# CFRipper ![Build Status](https://github.com/Skyscanner/cfripper/workflows/PyPI%20release/badge.svg) [![PyPI version](https://badge.fury.io/py/cfripper.svg)](https://badge.fury.io/py/cfripper) -[![Total alerts](https://img.shields.io/lgtm/alerts/g/Skyscanner/cfripper.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/Skyscanner/cfripper/alerts/) -[![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/Skyscanner/cfripper.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/Skyscanner/cfripper/context:python) +[![homebrew version](https://img.shields.io/homebrew/v/cfripper)](https://formulae.brew.sh/formula/cfripper) +![License](https://img.shields.io/github/license/skyscanner/cfripper) CFRipper is a Library and CLI security analyzer for AWS CloudFormation templates. You can use CFRipper to prevent deploying insecure AWS resources into your Cloud environment. You can write your own compliance checks by adding new custom plugins. diff --git a/cfripper/__version__.py b/cfripper/__version__.py deleted file mode 100644 index ae85267d..00000000 --- a/cfripper/__version__.py +++ /dev/null @@ -1,3 +0,0 @@ -VERSION = (1, 14, 0) - -__version__ = ".".join(map(str, VERSION)) diff --git a/cfripper/cli.py b/cfripper/cli.py index 32cf2710..e1739711 100644 --- a/cfripper/cli.py +++ b/cfripper/cli.py @@ -1,6 +1,7 @@ import logging import re import sys +from importlib.metadata import version from io import TextIOWrapper from pathlib import Path from typing import Dict, List, Optional, Tuple @@ -9,7 +10,6 @@ import pycfmodel from pycfmodel.model.cf_model import CFModel -from cfripper.__version__ import __version__ from cfripper.config.config import Config from cfripper.config.pluggy.utils import get_all_rules from cfripper.exceptions import FileEmptyException @@ -145,7 +145,7 @@ def validate_aws_principals(ctx: click.Context, param: str, value: str) -> Optio @click.command() -@click.version_option(prog_name="cfripper", version=__version__) +@click.version_option(prog_name="cfripper", version=version("cfripper")) @click.argument("templates", type=click.File("r"), nargs=-1) @click.option( "--resolve/--no-resolve", diff --git a/cfripper/config/config.py b/cfripper/config/config.py index 8bb811fa..7490c06f 100644 --- a/cfripper/config/config.py +++ b/cfripper/config/config.py @@ -3,11 +3,12 @@ import logging import sys from collections import defaultdict +from importlib.util import module_from_spec, spec_from_file_location from io import TextIOWrapper from pathlib import Path from typing import DefaultDict, Dict, List -from pydantic import BaseModel +from pydantic import RootModel from cfripper.config.constants import ( AWS_CLOUDTRAIL_ACCOUNT_IDS, @@ -117,6 +118,8 @@ class Config: "directconnect:", "trustedadvisor:", ] + RULES_CONFIG_MODULE_NAME = "__rules_config__" + FILTER_CONFIG_MODULE_NAME = "__filter_config__" def __init__( self, @@ -189,7 +192,7 @@ def load_rules_config_file(self, rules_config_file: TextIOWrapper): try: ext = Path(filename).suffix - module_name = "__rules_config__" + module_name = self.RULES_CONFIG_MODULE_NAME if ext not in [".py", ".pyc"]: raise RuntimeError("Configuration file should have a valid Python extension.") spec = importlib.util.spec_from_file_location(module_name, filename) @@ -198,44 +201,49 @@ def load_rules_config_file(self, rules_config_file: TextIOWrapper): spec.loader.exec_module(module) rules_config = vars(module).get("RULES_CONFIG") # Validate rules_config format - RulesConfigMapping(__root__=rules_config) + RulesConfigMapping.model_validate(rules_config) self.rules_config = rules_config except Exception: logger.exception(f"Failed to read config file: {filename}") raise def add_filters_from_dir(self, path: str): + self.add_filters(filters=self.get_filters_from_dir(path)) + + @classmethod + def get_filters_from_dir(cls, path: str) -> List[Filter]: + filters = [] + for filename in cls.get_filenames_from_dir(path): + try: + filters.extend(cls.get_filters_from_filename_path(filename)) + except Exception: + logger.exception(f"Failed to read files in path: {path} ({filename})") + raise + return filters + + @classmethod + def get_filenames_from_dir(cls, path: str) -> List[Path]: if not Path(path).is_dir(): raise RuntimeError(f"{path} doesn't exist") - - try: - module_name = "__rules_config__" - filenames = sorted(itertools.chain(Path(path).glob("*.py"), Path(path).glob("*.pyc"))) - for filename in filenames: - spec = importlib.util.spec_from_file_location(module_name, filename.absolute()) - module = importlib.util.module_from_spec(spec) - sys.modules[module_name] = module - spec.loader.exec_module(module) - filters = vars(module).get("FILTERS") - if not filters: - continue - # Validate filters format - RulesFiltersMapping(__root__=filters) - self.add_filters(filters=filters) - logger.debug(f"{filename} loaded") - except Exception: - logger.exception(f"Failed to read files in path: {path}") - raise + filenames = sorted(itertools.chain(Path(path).glob("*.py"), Path(path).glob("*.pyc"))) + return filenames + + @classmethod + def get_filters_from_filename_path(cls, filename: Path) -> List[Filter]: + spec = spec_from_file_location(cls.FILTER_CONFIG_MODULE_NAME, filename.absolute()) + module = module_from_spec(spec) + sys.modules[cls.FILTER_CONFIG_MODULE_NAME] = module + spec.loader.exec_module(module) + filters = vars(module).get("FILTERS") or [] + # Validate filters format + RulesFiltersMapping.model_validate(filters) + return filters def add_filters(self, filters: List[Filter]): - for filter in filters: - for rule in filter.rules: - self.rules_filters[rule].append(filter) - - -class RulesConfigMapping(BaseModel): - __root__: Dict[str, RuleConfig] + for rule_filter in filters: + for rule in rule_filter.rules: + self.rules_filters[rule].append(rule_filter) -class RulesFiltersMapping(BaseModel): - __root__: List[Filter] +RulesConfigMapping = RootModel[Dict[str, RuleConfig]] +RulesFiltersMapping = RootModel[List[Filter]] diff --git a/cfripper/config/filter.py b/cfripper/config/filter.py index 3f7c5870..b47e0963 100644 --- a/cfripper/config/filter.py +++ b/cfripper/config/filter.py @@ -2,28 +2,30 @@ import re from typing import Any, Callable, Dict, List, Optional, Set, Union -from pydantic import BaseModel, validator +from pydantic import BaseModel, field_validator from pydash.objects import get from cfripper.model.enums import RuleMode, RuleRisk -VALID_FUNCTIONS = [ +VALID_FUNCTIONS = { + "and", + "empty", "eq", - "ne", - "lt", + "exists", + "ge", "gt", + "in", "le", - "ge", + "lt", + "ne", "not", "or", - "and", - "in", + "ref", "regex", "regex:ignorecase", - "exists", - "empty", - "ref", -] + "set", + "sorted", +} logger = logging.getLogger(__file__) @@ -39,22 +41,37 @@ def wrap(*args, **kwargs): return wrap + def single_param_resolver(f): + def wrap(*args, **kwargs): + calculated_parameters = [arg(kwargs) for arg in args] + if len(calculated_parameters) == 1 and isinstance(calculated_parameters[0], (dict, set)): + result = f(*calculated_parameters, **kwargs) + else: + result = f(calculated_parameters, **kwargs) + if debug: + logger.debug(f"{function_name}({', '.join(str(x) for x in calculated_parameters)}) -> {result}") + return result + + return wrap + implemented_filter_functions = { + "and": lambda *args, **kwargs: all(arg(kwargs) for arg in args), + "empty": param_resolver(lambda *args, **kwargs: len(args) == 0), "eq": param_resolver(lambda a, b, **kwargs: a == b), - "ne": param_resolver(lambda a, b, **kwargs: a != b), - "lt": param_resolver(lambda a, b, **kwargs: a < b), + "exists": param_resolver(lambda a, **kwargs: a is not None), + "ge": param_resolver(lambda a, b, **kwargs: a >= b), "gt": param_resolver(lambda a, b, **kwargs: a > b), + "in": param_resolver(lambda a, b, **kwargs: a in b), "le": param_resolver(lambda a, b, **kwargs: a <= b), - "ge": param_resolver(lambda a, b, **kwargs: a >= b), + "lt": param_resolver(lambda a, b, **kwargs: a < b), + "ne": param_resolver(lambda a, b, **kwargs: a != b), "not": param_resolver(lambda a, **kwargs: not a), "or": lambda *args, **kwargs: any(arg(kwargs) for arg in args), - "and": lambda *args, **kwargs: all(arg(kwargs) for arg in args), - "in": param_resolver(lambda a, b, **kwargs: a in b), + "ref": param_resolver(lambda param_name, **kwargs: get(kwargs, param_name)), "regex": param_resolver(lambda *args, **kwargs: bool(re.match(*args))), "regex:ignorecase": param_resolver(lambda *args, **kwargs: bool(re.match(*args, re.IGNORECASE))), - "exists": param_resolver(lambda a, **kwargs: a is not None), - "empty": param_resolver(lambda *args, **kwargs: len(args) == 0), - "ref": param_resolver(lambda param_name, **kwargs: get(kwargs, param_name)), + "set": single_param_resolver(lambda *args, **kwargs: set(*args)), + "sorted": single_param_resolver(lambda *args, **kwargs: sorted(*args)), } return implemented_filter_functions[function_name] @@ -83,9 +100,10 @@ class Filter(BaseModel): risk_value: Optional[RuleRisk] = None rules: Set[str] = None - @validator("eval", pre=True) + @field_validator("eval", mode="before") + @classmethod def set_eval(cls, eval, values): - return build_evaluator(eval, values["debug"]) + return build_evaluator(eval, values.data["debug"]) def __call__(self, **kwargs): if self.debug: diff --git a/cfripper/config/regex.py b/cfripper/config/regex.py index 39bb40e2..7bd69ea6 100644 --- a/cfripper/config/regex.py +++ b/cfripper/config/regex.py @@ -173,3 +173,19 @@ - sns:Get* """ REGEX_HAS_STAR_OR_STAR_AFTER_COLON = re.compile(r"^(\w*:)*[*?]+$") + + +""" +Check that stack name only consists of alphanumerical characters and hyphens. +Valid: +- abcdefg +- ABCDEFG +- abcdEFG +- aBc-DeFG +- a1b2c3 +Invalid: +- abc_defg +- AB:cdefg +- !@£$$%aA +""" +REGEX_ALPHANUMERICAL_OR_HYPHEN = re.compile(r"^[A-Za-z0-9\-]+$") diff --git a/cfripper/model/result.py b/cfripper/model/result.py index 1c1e51f6..7426f938 100644 --- a/cfripper/model/result.py +++ b/cfripper/model/result.py @@ -1,6 +1,6 @@ from typing import Collection, List, Optional -from pydantic import BaseModel, Extra +from pydantic import BaseModel, ConfigDict from cfripper.model.enums import RuleGranularity, RuleMode, RuleRisk @@ -15,8 +15,7 @@ class Failure(BaseModel): resource_ids: Optional[set] = set() resource_types: Optional[set] = set() - class Config(BaseModel.Config): - extra = Extra.forbid + model_config = ConfigDict(extra="forbid") def serializable(self): return { @@ -32,9 +31,7 @@ def serializable(self): class Result(BaseModel): - class Config(BaseModel.Config): - extra = Extra.forbid - arbitrary_types_allowed = True + model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True) exceptions: List[Exception] = [] failures: List[Failure] = [] diff --git a/cfripper/rules/__init__.py b/cfripper/rules/__init__.py index 0522094f..6e0d0894 100644 --- a/cfripper/rules/__init__.py +++ b/cfripper/rules/__init__.py @@ -23,6 +23,7 @@ from cfripper.rules.managed_policy_on_user import ManagedPolicyOnUserRule from cfripper.rules.policy_on_user import PolicyOnUserRule from cfripper.rules.privilege_escalation import PrivilegeEscalationRule +from cfripper.rules.public_elb_checker_rule import PublicELBCheckerRule from cfripper.rules.rds_security_group import RDSSecurityGroupIngressOpenToWorldRule from cfripper.rules.s3_bucket_policy import S3BucketPolicyPrincipalRule from cfripper.rules.s3_lifecycle_configuration import S3LifecycleConfigurationRule @@ -38,6 +39,8 @@ SQSQueuePolicyNotPrincipalRule, SQSQueuePolicyPublicRule, ) +from cfripper.rules.stack_name_matches_regex import StackNameMatchesRegexRule +from cfripper.rules.storage_encrypted_rule import StorageEncryptedRule from cfripper.rules.wildcard_policies import ( GenericResourceWildcardPolicyRule, S3BucketPolicyWildcardActionRule, @@ -78,6 +81,7 @@ PartialWildcardPrincipalRule, PolicyOnUserRule, PrivilegeEscalationRule, + PublicELBCheckerRule, RDSSecurityGroupIngressOpenToWorldRule, S3BucketPolicyPrincipalRule, S3LifecycleConfigurationRule, @@ -87,6 +91,7 @@ S3BucketPublicReadAclRule, S3CrossAccountTrustRule, S3ObjectVersioningRule, + StorageEncryptedRule, SNSTopicDangerousPolicyActionsRule, SNSTopicPolicyNotPrincipalRule, SNSTopicPolicyWildcardActionRule, @@ -94,6 +99,7 @@ SQSQueuePolicyNotPrincipalRule, SQSQueuePolicyPublicRule, SQSQueuePolicyWildcardActionRule, + StackNameMatchesRegexRule, WildcardResourceRule, ) } diff --git a/cfripper/rules/base_rules.py b/cfripper/rules/base_rules.py index a735ef81..6e470865 100644 --- a/cfripper/rules/base_rules.py +++ b/cfripper/rules/base_rules.py @@ -67,7 +67,12 @@ def add_failure_to_result( if self._config.metrics_logger: self._config.metrics_logger(rule=self.__class__.__name__, filter_reason=rule_filter.reason) except Exception: - logger.exception(f"Exception raised while evaluating filter for `{rule_filter.reason}`", extra=context) + logger.exception( + f"Exception raised while evaluating rule {self.__class__.__name__} " + f"with filter for `{rule_filter.reason}`. " + f"Stack: {self._config.stack_name} Account: {self._config.aws_account_id}", + extra=context, + ) if rule_mode != RuleMode.ALLOWED: result.add_failure( diff --git a/cfripper/rules/kms_key_wildcard_principal.py b/cfripper/rules/kms_key_wildcard_principal.py index 3934cadf..4f7cc2f9 100644 --- a/cfripper/rules/kms_key_wildcard_principal.py +++ b/cfripper/rules/kms_key_wildcard_principal.py @@ -37,26 +37,27 @@ def invoke(self, cfmodel: CFModel, extras: Optional[Dict] = None) -> Result: result = Result() for logical_id, resource in cfmodel.Resources.items(): if isinstance(resource, KMSKey): - for statement in resource.Properties.KeyPolicy._statement_as_list(): - filtered_principals = statement.principals_with(self.CONTAINS_WILDCARD_PATTERN) - if statement.Effect == "Allow" and filtered_principals: - for principal in filtered_principals: - if statement.Condition and statement.Condition.dict(): - # Ignoring condition checks since they will get reviewed in other - # rules and future improvements - pass - else: - self.add_failure_to_result( - result, - self.REASON.format(logical_id), - resource_ids={logical_id}, - context={ - "config": self._config, - "extras": extras, - "logical_id": logical_id, - "resource": resource, - "statement": statement, - "principal": principal, - }, - ) + if resource.Properties.KeyPolicy: + for statement in resource.Properties.KeyPolicy._statement_as_list(): + filtered_principals = statement.principals_with(self.CONTAINS_WILDCARD_PATTERN) + if statement.Effect == "Allow" and filtered_principals: + for principal in filtered_principals: + if statement.Condition and statement.Condition.dict(): + # Ignoring condition checks since they will get reviewed in other + # rules and future improvements + pass + else: + self.add_failure_to_result( + result, + self.REASON.format(logical_id), + resource_ids={logical_id}, + context={ + "config": self._config, + "extras": extras, + "logical_id": logical_id, + "resource": resource, + "statement": statement, + "principal": principal, + }, + ) return result diff --git a/cfripper/rules/public_elb_checker_rule.py b/cfripper/rules/public_elb_checker_rule.py new file mode 100644 index 00000000..4f8a159d --- /dev/null +++ b/cfripper/rules/public_elb_checker_rule.py @@ -0,0 +1,41 @@ +from typing import Dict, Optional + +from pycfmodel.model.resources.generic_resource import GenericResource + +from cfripper.model.enums import RuleGranularity, RuleMode, RuleRisk +from cfripper.model.result import Result +from cfripper.rules.base_rules import ResourceSpecificRule + + +class PublicELBCheckerRule(ResourceSpecificRule): + """ + Rule to check if a public facing ELB is being created. + """ + + RESOURCE_TYPES = (GenericResource,) + ELB_RESOURCE_TYPES = ["AWS::ElasticLoadBalancing::LoadBalancer", "AWS::ElasticLoadBalancingV2::LoadBalancer"] + RISK_VALUE = RuleRisk.LOW + RULE_MODE = RuleMode.BLOCKING + REASON = "Creation of public facing ELBs is restricted. LogicalId: {}" + + def resource_invoke(self, resource: GenericResource, logical_id: str, extras: Optional[Dict] = None) -> Result: + result = Result() + if resource.Type in self.ELB_RESOURCE_TYPES: + elb_scheme = getattr(resource.Properties, "Scheme", "internal") + + if elb_scheme == "internet-facing": + self.add_failure_to_result( + result=result, + reason=self.REASON.format(logical_id), + resource_ids={logical_id}, + resource_types={resource.Type}, + context={ + "config": self._config, + "extras": extras, + "logical_id": logical_id, + "resource": resource, + }, + granularity=RuleGranularity.RESOURCE, + ) + + return result diff --git a/cfripper/rules/stack_name_matches_regex.py b/cfripper/rules/stack_name_matches_regex.py new file mode 100644 index 00000000..9307082b --- /dev/null +++ b/cfripper/rules/stack_name_matches_regex.py @@ -0,0 +1,45 @@ +from typing import Dict, Optional + +from pycfmodel.model.cf_model import CFModel + +from cfripper.config.regex import REGEX_ALPHANUMERICAL_OR_HYPHEN +from cfripper.model.enums import RuleGranularity, RuleMode, RuleRisk +from cfripper.model.result import Result +from cfripper.rules.base_rules import Rule + + +class StackNameMatchesRegexRule(Rule): + """ + Checks that a given stack follows the naming convention given by a regex. For this to work, + the stack name must be given either in the config or in the extras using the key + "stack_name". + """ + + RULE_MODE = RuleMode.DEBUG # for demonstration purposes + RISK_VALUE = RuleRisk.LOW + GRANULARITY = RuleGranularity.STACK + REASON = "The stack name {} does not follow the naming convention, reason: {}" + REGEX = REGEX_ALPHANUMERICAL_OR_HYPHEN + REGEX_REASON = "Only alphanumerical characters and hyphens allowed." + + def _stack_name_matches_regex(self, stack_name: str) -> bool: + """Check that stack name follows naming convention.""" + return bool(self.REGEX.match(stack_name)) + + def invoke(self, cfmodel: CFModel, extras: Optional[Dict] = None) -> Result: + result = Result() + if not extras: + extras = {} + stack_name = self._config.stack_name or extras.get("stack_name", "") + if not stack_name: + return result + + if not self._stack_name_matches_regex(stack_name): + self.add_failure_to_result( + result, + self.REASON.format(stack_name, self.REGEX_REASON), + self.GRANULARITY, + risk_value=self.RISK_VALUE, + context={"config": self._config, "extras": extras}, + ) + return result diff --git a/cfripper/rules/storage_encrypted_rule.py b/cfripper/rules/storage_encrypted_rule.py new file mode 100644 index 00000000..814daebf --- /dev/null +++ b/cfripper/rules/storage_encrypted_rule.py @@ -0,0 +1,39 @@ +from typing import Dict, Optional + +from pycfmodel.model.cf_model import CFModel + +from cfripper.model.enums import RuleGranularity, RuleMode, RuleRisk +from cfripper.model.result import Result +from cfripper.rules.base_rules import Rule + + +class StorageEncryptedRule(Rule): + RULE_MODE = RuleMode.DEBUG # for demonstration purposes + RISK_VALUE = RuleRisk.LOW + REASON = ( + "The database {} does not seem to be encrypted. Database resources should be encrypted and have the property " + "StorageEncrypted set to True." + ) + GRANULARITY = RuleGranularity.RESOURCE + + def invoke(self, cfmodel: CFModel, extras: Optional[Dict] = None) -> Result: + result = Result() + + for resource in cfmodel.Resources.values(): + is_encrypted = getattr(resource.Properties, "StorageEncrypted", False) + db_name = getattr(resource.Properties, "DBName", "(could not get DB name)") + if ( + resource.Type == "AWS::RDS::DBInstance" + and not is_encrypted + and not getattr(resource.Properties, "Engine", "").startswith( + "aurora" + ) # not applicable for aurora since the encryption for DB instances is managed by the DB cluster + ): + self.add_failure_to_result( + result, + self.REASON.format(db_name), + context={"config": self._config, "extras": extras}, + resource_types={resource.Type}, + ) + + return result diff --git a/docs/rule_config_and_filters.md b/docs/rule_config_and_filters.md index 78e6186c..3a412f37 100644 --- a/docs/rule_config_and_filters.md +++ b/docs/rule_config_and_filters.md @@ -29,7 +29,7 @@ When loading filters from a folder the order is alphabetical. ### Implemented filter functions | Function | Description | Example | -| :----------------: | :-------------------------------------------------------------------------: | :-------------------------------------: | +|:------------------:|:---------------------------------------------------------------------------:|:---------------------------------------:| | `eq` | Same as a == b | `{"eq": ["string", "string"]}` | | `ne` | Same as a != b | `{"ne": ["string", "not_that_string"]}` | | `lt` | Same as a < b | `{"lt": [0, 1]}` | @@ -45,6 +45,8 @@ When loading filters from a folder the order is alphabetical. | `exists` | True if a is not None | `{"exists": None}` | | `empty` | True if len(a) equals 0 | `{"empty": []}` | | `ref` | Get the value at any depth of the context based on the path described by a. | `{"ref": "param_a.param_b"}` | +| `set` | Turns the input into a set | `{"set": [80, 443]}` | +| `sorted` | Return the sorted input as a list | `{"sorted": [80, 443]}` | ### Examples diff --git a/pyproject.toml b/pyproject.toml index b937b719..fa85dcb0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,8 +1,132 @@ -[tool.black] +[build-system] +requires = ["setuptools>=64", "setuptools_scm>=8"] +build-backend = "setuptools.build_meta" + +[project] +name = "cfripper" +description="Library and CLI tool for analysing CloudFormation templates and check them for security compliance." +readme = "README.md" +requires-python = ">=3.9.0" +dynamic = ["version"] +license = { file = "LICENSE.md" } +authors = [ + { name = "Skyscanner Security", email = "security@skyscanner.net" } +] +keywords = [ + "security", + "cloudformation", + "aws", + "cli" +] + +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: Security" +] + +dependencies = [ + "boto3>=1.4.7,<2", + "cfn_flip>=1.2.0", + "click>=8.0.0", + "pluggy~=0.13.1", + "pycfmodel>=1.0.0", + "pydash>=4.7.6", + "PyYAML>=4.2b1" +] + +[project.urls] +documentation = "https://cfripper.readthedocs.io/" +repository = "https://github.com/Skyscanner/cfripper" + +[project.scripts] +cfripper = "cfripper.cli:cli" + +[project.optional-dependencies] +dev = [ + "moto[all]>=5", + "pytest-cov>=2.5.1", + "pytest>=3.6", + "ruff", + "uv", +] +docs = [ + "mkdocs==1.3.0", + "mkdocs-macros-plugin==0.7.0", + "mkdocs-material==8.2.8", + "mkdocs-material-extensions==1.0.3", + "mkdocs-minify-plugin==0.5.0", +] + +[tool.ruff] +# Exclude a variety of commonly ignored directories. +exclude = [ + ".eggs", + ".git", + ".git-rewrite", + ".pyenv", + ".pytest_cache", + ".ruff_cache", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "build", + "dist", + "node_modules", + "site", + "site-packages", + "venv", +] line-length = 120 -exclude = ''' -/( - | \.venv - | venv -)/ -''' +indent-width = 4 +target-version = "py38" + +[tool.ruff.lint] +select = ["E", "F", "W", "A", "PLC", "PLE", "PLW", "I"] +ignore = ["A002", "E501"] + +# Allow fix for all enabled rules (when `--fix`) is provided. +fixable = ["ALL"] +unfixable = [] + +# Allow unused variables when underscore-prefixed. +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" +skip-magic-trailing-comma = false +line-ending = "auto" +docstring-code-format = false +docstring-code-line-length = "dynamic" + +[tool.pytest.ini_options] +log_cli = true +log_level = "INFO" + +[tool.coverage.report] +show_missing = true +skip_covered = false + +[tool.coverage.run] +branch = true +source = ["cfripper"] + +[tool.setuptools] +include-package-data = false + +[tool.setuptools.packages.find] +# needed only because we did not adopt src layout yet +include = ["cfripper*"] + +[tool.setuptools_scm] diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 00000000..b4134c79 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,76 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile --no-emit-index-url --no-annotate pyproject.toml --extra dev --output-file requirements-dev.txt +annotated-types==0.7.0 +antlr4-python3-runtime==4.13.2 +attrs==24.2.0 +aws-sam-translator==1.91.0 +aws-xray-sdk==2.14.0 +boto3==1.35.54 +botocore==1.35.54 +certifi==2024.8.30 +cffi==1.17.1 +cfn-flip==1.3.0 +cfn-lint==1.18.3 +charset-normalizer==3.4.0 +click==8.1.7 +coverage==7.6.1 +cryptography==43.0.3 +docker==7.1.0 +exceptiongroup==1.2.2 +graphql-core==3.2.5 +idna==3.10 +importlib-resources==6.4.5 +iniconfig==2.0.0 +jinja2==3.1.4 +jmespath==1.0.1 +joserfc==1.0.0 +jsondiff==2.2.1 +jsonpatch==1.33 +jsonpath-ng==1.7.0 +jsonpointer==3.0.0 +jsonschema==4.23.0 +jsonschema-path==0.3.3 +jsonschema-specifications==2023.12.1 +lazy-object-proxy==1.10.0 +markupsafe==2.1.5 +moto==5.0.18 +mpmath==1.3.0 +multipart==1.1.0 +networkx==3.1 +openapi-schema-validator==0.6.2 +openapi-spec-validator==0.7.1 +packaging==24.1 +pathable==0.4.3 +pkgutil-resolve-name==1.3.10 +pluggy==0.13.1 +ply==3.11 +py-partiql-parser==0.5.6 +pycfmodel==1.0.0 +pycparser==2.22 +pydantic==2.9.2 +pydantic-core==2.23.4 +pydash==8.0.3 +pyparsing==3.1.4 +pytest==7.4.4 +pytest-cov==5.0.0 +python-dateutil==2.9.0.post0 +pyyaml==6.0.2 +referencing==0.35.1 +regex==2024.9.11 +requests==2.32.3 +responses==0.25.3 +rfc3339-validator==0.1.4 +rpds-py==0.20.1 +ruff==0.7.2 +s3transfer==0.10.3 +setuptools==75.3.0 +six==1.16.0 +sympy==1.13.3 +tomli==2.0.2 +typing-extensions==4.12.2 +urllib3==1.26.20 +uv==0.4.29 +werkzeug==3.1.1 +wrapt==1.16.0 +xmltodict==0.14.2 +zipp==3.20.2 diff --git a/requirements-docs.txt b/requirements-docs.txt new file mode 100644 index 00000000..acd5f5c6 --- /dev/null +++ b/requirements-docs.txt @@ -0,0 +1,93 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile --no-emit-index-url --no-annotate pyproject.toml --extra docs --extra dev --output-file requirements-docs.txt +annotated-types==0.7.0 +antlr4-python3-runtime==4.13.2 +attrs==24.2.0 +aws-sam-translator==1.91.0 +aws-xray-sdk==2.14.0 +boto3==1.35.54 +botocore==1.35.54 +certifi==2024.8.30 +cffi==1.17.1 +cfn-flip==1.3.0 +cfn-lint==1.18.3 +charset-normalizer==3.4.0 +click==8.1.7 +coverage==7.6.1 +cryptography==43.0.3 +csscompressor==0.9.5 +docker==7.1.0 +exceptiongroup==1.2.2 +ghp-import==2.1.0 +graphql-core==3.2.5 +htmlmin==0.1.12 +idna==3.10 +importlib-metadata==8.5.0 +importlib-resources==6.4.5 +iniconfig==2.0.0 +jinja2==3.1.4 +jmespath==1.0.1 +joserfc==1.0.0 +jsmin==3.0.1 +jsondiff==2.2.1 +jsonpatch==1.33 +jsonpath-ng==1.7.0 +jsonpointer==3.0.0 +jsonschema==4.23.0 +jsonschema-path==0.3.3 +jsonschema-specifications==2023.12.1 +lazy-object-proxy==1.10.0 +markdown==3.7 +markupsafe==2.1.5 +mergedeep==1.3.4 +mkdocs==1.3.0 +mkdocs-macros-plugin==0.7.0 +mkdocs-material==8.2.8 +mkdocs-material-extensions==1.0.3 +mkdocs-minify-plugin==0.5.0 +moto==5.0.18 +mpmath==1.3.0 +multipart==1.1.0 +networkx==3.1 +openapi-schema-validator==0.6.2 +openapi-spec-validator==0.7.1 +packaging==24.1 +pathable==0.4.3 +pkgutil-resolve-name==1.3.10 +pluggy==0.13.1 +ply==3.11 +py-partiql-parser==0.5.6 +pycfmodel==1.0.0 +pycparser==2.22 +pydantic==2.9.2 +pydantic-core==2.23.4 +pydash==8.0.3 +pygments==2.18.0 +pymdown-extensions==10.12 +pyparsing==3.1.4 +pytest==7.4.4 +pytest-cov==5.0.0 +python-dateutil==2.9.0.post0 +pyyaml==6.0.2 +pyyaml-env-tag==0.1 +referencing==0.35.1 +regex==2024.9.11 +requests==2.32.3 +responses==0.25.3 +rfc3339-validator==0.1.4 +rpds-py==0.20.1 +ruff==0.7.2 +s3transfer==0.10.3 +setuptools==75.3.0 +six==1.16.0 +sympy==1.13.3 +termcolor==2.4.0 +tomli==2.0.2 +typing-extensions==4.12.2 +urllib3==1.26.20 +uv==0.4.29 +watchdog==4.0.2 +werkzeug==3.1.1 +wrapt==1.16.0 +xmltodict==0.14.2 +zipp==3.20.2 diff --git a/requirements.txt b/requirements.txt index 54afd96e..83d494ca 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,21 +1,19 @@ -# -# This file is autogenerated by pip-compile with Python 3.9 -# by the following command: -# -# make freeze -# -boto3==1.21.31 -botocore==1.24.31 +# This file was autogenerated by uv via the following command: +# uv pip compile --no-emit-index-url --no-annotate pyproject.toml --output-file requirements.txt +annotated-types==0.7.0 +boto3==1.35.54 +botocore==1.35.54 cfn-flip==1.3.0 -click==8.1.2 -jmespath==1.0.0 +click==8.1.7 +jmespath==1.0.1 pluggy==0.13.1 -pycfmodel==0.20.0 -pydantic==1.9.0 -pydash==6.0.0 -python-dateutil==2.8.2 -pyyaml==6.0 -s3transfer==0.5.2 +pycfmodel==1.0.0 +pydantic==2.9.2 +pydantic-core==2.23.4 +pydash==8.0.3 +python-dateutil==2.9.0.post0 +pyyaml==6.0.2 +s3transfer==0.10.3 six==1.16.0 -typing-extensions==4.1.1 -urllib3==1.26.17 +typing-extensions==4.12.2 +urllib3==1.26.20 diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 551f0cd8..00000000 --- a/setup.cfg +++ /dev/null @@ -1,7 +0,0 @@ -[isort] -multi_line_output = 3 -include_trailing_comma = True -force_grid_wrap = 0 -combine_as_imports = True -line_length = 120 -skip = .venv,venv diff --git a/setup.py b/setup.py deleted file mode 100644 index cf2c6a66..00000000 --- a/setup.py +++ /dev/null @@ -1,75 +0,0 @@ -from pathlib import Path - -from setuptools import find_packages, setup - -from cfripper.__version__ import __version__ - -project_root_path = Path(__file__).parent - -install_requires = [ - "boto3>=1.4.7,<2", - "cfn_flip>=1.2.0", - "click>=8.0.0", - "pluggy~=0.13.1", - "pycfmodel>=0.20.0", - "pydash>=4.7.6", - "PyYAML>=4.2b1", -] - -dev_requires = [ - "black==22.3.0", - "flake8>=3.3.0", - "isort==4.3.21", - "pytest>=3.6", - "pytest-cov>=2.5.1", - "pip-tools>=5.3.1", - "moto[cloudformation,s3]==3.1.9", # coverage fails for 3.1.10, issue is https://github.com/spulec/moto/issues/5162 -] - -docs_requires = [ - "click==8.1.2", - "csscompressor==0.9.5", - "ghp-import==2.0.2", - "htmlmin==0.1.12", - "importlib-metadata==4.11.3", - "Jinja2==3.1.1", - "jsmin==3.0.1", - "Markdown==3.3.6", - "MarkupSafe==2.1.1", - "mergedeep==1.3.4", - "mkdocs==1.3.0", - "mkdocs-exclude==1.0.2", - "mkdocs-macros-plugin==0.7.0", - "mkdocs-material==8.2.8", - "mkdocs-material-extensions==1.0.3", - "mkdocs-minify-plugin==0.5.0", - "packaging==21.3", - "Pygments==2.11.2", - "pymdown-extensions==9.3", - "pyparsing==3.0.7", - "python-dateutil==2.8.2", - "PyYAML==6.0", - "pyyaml_env_tag==0.1", - "six==1.16.0", - "termcolor==1.1.0", - "watchdog==2.1.7", - "zipp==3.8.0", -] - -setup( - name="cfripper", - version=__version__, - author="Skyscanner Product Security", - author_email="security@skyscanner.net", - entry_points={"console_scripts": ["cfripper=cfripper.cli:cli"]}, - long_description=(project_root_path / "README.md").read_text(), - long_description_content_type="text/markdown", - url="https://github.com/Skyscanner/cfripper", - description="Library and CLI tool for analysing CloudFormation templates and check them for security compliance.", - packages=find_packages(exclude=("docs", "tests")), - platforms="any", - python_requires=">=3.7", - install_requires=install_requires, - tests_require=dev_requires, - extras_require={"dev": dev_requires, "docs": docs_requires}, -) diff --git a/tests/config/test_filter.py b/tests/config/test_filter.py index fe2f11ee..87b87200 100644 --- a/tests/config/test_filter.py +++ b/tests/config/test_filter.py @@ -29,7 +29,7 @@ def template_security_group_firehose_ips(): @pytest.mark.parametrize( - "filter, args, expected_result", + "filter_name, args, expected_result", [ (Filter(eval={"eq": ["string", "string"]}), {}, True), (Filter(eval={"eq": [1, 1]}), {}, True), @@ -219,7 +219,26 @@ def template_security_group_firehose_ips(): (Filter(eval={"ref": "param_a.param_b.param_c"}), {"param_a": {"param_b": {"param_c": [1]}}}, [1]), (Filter(eval={"ref": "param_a.param_b.param_c"}), {"param_a": {"param_b": {"param_c": [-1]}}}, [-1]), (Filter(eval={"ref": "param_a.param_b.param_c"}), {"param_a": {"param_b": {"param_c": [1.0]}}}, [1.0]), - (Filter(eval={"ref": "param_a.param_b.param_c"}), {"param_a": {"param_b": {"param_c": [-1.0]}}}, [-1.0]), + (Filter(eval={"set": []}), {}, set()), + (Filter(eval={"set": {}}), {}, set()), + (Filter(eval={"set": set()}), {}, set()), + (Filter(eval={"set": {"80"}}), {}, {"80"}), + (Filter(eval={"set": ["80"]}), {}, {"80"}), + (Filter(eval={"set": {"80": 100}}), {}, {"80"}), + (Filter(eval={"set": {"80": 100, "90": 100}}), {}, {"80", "90"}), + (Filter(eval={"set": ["80", "443"]}), {}, {"80", "443"}), + (Filter(eval={"set": {"80", "443"}}), {}, {"80", "443"}), + (Filter(eval={"set": ["80", "443", "8080"]}), {}, {"80", "443", "8080"}), + (Filter(eval={"sorted": []}), {}, []), + (Filter(eval={"sorted": {}}), {}, []), + (Filter(eval={"sorted": set()}), {}, []), + (Filter(eval={"sorted": {"80"}}), {}, ["80"]), + (Filter(eval={"sorted": ["80"]}), {}, ["80"]), + (Filter(eval={"sorted": {"80": 100}}), {}, ["80"]), + (Filter(eval={"sorted": {"80": 100, "90": 100}}), {}, ["80", "90"]), + (Filter(eval={"sorted": ["80", "443"]}), {}, ["443", "80"]), + (Filter(eval={"sorted": {"80", "443"}}), {}, ["443", "80"]), + (Filter(eval={"sorted": ["80", "443", "8080"]}), {}, ["443", "80", "8080"]), # Composed (Filter(eval={"eq": [{"ref": "param_a"}, "a"]}), {"param_a": "a"}, True), (Filter(eval={"eq": ["a", {"ref": "param_a"}]}), {"param_a": "a"}, True), @@ -242,8 +261,8 @@ def template_security_group_firehose_ips(): ), ], ) -def test_filter(filter, args, expected_result): - assert filter(**args) == expected_result +def test_filter(filter_name, args, expected_result): + assert filter_name(**args) == expected_result def test_exist_function_and_property_does_not_exist(template_cross_account_role_no_name): diff --git a/tests/rules/test_CrossAccountTrustRule.py b/tests/rules/test_CrossAccountTrustRule.py index a34fee61..bb7bb890 100644 --- a/tests/rules/test_CrossAccountTrustRule.py +++ b/tests/rules/test_CrossAccountTrustRule.py @@ -301,6 +301,14 @@ def test_kms_key_cross_account_sts(template, is_valid, failures): assert compare_lists_of_failures(result.failures, failures) +def test_kms_key__without_policy(): + rule = KMSKeyCrossAccountTrustRule(Config(aws_account_id="123456789", aws_principals=["999999999"])) + model = get_cfmodel_from("rules/CrossAccountTrustRule/kms_key_without_policy.yml") + result = rule.invoke(model) + assert result.valid + assert compare_lists_of_failures(result.failures, []) + + @pytest.mark.parametrize( "principal", [ diff --git a/tests/rules/test_KMSKeyWildcardPrincipal.py b/tests/rules/test_KMSKeyWildcardPrincipal.py index 72f81953..34d30cfa 100644 --- a/tests/rules/test_KMSKeyWildcardPrincipal.py +++ b/tests/rules/test_KMSKeyWildcardPrincipal.py @@ -1,22 +1,49 @@ -# import pytest -# -# from cfripper.rules.KMSKeyWildcardPrincipal import KMSKeyWildcardPrincipal -# from cfripper.model.result import Result -# from tests.utils import get_cfmodel_from - -# TODO Implement check if this is needed as GenericWildcardPrincipal rule seems to include this one -# @pytest.fixture() -# def abcdef(): -# return get_cfmodel_from("rules/KMSKeyWildcardPrincipal/abcdef.json").resolve() -# -# -# def test_abcdef(abcdef): -# result = Result() -# rule = KMSKeyWildcardPrincipal(None, result) -# rule.invoke(abcdef) -# -# assert not result.valid -# assert len(result.failed_rules) == 1 -# assert len(result.failed_monitored_rules) == 0 -# assert result.failed_rules[0].rule == "KMSKeyWildcardPrincipal" -# assert result.failed_rules[0].reason == "KMS Key policy {} should not allow wildcard principals" +import pytest + +from cfripper.model.result import Failure +from cfripper.rules import KMSKeyWildcardPrincipalRule +from tests.utils import compare_lists_of_failures, get_cfmodel_from + + +@pytest.fixture() +def kms_key_with_wildcard_policy(): + return get_cfmodel_from("rules/KMSKeyWildcardPrincipalRule/kms_key_with_wildcard_resource.json").resolve() + + +@pytest.fixture() +def kms_key_without_policy(): + return get_cfmodel_from("rules/KMSKeyWildcardPrincipalRule/kms_key_without_policy.yml").resolve() + + +def test_kms_key_with_wildcard_resource_not_allowed_is_flagged(kms_key_with_wildcard_policy): + rule = KMSKeyWildcardPrincipalRule(None) + rule._config.stack_name = "stack3" + rule.all_cf_actions = set() + result = rule.invoke(kms_key_with_wildcard_policy) + + assert result.valid is False + assert compare_lists_of_failures( + result.failures, + [ + Failure( + granularity="RESOURCE", + reason="KMS Key policy myKey should not allow wildcard principals", + risk_value="MEDIUM", + rule="KMSKeyWildcardPrincipalRule", + rule_mode="BLOCKING", + actions=None, + resource_ids={"myKey"}, + resource_types=None, + ) + ], + ) + + +def test_kms_key_without_policy_is_not_flagged(kms_key_without_policy): + rule = KMSKeyWildcardPrincipalRule(None) + rule._config.stack_name = "stack3" + rule.all_cf_actions = set() + result = rule.invoke(kms_key_without_policy) + + assert result.valid + assert compare_lists_of_failures(result.failures, []) diff --git a/tests/rules/test_PublicELBCheckerRule.py b/tests/rules/test_PublicELBCheckerRule.py new file mode 100644 index 00000000..5b195251 --- /dev/null +++ b/tests/rules/test_PublicELBCheckerRule.py @@ -0,0 +1,58 @@ +import pytest + +from cfripper.model.result import Failure +from cfripper.rules.public_elb_checker_rule import PublicELBCheckerRule +from tests.utils import get_cfmodel_from + + +@pytest.mark.parametrize( + "template", + [ + "rules/PublicELBCheckerRule/private_elb_instance.yml", + "rules/PublicELBCheckerRule/private_elb_v2_instance.yml", + ], +) +def test_invoke_private_elbs_passes(template): + rule = PublicELBCheckerRule(None) + rule._config.stack_name = "stackname" + result = rule.invoke(cfmodel=get_cfmodel_from(template).resolve()) + + assert result.valid + assert result.failures == [] + + +@pytest.mark.parametrize( + "template, logical_id, resource_type, reason", + [ + ( + "rules/PublicELBCheckerRule/public_facing_elb_instance.yml", + "PublicLoadBalancer", + "AWS::ElasticLoadBalancing::LoadBalancer", + "Creation of public facing ELBs is restricted. LogicalId: PublicLoadBalancer", + ), + ( + "rules/PublicELBCheckerRule/public_facing_elb_v2_instance.yml", + "PublicV2LoadBalancer", + "AWS::ElasticLoadBalancingV2::LoadBalancer", + "Creation of public facing ELBs is restricted. LogicalId: PublicV2LoadBalancer", + ), + ], +) +def test_invoke_public_elbs_fail(template, logical_id, resource_type, reason): + rule = PublicELBCheckerRule(None) + rule._config.stack_name = "stackname" + result = rule.invoke(cfmodel=get_cfmodel_from(template).resolve()) + + assert result.valid is False + assert result.failures == [ + Failure( + granularity="RESOURCE", + reason=reason, + risk_value="LOW", + rule="PublicELBCheckerRule", + rule_mode="BLOCKING", + actions=None, + resource_ids={logical_id}, + resource_types={resource_type}, + ) + ] diff --git a/tests/rules/test_S3LifecycleConfigurationRule.py b/tests/rules/test_S3LifecycleConfigurationRule.py index 4d7f5751..0c1ef036 100644 --- a/tests/rules/test_S3LifecycleConfigurationRule.py +++ b/tests/rules/test_S3LifecycleConfigurationRule.py @@ -17,7 +17,6 @@ def bad_template_no_configuration(): "template_path", [ "rules/S3LifecycleConfiguration/good_template.yaml", - "rules/S3LifecycleConfiguration/allowed_template_malformed_lifecycle_rules.yaml", ], ) def test_no_failures_are_raised(template_path): diff --git a/tests/rules/test_StackNameMatchesRegexRule.py b/tests/rules/test_StackNameMatchesRegexRule.py new file mode 100644 index 00000000..684f1377 --- /dev/null +++ b/tests/rules/test_StackNameMatchesRegexRule.py @@ -0,0 +1,60 @@ +import pytest +from pycfmodel.model.cf_model import CFModel + +from cfripper.config.config import Config +from cfripper.rules import StackNameMatchesRegexRule + + +@pytest.mark.parametrize( + "stack_name, expected_result", + [ + ("justlowercase", True), + ("lowercase-with-hyphens", True), + ("lowercaseANDUPPERCASE", True), + ("lowercase-AND-UPPERCASE-with-hyphens", True), + ("also-123-including-456-numbers", True), + ("including_underscore", False), + ("including space", False), + ("including-other-symbols!@£$%^&*()", False), + ], +) +def test_stack_name_matches_regex(stack_name, expected_result): + rule = StackNameMatchesRegexRule(Config(stack_name=stack_name, rules=["StackNameMatchesRegexRule"])) + assert rule._stack_name_matches_regex(stack_name) == expected_result + + +def test_works_with_extras(): + rule = StackNameMatchesRegexRule(Config(stack_name="some-valid-stack-name", rules=["StackNameMatchesRegexRule"])) + extras = {"stack": {"tags": [{"key": "project", "value": "some_project"}]}} + result = rule.invoke(cfmodel=CFModel(), extras=extras) + assert result.valid + + +def test_stack_name_from_extras(): + rule = StackNameMatchesRegexRule(Config(stack_name="some-valid-stack-name", rules=["StackNameMatchesRegexRule"])) + extras = {"stack": {"tags": [{"key": "project", "value": "some_project"}]}, "stack_name": "some_invalid_name"} + result = rule.invoke(cfmodel=CFModel(), extras=extras) + assert result.valid + + +def test_failure_is_added_for_invalid_stack_name(): + rule = StackNameMatchesRegexRule(Config(stack_name="some_invalid_stack_name", rules=["StackNameMatchesRegexRule"])) + result = rule.invoke(cfmodel=CFModel()) + assert result.failures + assert ( + result.failures[0].reason + == "The stack name some_invalid_stack_name does not follow the naming convention, reason: Only alphanumerical " + "characters and hyphens allowed." + ) + + +def test_failure_is_added_for_invalid_stack_name_from_extras(): + rule = StackNameMatchesRegexRule(Config(rules=["StackNameMatchesRegexRule"])) + extras = {"stack": {"tags": [{"key": "project", "value": "some_project"}]}, "stack_name": "some_invalid_stack_name"} + result = rule.invoke(cfmodel=CFModel(), extras=extras) + assert result.failures + assert ( + result.failures[0].reason + == "The stack name some_invalid_stack_name does not follow the naming convention, reason: Only alphanumerical " + "characters and hyphens allowed." + ) diff --git a/tests/rules/test_StorageEncryptedRule.py b/tests/rules/test_StorageEncryptedRule.py new file mode 100644 index 00000000..2e79ede1 --- /dev/null +++ b/tests/rules/test_StorageEncryptedRule.py @@ -0,0 +1,88 @@ +import pytest + +from cfripper.model.enums import RuleGranularity, RuleMode, RuleRisk +from cfripper.model.result import Failure +from cfripper.rules.storage_encrypted_rule import StorageEncryptedRule +from tests.utils import get_cfmodel_from + + +def test_storage_encrypted_rule_valid_results(): + rule = StorageEncryptedRule(None) + model = get_cfmodel_from("rules/StorageEncryptedRule/encrypted_db_resource.yml") + resolved_model = model.resolve() + result = rule.invoke(resolved_model) + + assert result.valid + assert result.failures == [] + + +def test_rule_not_failing_for_aurora(): + rule = StorageEncryptedRule(None) + model = get_cfmodel_from("rules/StorageEncryptedRule/aurora_engine_used.yml") + resolved_model = model.resolve() + result = rule.invoke(resolved_model) + + assert result.valid + assert result.failures == [] + + +@pytest.mark.parametrize( + "template, failures", + [ + ( + "rules/StorageEncryptedRule/missing_storage_encrypted_flag.yml", + [ + Failure( + granularity=RuleGranularity.RESOURCE, + reason="The database some-name does not seem to be encrypted. Database resources should be " + "encrypted and have the property StorageEncrypted set to True.", + risk_value=RuleRisk.LOW, + rule="StorageEncryptedRule", + rule_mode=RuleMode.DEBUG, + actions=None, + resource_ids=None, + resource_types={"AWS::RDS::DBInstance"}, + ) + ], + ), + ( + "rules/StorageEncryptedRule/two_resources_not_encrypted.yml", + [ + Failure( + granularity=RuleGranularity.RESOURCE, + reason="The database some-name does not seem to be encrypted. Database resources should be " + "encrypted and have the property StorageEncrypted set to True.", + risk_value=RuleRisk.LOW, + rule="StorageEncryptedRule", + rule_mode=RuleMode.DEBUG, + actions=None, + resource_ids=None, + resource_types={"AWS::RDS::DBInstance"}, + ), + Failure( + granularity=RuleGranularity.RESOURCE, + reason="The database some-name-backup does not seem to be encrypted. Database resources should be " + "encrypted and have the property StorageEncrypted set to True.", + risk_value=RuleRisk.LOW, + rule="StorageEncryptedRule", + rule_mode=RuleMode.DEBUG, + actions=None, + resource_ids=None, + resource_types={"AWS::RDS::DBInstance"}, + ), + ], + ), + ( + "rules/StorageEncryptedRule/no_db_resource.yml", + [], + ), + ], +) +def test_add_failure_if_db_resource_not_encrypted(template, failures): + rule = StorageEncryptedRule(None) + model = get_cfmodel_from(template) + resolved_model = model.resolve() + result = rule.invoke(resolved_model) + + assert result.valid + assert result.failures == failures diff --git a/tests/rules/test_WildcardResourceRule.py b/tests/rules/test_WildcardResourceRule.py index 14541df0..b4b7df15 100644 --- a/tests/rules/test_WildcardResourceRule.py +++ b/tests/rules/test_WildcardResourceRule.py @@ -17,7 +17,7 @@ def user_with_wildcard_resource(): @pytest.fixture() def kms_key_with_wildcard_policy(): - return get_cfmodel_from("rules/WildcardResourceRule/kms_key_with_wildcard_resource.json").resolve() + return get_cfmodel_from("rules/KMSKeyWildcardPrincipalRule/kms_key_with_wildcard_resource.json").resolve() @pytest.fixture() @@ -199,16 +199,16 @@ def test_multiple_resources_with_wildcard_resources_are_detected(user_and_policy rule="WildcardResourceRule", rule_mode="BLOCKING", actions={ - "dynamodb:CreateTable", - "dynamodb:BatchGet*", - "dynamodb:Scan", "dynamodb:Update*", + "dynamodb:DescribeStream", + "dynamodb:BatchGet*", + "dynamodb:CreateTable", "dynamodb:Query", "dynamodb:Delete*", - "dynamodb:PutItem", - "dynamodb:DescribeStream", - "dynamodb:DescribeTable", "dynamodb:BatchWrite*", + "dynamodb:DescribeTable", + "dynamodb:Scan", + "dynamodb:PutItem", "dynamodb:Get*", }, resource_ids={"RolePolicy"}, @@ -221,16 +221,16 @@ def test_multiple_resources_with_wildcard_resources_are_detected(user_and_policy rule="WildcardResourceRule", rule_mode="BLOCKING", actions={ - "dynamodb:CreateTable", - "dynamodb:BatchGet*", - "dynamodb:Scan", "dynamodb:Update*", + "dynamodb:DescribeStream", + "dynamodb:BatchGet*", + "dynamodb:CreateTable", "dynamodb:Query", "dynamodb:Delete*", - "dynamodb:PutItem", - "dynamodb:DescribeStream", - "dynamodb:DescribeTable", "dynamodb:BatchWrite*", + "dynamodb:DescribeTable", + "dynamodb:Scan", + "dynamodb:PutItem", "dynamodb:Get*", }, resource_ids={"RolePolicy"}, @@ -243,16 +243,16 @@ def test_multiple_resources_with_wildcard_resources_are_detected(user_and_policy rule="WildcardResourceRule", rule_mode="BLOCKING", actions={ - "dynamodb:CreateTable", - "dynamodb:BatchGet*", - "dynamodb:Scan", "dynamodb:Update*", + "dynamodb:DescribeStream", + "dynamodb:BatchGet*", + "dynamodb:CreateTable", "dynamodb:Query", "dynamodb:Delete*", - "dynamodb:PutItem", - "dynamodb:DescribeStream", - "dynamodb:DescribeTable", "dynamodb:BatchWrite*", + "dynamodb:DescribeTable", + "dynamodb:Scan", + "dynamodb:PutItem", "dynamodb:Get*", }, resource_ids={"RolePolicy"}, @@ -265,16 +265,16 @@ def test_multiple_resources_with_wildcard_resources_are_detected(user_and_policy rule="WildcardResourceRule", rule_mode="BLOCKING", actions={ - "dynamodb:CreateTable", - "dynamodb:BatchGet*", - "dynamodb:Scan", "dynamodb:Update*", + "dynamodb:DescribeStream", + "dynamodb:BatchGet*", + "dynamodb:CreateTable", "dynamodb:Query", "dynamodb:Delete*", - "dynamodb:PutItem", - "dynamodb:DescribeStream", - "dynamodb:DescribeTable", "dynamodb:BatchWrite*", + "dynamodb:DescribeTable", + "dynamodb:Scan", + "dynamodb:PutItem", "dynamodb:Get*", }, resource_ids={"RolePolicy"}, @@ -287,16 +287,38 @@ def test_multiple_resources_with_wildcard_resources_are_detected(user_and_policy rule="WildcardResourceRule", rule_mode="BLOCKING", actions={ - "dynamodb:CreateTable", - "dynamodb:BatchGet*", - "dynamodb:Scan", "dynamodb:Update*", + "dynamodb:DescribeStream", + "dynamodb:BatchGet*", + "dynamodb:CreateTable", "dynamodb:Query", "dynamodb:Delete*", + "dynamodb:BatchWrite*", + "dynamodb:DescribeTable", + "dynamodb:Scan", "dynamodb:PutItem", + "dynamodb:Get*", + }, + resource_ids={"RolePolicy"}, + resource_types={"AWS::IAM::Policy"}, + ), + Failure( + granularity="ACTION", + reason='"RolePolicy" is using a wildcard resource in "TheExtremePolicy" for "dynamodb:DeleteResourcePolicy"', + risk_value="MEDIUM", + rule="WildcardResourceRule", + rule_mode="BLOCKING", + actions={ + "dynamodb:Update*", "dynamodb:DescribeStream", - "dynamodb:DescribeTable", + "dynamodb:BatchGet*", + "dynamodb:CreateTable", + "dynamodb:Query", + "dynamodb:Delete*", "dynamodb:BatchWrite*", + "dynamodb:DescribeTable", + "dynamodb:Scan", + "dynamodb:PutItem", "dynamodb:Get*", }, resource_ids={"RolePolicy"}, @@ -309,16 +331,16 @@ def test_multiple_resources_with_wildcard_resources_are_detected(user_and_policy rule="WildcardResourceRule", rule_mode="BLOCKING", actions={ - "dynamodb:CreateTable", - "dynamodb:BatchGet*", - "dynamodb:Scan", "dynamodb:Update*", + "dynamodb:DescribeStream", + "dynamodb:BatchGet*", + "dynamodb:CreateTable", "dynamodb:Query", "dynamodb:Delete*", - "dynamodb:PutItem", - "dynamodb:DescribeStream", - "dynamodb:DescribeTable", "dynamodb:BatchWrite*", + "dynamodb:DescribeTable", + "dynamodb:Scan", + "dynamodb:PutItem", "dynamodb:Get*", }, resource_ids={"RolePolicy"}, @@ -331,16 +353,16 @@ def test_multiple_resources_with_wildcard_resources_are_detected(user_and_policy rule="WildcardResourceRule", rule_mode="BLOCKING", actions={ - "dynamodb:CreateTable", - "dynamodb:BatchGet*", - "dynamodb:Scan", "dynamodb:Update*", + "dynamodb:DescribeStream", + "dynamodb:BatchGet*", + "dynamodb:CreateTable", "dynamodb:Query", "dynamodb:Delete*", - "dynamodb:PutItem", - "dynamodb:DescribeStream", - "dynamodb:DescribeTable", "dynamodb:BatchWrite*", + "dynamodb:DescribeTable", + "dynamodb:Scan", + "dynamodb:PutItem", "dynamodb:Get*", }, resource_ids={"RolePolicy"}, @@ -353,16 +375,16 @@ def test_multiple_resources_with_wildcard_resources_are_detected(user_and_policy rule="WildcardResourceRule", rule_mode="BLOCKING", actions={ - "dynamodb:CreateTable", - "dynamodb:BatchGet*", - "dynamodb:Scan", "dynamodb:Update*", + "dynamodb:DescribeStream", + "dynamodb:BatchGet*", + "dynamodb:CreateTable", "dynamodb:Query", "dynamodb:Delete*", - "dynamodb:PutItem", - "dynamodb:DescribeStream", - "dynamodb:DescribeTable", "dynamodb:BatchWrite*", + "dynamodb:DescribeTable", + "dynamodb:Scan", + "dynamodb:PutItem", "dynamodb:Get*", }, resource_ids={"RolePolicy"}, @@ -375,16 +397,16 @@ def test_multiple_resources_with_wildcard_resources_are_detected(user_and_policy rule="WildcardResourceRule", rule_mode="BLOCKING", actions={ - "dynamodb:CreateTable", - "dynamodb:BatchGet*", - "dynamodb:Scan", "dynamodb:Update*", + "dynamodb:DescribeStream", + "dynamodb:BatchGet*", + "dynamodb:CreateTable", "dynamodb:Query", "dynamodb:Delete*", - "dynamodb:PutItem", - "dynamodb:DescribeStream", - "dynamodb:DescribeTable", "dynamodb:BatchWrite*", + "dynamodb:DescribeTable", + "dynamodb:Scan", + "dynamodb:PutItem", "dynamodb:Get*", }, resource_ids={"RolePolicy"}, @@ -397,16 +419,16 @@ def test_multiple_resources_with_wildcard_resources_are_detected(user_and_policy rule="WildcardResourceRule", rule_mode="BLOCKING", actions={ - "dynamodb:CreateTable", - "dynamodb:BatchGet*", - "dynamodb:Scan", "dynamodb:Update*", + "dynamodb:DescribeStream", + "dynamodb:BatchGet*", + "dynamodb:CreateTable", "dynamodb:Query", "dynamodb:Delete*", - "dynamodb:PutItem", - "dynamodb:DescribeStream", - "dynamodb:DescribeTable", "dynamodb:BatchWrite*", + "dynamodb:DescribeTable", + "dynamodb:Scan", + "dynamodb:PutItem", "dynamodb:Get*", }, resource_ids={"RolePolicy"}, @@ -419,16 +441,38 @@ def test_multiple_resources_with_wildcard_resources_are_detected(user_and_policy rule="WildcardResourceRule", rule_mode="BLOCKING", actions={ - "dynamodb:CreateTable", - "dynamodb:BatchGet*", - "dynamodb:Scan", "dynamodb:Update*", + "dynamodb:DescribeStream", + "dynamodb:BatchGet*", + "dynamodb:CreateTable", "dynamodb:Query", "dynamodb:Delete*", + "dynamodb:BatchWrite*", + "dynamodb:DescribeTable", + "dynamodb:Scan", "dynamodb:PutItem", + "dynamodb:Get*", + }, + resource_ids={"RolePolicy"}, + resource_types={"AWS::IAM::Policy"}, + ), + Failure( + granularity="ACTION", + reason='"RolePolicy" is using a wildcard resource in "TheExtremePolicy" for "dynamodb:GetResourcePolicy"', + risk_value="MEDIUM", + rule="WildcardResourceRule", + rule_mode="BLOCKING", + actions={ + "dynamodb:Update*", "dynamodb:DescribeStream", - "dynamodb:DescribeTable", + "dynamodb:BatchGet*", + "dynamodb:CreateTable", + "dynamodb:Query", + "dynamodb:Delete*", "dynamodb:BatchWrite*", + "dynamodb:DescribeTable", + "dynamodb:Scan", + "dynamodb:PutItem", "dynamodb:Get*", }, resource_ids={"RolePolicy"}, @@ -441,16 +485,16 @@ def test_multiple_resources_with_wildcard_resources_are_detected(user_and_policy rule="WildcardResourceRule", rule_mode="BLOCKING", actions={ - "dynamodb:CreateTable", - "dynamodb:BatchGet*", - "dynamodb:Scan", "dynamodb:Update*", + "dynamodb:DescribeStream", + "dynamodb:BatchGet*", + "dynamodb:CreateTable", "dynamodb:Query", "dynamodb:Delete*", - "dynamodb:PutItem", - "dynamodb:DescribeStream", - "dynamodb:DescribeTable", "dynamodb:BatchWrite*", + "dynamodb:DescribeTable", + "dynamodb:Scan", + "dynamodb:PutItem", "dynamodb:Get*", }, resource_ids={"RolePolicy"}, @@ -463,16 +507,16 @@ def test_multiple_resources_with_wildcard_resources_are_detected(user_and_policy rule="WildcardResourceRule", rule_mode="BLOCKING", actions={ - "dynamodb:CreateTable", - "dynamodb:BatchGet*", - "dynamodb:Scan", "dynamodb:Update*", + "dynamodb:DescribeStream", + "dynamodb:BatchGet*", + "dynamodb:CreateTable", "dynamodb:Query", "dynamodb:Delete*", - "dynamodb:PutItem", - "dynamodb:DescribeStream", - "dynamodb:DescribeTable", "dynamodb:BatchWrite*", + "dynamodb:DescribeTable", + "dynamodb:Scan", + "dynamodb:PutItem", "dynamodb:Get*", }, resource_ids={"RolePolicy"}, @@ -485,16 +529,16 @@ def test_multiple_resources_with_wildcard_resources_are_detected(user_and_policy rule="WildcardResourceRule", rule_mode="BLOCKING", actions={ - "dynamodb:CreateTable", - "dynamodb:BatchGet*", - "dynamodb:Scan", "dynamodb:Update*", + "dynamodb:DescribeStream", + "dynamodb:BatchGet*", + "dynamodb:CreateTable", "dynamodb:Query", "dynamodb:Delete*", - "dynamodb:PutItem", - "dynamodb:DescribeStream", - "dynamodb:DescribeTable", "dynamodb:BatchWrite*", + "dynamodb:DescribeTable", + "dynamodb:Scan", + "dynamodb:PutItem", "dynamodb:Get*", }, resource_ids={"RolePolicy"}, @@ -507,16 +551,16 @@ def test_multiple_resources_with_wildcard_resources_are_detected(user_and_policy rule="WildcardResourceRule", rule_mode="BLOCKING", actions={ - "dynamodb:CreateTable", - "dynamodb:BatchGet*", - "dynamodb:Scan", "dynamodb:Update*", + "dynamodb:DescribeStream", + "dynamodb:BatchGet*", + "dynamodb:CreateTable", "dynamodb:Query", "dynamodb:Delete*", - "dynamodb:PutItem", - "dynamodb:DescribeStream", - "dynamodb:DescribeTable", "dynamodb:BatchWrite*", + "dynamodb:DescribeTable", + "dynamodb:Scan", + "dynamodb:PutItem", "dynamodb:Get*", }, resource_ids={"RolePolicy"}, @@ -529,16 +573,16 @@ def test_multiple_resources_with_wildcard_resources_are_detected(user_and_policy rule="WildcardResourceRule", rule_mode="BLOCKING", actions={ - "dynamodb:CreateTable", - "dynamodb:BatchGet*", - "dynamodb:Scan", "dynamodb:Update*", + "dynamodb:DescribeStream", + "dynamodb:BatchGet*", + "dynamodb:CreateTable", "dynamodb:Query", "dynamodb:Delete*", - "dynamodb:PutItem", - "dynamodb:DescribeStream", - "dynamodb:DescribeTable", "dynamodb:BatchWrite*", + "dynamodb:DescribeTable", + "dynamodb:Scan", + "dynamodb:PutItem", "dynamodb:Get*", }, resource_ids={"RolePolicy"}, @@ -551,16 +595,16 @@ def test_multiple_resources_with_wildcard_resources_are_detected(user_and_policy rule="WildcardResourceRule", rule_mode="BLOCKING", actions={ - "dynamodb:CreateTable", - "dynamodb:BatchGet*", - "dynamodb:Scan", "dynamodb:Update*", + "dynamodb:DescribeStream", + "dynamodb:BatchGet*", + "dynamodb:CreateTable", "dynamodb:Query", "dynamodb:Delete*", - "dynamodb:PutItem", - "dynamodb:DescribeStream", - "dynamodb:DescribeTable", "dynamodb:BatchWrite*", + "dynamodb:DescribeTable", + "dynamodb:Scan", + "dynamodb:PutItem", "dynamodb:Get*", }, resource_ids={"RolePolicy"}, @@ -573,16 +617,16 @@ def test_multiple_resources_with_wildcard_resources_are_detected(user_and_policy rule="WildcardResourceRule", rule_mode="BLOCKING", actions={ - "dynamodb:CreateTable", - "dynamodb:BatchGet*", - "dynamodb:Scan", "dynamodb:Update*", + "dynamodb:DescribeStream", + "dynamodb:BatchGet*", + "dynamodb:CreateTable", "dynamodb:Query", "dynamodb:Delete*", - "dynamodb:PutItem", - "dynamodb:DescribeStream", - "dynamodb:DescribeTable", "dynamodb:BatchWrite*", + "dynamodb:DescribeTable", + "dynamodb:Scan", + "dynamodb:PutItem", "dynamodb:Get*", }, resource_ids={"RolePolicy"}, @@ -595,16 +639,38 @@ def test_multiple_resources_with_wildcard_resources_are_detected(user_and_policy rule="WildcardResourceRule", rule_mode="BLOCKING", actions={ - "dynamodb:CreateTable", - "dynamodb:BatchGet*", - "dynamodb:Scan", "dynamodb:Update*", + "dynamodb:DescribeStream", + "dynamodb:BatchGet*", + "dynamodb:CreateTable", "dynamodb:Query", "dynamodb:Delete*", + "dynamodb:BatchWrite*", + "dynamodb:DescribeTable", + "dynamodb:Scan", "dynamodb:PutItem", + "dynamodb:Get*", + }, + resource_ids={"RolePolicy"}, + resource_types={"AWS::IAM::Policy"}, + ), + Failure( + granularity="ACTION", + reason='"RolePolicy" is using a wildcard resource in "TheExtremePolicy" for "dynamodb:UpdateGlobalTableVersion"', + risk_value="MEDIUM", + rule="WildcardResourceRule", + rule_mode="BLOCKING", + actions={ + "dynamodb:Update*", "dynamodb:DescribeStream", - "dynamodb:DescribeTable", + "dynamodb:BatchGet*", + "dynamodb:CreateTable", + "dynamodb:Query", + "dynamodb:Delete*", "dynamodb:BatchWrite*", + "dynamodb:DescribeTable", + "dynamodb:Scan", + "dynamodb:PutItem", "dynamodb:Get*", }, resource_ids={"RolePolicy"}, @@ -617,16 +683,38 @@ def test_multiple_resources_with_wildcard_resources_are_detected(user_and_policy rule="WildcardResourceRule", rule_mode="BLOCKING", actions={ - "dynamodb:CreateTable", - "dynamodb:BatchGet*", - "dynamodb:Scan", "dynamodb:Update*", + "dynamodb:DescribeStream", + "dynamodb:BatchGet*", + "dynamodb:CreateTable", "dynamodb:Query", "dynamodb:Delete*", + "dynamodb:BatchWrite*", + "dynamodb:DescribeTable", + "dynamodb:Scan", "dynamodb:PutItem", + "dynamodb:Get*", + }, + resource_ids={"RolePolicy"}, + resource_types={"AWS::IAM::Policy"}, + ), + Failure( + granularity="ACTION", + reason='"RolePolicy" is using a wildcard resource in "TheExtremePolicy" for "dynamodb:UpdateKinesisStreamingDestination"', + risk_value="MEDIUM", + rule="WildcardResourceRule", + rule_mode="BLOCKING", + actions={ + "dynamodb:Update*", "dynamodb:DescribeStream", - "dynamodb:DescribeTable", + "dynamodb:BatchGet*", + "dynamodb:CreateTable", + "dynamodb:Query", + "dynamodb:Delete*", "dynamodb:BatchWrite*", + "dynamodb:DescribeTable", + "dynamodb:Scan", + "dynamodb:PutItem", "dynamodb:Get*", }, resource_ids={"RolePolicy"}, @@ -639,16 +727,16 @@ def test_multiple_resources_with_wildcard_resources_are_detected(user_and_policy rule="WildcardResourceRule", rule_mode="BLOCKING", actions={ - "dynamodb:CreateTable", - "dynamodb:BatchGet*", - "dynamodb:Scan", "dynamodb:Update*", + "dynamodb:DescribeStream", + "dynamodb:BatchGet*", + "dynamodb:CreateTable", "dynamodb:Query", "dynamodb:Delete*", - "dynamodb:PutItem", - "dynamodb:DescribeStream", - "dynamodb:DescribeTable", "dynamodb:BatchWrite*", + "dynamodb:DescribeTable", + "dynamodb:Scan", + "dynamodb:PutItem", "dynamodb:Get*", }, resource_ids={"RolePolicy"}, @@ -661,16 +749,16 @@ def test_multiple_resources_with_wildcard_resources_are_detected(user_and_policy rule="WildcardResourceRule", rule_mode="BLOCKING", actions={ - "dynamodb:CreateTable", - "dynamodb:BatchGet*", - "dynamodb:Scan", "dynamodb:Update*", + "dynamodb:DescribeStream", + "dynamodb:BatchGet*", + "dynamodb:CreateTable", "dynamodb:Query", "dynamodb:Delete*", - "dynamodb:PutItem", - "dynamodb:DescribeStream", - "dynamodb:DescribeTable", "dynamodb:BatchWrite*", + "dynamodb:DescribeTable", + "dynamodb:Scan", + "dynamodb:PutItem", "dynamodb:Get*", }, resource_ids={"RolePolicy"}, @@ -683,16 +771,16 @@ def test_multiple_resources_with_wildcard_resources_are_detected(user_and_policy rule="WildcardResourceRule", rule_mode="BLOCKING", actions={ - "dynamodb:CreateTable", - "dynamodb:BatchGet*", - "dynamodb:Scan", "dynamodb:Update*", + "dynamodb:DescribeStream", + "dynamodb:BatchGet*", + "dynamodb:CreateTable", "dynamodb:Query", "dynamodb:Delete*", - "dynamodb:PutItem", - "dynamodb:DescribeStream", - "dynamodb:DescribeTable", "dynamodb:BatchWrite*", + "dynamodb:DescribeTable", + "dynamodb:Scan", + "dynamodb:PutItem", "dynamodb:Get*", }, resource_ids={"RolePolicy"}, diff --git a/tests/test_boto3_client.py b/tests/test_boto3_client.py index 2f1c0d2d..639f4df7 100644 --- a/tests/test_boto3_client.py +++ b/tests/test_boto3_client.py @@ -4,7 +4,7 @@ import boto3 import pytest from botocore.exceptions import ClientError -from moto import mock_cloudformation, mock_s3, mock_sts +from moto import mock_aws from cfripper.boto3_client import Boto3Client from cfripper.model.utils import InvalidURLException, convert_json_or_yaml_to_dict @@ -19,7 +19,7 @@ @pytest.fixture def s3_bucket(default_aws_region): - with mock_s3(): + with mock_aws(): boto3.client("s3").create_bucket( Bucket=TEST_BUCKET_NAME, CreateBucketConfiguration={"LocationConstraint": default_aws_region} ) @@ -28,7 +28,7 @@ def s3_bucket(default_aws_region): @pytest.fixture def boto3_client(default_aws_region): - with mock_sts(): + with mock_aws(): yield Boto3Client("123456789", default_aws_region, "stack-id") @@ -301,7 +301,7 @@ def test_get_exports( assert patched_exceptions.mock_calls == mocked_exceptions -@mock_cloudformation +@mock_aws def test_export_values(boto3_client: Boto3Client): cf_client = boto3_client.session.client("cloudformation", "eu-west-1") cf_client.create_stack( @@ -324,4 +324,4 @@ def test_export_values(boto3_client: Boto3Client): # actual suffix changes between tests export_values = boto3_client.get_exports() assert len(export_values) == 1 - assert "arn:aws:sqs:eu-west-1:123456789012:Test-Stack-MyQueue-" in export_values["MainQueue"] + assert "arn:aws:sqs:eu-west-1:123456789:Test-Stack-MyQueue-" in export_values["MainQueue"] diff --git a/tests/test_templates/rules/CrossAccountTrustRule/kms_key_without_policy.yml b/tests/test_templates/rules/CrossAccountTrustRule/kms_key_without_policy.yml new file mode 100644 index 00000000..b3400c53 --- /dev/null +++ b/tests/test_templates/rules/CrossAccountTrustRule/kms_key_without_policy.yml @@ -0,0 +1,9 @@ +--- +AWSTemplateFormatVersion: "2010-09-09" + +Resources: + MyKey: + Type: "AWS::KMS::Key" + Properties: + EnableKeyRotation: true + Enabled: true diff --git a/tests/test_templates/rules/IAMRolesOverprivilegedRule/invalid_role_inline_policy_fn_if.json b/tests/test_templates/rules/IAMRolesOverprivilegedRule/invalid_role_inline_policy_fn_if.json index c1e25866..644828c3 100644 --- a/tests/test_templates/rules/IAMRolesOverprivilegedRule/invalid_role_inline_policy_fn_if.json +++ b/tests/test_templates/rules/IAMRolesOverprivilegedRule/invalid_role_inline_policy_fn_if.json @@ -35,7 +35,7 @@ { "Action": "sts:AssumeRole", "Effect": "Allow", - "Resource": "arn:aws:iam::325714046698:role/sandbox-secrets-access" + "Resource": "arn:aws:iam::123456789012:role/test-role" } ], "Version": "2012-10-17" @@ -65,4 +65,4 @@ } } } -} \ No newline at end of file +} diff --git a/tests/test_templates/rules/WildcardResourceRule/kms_key_with_wildcard_resource.json b/tests/test_templates/rules/KMSKeyWildcardPrincipalRule/kms_key_with_wildcard_resource.json similarity index 96% rename from tests/test_templates/rules/WildcardResourceRule/kms_key_with_wildcard_resource.json rename to tests/test_templates/rules/KMSKeyWildcardPrincipalRule/kms_key_with_wildcard_resource.json index 45d5691a..1599b66b 100644 --- a/tests/test_templates/rules/WildcardResourceRule/kms_key_with_wildcard_resource.json +++ b/tests/test_templates/rules/KMSKeyWildcardPrincipalRule/kms_key_with_wildcard_resource.json @@ -12,7 +12,7 @@ "Sid": "Enable IAM User Permissions", "Effect": "Allow", "Principal": { - "AWS": "arn:aws:iam::111122223333:root" + "AWS": "*" }, "Action": "kms:*", "Resource": "*" diff --git a/tests/test_templates/rules/KMSKeyWildcardPrincipalRule/kms_key_without_policy.yml b/tests/test_templates/rules/KMSKeyWildcardPrincipalRule/kms_key_without_policy.yml new file mode 100644 index 00000000..b3400c53 --- /dev/null +++ b/tests/test_templates/rules/KMSKeyWildcardPrincipalRule/kms_key_without_policy.yml @@ -0,0 +1,9 @@ +--- +AWSTemplateFormatVersion: "2010-09-09" + +Resources: + MyKey: + Type: "AWS::KMS::Key" + Properties: + EnableKeyRotation: true + Enabled: true diff --git a/tests/test_templates/rules/PublicELBCheckerRule/private_elb_instance.yml b/tests/test_templates/rules/PublicELBCheckerRule/private_elb_instance.yml new file mode 100644 index 00000000..5f8afe07 --- /dev/null +++ b/tests/test_templates/rules/PublicELBCheckerRule/private_elb_instance.yml @@ -0,0 +1,32 @@ +Resources: + PublicLoadBalancer: + Type: 'AWS::ElasticLoadBalancing::LoadBalancer' + Properties: + Name: 'AWS::StackName-extlb' + Scheme: internal + SecurityGroups: + - !GetAtt + - LoadBalancerHttpsSG + - GroupId + Subnets: !If + - ExtLoadBalancer + - - !ImportValue PublicSubnetA + - !ImportValue PublicSubnetB + - !ImportValue PublicSubnetC + - - !ImportValue PrivateSubnetA + - !ImportValue PrivateSubnetB + - !ImportValue PrivateSubnetC + ConnectionSettings: + - IdleTimeout: 3600 + Tags: + - Key: Name + Value: !Join + - '-' + - - !Ref 'AWS::StackName' + - LoadBalancerv2 + - Key: Project + Value: !Ref ProjectName + - Key: Contact + Value: !Ref ContactEmail + - Key: StackName + Value: !Ref 'AWS::StackName' \ No newline at end of file diff --git a/tests/test_templates/rules/PublicELBCheckerRule/private_elb_v2_instance.yml b/tests/test_templates/rules/PublicELBCheckerRule/private_elb_v2_instance.yml new file mode 100644 index 00000000..6c6da07c --- /dev/null +++ b/tests/test_templates/rules/PublicELBCheckerRule/private_elb_v2_instance.yml @@ -0,0 +1,34 @@ +Resources: + PublicV2LoadBalancer: + Type: 'AWS::ElasticLoadBalancingV2::LoadBalancer' + Properties: + Name: 'AWS::StackName-extlb' + Scheme: internal + SecurityGroups: + - !GetAtt + - LoadBalancerHttpsSG + - GroupId + Subnets: !If + - ExtLoadBalancer + - - !ImportValue PublicSubnetA + - !ImportValue PublicSubnetB + - !ImportValue PublicSubnetC + - - !ImportValue PrivateSubnetA + - !ImportValue PrivateSubnetB + - !ImportValue PrivateSubnetC + Type: application + LoadBalancerAttributes: + - Key: idle_timeout.timeout_seconds + Value: '3600' + Tags: + - Key: Name + Value: !Join + - '-' + - - !Ref 'AWS::StackName' + - LoadBalancerv2 + - Key: Project + Value: !Ref ProjectName + - Key: Contact + Value: !Ref ContactEmail + - Key: StackName + Value: !Ref 'AWS::StackName' \ No newline at end of file diff --git a/tests/test_templates/rules/PublicELBCheckerRule/public_facing_elb_instance.yml b/tests/test_templates/rules/PublicELBCheckerRule/public_facing_elb_instance.yml new file mode 100644 index 00000000..5818b3ee --- /dev/null +++ b/tests/test_templates/rules/PublicELBCheckerRule/public_facing_elb_instance.yml @@ -0,0 +1,32 @@ +Resources: + PublicLoadBalancer: + Type: 'AWS::ElasticLoadBalancing::LoadBalancer' + Properties: + Name: 'AWS::StackName-extlb' + Scheme: internet-facing + SecurityGroups: + - !GetAtt + - LoadBalancerHttpsSG + - GroupId + Subnets: !If + - ExtLoadBalancer + - - !ImportValue PublicSubnetA + - !ImportValue PublicSubnetB + - !ImportValue PublicSubnetC + - - !ImportValue PrivateSubnetA + - !ImportValue PrivateSubnetB + - !ImportValue PrivateSubnetC + ConnectionSettings: + - IdleTimeout: 3600 + Tags: + - Key: Name + Value: !Join + - '-' + - - !Ref 'AWS::StackName' + - LoadBalancerv2 + - Key: Project + Value: !Ref ProjectName + - Key: Contact + Value: !Ref ContactEmail + - Key: StackName + Value: !Ref 'AWS::StackName' \ No newline at end of file diff --git a/tests/test_templates/rules/PublicELBCheckerRule/public_facing_elb_v2_instance.yml b/tests/test_templates/rules/PublicELBCheckerRule/public_facing_elb_v2_instance.yml new file mode 100644 index 00000000..6bef83ac --- /dev/null +++ b/tests/test_templates/rules/PublicELBCheckerRule/public_facing_elb_v2_instance.yml @@ -0,0 +1,34 @@ +Resources: + PublicV2LoadBalancer: + Type: 'AWS::ElasticLoadBalancingV2::LoadBalancer' + Properties: + Name: 'AWS::StackName-extlb' + Scheme: internet-facing + SecurityGroups: + - !GetAtt + - LoadBalancerHttpsSG + - GroupId + Subnets: !If + - ExtLoadBalancer + - - !ImportValue PublicSubnetA + - !ImportValue PublicSubnetB + - !ImportValue PublicSubnetC + - - !ImportValue PrivateSubnetA + - !ImportValue PrivateSubnetB + - !ImportValue PrivateSubnetC + Type: application + LoadBalancerAttributes: + - Key: idle_timeout.timeout_seconds + Value: '3600' + Tags: + - Key: Name + Value: !Join + - '-' + - - !Ref 'AWS::StackName' + - LoadBalancerv2 + - Key: Project + Value: !Ref ProjectName + - Key: Contact + Value: !Ref ContactEmail + - Key: StackName + Value: !Ref 'AWS::StackName' \ No newline at end of file diff --git a/tests/test_templates/rules/S3LifecycleConfiguration/allowed_template_malformed_lifecycle_rules.yaml b/tests/test_templates/rules/S3LifecycleConfiguration/allowed_template_malformed_lifecycle_rules.yaml deleted file mode 100644 index e7d158fe..00000000 --- a/tests/test_templates/rules/S3LifecycleConfiguration/allowed_template_malformed_lifecycle_rules.yaml +++ /dev/null @@ -1,10 +0,0 @@ -Resources: - OutputBucket: - Type: AWS::S3::Bucket - Properties: - BucketName: "foo" - AccessControl: BucketOwnerFullControl - LifecycleConfiguration: - # This is not valid for LifecycleConfiguration, but CFRipper will not parse it right now. - - aa - - bb diff --git a/tests/test_templates/rules/StorageEncryptedRule/aurora_engine_used.yml b/tests/test_templates/rules/StorageEncryptedRule/aurora_engine_used.yml new file mode 100644 index 00000000..cab515cb --- /dev/null +++ b/tests/test_templates/rules/StorageEncryptedRule/aurora_engine_used.yml @@ -0,0 +1,15 @@ +Resources: + DBMaster: + Type: AWS::RDS::DBInstance + Properties: + AllowMajorVersionUpgrade: false + AutoMinorVersionUpgrade: true + DBInstanceIdentifier: !Sub ${AWS::StackName}-master + DBName: "some-name" + Engine: aurora-postgresql + EngineVersion: "13.2" + KmsKeyId: "some-kms-key" + MultiAZ: true + Tags: + - Key: Name + Value: !Sub ${AWS::StackName}-master \ No newline at end of file diff --git a/tests/test_templates/rules/StorageEncryptedRule/encrypted_db_resource.yml b/tests/test_templates/rules/StorageEncryptedRule/encrypted_db_resource.yml new file mode 100644 index 00000000..8efac523 --- /dev/null +++ b/tests/test_templates/rules/StorageEncryptedRule/encrypted_db_resource.yml @@ -0,0 +1,18 @@ +Resources: + DBMaster: + Type: AWS::RDS::DBInstance + Properties: + AllocatedStorage: "100" + AllowMajorVersionUpgrade: false + AutoMinorVersionUpgrade: true + BackupRetentionPeriod: 14 + DBInstanceIdentifier: !Sub ${AWS::StackName}-master + DBName: "some-name" + Engine: mysql + EngineVersion: "13.2" + KmsKeyId: "some-kms-key" + MultiAZ: true + StorageEncrypted: true + Tags: + - Key: Name + Value: !Sub ${AWS::StackName}-master \ No newline at end of file diff --git a/tests/test_templates/rules/StorageEncryptedRule/missing_storage_encrypted_flag.yml b/tests/test_templates/rules/StorageEncryptedRule/missing_storage_encrypted_flag.yml new file mode 100644 index 00000000..850e1c09 --- /dev/null +++ b/tests/test_templates/rules/StorageEncryptedRule/missing_storage_encrypted_flag.yml @@ -0,0 +1,17 @@ +Resources: + DBMaster: + Type: AWS::RDS::DBInstance + Properties: + AllocatedStorage: "100" + AllowMajorVersionUpgrade: false + AutoMinorVersionUpgrade: true + BackupRetentionPeriod: 14 + DBInstanceIdentifier: !Sub ${AWS::StackName}-master + DBName: "some-name" + Engine: mysql + EngineVersion: "13.2" + KmsKeyId: "some-kms-key" + MultiAZ: true + Tags: + - Key: Name + Value: !Sub ${AWS::StackName}-master \ No newline at end of file diff --git a/tests/test_templates/rules/StorageEncryptedRule/no_db_resource.yml b/tests/test_templates/rules/StorageEncryptedRule/no_db_resource.yml new file mode 100644 index 00000000..2055a06e --- /dev/null +++ b/tests/test_templates/rules/StorageEncryptedRule/no_db_resource.yml @@ -0,0 +1,16 @@ +Resources: + SomeResource: + Type: AWS::RDS::DBCluster + Properties: + AllocatedStorage: "100" + AutoMinorVersionUpgrade: true + BackupRetentionPeriod: 14 + DBClusterIdentifier: !Sub ${AWS::StackName}-master + DatabaseName: "some-name" + Engine: mysql + EngineVersion: "13.2" + KmsKeyId: "some-kms-key" + StorageEncrypted: false + Tags: + - Key: Name + Value: !Sub ${AWS::StackName}-master \ No newline at end of file diff --git a/tests/test_templates/rules/StorageEncryptedRule/two_resources_not_encrypted.yml b/tests/test_templates/rules/StorageEncryptedRule/two_resources_not_encrypted.yml new file mode 100644 index 00000000..a9b4eea1 --- /dev/null +++ b/tests/test_templates/rules/StorageEncryptedRule/two_resources_not_encrypted.yml @@ -0,0 +1,35 @@ +Resources: + DBMaster: + Type: AWS::RDS::DBInstance + Properties: + AllocatedStorage: "100" + AllowMajorVersionUpgrade: false + AutoMinorVersionUpgrade: true + BackupRetentionPeriod: 14 + DBInstanceIdentifier: !Sub ${AWS::StackName}-master + DBName: "some-name" + Engine: mysql + EngineVersion: "13.2" + KmsKeyId: "some-kms-key" + MultiAZ: true + StorageEncrypted: false + Tags: + - Key: Name + Value: !Sub ${AWS::StackName}-master + DBBackup: + Type: AWS::RDS::DBInstance + Properties: + AllocatedStorage: "100" + AllowMajorVersionUpgrade: true + AutoMinorVersionUpgrade: false + BackupRetentionPeriod: 7 + DBInstanceIdentifier: !Sub ${AWS::StackName}-backup + DBName: "some-name-backup" + Engine: mysql + EngineVersion: "13.2" + KmsKeyId: "some-kms-key" + MultiAZ: true + StorageEncrypted: false + Tags: + - Key: Name + Value: !Sub ${AWS::StackName}-backup \ No newline at end of file