diff --git a/Makefile b/Makefile index fe908f27..d16d854c 100644 --- a/Makefile +++ b/Makefile @@ -31,4 +31,4 @@ unit: pipenv run nosetests -v integration: - panther_analysis_tool test --policies tests/fixtures/valid_policies/ + panther_analysis_tool test --path tests/fixtures/valid_policies/ diff --git a/README.md b/README.md index b9a1c2f1..ecd053a7 100644 --- a/README.md +++ b/README.md @@ -64,28 +64,29 @@ View available commands: $ panther_analysis_tool --help usage: panther_analysis_tool [-h] [--version] {test,zip,upload} ... -Panther Analysis Tool: A tool for writing, testing, and packaging Panther Policies/Rules +Panther Analaysis Tool: A command line tool for managing Panther policies and +rules. positional arguments: {test,zip,upload} - test Validate policy specifications and run policy tests. - zip Create an archive of local policies for uploading to - Panther. - upload Upload specified policies to a Panther deployment. + test Validate analysis specifications and run policy and rule + tests. + zip Create an archive of local policies and rules for + uploading to Panther. + upload Upload specified policies and rules to a Panther + deployment. optional arguments: -h, --help show this help message and exit - --version show program's version number and exit -``` + --version show program's version number and exit``` Run tests: ```bash -$ panther_analysis_tool test --policies tests/fixtures/valid_policies/ - -[INFO]: Testing Policies in tests/fixtures/valid_policies/ +$ panther_analysis_tool test --path tests/fixtures/valid_policies/ +[INFO]: Testing analysis packs in tests/fixtures/valid_policies/ -Testing policy 'AWS.IAM.MFAEnabled' +AWS.IAM.MFAEnabled [PASS] Root MFA not enabled fails compliance [PASS] User MFA not enabled fails compliance ``` @@ -93,24 +94,22 @@ Testing policy 'AWS.IAM.MFAEnabled' Create packages to upload via the Panther UI: ```bash -$ panther_analysis_tool zip --policies tests/fixtures/valid_policies/ --output-path tmp +$ panther_analysis_tool zip --path tests/fixtures/valid_policies/ --out tmp +[INFO]: Testing analysis packs in tests/fixtures/valid_policies/ -[INFO]: Testing Policies in tests/fixtures/valid_policies/ - -Testing policy 'AWS.IAM.MFAEnabled' +AWS.IAM.MFAEnabled [PASS] Root MFA not enabled fails compliance [PASS] User MFA not enabled fails compliance -[INFO]: Zipping policies in tests/fixtures/valid_policies/ to tmp -[INFO]: /Users/user_name/panther_analysis_tool/tmp/panther-policies-2019-01-01T16-00-00.zip +[INFO]: Zipping analysis packs in tests/fixtures/valid_policies/ to tmp +[INFO]: /tmp/panther-analysis-2020-03-23T12-48-18.zip ``` Upload packages to Panther directly. Note, this expects your environment to be setup the same way as if you were using the AWS CLI, see the setup instructions [here](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html). We also recommend using a credentials manager such as [aws-vault](https://github.com/99designs/aws-vault). ```bash -$ panther_analysis_tool upload --policies tests/fixtures/valid_policies/ --output-path tmp - -[INFO]: Testing Policies in tests/fixtures/valid_policies/ +$ panther_analysis_tool upload --path tests/fixtures/valid_policies/ --out tmp +[INFO]: Testing analysis packs in tests/fixtures/valid_policies/ AWS.IAM.MFAEnabled [PASS] Root MFA not enabled fails compliance @@ -124,7 +123,7 @@ AWS.CloudTrail.MFAEnabled [PASS] Root MFA not enabled fails compliance [PASS] User MFA not enabled fails compliance -[INFO]: Zipping policies in tests/fixtures/valid\_policies/ to tmp +[INFO]: Zipping analysis packs in tests/fixtures/valid_policies/ to tmp [INFO]: Found credentials in environment variables. [INFO]: Uploading pack to Panther [INFO]: Upload success. @@ -139,12 +138,6 @@ AWS.CloudTrail.MFAEnabled } ``` -In order to upload all currently available policies and rules to your Panther deployment, run the following command. This command will recursively traverse all directories under `analysis` and package all rules and policies it finds into one package and then upload them: - -```bash -$ panther_analysis_tool upload --policies analysis/ --output-path tmp -``` - ## Writing Policies Each Panther Policy consists of a Python body and a YAML or JSON specification file. diff --git a/panther_analysis_tool/main.py b/panther_analysis_tool/main.py index 262d1307..a105185d 100644 --- a/panther_analysis_tool/main.py +++ b/panther_analysis_tool/main.py @@ -94,7 +94,7 @@ def get(self, arg: str, default: Any = None) -> Any: def load_module(filename: str) -> Tuple[Any, Any]: - """Loads the Policy function module from a file. + """Loads the analysis function module from a file. Args: filename: The relative path to the file. @@ -114,14 +114,14 @@ def load_module(filename: str) -> Tuple[Any, Any]: return module, None -def load_policy_specs(directory: str) -> Iterator[Tuple[str, str, Any]]: - """Loads the Policy function module from a file. +def load_analysis_specs(directory: str) -> Iterator[Tuple[str, str, Any]]: + """Loads the analysis specifications from a file. Args: - directory: The relativie path to Panther policies. + directory: The relative path to Panther policies or rules. Yields: - A tuple of the relative filepath, directory name, and loaded policy specification dict. + A tuple of the relative filepath, directory name, and loaded analysis specification dict. """ for dir_name, _, file_list in os.walk(directory): for filename in sorted(file_list): @@ -148,10 +148,10 @@ def datetime_converted(obj: Any) -> Any: return obj -def zip_policies(args: argparse.Namespace) -> Tuple[int, str]: - """Tests, validates, and then archives all policies into a local zip file. +def zip_analysis(args: argparse.Namespace) -> Tuple[int, str]: + """Tests, validates, and then archives all policies and rules into a local zip file. - Returns 1 if the policy test or validation fails. + Returns 1 if the analysis tests or validation fails. Args: args: The populated Argparse namespace with parsed command-line arguments. @@ -159,27 +159,26 @@ def zip_policies(args: argparse.Namespace) -> Tuple[int, str]: Returns: A tuple of return code and the archive filename. """ - return_code, _ = test_policies(args) + return_code, _ = test_analysis(args) if return_code == 1: return return_code, '' - logging.info('Zipping policies in %s to %s', args.policies, - args.output_path) + logging.info('Zipping analysis packs in %s to %s', args.path, args.out) # example: 2019-08-05T18-23-25 # The colon character is not valid in filenames. current_time = datetime.now().isoformat(timespec='seconds').replace( ':', '-') - filename = 'panther-policies' + filename = 'panther-analysis' return 0, shutil.make_archive( - os.path.join(args.output_path, '{}-{}'.format(filename, current_time)), - 'zip', args.policies) + os.path.join(args.out, '{}-{}'.format(filename, current_time)), 'zip', + args.path) -def upload_policies(args: argparse.Namespace) -> Tuple[int, str]: - """Tests, validates, packages, and then uploads all policies into a Panther deployment. +def upload_analysis(args: argparse.Namespace) -> Tuple[int, str]: + """Tests, validates, packages, and uploads all policies and rules into a Panther deployment. - Returns 1 if the policy tests, validation, or packaging fails. + Returns 1 if the analysis tests, validation, or packaging fails. Args: args: The populated Argparse namespace with parsed command-line arguments. @@ -187,7 +186,7 @@ def upload_policies(args: argparse.Namespace) -> Tuple[int, str]: Returns: A tuple of return code and the archive filename. """ - return_code, archive = zip_policies(args) + return_code, archive = zip_analysis(args) if return_code == 1: return return_code, '' @@ -230,8 +229,8 @@ def upload_policies(args: argparse.Namespace) -> Tuple[int, str]: return 0, '' -def test_policies(args: argparse.Namespace) -> Tuple[int, list]: - """Imports each Policy/Rule and runs their tests. +def test_analysis(args: argparse.Namespace) -> Tuple[int, list]: + """Imports each policy or rule and runs their tests. Args: args: The populated Argparse namespace with parsed command-line arguments. @@ -242,60 +241,61 @@ def test_policies(args: argparse.Namespace) -> Tuple[int, list]: invalid_specs = [] failed_tests: DefaultDict[str, list] = defaultdict(list) tests: List[str] = [] - logging.info('Testing Policies in %s\n', args.policies) + logging.info('Testing analysis packs in %s\n', args.path) # First import the globals file - specs = list(load_policy_specs(args.policies)) - for policy_spec_filename, dir_name, policy_spec in specs: - if policy_spec.get('PolicyID') != 'aws_globals': + specs = list(load_analysis_specs(args.path)) + for analysis_spec_filename, dir_name, analysis_spec in specs: + if analysis_spec.get('PolicyID') != 'aws_globals': continue module, load_err = load_module( - os.path.join(dir_name, policy_spec['Filename'])) + os.path.join(dir_name, analysis_spec['Filename'])) # If the module could not be loaded, continue to the next if load_err: - invalid_specs.append((policy_spec_filename, load_err)) + invalid_specs.append((analysis_spec_filename, load_err)) break sys.modules['aws_globals'] = module - # Next import each policy and run its tests - for policy_spec_filename, dir_name, policy_spec in specs: - if policy_spec.get('PolicyID') == 'aws_globals': + # Next import each policy or rule and run its tests + for analysis_spec_filename, dir_name, analysis_spec in specs: + if analysis_spec.get('PolicyID') == 'aws_globals': continue try: - SPEC_SCHEMA.validate(policy_spec) + SPEC_SCHEMA.validate(analysis_spec) except (SchemaError, SchemaMissingKeyError, SchemaForbiddenKeyError, SchemaUnexpectedTypeError) as err: - invalid_specs.append((policy_spec_filename, err)) + invalid_specs.append((analysis_spec_filename, err)) continue - print(policy_spec['PolicyID']) + print(analysis_spec['PolicyID']) # Check if the PolicyID has already been loaded - if policy_spec['PolicyID'] in tests: + if analysis_spec['PolicyID'] in tests: print('\t[ERROR] Conflicting PolicyID\n') invalid_specs.append( - (policy_spec_filename, - 'Conflicting PolicyID: {}'.format(policy_spec['PolicyID']))) + (analysis_spec_filename, + 'Conflicting PolicyID: {}'.format(analysis_spec['PolicyID']))) continue module, load_err = load_module( - os.path.join(dir_name, policy_spec['Filename'])) + os.path.join(dir_name, analysis_spec['Filename'])) # If the module could not be loaded, continue to the next if load_err: - invalid_specs.append((policy_spec_filename, load_err)) + invalid_specs.append((analysis_spec_filename, load_err)) continue - tests.append(policy_spec['PolicyID']) - if policy_spec['AnalysisType'] == 'policy': + tests.append(analysis_spec['PolicyID']) + if analysis_spec['AnalysisType'] == 'policy': run_func = module.policy - elif policy_spec['AnalysisType'] == 'rule': + elif analysis_spec['AnalysisType'] == 'rule': run_func = module.rule - failed_tests = run_tests(policy_spec, run_func, failed_tests) + failed_tests = run_tests(analysis_spec, run_func, failed_tests) print('') - for policy_id in failed_tests: - print("Failed: {}\n\t{}\n".format(policy_id, failed_tests[policy_id])) + for analysis_id in failed_tests: + print("Failed: {}\n\t{}\n".format(analysis_id, + failed_tests[analysis_id])) for spec_filename, spec_error in invalid_specs: print("Invalid: {}\n\t{}\n".format(spec_filename, spec_error)) @@ -303,10 +303,10 @@ def test_policies(args: argparse.Namespace) -> Tuple[int, list]: return int(bool(failed_tests or invalid_specs)), invalid_specs -def run_tests(policy: Dict[str, Any], run_func: Callable[[TestCase], bool], +def run_tests(analysis: Dict[str, Any], run_func: Callable[[TestCase], bool], failed_tests: DefaultDict[str, list]) -> DefaultDict[str, list]: - for unit_test in policy['Tests']: + for unit_test in analysis['Tests']: try: test_case = TestCase(unit_test['Resource'], unit_test['ResourceType']) @@ -317,7 +317,7 @@ def run_tests(policy: Dict[str, Any], run_func: Callable[[TestCase], bool], test_result = 'PASS' if result != unit_test['ExpectedResult']: test_result = 'FAIL' - failed_tests[policy['PolicyID']].append(unit_test['Name']) + failed_tests[analysis['PolicyID']].append(unit_test['Name']) print('\t[{}] {}'.format(test_result, unit_test['Name'])) return failed_tests @@ -326,47 +326,56 @@ def run_tests(policy: Dict[str, Any], run_func: Callable[[TestCase], bool], def setup_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( description= - 'Panther Analaysis Tool: A tool for writing, testing, and packaging Panther Policies/Rules', + 'Panther Analaysis Tool: A command line tool for managing Panther policies and rules.', prog='panther_analysis_tool') parser.add_argument('--version', action='version', - version='panther_analysis_tool 0.1.4') + version='panther_analysis_tool 0.1.7') subparsers = parser.add_subparsers() test_parser = subparsers.add_parser( - 'test', help='Validate policy specifications and run policy tests.') - test_parser.add_argument('--policies', - type=str, - help='The relative path to Panther policies.', - required=True) - test_parser.set_defaults(func=test_policies) + 'test', + help='Validate analysis specifications and run policy and rule tests.') + test_parser.add_argument( + '--path', + type=str, + help='The relative path to Panther policies and rules.', + required=True) + test_parser.set_defaults(func=test_analysis) zip_parser = subparsers.add_parser( 'zip', - help='Create an archive of local policies for uploading to Panther.') - zip_parser.add_argument('--policies', - type=str, - help='The relative path to Panther policies.', - required=True) - zip_parser.add_argument('--output-path', - type=str, - help='The path to write zipped policies to.', - required=True) - zip_parser.set_defaults(func=zip_policies) + help= + 'Create an archive of local policies and rules for uploading to Panther.' + ) + zip_parser.add_argument( + '--path', + type=str, + help='The relative path to Panther policies and rules.', + required=True) + zip_parser.add_argument( + '--out', + type=str, + help='The path to write zipped policies and rules to.', + required=True) + zip_parser.set_defaults(func=zip_analysis) upload_parser = subparsers.add_parser( - 'upload', help='Upload specified policies to a Panther deployment.') - upload_parser.add_argument('--policies', - type=str, - help='The relative path to Panther policies.', - required=True) + 'upload', + help='Upload specified policies and rules to a Panther deployment.') + upload_parser.add_argument( + '--path', + type=str, + help='The relative path to Panther policies and rules.', + required=True) upload_parser.add_argument( - '--output-path', + '--out', default='.', type=str, - help='The location to store a local copy of the packaged policies.', + help= + 'The location to store a local copy of the packaged policies and rules.', required=False) - upload_parser.set_defaults(func=upload_policies) + upload_parser.set_defaults(func=upload_analysis) return parser diff --git a/requirements.txt b/requirements.txt index ef7fad28..aca6da93 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,7 @@ # functional dependencies PyYAML==5.3.1 schema==0.7.1 -policyuniverse==1.3.2.1 -boto3==1.12.24 +boto3==1.12.26 # ci dependencies bandit==1.6.2 mypy==0.770 @@ -21,7 +20,7 @@ awscli==1.16.286 backcall==0.1.0 beautifulsoup4==4.8.1 binaryornot==0.4.4 -botocore==1.15.24 +botocore==1.15.26 bs4==0.0.1 certifi==2019.9.11 cfn-lint==0.26.0 @@ -70,6 +69,7 @@ pbr==5.4.1 pexpect==4.7.0 pickleshare==0.7.5 pipenv==2018.11.26 +policyuniverse==1.3.2.1 poyo==0.5.0 prompt-toolkit==2.0.10 ptyprocess==0.6.0 diff --git a/setup.py b/setup.py index 14246e1a..322613eb 100644 --- a/setup.py +++ b/setup.py @@ -2,14 +2,14 @@ setup( name='panther_analysis_tool', packages=['panther_analysis_tool'], - version='0.1.6', + version='0.1.7', license='apache-2.0', description= 'Panther command line interface for writing, testing, and packaging policies/rules.', author='Panther Labs Inc', author_email='pypi@runpanther.io', url='https://github.com/panther-labs/panther_analysis_tool', - download_url = 'https://github.com/panther-labs/panther_analysis_tool/archive/v0.1.6.tar.gz', + download_url = 'https://github.com/panther-labs/panther_analysis_tool/archive/v0.1.7.tar.gz', keywords=['Security', 'CLI'], scripts=['bin/panther_analysis_tool'], install_requires=[ diff --git a/tests/unit/panther_cli/__init__.py b/tests/unit/panther_analysis_tool/__init__.py similarity index 100% rename from tests/unit/panther_cli/__init__.py rename to tests/unit/panther_analysis_tool/__init__.py diff --git a/tests/unit/panther_cli/test_main.py b/tests/unit/panther_analysis_tool/test_main.py similarity index 75% rename from tests/unit/panther_cli/test_main.py rename to tests/unit/panther_analysis_tool/test_main.py index 0e26e3c3..554f9fa6 100644 --- a/tests/unit/panther_cli/test_main.py +++ b/tests/unit/panther_analysis_tool/test_main.py @@ -9,7 +9,7 @@ from panther_analysis_tool import main as pat -class TestPantherCLI(TestCase): +class TestPantherAnalysisTool(TestCase): fixture_path = 'tests/fixtures/' def setUp(self): @@ -17,13 +17,13 @@ def setUp(self): self.fs.add_real_directory(self.fixture_path) def test_valid_json_policy_spec(self): - for spec_filename, _, loaded_spec in pat.load_policy_specs('tests/fixtures'): + for spec_filename, _, loaded_spec in pat.load_analysis_specs('tests/fixtures'): if spec_filename == 'example_policy.json': assert_is_instance(loaded_spec, dict) assert_true(loaded_spec != {}) def test_valid_yaml_policy_spec(self): - for spec_filename, _, loaded_spec in pat.load_policy_specs('tests/fixtures'): + for spec_filename, _, loaded_spec in pat.load_analysis_specs('tests/fixtures'): if spec_filename == 'example_policy.yml': assert_is_instance(loaded_spec, dict) assert_true(loaded_spec != {}) @@ -34,13 +34,13 @@ def test_datetime_converted(self): assert_is_instance(test_date_string, str) def test_load_policy_specs_from_folder(self): - args = pat.setup_parser().parse_args('test --policies tests/fixtures'.split()) - return_code, invalid_specs = pat.test_policies(args) + args = pat.setup_parser().parse_args('test --path tests/fixtures'.split()) + return_code, invalid_specs = pat.test_analysis(args) assert_equal(return_code, 1) assert_equal(invalid_specs[0][0], 'tests/fixtures/example_malformed_policy.yml') - def test_zip_policies(self): + def test_zip_analysis(self): # Note: This is a workaround for CI try: self.fs.create_dir('tmp/') @@ -48,8 +48,8 @@ def test_zip_policies(self): pass args = pat.setup_parser().parse_args( - 'zip --policies tests/fixtures/valid_policies --output-path tmp/'.split()) - return_code, out_filename = pat.zip_policies(args) + 'zip --path tests/fixtures/valid_policies --out tmp/'.split()) + return_code, out_filename = pat.zip_analysis(args) statinfo = os.stat(out_filename) assert_true(statinfo.st_size > 0) assert_equal(return_code, 0)