Skip to content

Commit

Permalink
[FEATURE] Add first version of CLI (#90)
Browse files Browse the repository at this point in the history
* update setup.py to grap version from __version__

* add version file

* add cli

* Update changelog

* add entry point to setup

* fix nit

* apply comments from pr

* update cli and tests

* update docs

* fix nit

* apply comments form PR

* apply comments form PR

* remove resasons from result class

* run formatter

* update docs index

* change package description

* apply pr comments

* remove unsued import

* apply comments from PR

* update readme to include cli examples
  • Loading branch information
oscarbc96 committed Jan 8, 2020
1 parent 0025ab8 commit dc6771f
Show file tree
Hide file tree
Showing 9 changed files with 291 additions and 9 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# Changelog
All notable changes to this project will be documented in this file.

## [0.12.0] - 2019-12-17
## [0.12.0] - 2020-01-03
### Added
- Adds CLI to package
- `KMSKeyCrossAccountTrustRule`
### Changed
- `GenericWildcardPrincipalRule`, `PartialWildcardPrincipalRule`, `FullWildcardPrincipalRule` no longer check for
Expand Down
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ install:
install-dev: install
pip install -e ".[dev]"

install-docs:
pip install -e ".[dev,docs]"

format:
isort --recursive .
black .
Expand Down
48 changes: 47 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,52 @@
[![Build Status](https://travis-ci.org/Skyscanner/cfripper.svg?branch=master)](https://travis-ci.org/Skyscanner/cfripper)
[![PyPI version](https://badge.fury.io/py/cfripper.svg)](https://badge.fury.io/py/cfripper)

Library designed to be used as part of a Lambda function to "rip apart" a CloudFormation template and check it for security compliance.
Library and CLI tool for analysing CloudFormation templates and check them for security compliance.

Docs available in https://cfripper.readthedocs.io/

## CLI Usage

### Normal execution
```bash
$ cfripper /tmp/root.yaml /tmp/root_bypass.json --format txt
Analysing /tmp/root.yaml...
Not adding CrossAccountTrustRule failure in rootRole because no AWS Account ID was found in the config.
Valid: False
Issues found:
- FullWildcardPrincipalRule: rootRole should not allow wildcards in principals (principal: '*')
- IAMRolesOverprivilegedRule: Role 'rootRole' contains an insecure permission '*' in policy 'root'
Analysing /tmp/root_bypass.json...
Valid: True
```

### Using resolve flag
```bash
$ cfripper /tmp/root.yaml /tmp/root_bypass.json --format txt --resolve
Analysing /tmp/root.yaml...
Not adding CrossAccountTrustRule failure in rootRole because no AWS Account ID was found in the config.
Valid: False
Issues found:
- FullWildcardPrincipalRule: rootRole should not allow wildcards in principals (principal: '*')
- IAMRolesOverprivilegedRule: Role 'rootRole' contains an insecure permission '*' in policy 'root'
Analysing /tmp/root_bypass.json...
Not adding CrossAccountTrustRule failure in rootRole because no AWS Account ID was found in the config.
Valid: False
Issues found:
- IAMRolesOverprivilegedRule: Role 'rootRole' contains an insecure permission '*' in policy 'root'
Monitored issues found:
- PartialWildcardPrincipalRule: rootRole contains an unknown principal: 123456789012
- PartialWildcardPrincipalRule: rootRole should not allow wildcard in principals or account-wide principals
(principal: 'arn:aws:iam::123456789012:root')
```

### Using json format and output-folder argument
```bash
$ cfripper /tmp/root.yaml /tmp/root_bypass.json --format json --resolve --output-folder /tmp
Analysing /tmp/root.yaml...
Not adding CrossAccountTrustRule failure in rootRole because no AWS Account ID was found in the config.
Result saved in /tmp/root.yaml.cfripper.results.json
Analysing /tmp/root_bypass.json...
Not adding CrossAccountTrustRule failure in rootRole because no AWS Account ID was found in the config.
Result saved in /tmp/root_bypass.json.cfripper.results.json
```
3 changes: 3 additions & 0 deletions cfripper/__version__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
VERSION = (0, 12, 0)

__version__ = ".".join(map(str, VERSION))
165 changes: 165 additions & 0 deletions cfripper/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import logging
import sys
from io import TextIOWrapper
from pathlib import Path
from typing import Dict, Optional, Tuple

import click
import pycfmodel
from pycfmodel.model.cf_model import CFModel

from cfripper.__version__ import __version__
from cfripper.config.config import Config
from cfripper.model.result import Result
from cfripper.model.utils import convert_json_or_yaml_to_dict
from cfripper.rule_processor import RuleProcessor
from cfripper.rules import DEFAULT_RULES

LOGGING_LEVELS = {
"ERROR": logging.ERROR,
"WARNING": logging.WARNING,
"INFO": logging.INFO,
"DEBUG": logging.DEBUG,
}


def setup_logging(level: str) -> None:
logging.basicConfig(level=LOGGING_LEVELS[level], format="%(message)s")


def init_cfripper() -> Tuple[Config, Result, RuleProcessor]:
config = Config(rules=DEFAULT_RULES.keys())
result = Result()
rule_processor = RuleProcessor(*[DEFAULT_RULES.get(rule)(config, result) for rule in config.rules])
return config, result, rule_processor


def get_cfmodel(template: TextIOWrapper) -> CFModel:
template = convert_json_or_yaml_to_dict(template.read())
cfmodel = pycfmodel.parse(template)
return cfmodel


def analyse_template(cfmodel: CFModel, rule_processor: RuleProcessor, config: Config, result: Result) -> None:
rule_processor.process_cf_template(cfmodel, config, result)


def format_result_json(result: Result) -> str:
return result.json()


def format_result_txt(result: Result) -> str:
result_lines = [f"Valid: {result.valid}"]
if result.failed_rules:
result_lines.append("Issues found:")
[result_lines.append(f"\t- {r.rule}: {r.reason}") for r in result.failed_rules]
if result.failed_monitored_rules:
result_lines.append("Monitored issues found:")
[result_lines.append(f"\t- {r.rule}: {r.reason}") for r in result.failed_monitored_rules]
return "\n".join(result_lines)


def format_result(result: Result, output_format: str) -> str:
if output_format == "json":
return format_result_json(result)
else:
return format_result_txt(result)


def save_to_file(path: Path, result: str) -> None:
path.write_text(result)
logging.info(f"Result saved in {path}")


def print_to_stdout(result: str) -> None:
click.echo(result)


def output_handling(template_name: str, result: str, output_format: str, output_folder: Optional[str]) -> None:
if output_folder:
save_to_file(Path(output_folder) / f"{template_name}.cfripper.results.{output_format}", result)
else:
print_to_stdout(result)


def process_template(
template, resolve: bool, resolve_parameters: Optional[Dict], output_folder: Optional[str], output_format: str
) -> None:
logging.info(f"Analysing {template.name}...")

cfmodel = get_cfmodel(template)
if resolve:
cfmodel = cfmodel.resolve(resolve_parameters)

config, result, rule_processor = init_cfripper()

analyse_template(cfmodel, rule_processor, config, result)

formatted_result = format_result(result, output_format)

output_handling(template.name, formatted_result, output_format, output_folder)


@click.command()
@click.version_option(prog_name="cfripper", version=__version__)
@click.argument("templates", type=click.File("r"), nargs=-1)
@click.option(
"--resolve/--no-resolve",
is_flag=True,
default=False,
help="Resolves cloudformation variables and intrinsic functions",
show_default=True,
)
@click.option(
"--resolve-parameters",
type=click.File("r"),
help=(
"JSON/YML file containing key-value pairs used for resolving CloudFormation files with templated parameters. "
'For example, {"abc": "ABC"} will change all occurrences of {"Ref": "abc"} in the CloudFormation file to "ABC".'
),
)
@click.option(
"--format",
"output_format",
type=click.Choice(["json", "txt"], case_sensitive=False),
default="txt",
help="Output format",
show_default=True,
)
@click.option(
"--output-folder",
type=click.Path(exists=True, resolve_path=True, writable=True, file_okay=False),
help="If not present, result will be sent to stdout",
)
@click.option(
"--logging",
"logging_level",
type=click.Choice(LOGGING_LEVELS.keys(), case_sensitive=True),
default="INFO",
help="Logging level",
show_default=True,
)
def cli(templates, logging_level, resolve_parameters, **kwargs):
"""Analyse AWS Cloudformation templates passed by parameter."""
try:
setup_logging(logging_level)

if kwargs["resolve"] and resolve_parameters:
resolve_parameters = convert_json_or_yaml_to_dict(resolve_parameters.read())

for template in templates:
process_template(template=template, resolve_parameters=resolve_parameters, **kwargs)

except Exception as e:
logging.exception(
"Unhandled exception raised, please create an issue wit the error message at "
"https://github.com/Skyscanner/cfripper/issues"
)
try:
sys.exit(e.errno)
except AttributeError:
sys.exit(1)


if __name__ == "__main__":
cli()
51 changes: 51 additions & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# CLI

```bash
{{ cfripper_cli_help() }}
```

## Examples

### Normal execution
```bash
$ cfripper /tmp/root.yaml /tmp/root_bypass.json --format txt
Analysing /tmp/root.yaml...
Not adding CrossAccountTrustRule failure in rootRole because no AWS Account ID was found in the config.
Valid: False
Issues found:
- FullWildcardPrincipalRule: rootRole should not allow wildcards in principals (principal: '*')
- IAMRolesOverprivilegedRule: Role 'rootRole' contains an insecure permission '*' in policy 'root'
Analysing /tmp/root_bypass.json...
Valid: True
```

### Using resolve flag
```bash
$ cfripper /tmp/root.yaml /tmp/root_bypass.json --format txt --resolve
Analysing /tmp/root.yaml...
Not adding CrossAccountTrustRule failure in rootRole because no AWS Account ID was found in the config.
Valid: False
Issues found:
- FullWildcardPrincipalRule: rootRole should not allow wildcards in principals (principal: '*')
- IAMRolesOverprivilegedRule: Role 'rootRole' contains an insecure permission '*' in policy 'root'
Analysing /tmp/root_bypass.json...
Not adding CrossAccountTrustRule failure in rootRole because no AWS Account ID was found in the config.
Valid: False
Issues found:
- IAMRolesOverprivilegedRule: Role 'rootRole' contains an insecure permission '*' in policy 'root'
Monitored issues found:
- PartialWildcardPrincipalRule: rootRole contains an unknown principal: 123456789012
- PartialWildcardPrincipalRule: rootRole should not allow wildcard in principals or account-wide principals
(principal: 'arn:aws:iam::123456789012:root')
```

### Using json format and output-folder argument
```bash
$ cfripper /tmp/root.yaml /tmp/root_bypass.json --format json --resolve --output-folder /tmp
Analysing /tmp/root.yaml...
Not adding CrossAccountTrustRule failure in rootRole because no AWS Account ID was found in the config.
Result saved in /tmp/root.yaml.cfripper.results.json
Analysing /tmp/root_bypass.json...
Not adding CrossAccountTrustRule failure in rootRole because no AWS Account ID was found in the config.
Result saved in /tmp/root_bypass.json.cfripper.results.json
```
8 changes: 8 additions & 0 deletions docs/macros.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import importlib
import inspect

import click

from cfripper import rules
from cfripper.cli import cli
from cfripper.model.enums import RuleMode


Expand All @@ -28,6 +31,11 @@ def inline_source(reference):
source = "".join(inspect.getsourcelines(obj)[0])
return f"```python3\n{source}```"

@env.macro
def cfripper_cli_help():
with click.Context(cli) as ctx:
return cli.get_help(ctx)


def get_object_from_reference(reference):
split = reference.split(".")
Expand Down
3 changes: 2 additions & 1 deletion mkdocs.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
site_name: CFRipper
site_description: "Lambda function to \"rip apart\" a CloudFormation template and check it for security compliance."
site_description: "Library and CLI tool for analysing CloudFormation templates and check them for security compliance."
strict: true
site_url: https://cfripper.readthedocs.io

Expand All @@ -12,6 +12,7 @@ repo_url: https://github.com/Skyscanner/cfripper

nav:
- Home: index.md
- CLI: cli.md
- Rules: rules.md
- Changelog: changelog.md
- Contributing: contributing.md
Expand Down
16 changes: 10 additions & 6 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
from pathlib import Path

from setuptools import find_packages, setup

with open("README.md", "r") as fh:
long_description = fh.read()
from cfripper.__version__ import __version__

project_root_path = Path(__file__).parent

install_requires = ["boto3>=1.4.7,<2", "PyYAML>=4.2b1", "pycfmodel>=0.5.1", "cfn_flip>=1.2.0"]

Expand All @@ -27,17 +30,18 @@

setup(
name="cfripper",
version="0.12.0",
version=__version__,
author="Skyscanner Product Security",
author_email="security@skyscanner.net",
long_description=long_description,
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='Lambda function to "rip apart" a CloudFormation template and check it for security compliance.',
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,},
extras_require={"dev": dev_requires, "docs": docs_requires},
)

0 comments on commit dc6771f

Please sign in to comment.