From 45358155b8914802325961af31777c7512a20af0 Mon Sep 17 00:00:00 2001 From: Rony Xavier Date: Wed, 24 Jan 2018 20:39:58 -0500 Subject: [PATCH 01/12] dev harderning setup similar to inspec-aws bundle setup Signed-off-by: Rony Xavier --- .gitignore | 64 +-- .kitchen.yml | 37 -- .rubocop.yml | 99 +++++ .travis.yml | 9 + CONTRIBUTING.md | 155 ++++++++ Gemfile | 18 +- LICENSE | 190 +-------- README.md | 148 ++++--- Rakefile | 95 +++++ TESTING_AGAINST_AWS.md | 108 ++++++ data/private-pic.jpg | Bin 16027 -> 0 bytes data/public-pic.jpg | Bin 16027 -> 0 bytes docs/resources/aws_cloudtrail_trail.md | 132 +++++++ docs/resources/aws_cloudtrail_trails.md | 70 ++++ docs/resources/aws_cloudwatch_alarm.md | 76 ++++ .../aws_cloudwatch_log_metric_filter.md | 130 +++++++ docs/resources/aws_ec2_instance.md | 99 +++++ docs/resources/aws_ec2_security_group.md | 142 +++++++ docs/resources/aws_ec2_security_groups.md | 81 ++++ docs/resources/aws_iam_access_key.md | 56 +++ docs/resources/aws_iam_access_keys.md | 183 +++++++++ docs/resources/aws_iam_password_policy.md | 69 ++++ docs/resources/aws_iam_role.md | 54 +++ docs/resources/aws_iam_root_user.md | 57 +++ docs/resources/aws_iam_user.md | 63 +++ docs/resources/aws_iam_users.md | 55 +++ docs/resources/aws_s3_bucket.md | 123 ++++++ docs/resources/aws_sns_topic.md | 58 +++ docs/resources/aws_vpc.md | 110 ++++++ docs/resources/aws_vpcs.md | 45 +++ inspec.yml | 7 + libraries/_aws.rb | 7 + libraries/_aws_backend_factory_mixin.rb | 12 + libraries/_aws_connection.rb | 63 +++ libraries/_aws_resource_mixin.rb | 52 +++ libraries/aws_aaa_shim.rb | 3 + libraries/aws_cloudtrail_trail.rb | 74 ++++ libraries/aws_cloudtrail_trails.rb | 44 +++ libraries/aws_cloudwatch_alarm.rb | 61 +++ libraries/aws_cloudwatch_log_metric_filter.rb | 96 +++++ libraries/aws_ec2_instance.rb | 127 ++++++ libraries/aws_ec2_security_group.rb | 91 +++++ libraries/aws_ec2_security_groups.rb | 96 +++++ libraries/aws_iam_access_key.rb | 106 +++++ libraries/aws_iam_access_keys.rb | 178 +++++++++ libraries/aws_iam_password_policy.rb | 74 ++++ libraries/aws_iam_role.rb | 51 +++ libraries/aws_iam_root_user.rb | 34 ++ libraries/aws_iam_user.rb | 109 ++++++ libraries/aws_iam_users.rb | 104 +++++ libraries/aws_s3_bucket.rb | 102 +++++ libraries/aws_sns_topic.rb | 53 +++ libraries/aws_vpc.rb | 69 ++++ libraries/aws_vpcs.rb | 44 +++ pre-kitchen.rb | 21 - test/integration/cis-aws-foundations-baseline | 1 - test/integration/default/build/aws.tf | 21 + test/integration/default/build/cloudtrail.tf | 230 +++++++++++ test/integration/default/build/cloudwatch.tf | 95 +++++ test/integration/default/build/ec2.tf | 199 ++++++++++ test/integration/default/build/iam.tf | 90 +++++ .../integration/default/build/inspec-logo.png | Bin 0 -> 8501 bytes test/integration/default/build/s3.tf | 100 +++++ test/integration/default/build/sns.tf | 37 ++ .../verify/controls/aws_cloudtrail_trail.rb | 70 ++++ .../verify/controls/aws_cloudtrail_trails.rb | 5 + .../verify/controls/aws_cloudwatch_alarm.rb | 29 ++ .../aws_cloudwatch_log_metric_filter.rb | 74 ++++ .../verify/controls/aws_ec2_instance.rb | 78 ++++ .../verify/controls/aws_ec2_security_group.rb | 41 ++ .../controls/aws_ec2_security_groups.rb | 50 +++ .../verify/controls/aws_iam_access_key.rb | 89 +++++ .../default/verify/controls/aws_iam_role.rb | 6 + .../verify/controls/aws_iam_root_user.rb | 27 ++ .../default/verify/controls/aws_iam_user.rb | 46 +++ .../default/verify/controls/aws_iam_users.rb | 4 + .../default/verify/controls/aws_s3_bucket.rb | 113 ++++++ .../default/verify/controls/aws_sns_topic.rb | 39 ++ .../default/verify/controls/aws_vpc.rb | 59 +++ .../default/verify/controls/aws_vpcs.rb | 5 + test/integration/default/verify/inspec.yml | 4 + test/integration/minimal/build/aws.tf | 12 + .../verify/controls/aws_iam_root_user.rb | 27 ++ test/integration/minimal/verify/inspec.yml | 4 + test/unit/helper.rb | 10 + .../resources/aws_cloudtrail_trail_test.rb | 171 ++++++++ .../resources/aws_cloudtrail_trails_test.rb | 110 ++++++ .../resources/aws_cloudwatch_alarm_test.rb | 167 ++++++++ .../aws_cloudwatch_log_metric_filter_test.rb | 152 ++++++++ test/unit/resources/aws_ec2_instance_test.rb | 118 ++++++ .../resources/aws_ec2_security_group_test.rb | 121 ++++++ .../resources/aws_ec2_security_groups_test.rb | 105 +++++ .../unit/resources/aws_iam_access_key_test.rb | 287 ++++++++++++++ .../resources/aws_iam_access_keys_test.rb | 367 ++++++++++++++++++ .../resources/aws_iam_password_policy_test.rb | 83 ++++ test/unit/resources/aws_iam_role_test.rb | 103 +++++ test/unit/resources/aws_iam_root_user_test.rb | 48 +++ test/unit/resources/aws_iam_user_test.rb | 256 ++++++++++++ test/unit/resources/aws_iam_users_test.rb | 152 ++++++++ test/unit/resources/aws_s3_bucket_test.rb | 289 ++++++++++++++ test/unit/resources/aws_sns_topic_test.rb | 125 ++++++ test/unit/resources/aws_vpc.notes | 91 +++++ test/unit/resources/aws_vpc_test.rb | 163 ++++++++ test/unit/resources/aws_vpcs_test.rb | 97 +++++ tf_build/ec2.tf | 44 --- tf_build/output.tf | 5 - tf_build/s3.tf | 49 --- tf_build/variables.tf | 39 -- 108 files changed, 8420 insertions(+), 491 deletions(-) delete mode 100644 .kitchen.yml create mode 100644 .rubocop.yml create mode 100644 .travis.yml create mode 100644 CONTRIBUTING.md create mode 100644 Rakefile create mode 100644 TESTING_AGAINST_AWS.md delete mode 100644 data/private-pic.jpg delete mode 100644 data/public-pic.jpg create mode 100644 docs/resources/aws_cloudtrail_trail.md create mode 100644 docs/resources/aws_cloudtrail_trails.md create mode 100644 docs/resources/aws_cloudwatch_alarm.md create mode 100644 docs/resources/aws_cloudwatch_log_metric_filter.md create mode 100644 docs/resources/aws_ec2_instance.md create mode 100644 docs/resources/aws_ec2_security_group.md create mode 100644 docs/resources/aws_ec2_security_groups.md create mode 100644 docs/resources/aws_iam_access_key.md create mode 100644 docs/resources/aws_iam_access_keys.md create mode 100644 docs/resources/aws_iam_password_policy.md create mode 100644 docs/resources/aws_iam_role.md create mode 100644 docs/resources/aws_iam_root_user.md create mode 100644 docs/resources/aws_iam_user.md create mode 100644 docs/resources/aws_iam_users.md create mode 100644 docs/resources/aws_s3_bucket.md create mode 100644 docs/resources/aws_sns_topic.md create mode 100644 docs/resources/aws_vpc.md create mode 100644 docs/resources/aws_vpcs.md create mode 100644 inspec.yml create mode 100644 libraries/_aws.rb create mode 100644 libraries/_aws_backend_factory_mixin.rb create mode 100644 libraries/_aws_connection.rb create mode 100644 libraries/_aws_resource_mixin.rb create mode 100644 libraries/aws_aaa_shim.rb create mode 100644 libraries/aws_cloudtrail_trail.rb create mode 100644 libraries/aws_cloudtrail_trails.rb create mode 100644 libraries/aws_cloudwatch_alarm.rb create mode 100644 libraries/aws_cloudwatch_log_metric_filter.rb create mode 100644 libraries/aws_ec2_instance.rb create mode 100644 libraries/aws_ec2_security_group.rb create mode 100644 libraries/aws_ec2_security_groups.rb create mode 100644 libraries/aws_iam_access_key.rb create mode 100644 libraries/aws_iam_access_keys.rb create mode 100644 libraries/aws_iam_password_policy.rb create mode 100644 libraries/aws_iam_role.rb create mode 100644 libraries/aws_iam_root_user.rb create mode 100644 libraries/aws_iam_user.rb create mode 100644 libraries/aws_iam_users.rb create mode 100644 libraries/aws_s3_bucket.rb create mode 100644 libraries/aws_sns_topic.rb create mode 100644 libraries/aws_vpc.rb create mode 100644 libraries/aws_vpcs.rb delete mode 100755 pre-kitchen.rb delete mode 120000 test/integration/cis-aws-foundations-baseline create mode 100644 test/integration/default/build/aws.tf create mode 100644 test/integration/default/build/cloudtrail.tf create mode 100644 test/integration/default/build/cloudwatch.tf create mode 100644 test/integration/default/build/ec2.tf create mode 100644 test/integration/default/build/iam.tf create mode 100644 test/integration/default/build/inspec-logo.png create mode 100644 test/integration/default/build/s3.tf create mode 100644 test/integration/default/build/sns.tf create mode 100644 test/integration/default/verify/controls/aws_cloudtrail_trail.rb create mode 100644 test/integration/default/verify/controls/aws_cloudtrail_trails.rb create mode 100644 test/integration/default/verify/controls/aws_cloudwatch_alarm.rb create mode 100644 test/integration/default/verify/controls/aws_cloudwatch_log_metric_filter.rb create mode 100644 test/integration/default/verify/controls/aws_ec2_instance.rb create mode 100644 test/integration/default/verify/controls/aws_ec2_security_group.rb create mode 100644 test/integration/default/verify/controls/aws_ec2_security_groups.rb create mode 100644 test/integration/default/verify/controls/aws_iam_access_key.rb create mode 100644 test/integration/default/verify/controls/aws_iam_role.rb create mode 100644 test/integration/default/verify/controls/aws_iam_root_user.rb create mode 100644 test/integration/default/verify/controls/aws_iam_user.rb create mode 100644 test/integration/default/verify/controls/aws_iam_users.rb create mode 100644 test/integration/default/verify/controls/aws_s3_bucket.rb create mode 100644 test/integration/default/verify/controls/aws_sns_topic.rb create mode 100644 test/integration/default/verify/controls/aws_vpc.rb create mode 100644 test/integration/default/verify/controls/aws_vpcs.rb create mode 100644 test/integration/default/verify/inspec.yml create mode 100644 test/integration/minimal/build/aws.tf create mode 100644 test/integration/minimal/verify/controls/aws_iam_root_user.rb create mode 100644 test/integration/minimal/verify/inspec.yml create mode 100644 test/unit/helper.rb create mode 100644 test/unit/resources/aws_cloudtrail_trail_test.rb create mode 100644 test/unit/resources/aws_cloudtrail_trails_test.rb create mode 100644 test/unit/resources/aws_cloudwatch_alarm_test.rb create mode 100644 test/unit/resources/aws_cloudwatch_log_metric_filter_test.rb create mode 100644 test/unit/resources/aws_ec2_instance_test.rb create mode 100644 test/unit/resources/aws_ec2_security_group_test.rb create mode 100644 test/unit/resources/aws_ec2_security_groups_test.rb create mode 100644 test/unit/resources/aws_iam_access_key_test.rb create mode 100644 test/unit/resources/aws_iam_access_keys_test.rb create mode 100644 test/unit/resources/aws_iam_password_policy_test.rb create mode 100644 test/unit/resources/aws_iam_role_test.rb create mode 100644 test/unit/resources/aws_iam_root_user_test.rb create mode 100644 test/unit/resources/aws_iam_user_test.rb create mode 100644 test/unit/resources/aws_iam_users_test.rb create mode 100644 test/unit/resources/aws_s3_bucket_test.rb create mode 100644 test/unit/resources/aws_sns_topic_test.rb create mode 100644 test/unit/resources/aws_vpc.notes create mode 100644 test/unit/resources/aws_vpc_test.rb create mode 100644 test/unit/resources/aws_vpcs_test.rb delete mode 100644 tf_build/ec2.tf delete mode 100644 tf_build/output.tf delete mode 100644 tf_build/s3.tf delete mode 100644 tf_build/variables.tf diff --git a/.gitignore b/.gitignore index 64f4569..ecd5c24 100644 --- a/.gitignore +++ b/.gitignore @@ -1,56 +1,8 @@ -*.gem -*.rbc -/.config -/coverage/ -/InstalledFiles -/pkg/ -/spec/reports/ -/spec/examples.txt -/test/tmp/ -/test/version_tmp/ -/tmp/ -.kitchen/** -/*.DS_STORE -# Local .terraform directories -**/.terraform/* -./terraform.tfstate.d/* - -# .tfstate files -*.tfstate -*.tfstate.* - -.DS_Store -.AppleDouble -.LSOverride -._* - -# Used by dotenv library to load environment variables. -# .env - -## Specific to RubyMotion (use of CocoaPods): -# -# We recommend against adding the Pods directory to your .gitignore. However -# you should judge for yourself, the pros and cons are mentioned at: -# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control -# -# vendor/Pods/ - -## Documentation cache and generated files: -/.yardoc/ -/_yardoc/ -/doc/ -/rdoc/ - -## Environment normalization: -/.bundle/ -/vendor/bundle -/lib/bundler/man/ - -# for a library or gem, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -Gemfile.lock -.ruby-version -# .ruby-gemset - -# unless supporting rvm < 1.11.0 or doing something fancy, ignore this: -.rvmrc +.attribute.yml +.bundle/ +.terraform/ +inspec.lock +Gemfile.lock +terraform.tfstate* +terraform.tfstate.backup + diff --git a/.kitchen.yml b/.kitchen.yml deleted file mode 100644 index 7901ee1..0000000 --- a/.kitchen.yml +++ /dev/null @@ -1,37 +0,0 @@ ---- -driver: - name: terraform - variables: - aws_ssh_key_name: "<%= ENV['AWS_SSH_KEY_ID'] %>" - aws_access_key: "<%= ENV['AWS_ACCESS_KEY_ID'] %>" - aws_secret_key: "<%= ENV['AWS_SECRET_ACCESS_KEY'] %>" - aws_instance_type: "<%= ENV['AWS_DEFAULT_INSTANCE_TYPE'] %>" - aws_subnet_id: "<%= ENV['AWS_SUBNET_ID'] %>" - aws_region: "<%= ENV['AWS_REGION'] %>" - aws_ami_id: "<%= ENV['AWS_AMI_ID'] %>" - aws_security_group: "<%= ENV['AWS_SG_ID'] %>" - - root_module_directory: ./tf_build - -provisioner: - name: terraform - -platforms: - - name: centos-7 - -transport: - name: ssh - ssh_key: "<%= ENV['AWS_SSH_KEY_ID'] %>" - -verifier: - name: terraform - #format: json - #output: "%{platform}_%{suite}-<%= Time.now.iso8601 %>.json" - groups: - - name: cis-aws-foundations-baseline - -suites: - - name: cis-aws-foundations-baseline - - - diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..46550f9 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,99 @@ +--- +AllCops: + TargetRubyVersion: 2.3 + Exclude: + - Gemfile + - Rakefile + - 'test/**/*' + - 'examples/**/*' + - 'vendor/**/*' + - 'lib/bundles/inspec-init/templates/**/*' + - 'www/tutorial/**/*' +AlignParameters: + Enabled: true +BlockDelimiters: + Enabled: false +Documentation: + Enabled: false +EmptyLinesAroundBlockBody: + Enabled: false +FrozenStringLiteralComment: + Enabled: false +HashSyntax: + Enabled: true +LineLength: + Enabled: false +Layout/AlignHash: + Enabled: false +Layout/EmptyLineAfterMagicComment: + Enabled: false +Layout/EndOfLine: + Enabled: true + EnforcedStyle: lf +Layout/SpaceAroundOperators: + Enabled: false +MethodLength: + Max: 40 +Metrics/AbcSize: + Max: 33 +Metrics/BlockLength: + Max: 50 +Metrics/CyclomaticComplexity: + Max: 10 +Metrics/PerceivedComplexity: + Max: 11 +Naming/FileName: + Enabled: false +Naming/HeredocDelimiterNaming: + Enabled: false +Naming/PredicateName: + Enabled: false +NumericLiterals: + MinDigits: 10 +Security/YAMLLoad: + Enabled: false +Style/AndOr: + Enabled: false +Style/BracesAroundHashParameters: + Enabled: false +Style/ClassAndModuleChildren: + Enabled: false +Style/ConditionalAssignment: + Enabled: false +Style/EmptyMethod: + Enabled: false +Style/Encoding: + Enabled: false +Style/IfUnlessModifier: + Enabled: false +Style/MethodMissing: + Enabled: false +Style/MultilineIfModifier: + Enabled: false +Style/NegatedIf: + Enabled: false +Style/Not: + Enabled: false +Style/NumericLiteralPrefix: + Enabled: false +Style/NumericPredicate: + Enabled: false +Style/PercentLiteralDelimiters: + PreferredDelimiters: + '%': '{}' + '%i': () + '%q': '{}' + '%Q': () + '%r': '{}' + '%s': () + '%w': '{}' + '%W': () + '%x': () +Style/SymbolArray: + Enabled: false +Style/TrailingCommaInArguments: + EnforcedStyleForMultiline: comma +Style/TrailingCommaInLiteral: + EnforcedStyleForMultiline: comma +Style/UnlessElse: + Enabled: false diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..cb954c8 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,9 @@ +sudo: false +language: ruby +cache: bundler + +rvm: + - 2.3.1 + +bundler_args: --without integration +script: bundle exec rake diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..b924229 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,155 @@ +# Contributing to InSpec + +We are glad you want to contribute to InSpec! This document will help answer common questions you may have during your first contribution. + +## Submitting Issues + +We utilize **Github Issues** for issue tracking and contributions. You can contribute in two ways: + +1. Reporting an issue or making a feature request [here](#issues). +2. Adding features or fixing bugs yourself and contributing your code to InSpec. + +We ask you not to submit security concerns via Github. For details on submitting potential security issues please see + +## Contribution Process + +We have a 3 step process for contributions: + +1. Commit changes to a git branch, making sure to sign-off those changes for the [Developer Certificate of Origin](#developer-certification-of-origin-dco). +2. Create a Github Pull Request for your change, following the instructions in the pull request template. +3. Perform a [Code Review](#code-review-process) with the project maintainers on the pull request. + +### Pull Request Requirements + +Chef Projects are built to last. We strive to ensure high quality throughout the experience. In order to ensure this, we require that all pull requests to Chef projects meet these specifications: + +1. **Tests:** To ensure high quality code and protect against future regressions, we require all the code in Chef Projects to have at least unit test coverage. See the [test/unit](https://github.com/chef/inspec/tree/master/test/unit) +directory for the existing tests and use ```bundle exec rake test``` to run them. +2. **Green CI Tests:** We use [Travis CI](https://travis-ci.org/) and/or [AppVeyor](https://www.appveyor.com/) CI systems to test all pull requests. We require these test runs to succeed on every pull request before being merged. +3. **Up-to-date Documentation:** Every code change should be reflected in an update for our [documentation](https://github.com/chef/inspec/tree/master/docs). We expect PRs to update the documentation with the code change. + +In addition to this it would be nice to include the description of the problem you are solving + with your change. You can use [Issue Template](#issuetemplate) in the description section + of the pull request. + +### Code Review Process + +Code review takes place in Github pull requests. See [this article](https://help.github.com/articles/about-pull-requests/) if you're not familiar with Github Pull Requests. + +Once you open a pull request, project maintainers will review your code and respond to your pull request with any feedback they might have. The process at this point is as follows: + +1. Two thumbs-up (:+1:) are required from project maintainers. See the master maintainers document for InSpec projects at . +2. When ready, your pull request will be merged into `master`, we may require you to rebase your PR to the latest `master`. +3. Once the PR is merged, you will be included in `CHANGELOG.md`. + +If you would like to learn about when your code will be available in a release of Chef, read more about [Chef Release Cycles](#release-cycles). + + +### Developer Certification of Origin (DCO) + +Licensing is very important to open source projects. It helps ensure the software continues to be available under the terms that the author desired. + +Chef uses [the Apache 2.0 license](https://github.com/chef/chef/blob/master/LICENSE) to strike a balance between open contribution and allowing you to use the software however you would like to. + +The license tells you what rights you have that are provided by the copyright holder. It is important that the contributor fully understands what rights they are licensing and agrees to them. Sometimes the copyright holder isn't the contributor, such as when the contributor is doing work on behalf of a company. + +To make a good faith effort to ensure these criteria are met, Chef requires the Developer Certificate of Origin (DCO) process to be followed. + +The DCO is an attestation attached to every contribution made by every developer. In the commit message of the contribution, the developer simply adds a Signed-off-by statement and thereby agrees to the DCO, which you can find below or at . + +``` +Developer's Certificate of Origin 1.1 + +By making a contribution to this project, I certify that: + +(a) The contribution was created in whole or in part by me and I + have the right to submit it under the open source license + indicated in the file; or + +(b) The contribution is based upon previous work that, to the + best of my knowledge, is covered under an appropriate open + source license and I have the right under that license to + submit that work with modifications, whether created in whole + or in part by me, under the same open source license (unless + I am permitted to submit under a different license), as + Indicated in the file; or + +(c) The contribution was provided directly to me by some other + person who certified (a), (b) or (c) and I have not modified + it. + +(d) I understand and agree that this project and the contribution + are public and that a record of the contribution (including + all personal information I submit with it, including my + sign-off) is maintained indefinitely and may be redistributed + consistent with this project or the open source license(s) + involved. +``` + +For more information on the change see the Chef Blog post [Introducing Developer Certificate of Origin](https://blog.chef.io/2016/09/19/introducing-developer-certificate-of-origin/) + +#### DCO Sign-Off Methods + +The DCO requires a sign-off message in the following format appear on each commit in the pull request: + +``` +Signed-off-by: Julia Child +``` + +The DCO text can either be manually added to your commit body, or you can add either **-s** or **--signoff** to your usual git commit commands. If you forget to add the sign-off you can also amend a previous commit with the sign-off by running **git commit --amend -s**. If you've pushed your changes to Github already you'll need to force push your branch after this with **git push -f**. + +### Obvious Fix Policy + +Small contributions, such as fixing spelling errors, where the content is small enough to not be considered intellectual property, can be submitted without signing the contribution for the DCO. + +As a rule of thumb, changes are obvious fixes if they do not introduce any new functionality or creative thinking. Assuming the change does not affect functionality, some common obvious fix examples include the following: + +- Spelling / grammar fixes +- Typo correction, white space and formatting changes +- Comment clean up +- Bug fixes that change default return values or error codes stored in constants +- Adding logging messages or debugging output +- Changes to 'metadata' files like Gemfile, .gitignore, build scripts, etc. +- Moving source files from one directory or package to another + +**Whenever you invoke the "obvious fix" rule, please say so in your commit message:** + +``` +------------------------------------------------------------------------ +commit 370adb3f82d55d912b0cf9c1d1e99b132a8ed3b5 +Author: Julia Child +Date: Wed Sep 18 11:44:40 2015 -0700 + + Fix typo in the README. + + Obvious fix. + +------------------------------------------------------------------------ +``` + +## Release Cycles + +Our primary shipping vehicle is operating system specific packages that includes all the requirements of InSpec. We call these [Omnibus packages](https://github.com/chef/omnibus) + +We also release our software as gems to [Rubygems](https://rubygems.org/) but we strongly recommend using InSpec or ChefDK packages. + +Our version numbering roughly follows [Semantic Versioning](http://semver.org/) standard. Our standard version numbers look like X.Y.Z which mean: + +- X is a major release, which may not be fully compatible with prior major releases +- Y is a minor release, which adds both new features and bug fixes +- Z is a patch release, which adds just bug fixes + +After shipping a release of InSpec we bump at least the `Minor` version by one to start development of the next minor release. We do a release approximately every week. Announcements of releases are made to the [InSpec mailing list](https://discourse.chef.io/c/chef-release) when they are available. + +## InSpec Community + +InSpec is made possible by a strong community of developers, system administrators, auditor and security experts. If you have any questions or if you would like to get involved in the InSpec community you can check out: + +- [InSpec Mailing List](https://discourse.chef.io/c/inspec) +- [Chef Community Slack](https://community-slack.chef.io/) + +Also here are some additional pointers to some awesome Chef content: + +- [InSpec Docs](http://inspec.io/docs/) +- [Learn Chef](https://learn.chef.io/) +- [Chef Website](https://www.chef.io/) diff --git a/Gemfile b/Gemfile index 79dca9e..6037f6b 100644 --- a/Gemfile +++ b/Gemfile @@ -1,8 +1,14 @@ -# encoding: utf-8 - source 'https://rubygems.org' -gem 'kitchen-terraform', "~> 3.0.0" -gem 'test-kitchen', "~> 1.16.0" -gem 'inspec' -gem 'kitchen-inspec' +gem 'inspec', '~> 1' +gem 'aws-sdk', '~> 2' + +group :tools do + gem 'github_changelog_generator', '~> 1.12.0' +end + +group :test do + gem 'rake' + gem 'rubocop', '~> 0.51.0' + gem 'minitest', '5.10.1' +end \ No newline at end of file diff --git a/LICENSE b/LICENSE index 261eeb9..161d799 100644 --- a/LICENSE +++ b/LICENSE @@ -1,192 +1,4 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] +Copyright (c) 2016 Chef Software Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index 4226e0f..e172bde 100644 --- a/README.md +++ b/README.md @@ -1,64 +1,124 @@ -# cis-aws-foundations-hardening - v1.0.0 +# InSpec for AWS -A terraform / kitchen-terraform hardening baseline the CIS AWS Foundations Benchmark v1.10. +## Roadmap -## Overview +This repository is the development repository for InSpec for AWS. Once [RFC Platforms](https://github.com/chef/inspec/issues/1661) is fully implemented in InSpec, this repository is going to be merged into core InSpec. -This will help you setup and validate an AWS VPC/ENV ... +As of now, AWS resources are implemented as an InSpec resource pack. It will ship with the required resources to write your own AWS tests. -### Tech Used -- kitchen-terraform (v3.0.0) -- test-kitchen (v.1.60.0) -- inspec.io -- terraform (v0.10.0) -- tfenv -- awscli (v1.1) +``` +├── README.md - this readme +└── libraries - contains AWS resources +``` + +## Get started + +Before running the profile with InSpec, define environment variables with your AWS region and credentials. InSpec supports the following standard AWS variables: + +- `AWS_REGION` +- `AWS_DEFAULT_REGION` +- `AWS_ACCESS_KEY_ID` +- `AWS_SECRET_ACCESS_KEY` +- `AWS_SESSION_TOKEN` (optional) + +Those variables are defined in [AWS CLI Docs](http://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html#cli-environment) + +## Use the resources + +Since this is a InSpec resource pack, it only defines InSpec resources. It includes example tests only. You can easily use the AWS InSpec resources in your tests do the following: + +### Create a new profile + +``` +inspec init profile my-profile +``` + +### Adapt the `inspec.yml` + +``` +name: my-profile +title: My own AWS profile +version: 0.1.0 +depends: + - name: aws + url: https://github.com/chef/inspec-aws/archive/master.tar.gz +``` -## Pre-Checks +### Add controls + +Since your profile depends on the resource pack, you can use those resources in your own profile: -A. Use `tfenv` to switch to your `tf v0.10.0` environment -B. Install any needed gems via `bundle install` -C. Use the `pre-kitchen.rb` script to ensure you have all the env_vars setup as needed. ``` -ruby pre-kitchen.rb +control "aws-1" do + impact 0.7 + title 'Checks the machine is running' + + describe aws_ec2_instance('my-ec2-machine') do + it { should be_running } + end +end ``` -D. Go to the 'Usage' section -## Usage +### Running your profile + +Then use `inspec exec my-profile` to execute your new profile. + +Our future intent is to support an `aws` target for InSpec/Train, so you may also pass credentials `inspec exec my-profile -t aws://accesskey:secret@region`. + +* See [train/issues/229](https://github.com/chef/train/issues/229). + +### Available Resources -### Setup your Environment + * `aws_ec2_instance` - This resource reads information about an ec2 instance + * `aws_iam_access_key` - Verifies settings for AWS IAM access keys + * `aws_iam_password_policy` - Verifies iam password policy + * `aws_iam_root_user` - Verifies settings for AWS root account + * `aws_iam_user` - Verifies settings for a specific AWS IAM user + * `aws_iam_users` - Verifies settings for AWS IAM users -You will need to set the following env_vars for this to work. +### Roadmap + + * `aws_ami` + * `aws_s3bucket` + * `aws_security_group` + * `aws_iam_group` + * `aws_iam_policy` + * `aws_iam_role` + +## Developing and Testing the AWS Resources Pack + +### Unit tests + +To execute the unit tests, run: + +``` +bundle exec rake test +``` -- AWS_SUBNET_ID - The AWS Subnet you wish to use ... (default: none) -- AWS_SSH_KEY_ID - The SSH Key that is associated with ... (default: none) -- AWS_ACCESS_KEY_ID - The AWS Access Key that is ... (default: none) -- AWS_SECRET_ACCESS_KEY ... (default: none) -- AWS_DEFAULT_INSTANCE_TYPE ... (default: none) (suggested: t2.micro) -- AWS_REGION - The AWS Region you would like to use (default: us-east-1) -- AWS_AMI_ID ... (default: none) -- AWS_SG_ID ... (default: none) +### Integration tests +Please see TESTING_AGAINST_AWS.md for details on how to setup the needed AWS accounts to perform testing. -1. Switch to your Terraform 0.10.0 environment -2. Ensure your environment variables are configured (see above) -3. Run Test kitchen +## Kudos - **A.** **Run Each Phase of the Test Suite** - a. `bundle exec kitchen create` - b. `bundle exec kitchen converge` - c. `bundle exec kitchen verify` - d. `bundle exec kitchen destroy` - **B.** **Run the Fully Automated Suite** - a. `bundle exec kitchen test --destroy=always` +This project was inspired by [inspec-aws](https://github.com/arothian/inspec-aws) from [arothian](https://github.com/arothian). -## Quetions: +## License -- see: https://newcontext-oss.github.io/kitchen-terraform/tutorials/amazon_provider_ec2.html -- see: https://github.com/chef/inspec-aws +| | | +| ------ | --- | +| **Author:** | Christoph Hartmann () | +| **Copyright:** | Copyright (c) 2017 Chef Software Inc. | +| **License:** | Apache License, Version 2.0 | -## Developing +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at -## Contributing + http://www.apache.org/licenses/LICENSE-2.0 -## Pushing a Pull Request +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..8169744 --- /dev/null +++ b/Rakefile @@ -0,0 +1,95 @@ +#!/usr/bin/env rake +# encoding: utf-8 + +require 'rake/testtask' +require 'rubocop/rake_task' +require 'securerandom' + +def prompt(message) + print(message) + STDIN.gets.chomp +end + +# Rubocop +desc 'Run Rubocop lint checks' +task :rubocop do + RuboCop::RakeTask.new +end + +# Minitest +Rake::TestTask.new do |t| + t.libs << 'libraries' + t.libs << 'test/unit' + t.pattern = "test/unit/**/*_test.rb" +end + +# lint the project +desc 'Run robocop linter' +task lint: [:rubocop] + +# run tests +task default: [:lint, :test] + +namespace :test do + project_dir = File.dirname(__FILE__) + + # run inspec check to verify that the profile is properly configured + task :check do + sh("bundle exec inspec check #{project_dir}") + end + + namespace :aws do + ['default', 'minimal'].each do |account| + integration_dir = File.join(project_dir, 'test', 'integration', account) + attribute_file = File.join(integration_dir, '.attribute.yml') + + task :"setup:#{account}", :tf_workspace do |t, args| + tf_workspace = args[:tf_workspace] || ENV['INSPEC_TERRAFORM_ENV'] + abort("You must either call the top-level test:aws:#{account} task, or set the INSPEC_TERRAFORM_ENV variable.") unless tf_workspace + puts "----> Setup" + abort("You must set the environment variable AWS_REGION") unless ENV['AWS_REGION'] + puts "----> Checking for required AWS profile..." + sh("aws configure get aws_access_key_id --profile inspec-aws-test-#{account} > /dev/null") + sh("cd #{integration_dir}/build/ && terraform init") + sh("cd #{integration_dir}/build/ && terraform workspace new #{tf_workspace}") + sh("cd #{integration_dir}/build/ && AWS_PROFILE=inspec-aws-test-#{account} terraform plan") + sh("cd #{integration_dir}/build/ && AWS_PROFILE=inspec-aws-test-#{account} terraform apply") + Rake::Task["test:aws:dump_attrs:#{account}"].execute + end + + task :"dump_attrs:#{account}" do + sh("cd #{integration_dir}/build/ && AWS_PROFILE=inspec-aws-test-#{account} terraform output > #{attribute_file}") + raw_output = File.read(attribute_file) + yaml_output = raw_output.gsub(" = ", " : ") + File.open(attribute_file, "w") {|file| file.puts yaml_output} + end + + task :"run:#{account}" do + puts "----> Run" + sh("AWS_PROFILE=inspec-aws-test-#{account} bundle exec inspec exec #{integration_dir}/verify --attrs #{attribute_file}") + end + + task :"cleanup:#{account}", :tf_workspace do |t, args| + tf_workspace = args[:tf_workspace] || ENV['INSPEC_TERRAFORM_ENV'] + abort("You must either call the top-level test:aws:#{account} task, or set the INSPEC_TERRAFORM_ENV variable.") unless tf_workspace + puts "----> Cleanup" + sh("cd #{integration_dir}/build/ && AWS_PROFILE=inspec-aws-test-#{account} terraform destroy -force") + sh("cd #{integration_dir}/build/ && terraform workspace select default") + sh("cd #{integration_dir}/build && terraform workspace delete #{tf_workspace}") + end + + task :"#{account}" do + tf_workspace = ENV['INSPEC_TERRAFORM_ENV'] || prompt("Please enter a workspace for your integration tests to run in: ") + begin + Rake::Task["test:aws:setup:#{account}"].execute({:tf_workspace => tf_workspace}) + Rake::Task["test:aws:run:#{account}"].execute + rescue + abort("Integration testing has failed for the #{account} account") + ensure + Rake::Task["test:aws:cleanup:#{account}"].execute({:tf_workspace => tf_workspace}) + end + end + end + end + task aws: [:'aws:default', :'aws:minimal'] +end \ No newline at end of file diff --git a/TESTING_AGAINST_AWS.md b/TESTING_AGAINST_AWS.md new file mode 100644 index 0000000..51fc73a --- /dev/null +++ b/TESTING_AGAINST_AWS.md @@ -0,0 +1,108 @@ +# Testing Against AWS - Integration Testing + +## Problem Statement + +We want to be able to test AWS-related InSpec resources against AWS itself. This means we need to create constructs ("test fixtures") in AWS to examine using InSpec. For cost management, we also want to be able to destroy + +## General Approach + +We use Terraform to setup test fixtures in AWS, then run a defined set of InSpec controls against these (which should all pass), and finally tear down the test fixtures with Terraform. For fixtures that cannot be managed by Terraform, we manually setup fixtures using instructions below. + +We use the AWS CLI credentials system to manage credentials. + + +### Installing Terraform + +Download [Terraform](https://www.terraform.io/downloads.html). We require at least v0.10 . To install and choose from multiple Terraform versions, consider using [tfenv](https://github.com/kamatama41/tfenv). + +### Installing AWS CLI + +Install the [AWS CLI](http://docs.aws.amazon.com/cli/latest/userguide/installing.html). We will store profiles for testing in the `~/.aws/credentials` file. + +## Limitations + +There are some things that we can't (or very much shouldn't) do via Terraform - like manipulating the root account MFA settings. + +Also, there are some singleton resources (such as the default VPC, or Config status) that we should not manipulate without consequences. + +## Current Solution + +Our solution is to create two AWS accounts, each dedicated to the task of integration testing inspec-aws. + +In the "default" account, we setup all fixtures that can be handled by Terraform. For any remaining fixtures, +such as enabling MFA on the root account, we manually set one value in the "default" account, and manually set the opposing value in the "minimal" account. This allows use to perform testing on any reachable resource or property, regardless of whether or not Terraform can manage it. + +All tests (and test fixtures) that do not require special handling are placed in the "default" set. That includes both positive and negative checks. + +Note that some tests will fail for the first day or two after you set up the accounts, due to the tests checking properties such as the last usage time of an access key, for example. + +Additionally, the first time you run the tests, you will need to accept the user agreement in the AWS marketplace for the linux AMIs we use. You'll need to do it 4 times, once for each of debian and centos on the two accounts. + +### Creating the Default account + +Follow these instructions carefully. Do not perform any action not specified. + +1. Create an AWS account. Make a note of the account email and root password in a secure secret storage system. +2. Create an IAM user named `test-fixture-maker`. + * Enable programmatic access (to generate an access key) + * Direct-attach the policy AdministratorAccess + * Note the access key and secret key ID that are generated. +3. Using the aws command line tool, store the access key and secret key in a profile with a special name: + `aws configure --profile inspec-aws-test-default` + +#### Test Fixtures for the Default Account + +1. As the root user, enable a virtual MFA device. +2. Create an IAM user named 'test-user-last-key-use'. + * Enable programmatic access (to generate an access key) + * Note the access key and secret key ID that are generated. + * Direct-attach the policy AmazonEC2ReadOnlyAccess + * Using the AWS CLI and the credentials, execute the command `aws ec2 describe-instances`. + * The goal here is to have an access key that was used at one point. + +### Creating the Minimal Account + +Follow these instructions carefully. Do not perform any action not specified. + +1. Create an AWS account. Make a note of the account email and root password in a secure secret storage system. +2. Create an IAM user named `test-fixture-maker`. + * Enable programmatic access (to generate an access key) + * Direct-attach the policy AdministratorAccess + * Note the access key and secret key ID that are generated. +3. Using the aws command line tool, store the access key and secret key in a profile with a special name: + `aws configure --profile inspec-aws-test-minimal` + +#### Test Fixtures for the Minimal Account + +1. Create an Access Key for the root user. You do not have to save the access key. + +## Running the integration tests + +To run all AWS integration tests, run: + + ``` + bundle exec rake test:aws + ``` + +To run the tests against one account only: + + ``` + bundle exec rake test:aws:default + ``` + + or + + ``` + bundle exec rake test:aws:minimal + ``` + +Each account has separate tasks for setup, running the tests, and cleanup. You may run them separately: + +``` +bundle exec rake test:aws:setup:default +bundle exec rake test:aws:run:default +bundle exec rake test:aws:cleanup:default +``` + + + diff --git a/data/private-pic.jpg b/data/private-pic.jpg deleted file mode 100644 index 2d794df16e133b979ddc2b284ba382cb89c7f8c2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16027 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 b*M2lR_P+o9QiHuY+MA<24E+0H037^(V1Z?f diff --git a/data/public-pic.jpg b/data/public-pic.jpg deleted file mode 100644 index 2d794df16e133b979ddc2b284ba382cb89c7f8c2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16027 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 b*M2lR_P+o9QiHuY+MA<24E+0H037^(V1Z?f diff --git a/docs/resources/aws_cloudtrail_trail.md b/docs/resources/aws_cloudtrail_trail.md new file mode 100644 index 0000000..b956bc6 --- /dev/null +++ b/docs/resources/aws_cloudtrail_trail.md @@ -0,0 +1,132 @@ +--- +title: About the aws_cloudtrail_trail Resource +--- + +# aws_cloudtrail_trail + +Use the `aws_cloudtrail_trail` InSpec audit resource to test properties of a single AWS Cloudtrail Trail. + +AWS CloudTrail is a service that enables governance, compliance, operational auditing, and risk auditing of your AWS account. With CloudTrail, you can log, continuously monitor, and retain account activity related to actions across your AWS infrastructure. CloudTrail provides event history of your AWS account activity, including actions taken through the AWS Management Console, AWS SDKs, command line tools, and other AWS services. This event history simplifies security analysis, resource change tracking, and troubleshooting. + +Each AWS Cloudtrail Trail is uniquely identified by its trail_name or trail_arn. + +
+ +## Syntax + +An `aws_cloudtrail_trail` resource block identifies a trail by trail_name. + + # Find a trail by name + describe aws_cloudtrail_trail('trail-name') do + it { should exist } + end + + # Hash syntax for trail name + describe aws_cloudtrail_trail(trail_name: 'trail-name') do + it { should exist } + end + +
+ +## Examples + +The following examples show how to use this InSpec audit resource. + +### Test that the specified trail does exist + + describe aws_cloudtrail_trail('trail-name') do + it { should exist } + end + +### Test that the specified trail is encrypted using SSE-KMS + + describe aws_cloudtrail_trail('trail-name') do + it { should be_encrypted } + end + +### Test that the specified trail is a multi region trail + + describe aws_cloudtrail_trail('trail-name') do + it { should be_multi_region_trail } + end + +
+ +## Properties + +### s3_bucket_name + +Specifies the name of the Amazon S3 bucket designated for publishing log files. + + describe aws_cloudtrail_trail('trail-name') do + its('s3_bucket_name') { should cmp "s3-bucket-name" } + end + +### trail_arn + +The ARN identifier of the specified trail. An ARN uniquely identifies the trail within AWS. + + describe aws_cloudtrail_trail('trail-name') do + its('trail_arn') { should cmp "arn:aws:cloudtrail:us-east-1:484747447281:trail/trail-name" } + end + +### cloud_watch_logs_role_arn + +Specifies the role for the CloudWatch Logs endpoint to assume to write to a user\'s log group. + + describe aws_cloudtrail_trail('trail-name') do + its('cloud_watch_logs_role_arn') { should include "arn:aws:iam:::role/CloudTrail_CloudWatchLogs_Role" } + end + +### cloud_watch_logs_log_group_arn + +Specifies a log group name using an Amazon Resource Name (ARN), a unique identifier that represents the log group to which CloudTrail logs will be delivered. + + describe aws_cloudtrail_trail('trail-name') do + its('cloud_watch_logs_log_group_arn') { should include "arn:aws:logs:us-east-1::log-group:test:*" } + end + +### kms_key_id + +Specifies the KMS key ID to used to encrypt the logs delivered by CloudTrail. + + describe aws_cloudtrail_trail('trail-name') do + its('kms_key_id') { should include "key-arn" } + end + +### home_region + +Specifies the region in which the trail was created. + + describe aws_cloudtrail_trail('trail-name') do + its('home_region') { should include "us-east-1" } + end + + +## Matchers + +This InSpec audit resource has the following special matchers. For a full list of available matchers (such as `exist`) please visit our [matchers page](https://www.inspec.io/docs/reference/matchers/). + +### be_multi_region_trail + +The test will pass if the identified trail is a multi region trail. + + describe aws_cloudtrail_trail('trail-name') do + it { should be_multi_region_trail } + end + +### be_encrypted + +The test will pass if the logs delivered by the identified trail is encrypted. + + describe aws_cloudtrail_trail('trail-name') do + it { should be_encrypted } + end + +### be_log_file_validation_enabled + +The test will pass if the identified trail has log file integrity validation is enabled. + + describe aws_cloudtrail_trail('trail-name') do + it { should be_log_file_validation_enabled } + end diff --git a/docs/resources/aws_cloudtrail_trails.md b/docs/resources/aws_cloudtrail_trails.md new file mode 100644 index 0000000..04c405c --- /dev/null +++ b/docs/resources/aws_cloudtrail_trails.md @@ -0,0 +1,70 @@ +--- +title: About the aws_cloudtrail_trails Resource +--- + +# aws_cloudtrail_trails + +Use the `aws_cloudtrail_trails` InSpec audit resource to test properties of some or all AWS CloudTrail Trails. + +AWS CloudTrail is a service that enables governance, compliance, operational auditing, and risk auditing of your AWS account. With CloudTrail, you can log, continuously monitor, and retain account activity related to actions across your AWS infrastructure. CloudTrail provides event history of your AWS account activity, including actions taken through the AWS Management Console, AWS SDKs, command line tools, and other AWS services. This event history simplifies security analysis, resource change tracking, and troubleshooting. + +Each AWS CloudTrail Trails is uniquely identified by its trail name or trail arn. + +
+ +## Syntax + +An `aws_cloudtrail_trails` resource block collects a group of CloudTrail Trails and then tests that group. + + # Verify the number of CloudTrail Trails in the AWS account + describe aws_cloudtrail_trails do + its('entries.count') { should cmp 10 } + end + +
+ +## Examples + +The following examples show how to use this InSpec audit resource. + +As this is the initial release of `aws_cloudtrail_trails`, its limited functionality precludes examples. + +
+ +## Matchers + +### exists + +The control will pass if the filter returns at least one result. Use should_not if you expect zero matches. + + # Verify that at least one CloudTrail Trail exists. + describe aws_cloudtrail_trails + it { should exist } + end + +## Properties + +### names + +Provides a list of trail names for all CloudTrail Trails in the AWS account. + + describe aws_cloudtrail_trails do + its('names') { should include('trail-1') } + end + +### trail_arns + +Provides a list of trail arns for all CloudTrail Trails in the AWS account. + + describe aws_cloudtrail_trails do + its('trail_arns') { should include('arn:aws:cloudtrail:us-east-1::trail/trail-1') } + end + +### entries + +Provides access to the raw results of the query. This can be useful for checking counts and other advanced operations. + + # Allow at most 100 CloudTrail Trails on the account + describe aws_cloudtrail_trails do + its('entries.count') { should be <= 100} + end diff --git a/docs/resources/aws_cloudwatch_alarm.md b/docs/resources/aws_cloudwatch_alarm.md new file mode 100644 index 0000000..77548c5 --- /dev/null +++ b/docs/resources/aws_cloudwatch_alarm.md @@ -0,0 +1,76 @@ +--- +title: About the aws_cloudwatch_alarm Resource +--- + +# aws_cloudwatch_alarm + +Use the `aws_cloudwatch_alarm` InSpec audit resource to test properties of a single Cloudwatch Alarm. + +Cloudwatch Alarms are currently identified using the metric name and metric namespace. Future work may allow other approaches to identifying alarms. + +
+ +## Syntax + +An `aws_cloudwatch_alarm` resource block searches for a Cloudwatch Alarm, specified by several search options. If more than one Alarm matches, an error occurs. + + # Look for a specific alarm + aws_cloudwatch_alarm( + metric: 'my-metric-name', + metric_namespace: 'my-metric-namespace', + ) do + it { should exist } + end + +
+ +## Examples + +The following examples show how to use this InSpec audit resource. + +### Ensure an Alarm has at least one alarm action + + describe aws_cloudwatch_alarm( + metric: 'my-metric-name', + metric_namespace: 'my-metric-namespace', + ) do + its('alarm_actions') { should_not be_empty } + end + +
+ +## Matchers + +### exists + +The control will pass if a Cloudwatch Alarm could be found. Use should_not if you expect zero matches. + + # Expect good metric + describe aws_cloudwatch_alarm( + metric: 'good-metric', + metric_namespace: 'my-metric-namespace', + ) do + it { should exist } + end + + # Disallow alarms based on bad-metric + describe aws_cloudwatch_alarm( + metric: 'bed-metric', + metric_namespace: 'my-metric-namespace', + ) do + it { should_not exist } + end + +## Properties + +### alarm_actions + +`alarm_actions` returns a list of strings. Each string is the ARN of an action that will be taken should the alarm be triggered. + + # Ensure that the alarm has at least one action + describe aws_cloudwatch_alarm( + metric: 'bed-metric', + metric_namespace: 'my-metric-namespace', + ) do + its('alarm_actions') { should_not be_empty } + end \ No newline at end of file diff --git a/docs/resources/aws_cloudwatch_log_metric_filter.md b/docs/resources/aws_cloudwatch_log_metric_filter.md new file mode 100644 index 0000000..5fe6c85 --- /dev/null +++ b/docs/resources/aws_cloudwatch_log_metric_filter.md @@ -0,0 +1,130 @@ +--- +title: About the aws_cloudwatch_log_metric_filter Resource +--- + +# aws_cloudwatch_log_metric_filter + +Use the `aws_cloudwatch_log_metric_filter` InSpec audit resource to search for and test properties of individual AWS Cloudwatch Log Metric Filters. + +A Log Metric Filter (LMF) is an AWS resource that observes log traffic, looks for a specified pattern, and updates a metric about the number times the match occurs. The metric can also be connected to AWS Cloudwatch Alarms, so that actions can be taken when a match occurs. + +
+ +## Syntax + +An `aws_cloudwatch_log_metric_filter` resource block searches for an LMF, specified by several search options. If more than one log metric filter matches, an error occurs. + + # Look for a LMF by its filter name and log group name. This combination + # will always either find at most one LMF - no duplicates. + describe aws_cloudwatch_log_metric_filter( + filter_name: 'my-filter', + log_group_name: 'my-log-group' + ) do + it { should exist } + end + + # Search for an LMF by pattern and log group. + # This could result in an error if the results are not unique. + describe aws_cloudwatch_log_metric_filter( + log_group_name: 'my-log-group', + pattern: 'my-filter' + ) do + it { should exist } + end + +
+ +## Resource Parameters + +### filter_name + +This is the identifier of the log metric filter within its log group. To ensure you have a unique result, you must also provide the log_group_name. + + describe aws_cloudwatch_log_metric_filter( + filter_name: 'my-filter' + ) do + it { should exist } + end + +### log_group_name + +The name of the Cloudwatch Log Group that the LMF is watching. Together with filter_name, this uniquely identifies an LMF. + + describe aws_cloudwatch_log_metric_filter( + log_group_name: 'my-log-group', + ) do + it { should exist } + end + +### pattern + +The filter pattern used to match entries from the logs in the log group. + + describe aws_cloudwatch_log_metric_filter( + pattern: '"ERROR" - "Exiting"', + ) do + it { should exist } + end + +## Matchers + +### exist + +Matches (i.e., passes the test) if the resource parameters (search criteria) were able to locate exactly one LMF. + + describe aws_cloudwatch_log_metric_filter( + log_group_name: 'my-log-group', + ) do + it { should exist } + end + +## Properties + +### filter_name + +The name of the LMF within the log_group. + + # Check the name of the LMF that has a certain pattern + describe aws_cloudwatch_log_metric_filter( + log_group_name: 'app-log-group', + pattern: 'KERBLEWIE', + ) do + its('filter_name') { should cmp 'kaboom_lmf' } + end + +### log_group_name + +The name of the log group that the LMF is watching. + + # Check which log group the LMF 'error-watcher' is watching + describe aws_cloudwatch_log_metric_filter( + filter_name: 'error-watcher', + ) do + its('log_group_name') { should cmp 'app-log-group' } + end + +### metric_name, metric_namespace + +The name and namespace of the Cloudwatch Metric that will be updated when the LMF matches. You also need the `metric_namespace` to uniquely identify the metric. + + # Ensure that the LMF has the right metric name + describe aws_cloudwatch_log_metric_filter( + filter_name: 'my-filter', + log_group_name: 'my-log-group', + ) do + its('metric_name') { should cmp 'MyMetric' } + its('metric_namespace') { should cmp 'MyFantasticMetrics' } + end + +### pattern + +The pattern used to match entries from the logs in the log group. + + # Ensure that the LMF is watching for errors + describe aws_cloudwatch_log_metric_filter( + filter_name: 'error-watcher', + log_group_name: 'app-log-group', + ) do + its('pattern') { should cmp 'ERROR' } + end + diff --git a/docs/resources/aws_ec2_instance.md b/docs/resources/aws_ec2_instance.md new file mode 100644 index 0000000..8ef74e6 --- /dev/null +++ b/docs/resources/aws_ec2_instance.md @@ -0,0 +1,99 @@ +--- +title: About the aws_ec2_instance Resource +--- + +# aws_ec2_instance + +Use the `aws_ec2_instance` InSpec audit resource to test properties of a single AWS EC2 instance. + +
+ +## Syntax + +An `aws_ec2_instance` resource block declares the tests for a single AWS EC2 instance by either name or id. + + describe aws_ec2_instance('i-01a2349e94458a507') do + it { should exist } + end + + describe aws_ec2_instance(name: 'my-instance') do + it { should be_running } + end + +
+ +## Examples + +The following examples show how to use this InSpec audit resource. + +### Test that an EC2 instance does not exist + + describe aws_ec2_instance(name: 'dev-server') do + it { should_not exist } + end + +### Test that an EC2 instance is running + + describe aws_ec2_instance(name: 'prod-database') do + it { should be_running } + end + +### Test that an EC2 instance is using the correct image ID + + describe aws_iam_instance(name: 'my-instance') do + its('image_id') { should eq 'ami-27a58d5c' } + end + +### Test that an EC2 instance has the correct tag + + describe aws_ec2_instance('i-090c29e4f4c165b74') do + its('tags') { should include(key: 'Contact', value: 'Gilfoyle') } + end + +
+ +## Matchers + +This InSpec audit resource has the following special matchers. For a full list of available matchers (such as `exist`) please visit our [matchers page](https://www.inspec.io/docs/reference/matchers/). + +### be_pending + +The `be_pending` matcher tests if the described EC2 instance state is `pending`. This indicates that an instance is provisioning. This state should be temporary. + + it { should be_pending } + +### be_running + +The `be_running` matcher tests if the described EC2 instance state is `running`. This indicates the instance is fully operational from AWS's perspective. + + it { should be_running } + +### be_shutting_down + +The `be_shutting_down` matcher tests if the described EC2 instance state is `shutting-down`. This indicates the instance has received a termination command and is in the process of being permanently halted and de-provisioned. This state should be temporary. + + it { should be_shutting_down } + +### be_stopped + +The `be_stopped` matcher tests if the described EC2 instance state is `stopped`. This indicates that the instance is suspended and may be started again. + + it { should be_stopped } + +### be_stopping + +The `be_stopping` matcher tests if the described EC2 instance state is `stopping`. This indicates that an AWS stop command has been issued, which will suspend the instance in an OS-unaware manner. This state should be temporary. + + it { should be_stopping } + +### be_terminated + +The `be_terminated` matcher tests if the described EC2 instance state is `terminated`. This indicates the instance is permanently halted and will be removed from the instance listing in a short period. This state should be temporary. + + it { should be_terminated } + +### be_unknown + +The `be_unknown` matcher tests if the described EC2 instance state is `unknown`. This indicates an error condition in the AWS management system. This state should be temporary. + + it { should be_unknown } diff --git a/docs/resources/aws_ec2_security_group.md b/docs/resources/aws_ec2_security_group.md new file mode 100644 index 0000000..b469191 --- /dev/null +++ b/docs/resources/aws_ec2_security_group.md @@ -0,0 +1,142 @@ +--- +title: About the aws_ec2_security_group Resource +--- + +# aws_ec2_security_group + +Use the `aws_ec2_security_group` InSpec audit resource to test detailed properties of an individual Security Group (SG). + +SGs are a networking construct which contain ingress and egress rules for network communications. SGs may be attached to EC2 instances, as well as certain other AWS resources. Along with Network Access Control Lists, SGs are one of the two main mechanisms of enforcing network-level security. + +
+ +## Syntax + +An `aws_ec2_security_group` resource block uses resource parameters to search for a Security Group, and then tests that Security Group. If no SGs match, no error is raised, but the `exists` matcher will return `false` and all properties will be `nil`. If more than one SG matches (due to vague search parameters), an error is raised. + + # Ensure you have a security group with a certain ID + # This is "safe" - SG IDs are unique within an account + describe aws_ec2_security_group('sg-12345678') do + it { should exist } + end + + # Ensure you have a security group with a certain ID + # This uses hash syntax + describe aws_ec2_security_group(id: 'sg-12345678') do + it { should exist } + end + +
+ +## Examples + +The following examples show how to use this InSpec audit resource. + +As this is the initial release of `aws_ec2_security_group`, its limited functionality precludes examples. + +
+ +## Resource Parameters + +This InSpec resource accepts the following parameters, which are used to search for the Security Group. + +### id, group_id + +The Security Group ID of the Security Group. This is of the format `sg-` followed by 8 hexadecimal characters. The ID is unique within your AWS account; using ID ensures that you will never match more than one SG. The ID is also the default resource parameter, so you may omit the hash syntax. + + # Using Hash syntax + describe aws_ec2_security_group(id: 'sg-12345678') do + it { should exist } + end + + # group_id is an alias for id + describe aws_ec2_security_group(group_id: 'sg-12345678') do + it { should exist } + end + + # Or omit hash syntax, rely on it being the default parameter + describe aws_ec2_security_group('sg-12345678') do + it { should exist } + end + +### group_name + +The string Name of the Security Group. Every VPC has a security group named 'default'. Names are unique within a VPC, but not within an AWS account. + + # Get default security group for a certain VPC + describe aws_ec2_security_group(group_name: 'default', vpc_id: vpc_id: 'vpc-12345678') do + it { should exist } + end + + # This will throw an error if there is a 'backend' SG in more than one VPC. + describe aws_ec2_security_group(group_name: 'backend') do + it { should exist } + end + +### vpc_id + +A string identifying the VPC which contains the security group. Since VPCs commonly contain many SGs, you should add additional parameters to ensure you find exactly one SG. + + # This will error if there is more than the default SG + describe aws_ec2_security_group(vpc_id: 'vpc-12345678') do + it { should exist } + end + +
+ +## Matchers + +### exists + +The control will pass if the specified SG was found. Use should_not if you want to verify that the specified SG does not exist. + + # You will always have at least one SG, the VPC default SG + describe aws_ec2_security_group(group_name: 'default') + it { should exist } + end + + # Make sure we don't have any security groups with the name 'nogood' + describe aws_ec2_security_group(group_name: 'nogood') + it { should_not exist } + end + +## Properties + +### group_id + +Provides the Security Group ID. + + # Inspect the group ID of the default group + describe aws_ec2_security_group(group_name: 'default', vpc_id: vpc_id: 'vpc-12345678') do + its('group_id') { should cmp 'sg-12345678' } + end + + # Store the group ID in a Ruby variable for use elsewhere + sg_id = aws_ec2_security_group(group_name: 'default', vpc_id: vpc_id: 'vpc-12345678').group_id + +### group_name + +A String reflecting the name that was given to the SG at creation time. + + # Inspect the group name of a particular group + describe aws_ec2_security_group('sg-12345678') do + its('group_name') { should cmp 'my_group' } + end + +### description + +A String reflecting the human-meaningful description that was given to the SG at creation time. + + # Require a description of a particular group + describe aws_ec2_security_group('sg-12345678') do + its('description') { should_not be_empty } + end + +### vpc_id + +A String in the format 'vpc-' followed by 8 hexadecimal characters reflecting VPC that contains the security group. + + # Inspec the VPC ID of a particular group + describe aws_ec2_security_group('sg-12345678') do + its('vpc_id') { should cmp 'vpc-12345678' } + end \ No newline at end of file diff --git a/docs/resources/aws_ec2_security_groups.md b/docs/resources/aws_ec2_security_groups.md new file mode 100644 index 0000000..efddd8f --- /dev/null +++ b/docs/resources/aws_ec2_security_groups.md @@ -0,0 +1,81 @@ +--- +title: About the aws_ec2_security_groups Resource +--- + +# aws_ec2_security_groups + +Use the `aws_ec2_security_groups` InSpec audit resource to test properties of some or all security groups. + +Security groups are a networking construct which contain ingress and egress rules for network communications. Security groups may be attached to EC2 instances, as well as certain other AWS resources. Along with Network Access Control Lists, Security Groups are one of the two main mechanisms of enforcing network-level security. + +
+ +## Syntax + +An `aws_ec2_security_groups` resource block uses an optional filter to select a group of security groups and then tests that group. + + # Verify you have more than the default security group + describe aws_ec2_security_groups do + its('entries.count') { should be > 1 } + end + +
+ +## Examples + +The following examples show how to use this InSpec audit resource. + +As this is the initial release of `aws_ec2_security_groups`, its limited functionality precludes examples. + +
+ +## Matchers + +### exists + +The control will pass if the filter returns at least one result. Use should_not if you expect zero matches. + + # You will always have at least one SG, the VPC default SG + describe aws_ec2_security_groups + it { should exist } + end + +## Filter Criteria + +### vpc_id + +A string identifying the VPC which contains the security group. + + # Look for a particular security group in just one VPC + describe aws_ec2_security_groups.where( vpc_id: 'vpc-12345678') do + its('group_ids') { should include('sg-abcdef12')} + end + +### group_name + +A string identifying a group. Since groups are contained in VPCs, group names are unique within the AWS account, but not across VPCs. + + # Examine the default security group in all VPCs + describe aws_ec2_security_groups.where( group_name: 'default') do + it { should exist } + end + + +## Properties + +### group_ids + +Provides a list of all security group IDs matched. + + describe aws_ec2_security_groups do + its('group_ids') { should include('sg-12345678') } + end + +### entries + +Provides access to the raw results of the query. This can be useful for checking counts and other advanced operations. + + # Allow at most 100 security groups on the account + describe aws_ec2_security_groups do + its('entries.count') { should be <= 100} + end diff --git a/docs/resources/aws_iam_access_key.md b/docs/resources/aws_iam_access_key.md new file mode 100644 index 0000000..dfb784c --- /dev/null +++ b/docs/resources/aws_iam_access_key.md @@ -0,0 +1,56 @@ +--- +title: About the aws_iam_access_key Resource +--- + +# aws_iam_access_key + +Use the `aws_iam_access_key` InSpec audit resource to test properties of a single AWS IAM access key. + +
+ +## Syntax + +An `aws_iam_access_key` resource block declares the tests for a single AWS IAM access key by username and id. + + describe aws_iam_access_key(username: 'username', id: 'access-key-id') do + it { should exist } + it { should_not be_active } + its('create_date') { should be > Time.now - 365 * 86400 } + its('last_used_date') { should be > Time.now - 90 * 86400 } + end + +
+ +## Examples + +The following examples show how to use this InSpec audit resource. + +### Test that an IAM access key is not active + + describe aws_iam_access_key(username: 'username', id: 'access-key-id') do + it { should_not be_active } + end + +### Test that an IAM access key is older than one year + + describe aws_iam_access_key(username: 'username', id: 'access-key-id') do + its('create_date') { should be > Time.now - 365 * 86400 } + end + +### Test that an IAM access key has been used in the past 90 days + + describe aws_iam_access_key(username: 'username', id: 'access-key-id') do + its('last_used_date') { should be > Time.now - 90 * 86400 } + end + +
+ +## Matchers + +This InSpec audit resource has the following special matchers. For a full list of available matchers (such as `exist`) please visit our [matchers page](https://www.inspec.io/docs/reference/matchers/). + +### be_active + +The `be_active` matcher tests if the described IAM access key is active. + + it { should be_active } diff --git a/docs/resources/aws_iam_access_keys.md b/docs/resources/aws_iam_access_keys.md new file mode 100644 index 0000000..c5c9487 --- /dev/null +++ b/docs/resources/aws_iam_access_keys.md @@ -0,0 +1,183 @@ +--- +title: About the aws_iam_access_keys Resource +--- + +# aws_iam_access_keys + +Use the `aws_iam_access_keys` InSpec audit resource to test properties of some or all IAM Access Keys. + +To test properties of a single Access Key, use the `aws_iam_access_key` resource instead. +To test properties of an individual user's access keys, use the `aws_iam_user` resource. + +Access Keys are closely related to AWS User resources. Use this resource to perform audits of all keys or of keys specified by criteria unrelated to any particular user. + +
+ +## Syntax + +An `aws_iam_access_keys` resource block uses an optional filter to select a group of access keys and then tests that group. + + # Do not allow any access keys + describe aws_iam_access_keys do + it { should_not exist } + end + + # Don't let fred have access keys, using filter argument syntax + describe aws_iam_access_keys.where(username: 'fred') do + it { should_not exist } + end + + # Don't let fred have access keys, using filter block syntax (most flexible) + describe aws_iam_access_keys.where { username == 'fred' } do + it { should_not exist } + end + +
+ +## Examples + +The following examples show how to use this InSpec audit resource. + +### Disallow access keys created more than 90 days ago + + describe aws_iam_access_keys.where { created_age > 90 } do + it { should_not exist } + end + +
+ +## Matchers + +### exists + +The control will pass if the filter returns at least one result. Use should_not if you expect zero matches. + + # Sally should have at least one access key + describe aws_iam_access_keys.where(username: 'sally') do + it { should exist } + end + + # Don't let fred have access keys + describe aws_iam_access_keys.where(username: 'fred') do + it { should_not exist } + end + +## Filter Criteria + +### active + +A true / false value indicating if an Access Key is currently "Active" (the normal state) in the AWS console. See also: `inactive`. + + # Check whether a particular key is enabled + describe aws_iam_access_keys.where { active } do + its('access_key_ids') { should include('AKIA1234567890ABCDEF')} + end + +### created_date + +A DateTime identifying when the Access Key was created. See also `created_days_ago` and `created_hours_ago`. + + # Detect keys older than 2017 + describe aws_iam_access_keys.where { created_date < DateTime.parse('2017-01-01') } do + it { should_not exist } + end + +### created_days_ago, created_hours_ago + +An integer, representing how old the access key is. + + # Don't allow keys that are older than 90 days + describe aws_iam_access_keys.where { created_days_ago > 90 } do + it { should_not exist } + end + +### created_with_user + +A true / false value indicating if the Access Key was likely created at the same time as the user, by checking if the difference between created_date and user_created_date is less than 1 hour. + + # Do not automatically create keys for users + describe aws_iam_access_keys.where { created_with_user } do + it { should_not exist } + end + +### ever_used + +A true / false value indicating if the Access Key has ever been used, based on the last_used_date. See also: `never_used`. + + # Check to see if a particular key has ever been used + describe aws_iam_access_keys.where { ever_used } do + its('access_key_ids') { should include('AKIA1234567890ABCDEF')} + end + + +### inactive + +A true / false value indicating if the Access Key has been marked Inactive in the AWS console. See also: `active`. + + # Don't leave inactive keys laying around + describe aws_iam_access_keys.where { inactive } do + it { should_not exist } + end + +### last_used_date + +A DateTime identifying when the Access Key was last used. Returns nil if the key has never been used. See also: `ever_used`, `last_used_days_ago`, `last_used_hours_ago`, and `never_used`. + + # No one should do anything on Mondays + describe aws_iam_access_keys.where { ever_used and last_used_date.monday? } do + it { should_not exist } + end + +### last_used_days_ago, last_used_hours_ago + +An integer representing when the key was last used. See also: `ever_used`, `last_used_date`, and `never_used`. + + # Don't allow keys that sit unused for more than 90 days + describe aws_iam_access_keys.where { last_used_days_ago > 90 } do + it { should_not exist } + end + +### never_used + +A true / false value indicating if the Access Key has never been used, based on the last_used_date. See also: `ever_used`. + + # Don't allow unused keys to lay around + describe aws_iam_access_keys.where { never_used } do + it { should_not exist } + end + +### username + +Searches for access keys owned by the named user. Each user may have zero, one, or two access keys. + + describe aws_iam_access_keys(username: 'bob') do + it { should exist } + end + +### user_created_date + +The date at which the user was created. + + # Users have to be a week old to have a key + describe aws_iam_access_keys.where { user_created_date > Date.now - 7 } + it { should_not exist } + end + +## Properties + +### access_key_ids + +Provides a list of all access key IDs matched. + + describe aws_iam_access_keys do + its('access_key_ids') { should include('AKIA1234567890ABCDEF') } + end + +### entries + +Provides access to the raw results of the query. This can be useful for checking counts and other advanced operations. + + # Allow at most 100 access keys on the account + describe aws_iam_access_keys do + its('entries.count') { should be <= 100} + end diff --git a/docs/resources/aws_iam_password_policy.md b/docs/resources/aws_iam_password_policy.md new file mode 100644 index 0000000..57050ea --- /dev/null +++ b/docs/resources/aws_iam_password_policy.md @@ -0,0 +1,69 @@ +--- +title: About the aws_iam_password_policy Resource +--- + +# aws_iam_password_policy + +Use the `aws_iam_password_policy` InSpec audit resource to test properties of the AWS IAM Password Policy. + +
+ +## Syntax + +An `aws_iam_password_policy` resource block takes no parameters, but uses several matchers. + + describe aws_iam_password_policy do + its('requires_lowercase_characters?') { should be true } + end + +
+ +## Examples + +The following examples show how to use this InSpec audit resource. + +### Test that the IAM Password Policy requires lowercase characters, uppercase characters, numbers, symbols, and a minimum length greater than eight + + describe aws_iam_password_policy do + its('requires_lowercase_characters?') { should be true } + its('requires_uppercase_characters?') { should be true } + its('requires_numbers?') { should be true } + its('requires_symbols?') { should be true } + its('minimum_password_length') { should be > 8 } + end + +### Test that the IAM Password Policy allows users to change their password + + describe aws_iam_password_policy do + its('allows_user_to_change_password?') { should be true } + end + +### Test that the IAM Password Policy expires passwords + + describe aws_iam_password_policy do + its('expires_passwords?') { should be true } + end + +### Test that the IAM Password Policy has a max password age + + describe aws_iam_password_policy do + its('max_password_age') { should be > 90 * 86400 } + end + +### Test that the IAM Password Policy prevents password reuse + + describe aws_iam_password_policy do + its('prevents_password_reuse?') { should be true } + end + +### Test that the IAM Password Policy requires users to remember 3 previous passwords + + describe aws_iam_password_policy do + its('number_of_passwords_to_remember') { should eq 3 } + end + +
+ +## Matchers + +For a full list of available matchers (such as `exist`) please visit our [matchers page](https://www.inspec.io/docs/reference/matchers/). diff --git a/docs/resources/aws_iam_role.md b/docs/resources/aws_iam_role.md new file mode 100644 index 0000000..57756d4 --- /dev/null +++ b/docs/resources/aws_iam_role.md @@ -0,0 +1,54 @@ +--- +title: About the aws_iam_role Resource +--- + +# aws_iam_role + +Use the `aws_iam_role` InSpec audit resource to test properties of a single IAM Role. A Role is a collection of permissions that may be temporarily assumed by a user, EC2 Instance, Lambda Function, or certain other resources. + +
+ +## Syntax + + # Ensure that a certain role exists by name + describe aws_iam_role('my-role') do + it { should exist } + end + +## Resource Parameters + +### role_name + +This resource expects a single parameter that uniquely identifes the IAM Role, the Role Name. You may pass it as a string, or as the value in a hash: + + describe aws_iam_role('my-role') do + it { should exist } + end + # Same + describe aws_iam_role(role_name: 'my-role') do + it { should exist } + end + +## Matchers + +### exist + +Indicates that the Role Name provided was found. Use should_not to test for IAM Roles that should not exist. + + describe aws_iam_role('should-be-there') do + it { should exist } + end + + describe aws_iam_role('should-not-be-there') do + it { should_not exist } + end + +## Properties + +### description + +A textual description of the IAM Role. + + describe aws_iam_role('my-role') do + its('description') { should be('Our most important Role')} + end diff --git a/docs/resources/aws_iam_root_user.md b/docs/resources/aws_iam_root_user.md new file mode 100644 index 0000000..2b31b15 --- /dev/null +++ b/docs/resources/aws_iam_root_user.md @@ -0,0 +1,57 @@ +--- +title: About the aws_iam_root_user Resource +--- + +# aws_iam_root_user + +Use the `aws_iam_root_user` InSpec audit resource to test properties of the root user (owner of the account). + +To test properties of all or multiple users, use the `aws_iam_users` resource. + +To test properties of a specific AWS user use the `aws_iam_user` resource. + +
+ +## Syntax + +An `aws_iam_root_user` resource block requires no parameters but has several matchers + + describe aws_iam_root_user do + its { should have_mfa_enabled } + end + +
+ +## Examples + +The following examples show how to use this InSpec audit resource. + +### Test that the AWS root account has at-least one access key + + describe aws_iam_root_user do + it { should have_access_key } + end + +### Test that the AWS root account has Multi-Factor Authentication enabled + + describe aws_iam_root_user do + it { should have_mfa_enabled } + end + +
+ +## Matchers + +This InSpec audit resource has the following special matchers. For a full list of available matchers (such as `exist`) please visit our [matchers page](https://www.inspec.io/docs/reference/matchers/). + +### have_mfa_enabled + +The `have_mfa_enabled` matcher tests if the AWS root user has Multi-Factor Authentication enabled, requiring them to enter a secondary code when they login to the web console. + + it { should have_mfa_enabled } + +### have_access_key + +The `have_access_key` matcher tests if the AWS root user has at least one access key. + + it { should have_access_key } diff --git a/docs/resources/aws_iam_user.md b/docs/resources/aws_iam_user.md new file mode 100644 index 0000000..16319b6 --- /dev/null +++ b/docs/resources/aws_iam_user.md @@ -0,0 +1,63 @@ +--- +title: About the aws_iam_user Resource +--- + +# aws_iam_user + +Use the `aws_iam_user` InSpec audit resource to test properties of a single AWS IAM user. + +To test properties of all or multiple users, use the `aws_iam_users` resource. + +To test properties of the special AWS root user (which owns the account), use the `aws_iam_root_user` resource. + +
+ +## Syntax + +An `aws_iam_user` resource block declares a user by name, and then lists tests to be performed. + + describe aws_iam_user(name: 'test_user') do + it { should exist } + end + +
+ +## Examples + +The following examples show how to use this InSpec audit resource. + +### Test that a user does not exist + + describe aws_iam_user(name: 'gone') do + it { should_not exist } + end + +### Test that a user has multi-factor authentication enabled + + describe aws_iam_user(name: 'test_user') do + it { should have_mfa_enabled } + end + +### Test that a service user does not have a password + + describe aws_iam_user(name: 'test_user') do + it { should have_console_password } + end + +
+ +## Matchers + +This InSpec audit resource has the following special matchers. For a full list of available matchers (such as `exist`) please visit our [matchers page](https://www.inspec.io/docs/reference/matchers/). + +### have_console_password + +The `have_console_password` matcher tests if the user has a password that could be used to log into the AWS web console. + + it { should have_console_password } + +### have_mfa_enabled + +The `have_mfa_enabled` matcher tests if the user has Multi-Factor Authentication enabled, requiring them to enter a secondary code when they login to the web console. + + it { should have_mfa_enabled } diff --git a/docs/resources/aws_iam_users.md b/docs/resources/aws_iam_users.md new file mode 100644 index 0000000..1288842 --- /dev/null +++ b/docs/resources/aws_iam_users.md @@ -0,0 +1,55 @@ +--- +title: About the aws_iam_users Resource +--- + +# aws_iam_users + +Use the `aws_iam_users` InSpec audit resource to test properties of a all or multiple users. + +To test properties of a single user, use the `aws_iam_user` resource. + +To test properties of the special AWS root user (which owns the account), use the `aws_iam_root_user` resource. + +
+ +## Syntax + +An `aws_iam_users` resource block users a filter to select a group of users and then tests that group + + describe aws_iam_users.where(has_mfa_enabled?: false) do + it { should_not exist } + end + +
+ +## Examples + +The following examples show how to use this InSpec audit resource. + +### Test that all users have Multi-Factor Authentication enabled + + describe aws_iam_users.where(has_mfa_enabled?: false) do + it { should_not exist } + end + +### Test that at least one user has a console password to log into the AWS web console + + describe aws_iam_users.where(has_console_password?: true) do + it { should exist } + end + +### Test that all users that have a console password have Multi-Factor Authentication enabled + + console_users_without_mfa = aws_iam_users + .where(has_console_password?: true) + .where(has_mfa_enabled?: false) + + describe console_users_without_mfa do + it { should_not exist } + end + +
+ +## Matchers + +This InSpec audit resource has no specific matchers. \ No newline at end of file diff --git a/docs/resources/aws_s3_bucket.md b/docs/resources/aws_s3_bucket.md new file mode 100644 index 0000000..5f43e4e --- /dev/null +++ b/docs/resources/aws_s3_bucket.md @@ -0,0 +1,123 @@ +--- +title: About the aws_s3_bucket Resource +--- + +# aws_s3_bucket + +Use the `aws_s3_bucket` InSpec audit resource to test properties of a single AWS bucket. + +To test properties of a multiple S3 buckets, use the `aws_s3_buckets` resource. + +
+ +## Limitations + +S3 bucket security is a complex matter. For details on how AWS evaluates requests for access, please see [the AWS documentation](https://docs.aws.amazon.com/AmazonS3/latest/dev/how-s3-evaluates-access-control.html). S3 buckets and the objects they contain support three different types of access control: bucket ACLs, bucket policies, and object ACLs. + +As of January 2018, this resource supports evaluating bucket ACLs and bucket policies. We do not support evaluating object ACLs because it introduces scalability concerns in the AWS API; we recommend using AWS mechanisms such as CloudTrail and Config to detect insecure object ACLs. + +In particular, users of the `be_public` matcher should carefully examine the conditions under which the matcher will detect an insecure bucket. See the `be_public` section under the Matchers section below. + +## Syntax + +An `aws_s3_bucket` resource block declares a bucket by name, and then lists tests to be performed. + + describe aws_s3_bucket(bucket_name: 'test_bucket') do + it { should exist } + it { should_not be_public } + end + + describe aws_s3_bucket('test_bucket') do + it { should exist } + end + +
+ +## Examples + +The following examples show how to use this InSpec audit resource. + +### Test a bucket's bucket-level ACL + + describe aws_s3_bucket('test_bucket') do + its('bucket_acl.count') { should eq 1 } + end + +### Check to see if a bucket has a bucket policy + + describe aws_s3_bucket('test_bucket') do + its('bucket_policy') { should be_empty } + end + +### Check to see if a bucket appears to be exposed to the public + + # See Limitations section above + describe aws_s3_bucket('test_bucket') do + it { should_not be_public } + end +
+ +## Supported Properties + +### region + +The `region` property identifies the AWS Region in which the S3 bucket is located. + + describe aws_s3_bucket('test_bucket') do + # Check if the correct region is set + its('region') { should eq 'us-east-1' } + end + +## Unsupported Properties + +### bucket_acl + +The `bucket_acl` property is a low-level property that lists the individual Bucket ACL grants that are in effect on the bucket. Other higher-level properties, such as be\_public, are more concise and easier to use. You can use the `bucket_acl` property to investigate which grants are in effect, causing be\_public to fail. + +The value of bucket_acl is an Array of simple objects. Each object has a `permission` property and a `grantee` property. The `permission` property will be a string such as 'READ', 'WRITE' etc (See the [AWS documentation](https://docs.aws.amazon.com/sdkforruby/api/Aws/S3/Client.html#get_bucket_acl-instance_method) for a full list). The `grantee` property contains sub-properties, such as `type` and `uri`. + + + bucket_acl = aws_s3_bucket('my-bucket') + + # Look for grants to "AllUsers" (that is, the public) + all_users_grants = bucket_acl.select do |g| + g.grantee.type == 'Group' && g.grantee.uri =~ /AllUsers/ + end + + # Look for grants to "AuthenticatedUsers" (that is, any authenticated AWS user - nearly public) + auth_grants = bucket_acl.select do |g| + g.grantee.type == 'Group' && g.grantee.uri =~ /AuthenticatedUsers/ + end + +### bucket_policy + +The `bucket_policy` is a low-level property that describes the IAM policy document controlling access to the bucket. The `bucket_policy` property returns a Ruby structure that you can probe to check for particular statements. We recommend using a higher-level property, such as `be_public`, which is concise and easier to implement in your policy files. + +The `bucket_policy` property returns an Array of simple objects, each object being an IAM Policy Statement. See the [AWS documentation](https://docs.aws.amazon.com/AmazonS3/latest/dev/example-bucket-policies.html#example-bucket-policies-use-case-2) for details about the structure of this data. + +If there is no bucket policy, this property will return an empty Array. + + bucket_policy = aws_s3_bucket('my-bucket') + + # Look for statements that allow the general public to do things + # This may be a false positive; it's possible these statements + # could be protected by conditions, such as IP restrictions. + public_statements = bucket_policy.select do |s| + s.effect == 'Allow' && s.principal == '*' + end + +## Matchers + +This InSpec audit resource has the following special matchers. For a full list of available matchers (such as `exist`) please visit our [matchers page](https://www.inspec.io/docs/reference/matchers/). + +### be_public + +The `be_public` matcher tests if the bucket has potentially insecure access controls. This high-level matcher detects several insecure conditions, which may be enhanced in the future. Currently, the matcher reports an insecure bucket if any of the following conditions are met: + + 1. A bucket ACL grant exists for the 'AllUsers' group + 2. A bucket ACL grant exists for the 'AuthenticatedUsers' group + 3. A bucket policy has an effect 'Allow' and principal '*' + +Note: This resource does not detect insecure object ACLs. + + it { should_not be_public } diff --git a/docs/resources/aws_sns_topic.md b/docs/resources/aws_sns_topic.md new file mode 100644 index 0000000..1196056 --- /dev/null +++ b/docs/resources/aws_sns_topic.md @@ -0,0 +1,58 @@ +--- +title: About the aws_sns_topic Resource +--- + +# aws_sns_topic + +Use the `aws_sns_topic` InSpec audit resource to test properties of a single AWS Simple Notification Service Topic. SNS topics are channels for related events. AWS resources will place events in the SNS topic, while other AWS resources will _subscribe_ to receive notifications when new events have appeared. + +
+ +## Syntax + + # Ensure that a topic exists and has at least one subscription + describe aws_sns_topic('arn:aws:sns:*::my-topic-name') do + it { should exist } + its('confirmed_subscription_count') { should_not be_zero } + end + + # You may also use has syntax to pass the ARN + describe aws_sns_topic(arn: 'arn:aws:sns:*::my-topic-name') do + it { should exist } + end + + +## Resource Parameters + +### ARN + +This resource expects a single parameter that uniquely identifes the SNS Topic, an ARN. Amazon Resource Names for SNS topics have the format `arn:aws:sns:region:account-id:topicname`. AWS requires a fully-specified ARN for looking up an SNS topic. The account ID and region are required. Wildcards are not permitted. + +See also the (AWS documentation on ARNs)[http://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html]. + +## Matchers + +### exist + +Indicates that the ARN provided was found. Use should_not to test for SNS topics that should not exist. + + # Expect good news + describe aws_sns_topic('arn:aws:sns:*::good-news') do + it { should exist } + end + + # No bad news allowed + describe aws_sns_topic('arn:aws:sns:*::bad-news') do + it { should_not exist } + end + +## Properties + +### confirmed_subscription_count + +An integer indicating the number of currently active subscriptions. + + # Make sure someone is listening + describe aws_sns_topic('arn:aws:sns:*::my-topic-name') do + its('confirmed_subscription_count') { should_not be_zero} + end diff --git a/docs/resources/aws_vpc.md b/docs/resources/aws_vpc.md new file mode 100644 index 0000000..7edd8ef --- /dev/null +++ b/docs/resources/aws_vpc.md @@ -0,0 +1,110 @@ +--- +title: About the aws_vpc Resource +--- + +# aws_vpc + +Use the `aws_vpc` InSpec audit resource to test properties of a single AWS Virtual Private Cloud (VPC). + +To test properties of all or multiple VPCs, use the `aws_vpcs` resource. + +A VPC is a networking construct that provides an isolated environment. A VPC is contained in a geographic region, but spans availability zones in that region. Within a VPC, you may have multiple subnets, internet gateways, and other networking resources. Computing resources such as EC2 instances reside on subnets within the VPC. + +Each VPC is uniquely identified by its VPC ID. In addition, each VPC has a non-unique CIDR IP Address range (such as 10.0.0.0/16) which it manages. + +Every AWS account has at least one VPC, the "default" VPC, in every region. + +
+ +## Syntax + +An `aws_vpc` resource block identifies a VPC by id. If no VPC ID is provided, the default VPC is used. + + # Find the default VPC + describe aws_vpc do + it { should exist } + end + + # Find a VPC by ID + describe aws_vpc('vpc-12345678') do + it { should exist } + end + + # Hash syntax for ID + describe aws_vpc(vpc_id: 'vpc-12345678') do + it { should exist } + end + +
+ +## Examples + +The following examples show how to use this InSpec audit resource. + +### Test that a VPC does not exist + + describe aws_vpc('vpc-87654321') do + it { should_not exist } + end + +### Test the CIDR of a named VPC + + describe aws_vpc('vpc-87654321') do + its('cidr_block') { should cmp '10.0.0.0/16' } + end + +
+ +## Matchers + +This InSpec audit resource has the following special matchers. For a full list of available matchers (such as `exist`) please visit our [matchers page](https://www.inspec.io/docs/reference/matchers/). + +### be_default + +The test will pass if the identified VPC is the default VPC for the region. + + describe aws_vpc('vpc-87654321') do + it { should be_default } + end + +## Properties + +### cidr_block + +The IPv4 address range that is managed by the VPC. + + describe aws_vpc('vpc-87654321') do + its('cidr_block') { should cmp '10.0.0.0/16' } + end + +### dhcp\_options\_id + +The ID of the set of DHCP options you've associated with the VPC (or `default` if the default options are associated with the VPC). + + describe aws_vpc do + its ('dhcp_options_id') { should eq 'dopt-a94671d0' } + end + +### state + +The state of the VPC (`pending` | `available`). + + describe aws_vpc do + its ('state') { should eq 'available' } + end + +### vpc_id + +The ID of the VPC. + + describe aws_vpc do + its('vpc_id') { should eq 'vpc-87654321' } + end + +### instance_tenancy + +The allowed tenancy of instances launched into the VPC. + + describe aws_vpc do + its ('instance_tenancy') { should eq 'default' } + end diff --git a/docs/resources/aws_vpcs.md b/docs/resources/aws_vpcs.md new file mode 100644 index 0000000..a835371 --- /dev/null +++ b/docs/resources/aws_vpcs.md @@ -0,0 +1,45 @@ +--- +title: About the aws_vpcs Resource +--- + +# aws_vpcs + +Use the `aws_vpcs` InSpec audit resource to test properties of some or all AWS Virtual Private Clouds (VPCs). + +A VPC is a networking construct that provides an isolated environment. A VPC is contained in a geographic region, but spans availability zones in that region. Within a VPC, you may have multiple subnets, internet gateways, and other networking resources. Computing resources such as EC2 instances reside on subnets within the VPC. + +Each VPC is uniquely identified by its VPC ID. In addition, each VPC has a non-unique CIDR IP Address range (such as 10.0.0.0/16) which it manages. + +Every AWS account has at least one VPC, the "default" VPC, in every region. + +
+ +## Syntax + +An `aws_vpcs` resource block uses an optional filter to select a group of VPCs and then tests that group. + + # The control will pass if the filter returns at least one result. Use should_not if you expect zero matches. + describe aws_vpcs do + it { should exist } + end + +
+ +## Examples + +The following examples show how to use this InSpec audit resource. + +As this is the initial release of `aws_vpcs`, its limited functionality precludes examples. + +
+ +## Matchers + +### exists + +The control will pass if the filter returns at least one result. Use should_not if you expect zero matches. + + # You will always have at least one VPC + describe aws_vpcs + it { should exist } + end diff --git a/inspec.yml b/inspec.yml new file mode 100644 index 0000000..afc622c --- /dev/null +++ b/inspec.yml @@ -0,0 +1,7 @@ +name: inspec-aws +title: InSpec AWS Resource Pack +maintainer: Chef Software Inc. +copyright: chris@lollyrock.com +copyright_email: chris@lollyrock.com +license: Apache 2 license +version: 1.0.0 diff --git a/libraries/_aws.rb b/libraries/_aws.rb new file mode 100644 index 0000000..5adece0 --- /dev/null +++ b/libraries/_aws.rb @@ -0,0 +1,7 @@ +# Main AWS loader file. The intent is for this to be +# loaded only if AWS resources are needed. + +require 'aws-sdk' # TODO: split once ADK v3 is in use +require '_aws_backend_factory_mixin' +require '_aws_resource_mixin' +require '_aws_connection' diff --git a/libraries/_aws_backend_factory_mixin.rb b/libraries/_aws_backend_factory_mixin.rb new file mode 100644 index 0000000..67191af --- /dev/null +++ b/libraries/_aws_backend_factory_mixin.rb @@ -0,0 +1,12 @@ +# Intended to be pulled in via extend, not include +module AwsBackendFactoryMixin + def create + @selected_backend.new + end + + def select(klass) + @selected_backend = klass + end + + alias set_default_backend select +end diff --git a/libraries/_aws_connection.rb b/libraries/_aws_connection.rb new file mode 100644 index 0000000..06c3b30 --- /dev/null +++ b/libraries/_aws_connection.rb @@ -0,0 +1,63 @@ +# author: Christoph Hartmann + +# This class exists so that we can intercept AWS API connection setup +# and have an opportunity to provide credentials from another mechanism +# (such as a train transport URI) in the future. +# +# We commit to always supporting the standard AWS environment variables. + +class AWSConnection + def initialize + creds = nil + if ENV['AWS_PROFILE'] + creds = Aws::SharedCredentials.new(profile_name: ENV['AWS_PROFILE']) + else + creds = Aws::Credentials.new( + ENV['AWS_ACCESS_KEY_ID'], + ENV['AWS_SECRET_ACCESS_KEY'], + ENV['AWS_SESSION_TOKEN'], + ) + end + opts = { + region: ENV['AWS_REGION'] || ENV['AWS_DEFAULT_REGION'], + credentials: creds, + } + Aws.config.update(opts) + end + + def sns_client + @sns_client ||= Aws::SNS::Client.new + end + + def cloudwatch_client + @cloudwatch_client ||= Aws::CloudWatch::Client.new + end + + def cloudwatch_logs_client + @cloudwatch_logs_client ||= Aws::CloudWatchLogs::Client.new + end + + def cloudtrail_client + @cloudtrail_client ||= Aws::CloudTrail::Client.new + end + + def ec2_resource + @ec2_resource ||= Aws::EC2::Resource.new + end + + def ec2_client + @ec2_client ||= Aws::EC2::Client.new + end + + def iam_resource + @iam_resource ||= Aws::IAM::Resource.new + end + + def iam_client + @iam_client ||= Aws::IAM::Client.new + end + + def s3_client + @s3_client ||= Aws::S3::Client.new + end +end diff --git a/libraries/_aws_resource_mixin.rb b/libraries/_aws_resource_mixin.rb new file mode 100644 index 0000000..6b0a117 --- /dev/null +++ b/libraries/_aws_resource_mixin.rb @@ -0,0 +1,52 @@ +module AwsResourceMixin + def initialize(resource_params = {}) + validate_params(resource_params).each do |param, value| + instance_variable_set(:"@#{param}", value) + end + fetch_from_aws + end + + def check_resource_param_names(raw_params: {}, allowed_params: [], allowed_scalar_name: nil, allowed_scalar_type: nil) + # Some resources allow passing in a single ID value. Check and convert to hash if so. + if allowed_scalar_name && !raw_params.is_a?(Hash) + value_seen = raw_params + if value_seen.is_a?(allowed_scalar_type) + raw_params = { allowed_scalar_name => value_seen } + else + raise ArgumentError, 'If you pass a single value to the resource, it must ' \ + "be a #{allowed_scalar_type}, not an #{value_seen.class}." + end + end + + # Remove all expected params from the raw param hash + recognized_params = {} + allowed_params.each do |expected_param| + recognized_params[expected_param] = raw_params.delete(expected_param) if raw_params.key?(expected_param) + end + + # Any leftovers are unwelcome + unless raw_params.empty? + raise ArgumentError, "Unrecognized resource param '#{raw_params.keys.first}'. Expected parameters: #{allowed_params.join(', ')}" + end + + recognized_params + end + + def exists? + @exists + end + + # This sets up a class, AwsSomeResource::BackendFactory, that + # provides a mechanism to create and use backends without + # having to know which is selected. This is mainly used for + # unit testing. + def self.included(base) + # Create a new class, whose body is simply to extend the + # backend factory mixin + resource_backend_factory_class = Class.new(Object) do + extend AwsBackendFactoryMixin + end + # Name that class + base.const_set('BackendFactory', resource_backend_factory_class) + end +end diff --git a/libraries/aws_aaa_shim.rb b/libraries/aws_aaa_shim.rb new file mode 100644 index 0000000..685c9f0 --- /dev/null +++ b/libraries/aws_aaa_shim.rb @@ -0,0 +1,3 @@ +# This file simply acts as a loader when inspec-aws +# is being used as a resource pack. +require '_aws' diff --git a/libraries/aws_cloudtrail_trail.rb b/libraries/aws_cloudtrail_trail.rb new file mode 100644 index 0000000..fc9b2e0 --- /dev/null +++ b/libraries/aws_cloudtrail_trail.rb @@ -0,0 +1,74 @@ +class AwsCloudTrailTrail < Inspec.resource(1) + name 'aws_cloudtrail_trail' + desc 'Verifies settings for an individual AWS CloudTrail Trail' + example " + describe aws_cloudtrail_trail('trail-name') do + it { should exist } + end + " + + include AwsResourceMixin + attr_reader :s3_bucket_name, :trail_arn, :cloud_watch_logs_role_arn, + :cloud_watch_logs_log_group_arn, :kms_key_id, :home_region + + def to_s + "CloudTrail #{@trail_name}" + end + + def multi_region_trail? + @is_multi_region_trail + end + + def log_file_validation_enabled? + @log_file_validation_enabled + end + + def encrypted? + !kms_key_id.nil? + end + + private + + def validate_params(raw_params) + validated_params = check_resource_param_names( + raw_params: raw_params, + allowed_params: [:trail_name], + allowed_scalar_name: :trail_name, + allowed_scalar_type: String, + ) + + if validated_params.empty? + raise ArgumentError, "You must provide the parameter 'trail_name' to aws_cloudtrail_trail." + end + + validated_params + end + + def fetch_from_aws + backend = AwsCloudTrailTrail::BackendFactory.create + + query = { trail_name_list: [@trail_name] } + resp = backend.describe_trails(query) + + @trail = resp.trail_list[0].to_h + @exists = !@trail.empty? + @s3_bucket_name = @trail[:s3_bucket_name] + @is_multi_region_trail = @trail[:is_multi_region_trail] + @trail_arn = @trail[:trail_arn] + @log_file_validation_enabled = @trail[:log_file_validation_enabled] + @cloud_watch_logs_role_arn = @trail[:cloud_watch_logs_role_arn] + @cloud_watch_logs_log_group_arn = @trail[:cloud_watch_logs_log_group_arn] + @kms_key_id = @trail[:kms_key_id] + @home_region = @trail[:home_region] + end + + class Backend + class AwsClientApi + BackendFactory.set_default_backend(self) + + def describe_trails(query) + AWSConnection.new.cloudtrail_client.describe_trails(query) + end + end + end +end diff --git a/libraries/aws_cloudtrail_trails.rb b/libraries/aws_cloudtrail_trails.rb new file mode 100644 index 0000000..97cb9cb --- /dev/null +++ b/libraries/aws_cloudtrail_trails.rb @@ -0,0 +1,44 @@ +class AwsCloudTrailTrails < Inspec.resource(1) + name 'aws_cloudtrail_trails' + desc 'Verifies settings for AWS CloudTrail Trails in bulk' + example ' + describe aws_cloudtrail_trails do + it { should exist } + end + ' + + # Underlying FilterTable implementation. + filter = FilterTable.create + filter.add_accessor(:entries) + .add(:exists?) { |x| !x.entries.empty? } + .add(:names, field: :name) + .add(:trail_arns, field: :trail_arn) + filter.connect(self, :trail_data) + + def trail_data + @table + end + + def to_s + 'CloudTrail Trails' + end + + def initialize + backend = AwsCloudTrailTrails::BackendFactory.create + @table = backend.describe_trails({}).to_h[:trail_list] + end + + class BackendFactory + extend AwsBackendFactoryMixin + end + + class Backend + class AwsClientApi + BackendFactory.set_default_backend(self) + + def describe_trails(query) + AWSConnection.new.cloudtrail_client.describe_trails(query) + end + end + end +end diff --git a/libraries/aws_cloudwatch_alarm.rb b/libraries/aws_cloudwatch_alarm.rb new file mode 100644 index 0000000..3607107 --- /dev/null +++ b/libraries/aws_cloudwatch_alarm.rb @@ -0,0 +1,61 @@ +require '_aws' + +class AwsCloudwatchAlarm < Inspec.resource(1) + name 'aws_cloudwatch_alarm' + desc <<-EOD + # Look for a specific alarm + aws_cloudwatch_alarm( + metric: 'my-metric-name', + metric_namespace: 'my-metric-namespace', + ) do + it { should exist } + end + EOD + + include AwsResourceMixin + attr_reader :alarm_name, :metric_name, :metric_namespace, :alarm_actions + + private + + def validate_params(raw_params) + recognized_params = check_resource_param_names( + raw_params: raw_params, + allowed_params: [:metric_name, :metric_namespace], + ) + validated_params = {} + # Currently you must specify exactly metric_name and metric_namespace + [:metric_name, :metric_namespace].each do |param| + raise ArgumentError, "Missing resource param #{param}" unless recognized_params.key?(param) + validated_params[param] = recognized_params.delete(param) + end + + validated_params + end + + def fetch_from_aws + aws_alarms = BackendFactory.create.describe_alarms_for_metric( + metric_name: @metric_name, + namespace: @metric_namespace, + ) + if aws_alarms.metric_alarms.empty? + @exists = false + elsif aws_alarms.metric_alarms.count > 1 + alarms = aws_alarms.metric_alarms.map(&:alarm_name) + raise 'More than one Cloudwatch Alarm was matched. Try using ' \ + "more specific resource parameters. Alarms matched: #{alarms.join(', ')}" + else + @alarm_actions = aws_alarms.metric_alarms.first.alarm_actions + @alarm_name = aws_alarms.metric_alarms.first.alarm_name + @exists = true + end + end + + class Backend + class AwsClientApi < Backend + BackendFactory.set_default_backend(self) + def describe_alarms_for_metric(criteria) + AWSConnection.new.cloudwatch_client.describe_alarms_for_metric(criteria) + end + end + end +end diff --git a/libraries/aws_cloudwatch_log_metric_filter.rb b/libraries/aws_cloudwatch_log_metric_filter.rb new file mode 100644 index 0000000..55fa1e0 --- /dev/null +++ b/libraries/aws_cloudwatch_log_metric_filter.rb @@ -0,0 +1,96 @@ +require '_aws' + +class AwsCloudwatchLogMetricFilter < Inspec.resource(1) + name 'aws_cloudwatch_log_metric_filter' + desc 'Verifies individual Cloudwatch Log Metric Filters' + example <<-EOX + # Look for a LMF by its filter name and log group name. This combination + # will always either find at most one LMF - no duplicates. + describe aws_cloudwatch_log_metric_filter( + filter_name: 'my-filter', + log_group_name: 'my-log-group' + ) do + it { should exist } + end + + # Search for an LMF by pattern and log group. + # This could result in an error if the results are not unique. + describe aws_cloudwatch_log_metric_filter( + log_group_name: 'my-log-group', + pattern: 'my-filter' + ) do + it { should exist } + end +EOX + + include AwsResourceMixin + attr_reader :filter_name, :log_group_name, :pattern, :metric_name, :metric_namespace + + private + + def validate_params(raw_params) + validated_params = check_resource_param_names( + raw_params: raw_params, + allowed_params: [:filter_name, :log_group_name, :pattern], + ) + if validated_params.empty? + raise ArgumentError, 'You must provide either filter_name, log_group, or pattern to aws_cloudwatch_log_metric_filter.' + end + validated_params + end + + def fetch_from_aws + # get a backend + backend = BackendFactory.create + + # Perform query with remote filtering + aws_search_criteria = {} + aws_search_criteria[:filter_name] = filter_name if filter_name + aws_search_criteria[:log_group_name] = log_group_name if log_group_name + aws_results = backend.describe_metric_filters(aws_search_criteria) + + # Then perform local filtering + if pattern + aws_results.select! { |lmf| lmf.filter_pattern == pattern } + end + + # Check result count. We're a singular resource and can tolerate + # 0 or 1 results, not multiple. + if aws_results.count > 1 + raise 'More than one result was returned, but aws_cloudwatch_log_metric_filter '\ + 'can only handle a single AWS resource. Consider passing more resource '\ + 'parameters to narrow down the search.' + elsif aws_results.empty? + @exists = false + else + @exists = true + # Unpack the funny-shaped object we got back from AWS into our instance vars + lmf = aws_results.first + @filter_name = lmf.filter_name + @log_group_name = lmf.log_group_name + @pattern = lmf.filter_pattern # Note inconsistent name + # AWS SDK returns an array of metric transformations + # but only allows one (mandatory) entry, let's flatten that + @metric_name = lmf.metric_transformations.first.metric_name + @metric_namespace = lmf.metric_transformations.first.metric_namespace + end + end + + class Backend + # Uses the cloudwatch API to really talk to AWS + class AwsClientApi < Backend + BackendFactory.set_default_backend(self) + def describe_metric_filters(criteria) + cwl_client = AWSConnection.new.cloudwatch_logs_client + query = {} + query[:filter_name_prefix] = criteria[:filter_name] if criteria[:filter_name] + query[:log_group_name] = criteria[:log_group_name] if criteria[:log_group_name] + # 'pattern' is not available as a remote filter, + # we filter it after the fact locally + # TODO: handle pagination? Max 50/page. Maybe you want a plural resource? + aws_response = cwl_client.describe_metric_filters(query) + aws_response.metric_filters + end + end + end +end diff --git a/libraries/aws_ec2_instance.rb b/libraries/aws_ec2_instance.rb new file mode 100644 index 0000000..9ae09bc --- /dev/null +++ b/libraries/aws_ec2_instance.rb @@ -0,0 +1,127 @@ +require '_aws' + +# author: Christoph Hartmann +class AwsEc2Instance < Inspec.resource(1) + name 'aws_ec2_instance' + desc 'Verifies settings for an EC2 instance' + + example " + describe aws_ec2_instance('i-123456') do + it { should be_running } + it { should have_roles } + end + + describe aws_ec2_instance(name: 'my-instance') do + it { should be_running } + it { should have_roles } + end + " + + def initialize(opts, conn = AWSConnection.new) + @opts = opts + @opts.is_a?(Hash) ? @display_name = @opts[:name] : @display_name = opts + @ec2_client = conn.ec2_client + @ec2_resource = conn.ec2_resource + @iam_resource = conn.iam_resource + end + + def id + return @instance_id if defined?(@instance_id) + if @opts.is_a?(Hash) + first = @ec2_resource.instances( + { + filters: [{ + name: 'tag:Name', + values: [@opts[:name]], + }], + }, + ).first + # catch case where the instance is not known + @instance_id = first.id unless first.nil? + else + @instance_id = @opts + end + end + alias instance_id id + + def exists? + return false if instance.nil? + instance.exists? + end + + # returns the instance state + def state + instance&.state&.name + end + + # helper methods for each state + %w{ + pending running shutting-down + terminated stopping stopped unknown + }.each do |state_name| + define_method state_name.tr('-', '_') + '?' do + state == state_name + end + end + + # attributes that we want to expose + %w{ + public_ip_address private_ip_address key_name private_dns_name + public_dns_name subnet_id architecture root_device_type + root_device_name virtualization_type client_token launch_time + instance_type image_id vpc_id + }.each do |attribute| + define_method attribute do + instance.send(attribute) if instance + end + end + + def security_groups + @security_groups ||= instance.security_groups.map { |sg| + { id: sg.group_id, name: sg.group_name } + } + end + + def tags + @tags ||= instance.tags.map { |tag| { key: tag.key, value: tag.value } } + end + + def to_s + "EC2 Instance #{@display_name}" + end + + def has_roles? + instance_profile = instance.iam_instance_profile + + if instance_profile + roles = @iam_resource.instance_profile( + instance_profile.arn.gsub(%r{^.*\/}, ''), + ).roles + else + roles = nil + end + + roles && !roles.empty? + end + + private + + def instance + @instance ||= @ec2_resource.instance(id) + end +end + +# Deprecated +class AwsEc2 < AwsEc2Instance + name 'aws_ec2' + + def initialize(opts, conn = AWSConnection.new) + deprecated + super(opts, conn) + end + + def deprecated + warn '[DEPRECATION] `aws_ec2(parameter)` is deprecated. ' \ + 'Please use `aws_ec2_instance(parameter)` instead.' + end +end diff --git a/libraries/aws_ec2_security_group.rb b/libraries/aws_ec2_security_group.rb new file mode 100644 index 0000000..8cc280d --- /dev/null +++ b/libraries/aws_ec2_security_group.rb @@ -0,0 +1,91 @@ +require '_aws' + +class AwsEc2SecurityGroup < Inspec.resource(1) + name 'aws_ec2_security_group' + desc 'Verifies settings for an individual AWS Security Group.' + example ' + describe aws_ec2_security_group("sg-12345678") do + it { should exist } + end + ' + + include AwsResourceMixin + attr_reader :description, :group_id, :group_name, :vpc_id + + def to_s + "EC2 Security Group #{@group_id}" + end + + private + + def validate_params(raw_params) + recognized_params = check_resource_param_names( + raw_params: raw_params, + allowed_params: [:id, :group_id, :group_name, :vpc_id], + allowed_scalar_name: :group_id, + allowed_scalar_type: String, + ) + + # id is an alias for group_id + recognized_params[:group_id] = recognized_params.delete(:id) if recognized_params.key?(:id) + + if recognized_params.key?(:group_id) && recognized_params[:group_id] !~ /^sg\-[0-9a-f]{8}/ + raise ArgumentError, 'aws_ec2_security_group security group ID must be in the format "sg-" followed by 8 hexadecimal characters.' + end + + if recognized_params.key?(:vpc_id) && recognized_params[:vpc_id] !~ /^vpc\-[0-9a-f]{8}/ + raise ArgumentError, 'aws_ec2_security_group VPC ID must be in the format "vpc-" followed by 8 hexadecimal characters.' + end + + validated_params = recognized_params + + if validated_params.empty? + raise ArgumentError, 'You must provide parameters to aws_ec2_security_group, such as group_name, group_id, or vpc_id.g_group.' + end + validated_params + end + + def fetch_from_aws + backend = AwsEc2SecurityGroup::BackendFactory.create + + # Transform into filter format expected by AWS + filters = [] + [ + :description, + :group_id, + :group_name, + :vpc_id, + ].each do |criterion_name| + val = instance_variable_get("@#{criterion_name}".to_sym) + next if val.nil? + filters.push( + { + name: criterion_name.to_s.tr('_', '-'), + values: [val], + }, + ) + end + dsg_response = backend.describe_security_groups(filters: filters) + + if dsg_response.security_groups.empty? + @exists = false + return + end + + @exists = true + @description = dsg_response.security_groups[0].description + @group_id = dsg_response.security_groups[0].group_id + @group_name = dsg_response.security_groups[0].group_name + @vpc_id = dsg_response.security_groups[0].vpc_id + end + + class Backend + class AwsClientApi < Backend + AwsEc2SecurityGroup::BackendFactory.set_default_backend self + + def describe_security_groups(query) + AWSConnection.new.ec2_client.describe_security_groups(query) + end + end + end +end diff --git a/libraries/aws_ec2_security_groups.rb b/libraries/aws_ec2_security_groups.rb new file mode 100644 index 0000000..4200153 --- /dev/null +++ b/libraries/aws_ec2_security_groups.rb @@ -0,0 +1,96 @@ +require '_aws' + +class AwsEc2SecurityGroups < Inspec.resource(1) + name 'aws_ec2_security_groups' + desc 'Verifies settings for AWS Security Groups in bulk' + example " + # Verify that you have security groups defined + describe aws_ec2_security_groups do + it { should exist } + end + + # Verify you have more than the default security group + describe aws_ec2_security_groups do + its('entries.count') { should be > 1 } + end + " + + # Constructor. Args are reserved for row fetch filtering. + def initialize(raw_criteria = {}) + validated_criteria = validate_filter_criteria(raw_criteria) + fetch_from_backend(validated_criteria) + end + + # Underlying FilterTable implementation. + filter = FilterTable.create + filter.add_accessor(:where) + .add_accessor(:entries) + .add(:exists?) { |x| !x.entries.empty? } + .add(:group_ids, field: :group_id) + filter.connect(self, :access_key_data) + + def access_key_data + @table + end + + def to_s + 'EC2 Security Groups' + end + + private + + def validate_filter_criteria(raw_criteria) + unless raw_criteria.is_a? Hash + raise 'Unrecognized criteria for fetching Security Groups. ' \ + "Use 'criteria: value' format." + end + + # No criteria yet + recognized_criteria = check_criteria_names(raw_criteria) + + recognized_criteria + end + + def check_criteria_names(raw_criteria: {}, allowed_criteria: []) + # Remove all expected criteria from the raw criteria hash + recognized_criteria = {} + allowed_criteria.each do |expected_criterion| + recognized_criteria[expected_criterion] = raw_criteria.delete(expected_criterion) if raw_criteria.key?(expected_criterion) + end + + # Any leftovers are unwelcome + unless raw_criteria.empty? + raise ArgumentError, "Unrecognized filter criterion '#{raw_criteria.keys.first}'. Expected criteria: #{allowed_criteria.join(', ')}" + end + recognized_criteria + end + + def fetch_from_backend(criteria) + @table = [] + backend = AwsEc2SecurityGroups::BackendFactory.create + # Note: should we ever implement server-side filtering + # (and this is a very good resource for that), + # we will need to reformat the criteria we are sending to AWS. + backend.describe_security_groups(criteria).security_groups.each do |sg_info| + @table.push({ + group_id: sg_info.group_id, + group_name: sg_info.group_name, + vpc_id: sg_info.vpc_id, + }) + end + end + + class BackendFactory + extend AwsBackendFactoryMixin + end + + class Backend + class AwsClientApi < Backend + AwsEc2SecurityGroups::BackendFactory.set_default_backend self + + def describe_security_groups(query) + AWSConnection.new.ec2_client.describe_security_groups(query) + end + end + end +end diff --git a/libraries/aws_iam_access_key.rb b/libraries/aws_iam_access_key.rb new file mode 100644 index 0000000..164cb9e --- /dev/null +++ b/libraries/aws_iam_access_key.rb @@ -0,0 +1,106 @@ +require '_aws' + +# author: Chris Redekop +class AwsIamAccessKey < Inspec.resource(1) + name 'aws_iam_access_key' + desc 'Verifies settings for AWS IAM access keys' + example " + describe aws_iam_access_key(username: 'username', id: 'access-key id') do + it { should exist } + it { should_not be_active } + its('create_date') { should be > Time.now - 365 * 86400 } + its('last_used_date') { should be > Time.now - 90 * 86400 } + end + " + + def initialize(opts, decorator = IamClientDecorator.new) + @access_key = opts[:access_key] + @username = opts[:username] + @id = @access_key ? @access_key.access_key_id : opts[:id] + + @decorator = decorator + end + + def exists? + !access_key.nil? + rescue AccessKeyNotFoundError, Aws::IAM::Errors::NoSuchEntity + false + end + + def id + access_key.access_key_id + end + + def active? + 'Active'.eql? access_key.status + end + + def create_date + access_key.create_date + end + + def last_used_date + access_key_last_used.last_used_date + end + + def to_s + "IAM Access-Key #{@id}" + end + + class AccessKeyNotFoundError < StandardError + end + + class IamClientDecorator + def initialize(validator = ArgumentValidator.new, + conn = AWSConnection.new) + + @validator = validator + @client = conn.iam_client + end + + def get_access_key(username, id) + @validator.validate_username(username) + @validator.validate_id(id) + + access_key = + @client.list_access_keys({ user_name: username }) + .access_key_metadata.select { |x| x.access_key_id.eql? id }.first + + if access_key.nil? + raise AccessKeyNotFoundError, 'access key not found '.concat( + "[username = \"#{username}\", id = \"#{id}\"]", + ) + end + + access_key + end + + def get_access_key_last_used(id) + @validator.validate_id(id) + + @client.get_access_key_last_used({ access_key_id: id }) + .access_key_last_used + end + + class ArgumentValidator + [:username, :id].each do |argument| + define_method "validate_#{argument}" do |value| + return unless value.nil? + + raise ArgumentError, + "missing required resource argument \"#{argument}\"" + end + end + end + end + + private + + def access_key + @access_key ||= @decorator.get_access_key(@username, @id) + end + + def access_key_last_used + @access_key_last_used ||= @decorator.get_access_key_last_used(@id) + end +end diff --git a/libraries/aws_iam_access_keys.rb b/libraries/aws_iam_access_keys.rb new file mode 100644 index 0000000..430423f --- /dev/null +++ b/libraries/aws_iam_access_keys.rb @@ -0,0 +1,178 @@ +require '_aws' + +class AwsIamAccessKeys < Inspec.resource(1) + name 'aws_iam_access_keys' + desc 'Verifies settings for AWS IAM Access Keys in bulk' + example ' + describe aws_iam_access_keys do + it { should_not exist } + end + ' + + VALUED_CRITERIA = [ + :username, + :id, + :access_key_id, + :created_date, + ].freeze + + # Constructor. Args are reserved for row fetch filtering. + def initialize(filter_criteria = {}) + filter_criteria = validate_filter_criteria(filter_criteria) + @table = AccessKeyProvider.create.fetch(filter_criteria) + end + + def validate_filter_criteria(criteria) + # Allow passing a scalar string, the Access Key ID. + criteria = { access_key_id: criteria } if criteria.is_a? String + unless criteria.is_a? Hash + raise 'Unrecognized criteria for fetching Access Keys. ' \ + "Use 'criteria: value' format." + end + + # id and access_key_id are aliases; standardize on access_key_id + criteria[:access_key_id] = criteria.delete(:id) if criteria.key?(:id) + if criteria[:access_key_id] and + criteria[:access_key_id] !~ /^AKIA[0-9A-Z]{16}$/ + raise 'Incorrect format for Access Key ID - expected AKIA followed ' \ + 'by 16 letters or numbers' + end + + criteria.each_key do |criterion| + unless VALUED_CRITERIA.include?(criterion) # rubocop:disable Style/Next + raise 'Unrecognized filter criterion for aws_iam_access_keys, ' \ + "'#{criterion}'. Valid choices are " \ + "#{VALUED_CRITERIA.join(', ')}." + end + end + + criteria + end + + # Underlying FilterTable implementation. + filter = FilterTable.create + filter.add_accessor(:where) + .add_accessor(:entries) + .add(:exists?) { |x| !x.entries.empty? } + .add(:access_key_ids, field: :access_key_id) + .add(:created_date, field: :created_date) + .add(:created_days_ago, field: :created_days_ago) + .add(:created_with_user, field: :created_with_user) + .add(:created_hours_ago, field: :created_hours_ago) + .add(:usernames, field: :username) + .add(:active, field: :active) + .add(:inactive, field: :inactive) + .add(:last_used_date, field: :last_used_date) + .add(:last_used_hours_ago, field: :last_used_hours_ago) + .add(:last_used_days_ago, field: :last_used_days_ago) + .add(:ever_used, field: :ever_used) + .add(:never_used, field: :never_used) + .add(:user_created_date, field: :user_created_date) + filter.connect(self, :access_key_data) + + def access_key_data + @table + end + + def to_s + 'IAM Access Keys' + end + + # Internal support class. This is used to fetch + # the users and access keys. We have an abstract + # class with a concrete AWS implementation provided here; + # a few mock implementations are also provided in the unit tests. + class AccessKeyProvider + # Implementation of AccessKeyProvider which operates by looping over + # all users, then fetching their access keys. + # TODO: An alternate, more scalable implementation could be made + # using the Credential Report. + class AwsUserIterator < AccessKeyProvider + def fetch(criteria) + iam_client = AWSConnection.new.iam_client + + user_details = {} + if criteria.key?(:username) + begin + user_details[criteria[:username]] = iam_client.get_user(user_name: criteria[:username]).user + rescue Aws::IAM::Errors::NoSuchEntity # rubocop:disable Lint/HandleExceptions + # Swallow - a miss on search results should return an empty table + end + else + # TODO: pagination check and resume + iam_client.list_users.users.each do |info| + user_details[info.user_name] = info + end + end + + access_key_data = [] + user_details.each_key do |username| + begin + user_keys = iam_client.list_access_keys(user_name: username) + .access_key_metadata + user_keys = user_keys.map do |metadata| + { + access_key_id: metadata.access_key_id, + username: username, + status: metadata.status, + create_date: metadata.create_date, # DateTime.parse(metadata.create_date), + } + end + + # Copy in from user data + # Synthetics + user_keys.each do |key_info| + add_synthetic_fields(key_info, user_details[username]) + end + access_key_data.concat(user_keys) + rescue Aws::IAM::Errors::NoSuchEntity # rubocop:disable Lint/HandleExceptions + # Swallow - a miss on search results should return an empty table + end + end + access_key_data + end + + def add_synthetic_fields(key_info, user_details) # rubocop:disable Metrics/AbcSize + key_info[:id] = key_info[:access_key_id] + key_info[:active] = key_info[:status] == 'Active' + key_info[:inactive] = key_info[:status] != 'Active' + key_info[:created_hours_ago] = ((Time.now - key_info[:create_date]) / (60*60)).to_i + key_info[:created_days_ago] = (key_info[:created_hours_ago] / 24).to_i + key_info[:user_created_date] = user_details[:create_date] + key_info[:created_with_user] = (key_info[:create_date] - key_info[:user_created_date]).abs < 1.0/24.0 + + # Last used is a separate API call + iam_client = AWSConnection.new.iam_client + last_used = + iam_client.get_access_key_last_used(access_key_id: key_info[:access_key_id]) + .access_key_last_used.last_used_date + key_info[:ever_used] = !last_used.nil? + key_info[:never_used] = last_used.nil? + key_info[:last_used_time] = last_used + return unless last_used + key_info[:last_used_hours_ago] = ((Time.now - last_used) / (60*60)).to_i + key_info[:last_used_days_ago] = (key_info[:last_used_hours_ago]/24).to_i + end + end + + DEFAULT_PROVIDER = AwsIamAccessKeys::AccessKeyProvider::AwsUserIterator + @selected_implementation = DEFAULT_PROVIDER + + # Use this to change what class is created by create(). + def self.select(klass) + @selected_implementation = klass + end + + def self.reset + @selected_implementation = DEFAULT_PROVIDER + end + + def self.create + @selected_implementation.new + end + + def fetch(_filter_criteria) + raise 'Unimplemented abstract method - internal error.' + end + end +end diff --git a/libraries/aws_iam_password_policy.rb b/libraries/aws_iam_password_policy.rb new file mode 100644 index 0000000..bd1c771 --- /dev/null +++ b/libraries/aws_iam_password_policy.rb @@ -0,0 +1,74 @@ +require '_aws' + +# author: Viktor Yakovlyev +class AwsIamPasswordPolicy < Inspec.resource(1) + name 'aws_iam_password_policy' + desc 'Verifies iam password policy' + + example " + describe aws_iam_password_policy do + its('requires_lowercase_characters?') { should be true } + end + + describe aws_iam_password_policy do + its('requires_uppercase_characters?') { should be true } + end + " + + def initialize(conn = AWSConnection.new) + @policy = conn.iam_resource.account_password_policy + rescue Aws::IAM::Errors::NoSuchEntity + @policy = nil + end + + def exists? + !@policy.nil? + end + + def requires_lowercase_characters? + @policy.require_lowercase_characters + end + + def requires_uppercase_characters? + @policy.require_uppercase_characters + end + + def minimum_password_length + @policy.minimum_password_length + end + + def requires_numbers? + @policy.require_numbers + end + + def requires_symbols? + @policy.require_symbols + end + + def allows_users_to_change_password? + @policy.allow_users_to_change_password + end + + def expires_passwords? + @policy.expire_passwords + end + + def max_password_age + raise 'this policy does not expire passwords' unless expires_passwords? + @policy.max_password_age + end + + def prevents_password_reuse? + !@policy.password_reuse_prevention.nil? + end + + def number_of_passwords_to_remember + raise 'this policy does not prevent password reuse' \ + unless prevents_password_reuse? + @policy.password_reuse_prevention + end + + def to_s + 'IAM Password-Policy' + end +end diff --git a/libraries/aws_iam_role.rb b/libraries/aws_iam_role.rb new file mode 100644 index 0000000..ea11b88 --- /dev/null +++ b/libraries/aws_iam_role.rb @@ -0,0 +1,51 @@ +require '_aws' + +class AwsIamRole < Inspec.resource(1) + name 'aws_iam_role' + desc 'Verifies settings for an IAM Role' + example " + describe aws_iam_role('my-role') do + it { should exist } + end + " + + include AwsResourceMixin + attr_reader :role_name, :description + + private + + def validate_params(raw_params) + validated_params = check_resource_param_names( + raw_params: raw_params, + allowed_params: [:role_name], + allowed_scalar_name: :role_name, + allowed_scalar_type: String, + ) + if validated_params.empty? + raise ArgumentError, 'You must provide a role_name to aws_iam_role.' + end + validated_params + end + + def fetch_from_aws + role_info = nil + begin + role_info = AwsIamRole::BackendFactory.create.get_role(role_name: role_name) + rescue Aws::IAM::Errors::NoSuchEntity + @exists = false + return + end + @exists = true + @description = role_info.role.description + end + + # Uses the SDK API to really talk to AWS + class Backend + class AwsClientApi + BackendFactory.set_default_backend(self) + def get_role(query) + AWSConnection.new.iam_client.get_role(query) + end + end + end +end diff --git a/libraries/aws_iam_root_user.rb b/libraries/aws_iam_root_user.rb new file mode 100644 index 0000000..89a9fae --- /dev/null +++ b/libraries/aws_iam_root_user.rb @@ -0,0 +1,34 @@ +require '_aws' + +# author: Miles Tjandrawidjaja +class AwsIamRootUser < Inspec.resource(1) + name 'aws_iam_root_user' + desc 'Verifies settings for AWS root account' + example " + describe aws_iam_root_user do + it { should have_access_key } + end + " + + def initialize(conn = AWSConnection.new) + @client = conn.iam_client + end + + def has_access_key? + summary_account['AccountAccessKeysPresent'] == 1 + end + + def has_mfa_enabled? + summary_account['AccountMFAEnabled'] == 1 + end + + def to_s + 'AWS Root-User' + end + + private + + def summary_account + @summary_account ||= @client.get_account_summary.summary_map + end +end diff --git a/libraries/aws_iam_user.rb b/libraries/aws_iam_user.rb new file mode 100644 index 0000000..1d9f372 --- /dev/null +++ b/libraries/aws_iam_user.rb @@ -0,0 +1,109 @@ +require '_aws' + +# author: Alex Bedley +# author: Steffanie Freeman +# author: Simon Varlow +# author: Chris Redekop +class AwsIamUser < Inspec.resource(1) + name 'aws_iam_user' + desc 'Verifies settings for AWS IAM user' + example " + describe aws_iam_user(username: 'test_user') do + it { should have_mfa_enabled } + it { should_not have_console_password } + end + " + + include AwsResourceMixin + attr_reader :username, :has_mfa_enabled, :has_console_password, :access_keys + alias has_mfa_enabled? has_mfa_enabled + alias has_console_password? has_console_password + + def name + warn "[DEPRECATION] - Property ':name' is deprecated on the aws_iam_user resource. Use ':username' instead." + username + end + + def to_s + "IAM User #{username}" + end + + private + + def validate_params(raw_params) + validated_params = check_resource_param_names( + raw_params: raw_params, + allowed_params: [:username, :aws_user_struct, :name, :user], + allowed_scalar_name: :username, + allowed_scalar_type: String, + ) + # If someone passed :name, rename it to :username + if validated_params.key?(:name) + warn "[DEPRECATION] - Resource parameter ':name' is deprecated on the aws_iam_user resource. Use ':username' instead." + validated_params[:username] = validated_params.delete(:name) + end + + # If someone passed :user, rename it to :aws_user_struct + if validated_params.key?(:user) + warn "[DEPRECATION] - Resource parameter ':user' is deprecated on the aws_iam_user resource. Use ':aws_user_struct' instead." + validated_params[:aws_user_struct] = validated_params.delete(:user) + end + + if validated_params.empty? + raise ArgumentError, 'You must provide a username to aws_iam_user.' + end + validated_params + end + + def fetch_from_aws + backend = BackendFactory.create + @aws_user_struct ||= nil # silence unitialized warning + unless @aws_user_struct + begin + @aws_user_struct = backend.get_user(user_name: username) + rescue Aws::IAM::Errors::NoSuchEntity + @exists = false + return + end + end + # TODO: extract properties from aws_user_struct? + + @exists = true + + begin + _login_profile = backend.get_login_profile(user_name: username) + @has_console_password = true + # Password age also available here + rescue Aws::IAM::Errors::NoSuchEntity + @has_console_password = false + end + + mfa_info = backend.list_mfa_devices(user_name: username) + @has_mfa_enabled = !mfa_info.mfa_devices.empty? + + # TODO: consider returning Inspec AwsIamAccessKey objects + @access_keys = backend.list_access_keys(user_name: username).access_key_metadata + end + + class Backend + class AwsClientApi + BackendFactory.set_default_backend(self) + + def get_user(criteria) + AWSConnection.new.iam_client.get_user(criteria) + end + + def get_login_profile(criteria) + AWSConnection.new.iam_client.get_login_profile(criteria) + end + + def list_mfa_devices(criteria) + AWSConnection.new.iam_client.list_mfa_devices(criteria) + end + + def list_access_keys(criteria) + AWSConnection.new.iam_client.list_access_keys(criteria) + end + end + end +end diff --git a/libraries/aws_iam_users.rb b/libraries/aws_iam_users.rb new file mode 100644 index 0000000..5ca9faf --- /dev/null +++ b/libraries/aws_iam_users.rb @@ -0,0 +1,104 @@ +require '_aws' + +# author: Alex Bedley +# author: Steffanie Freeman +# author: Simon Varlow +# author: Chris Redekop +class AwsIamUsers < Inspec.resource(1) + name 'aws_iam_users' + desc 'Verifies settings for AWS IAM users' + example ' + describe aws_iam_users.where(has_mfa_enabled?: false) do + it { should_not exist } + end + + describe aws_iam_users.where(has_console_password?: true) do + it { should exist } + end + ' + + filter = FilterTable.create + filter.add_accessor(:where) + .add_accessor(:entries) + .add(:exists?) { |x| !x.entries.empty? } + .add(:has_mfa_enabled?, field: :has_mfa_enabled) + .add(:has_console_password?, field: :has_console_password) + .add(:username, field: :user_name) + filter.connect(self, :collect_user_details) + + # No resource params => no overridden constructor + # AWS API only offers filtering on path prefix; + # little other opportunity for server-side filtering. + + def collect_user_details + backend = Backend.create + users = backend.list_users.users.map(&:to_h) + + # TODO: lazy columns - https://github.com/chef/inspec-aws/issues/100 + users.each do |user| + begin + _login_profile = backend.get_login_profile(user_name: user[:user_name]) + user[:has_console_password] = true + rescue Aws::IAM::Errors::NoSuchEntity + user[:has_console_password] = false + end + user[:has_console_password?] = user[:has_console_password] + + begin + aws_mfa_devices = backend.list_mfa_devices(user_name: user[:user_name]) + user[:has_mfa_enabled] = !aws_mfa_devices.mfa_devices.empty? + rescue Aws::IAM::Errors::NoSuchEntity + user[:has_mfa_enabled] = false + end + user[:has_mfa_enabled?] = user[:has_mfa_enabled] + end + users + end + + def to_s + 'IAM Users' + end + + # Entry cooker. Needs discussion. + # def users + # end + + #===========================================================================# + # Backend Implementation + #===========================================================================# + class Backend + #=====================================================# + # Concrete Implementation + #=====================================================# + # Uses AWS API to really talk to AWS + class AwsClientApi < Backend + # TODO: delegate this out + def list_users(query = {}) + AWSConnection.new.iam_client.list_users(query) + end + + def get_login_profile(query) + AWSConnection.new.iam_client.get_login_profile(query) + end + + def list_mfa_devices(query) + AWSConnection.new.iam_client.list_mfa_devices(query) + end + end + + #=====================================================# + # Factory Interface + #=====================================================# + # TODO: move this to a mix-in + DEFAULT_BACKEND = AwsClientApi + @selected_backend = DEFAULT_BACKEND + + def self.create + @selected_backend.new + end + + def self.select(klass) + @selected_backend = klass + end + end +end diff --git a/libraries/aws_s3_bucket.rb b/libraries/aws_s3_bucket.rb new file mode 100644 index 0000000..07cb669 --- /dev/null +++ b/libraries/aws_s3_bucket.rb @@ -0,0 +1,102 @@ +require '_aws' + +# author: Matthew Dromazos +class AwsS3Bucket < Inspec.resource(1) + name 'aws_s3_bucket' + desc 'Verifies settings for a s3 bucket' + example " + describe aws_s3_bucket(bucket_name: 'test_bucket') do + it { should exist } + end + " + + include AwsResourceMixin + attr_reader :bucket_name, :region + + def to_s + "S3 Bucket #{@bucket_name}" + end + + def bucket_acl + # This is simple enough to inline it. + @bucket_acl ||= AwsS3Bucket::BackendFactory.create.get_bucket_acl(bucket: bucket_name).grants + end + + def bucket_policy + @bucket_policy ||= fetch_bucket_policy + end + + # RSpec will alias this to be_public + def public? + # first line just for formatting + false || \ + bucket_acl.any? { |g| g.grantee.type == 'Group' && g.grantee.uri =~ /AllUsers/ } || \ + bucket_acl.any? { |g| g.grantee.type == 'Group' && g.grantee.uri =~ /AuthenticatedUsers/ } || \ + bucket_policy.any? { |s| s.effect == 'Allow' && s.principal == '*' } + end + + private + + def validate_params(raw_params) + validated_params = check_resource_param_names( + raw_params: raw_params, + allowed_params: [:bucket_name], + allowed_scalar_name: :bucket_name, + allowed_scalar_type: String, + ) + if validated_params.empty? or !validated_params.key?(:bucket_name) + raise ArgumentError, 'You must provide a bucket_name to aws_s3_bucket.' + end + + validated_params + end + + def fetch_from_aws + backend = AwsS3Bucket::BackendFactory.create + + # Since there is no basic "get_bucket" API call, use the + # region fetch as the existence check. + begin + @region = backend.get_bucket_location(bucket: bucket_name).location_constraint + rescue Aws::S3::Errors::NoSuchBucket + @exists = false + return + end + @exists = true + end + + def fetch_bucket_policy + backend = AwsS3Bucket::BackendFactory.create + + begin + # AWS SDK returns a StringIO, we have to read() + raw_policy = backend.get_bucket_policy(bucket: bucket_name).policy + return JSON.parse(raw_policy.read)['Statement'].map do |statement| + lowercase_hash = {} + statement.each_key { |k| lowercase_hash[k.downcase] = statement[k] } + OpenStruct.new(lowercase_hash) + end + rescue Aws::S3::Errors::NoSuchBucketPolicy + return [] + end + end + + # Uses the SDK API to really talk to AWS + class Backend + class AwsClientApi + BackendFactory.set_default_backend(self) + + def get_bucket_acl(query) + AWSConnection.new.s3_client.get_bucket_acl(query) + end + + def get_bucket_location(query) + AWSConnection.new.s3_client.get_bucket_location(query) + end + + def get_bucket_policy(query) + AWSConnection.new.s3_client.get_bucket_policy(query) + end + end + end +end diff --git a/libraries/aws_sns_topic.rb b/libraries/aws_sns_topic.rb new file mode 100644 index 0000000..d9c790a --- /dev/null +++ b/libraries/aws_sns_topic.rb @@ -0,0 +1,53 @@ +require '_aws' + +class AwsSnsTopic < Inspec.resource(1) + name 'aws_sns_topic' + desc 'Verifies settings for an SNS Topic' + example " + describe aws_sns_topic('arn:aws:sns:us-east-1:123456789012:some-topic') do + it { should exist } + its('confirmed_subscription_count') { should_not be_zero } + end + " + + include AwsResourceMixin + attr_reader :arn, :confirmed_subscription_count + + private + + def validate_params(raw_params) + validated_params = check_resource_param_names( + raw_params: raw_params, + allowed_params: [:arn], + allowed_scalar_name: :arn, + allowed_scalar_type: String, + ) + # Validate the ARN + unless validated_params[:arn] =~ /^arn:aws:sns:[\w\-]+:\d{12}:[\S]+$/ + raise ArgumentError, 'Malformed ARN for SNS topics. Expected an ARN of the form ' \ + "'arn:aws:sns:REGION:ACCOUNT-ID:TOPIC-NAME'" + end + validated_params + end + + def fetch_from_aws + aws_response = AwsSnsTopic::BackendFactory.create.get_topic_attributes(topic_arn: @arn).attributes + @exists = true + + # The response has a plain hash with CamelCase plain string keys and string values + @confirmed_subscription_count = aws_response['SubscriptionsConfirmed'].to_i + rescue Aws::SNS::Errors::NotFound + @exists = false + end + + # Uses the SDK API to really talk to AWS + class Backend + class AwsClientApi + BackendFactory.set_default_backend(self) + + def get_topic_attributes(criteria) + AWSConnection.new.sns_client.get_topic_attributes(criteria) + end + end + end +end diff --git a/libraries/aws_vpc.rb b/libraries/aws_vpc.rb new file mode 100644 index 0000000..f8453dc --- /dev/null +++ b/libraries/aws_vpc.rb @@ -0,0 +1,69 @@ +require '_aws' + +class AwsVpc < Inspec.resource(1) + name 'aws_vpc' + desc 'Verifies settings for AWS VPC' + example " + describe aws_vpc do + it { should be_default } + its('cidr_block') { should cmp '10.0.0.0/16' } + end + " + + include AwsResourceMixin + + def to_s + "VPC #{vpc_id}" + end + + [:cidr_block, :dhcp_options_id, :state, :vpc_id, :instance_tenancy, :is_default].each do |property| + define_method(property) do + @vpc[property] + end + end + + alias default? is_default + + private + + def validate_params(raw_params) + validated_params = check_resource_param_names( + raw_params: raw_params, + allowed_params: [:vpc_id], + allowed_scalar_name: :vpc_id, + allowed_scalar_type: String, + ) + + if validated_params.key?(:vpc_id) && validated_params[:vpc_id] !~ /^vpc\-[0-9a-f]{8}/ + raise ArgumentError, 'aws_vpc VPC ID must be in the format "vpc-" followed by 8 hexadecimal characters.' + end + + validated_params + end + + def fetch_from_aws + backend = AwsVpc::BackendFactory.create + + if @vpc_id.nil? + filter = { name: 'isDefault', values: ['true'] } + else + filter = { name: 'vpc-id', values: [@vpc_id] } + end + + resp = backend.describe_vpcs({ filters: [filter] }) + + @vpc = resp.vpcs[0].to_h + @vpc_id = @vpc[:vpc_id] + @exists = !@vpc.empty? + end + + class Backend + class AwsClientApi + BackendFactory.set_default_backend(self) + + def describe_vpcs(query) + AWSConnection.new.ec2_client.describe_vpcs(query) + end + end + end +end diff --git a/libraries/aws_vpcs.rb b/libraries/aws_vpcs.rb new file mode 100644 index 0000000..a9891be --- /dev/null +++ b/libraries/aws_vpcs.rb @@ -0,0 +1,44 @@ +require '_aws' + +class AwsVpcs < Inspec.resource(1) + name 'aws_vpcs' + desc 'Verifies settings for AWS VPCs in bulk' + example ' + describe aws_vpcs do + it { should exist } + end + ' + + # Underlying FilterTable implementation. + filter = FilterTable.create + filter.add_accessor(:entries) + .add(:exists?) { |x| !x.entries.empty? } + filter.connect(self, :vpc_data) + + def vpc_data + @table + end + + def to_s + 'VPCs' + end + + def initialize + backend = AwsVpcs::BackendFactory.create + @table = backend.describe_vpcs.to_h[:vpcs] + end + + class BackendFactory + extend AwsBackendFactoryMixin + end + + class Backend + class AwsClientApi + BackendFactory.set_default_backend(self) + + def describe_vpcs(query = {}) + AWSConnection.new.ec2_client.describe_vpcs(query) + end + end + end +end diff --git a/pre-kitchen.rb b/pre-kitchen.rb deleted file mode 100755 index 7f0707c..0000000 --- a/pre-kitchen.rb +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/ruby -require 'yaml' - -@run_kitchen = true -file_content = YAML.load_file(".kitchen.yml") -my_vars = file_content['driver']['variables'] - -my_vars.each_pair do |key, value| - x = ENV.key?(value.split("'")[1]) ? "#{ENV[value.split("'")[1]]}" : 'unset' - if x == 'unset' - @run_kitchen = false - puts "Please set #{value.split("'")[1]}" - else - puts key.to_s + ': ' + x.to_s - end -end - -puts '' -puts '----------' -puts 'You are OK to run Test Kitchen' if @run_kitchen == true -puts 'Please SET the above Envrioment variables before running kitchen' if @run_kitchen == false diff --git a/test/integration/cis-aws-foundations-baseline b/test/integration/cis-aws-foundations-baseline deleted file mode 120000 index f4677e8..0000000 --- a/test/integration/cis-aws-foundations-baseline +++ /dev/null @@ -1 +0,0 @@ -../../../cis-aws-foundations-baseline \ No newline at end of file diff --git a/test/integration/default/build/aws.tf b/test/integration/default/build/aws.tf new file mode 100644 index 0000000..95f0469 --- /dev/null +++ b/test/integration/default/build/aws.tf @@ -0,0 +1,21 @@ +terraform { + required_version = "~> 0.10.0" +} + +provider "aws" { + version = "= 1.1" +} + +data "aws_caller_identity" "creds" {} + +output "aws_account_id" { + value = "${data.aws_caller_identity.creds.account_id}" +} + +data "aws_region" "region" { + current = true +} + +output "aws_region" { + value = "${data.aws_region.region.name}" +} diff --git a/test/integration/default/build/cloudtrail.tf b/test/integration/default/build/cloudtrail.tf new file mode 100644 index 0000000..965db8c --- /dev/null +++ b/test/integration/default/build/cloudtrail.tf @@ -0,0 +1,230 @@ +resource "aws_s3_bucket" "trail_1_bucket" { + bucket = "${terraform.env}-trail-01-bucket" + force_destroy = true + + policy = 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 2 } + end +end diff --git a/test/integration/default/verify/controls/aws_iam_access_key.rb b/test/integration/default/verify/controls/aws_iam_access_key.rb new file mode 100644 index 0000000..12f565f --- /dev/null +++ b/test/integration/default/verify/controls/aws_iam_access_key.rb @@ -0,0 +1,89 @@ +fixtures = {} +[ + 'iam_user_with_access_key', + 'iam_user_without_access_key', + 'iam_user_recall_miss', + 'iam_access_key_recall_hit', + 'iam_access_key_recall_miss', +].each do |fixture_name| + fixtures[fixture_name] = attribute( + fixture_name, + default: "default.#{fixture_name}", + description: 'See ../build/iam.tf', + ) +end + +#======================================================# +# IAM Access Key - Singular +#======================================================# + +#------------------- Recall / Miss -------------------# + +control "aws_iam_access_key recall" do + # Neither user nor access key ID exist + describe aws_iam_access_key(username: fixtures['iam_user_recall_miss'], id: fixtures['iam_access_key_recall_miss']) do + it { should_not exist } + end + + # User exists but has no keys + describe aws_iam_access_key(username: fixtures['iam_user_without_access_key'], id: fixtures['iam_access_key_recall_miss']) do + it { should_not exist } + end + + # User exists and has an access key + describe aws_iam_access_key(username: fixtures['iam_user_with_access_key'], id: fixtures['iam_access_key_recall_hit']) do + it { should exist } + end +end + +#------------- Property - create_date -------------# +# TODO: create_date tests + +#------------- Property - last_used_date -------------# +# TODO: last_used_date tests + +#======================================================# +# IAM Access Key - Plural +#======================================================# + +control 'IAM Access Keys - fetch all' do + describe aws_iam_access_keys do + it { should exist } + end +end + +control 'IAM Access Keys - Client-side filtering' do + all_keys = aws_iam_access_keys + describe all_keys.where(username: fixtures['iam_user_with_access_key']) do + its('entries.length') { should be 1 } + its('access_key_ids.first') { should eq fixtures['iam_access_key_recall_hit'] } + end + describe all_keys.where(created_days_ago: 0) do + it { should exist } + end + describe all_keys.where { active } do + it { should exist } + end + + # This would presumably refer to your test-user-last-key-use IAM user + # This test will fail if you have very recently setup your + # testing environment + describe all_keys.where { ever_used } + .where { last_used_days_ago > 0 } do + it { should exist } + end + describe all_keys.where { created_with_user } do + it { should exist } + end +end + +control 'IAM Access Keys - fetch-time filtering' do + describe aws_iam_access_keys(username: fixtures['iam_user_with_access_key']) do + its('entries.length') { should be 1 } + its('access_key_ids.first') { should eq fixtures['iam_access_key_recall_hit'] } + end + + describe aws_iam_access_keys(username: fixtures['iam_user_without_access_key']) do + it { should_not exist } + end +end \ No newline at end of file diff --git a/test/integration/default/verify/controls/aws_iam_role.rb b/test/integration/default/verify/controls/aws_iam_role.rb new file mode 100644 index 0000000..b77585b --- /dev/null +++ b/test/integration/default/verify/controls/aws_iam_role.rb @@ -0,0 +1,6 @@ +control 'AWS IAM Role search for default AWS role' do + # Every AWS account comes with this one by default + describe aws_iam_role('AWSServiceRoleForOrganizations') do + it { should exist } + end +end \ No newline at end of file diff --git a/test/integration/default/verify/controls/aws_iam_root_user.rb b/test/integration/default/verify/controls/aws_iam_root_user.rb new file mode 100644 index 0000000..e5ff11c --- /dev/null +++ b/test/integration/default/verify/controls/aws_iam_root_user.rb @@ -0,0 +1,27 @@ +fixtures = {} +[ + 'aws_account_id', +].each do |fixture_name| + fixtures[fixture_name] = attribute( + fixture_name, + default: "default.#{fixture_name}", + description: 'See ../build/iam.tf', + ) +end + +#------------- Property - has_mfa_enabled -------------# +# Negative test in 'minimal' test set. See TESTING_AGAINST_AWS.md +# for fixture instructions. +control "aws_iam_root_user has_mfa_enabled property" do + describe aws_iam_root_user do + it { should have_mfa_enabled } + end +end + +#------------- Property - has_access_key -------------# +# Positive test in 'minimal' test set +control "aws_iam_root_user has_access_key property" do + describe aws_iam_root_user do + it { should_not have_access_key } + end +end \ No newline at end of file diff --git a/test/integration/default/verify/controls/aws_iam_user.rb b/test/integration/default/verify/controls/aws_iam_user.rb new file mode 100644 index 0000000..ea622d9 --- /dev/null +++ b/test/integration/default/verify/controls/aws_iam_user.rb @@ -0,0 +1,46 @@ +fixtures = {} +[ + 'iam_user_recall_hit', + 'iam_user_recall_miss', + 'iam_user_no_mfa_enabled', + 'iam_user_has_console_password', + 'iam_user_with_access_key', +].each do |fixture_name| + fixtures[fixture_name] = attribute( + fixture_name, + default: "default.#{fixture_name}", + description: 'See ../build/iam.tf', + ) +end + +#------------------- Recall / Miss -------------------# +describe aws_iam_user(username: fixtures['iam_user_recall_hit']) do + it { should exist } +end + +describe aws_iam_user(username: fixtures['iam_user_recall_miss']) do + it { should_not exist } +end + +#------------- Property - has_mfa_enabled -------------# + +# TODO: fixture and test for has_mfa_enabled + +describe aws_iam_user(username: fixtures['iam_user_no_mfa_enabled']) do + it { should_not have_mfa_enabled } + it { should_not have_console_password } # TODO: this is working by accident, we should have a dedicated fixture +end + +#---------- Property - has_console_password -----------# + +describe aws_iam_user(username: fixtures['iam_user_has_console_password']) do + it { should have_console_password } +end + +#------------- Property - access_keys -------------# + +aws_iam_user(username: fixtures['iam_user_with_access_key']).access_keys.each { |access_key| + describe access_key do + its('status') { should eq 'Active' } + end +} diff --git a/test/integration/default/verify/controls/aws_iam_users.rb b/test/integration/default/verify/controls/aws_iam_users.rb new file mode 100644 index 0000000..6c53f25 --- /dev/null +++ b/test/integration/default/verify/controls/aws_iam_users.rb @@ -0,0 +1,4 @@ +describe aws_iam_users.where(has_console_password?: true) + .where(has_mfa_enabled?: false) do + it { should exist } +end diff --git a/test/integration/default/verify/controls/aws_s3_bucket.rb b/test/integration/default/verify/controls/aws_s3_bucket.rb new file mode 100644 index 0000000..45dc901 --- /dev/null +++ b/test/integration/default/verify/controls/aws_s3_bucket.rb @@ -0,0 +1,113 @@ +fixtures = {} +[ + 's3_bucket_public_name', + 's3_bucket_private_name', + 's3_bucket_auth_name', + 's3_bucket_private_acl_public_policy_name', + 's3_bucket_public_region', +].each do |fixture_name| + fixtures[fixture_name] = attribute( + fixture_name, + default: "default.#{fixture_name}", + description: 'See ../build/s3.tf', + ) +end + +control 'aws_s3_bucket recall tests' do + #------------------- Exists -------------------# + describe aws_s3_bucket(bucket_name: fixtures['s3_bucket_public_name']) do + it { should exist } + end + + #------------------- Does Not Exist -------------------# + describe aws_s3_bucket(bucket_name: 'inspec-testing-NonExistentBucket.chef.io') do + it { should_not exist } + end +end + +control 'aws_s3_bucket properties tests' do + #--------------------------- Region --------------------------# + describe aws_s3_bucket(bucket_name: fixtures['s3_bucket_public_name']) do + its('region') { should eq fixtures['s3_bucket_public_region'] } + end + + #------------------- bucket_acl -------------------# + describe "Bucket ACL: Public grants on a public bucket" do + subject do + aws_s3_bucket(bucket_name: fixtures['s3_bucket_public_name']).bucket_acl.select do |g| + g.grantee.type == 'Group' && g.grantee.uri =~ /AllUsers/ + end + end + it { should_not be_empty } + end + + describe "Bucket ACL: Public grants on a private bucket" do + subject do + aws_s3_bucket(bucket_name: fixtures['s3_bucket_private_name']).bucket_acl.select do |g| + g.grantee.type == 'Group' && g.grantee.uri =~ /AllUsers/ + end + end + it { should be_empty } + end + + describe "Bucket ACL: AuthUser grants on a private bucket" do + subject do + aws_s3_bucket(bucket_name: fixtures['s3_bucket_private_name']).bucket_acl.select do |g| + g.grantee.type == 'Group' && g.grantee.uri =~ /AuthenticatedUsers/ + end + end + it { should be_empty } + end + + describe "Bucket ACL: AuthUser grants on an AuthUser bucket" do + subject do + aws_s3_bucket(bucket_name: fixtures['s3_bucket_auth_name']).bucket_acl.select do |g| + g.grantee.type == 'Group' && g.grantee.uri =~ /AuthenticatedUsers/ + end + end + it { should_not be_empty } + end + + #------------------- bucket_policy -------------------# + describe "Bucket Policy: Allow GetObject Statement For Everyone on public" do + subject do + bucket_policy = aws_s3_bucket(bucket_name: fixtures['s3_bucket_public_name']).bucket_policy + allow_all = bucket_policy.select { |s| s.effect == 'Allow' && s.principal == '*' } + allow_all.count + end + it { should == 1 } + end + + describe "Bucket Policy: Allow GetObject Statement For Everyone on private" do + subject do + bucket_policy = aws_s3_bucket(bucket_name: fixtures['s3_bucket_private_name']).bucket_policy + allow_all = bucket_policy.select { |s| s.effect == 'Allow' && s.principal == '*' } + allow_all.count + end + it { should be_zero } + end + + describe "Bucket Policy: Empty policy on auth" do + subject do + aws_s3_bucket(bucket_name: fixtures['s3_bucket_auth_name']).bucket_policy + end + it { should be_empty } + end +end + +control 'aws_s3_bucket matchers test' do + + #------------------------ be_public --------------------------# + describe aws_s3_bucket(bucket_name: fixtures['s3_bucket_public_name']) do + it { should be_public } + end + describe aws_s3_bucket(bucket_name: fixtures['s3_bucket_auth_name']) do + it { should be_public } + end + describe aws_s3_bucket(bucket_name: fixtures['s3_bucket_private_name']) do + it { should_not be_public } + end + describe aws_s3_bucket(bucket_name: fixtures['s3_bucket_private_acl_public_policy_name']) do + it { should be_public } + end +end diff --git a/test/integration/default/verify/controls/aws_sns_topic.rb b/test/integration/default/verify/controls/aws_sns_topic.rb new file mode 100644 index 0000000..949c132 --- /dev/null +++ b/test/integration/default/verify/controls/aws_sns_topic.rb @@ -0,0 +1,39 @@ +fixtures = {} +[ + 'sns_topic_recall_hit_arn', + 'sns_topic_with_subscription_arn', + 'sns_topic_no_subscription_arn', +].each do |fixture_name| + fixtures[fixture_name] = attribute( + fixture_name, + default: "default.#{fixture_name}", + description: 'See ../build/sns.tf', + ) +end + +control 'aws_sns_topic recall' do + + # Split the ARNs so we can test various ways of missing + scheme, partition, service, region, account, topic = fixtures['sns_topic_recall_hit_arn'].split(':') + arn_prefix = [scheme, partition, service].join(':') + + # Search miss + no_such_topic_arn = [arn_prefix, region, account, 'no-such-topic-for-realz'].join(':') + describe aws_sns_topic(no_such_topic_arn) do + it { should_not exist } + end + + # Search hit + describe aws_sns_topic(fixtures['sns_topic_recall_hit_arn']) do + it { should exist } + end +end + +control "aws_sns_topic confirmed_subscription_count property" do + describe aws_sns_topic(fixtures['sns_topic_with_subscription_arn']) do + its('confirmed_subscription_count') { should_not be_zero } + end + describe aws_sns_topic(fixtures['sns_topic_no_subscription_arn']) do + its('confirmed_subscription_count') { should be_zero } + end +end \ No newline at end of file diff --git a/test/integration/default/verify/controls/aws_vpc.rb b/test/integration/default/verify/controls/aws_vpc.rb new file mode 100644 index 0000000..4fff465 --- /dev/null +++ b/test/integration/default/verify/controls/aws_vpc.rb @@ -0,0 +1,59 @@ +fixtures = {} +[ + 'ec2_security_group_default_vpc_id', + 'vpc_non_default_id', + 'vpc_non_default_cidr_block', + 'vpc_non_default_instance_tenancy' +].each do |fixture_name| + fixtures[fixture_name] = attribute( + fixture_name, + default: "default.#{fixture_name}", + description: 'See ../build/ec2.tf', + ) +end + +control "aws_vpc recall" do + describe aws_vpc(fixtures['ec2_security_group_default_vpc_id']) do + it { should exist} + end + + describe aws_vpc do + it { should exist } + end + + describe aws_vpc(vpc_id: fixtures['vpc_non_default_id']) do + it { should exist } + end + + describe aws_vpc('vpc-12345678') do + it { should_not exist } + end +end + +control "aws_vpc properties" do + describe aws_vpc(fixtures['vpc_non_default_id']) do + its('vpc_id') { should eq fixtures['vpc_non_default_id'] } + its('state') { should eq 'available' } + its('cidr_block') { should eq fixtures['vpc_non_default_cidr_block']} + its('instance_tenancy') { should eq fixtures['vpc_non_default_instance_tenancy']} + # TODO: figure out how to access the dhcp_options_id + end + + describe aws_vpc do + its('vpc_id') { should eq fixtures['ec2_security_group_default_vpc_id'] } + end +end + +control "aws_vpc matchers" do + describe aws_vpc do + it { should be_default } + end + + describe aws_vpc(fixtures['ec2_security_group_default_vpc_id']) do + it { should be_default } + end + + describe aws_vpc(fixtures['vpc_non_default_id']) do + it { should_not be_default } + end +end diff --git a/test/integration/default/verify/controls/aws_vpcs.rb b/test/integration/default/verify/controls/aws_vpcs.rb new file mode 100644 index 0000000..bc6461d --- /dev/null +++ b/test/integration/default/verify/controls/aws_vpcs.rb @@ -0,0 +1,5 @@ +control "aws_vpcs recall" do + describe aws_vpcs do + it { should exist } + end +end diff --git a/test/integration/default/verify/inspec.yml b/test/integration/default/verify/inspec.yml new file mode 100644 index 0000000..9b140cc --- /dev/null +++ b/test/integration/default/verify/inspec.yml @@ -0,0 +1,4 @@ +name: inspec-aws-integration-tests +depends: + - name: aws + path: ../../../../ diff --git a/test/integration/minimal/build/aws.tf b/test/integration/minimal/build/aws.tf new file mode 100644 index 0000000..3c6d6a6 --- /dev/null +++ b/test/integration/minimal/build/aws.tf @@ -0,0 +1,12 @@ +terraform { + required_version = "~> 0.10.0" +} + +provider "aws" { + version = "= 1.1" +} + +data "aws_caller_identity" "creds" {} +output "aws_account_id" { + value = "${data.aws_caller_identity.creds.account_id}" +} diff --git a/test/integration/minimal/verify/controls/aws_iam_root_user.rb b/test/integration/minimal/verify/controls/aws_iam_root_user.rb new file mode 100644 index 0000000..63a6c04 --- /dev/null +++ b/test/integration/minimal/verify/controls/aws_iam_root_user.rb @@ -0,0 +1,27 @@ + +fixtures = {} +[ + 'aws_account_id', +].each do |fixture_name| + fixtures[fixture_name] = attribute( + fixture_name, + default: "default.#{fixture_name}", + description: 'See ../build/iam.tf', + ) +end + +#------------- Property - has_mfa_enabled -------------# +# Positive test in 'default' test set +control "aws_iam_root_user has_mfa_enabled property" do + describe aws_iam_root_user do + it { should_not have_mfa_enabled } + end +end + +#------------- Property - has_access_key -------------# +# Negative test in 'default' test set +control "aws_iam_root_user has_access_key property" do + describe aws_iam_root_user do + it { should have_access_key } + end +end \ No newline at end of file diff --git a/test/integration/minimal/verify/inspec.yml b/test/integration/minimal/verify/inspec.yml new file mode 100644 index 0000000..9b140cc --- /dev/null +++ b/test/integration/minimal/verify/inspec.yml @@ -0,0 +1,4 @@ +name: inspec-aws-integration-tests +depends: + - name: aws + path: ../../../../ diff --git a/test/unit/helper.rb b/test/unit/helper.rb new file mode 100644 index 0000000..5aa8a7a --- /dev/null +++ b/test/unit/helper.rb @@ -0,0 +1,10 @@ +require 'minitest/autorun' +require 'minitest/unit' +require 'minitest/pride' + +# Data formats commonly used in testing +require 'json' +require 'ostruct' + +require 'inspec/resource' +require_relative '../../libraries/_aws' diff --git a/test/unit/resources/aws_cloudtrail_trail_test.rb b/test/unit/resources/aws_cloudtrail_trail_test.rb new file mode 100644 index 0000000..ea37327 --- /dev/null +++ b/test/unit/resources/aws_cloudtrail_trail_test.rb @@ -0,0 +1,171 @@ +require 'helper' +require 'aws_cloudtrail_trail' + +# MACTTSB = MockAwsCloudTrailTrailSingularBackend +# Abbreviation not used outside this file + +#=============================================================================# +# Constructor Tests +#=============================================================================# +class AwsCloudTrailTrailConstructorTest < Minitest::Test + + def setup + AwsCloudTrailTrail::BackendFactory.select(MACTTSB::Empty) + end + + def test_rejects_empty_params + assert_raises(ArgumentError) { AwsCloudTrailTrail.new } + end + + def test_accepts_trail_name_as_scalar + AwsCloudTrailTrail.new('test-trail-1') + end + + def test_accepts_trail_name_as_hash + AwsCloudTrailTrail.new(trail_name: 'test-trail-1') + end + + def test_rejects_unrecognized_params + assert_raises(ArgumentError) { AwsCloudTrailTrail.new(shoe_size: 9) } + end +end + + +#=============================================================================# +# Search / Recall +#=============================================================================# +class AwsCloudTrailTrailRecallTest < Minitest::Test + + def setup + AwsCloudTrailTrail::BackendFactory.select(MACTTSB::Basic) + end + + def test_search_hit_via_scalar_works + assert AwsCloudTrailTrail.new('test-trail-1').exists? + end + + def test_search_hit_via_hash_works + assert AwsCloudTrailTrail.new(trail_name: 'test-trail-1').exists? + end + + def test_search_miss_is_not_an_exception + refute AwsCloudTrailTrail.new(trail_name: 'non-existant').exists? + end +end + +#=============================================================================# +# Properties +#=============================================================================# +class AwsCloudTrailTrailPropertiesTest < Minitest::Test + + def setup + AwsCloudTrailTrail::BackendFactory.select(MACTTSB::Basic) + end + + def test_property_s3_bucket_name + assert_equal('aws-s3-bucket-test-trail-1', AwsCloudTrailTrail.new('test-trail-1').s3_bucket_name) + assert_nil(AwsCloudTrailTrail.new(trail_name: 'non-existant').s3_bucket_name) + end + + def test_property_trail_arn + assert_equal("arn:aws:cloudtrail:us-east-1::trail/test-trail-1", AwsCloudTrailTrail.new('test-trail-1').trail_arn) + assert_nil(AwsCloudTrailTrail.new(trail_name: 'non-existant').trail_arn) + end + + def test_property_cloud_watch_logs_role_arn + assert_equal("arn:aws:iam:::role/CloudTrail_CloudWatchLogs_Role", AwsCloudTrailTrail.new('test-trail-1').cloud_watch_logs_role_arn) + assert_nil(AwsCloudTrailTrail.new(trail_name: 'non-existant').cloud_watch_logs_role_arn) + end + + def test_property_cloud_watch_logs_log_group_arn + assert_equal("arn:aws:logs:us-east-1::log-group:test:*", AwsCloudTrailTrail.new('test-trail-1').cloud_watch_logs_log_group_arn) + assert_nil(AwsCloudTrailTrail.new(trail_name: 'non-existant').cloud_watch_logs_log_group_arn) + end + + def test_property_kms_key_id + assert_equal("arn:aws:kms:us-east-1::key/88197884-041f-4f8e-a801-cf120e4845a8", AwsCloudTrailTrail.new('test-trail-1').kms_key_id) + assert_nil(AwsCloudTrailTrail.new(trail_name: 'non-existant').kms_key_id) + end + + def test_property_home_region + assert_equal("us-east-1", AwsCloudTrailTrail.new('test-trail-1').home_region) + assert_nil(AwsCloudTrailTrail.new(trail_name: 'non-existant').home_region) + end +end + + +#=============================================================================# +# Matchers +#=============================================================================# +class AwsCloudTrailTrailMatchersTest < Minitest::Test + + def setup + AwsCloudTrailTrail::BackendFactory.select(MACTTSB::Basic) + end + + def test_matcher_encrypted_positive + assert AwsCloudTrailTrail.new('test-trail-1').encrypted? + end + + def test_matcher_encrypted_negative + refute AwsCloudTrailTrail.new('test-trail-2').encrypted? + end + + def test_matcher_multi_region_trail_positive + assert AwsCloudTrailTrail.new('test-trail-1').multi_region_trail? + end + + def test_matcher_multi_region_trail_negative + refute AwsCloudTrailTrail.new('test-trail-2').multi_region_trail? + end + + def test_matcher_log_file_validation_enabled_positive + assert AwsCloudTrailTrail.new('test-trail-1').log_file_validation_enabled? + end + + def test_matcher_log_file_validation_enabled_negative + refute AwsCloudTrailTrail.new('test-trail-2').log_file_validation_enabled? + end +end + +#=============================================================================# +# Test Fixtures +#=============================================================================# +module MACTTSB + class Empty < AwsCloudTrailTrail::Backend + def describe_trails(query) + OpenStruct.new(trail_list: []) + end + end + + class Basic < AwsCloudTrailTrail::Backend + def describe_trails(query) + fixtures = [ + OpenStruct.new({ + name: "test-trail-1", + s3_bucket_name: "aws-s3-bucket-test-trail-1", + is_multi_region_trail: true, + home_region: "us-east-1", + trail_arn: "arn:aws:cloudtrail:us-east-1::trail/test-trail-1", + log_file_validation_enabled: true, + cloud_watch_logs_log_group_arn: "arn:aws:logs:us-east-1::log-group:test:*", + cloud_watch_logs_role_arn: "arn:aws:iam:::role/CloudTrail_CloudWatchLogs_Role", + kms_key_id: "arn:aws:kms:us-east-1::key/88197884-041f-4f8e-a801-cf120e4845a8" + }), + OpenStruct.new({ + name: "test-trail-2", + s3_bucket_name: "aws-s3-bucket-test-trail-2", + home_region: "us-east-1", + trail_arn: "arn:aws:cloudtrail:us-east-1::trail/test-trail-2", + cloud_watch_logs_log_group_arn: "arn:aws:logs:us-east-1::log-group:test:*", + cloud_watch_logs_role_arn: "arn:aws:iam:::role/CloudTrail_CloudWatchLogs_Role", + }), + ] + + selected = fixtures.detect do |fixture| + fixture.name == query[:trail_name_list].first + end + OpenStruct.new({ trail_list: [selected] }) + end + end +end \ No newline at end of file diff --git a/test/unit/resources/aws_cloudtrail_trails_test.rb b/test/unit/resources/aws_cloudtrail_trails_test.rb new file mode 100644 index 0000000..e4fcc5d --- /dev/null +++ b/test/unit/resources/aws_cloudtrail_trails_test.rb @@ -0,0 +1,110 @@ +require 'helper' +require 'aws_cloudtrail_trails' + +# MACTTPB = MockAwsCloudTrailTrailsPluralBackend +# Abbreviation not used outside this file + +#=============================================================================# +# Constructor Tests +#=============================================================================# +class AwsCloudTrailTrailsConstructorTest < Minitest::Test + + def setup + AwsCloudTrailTrails::BackendFactory.select(MACTTPB::Empty) + end + + def test_empty_params_ok + AwsCloudTrailTrails.new + end + + def test_rejects_unrecognized_params + assert_raises(ArgumentError) { AwsCloudTrailTrails.new(shoe_size: 9) } + end +end + + +#=============================================================================# +# Search / Recall +#=============================================================================# +class AwsCloudTrailTrailsRecallEmptyTest < Minitest::Test + + def setup + AwsCloudTrailTrails::BackendFactory.select(MACTTPB::Empty) + end + + def test_search_miss_trail_empty_trail_list + refute AwsCloudTrailTrails.new.exists? + end +end + +class AwsCloudTrailTrailsRecallBasicTest < Minitest::Test + + def setup + AwsCloudTrailTrails::BackendFactory.select(MACTTPB::Basic) + end + + def test_search_hit_via_empty_filter + assert AwsCloudTrailTrails.new.exists? + end +end + +#=============================================================================# +# Properties +#=============================================================================# +class AwsCloudTrailTrailsProperties < Minitest::Test + def setup + AwsCloudTrailTrails::BackendFactory.select(MACTTPB::Basic) + end + + def test_property_names + basic = AwsCloudTrailTrails.new + assert_kind_of(Array, basic.names) + assert(basic.names.include?('test-trail-1')) + refute(basic.names.include?(nil)) + end + + def test_property_trail_arns + basic = AwsCloudTrailTrails.new + assert_kind_of(Array, basic.trail_arns) + assert(basic.trail_arns.include?('arn:aws:cloudtrail:us-east-1::trail/test-trail-1')) + refute(basic.trail_arns.include?(nil)) + end +end +#=============================================================================# +# Test Fixtures +#=============================================================================# +module MACTTPB + class Empty < AwsCloudTrailTrails::Backend + def describe_trails(query = {}) + OpenStruct.new({ trail_list: [] }) + end + end + + class Basic < AwsCloudTrailTrails::Backend + def describe_trails(query = {}) + fixtures = [ + OpenStruct.new({ + name: "test-trail-1", + s3_bucket_name: "aws-s3-bucket-test-trail-1", + is_multi_region_trail: true, + home_region: "us-east-1", + trail_arn: "arn:aws:cloudtrail:us-east-1::trail/test-trail-1", + log_file_validation_enabled: true, + cloud_watch_logs_log_group_arn: "arn:aws:logs:us-east-1::log-group:test:*", + cloud_watch_logs_role_arn: "arn:aws:iam:::role/CloudTrail_CloudWatchLogs_Role", + kms_key_id: "arn:aws:kms:us-east-1::key/88197884-041f-4f8e-a801-cf120e4845a8" + }), + OpenStruct.new({ + name: "test-trail-2", + s3_bucket_name: "aws-s3-bucket-test-trail-2", + home_region: "us-east-1", + trail_arn: "arn:aws:cloudtrail:us-east-1::trail/test-trail-2", + cloud_watch_logs_log_group_arn: "arn:aws:logs:us-east-1::log-group:test:*", + cloud_watch_logs_role_arn: "arn:aws:iam:::role/CloudTrail_CloudWatchLogs_Role", + }), + ] + + OpenStruct.new({ trail_list: fixtures }) + end + end +end diff --git a/test/unit/resources/aws_cloudwatch_alarm_test.rb b/test/unit/resources/aws_cloudwatch_alarm_test.rb new file mode 100644 index 0000000..947bb28 --- /dev/null +++ b/test/unit/resources/aws_cloudwatch_alarm_test.rb @@ -0,0 +1,167 @@ +require 'ostruct' +require 'helper' +require 'aws_cloudwatch_alarm' + +# MCWAB = MockCloudwatchAlarmBackend +# Abbreviation not used outside this file + +#=============================================================================# +# Constructor Tests +#=============================================================================# +class AwsCWAConstructor < Minitest::Test + def setup + AwsCloudwatchAlarm::BackendFactory.select(AwsMCWAB::Empty) + end + + def test_constructor_some_args_required + assert_raises(ArgumentError) { AwsCloudwatchAlarm.new } + end + + def test_constructor_accepts_known_resource_params_combos + [ + { metric_name: 'some-val', metric_namespace: 'some-val' }, + ].each do |combo| + AwsCloudwatchAlarm.new(combo) + end + end + + def test_constructor_rejects_bad_resource_params_combos + [ + { metric_name: 'some-val' }, + { metric_namespace: 'some-val' }, + ].each do |combo| + assert_raises(ArgumentError) { AwsCloudwatchAlarm.new(combo) } + end + end + + def test_constructor_reject_unknown_resource_params + assert_raises(ArgumentError) { AwsCloudwatchAlarm.new(beep: 'boop') } + end +end + +#=============================================================================# +# Search / Recall +#=============================================================================# + +class AwsCWARecall < Minitest::Test + def setup + AwsCloudwatchAlarm::BackendFactory.select(AwsMCWAB::Basic) + end + + def test_recall_no_match_is_no_exception + alarm = AwsCloudwatchAlarm.new(metric_name: 'nope', metric_namespace: 'nope') + refute alarm.exists? + end + + def test_recall_match_single_result_works + alarm = AwsCloudwatchAlarm.new( + metric_name: 'metric-01', + metric_namespace: 'metric-namespace-01', + ) + assert alarm.exists? + end + + def test_recall_multiple_result_raises + assert_raises(RuntimeError) do + AwsCloudwatchAlarm.new( + metric_name: 'metric-02', + metric_namespace: 'metric-namespace-01', + ) + end + end +end + +#=============================================================================# +# Properties +#=============================================================================# + +class AwsCWAProperties < Minitest::Test + def setup + AwsCloudwatchAlarm::BackendFactory.select(AwsMCWAB::Basic) + end + + #--------------------------------------- + # alarm_actions + #--------------------------------------- + def test_prop_actions_empty + alarm = AwsCloudwatchAlarm.new( + metric_name: 'metric-02', + metric_namespace: 'metric-namespace-02', + ) + assert_kind_of Array, alarm.alarm_actions + assert_empty alarm.alarm_actions + end + + def test_prop_actions_hit + alarm = AwsCloudwatchAlarm.new( + metric_name: 'metric-01', + metric_namespace: 'metric-namespace-01', + ) + assert_kind_of Array, alarm.alarm_actions + refute_empty alarm.alarm_actions + assert_kind_of String, alarm.alarm_actions.first + end +end + +#=============================================================================# +# Test Fixtures +#=============================================================================# + +module AwsMCWAB + class Empty < AwsCloudwatchAlarm::Backend + def describe_alarms_for_metric(_criteria) + OpenStruct.new({ + metric_alarms: [], + }) + end + end + + class Basic < AwsCloudwatchAlarm::Backend + def describe_alarms_for_metric(criteria) + OpenStruct.new({ + metric_alarms: [ # rubocop:disable Metrics/BlockLength + # Each one here is an alarm that is subscribed to the given metric + # Each has an enormous number of properties, most omitted here + # http://docs.aws.amazon.com/sdkforruby/api/Aws/CloudWatch/Client.html#describe_alarms_for_metric-instance_method + OpenStruct.new({ + alarm_name: 'alarm-01', + metric_name: 'metric-01', + namespace: 'metric-namespace-01', + statistic: 'SampleCount', + alarm_actions: [ + 'arn::::' # TODO: get SNS ARN format + ], + }), + OpenStruct.new({ + # Alarm 02 and 03 both watch metric-01, metric-namespace-01 + alarm_name: 'alarm-02', + metric_name: 'metric-02', + namespace: 'metric-namespace-01', + statistic: 'SampleCount', + alarm_actions: [], + }), + OpenStruct.new({ + # Alarm 02 and 03 both watch metric-02, metric-namespace-01 + alarm_name: 'alarm-03', + metric_name: 'metric-02', + namespace: 'metric-namespace-01', + statistic: 'SampleCount', + alarm_actions: [], + }), + OpenStruct.new({ + alarm_name: 'alarm-04', + metric_name: 'metric-02', + namespace: 'metric-namespace-02', + statistic: 'SampleCount', + alarm_actions: [], + }), + ].select do |alarm| + criteria.keys.all? do |criterion| + criterion = 'namespace' if criterion == 'metric_namespace' + alarm[criterion] == criteria[criterion] + end + end, + }) + end + end +end diff --git a/test/unit/resources/aws_cloudwatch_log_metric_filter_test.rb b/test/unit/resources/aws_cloudwatch_log_metric_filter_test.rb new file mode 100644 index 0000000..dbe5d2f --- /dev/null +++ b/test/unit/resources/aws_cloudwatch_log_metric_filter_test.rb @@ -0,0 +1,152 @@ +require 'ostruct' +require 'helper' +require 'aws_cloudwatch_log_metric_filter' + +# CWLMF = CloudwatchLogMetricFilter +# Abbreviation not used outside this file + +#=============================================================================# +# Constructor Tests +#=============================================================================# +class AwsCWLMFConstructor < Minitest::Test + def setup + AwsCloudwatchLogMetricFilter::BackendFactory.select(AwsMockCWLMFBackend::Empty) + end + + def test_constructor_some_args_required + assert_raises(ArgumentError) { AwsCloudwatchLogMetricFilter.new } + end + + def test_constructor_accepts_known_resource_params + [ + :filter_name, + :pattern, + :log_group_name, + ].each do |resource_param| + AwsCloudwatchLogMetricFilter.new(resource_param => 'some_val') + end + end + + def test_constructor_reject_bad_resource_params + assert_raises(ArgumentError) { AwsCloudwatchLogMetricFilter.new(i_am_a_martian: 'beep') } + end +end + +#=============================================================================# +# Search Tests # +#=============================================================================# +class AwsCWLMFSearch < Minitest::Test + def setup + # Reset to the Basic kit each time + AwsCloudwatchLogMetricFilter::BackendFactory.select(AwsMockCWLMFBackend::Basic) + end + + def test_using_lg_and_lmf_name_when_exactly_one + lmf = AwsCloudwatchLogMetricFilter.new( + log_group_name: 'test-log-group-01', + filter_name: 'test-01', + ) + assert lmf.exists? + end + + def test_using_lg_and_lmf_name_when_not_present + lmf = AwsCloudwatchLogMetricFilter.new( + log_group_name: 'test-log-group-01', + filter_name: 'test-1000-nope', + ) + refute lmf.exists? + end + + def test_using_log_group_name_resulting_in_duplicates + assert_raises(RuntimeError) do + AwsCloudwatchLogMetricFilter.new( + log_group_name: 'test-log-group-01', + ) + end + end + + def test_duplicate_locally_uniqued_using_pattern + lmf = AwsCloudwatchLogMetricFilter.new( + log_group_name: 'test-log-group-01', + pattern: 'INFO', + ) + assert lmf.exists? + end +end +#=============================================================================# +# Property Tests # +#=============================================================================# +class AwsCWLMFProperties < Minitest::Test + def setup + # Reset to the Basic kit each time + AwsCloudwatchLogMetricFilter::BackendFactory.select(AwsMockCWLMFBackend::Basic) + end + + def test_property_values + lmf = AwsCloudwatchLogMetricFilter.new( + log_group_name: 'test-log-group-01', + filter_name: 'test-01', + ) + assert_equal('ERROR', lmf.pattern) + assert_equal('alpha', lmf.metric_name) + assert_equal('awesome_metrics', lmf.metric_namespace) + end +end + +#=============================================================================# +# Support Classes - Mock Data Providers # +#=============================================================================# +class AwsMockCWLMFBackend + class Empty < AwsCloudwatchLogMetricFilter::Backend + def describe_metric_filters(_criteria) + [] + end + end + class Basic < AwsCloudwatchLogMetricFilter::Backend + def describe_metric_filters(criteria) # rubocop:disable Metrics/MethodLength + everything = [ + OpenStruct.new({ + filter_name: 'test-01', + filter_pattern: 'ERROR', + log_group_name: 'test-log-group-01', + metric_transformations: [ + OpenStruct.new({ + metric_name: 'alpha', + metric_namespace: 'awesome_metrics', + }), + ], + }), + OpenStruct.new({ + filter_name: 'test-01', # Intentional duplicate + filter_pattern: 'ERROR', + log_group_name: 'test-log-group-02', + metric_transformations: [ + OpenStruct.new({ + metric_name: 'beta', + metric_namespace: 'awesome_metrics', + }), + ], + }), + OpenStruct.new({ + filter_name: 'test-03', + filter_pattern: 'INFO', + log_group_name: 'test-log-group-01', + metric_transformations: [ + OpenStruct.new({ + metric_name: 'gamma', + metric_namespace: 'awesome_metrics', + }), + ], + }), + ] + selection = everything + # Here we filter on anything the AWS SDK lets us filter on remotely + # - which notably does not include the 'pattern' criteria + [:log_group_name, :filter_name].each do |remote_filter| + next unless criteria.key?(remote_filter) + selection.select! { |lmf| lmf[remote_filter] == criteria[remote_filter] } + end + selection + end + end +end diff --git a/test/unit/resources/aws_ec2_instance_test.rb b/test/unit/resources/aws_ec2_instance_test.rb new file mode 100644 index 0000000..70b0ecf --- /dev/null +++ b/test/unit/resources/aws_ec2_instance_test.rb @@ -0,0 +1,118 @@ +require 'helper' +require 'aws_ec2_instance' + +class TestEc2 < Minitest::Test + Id = 'instance-id'.freeze + InstanceProfile = 'instance-role'.freeze + Arn = 'arn:aws:iam::123456789012:instance-profile/instance-role'.freeze + + def setup + @mock_conn = Minitest::Mock.new + @mock_client = Minitest::Mock.new + @mock_resource = Minitest::Mock.new + @mock_iam_resource = Minitest::Mock.new + + @mock_conn.expect :ec2_client, @mock_client + @mock_conn.expect :ec2_resource, @mock_resource + @mock_conn.expect :iam_resource, @mock_iam_resource + end + + def test_that_id_returns_id_directly_when_constructed_with_an_id + assert_equal Id, AwsEc2Instance.new(Id, @mock_conn).id + end + + def test_that_id_returns_fetched_id_when_constructed_with_a_name + mock_instance = Minitest::Mock.new + mock_instance.expect :nil?, false + mock_instance.expect :id, Id + @mock_resource.expect :instances, [mock_instance], [Hash] + assert_equal Id, AwsEc2Instance.new({ name: 'cut' }, @mock_conn).id + end + + def test_that_instance_returns_instance_when_instance_exists + mock_instance = Object.new + + @mock_resource.expect :instance, mock_instance, [Id] + assert_same( + mock_instance, + AwsEc2Instance.new(Id, @mock_conn).send(:instance), + ) + end + + def test_that_instance_returns_nil_when_instance_does_not_exist + @mock_resource.expect :instance, nil, [Id] + assert AwsEc2Instance.new(Id, @mock_conn).send(:instance).nil? + end + + def test_that_exists_returns_true_when_instance_exists + mock_instance = Minitest::Mock.new + mock_instance.expect :nil?, false + mock_instance.expect :exists?, true + @mock_resource.expect :instance, mock_instance, [Id] + assert AwsEc2Instance.new(Id, @mock_conn).exists? + end + + def test_that_exists_returns_false_when_instance_does_not_exist + mock_instance = Minitest::Mock.new + mock_instance.expect :nil?, false + mock_instance.expect :exists?, false + @mock_resource.expect :instance, mock_instance, [Id] + assert !AwsEc2Instance.new(Id, @mock_conn).exists? + end + + def stub_iam_instance_profile + OpenStruct.new({ arn: Arn }) + end + + def stub_instance_profile(roles) + OpenStruct.new({ roles: roles }) + end + + def test_that_has_roles_returns_false_when_roles_is_empty + mock_instance = Minitest::Mock.new + mock_instance.expect :iam_instance_profile, stub_iam_instance_profile + @mock_resource.expect :instance, mock_instance, [Id] + + mock_roles = Minitest::Mock.new + mock_roles.expect :empty?, true + + @mock_iam_resource.expect( + :instance_profile, + stub_instance_profile(mock_roles), + [InstanceProfile], + ) + + refute AwsEc2Instance.new(Id, @mock_conn).has_roles? + end + + def test_that_has_roles_returns_true_when_roles_is_not_empty + mock_instance = Minitest::Mock.new + mock_instance.expect :iam_instance_profile, stub_iam_instance_profile + @mock_resource.expect :instance, mock_instance, [Id] + + mock_roles = Minitest::Mock.new + mock_roles.expect :empty?, false + + @mock_iam_resource.expect( + :instance_profile, + stub_instance_profile(mock_roles), + [InstanceProfile], + ) + + assert AwsEc2Instance.new(Id, @mock_conn).has_roles? + end + + def test_that_has_roles_returns_false_when_roles_does_not_exist + mock_instance = Minitest::Mock.new + mock_instance.expect :iam_instance_profile, stub_iam_instance_profile + @mock_resource.expect :instance, mock_instance, [Id] + + @mock_iam_resource.expect( + :instance_profile, + stub_instance_profile(nil), + [InstanceProfile], + ) + + refute AwsEc2Instance.new(Id, @mock_conn).has_roles? + end +end diff --git a/test/unit/resources/aws_ec2_security_group_test.rb b/test/unit/resources/aws_ec2_security_group_test.rb new file mode 100644 index 0000000..324c633 --- /dev/null +++ b/test/unit/resources/aws_ec2_security_group_test.rb @@ -0,0 +1,121 @@ +require 'ostruct' +require 'helper' +require 'aws_ec2_security_group' + +# MESGSB = MockEc2SecurityGroupSingleBackend +# Abbreviation not used outside this file + +#=============================================================================# +# Constructor Tests +#=============================================================================# +class AwsESGSConstructor < Minitest::Test + def setup + AwsEc2SecurityGroup::BackendFactory.select(AwsMESGSB::Empty) + end + + def test_constructor_no_args_raises + assert_raises(ArgumentError) { AwsEc2SecurityGroup.new } + end + + def test_constructor_accept_scalar_param + AwsEc2SecurityGroup.new('sg-12345678') + end + + def test_constructor_expected_well_formed_args + { + id: 'sg-1234abcd', + group_id: 'sg-1234abcd', + vpc_id: 'vpc-1234abcd', + group_name: 'some-group', + }.each do |param, value| + AwsEc2SecurityGroup.new(param => value) + end + end + + def test_constructor_reject_malformed_args + { + id: 'sg-xyz-123', + group_id: '1234abcd', + vpc_id: 'vpc_1234abcd', + }.each do |param, value| + assert_raises(ArgumentError) { AwsEc2SecurityGroup.new(param => value) } + end + end + + def test_constructor_reject_unknown_resource_params + assert_raises(ArgumentError) { AwsEc2SecurityGroup.new(beep: 'boop') } + end +end + +#=============================================================================# +# Properties +#=============================================================================# + +class AwsESGSConstructor < Minitest::Test + def setup + AwsEc2SecurityGroup::BackendFactory.select(AwsMESGSB::Basic) + end + + def test_property_group_id + assert_equal('sg-12345678', AwsEc2SecurityGroup.new('sg-12345678').group_id) + assert_nil(AwsEc2SecurityGroup.new(group_name: 'my-group').group_id) + end + + def test_property_group_name + assert_equal('beta', AwsEc2SecurityGroup.new('sg-12345678').group_name) + assert_nil(AwsEc2SecurityGroup.new('sg-87654321').group_name) + end + + def test_property_vpc_id + assert_equal('vpc-aaaabbbb', AwsEc2SecurityGroup.new('sg-aaaabbbb').vpc_id) + assert_nil(AwsEc2SecurityGroup.new('sg-87654321').vpc_id) + end + + def test_property_description + assert_equal('Awesome Group', AwsEc2SecurityGroup.new('sg-12345678').description) + assert_nil(AwsEc2SecurityGroup.new('sg-87654321').description) + end + +end + +#=============================================================================# +# Test Fixtures +#=============================================================================# + +module AwsMESGSB + class Empty < AwsEc2SecurityGroup::Backend + def describe_security_groups(_query) + OpenStruct.new({ + security_groups: [], + }) + end + end + + class Basic < AwsEc2SecurityGroup::Backend + def describe_security_groups(query) + fixtures = [ + OpenStruct.new({ + description: 'Some Group', + group_id: 'sg-aaaabbbb', + group_name: 'alpha', + vpc_id: 'vpc-aaaabbbb', + }), + OpenStruct.new({ + description: 'Awesome Group', + group_id: 'sg-12345678', + group_name: 'beta', + vpc_id: 'vpc-12345678', + }), + ] + + selected = fixtures.select do |sg| + query[:filters].all? do |filter| + filter[:values].include?(sg[filter[:name].tr('-','_')]) + end + end + + OpenStruct.new({ security_groups: selected }) + end + end + +end diff --git a/test/unit/resources/aws_ec2_security_groups_test.rb b/test/unit/resources/aws_ec2_security_groups_test.rb new file mode 100644 index 0000000..c977aca --- /dev/null +++ b/test/unit/resources/aws_ec2_security_groups_test.rb @@ -0,0 +1,105 @@ +require 'ostruct' +require 'helper' +require 'aws_ec2_security_groups' + +# MESGB = MockEc2SecurityGroupBackend +# Abbreviation not used outside this file + +#=============================================================================# +# Constructor Tests +#=============================================================================# +class AwsESGConstructor < Minitest::Test + def setup + AwsEc2SecurityGroups::BackendFactory.select(AwsMESGB::Empty) + end + + def test_constructor_no_args_ok + AwsEc2SecurityGroups.new + end + + def test_constructor_reject_unknown_resource_params + assert_raises(ArgumentError) { AwsEc2SecurityGroups.new(beep: 'boop') } + end +end + +#=============================================================================# +# Filter Criteria +#=============================================================================# +class AwsESGFilterCriteria < Minitest::Test + def setup + AwsEc2SecurityGroups::BackendFactory.select(AwsMESGB::Basic) + end + + def test_filter_vpc_id + hit = AwsEc2SecurityGroups.new.where(vpc_id: 'vpc-12345678') + assert(hit.exists?) + + miss = AwsEc2SecurityGroups.new.where(vpc_id: 'vpc-87654321') + refute(miss.exists?) + end + + def test_filter_group_name + hit = AwsEc2SecurityGroups.new.where(group_name: 'alpha') + assert(hit.exists?) + + miss = AwsEc2SecurityGroups.new.where(group_name: 'nonesuch') + refute(miss.exists?) + end + +end + +#=============================================================================# +# Properties +#=============================================================================# +class AwsESGProperties < Minitest::Test + def setup + AwsEc2SecurityGroups::BackendFactory.select(AwsMESGB::Basic) + end + + def test_property_group_ids + basic = AwsEc2SecurityGroups.new + assert_kind_of(Array, basic.group_ids) + assert(basic.group_ids.include?('sg-aaaabbbb')) + refute(basic.group_ids.include?(nil)) + end +end + +#=============================================================================# +# Test Fixtures +#=============================================================================# + +module AwsMESGB + class Empty < AwsEc2SecurityGroups::Backend + def describe_security_groups(_query) + OpenStruct.new({ + security_groups: [], + }) + end + end + + class Basic < AwsEc2SecurityGroups::Backend + def describe_security_groups(query) + fixtures = [ + OpenStruct.new({ + group_id: 'sg-aaaabbbb', + group_name: 'alpha', + vpc_id: 'vpc-aaaabbbb', + }), + OpenStruct.new({ + group_id: 'sg-12345678', + group_name: 'beta', + vpc_id: 'vpc-12345678', + }), + ] + + selected = fixtures.select do |sg| + query.keys.all? do |criterion| + query[criterion] == sg[criterion] + end + end + + OpenStruct.new({ security_groups: selected }) + end + end + +end diff --git a/test/unit/resources/aws_iam_access_key_test.rb b/test/unit/resources/aws_iam_access_key_test.rb new file mode 100644 index 0000000..b07f4c7 --- /dev/null +++ b/test/unit/resources/aws_iam_access_key_test.rb @@ -0,0 +1,287 @@ +# author: Chris Redekop + +require 'helper' +require 'aws_iam_access_key' + +class AwsIamAccessKeyTest < Minitest::Test + Username = 'test'.freeze + Id = 'id'.freeze + Date = 'date'.freeze + + module AccessKeyFactory + def aws_iam_access_key(decorator = mock_decorator(stub_access_key)) + AwsIamAccessKey.new({ username: Username, id: Id }, decorator) + end + + def stub_access_key( + id: Id, + status: 'Active', + create_date: Date + ) + OpenStruct.new( + { + nil?: nil, + access_key_id: id, + status: status, + create_date: create_date, + }, + ) + end + end + + include AccessKeyFactory + + def test_initialize_accepts_fields + assert_equal( + Id, + AwsIamAccessKey.new({ id: Id, username: Username }, nil) + .instance_variable_get('@id'), + ) + end + + def test_initialize_accepts_access_key + assert_equal( + Id, + AwsIamAccessKey.new( + { + access_key: OpenStruct.new(access_key_id: Id), + }, nil + ).instance_variable_get('@id'), + ) + end + + def test_initialize_prefers_access_key + assert_equal( + Id, + AwsIamAccessKey.new( + { + id: 'foo', + access_key: OpenStruct.new(access_key_id: Id), + }, nil + ).instance_variable_get('@id'), + ) + end + + def test_exists_returns_true_when_access_key_exists + assert aws_iam_access_key.exists? + end + + def test_exists_returns_false_when_sdk_raises + mock_decorator = mock_decorator_raise( + Aws::IAM::Errors::NoSuchEntity.new(nil, nil), + ) + + refute aws_iam_access_key(mock_decorator).exists? + + mock_decorator.verify + end + + def test_exists_returns_false_when_access_key_does_not_exist + mock_decorator = mock_decorator_raise( + AwsIamAccessKey::AccessKeyNotFoundError.new, + ) + + refute aws_iam_access_key(mock_decorator).exists? + + mock_decorator.verify + end + + def test_id_returns_id_when_access_key_exists + assert_equal Id, aws_iam_access_key.id + end + + def test_active_returns_true_when_access_key_is_active + assert aws_iam_access_key.active? + end + + def test_active_returns_false_when_access_key_is_not_active + refute aws_iam_access_key(mock_decorator(stub_access_key(status: 'Foo'))) + .active? + end + + def test_create_date_returns_create_date_always + assert_equal Date, aws_iam_access_key.create_date + end + + def test_last_used_date_returns_last_used_date_always + assert_equal( + Date, + aws_iam_access_key( + mock_decorator( + nil, + OpenStruct.new({ last_used_date: Date }), + ), + ).last_used_date, + ) + end + + class IamClientDecoratorTest < Minitest::Test + include AccessKeyFactory + + def test_get_access_key_raises_when_no_access_keys_found + validator = mock_validator + + e = assert_raises AwsIamAccessKey::AccessKeyNotFoundError do + iam_client_decorator(validator).get_access_key(Username, Id) + end + + assert_match(/.*access key not found.*/, e.message) + assert_match(/.*#{Username}.*/, e.message) + assert_match(/.*#{Id}.*/, e.message) + + validator.verify + end + + def test_get_access_key_raises_when_matching_access_key_not_found + validator = mock_validator + + e = assert_raises AwsIamAccessKey::AccessKeyNotFoundError do + iam_client_decorator( + validator, + [stub_access_key(id: 'Foo')], + ).get_access_key(Username, Id) + end + + assert_match(/.*access key not found.*/, e.message) + assert_match(/.*#{Username}.*/, e.message) + assert_match(/.*#{Id}.*/, e.message) + + validator.verify + end + + def test_get_access_key_returns_access_key_when_access_key_found + access_key = stub_access_key + validator = mock_validator + + assert_equal( + access_key, + iam_client_decorator( + validator, + [access_key], + ).get_access_key(Username, Id), + ) + + validator.verify + end + + def test_get_access_key_last_used_returns_last_used_when_last_used_found + access_key_last_used = Object.new + validator = mock_validator(false) + + assert_equal( + access_key_last_used, + iam_client_decorator( + validator, + nil, + access_key_last_used, + ).get_access_key_last_used(Id), + ) + + validator.verify + end + + class ArgumentValidatorTest < Minitest::Test + def test_validate_id_raises_when_id_is_nil + argument_validator.validate_id(nil) + flunk + rescue ArgumentError => e + assert_match(/.*missing.*"id".*/, e.message) + end + + def test_validate_id_does_nothing_when_id_is_not_nil + argument_validator.validate_id(Id) + end + + def test_validate_username_raises_when_username_is_nil + argument_validator.validate_username(nil) + flunk + rescue ArgumentError => e + assert_match(/.*missing.*"username".*/, e.message) + end + + def test_validate_username_does_nothing_when_username_is_not_nil + argument_validator.validate_username(Username) + end + + def argument_validator + AwsIamAccessKey::IamClientDecorator::ArgumentValidator.new + end + end + + def mock_validator(validate_username = true) + mock_validator = Minitest::Mock.new.expect :validate_id, nil, [Id] + + if validate_username + mock_validator.expect :validate_username, nil, [Username] + end + + mock_validator + end + + def mock_conn(access_keys, access_key_last_used = nil) + Minitest::Mock.new.expect( + :iam_client, + mock_client(access_keys, access_key_last_used), + ) + end + + def mock_client(access_keys, access_key_last_used) + mock_iam_client = Minitest::Mock.new + + if access_keys + mock_iam_client.expect( + :list_access_keys, + OpenStruct.new({ 'access_key_metadata' => access_keys }), + [{ user_name: Username }], + ) + end + + if access_key_last_used + mock_iam_client.expect( + :get_access_key_last_used, + OpenStruct.new({ 'access_key_last_used' => access_key_last_used }), + [{ access_key_id: Id }], + ) + end + + mock_iam_client + end + + def iam_client_decorator( + validator, + access_keys = [], + access_key_last_used = nil + ) + AwsIamAccessKey::IamClientDecorator.new( + validator, mock_conn(access_keys, access_key_last_used) + ) + end + end + + def mock_decorator(access_key, access_key_last_used = nil) + mock_decorator = Minitest::Mock.new + + if access_key + mock_decorator.expect :get_access_key, access_key, [Username, Id] + end + + if access_key_last_used + mock_decorator.expect( + :get_access_key_last_used, + access_key_last_used, + [Id], + ) + end + + mock_decorator + end + + def mock_decorator_raise(error) + Minitest::Mock.new.expect(:get_access_key, nil) do |username, id| + assert_equal Username, username + assert_equal Id, id + + raise error + end + end +end diff --git a/test/unit/resources/aws_iam_access_keys_test.rb b/test/unit/resources/aws_iam_access_keys_test.rb new file mode 100644 index 0000000..15d5fe4 --- /dev/null +++ b/test/unit/resources/aws_iam_access_keys_test.rb @@ -0,0 +1,367 @@ + +require 'aws-sdk' +require 'helper' +require 'aws_iam_access_keys' + +#==========================================================# +# Constructor Tests # +#==========================================================# + +class AwsIamAccessKeysConstructorTest < Minitest::Test + # Reset provider back to the implementation default prior + # to each test. Tests must explicitly select an alternate. + def setup + AwsIamAccessKeys::AccessKeyProvider.reset + end + + def test_bare_constructor_does_not_explode + AwsIamAccessKeys::AccessKeyProvider.select(AlwaysEmptyMAKP) + AwsIamAccessKeys.new + end +end + +#==========================================================# +# Filtering Tests # +#==========================================================# + +class AwsIamAccessKeysFilterTest < Minitest::Test + # Reset provider back to the implementation default prior + # to each test. Tests must explicitly select an alternate. + def setup + AwsIamAccessKeys::AccessKeyProvider.reset + end + + def test_filter_methods_should_exist + AwsIamAccessKeys::AccessKeyProvider.select(AlwaysEmptyMAKP) + resource = AwsIamAccessKeys.new + [:where, :'exists?'].each do |meth| + assert_respond_to(resource, meth) + end + end + + def test_filter_method_where_should_be_chainable + AwsIamAccessKeys::AccessKeyProvider.select(AlwaysEmptyMAKP) + resource = AwsIamAccessKeys.new + assert_respond_to(resource.where, :where) + end + + def test_filter_method_exists_should_probe_empty_when_empty + AwsIamAccessKeys::AccessKeyProvider.select(AlwaysEmptyMAKP) + resource = AwsIamAccessKeys.new + refute(resource.exists?) + end + + def test_filter_method_exists_should_probe_present_when_present + AwsIamAccessKeys::AccessKeyProvider.select(BasicMAKP) + resource = AwsIamAccessKeys.new + assert(resource.exists?) + end +end + +#==========================================================# +# Filter Criteria Tests # +#==========================================================# + +class AwsIamAccessKeysFilterCriteriaTest < Minitest::Test + def setup + # Here we always want no results. + AwsIamAccessKeys::AccessKeyProvider.select(AlwaysEmptyMAKP) + @valued_criteria = { + username: 'bob', + id: 'AKIA1234567890ABCDEF', + access_key_id: 'AKIA1234567890ABCDEF', + } + end + + def test_criteria_when_used_in_constructor_with_value + @valued_criteria.each do |criterion, value| + AwsIamAccessKeys.new(criterion => value) + end + end + + def test_criteria_when_used_in_where_with_value + @valued_criteria.each do |criterion, value| + AwsIamAccessKeys.new.where(criterion => value) + end + end + + # Negative cases + def test_criteria_when_used_in_constructor_with_bad_criterion + assert_raises(RuntimeError) do + AwsIamAccessKeys.new(nope: 'some_val') + end + end + + def test_criteria_when_used_in_where_with_bad_criterion + assert_raises(RuntimeError) do + AwsIamAccessKeys.new(nope: 'some_val') + end + end + + # Identity criterion is allowed based on regex + def test_identity_criterion_when_used_in_constructor_positive + AwsIamAccessKeys.new('AKIA1234567890ABCDEF') + end + + # Permitted by FilterTable? + def test_identity_criterion_when_used_in_where_positive + AwsIamAccessKeys.new.where('AKIA1234567890ABCDEF') + end + + def test_identity_criterion_when_used_in_constructor_negative + assert_raises(RuntimeError) do + AwsIamAccessKeys.new('NopeAKIA1234567890ABCDEF') + end + end + + # Permitted by FilterTable? + # def test_identity_criterion_when_used_in_where_negative + # assert_raises(RuntimeError) do + # AwsIamAccessKeys.new.where('NopeAKIA1234567890ABCDEF') + # end + # end +end + +#==========================================================# +# Property Tests # +#==========================================================# +class AwsIamAccessKeysPropertiesTest < Minitest::Test + def setup + # Reset back to the basic kit each time. + AwsIamAccessKeys::AccessKeyProvider.select(BasicMAKP) + @all_basic = AwsIamAccessKeys.new + end + + #----------------------------------------------------------# + # created_date / created_days_ago / created_hours_ago # + #----------------------------------------------------------# + def test_property_created_date + assert_kind_of(DateTime, @all_basic.entries.first.created_date) + + arg_filtered = @all_basic.where(created_date: DateTime.parse('2017-10-27T17:58:00Z')) + assert_equal(1, arg_filtered.entries.count) + assert arg_filtered.access_key_ids.first.end_with?('BOB') + + block_filtered = @all_basic.where { created_date.friday? } + assert_equal(1, block_filtered.entries.count) + assert block_filtered.access_key_ids.first.end_with?('BOB') + end + + def test_property_created_days_ago + assert_kind_of(Integer, @all_basic.entries.first.created_days_ago) + + arg_filtered = @all_basic.where(created_days_ago: 9) + assert_equal(1, arg_filtered.entries.count) + assert arg_filtered.access_key_ids.first.end_with?('SALLY') + + block_filtered = @all_basic.where { created_days_ago > 2 } + assert_equal(2, block_filtered.entries.count) + end + + def test_property_created_hours_ago + assert_kind_of(Integer, @all_basic.entries.first.created_hours_ago) + + arg_filtered = @all_basic.where(created_hours_ago: 222) + assert_equal(1, arg_filtered.entries.count) + assert arg_filtered.access_key_ids.first.end_with?('SALLY') + + block_filtered = @all_basic.where { created_hours_ago > 100 } + assert_equal(2, block_filtered.entries.count) + end + + #----------------------------------------------------------# + # created_with_user # + #----------------------------------------------------------# + def test_property_created_with_user + assert_kind_of(TrueClass, @all_basic.entries[0].created_with_user) + assert_kind_of(FalseClass, @all_basic.entries[1].created_with_user) + + arg_filtered = @all_basic.where(created_with_user: true) + assert_equal(2, arg_filtered.entries.count) + assert arg_filtered.access_key_ids.first.end_with?('BOB') + + block_filtered = @all_basic.where { created_with_user } + assert_equal(2, block_filtered.entries.count) + end + + #----------------------------------------------------------# + # active / inactive # + #----------------------------------------------------------# + def test_property_active + assert_kind_of(TrueClass, @all_basic.entries.first.active) + + arg_filtered = @all_basic.where(active: true) + assert_equal(2, arg_filtered.entries.count) + + block_filtered = @all_basic.where { active } + assert_equal(2, block_filtered.entries.count) + assert block_filtered.access_key_ids.first.end_with?('BOB') + end + + def test_property_inactive + assert_kind_of(FalseClass, @all_basic.entries.first.inactive) + + arg_filtered = @all_basic.where(inactive: true) + assert_equal(1, arg_filtered.entries.count) + + block_filtered = @all_basic.where { inactive } + assert_equal(1, block_filtered.entries.count) + assert block_filtered.access_key_ids.first.end_with?('ROBIN') + end + + #-----------------------------------------------------------# + # last_used_date / last_used_days_ago / last_used_hours_ago # + #-----------------------------------------------------------# + def test_property_last_used_date + assert_kind_of(NilClass, @all_basic.entries[0].last_used_date) + assert_kind_of(DateTime, @all_basic.entries[1].last_used_date) + + arg_filtered = @all_basic.where(last_used_date: DateTime.parse('2017-10-27T17:58:00Z')) + assert_equal(1, arg_filtered.entries.count) + assert arg_filtered.access_key_ids.first.end_with?('SALLY') + + block_filtered = @all_basic.where { last_used_date and last_used_date.friday? } + assert_equal(1, block_filtered.entries.count) + assert block_filtered.access_key_ids.first.end_with?('SALLY') + end + + def test_property_last_used_days_ago + assert_kind_of(NilClass, @all_basic.entries[0].last_used_days_ago) + assert_kind_of(Integer, @all_basic.entries[1].last_used_days_ago) + + arg_filtered = @all_basic.where(last_used_days_ago: 4) + assert_equal(1, arg_filtered.entries.count) + assert arg_filtered.access_key_ids.first.end_with?('SALLY') + + block_filtered = @all_basic.where { last_used_days_ago and last_used_days_ago < 2 } + assert_equal(1, block_filtered.entries.count) + assert block_filtered.access_key_ids.first.end_with?('ROBIN') + end + + def test_property_last_used_hours_ago + assert_kind_of(NilClass, @all_basic.entries[0].last_used_hours_ago) + assert_kind_of(Integer, @all_basic.entries[1].last_used_hours_ago) + + arg_filtered = @all_basic.where(last_used_hours_ago: 102) + assert_equal(1, arg_filtered.entries.count) + assert arg_filtered.access_key_ids.first.end_with?('SALLY') + + block_filtered = @all_basic.where { last_used_hours_ago and last_used_hours_ago < 10 } + assert_equal(1, block_filtered.entries.count) + assert block_filtered.access_key_ids.first.end_with?('ROBIN') + end + + #-----------------------------------------------------------# + # ever_used / never_used # + #-----------------------------------------------------------# + def test_property_ever_used + assert_kind_of(FalseClass, @all_basic.entries[0].ever_used) + assert_kind_of(TrueClass, @all_basic.entries[1].ever_used) + + arg_filtered = @all_basic.where(ever_used: true) + assert_equal(2, arg_filtered.entries.count) + + block_filtered = @all_basic.where { ever_used } + assert_equal(2, block_filtered.entries.count) + assert block_filtered.access_key_ids.first.end_with?('SALLY') + end + + def test_property_never_used + assert_kind_of(TrueClass, @all_basic.entries[0].never_used) + assert_kind_of(FalseClass, @all_basic.entries[1].never_used) + + arg_filtered = @all_basic.where(never_used: true) + assert_equal(1, arg_filtered.entries.count) + + block_filtered = @all_basic.where { never_used } + assert_equal(1, block_filtered.entries.count) + assert block_filtered.access_key_ids.first.end_with?('BOB') + end + + #----------------------------------------------------------# + # user_created_date # + #----------------------------------------------------------# + def test_property_user_created_date + assert_kind_of(DateTime, @all_basic.entries.first.user_created_date) + arg_filtered = @all_basic.where(user_created_date: DateTime.parse('2017-10-21T17:58:00Z')) + assert_equal(1, arg_filtered.entries.count) + assert arg_filtered.access_key_ids.first.end_with?('SALLY') + + block_filtered = @all_basic.where { user_created_date.saturday? } + assert_equal(1, block_filtered.entries.count) + assert block_filtered.access_key_ids.first.end_with?('SALLY') + end +end +#==========================================================# +# Mock Support Classes # +#==========================================================# + +# MAKP = MockAccessKeyProvider. Abbreviation not used +# outside this file. + +class AlwaysEmptyMAKP < AwsIamAccessKeys::AccessKeyProvider + def fetch(_filter_criteria) + [] + end +end + +class BasicMAKP < AwsIamAccessKeys::AccessKeyProvider + def fetch(_filter_criteria) # rubocop:disable Metrics/MethodLength + [ + { + username: 'bob', + access_key_id: 'AKIA1234567890123BOB', + id: 'AKIA1234567890123BOB', + created_date: DateTime.parse('2017-10-27T17:58:00Z'), + created_days_ago: 4, + created_hours_ago: 102, + created_with_user: true, + status: 'Active', + active: true, + inactive: false, + last_used_date: nil, + last_used_days_ago: nil, + last_used_hours_ago: nil, + ever_used: false, + never_used: true, + user_created_date: DateTime.parse('2017-10-27T17:58:00Z'), + }, + { + username: 'sally', + access_key_id: 'AKIA12345678901SALLY', + id: 'AKIA12345678901SALLY', + created_date: DateTime.parse('2017-10-22T17:58:00Z'), + created_days_ago: 9, + created_hours_ago: 222, + created_with_user: false, + status: 'Active', + active: true, + inactive: false, + last_used_date: DateTime.parse('2017-10-27T17:58:00Z'), + last_used_days_ago: 4, + last_used_hours_ago: 102, + ever_used: true, + never_used: false, + user_created_date: DateTime.parse('2017-10-21T17:58:00Z'), + }, + { + username: 'robin', + access_key_id: 'AKIA12345678901ROBIN', + id: 'AKIA12345678901ROBIN', + created_date: DateTime.parse('2017-10-31T17:58:00Z'), + created_days_ago: 1, + created_hours_ago: 12, + created_with_user: true, + status: 'Inactive', + active: false, + inactive: true, + last_used_date: DateTime.parse('2017-10-31T20:58:00Z'), + last_used_days_ago: 0, + last_used_hours_ago: 5, + ever_used: true, + never_used: false, + user_created_date: DateTime.parse('2017-10-31T17:58:00Z'), + }, + ] + end +end diff --git a/test/unit/resources/aws_iam_password_policy_test.rb b/test/unit/resources/aws_iam_password_policy_test.rb new file mode 100644 index 0000000..37e9dec --- /dev/null +++ b/test/unit/resources/aws_iam_password_policy_test.rb @@ -0,0 +1,83 @@ +require 'helper' +require 'aws_iam_password_policy' + +class AwsIamPasswordPolicyTest < Minitest::Test + def setup + @mock_conn = Minitest::Mock.new + @mock_resource = Minitest::Mock.new + @mock_policy = Minitest::Mock.new + + @mock_conn.expect :iam_resource, @mock_resource + end + + def test_policy_exists_when_policy_exists + @mock_resource.expect :account_password_policy, true + + assert_equal true, AwsIamPasswordPolicy.new(@mock_conn).exists? + end + + def test_policy_does_not_exists_when_no_policy + @mock_resource.expect :account_password_policy, nil do + raise Aws::IAM::Errors::NoSuchEntity.new nil, nil + end + + assert_equal false, AwsIamPasswordPolicy.new(@mock_conn).exists? + end + + def test_throws_when_password_age_0 + @mock_policy.expect :expire_passwords, false + @mock_resource.expect :account_password_policy, @mock_policy + + e = assert_raises Exception do + AwsIamPasswordPolicy.new(@mock_conn).max_password_age + end + + assert_equal e.message, 'this policy does not expire passwords' + end + + def test_prevents_password_reuse_returns_true_when_not_nil + configure_policy_password_reuse_prevention(value: Object.new) + + assert AwsIamPasswordPolicy.new(@mock_conn).prevents_password_reuse? + end + + def test_prevents_password_reuse_returns_false_when_nil + configure_policy_password_reuse_prevention(value: nil) + + refute AwsIamPasswordPolicy.new(@mock_conn).prevents_password_reuse? + end + + def test_number_of_passwords_to_remember_throws_when_nil + configure_policy_password_reuse_prevention(value: nil) + + e = assert_raises Exception do + AwsIamPasswordPolicy.new(@mock_conn).number_of_passwords_to_remember + end + + assert_equal e.message, 'this policy does not prevent password reuse' + end + + def test_number_of_passwords_to_remember_returns_configured_value + expected_value = 5 + configure_policy_password_reuse_prevention(value: expected_value, n: 2) + + assert_equal( + expected_value, + AwsIamPasswordPolicy.new(@mock_conn).number_of_passwords_to_remember, + ) + end + + def test_policy_to_s + configure_policy_password_reuse_prevention(value: Object.new) + expected_value = 'IAM Password-Policy' + test = AwsIamPasswordPolicy.new(@mock_conn).to_s + assert_equal expected_value, test + end + + private + + def configure_policy_password_reuse_prevention(value: value=nil, n: 1) + n.times { @mock_policy.expect :password_reuse_prevention, value } + @mock_resource.expect :account_password_policy, @mock_policy + end +end diff --git a/test/unit/resources/aws_iam_role_test.rb b/test/unit/resources/aws_iam_role_test.rb new file mode 100644 index 0000000..418328d --- /dev/null +++ b/test/unit/resources/aws_iam_role_test.rb @@ -0,0 +1,103 @@ +require 'helper' +require 'aws_iam_role' + +# MIRB = MockIamRoleBackend +# Abbreviation not used outside this file + +#=============================================================================# +# Constructor Tests +#=============================================================================# +class AwsIamRoleConstructorTest < Minitest::Test + def setup + AwsIamRole::BackendFactory.select(AwsMIRB::Basic) + end + + def test_constructor_some_args_required + assert_raises(ArgumentError) { AwsIamRole.new } + end + + def test_constructor_accepts_scalar_role_name + AwsIamRole.new('alpha') + end + + def test_constructor_accepts_role_name_as_hash + AwsIamRole.new(role_name: 'alpha') + end + + def test_constructor_rejects_unrecognized_resource_params + assert_raises(ArgumentError) { AwsIamRole.new(beep: 'boop') } + end +end + +#=============================================================================# +# Search / Recall +#=============================================================================# +class AwsIamRoleRecallTest < Minitest::Test + # No setup here - each test needs to explicitly declare + # what they want from the backend. + + def test_recall_no_match_is_no_exception + AwsIamRole::BackendFactory.select(AwsMIRB::Miss) + refute AwsIamRole.new('nonesuch').exists? + end + + def test_recall_match_single_result_works + AwsIamRole::BackendFactory.select(AwsMIRB::Basic) + assert AwsIamRole.new('alpha').exists? + end +end + + +#=============================================================================# +# Properties +#=============================================================================# + +class AwsIamRolePropertiesTest < Minitest::Test + def setup + AwsIamRole::BackendFactory.select(AwsMIRB::Basic) + end + + #--------------------------------------- + # description + #--------------------------------------- + def test_property_description + assert_equal('alpha role', AwsIamRole.new('alpha').description) + end + + def test_prop_conf_sub_count_zero + assert_empty(AwsIamRole.new('beta').description) + end +end + + +#=============================================================================# +# Test Fixtures +#=============================================================================# +module AwsMIRB + class Miss + def get_role(query) + raise Aws::IAM::Errors::NoSuchEntity.new('Nope', 'Nope') + end + end + + class Basic + def get_role(query) + fixtures = { + 'alpha' => OpenStruct.new({ + role_name: 'alpha', + description: 'alpha role', + }), + 'beta' => OpenStruct.new({ + role_name: 'beta', + description: '', + }), + } + unless fixtures.key?(query[:role_name]) + raise Aws::IAM::Errors::NoSuchEntity.new('Nope', 'Nope') + end + OpenStruct.new({ + role: fixtures[query[:role_name]] + }) + end + end +end \ No newline at end of file diff --git a/test/unit/resources/aws_iam_root_user_test.rb b/test/unit/resources/aws_iam_root_user_test.rb new file mode 100644 index 0000000..a616e15 --- /dev/null +++ b/test/unit/resources/aws_iam_root_user_test.rb @@ -0,0 +1,48 @@ +# author: Miles Tjandrawidjaja +require 'helper' +require 'aws_iam_root_user' + +class AwsIamRootUserTest < Minitest::Test + def setup + @mock_conn = Minitest::Mock.new + @mock_client = Minitest::Mock.new + + @mock_conn.expect :iam_client, @mock_client + end + + def test_has_access_key_returns_true_from_summary_account + test_summary_map = OpenStruct.new( + summary_map: { 'AccountAccessKeysPresent' => 1 }, + ) + @mock_client.expect :get_account_summary, test_summary_map + + assert_equal true, AwsIamRootUser.new(@mock_conn).has_access_key? + end + + def test_has_access_key_returns_false_from_summary_account + test_summary_map = OpenStruct.new( + summary_map: { 'AccountAccessKeysPresent' => 0 }, + ) + @mock_client.expect :get_account_summary, test_summary_map + + assert_equal false, AwsIamRootUser.new(@mock_conn).has_access_key? + end + + def test_has_mfa_enabled_returns_true_when_account_mfa_devices_is_one + test_summary_map = OpenStruct.new( + summary_map: { 'AccountMFAEnabled' => 1 }, + ) + @mock_client.expect :get_account_summary, test_summary_map + + assert_equal true, AwsIamRootUser.new(@mock_conn).has_mfa_enabled? + end + + def test_has_mfa_enabled_returns_false_when_account_mfa_devices_is_zero + test_summary_map = OpenStruct.new( + summary_map: { 'AccountMFAEnabled' => 0 }, + ) + @mock_client.expect :get_account_summary, test_summary_map + + assert_equal false, AwsIamRootUser.new(@mock_conn).has_mfa_enabled? + end +end diff --git a/test/unit/resources/aws_iam_user_test.rb b/test/unit/resources/aws_iam_user_test.rb new file mode 100644 index 0000000..3585b88 --- /dev/null +++ b/test/unit/resources/aws_iam_user_test.rb @@ -0,0 +1,256 @@ +# author: Simon Varlow +require 'helper' +require 'aws_iam_user' + +# MAUIB = MockAwsIamUserBackend +# Abbreviation not used outside this file + +#=============================================================================# +# Constructor Tests +#=============================================================================# +class AwsIamUserConstructorTest < Minitest::Test + + def setup + AwsIamUser::BackendFactory.select(MAIUB::Three) + end + + def test_empty_params_throws_exception + assert_raises(ArgumentError) { AwsIamUser.new } + end + + def test_accepts_username_as_scalar + AwsIamUser.new('erin') + end + + def test_accepts_username_as_hash + AwsIamUser.new(username: 'erin') + end + + def test_rejects_unrecognized_params + assert_raises(ArgumentError) { AwsIamUser.new(shoe_size: 9) } + end +end + +#=============================================================================# +# Search / Recall +#=============================================================================# +class AwsIamUserRecallTest < Minitest::Test + def setup + AwsIamUser::BackendFactory.select(MAIUB::Three) + end + + def test_search_miss_is_not_an_exception + user = AwsIamUser.new('tommy') + refute user.exists? + end + + def test_search_hit_via_scalar_works + user = AwsIamUser.new('erin') + assert user.exists? + assert_equal('erin', user.username) + end + + def test_search_hit_via_hash_works + user = AwsIamUser.new(username: 'erin') + assert user.exists? + assert_equal('erin', user.username) + end +end + +#=============================================================================# +# Properties +#=============================================================================# + +class AwsIamUserPropertiesTest < Minitest::Test + def setup + AwsIamUser::BackendFactory.select(MAIUB::Three) + end + + #-----------------------------------------------------# + # username property + #-----------------------------------------------------# + def test_property_username_correct_on_hit + user = AwsIamUser.new(username: 'erin') + assert_equal('erin', user.username) + end + + #-----------------------------------------------------# + # has_console_password property and predicate + #-----------------------------------------------------# + def test_property_password_positive + user = AwsIamUser.new(username: 'erin') + assert_equal(true, user.has_console_password) + assert_equal(true, user.has_console_password?) + end + + def test_property_password_negative + user = AwsIamUser.new(username: 'leslie') + assert_equal(false, user.has_console_password) + assert_equal(false, user.has_console_password?) + end + + #-----------------------------------------------------# + # has_mfa_enabled property and predicate + #-----------------------------------------------------# + def test_property_mfa_positive + user = AwsIamUser.new(username: 'erin') + assert_equal(true, user.has_mfa_enabled) + assert_equal(true, user.has_mfa_enabled?) + end + + def test_property_mfa_negative + user = AwsIamUser.new(username: 'leslie') + assert_equal(false, user.has_mfa_enabled) + assert_equal(false, user.has_mfa_enabled?) + end + + #-----------------------------------------------------# + # access_keys property + #-----------------------------------------------------# + def test_property_access_keys_positive + keys = AwsIamUser.new(username: 'erin').access_keys + assert_kind_of(Array, keys) + assert_equal(keys.length, 2) + # We don't currently promise that the results + # will be Inspec resource objects. + # assert_kind_of(AwsIamAccessKey, keys.first) + end + + def test_property_access_keys_negative + keys = AwsIamUser.new(username: 'leslie').access_keys + assert_kind_of(Array, keys) + assert(keys.empty?) + end +end + +#=============================================================================# +# Test Fixtures +#=============================================================================# + +module MAIUB + class Three < AwsIamUser::Backend + def get_user(criteria) + people = { + 'erin' => OpenStruct.new({ + user: OpenStruct.new({ + arn: "arn:aws:iam::123456789012:user/erin", + create_date: Time.parse("2016-09-21T23:03:13Z"), + path: "/", + user_id: "AKIAIOSFODNN7EXAERIN", + user_name: "erin", + }), + }), + 'leslie' => OpenStruct.new({ + user: OpenStruct.new({ + arn: "arn:aws:iam::123456789012:user/leslie", + create_date: Time.parse("2017-09-21T23:03:13Z"), + path: "/", + user_id: "AKIAIOSFODNN7EXAERIN", + user_name: "leslie", + }), + }), + 'jared' => OpenStruct.new({ + user: OpenStruct.new({ + arn: "arn:aws:iam::123456789012:user/jared", + create_date: Time.parse("2017-09-21T23:03:13Z"), + path: "/", + user_id: "AKIAIOSFODNN7EXAERIN", + user_name: "jared", + }), + }), + } + raise Aws::IAM::Errors::NoSuchEntity.new(nil, nil) unless people.key?(criteria[:user_name]) + people[criteria[:user_name]] + end + + def get_login_profile(criteria) + # Leslie has no password + # Jared's is expired + people = { + 'erin' => OpenStruct.new({ + login_profile: OpenStruct.new({ + user_name: 'erin', + password_reset_required: false, + create_date: Time.parse("2016-09-21T23:03:13Z"), + }), + }), + 'jared' => OpenStruct.new({ + login_profile: OpenStruct.new({ + user_name: 'jared', + password_reset_required: true, + create_date: Time.parse("2017-09-21T23:03:13Z"), + }), + }), + } + raise Aws::IAM::Errors::NoSuchEntity.new(nil, nil) unless people.key?(criteria[:user_name]) + people[criteria[:user_name]] + end + def list_mfa_devices(criteria) + # Erin has 2, one soft and one hw + # Leslie has none + # Jared has one soft + people = { + 'erin' => OpenStruct.new({ + mfa_devices: [ + OpenStruct.new({ + user_name: 'erin', + serial_number: 'arn:blahblahblah', + enable_date: Time.parse("2016-09-21T23:03:13Z"), + }), + OpenStruct.new({ + user_name: 'erin', + serial_number: '1234567890', + enable_date: Time.parse("2016-09-21T23:03:13Z"), + }), + ] + }), + 'leslie' => OpenStruct.new({mfa_devices: []}), + 'jared' => OpenStruct.new({ + mfa_devices: [ + OpenStruct.new({ + user_name: 'jared', + serial_number: 'arn:blahblahblah', + enable_date: Time.parse("2016-09-21T23:03:13Z"), + }), + ] + }), + } + people[criteria[:user_name]] + end + def list_access_keys(criteria) + # Erin has 2 + # Leslie has none + # Jared has one + people = { + 'erin' => OpenStruct.new({ + access_key_metadata: [ + OpenStruct.new({ + user_name: 'erin', + access_key_id: 'AKIA111111111EXAMPLE', + create_date: Time.parse("2016-09-21T23:03:13Z"), + status: 'Active', + }), + OpenStruct.new({ + user_name: 'erin', + access_key_id: 'AKIA222222222EXAMPLE', + create_date: Time.parse("2016-09-21T23:03:13Z"), + status: 'Active', + }), + ] + }), + 'leslie' => OpenStruct.new({access_key_metadata: []}), + 'jared' => OpenStruct.new({ + access_key_metadata: [ + OpenStruct.new({ + user_name: 'jared', + access_key_id: 'AKIA3333333333EXAMPLE', + create_date: Time.parse("2017-10-21T23:03:13Z"), + status: 'Active', + }), + ] + }), + } + people[criteria[:user_name]] + end + end +end \ No newline at end of file diff --git a/test/unit/resources/aws_iam_users_test.rb b/test/unit/resources/aws_iam_users_test.rb new file mode 100644 index 0000000..f10f32a --- /dev/null +++ b/test/unit/resources/aws_iam_users_test.rb @@ -0,0 +1,152 @@ +require 'helper' +require 'ostruct' +require 'aws_iam_users' + +# Maiusb = Mock AwsIamUsers::Backend +# Abbreviation not used outside of this file + +class AwsIamUsersTestConstructor < Minitest::Test + def setup + AwsIamUsers::Backend.select(Maiusb::Empty) + end + + def test_users_no_params_does_not_explode + AwsIamUsers.new + end + + def test_users_all_params_rejected + assert_raises(ArgumentError) { AwsIamUsers.new(something: 'somevalue') } + end +end + +class AwsIamUsersTestFilterCriteria < Minitest::Test + def setup + # Reset to empty, that's harmless + AwsIamUsers::Backend.select(Maiusb::Empty) + end + + #------------------------------------------# + # Open Filter + #------------------------------------------# + def test_users_empty_result_when_no_users_no_criteria + users = AwsIamUsers.new.where {} + assert users.entries.empty? + end + + def test_users_all_returned_when_some_users_no_criteria + AwsIamUsers::Backend.select(Maiusb::Basic) + users = AwsIamUsers.new.where {} + assert(3, users.entries.count) + end + + #------------------------------------------# + # has_mfa_enabled? + #------------------------------------------# + def test_users_criteria_has_mfa_enabled + AwsIamUsers::Backend.select(Maiusb::Basic) + users = AwsIamUsers.new.where { has_mfa_enabled } + assert(1, users.entries.count) + assert_includes users.entries.map{ |u| u[:user_name] }, 'carol' + refute_includes users.entries.map{ |u| u[:user_name] }, 'alice' + end + + #------------------------------------------# + # has_console_password? + #------------------------------------------# + def test_users_criteria_has_console_password? + AwsIamUsers::Backend.select(Maiusb::Basic) + users = AwsIamUsers.new.where { has_console_password } + assert(2, users.entries.count) + assert_includes users.entries.map{ |u| u[:user_name] }, 'carol' + refute_includes users.entries.map{ |u| u[:user_name] }, 'alice' + end +end + +#=============================================================================# +# Test Fixture Classes +#=============================================================================# +module Maiusb + + # -------------------------------- + # Empty - No users + # -------------------------------- + class Empty < AwsIamUsers::Backend + def list_users + OpenStruct.new({ + users: [] + }) + end + + def get_login_profile(criteria) + raise Aws::IAM::Errors::NoSuchEntity.new("No login profile for #{criteria[:user_name]}", 'Nope') + end + + def list_mfa_devices(_criteria) + OpenStruct.new({ + mfa_devices: [] + }) + end + end + + # -------------------------------- + # Basic - 3 Users + # -------------------------------- + # Alice has no password or MFA device + # Bob has a password but no MFA device + # Carol has a password and MFA device + class Basic < AwsIamUsers::Backend + # arn, path, user_id omitted + def list_users + OpenStruct.new({ + users: [ + OpenStruct.new({ + user_name: 'alice', + create_date: DateTime.parse('2017-10-10T16:19:30Z'), + # Password last used is absent, never logged in w/ password + }), + OpenStruct.new({ + user_name: 'bob', + create_date: DateTime.parse('2017-11-06T16:19:30Z'), + password_last_used: DateTime.parse('2017-11-06T19:19:30Z'), + }), + OpenStruct.new({ + user_name: 'carol', + create_date: DateTime.parse('2017-10-10T16:19:30Z'), + password_last_used: DateTime.parse('2017-10-28T19:19:30Z'), + }), + ] + }) + end + + def get_login_profile(criteria) + if ['bob', 'carol'].include?(criteria[:user_name]) + OpenStruct.new({ + login_profile: OpenStruct.new({ + user_name: criteria[:user_name], + created_date: DateTime.parse('2017-10-10T16:19:30Z') + }) + }) + else + raise Aws::IAM::Errors::NoSuchEntity.new("No login profile for #{criteria[:user_name]}", 'Nope') + end + end + + def list_mfa_devices(criteria) + if ['carol'].include?(criteria[:user_name]) + OpenStruct.new({ + mfa_devices: [ + OpenStruct.new({ + user_name: criteria[:user_name], + serial_number: '1234567890', + enable_date: DateTime.parse('2017-10-10T16:19:30Z'), + }) + ] + }) + else + OpenStruct.new({ + mfa_devices: [] + }) + end + end + end +end diff --git a/test/unit/resources/aws_s3_bucket_test.rb b/test/unit/resources/aws_s3_bucket_test.rb new file mode 100644 index 0000000..d64d51f --- /dev/null +++ b/test/unit/resources/aws_s3_bucket_test.rb @@ -0,0 +1,289 @@ +# encoding: utf-8 +require 'helper' +require 'aws_s3_bucket' + +# MSBSB = MockS3BucketSingleBackend +# Abbreviation not used outside this file + +#=============================================================================# +# Constructor Tests +#=============================================================================# +class AwsS3BucketConstructor < Minitest::Test + def setup + AwsS3Bucket::BackendFactory.select(AwsMSBSB::Basic) + end + + def test_constructor_no_args_raises + assert_raises(ArgumentError) { AwsS3Bucket.new } + end + + def test_constructor_accept_scalar_param + AwsS3Bucket.new('some-bucket') + end + + def test_constructor_accept_hash + AwsS3Bucket.new(bucket_name: 'some-bucket') + end + + def test_constructor_reject_unknown_resource_params + assert_raises(ArgumentError) { AwsS3Bucket.new(bla: 'blabla') } + end +end + +#=============================================================================# +# Search / Recall +#=============================================================================# +class AwsS3BucketPropertiesTest < Minitest::Test + def setup + AwsS3Bucket::BackendFactory.select(AwsMSBSB::Basic) + end + + def test_recall_no_match_is_no_exception + refute AwsS3Bucket.new('NonExistentBucket').exists? + end + + def test_recall_match_single_result_works + assert AwsS3Bucket.new('public').exists? + end + + # No need to handle multiple hits; S3 bucket names are globally unique. +end + +#=============================================================================# +# Properties +#=============================================================================# + +class AwsS3BucketPropertiesTest < Minitest::Test + def setup + AwsS3Bucket::BackendFactory.select(AwsMSBSB::Basic) + end + + #---------------------Bucket Name----------------------------# + def test_property_bucket_name + assert_equal('public', AwsS3Bucket.new('public').bucket_name) + end + + #--------------------- Region ----------------------------# + def test_property_region + assert_equal('us-east-2', AwsS3Bucket.new('public').region) + assert_equal('EU', AwsS3Bucket.new('private').region) + end + + #---------------------- bucket_acl -------------------------------# + def test_property_bucket_acl_structure + bucket_acl = AwsS3Bucket.new('public').bucket_acl + + assert_kind_of(Array, bucket_acl) + assert(bucket_acl.size > 0) + assert(bucket_acl.all? { |g| g.respond_to?(:permission)}) + assert(bucket_acl.all? { |g| g.respond_to?(:grantee)}) + assert(bucket_acl.all? { |g| g.grantee.respond_to?(:type)}) + end + + def test_property_bucket_acl_public + bucket_acl = AwsS3Bucket.new('public').bucket_acl + + public_grants = bucket_acl.select do |g| + g.grantee.type == 'Group' && g.grantee.uri =~ /AllUsers/ + end + refute_empty(public_grants) + end + + def test_property_bucket_acl_private + bucket_acl = AwsS3Bucket.new('private').bucket_acl + + public_grants = bucket_acl.select do |g| + g.grantee.type == 'Group' && g.grantee.uri =~ /AllUsers/ + end + assert_empty(public_grants) + + auth_users_grants = bucket_acl.select do |g| + g.grantee.type == 'Group' && g.grantee.uri =~ /AuthenticatedUsers/ + end + assert_empty(auth_users_grants) + end + + def test_property_bucket_acl_auth_users + bucket_acl = AwsS3Bucket.new('auth-users').bucket_acl + + public_grants = bucket_acl.select do |g| + g.grantee.type == 'Group' && g.grantee.uri =~ /AllUsers/ + end + assert_empty(public_grants) + + auth_users_grants = bucket_acl.select do |g| + g.grantee.type == 'Group' && g.grantee.uri =~ /AuthenticatedUsers/ + end + refute_empty(auth_users_grants) + end + + #---------------------- bucket_policy -------------------------------# + def test_property_bucket_policy_structure + bucket_policy = AwsS3Bucket.new('public').bucket_policy + assert_kind_of(Array, bucket_policy) + assert_kind_of(OpenStruct, bucket_policy.first) + [:effect, :principal, :action, :resource].each do |field| + assert_respond_to(bucket_policy.first, field) + end + end + + def test_property_bucket_policy_public + bucket_policy = AwsS3Bucket.new('public').bucket_policy + allow_all = bucket_policy.select { |s| s.effect == 'Allow' && s.principal == '*' } + assert_equal(1, allow_all.count) + end + + def test_property_bucket_policy_private + bucket_policy = AwsS3Bucket.new('private').bucket_policy + allow_all = bucket_policy.select { |s| s.effect == 'Allow' && s.principal == '*' } + assert_equal(0, allow_all.count) + end + + def test_property_bucket_policy_auth + bucket_policy = AwsS3Bucket.new('auth').bucket_policy + assert_empty(bucket_policy) + end + +end + +#=============================================================================# +# Test Matchers +#=============================================================================# + +class AwsS3BucketPropertiesTest < Minitest::Test + def setup + AwsS3Bucket::BackendFactory.select(AwsMSBSB::Basic) + end + + def test_be_public_public_acl + assert(AwsS3Bucket.new('public').public?) + end + def test_be_public_auth_acl + assert(AwsS3Bucket.new('auth-users').public?) + end + def test_be_public_private_acl + refute(AwsS3Bucket.new('private').public?) + end + def test_be_public_public_acl + assert(AwsS3Bucket.new('public').public?) + end + +end + +#=============================================================================# +# Test Fixtures +#=============================================================================# + +module AwsMSBSB + class Basic < AwsS3Bucket::Backend + def get_bucket_acl(query) + owner_full_control = OpenStruct.new({ + grantee: OpenStruct.new({ + type: 'CanonicalUser', + }), + permission: 'FULL_CONTROL', + }) + + buckets = { + 'public' => OpenStruct.new({ + :grants => [ + owner_full_control, + OpenStruct.new({ + grantee: OpenStruct.new({ + type: 'Group', + uri: 'http://acs.amazonaws.com/groups/global/AllUsers' + }), + permission: 'READ', + }), + ] + }), + 'auth-users' => OpenStruct.new({ + :grants => [ + owner_full_control, + OpenStruct.new({ + grantee: OpenStruct.new({ + type: 'Group', + uri: 'http://acs.amazonaws.com/groups/global/AuthenticatedUsers' + }), + permission: 'READ', + }), + ] + }), + 'private' => OpenStruct.new({ :grants => [ owner_full_control ] }), + 'private-acl-public-policy' => OpenStruct.new({ :grants => [ owner_full_control ] }), + } + buckets[query[:bucket]] + end + + def get_bucket_location(query) + buckets = { + 'public' => OpenStruct.new({ location_constraint: 'us-east-2' }), + 'private' => OpenStruct.new({ location_constraint: 'EU' }), + 'auth-users' => OpenStruct.new({ location_constraint: 'ap-southeast-1' }), + 'private-acl-public-policy' => OpenStruct.new({ location_constraint: 'ap-southeast-2' }), + } + unless buckets.key?(query[:bucket]) + raise Aws::S3::Errors::NoSuchBucket.new(nil, nil) + end + buckets[query[:bucket]] + end + + def get_bucket_policy(query) + buckets = { + 'public' => OpenStruct.new({ + policy: StringIO.new(<<'EOP') +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "AllowGetObject", + "Effect": "Allow", + "Principal": "*", + "Action": "s3:GetObject", + "Resource": "arn:aws:s3:::public/*" + } + ] +} +EOP + }), + 'private' => OpenStruct.new({ + policy: StringIO.new(<<'EOP') +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "DenyGetObject", + "Effect": "Deny", + "Principal": "*", + "Action": "s3:GetObject", + "Resource": "arn:aws:s3:::private/*" + } + ] +} +EOP + }), + 'private-acl-public-policy' => OpenStruct.new({ + policy: StringIO.new(<<'EOP') +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "AllowGetObject", + "Effect": "Allow", + "Principal": "*", + "Action": "s3:GetObject", + "Resource": "arn:aws:s3:::private-acl-public-policy/*" + } + ] +} +EOP + }), + # No policies for auth bucket + } + unless buckets.key?(query[:bucket]) + raise Aws::S3::Errors::NoSuchBucketPolicy.new(nil, nil) + end + buckets[query[:bucket]] + end + end +end diff --git a/test/unit/resources/aws_sns_topic_test.rb b/test/unit/resources/aws_sns_topic_test.rb new file mode 100644 index 0000000..77f5f4b --- /dev/null +++ b/test/unit/resources/aws_sns_topic_test.rb @@ -0,0 +1,125 @@ +require 'helper' +require 'aws_sns_topic' + +# MSNB = MockSnsBackend +# Abbreviation not used outside this file + +#=============================================================================# +# Constructor Tests +#=============================================================================# +class AwsSnsTopicConstructorTest < Minitest::Test + def setup + AwsSnsTopic::BackendFactory.select(AwsMSNB::NoSubscriptions) + end + + def test_constructor_some_args_required + assert_raises(ArgumentError) { AwsSnsTopic.new } + end + + def test_constructor_accepts_scalar_arn + AwsSnsTopic.new('arn:aws:sns:us-east-1:123456789012:some-topic') + end + + def test_constructor_accepts_arn_as_hash + AwsSnsTopic.new(arn: 'arn:aws:sns:us-east-1:123456789012:some-topic') + end + + def test_constructor_rejects_unrecognized_resource_params + assert_raises(ArgumentError) { AwsSnsTopic.new(beep: 'boop') } + end + + def test_constructor_rejects_non_arn_formats + [ + 'not-even-like-an-arn', + 'arn:::::', # Empty + 'arn::::::', # Too many colons + 'arn:aws::us-east-1:123456789012:some-topic', # Omits SNS service + 'arn::sns:us-east-1:123456789012:some-topic', # Omits partition + 'arn:aws:sns:*:123456789012:some-topic', # All-region - not permitted for lookup + 'arn:aws:sns:us-east-1::some-topic', # Default account - not permitted for lookup + ].each do |example| + assert_raises(ArgumentError) { AwsSnsTopic.new(arn: example) } + end + end +end + +#=============================================================================# +# Search / Recall +#=============================================================================# +class AwsSnsTopicRecallTest < Minitest::Test + # No setup here - each test needs to explicitly declare + # what they want from the backend. + + def test_recall_no_match_is_no_exception + AwsSnsTopic::BackendFactory.select(AwsMSNB::Miss) + topic = AwsSnsTopic.new('arn:aws:sns:us-east-1:123456789012:nope') + refute topic.exists? + end + + def test_recall_match_single_result_works + AwsSnsTopic::BackendFactory.select(AwsMSNB::NoSubscriptions) + topic = AwsSnsTopic.new('arn:aws:sns:us-east-1:123456789012:does-not-matter') + assert topic.exists? + end +end + +#=============================================================================# +# Properties +#=============================================================================# + +class AwsSnsTopicPropertiesTest < Minitest::Test + # No setup here - each test needs to explicitly declare + # what they want from the backend. + + #--------------------------------------- + # confirmed_subscription_count + #--------------------------------------- + def test_prop_conf_sub_count_zero + AwsSnsTopic::BackendFactory.select(AwsMSNB::NoSubscriptions) + topic = AwsSnsTopic.new('arn:aws:sns:us-east-1:123456789012:does-not-matter') + assert_equal(0, topic.confirmed_subscription_count) + end + + def test_prop_conf_sub_count_zero + AwsSnsTopic::BackendFactory.select(AwsMSNB::OneSubscription) + topic = AwsSnsTopic.new('arn:aws:sns:us-east-1:123456789012:does-not-matter') + assert_equal(1, topic.confirmed_subscription_count) + end +end + +#=============================================================================# +# Test Fixtures +#=============================================================================# + +module AwsMSNB + + class Miss + def get_topic_attributes(criteria) + raise Aws::SNS::Errors::NotFound.new("No SNS topic for #{criteria[:topic_arn]}", 'Nope') + end + end + + class NoSubscriptions + def get_topic_attributes(_criteria) + OpenStruct.new({ + attributes: { # Note that this is a plain hash, odd for AWS SDK + # Many other attributes available, see + # http://docs.aws.amazon.com/sdkforruby/api/Aws/SNS/Types/GetTopicAttributesResponse.html + "SubscriptionsConfirmed" => 0 + } + }) + end + end + + class OneSubscription + def get_topic_attributes(_criteria) + OpenStruct.new({ + attributes: { # Note that this is a plain hash, odd for AWS SDK + # Many other attributes available, see + # http://docs.aws.amazon.com/sdkforruby/api/Aws/SNS/Types/GetTopicAttributesResponse.html + "SubscriptionsConfirmed" => 1 + } + }) + end + end +end \ No newline at end of file diff --git a/test/unit/resources/aws_vpc.notes b/test/unit/resources/aws_vpc.notes new file mode 100644 index 0000000..5112797 --- /dev/null +++ b/test/unit/resources/aws_vpc.notes @@ -0,0 +1,91 @@ +#=============================================================================# +# Search / Recall +#=============================================================================# +class AwsVpcRecallTest < Minitest::Test + def setup + AwsVpc::BackendFactory.select(MAVSB::Three) + end + + def test_search_miss_is_not_an_exception + user = AwsVpc.new('vpc-87654321') + refute user.exists? + end + + def test_search_hit_via_scalar_works + user = AwsVpc.new('') + assert user.exists? + assert_equal('erin', user.username) + end + + def test_search_hit_via_hash_works + user = AwsVpc.new(username: 'erin') + assert user.exists? + assert_equal('erin', user.username) + end +end + +#=============================================================================# +# Properties +#=============================================================================# + +class AwsVpcPropertiesTest < Minitest::Test + def setup + AwsVpc::BackendFactory.select(MAVSB::Three) + end + + #-----------------------------------------------------# + # username property + #-----------------------------------------------------# + def test_property_username_correct_on_hit + user = AwsVpc.new(username: 'erin') + assert_equal('erin', user.username) + end + + #-----------------------------------------------------# + # has_console_password property and predicate + #-----------------------------------------------------# + def test_property_password_positive + user = AwsVpc.new(username: 'erin') + assert_equal(true, user.has_console_password) + assert_equal(true, user.has_console_password?) + end + + def test_property_password_negative + user = AwsVpc.new(username: 'leslie') + assert_equal(false, user.has_console_password) + assert_equal(false, user.has_console_password?) + end + + #-----------------------------------------------------# + # has_mfa_enabled property and predicate + #-----------------------------------------------------# + def test_property_mfa_positive + user = AwsVpc.new(username: 'erin') + assert_equal(true, user.has_mfa_enabled) + assert_equal(true, user.has_mfa_enabled?) + end + + def test_property_mfa_negative + user = AwsVpc.new(username: 'leslie') + assert_equal(false, user.has_mfa_enabled) + assert_equal(false, user.has_mfa_enabled?) + end + + #-----------------------------------------------------# + # access_keys property + #-----------------------------------------------------# + def test_property_access_keys_positive + keys = AwsVpc.new(username: 'erin').access_keys + assert_kind_of(Array, keys) + assert_equal(keys.length, 2) + # We don't currently promise that the results + # will be Inspec resource objects. + # assert_kind_of(AwsIamAccessKey, keys.first) + end + + def test_property_access_keys_negative + keys = AwsVpc.new(username: 'leslie').access_keys + assert_kind_of(Array, keys) + assert(keys.empty?) + end +end diff --git a/test/unit/resources/aws_vpc_test.rb b/test/unit/resources/aws_vpc_test.rb new file mode 100644 index 0000000..8a067b0 --- /dev/null +++ b/test/unit/resources/aws_vpc_test.rb @@ -0,0 +1,163 @@ +require 'helper' +require 'aws_vpc' + +# MAVSB = MockAwsVpcSingularBackend +# Abbreviation not used outside this file + +#=============================================================================# +# Constructor Tests +#=============================================================================# +class AwsVpcConstructorTest < Minitest::Test + + def setup + AwsVpc::BackendFactory.select(MAVSB::Empty) + end + + def test_empty_params_ok + AwsVpc.new + end + + def test_accepts_vpc_id_as_scalar + AwsVpc.new('vpc-12345678') + end + + def test_accepts_vpc_id_as_hash + AwsVpc.new(vpc_id: 'vpc-1234abcd') + end + + def test_rejects_unrecognized_params + assert_raises(ArgumentError) { AwsVpc.new(shoe_size: 9) } + end + + def test_rejects_invalid_vpc_id + assert_raises(ArgumentError) { AwsVpc.new('vpc-rofl') } + end +end + + +#=============================================================================# +# Search / Recall +#=============================================================================# +class AwsVpcRecallTest < Minitest::Test + + def setup + AwsVpc::BackendFactory.select(MAVSB::Basic) + end + + def test_search_hit_via_default_works + assert AwsVpc.new.exists? + end + + def test_search_hit_via_scalar_works + assert AwsVpc.new('vpc-12344321').exists? + end + + def test_search_hit_via_hash_works + assert AwsVpc.new(vpc_id: 'vpc-12344321').exists? + end + + def test_search_miss_is_not_an_exception + refute AwsVpc.new(vpc_id: 'vpc-00000000').exists? + end +end + +#=============================================================================# +# Properties +#=============================================================================# +class AwsVpcPropertiesTest < Minitest::Test + + def setup + AwsVpc::BackendFactory.select(MAVSB::Basic) + end + + def test_property_vpc_id + assert_equal('vpc-aaaabbbb', AwsVpc.new('vpc-aaaabbbb').vpc_id) + assert_nil(AwsVpc.new(vpc_id: 'vpc-00000000').vpc_id) + end + + def test_property_cidr_block + assert_equal('10.0.0.0/16', AwsVpc.new('vpc-aaaabbbb').cidr_block) + assert_nil(AwsVpc.new('vpc-00000000').cidr_block) + end + + def test_property_dhcp_options_id + assert_equal('dopt-aaaabbbb', AwsVpc.new('vpc-aaaabbbb').dhcp_options_id) + assert_nil(AwsVpc.new('vpc-00000000').dhcp_options_id) + end + + def test_property_state + assert_equal('available', AwsVpc.new('vpc-12344321').state) + assert_nil(AwsVpc.new('vpc-00000000').state) + end + + def test_property_instance_tenancy + assert_equal('default', AwsVpc.new('vpc-12344321').instance_tenancy) + assert_nil(AwsVpc.new('vpc-00000000').instance_tenancy) + end +end + + +#=============================================================================# +# Matchers +#=============================================================================# +class AwsVpcMatchersTest < Minitest::Test + + def setup + AwsVpc::BackendFactory.select(MAVSB::Basic) + end + + def test_matcher_default_positive + assert AwsVpc.new('vpc-aaaabbbb').default? + end + + def test_matcher_default_negative + refute AwsVpc.new('vpc-12344321').default? + end + +end + +#=============================================================================# +# Test Fixtures +#=============================================================================# +module MAVSB + class Empty < AwsVpc::Backend + def describe_vpcs(query) + OpenStruct.new(vpcs: []) + end + end + + class Basic < AwsVpc::Backend + def describe_vpcs(query) + fixtures = [ + OpenStruct.new({ + cidr_block: '10.0.0.0/16', + dhcp_options_id: 'dopt-aaaabbbb', + state: 'available', + vpc_id: 'vpc-aaaabbbb', + instance_tenancy: 'default', + is_default: true + }), + OpenStruct.new({ + cidr_block: '10.1.0.0/16', + dhcp_options_id: 'dopt-43211234', + state: 'available', + vpc_id: 'vpc-12344321', + instance_tenancy: 'default', + is_default: false + }), + ] + + selected = fixtures.select do |vpc| + query[:filters].all? do |filter| + if filter[:name].eql? "isDefault" + filter[:name] = "is_default" + end + filter[:values].include?(vpc[filter[:name].tr('-','_')].to_s) + end + end + + OpenStruct.new({ vpcs: selected }) + end + end + +end diff --git a/test/unit/resources/aws_vpcs_test.rb b/test/unit/resources/aws_vpcs_test.rb new file mode 100644 index 0000000..bd3786f --- /dev/null +++ b/test/unit/resources/aws_vpcs_test.rb @@ -0,0 +1,97 @@ +require 'helper' +require 'aws_vpcs' + +# MAVPB = MockAwsVpcsPluralBackend +# Abbreviation not used outside this file + +#=============================================================================# +# Constructor Tests +#=============================================================================# +class AwsVpcsConstructorTest < Minitest::Test + + def setup + AwsVpcs::BackendFactory.select(MAVPB::Empty) + end + + def test_empty_params_ok + AwsVpcs.new + end + + def test_rejects_unrecognized_params + assert_raises(ArgumentError) { AwsVpcs.new(shoe_size: 9) } + end +end + + +#=============================================================================# +# Search / Recall +#=============================================================================# +class AwsVpcsRecallEmptyTest < Minitest::Test + + def setup + AwsVpcs::BackendFactory.select(MAVPB::Empty) + end + + def test_search_miss_via_empty_vpcs + refute AwsVpcs.new.exists? + end +end + +class AwsVpcsRecallBasicTest < Minitest::Test + + def setup + AwsVpcs::BackendFactory.select(MAVPB::Basic) + end + + def test_search_hit_via_empty_filter + assert AwsVpcs.new.exists? + end +end + +#=============================================================================# +# Test Fixtures +#=============================================================================# +module MAVPB + class Empty < AwsVpcs::Backend + def describe_vpcs(query = {}) + OpenStruct.new({ vpcs: [] }) + end + end + + class Basic < AwsVpcs::Backend + def describe_vpcs(query = {}) + fixtures = [ + OpenStruct.new({ + cidr_block: '10.0.0.0/16', + dhcp_options_id: 'dopt-aaaabbbb', + state: 'available', + vpc_id: 'vpc-aaaabbbb', + instance_tenancy: 'default', + is_default: true + }), + OpenStruct.new({ + cidr_block: '10.1.0.0/16', + dhcp_options_id: 'dopt-43211234', + state: 'available', + vpc_id: 'vpc-12344321', + instance_tenancy: 'default', + is_default: false + }), + ] + + query[:filters] = [] if query[:filters].nil? + + selected = fixtures.select do |vpc| + query[:filters].all? do |filter| + if filter[:name].eql? "isDefault" + filter[:name] = "is_default" + end + filter[:values].include?(vpc[filter[:name].tr('-','_')].to_s) + end + end + + OpenStruct.new({ vpcs: selected }) + end + end + +end diff --git a/tf_build/ec2.tf b/tf_build/ec2.tf deleted file mode 100644 index 7216752..0000000 --- a/tf_build/ec2.tf +++ /dev/null @@ -1,44 +0,0 @@ -resource "random_id" "bucket_id" { - byte_length = 8 -} - -provider "aws" { - version = "=1.1" - access_key = "${var.aws_access_key}" - secret_key = "${var.aws_secret_key}" - region = "${var.aws_region}" -} - -resource "aws_instance" "example" { - ami = "${var.aws_ami_id}" - subnet_id = "${var.aws_subnet_id}" - instance_type = "${var.aws_instance_type}" - key_name = "${var.aws_ssh_key_name}" - vpc_security_group_ids = ["${var.aws_security_group}"] -} - -#============================================================# -# Security Groups -#============================================================# - -# Look up the default VPC and the default security group for it -data "aws_vpc" "default" { - default = "true" -} - -data "aws_security_group" "default" { - vpc_id = "${data.aws_vpc.default.id}" - name = "default" -} - -output "ec2_security_group_default_vpc_id" { - value = "${data.aws_vpc.default.id}" -} - -output "ec2_security_group_default_group_id" { - value = "${data.aws_security_group.default.id}" -} - -# valid ACLs are Error: aws_s3_bucket_object.public: "acl" contains an invalid canned ACL type "public". V -#alid types are either "authenticated-read", "aws-exec-read", "bucket-owner-full-control", "bucket-owner-read", -# "private", "public-read", or "public-read-write" diff --git a/tf_build/output.tf b/tf_build/output.tf deleted file mode 100644 index 6115d8f..0000000 --- a/tf_build/output.tf +++ /dev/null @@ -1,5 +0,0 @@ -output "public_dns" { - value = "${aws_instance.example.public_dns}" -} - -# add vpc_id so we can run the next kitchen run diff --git a/tf_build/s3.tf b/tf_build/s3.tf deleted file mode 100644 index ab8a761..0000000 --- a/tf_build/s3.tf +++ /dev/null @@ -1,49 +0,0 @@ -resource "aws_s3_bucket" "aws_demo_bucket" { - bucket = "aws_demo_s3_bucket-${random_id.bucket_id.hex}" - acl = "public-read" - force_destroy = true - - tags { - Name = "aws_demo_bucket" - Environment = "prod" - } -} - -output "s3_bucket_name" { - value = "aws_demo_s3_bucket-${random_id.bucket_id.hex}" -} - -# add s3 bucket elements - pub - -resource "aws_s3_bucket_object" "public-read" { - bucket = "aws_demo_s3_bucket-${random_id.bucket_id.hex}" - acl = "public-read" - key = "public-pic-read.jpg" - source = "./data/public-pic.jpg" - etag = "${md5(file("./data/public-pic.jpg"))}" - - depends_on = ["aws_s3_bucket.aws_demo_bucket"] -} - -# add s3 bucket elements - pub Authenticated Users only - -resource "aws_s3_bucket_object" "authenticated-read" { - bucket = "aws_demo_s3_bucket-${random_id.bucket_id.hex}" - acl = "authenticated-read" - key = "public-pic-authenticated.jpg" - source = "./data/public-pic.jpg" - etag = "${md5(file("./data/public-pic.jpg"))}" - - depends_on = ["aws_s3_bucket.aws_demo_bucket"] -} -# add s3 bucket elements - pri - -resource "aws_s3_bucket_object" "private" { - bucket = "aws_demo_s3_bucket-${random_id.bucket_id.hex}" - acl = "private" - key = "private-pic.jpg" - source = "./data/private-pic.jpg" - etag = "${md5(file("./data/private-pic.jpg"))}" - - depends_on = ["aws_s3_bucket.aws_demo_bucket"] -} diff --git a/tf_build/variables.tf b/tf_build/variables.tf deleted file mode 100644 index 3f65fba..0000000 --- a/tf_build/variables.tf +++ /dev/null @@ -1,39 +0,0 @@ -variable aws_ssh_key_name { - default = "" - } - -variable aws_access_key { - default = "" - } - -variable aws_secret_key { - default = "" - } - -variable aws_instance_type { - default = "" - } - -variable aws_subnet_id { - default = "" - } - -variable aws_bucket_prefix { - default = "" - } - -variable aws_region { - default = "" - } - -variable aws_ami_id { - default = "" - } - -variable aws_security_group { - default = "" - } - -variable s3_name { - default = "" -} From c6fc2be72a6a76245c8d8ae2792c91c6b0864d75 Mon Sep 17 00:00:00 2001 From: Rony Xavier Date: Wed, 24 Jan 2018 21:29:56 -0500 Subject: [PATCH 02/12] updates Signed-off-by: Rony Xavier --- Rakefile | 4 +- test/integration/cis/build/aws.tf | 21 ++ test/integration/cis/build/cloudtrail.tf | 230 +++++++++++++++++++++ test/integration/cis/build/cloudwatch.tf | 95 +++++++++ test/integration/cis/build/ec2.tf | 199 ++++++++++++++++++ test/integration/cis/build/iam.tf | 90 ++++++++ test/integration/cis/build/inspec-logo.png | Bin 0 -> 8501 bytes test/integration/cis/build/s3.tf | 100 +++++++++ test/integration/cis/build/sns.tf | 37 ++++ test/integration/cis/verify | 1 + 10 files changed, 775 insertions(+), 2 deletions(-) create mode 100644 test/integration/cis/build/aws.tf create mode 100644 test/integration/cis/build/cloudtrail.tf create mode 100644 test/integration/cis/build/cloudwatch.tf create mode 100644 test/integration/cis/build/ec2.tf create mode 100644 test/integration/cis/build/iam.tf create mode 100644 test/integration/cis/build/inspec-logo.png create mode 100644 test/integration/cis/build/s3.tf create mode 100644 test/integration/cis/build/sns.tf create mode 160000 test/integration/cis/verify diff --git a/Rakefile b/Rakefile index 8169744..2ac64b3 100644 --- a/Rakefile +++ b/Rakefile @@ -39,7 +39,7 @@ namespace :test do end namespace :aws do - ['default', 'minimal'].each do |account| + ['default', 'minimal', 'cis'].each do |account| integration_dir = File.join(project_dir, 'test', 'integration', account) attribute_file = File.join(integration_dir, '.attribute.yml') @@ -91,5 +91,5 @@ namespace :test do end end end - task aws: [:'aws:default', :'aws:minimal'] + task aws: [:'aws:default', :'aws:minimal', :'aws:cis'] end \ No newline at end of file diff --git a/test/integration/cis/build/aws.tf b/test/integration/cis/build/aws.tf new file mode 100644 index 0000000..95f0469 --- /dev/null +++ b/test/integration/cis/build/aws.tf @@ -0,0 +1,21 @@ +terraform { + required_version = "~> 0.10.0" +} + +provider "aws" { + version = "= 1.1" +} + +data "aws_caller_identity" "creds" {} + +output "aws_account_id" { + value = "${data.aws_caller_identity.creds.account_id}" +} + +data "aws_region" "region" { + current = true +} + +output "aws_region" { + value = "${data.aws_region.region.name}" +} diff --git a/test/integration/cis/build/cloudtrail.tf b/test/integration/cis/build/cloudtrail.tf new file mode 100644 index 0000000..965db8c --- /dev/null +++ b/test/integration/cis/build/cloudtrail.tf @@ -0,0 +1,230 @@ +resource "aws_s3_bucket" "trail_1_bucket" { + bucket = "${terraform.env}-trail-01-bucket" + force_destroy = true + + policy = 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 Date: Thu, 25 Jan 2018 01:40:07 -0500 Subject: [PATCH 03/12] Updates to tf to comply with CIS and added resources awaiting PR Signed-off-by: Rony Xavier --- README.md | 136 ++----- libraries/_aws_connection.rb | 4 + libraries/aws_iam_root_user.rb | 10 + libraries/aws_s3_bucket.rb | 10 + test/integration/cis/build/cloudtrail.tf | 44 ++- test/integration/cis/build/cloudwatch.tf | 444 ++++++++++++++++++++--- test/integration/cis/build/iam.tf | 64 ++-- test/integration/cis/build/s3.tf | 25 +- 8 files changed, 524 insertions(+), 213 deletions(-) diff --git a/README.md b/README.md index e172bde..017ffcf 100644 --- a/README.md +++ b/README.md @@ -1,124 +1,42 @@ -# InSpec for AWS +# cis-aws-foundations-hardening - v1.0.0 -## Roadmap +A terraform hardening baseline the CIS AWS Foundations Benchmark v1.10. -This repository is the development repository for InSpec for AWS. Once [RFC Platforms](https://github.com/chef/inspec/issues/1661) is fully implemented in InSpec, this repository is going to be merged into core InSpec. +## Overview -As of now, AWS resources are implemented as an InSpec resource pack. It will ship with the required resources to write your own AWS tests. +This will help you setup and validate an AWS VPC/ENV ... -``` -├── README.md - this readme -└── libraries - contains AWS resources -``` +## Usage -## Get started +### Setup your Environment -Before running the profile with InSpec, define environment variables with your AWS region and credentials. InSpec supports the following standard AWS variables: +You will need to set the following env_vars for this to work. -- `AWS_REGION` -- `AWS_DEFAULT_REGION` -- `AWS_ACCESS_KEY_ID` -- `AWS_SECRET_ACCESS_KEY` -- `AWS_SESSION_TOKEN` (optional) +- AWS_SUBNET_ID - The AWS Subnet you wish to use ... (default: none) +- AWS_SSH_KEY_ID - The SSH Key that is associated with ... (default: none) +- AWS_ACCESS_KEY_ID - The AWS Access Key that is ... (default: none) +- AWS_SECRET_ACCESS_KEY ... (default: none) +- AWS_DEFAULT_INSTANCE_TYPE ... (default: none) (suggested: t2.micro) +- AWS_REGION - The AWS Region you would like to use (default: us-east-1) +- AWS_AMI_ID ... (default: none) +- AWS_SG_ID ... (default: none) -Those variables are defined in [AWS CLI Docs](http://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html#cli-environment) -## Use the resources +1. Switch to your Terraform 0.10.0 environment +2. Ensure your environment variables are configured (see above) +3. Run Bundle Command +- ```bundle exec rake test:aws:setup:cis``` +- ```bundle exec rake test:aws:run:cis``` +- ```bundle exec rake test:aws:cleanup:cis``` -Since this is a InSpec resource pack, it only defines InSpec resources. It includes example tests only. You can easily use the AWS InSpec resources in your tests do the following: -### Create a new profile +## Quetions: -``` -inspec init profile my-profile -``` +- see: https://newcontext-oss.github.io/kitchen-terraform/tutorials/amazon_provider_ec2.html +- see: https://github.com/chef/inspec-aws -### Adapt the `inspec.yml` +## Developing -``` -name: my-profile -title: My own AWS profile -version: 0.1.0 -depends: - - name: aws - url: https://github.com/chef/inspec-aws/archive/master.tar.gz -``` +## Contributing -### Add controls - -Since your profile depends on the resource pack, you can use those resources in your own profile: - -``` -control "aws-1" do - impact 0.7 - title 'Checks the machine is running' - - describe aws_ec2_instance('my-ec2-machine') do - it { should be_running } - end -end -``` - -### Running your profile - -Then use `inspec exec my-profile` to execute your new profile. - -Our future intent is to support an `aws` target for InSpec/Train, so you may also pass credentials `inspec exec my-profile -t aws://accesskey:secret@region`. - -* See [train/issues/229](https://github.com/chef/train/issues/229). - -### Available Resources - - * `aws_ec2_instance` - This resource reads information about an ec2 instance - * `aws_iam_access_key` - Verifies settings for AWS IAM access keys - * `aws_iam_password_policy` - Verifies iam password policy - * `aws_iam_root_user` - Verifies settings for AWS root account - * `aws_iam_user` - Verifies settings for a specific AWS IAM user - * `aws_iam_users` - Verifies settings for AWS IAM users - -### Roadmap - - * `aws_ami` - * `aws_s3bucket` - * `aws_security_group` - * `aws_iam_group` - * `aws_iam_policy` - * `aws_iam_role` - -## Developing and Testing the AWS Resources Pack - -### Unit tests - -To execute the unit tests, run: - -``` -bundle exec rake test -``` - -### Integration tests - -Please see TESTING_AGAINST_AWS.md for details on how to setup the needed AWS accounts to perform testing. - -## Kudos - -This project was inspired by [inspec-aws](https://github.com/arothian/inspec-aws) from [arothian](https://github.com/arothian). - -## License - -| | | -| ------ | --- | -| **Author:** | Christoph Hartmann () | -| **Copyright:** | Copyright (c) 2017 Chef Software Inc. | -| **License:** | Apache License, Version 2.0 | - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. +## Pushing a Pull Request \ No newline at end of file diff --git a/libraries/_aws_connection.rb b/libraries/_aws_connection.rb index 06c3b30..f723fa5 100644 --- a/libraries/_aws_connection.rb +++ b/libraries/_aws_connection.rb @@ -60,4 +60,8 @@ def iam_client def s3_client @s3_client ||= Aws::S3::Client.new end + + def kms_client + @kms_client ||= Aws::KMS::Client.new + end end diff --git a/libraries/aws_iam_root_user.rb b/libraries/aws_iam_root_user.rb index 89a9fae..1ee32e1 100644 --- a/libraries/aws_iam_root_user.rb +++ b/libraries/aws_iam_root_user.rb @@ -22,6 +22,16 @@ def has_mfa_enabled? summary_account['AccountMFAEnabled'] == 1 end + def has_virtual_mfa_devices? + virtual_mfa_devices.each do |device| + if %r{arn:aws:iam::\d{12}:mfa\/root-account-mfa-device} =~ + device['serial_number'] + return true + end + end + false + end + def to_s 'AWS Root-User' end diff --git a/libraries/aws_s3_bucket.rb b/libraries/aws_s3_bucket.rb index 07cb669..e8ad98d 100644 --- a/libraries/aws_s3_bucket.rb +++ b/libraries/aws_s3_bucket.rb @@ -35,6 +35,12 @@ def public? bucket_policy.any? { |s| s.effect == 'Allow' && s.principal == '*' } end + def has_access_logging_enabled? + return unless @exists + # This is simple enough to inline it. + !AwsS3Bucket::BackendFactory.create.get_bucket_logging(bucket: bucket_name).logging_enabled.nil? + end + private def validate_params(raw_params) @@ -97,6 +103,10 @@ def get_bucket_location(query) def get_bucket_policy(query) AWSConnection.new.s3_client.get_bucket_policy(query) end + + def get_bucket_logging(query) + AWSConnection.new.s3_client.get_bucket_logging(query) + end end end end diff --git a/test/integration/cis/build/cloudtrail.tf b/test/integration/cis/build/cloudtrail.tf index 965db8c..61bfae5 100644 --- a/test/integration/cis/build/cloudtrail.tf +++ b/test/integration/cis/build/cloudtrail.tf @@ -1,7 +1,22 @@ +resource "aws_s3_bucket" "log_bucket" { + bucket = "inspec-testing-log-bucket-${terraform.env}.chef.io" + force_destroy = true + acl = "log-delivery-write" +} + +output "s3_bucket_log_bucket_name" { + value = "${aws_s3_bucket.log_bucket.id}" +} + resource "aws_s3_bucket" "trail_1_bucket" { bucket = "${terraform.env}-trail-01-bucket" force_destroy = true + logging { + target_bucket = "${aws_s3_bucket.log_bucket.id}" + target_prefix = "log/" + } + policy = < Date: Thu, 25 Jan 2018 01:48:52 -0500 Subject: [PATCH 04/12] Updates to tf to comply with CIS and added resources awaiting PR Signed-off-by: Rony Xavier --- README.md | 4 +- libraries/aws_iam_policy.rb | 114 +++++++++ libraries/aws_iam_policys.rb | 44 ++++ libraries/aws_kms__key.rb | 92 +++++++ libraries/aws_kms_keys.rb | 44 ++++ test/integration/cis/build/cloudtrail.tf.bak | 248 +++++++++++++++++++ test/integration/cis/build/group.tf | 13 + test/integration/cis/verify | 2 +- 8 files changed, 559 insertions(+), 2 deletions(-) create mode 100644 libraries/aws_iam_policy.rb create mode 100644 libraries/aws_iam_policys.rb create mode 100644 libraries/aws_kms__key.rb create mode 100644 libraries/aws_kms_keys.rb create mode 100644 test/integration/cis/build/cloudtrail.tf.bak create mode 100644 test/integration/cis/build/group.tf diff --git a/README.md b/README.md index 017ffcf..501ff48 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,9 @@ You will need to set the following env_vars for this to work. 1. Switch to your Terraform 0.10.0 environment 2. Ensure your environment variables are configured (see above) -3. Run Bundle Command +3. From cis-aws-foundations-hardening directory run: +- ``` git clone https://github.com/aaronlippold/cis-aws-foundations-baseline.git test/integration/cis/verify``` +4. Run Bundle Command - ```bundle exec rake test:aws:setup:cis``` - ```bundle exec rake test:aws:run:cis``` - ```bundle exec rake test:aws:cleanup:cis``` diff --git a/libraries/aws_iam_policy.rb b/libraries/aws_iam_policy.rb new file mode 100644 index 0000000..6c47311 --- /dev/null +++ b/libraries/aws_iam_policy.rb @@ -0,0 +1,114 @@ +class AwsIamPolicy < Inspec.resource(1) + name 'aws_iam_policy' + desc 'Verifies settings for individual AWS IAM Policy' + example " + describe aws_iam_policy('AWSSupportAccess') do + it { should be_attached } + end + " + + include AwsResourceMixin + + attr_reader :arn, :default_version_id, :attachment_count + + def to_s + "Policy #{@policy_name}" + end + + def attached? + !attachment_count.zero? + end + + def attached_users + return @attached_users if defined? @attached_users + fetch_attached_entities + @attached_users + end + + def attached_groups + return @attached_groups if defined? @attached_groups + fetch_attached_entities + @attached_groups + end + + def attached_roles + return @attached_roles if defined? @attached_roles + fetch_attached_entities + @attached_roles + end + + def attached_to_user?(user_name) + attached_users.include?(user_name) + end + + def attached_to_group?(group_name) + attached_groups.include?(group_name) + end + + def attached_to_role?(role_name) + attached_roles.include?(role_name) + end + + private + + def validate_params(raw_params) + validated_params = check_resource_param_names( + raw_params: raw_params, + allowed_params: [:policy_name], + allowed_scalar_name: :policy_name, + allowed_scalar_type: String, + ) + + if validated_params.empty? + raise ArgumentError, "You must provide the parameter 'policy_name' to aws_iam_policy." + end + + validated_params + end + + def fetch_from_aws + backend = AwsIamPolicy::BackendFactory.create + + criteria = { max_items: 1000 } # maxItems max value is 1000 + resp = backend.list_policies(criteria) + @policy = resp.policies.detect do |policy| + policy.policy_name == @policy_name + end + + @exists = !@policy.nil? + + return unless @exists + @arn = @policy[:arn] + @default_version_id = @policy[:default_version_id] + @attachment_count = @policy[:attachment_count] + end + + def fetch_attached_entities + unless @exists + @attached_groups = nil + @attached_users = nil + @attached_roles = nil + return + end + backend = AwsIamPolicy::BackendFactory.create + criteria = { policy_arn: arn } + resp = backend.list_entities_for_policy(criteria) + @attached_groups = resp.policy_groups.map(&:group_name) + @attached_users = resp.policy_users.map(&:user_name) + @attached_roles = resp.policy_roles.map(&:role_name) + end + + class Backend + class AwsClientApi + BackendFactory.set_default_backend(self) + + def list_policies(criteria) + AWSConnection.new.iam_client.list_policies(criteria) + end + + def list_entities_for_policy(criteria) + AWSConnection.new.iam_client.list_entities_for_policy(criteria) + end + end + end +end diff --git a/libraries/aws_iam_policys.rb b/libraries/aws_iam_policys.rb new file mode 100644 index 0000000..b335ca0 --- /dev/null +++ b/libraries/aws_iam_policys.rb @@ -0,0 +1,44 @@ +class AwsIamPolicys < Inspec.resource(1) + name 'aws_iam_policys' + desc 'Verifies settings for AWS IAM Policys in bulk' + example ' + describe aws_iam_policys do + it { should exist } + end + ' + + # Underlying FilterTable implementation. + filter = FilterTable.create + filter.add_accessor(:entries) + .add(:exists?) { |x| !x.entries.empty? } + .add(:policy_names, field: :policy_name) + .add(:arns, field: :arn) + filter.connect(self, :policy_data) + + def policy_data + @table + end + + def to_s + 'IAM Policys' + end + + def initialize + backend = AwsIamPolicys::BackendFactory.create + @table = backend.list_policies({}).to_h[:policies] + end + + class BackendFactory + extend AwsBackendFactoryMixin + end + + class Backend + class AwsClientApi + BackendFactory.set_default_backend(self) + + def list_policies(query) + AWSConnection.new.iam_client.list_policies(query) + end + end + end +end diff --git a/libraries/aws_kms__key.rb b/libraries/aws_kms__key.rb new file mode 100644 index 0000000..a86865c --- /dev/null +++ b/libraries/aws_kms__key.rb @@ -0,0 +1,92 @@ +class AwsKmsKey < Inspec.resource(1) + name 'aws_kms_key' + desc 'Verifies settings for an individual AWS KMS Key' + example " + describe aws_kms_key('arn:aws:kms:us-east-1::key/key-id') do + it { should exist } + end + " + + include AwsResourceMixin + + attr_reader :key_id, :arn, :creation_date, :key_usage, :key_state, :description, + :deletion_date, :valid_to, :origin, :expiration_model, :key_manager + + def to_s + "KMS Key #{@key_arn}" + end + + def enabled? + @enabled + end + + def rotation_enabled? + @rotation_enabled + end + + def created_days_ago + ((Time.now - creation_date)/(24*60*60)).to_i unless creation_date.nil? + end + + private + + def validate_params(raw_params) + validated_params = check_resource_param_names( + raw_params: raw_params, + allowed_params: [:key_arn], + allowed_scalar_name: :key_arn, + allowed_scalar_type: String, + ) + + if validated_params.empty? + raise ArgumentError, "You must provide the parameter 'key_id' to aws_kms_key." + end + + validated_params + end + + def fetch_from_aws + backend = AwsKmsKey::BackendFactory.create + + query = { key_id: @key_arn } + resp = backend.describe_key(query) + + @exists = !resp.empty? + return unless @exists + + @key = resp.key_metadata.to_h + @key_id = @key[:key_id] + @arn = @key[:arn] + @creation_date = @key[:creation_date] + @enabled = @key[:enabled] + @description = @key[:description] + @key_usage = @key[:key_usage] + @key_state = @key[:key_state] + @deletion_date = @key[:deletion_date] + @valid_to = @key[:valid_to] + @origin = @key[:origin] + @expiration_model = @key[:expiration_model] + @key_manager = @key[:key_manager] + + resp = backend.get_key_rotation_status(query) + @rotation_enabled = resp.key_rotation_enabled unless resp.empty? + end + + class Backend + class AwsClientApi + BackendFactory.set_default_backend(self) + + def describe_key(query) + AWSConnection.new.kms_client.describe_key(query) + rescue Aws::KMS::Errors::NotFoundException + return {} + end + + def get_key_rotation_status(query) + AWSConnection.new.kms_client.get_key_rotation_status(query) + rescue Aws::KMS::Errors::NotFoundException + return {} + end + end + end +end diff --git a/libraries/aws_kms_keys.rb b/libraries/aws_kms_keys.rb new file mode 100644 index 0000000..2b28925 --- /dev/null +++ b/libraries/aws_kms_keys.rb @@ -0,0 +1,44 @@ +class AwsKmsKeys < Inspec.resource(1) + name 'aws_kms_keys' + desc 'Verifies settings for AWS KMS Keys in bulk' + example ' + describe aws_kms_keys do + it { should exist } + end + ' + + # Underlying FilterTable implementation. + filter = FilterTable.create + filter.add_accessor(:entries) + .add(:exists?) { |x| !x.entries.empty? } + .add(:key_arns, field: :key_arn) + .add(:key_ids, field: :key_id) + filter.connect(self, :key_data) + + def key_data + @table + end + + def to_s + 'KMS Keys' + end + + def initialize + backend = AwsKmsKeys::BackendFactory.create + @table = backend.list_keys({ limit: 1000 }).to_h[:keys] # max value for limit is 1000 + end + + class BackendFactory + extend AwsBackendFactoryMixin + end + + class Backend + class AwsClientApi + BackendFactory.set_default_backend(self) + + def list_keys(query = {}) + AWSConnection.new.kms_client.list_keys(query) + end + end + end +end diff --git a/test/integration/cis/build/cloudtrail.tf.bak b/test/integration/cis/build/cloudtrail.tf.bak new file mode 100644 index 0000000..61bfae5 --- /dev/null +++ b/test/integration/cis/build/cloudtrail.tf.bak @@ -0,0 +1,248 @@ +resource "aws_s3_bucket" "log_bucket" { + bucket = "inspec-testing-log-bucket-${terraform.env}.chef.io" + force_destroy = true + acl = "log-delivery-write" +} + +output "s3_bucket_log_bucket_name" { + value = "${aws_s3_bucket.log_bucket.id}" +} + +resource "aws_s3_bucket" "trail_1_bucket" { + bucket = "${terraform.env}-trail-01-bucket" + force_destroy = true + + logging { + target_bucket = "${aws_s3_bucket.log_bucket.id}" + target_prefix = "log/" + } + + policy = < Date: Thu, 25 Jan 2018 02:31:28 -0500 Subject: [PATCH 05/12] Post known issue on Readme; tf kms updates Signed-off-by: Rony Xavier --- README.md | 7 ++++++- test/integration/cis/build/cloudtrail.tf | 3 ++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 501ff48..5ab3337 100644 --- a/README.md +++ b/README.md @@ -31,8 +31,13 @@ You will need to set the following env_vars for this to work. - ```bundle exec rake test:aws:run:cis``` - ```bundle exec rake test:aws:cleanup:cis``` +## Known Issue: -## Quetions: +SNS Topics: + +- It seems there is a slight delay in Terraform SNS Topics to take effect, so please run ```bundle exec rake test:aws:run:cis``` a few minutes after running the ```bundle exec rake test:aws:setup:cis``` command. + +## Questions: - see: https://newcontext-oss.github.io/kitchen-terraform/tutorials/amazon_provider_ec2.html - see: https://github.com/chef/inspec-aws diff --git a/test/integration/cis/build/cloudtrail.tf b/test/integration/cis/build/cloudtrail.tf index 61bfae5..1f84f00 100644 --- a/test/integration/cis/build/cloudtrail.tf +++ b/test/integration/cis/build/cloudtrail.tf @@ -110,7 +110,8 @@ resource "aws_cloudwatch_log_group" "trail_1_log_group" { resource "aws_kms_key" "trail_1_key" { description = "${terraform.env}-trail-01-key" - deletion_window_in_days = 10 + deletion_window_in_days = 7 + enable_key_rotation = true policy = < Date: Mon, 29 Jan 2018 17:50:02 -0500 Subject: [PATCH 06/12] Resource Updates Signed-off-by: Rony Xavier --- libraries/aws_cloudtrail_trail.rb | 12 +++++++ ...aws_iam_policys.rb => aws_iam_policies.rb} | 12 +++---- libraries/aws_iam_policy.rb | 25 +++++++++++++++ libraries/aws_iam_user.rb | 32 +++++++++++++++++++ libraries/aws_iam_users.rb | 9 +++++- test/integration/cis/build/ec2.tf | 32 +++++++++++-------- test/integration/cis/verify | 2 +- 7 files changed, 102 insertions(+), 22 deletions(-) rename libraries/{aws_iam_policys.rb => aws_iam_policies.rb} (76%) diff --git a/libraries/aws_cloudtrail_trail.rb b/libraries/aws_cloudtrail_trail.rb index fc9b2e0..f7197ae 100644 --- a/libraries/aws_cloudtrail_trail.rb +++ b/libraries/aws_cloudtrail_trail.rb @@ -27,6 +27,12 @@ def encrypted? !kms_key_id.nil? end + def status + query = { name: @trail_name } + data = AwsCloudTrailTrail::BackendFactory.create.get_trail_status(query).to_h + Hashie::Mash.new(data) + end + private def validate_params(raw_params) @@ -69,6 +75,12 @@ class AwsClientApi def describe_trails(query) AWSConnection.new.cloudtrail_client.describe_trails(query) end + + def get_trail_status(query) + AWSConnection.new.cloudtrail_client.get_trail_status(query) + rescue Aws::CloudTrail::Errors::TrailNotFoundException + return {} + end end end end diff --git a/libraries/aws_iam_policys.rb b/libraries/aws_iam_policies.rb similarity index 76% rename from libraries/aws_iam_policys.rb rename to libraries/aws_iam_policies.rb index b335ca0..5e903d0 100644 --- a/libraries/aws_iam_policys.rb +++ b/libraries/aws_iam_policies.rb @@ -1,8 +1,8 @@ -class AwsIamPolicys < Inspec.resource(1) - name 'aws_iam_policys' - desc 'Verifies settings for AWS IAM Policys in bulk' +class AwsIamPolicies < Inspec.resource(1) + name 'aws_iam_policies' + desc 'Verifies settings for AWS IAM Policies in bulk' example ' - describe aws_iam_policys do + describe aws_iam_policies do it { should exist } end ' @@ -20,11 +20,11 @@ def policy_data end def to_s - 'IAM Policys' + 'IAM Policies' end def initialize - backend = AwsIamPolicys::BackendFactory.create + backend = AwsIamPolicies::BackendFactory.create @table = backend.list_policies({}).to_h[:policies] end diff --git a/libraries/aws_iam_policy.rb b/libraries/aws_iam_policy.rb index 6c47311..a10bfda 100644 --- a/libraries/aws_iam_policy.rb +++ b/libraries/aws_iam_policy.rb @@ -49,6 +49,27 @@ def attached_to_role?(role_name) attached_roles.include?(role_name) end + filter = FilterTable.create + filter.add_accessor(:entries) + .add_accessor(:where) + .add(:exists?) { |x| !x.entries.empty? } + .add(:effects, field: :Effect) + .add(:actions, field: :Action) + .add(:resources, field: :Resource) + .add(:conditions, field: :Condition) + .add(:sids, field: :Sid) + .add(:principals, field: :Principal) + filter.connect(self, :document) + + def document + return unless @exists + document = URI.unescape(AwsIamPolicy::BackendFactory.create.get_policy_version({ + policy_arn: @arn, + version_id: @default_version_id, + }).policy_version.document) + JSON.parse(document,:symbolize_names => true)[:Statement] + end + private def validate_params(raw_params) @@ -109,6 +130,10 @@ def list_policies(criteria) def list_entities_for_policy(criteria) AWSConnection.new.iam_client.list_entities_for_policy(criteria) end + + def get_policy_version(criteria) + AWSConnection.new.iam_client.get_policy_version(criteria) + end end end end diff --git a/libraries/aws_iam_user.rb b/libraries/aws_iam_user.rb index 1d9f372..5529ed9 100644 --- a/libraries/aws_iam_user.rb +++ b/libraries/aws_iam_user.rb @@ -24,6 +24,30 @@ def name username end + def policies + begin + BackendFactory.create.list_user_policies(user_name: username).policy_names + rescue Aws::IAM::Errors::NoSuchEntity + [] + end + end + + def attached_policies + begin + BackendFactory.create.list_attached_user_policies(user_name: username).attached_policies + rescue Aws::IAM::Errors::NoSuchEntity + [] + end + end + + def has_policies? + !policies.empty? + end + + def has_attached_policies? + !attached_policies.empty? + end + def to_s "IAM User #{username}" end @@ -104,6 +128,14 @@ def list_mfa_devices(criteria) def list_access_keys(criteria) AWSConnection.new.iam_client.list_access_keys(criteria) end + + def list_attached_user_policies(criteria) + AWSConnection.new.iam_client.list_attached_user_policies(criteria) + end + + def list_user_policies(criteria) + AWSConnection.new.iam_client.list_user_policies(criteria) + end end end end diff --git a/libraries/aws_iam_users.rb b/libraries/aws_iam_users.rb index 5ca9faf..4e32767 100644 --- a/libraries/aws_iam_users.rb +++ b/libraries/aws_iam_users.rb @@ -11,7 +11,6 @@ class AwsIamUsers < Inspec.resource(1) describe aws_iam_users.where(has_mfa_enabled?: false) do it { should_not exist } end - describe aws_iam_users.where(has_console_password?: true) do it { should exist } end @@ -23,6 +22,9 @@ class AwsIamUsers < Inspec.resource(1) .add(:exists?) { |x| !x.entries.empty? } .add(:has_mfa_enabled?, field: :has_mfa_enabled) .add(:has_console_password?, field: :has_console_password) + .add(:password_ever_used?, field: :password_ever_used?) + .add(:password_never_used?, field: :password_never_used?) + .add(:password_last_used_days_ago, field: :password_last_used_days_ago) .add(:username, field: :user_name) filter.connect(self, :collect_user_details) @@ -51,6 +53,11 @@ def collect_user_details user[:has_mfa_enabled] = false end user[:has_mfa_enabled?] = user[:has_mfa_enabled] + password_last_used = user[:password_last_used] + user[:password_ever_used?] = !password_last_used.nil? + user[:password_never_used?] = password_last_used.nil? + next unless user[:password_ever_used?] + user[:password_last_used_days_ago] = ((Time.now - password_last_used) / (24*60*60)).to_i end users end diff --git a/test/integration/cis/build/ec2.tf b/test/integration/cis/build/ec2.tf index d2be103..22b2442 100644 --- a/test/integration/cis/build/ec2.tf +++ b/test/integration/cis/build/ec2.tf @@ -10,24 +10,23 @@ # alpha | debian | N | t2.micro # beta | centos | Y | t2.small - resource "aws_instance" "alpha" { ami = "${data.aws_ami.debian.id}" instance_type = "t2.micro" tags { - Name = "${terraform.env}.alpha" + Name = "${terraform.env}.alpha" X-Project = "inspec" } } resource "aws_instance" "beta" { - ami = "${data.aws_ami.centos.id}" - instance_type = "t2.small" + ami = "${data.aws_ami.centos.id}" + instance_type = "t2.micro" iam_instance_profile = "${aws_iam_instance_profile.profile_for_ec2_with_role.name}" tags { - Name = "${terraform.env}.beta" + Name = "${terraform.env}.beta" X-Project = "inspec" } } @@ -76,7 +75,7 @@ EOF } resource "aws_iam_instance_profile" "profile_for_ec2_with_role" { - name = "${terraform.env}.profile_for_ec2_with_role" + name = "${terraform.env}.profile_for_ec2_with_role" role = "${aws_iam_role.role_for_ec2_with_role.name}" } @@ -88,7 +87,8 @@ output "ec2_instance_has_role_id" { output "ec2_instance_type_t2_micro_id" { value = "${aws_instance.alpha.id}" } -output "ec2_instance_type_t2_small_id" { + +output "ec2_instance_type_t2_micro2_id" { value = "${aws_instance.beta.id}" } @@ -96,8 +96,8 @@ output "ec2_instance_type_t2_small_id" { # Debian data "aws_ami" "debian" { - most_recent = true - owners = ["679593333241"] + most_recent = true + owners = ["679593333241"] filter { name = "name" @@ -110,21 +110,23 @@ data "aws_ami" "debian" { } filter { - name = "root-device-type" + name = "root-device-type" values = ["ebs"] } } + output "ec2_ami_id_debian" { value = "${data.aws_ami.debian.id}" } + output "ec2_instance_debian_id" { value = "${aws_instance.alpha.id}" } # Centos data "aws_ami" "centos" { - most_recent = true - owners = ["679593333241"] + most_recent = true + owners = ["679593333241"] filter { name = "name" @@ -137,13 +139,15 @@ data "aws_ami" "centos" { } filter { - name = "root-device-type" + name = "root-device-type" values = ["ebs"] } } + output "ec2_ami_id_centos" { value = "${data.aws_ami.centos.id}" } + output "ec2_instance_centos_id" { value = "${aws_instance.beta.id}" } @@ -159,7 +163,7 @@ data "aws_vpc" "default" { data "aws_security_group" "default" { vpc_id = "${data.aws_vpc.default.id}" - name = "default" + name = "default" } output "ec2_security_group_default_vpc_id" { diff --git a/test/integration/cis/verify b/test/integration/cis/verify index d3494e8..275e147 160000 --- a/test/integration/cis/verify +++ b/test/integration/cis/verify @@ -1 +1 @@ -Subproject commit d3494e85bca86bc793799e2ee368c94d8edc1f33 +Subproject commit 275e14762b1b448bb7a6502796efdb7e165541bc From 41dc232ecd2aba3481bd5b0d4acb1665a5f6f556 Mon Sep 17 00:00:00 2001 From: Rony Xavier Date: Tue, 30 Jan 2018 02:17:18 -0500 Subject: [PATCH 07/12] Resource Updates Signed-off-by: Rony Xavier --- libraries/aws_iam_inline_policy.rb | 91 ++++++++++++++++++++++++++++++ libraries/aws_iam_policy.rb | 48 +++++++++++----- libraries/aws_iam_role.rb | 91 ++++++++++++++++++++++++++++++ test/integration/cis/verify | 2 +- 4 files changed, 216 insertions(+), 16 deletions(-) create mode 100644 libraries/aws_iam_inline_policy.rb diff --git a/libraries/aws_iam_inline_policy.rb b/libraries/aws_iam_inline_policy.rb new file mode 100644 index 0000000..47dea9e --- /dev/null +++ b/libraries/aws_iam_inline_policy.rb @@ -0,0 +1,91 @@ +# class AwsIamInlinePolicy < Inspec.resource(1) +# name 'aws_iam_inline_policy' +# desc 'Verifies settings for individual AWS IAM Inline Policy' +# example " +# describe aws_iam_inline_policy(role_name: 'role-1', policy_name: 'policy-01') do +# it { should exist } +# end +# " + +# include AwsResourceMixin + +# def to_s +# "Inline Policy #{@policy_name}" +# end + +# def document +# return PolicyDocumentFilter.new({}) unless @exists +# policy_data = URI.unescape(@policy.policy_document) +# document = JSON.parse(policy_data,:symbolize_names => true)[:Statement] +# PolicyDocumentFilter.new(document) +# end + +# private + +# def validate_params(raw_params) +# validated_params = check_resource_param_names( +# raw_params: raw_params, +# allowed_params: [:policy_name, :role_name, :group_name, :user_name], +# ) + +# if validated_params.empty? +# raise ArgumentError, "You must provide the parameter 'policy_name' to aws_iam_policy." +# end + +# validated_params +# end + +# def fetch_from_aws +# backend = AwsIamInlinePolicy::BackendFactory.create + +# if !@role_name.nil? +# query = { role_name: @role_name, policy_name: @policy_name } +# @policy = backend.get_role_policy(query) +# elsif !@group_name.nil? +# query = { group_name: @group_name, policy_name: @policy_name } +# @policy = backend.get_role_policy(query) +# elsif !@user_name.nil? +# query = { user_name: @user_name, policy_name: @policy_name } +# @policy = backend.get_role_policy(query) +# end + +# @exists = !@policy.nil? +# end + +# class Backend +# class AwsClientApi +# BackendFactory.set_default_backend(self) + +# def get_role_policy(criteria) +# AWSConnection.new.iam_client.get_role_policy(criteria) +# end + +# def get_user_policy(criteria) +# AWSConnection.new.iam_client.get_user_policy(criteria) +# end + +# def get_group_policy(criteria) +# AWSConnection.new.iam_client.get_group_policy(criteria) +# end +# end +# end +# end + +# class PolicyDocumentFilter +# filter = FilterTable.create +# filter.add_accessor(:entries) +# .add_accessor(:where) +# .add(:exists?) { |x| !x.entries.empty? } +# .add(:effects, field: :Effect) +# .add(:actions, field: :Action) +# .add(:resources, field: :Resource) +# .add(:conditions, field: :Condition) +# .add(:sids, field: :Sid) +# .add(:principals, field: :Principal) +# filter.connect(self, :document) + +# attr_reader :document +# def initialize(document) +# @document = document +# end +# end \ No newline at end of file diff --git a/libraries/aws_iam_policy.rb b/libraries/aws_iam_policy.rb index a10bfda..354acf2 100644 --- a/libraries/aws_iam_policy.rb +++ b/libraries/aws_iam_policy.rb @@ -11,6 +11,7 @@ class AwsIamPolicy < Inspec.resource(1) attr_reader :arn, :default_version_id, :attachment_count + def to_s "Policy #{@policy_name}" end @@ -49,25 +50,17 @@ def attached_to_role?(role_name) attached_roles.include?(role_name) end - filter = FilterTable.create - filter.add_accessor(:entries) - .add_accessor(:where) - .add(:exists?) { |x| !x.entries.empty? } - .add(:effects, field: :Effect) - .add(:actions, field: :Action) - .add(:resources, field: :Resource) - .add(:conditions, field: :Condition) - .add(:sids, field: :Sid) - .add(:principals, field: :Principal) - filter.connect(self, :document) - def document - return unless @exists - document = URI.unescape(AwsIamPolicy::BackendFactory.create.get_policy_version({ + return PolicyDocumentFilter.new({}) unless @exists + + policy_data = URI.unescape(AwsIamPolicy::BackendFactory.create.get_policy_version({ policy_arn: @arn, version_id: @default_version_id, }).policy_version.document) - JSON.parse(document,:symbolize_names => true)[:Statement] + + document = JSON.parse(policy_data,:symbolize_names => true)[:Statement] + + PolicyDocumentFilter.new(document, @policy_name) end private @@ -137,3 +130,28 @@ def get_policy_version(criteria) end end end + +class PolicyDocumentFilter + filter = FilterTable.create + filter.add_accessor(:entries) + .add_accessor(:where) + .add(:exists?) { |x| !x.entries.empty? } + .add(:exists?) { attachment_count } + .add(:effects, field: :Effect) + .add(:actions, field: :Action) + .add(:resources, field: :Resource) + .add(:conditions, field: :Condition) + .add(:sids, field: :Sid) + .add(:principals, field: :Principal) + filter.connect(self, :document) + + def to_s + "Policy #{@policy_name}" + end + + attr_reader :document + def initialize(document, policy_name) + @document = document + @policy_name = policy_name + end +end \ No newline at end of file diff --git a/libraries/aws_iam_role.rb b/libraries/aws_iam_role.rb index ea11b88..f7efc04 100644 --- a/libraries/aws_iam_role.rb +++ b/libraries/aws_iam_role.rb @@ -12,6 +12,41 @@ class AwsIamRole < Inspec.resource(1) include AwsResourceMixin attr_reader :role_name, :description + def inline_policies + return [] unless @exists + AwsIamRole::BackendFactory.create.list_role_policies(role_name: role_name).policy_names + end + + def attached_policies + return [] unless @exists + AwsIamRole::BackendFactory.create.list_attached_role_policies(role_name: role_name).attached_policies.map(&:policy_name) + end + + def inline_policy_document + return InlinePolicyDocumentFilter.new({}) unless @exists + documents = [] + backend = AwsIamRole::BackendFactory.create + + inline_policies.each do |policy| + policy_data = URI.unescape(backend.get_role_policy({ + role_name: role_name, + policy_name: policy, + }).policy_document) + + document = JSON.parse(policy_data,:symbolize_names => true)[:Statement] + + document.each do |item| + item[:Policy] = policy + end + documents << document + end + InlinePolicyDocumentFilter.new(documents.flatten) + end + + def attached_policy_document + return AttachedPolicyDocumentFilter.new({}) unless @exists + end + private def validate_params(raw_params) @@ -46,6 +81,62 @@ class AwsClientApi def get_role(query) AWSConnection.new.iam_client.get_role(query) end + + def list_role_policies(query) + AWSConnection.new.iam_client.list_role_policies(query) + end + + def list_attached_role_policies(query) + AWSConnection.new.iam_client.list_attached_role_policies(query) + end + + def get_role_policy(query) + AWSConnection.new.iam_client.get_role_policy(query) + end + + def get_policy_version(criteria) + AWSConnection.new.iam_client.get_policy_version(query) + end end end end + +class InlinePolicyDocumentFilter + filter = FilterTable.create + filter.add_accessor(:entries) + .add_accessor(:where) + .add(:exists?) { |x| !x.entries.empty? } + .add(:policies, field: :Policy) + .add(:effects, field: :Effect) + .add(:actions, field: :Action) + .add(:resources, field: :Resource) + .add(:conditions, field: :Condition) + .add(:sids, field: :Sid) + .add(:principals, field: :Principal) + filter.connect(self, :document) + + attr_reader :document + def initialize(document) + @document = document + end +end + +class AttachedPolicyDocumentFilter + filter = FilterTable.create + filter.add_accessor(:entries) + .add_accessor(:where) + .add(:exists?) { |x| !x.entries.empty? } + .add(:policies, field: :Policy) + .add(:effects, field: :Effect) + .add(:actions, field: :Action) + .add(:resources, field: :Resource) + .add(:conditions, field: :Condition) + .add(:sids, field: :Sid) + .add(:principals, field: :Principal) + filter.connect(self, :document) + + attr_reader :document + def initialize(document) + @document = document + end +end diff --git a/test/integration/cis/verify b/test/integration/cis/verify index 275e147..787b4bd 160000 --- a/test/integration/cis/verify +++ b/test/integration/cis/verify @@ -1 +1 @@ -Subproject commit 275e14762b1b448bb7a6502796efdb7e165541bc +Subproject commit 787b4bd5479944d2399e37f5fae2ae077c8e340a From 3a338f8aa2756fd07da69c668dfd2bd0d85ffc41 Mon Sep 17 00:00:00 2001 From: Rony Xavier Date: Wed, 31 Jan 2018 13:22:30 -0500 Subject: [PATCH 08/12] Resource Updates Signed-off-by: Rony Xavier --- libraries/aws_ec2_security_group.rb | 65 ++++++++++++++++++++++--- libraries/aws_iam_policy.rb | 18 +++---- libraries/aws_iam_role.rb | 75 +---------------------------- libraries/aws_vpc.rb | 19 +++++++- libraries/aws_vpcs.rb | 3 ++ test/integration/cis/verify | 2 +- 6 files changed, 90 insertions(+), 92 deletions(-) diff --git a/libraries/aws_ec2_security_group.rb b/libraries/aws_ec2_security_group.rb index 8cc280d..63ec135 100644 --- a/libraries/aws_ec2_security_group.rb +++ b/libraries/aws_ec2_security_group.rb @@ -10,12 +10,45 @@ class AwsEc2SecurityGroup < Inspec.resource(1) ' include AwsResourceMixin - attr_reader :description, :group_id, :group_name, :vpc_id + attr_reader :description, :group_id, :group_name, :vpc_id, :ingress_rules, :egress_rules def to_s "EC2 Security Group #{@group_id}" end + # Underlying FilterTable implementation. + filter = FilterTable.create + filter.add_accessor(:where) + .add_accessor(:entries) + .add(:type, field: :type) + .add(:group_ids, field: :group_id) + .add(:from_port, field: :from_port) + .add(:to_port, field: :to_port) + .add(:ip_protocol, field: :ip_protocol) + .add(:ip_ranges, field: :ip_ranges) + .add(:ipv_6_ranges, field: :ipv_6_ranges) + filter.connect(self, :access_key_data) + + def access_key_data + @table + end + + def open_on_port?(port) + @ingress_rules.each do |rule| + # Will skip unless the port is equal to the from port or + # the rule allows all traffic, or it is between the to and from port. + next unless port == rule.from_port \ + or (rule.to_port.nil? and rule.from_port.nil?) \ + or (!rule.to_port.nil? and port.between?(rule.from_port, rule.to_port)) + rule.ip_ranges.each do |ip_range| + if ip_range.cidr_ip == '0.0.0.0/0' or ip_range.cidr_ip == 'ALL' + return true + end + end + end + false + end + private def validate_params(raw_params) @@ -55,6 +88,8 @@ def fetch_from_aws :group_id, :group_name, :vpc_id, + :ingress_rules, + :egress_rules, ].each do |criterion_name| val = instance_variable_get("@#{criterion_name}".to_sym) next if val.nil? @@ -66,17 +101,33 @@ def fetch_from_aws ) end dsg_response = backend.describe_security_groups(filters: filters) - if dsg_response.security_groups.empty? @exists = false return end @exists = true - @description = dsg_response.security_groups[0].description - @group_id = dsg_response.security_groups[0].group_id - @group_name = dsg_response.security_groups[0].group_name - @vpc_id = dsg_response.security_groups[0].vpc_id + @description = dsg_response.security_groups[0].description + @group_id = dsg_response.security_groups[0].group_id + @group_name = dsg_response.security_groups[0].group_name + @vpc_id = dsg_response.security_groups[0].vpc_id + @ingress_rules = dsg_response.security_groups[0].ip_permissions + @egress_rules = dsg_response.security_groups[0].ip_permissions_egress + populate_ingress_egress_rules + end + + def populate_ingress_egress_rules + @table = [] + @ingress_rules.each do |rule| + rule = Hash[rule.each_pair.to_a] + rule[:type] = 'ingress' + @table.push(rule) + end + @egress_rules.each do |rule| + rule = Hash[rule.each_pair.to_a] + rule[:type] = 'egress' + @table.push(rule) + end end class Backend @@ -88,4 +139,4 @@ def describe_security_groups(query) end end end -end +end \ No newline at end of file diff --git a/libraries/aws_iam_policy.rb b/libraries/aws_iam_policy.rb index 354acf2..f637caa 100644 --- a/libraries/aws_iam_policy.rb +++ b/libraries/aws_iam_policy.rb @@ -11,7 +11,6 @@ class AwsIamPolicy < Inspec.resource(1) attr_reader :arn, :default_version_id, :attachment_count - def to_s "Policy #{@policy_name}" end @@ -53,12 +52,14 @@ def attached_to_role?(role_name) def document return PolicyDocumentFilter.new({}) unless @exists - policy_data = URI.unescape(AwsIamPolicy::BackendFactory.create.get_policy_version({ - policy_arn: @arn, - version_id: @default_version_id, - }).policy_version.document) + policy_data = CGI.unescape(AwsIamPolicy::BackendFactory.create.get_policy_version( + { + policy_arn: @arn, + version_id: @default_version_id, + }, + ).policy_version.document) - document = JSON.parse(policy_data,:symbolize_names => true)[:Statement] + document = JSON.parse(policy_data, symbolize_names: true)[:Statement] PolicyDocumentFilter.new(document, @policy_name) end @@ -136,7 +137,6 @@ class PolicyDocumentFilter filter.add_accessor(:entries) .add_accessor(:where) .add(:exists?) { |x| !x.entries.empty? } - .add(:exists?) { attachment_count } .add(:effects, field: :Effect) .add(:actions, field: :Action) .add(:resources, field: :Resource) @@ -144,7 +144,7 @@ class PolicyDocumentFilter .add(:sids, field: :Sid) .add(:principals, field: :Principal) filter.connect(self, :document) - + def to_s "Policy #{@policy_name}" end @@ -154,4 +154,4 @@ def initialize(document, policy_name) @document = document @policy_name = policy_name end -end \ No newline at end of file +end diff --git a/libraries/aws_iam_role.rb b/libraries/aws_iam_role.rb index f7efc04..4dd7eee 100644 --- a/libraries/aws_iam_role.rb +++ b/libraries/aws_iam_role.rb @@ -22,31 +22,6 @@ def attached_policies AwsIamRole::BackendFactory.create.list_attached_role_policies(role_name: role_name).attached_policies.map(&:policy_name) end - def inline_policy_document - return InlinePolicyDocumentFilter.new({}) unless @exists - documents = [] - backend = AwsIamRole::BackendFactory.create - - inline_policies.each do |policy| - policy_data = URI.unescape(backend.get_role_policy({ - role_name: role_name, - policy_name: policy, - }).policy_document) - - document = JSON.parse(policy_data,:symbolize_names => true)[:Statement] - - document.each do |item| - item[:Policy] = policy - end - documents << document - end - InlinePolicyDocumentFilter.new(documents.flatten) - end - - def attached_policy_document - return AttachedPolicyDocumentFilter.new({}) unless @exists - end - private def validate_params(raw_params) @@ -85,58 +60,10 @@ def get_role(query) def list_role_policies(query) AWSConnection.new.iam_client.list_role_policies(query) end - + def list_attached_role_policies(query) AWSConnection.new.iam_client.list_attached_role_policies(query) end - - def get_role_policy(query) - AWSConnection.new.iam_client.get_role_policy(query) - end - - def get_policy_version(criteria) - AWSConnection.new.iam_client.get_policy_version(query) - end end end end - -class InlinePolicyDocumentFilter - filter = FilterTable.create - filter.add_accessor(:entries) - .add_accessor(:where) - .add(:exists?) { |x| !x.entries.empty? } - .add(:policies, field: :Policy) - .add(:effects, field: :Effect) - .add(:actions, field: :Action) - .add(:resources, field: :Resource) - .add(:conditions, field: :Condition) - .add(:sids, field: :Sid) - .add(:principals, field: :Principal) - filter.connect(self, :document) - - attr_reader :document - def initialize(document) - @document = document - end -end - -class AttachedPolicyDocumentFilter - filter = FilterTable.create - filter.add_accessor(:entries) - .add_accessor(:where) - .add(:exists?) { |x| !x.entries.empty? } - .add(:policies, field: :Policy) - .add(:effects, field: :Effect) - .add(:actions, field: :Action) - .add(:resources, field: :Resource) - .add(:conditions, field: :Condition) - .add(:sids, field: :Sid) - .add(:principals, field: :Principal) - filter.connect(self, :document) - - attr_reader :document - def initialize(document) - @document = document - end -end diff --git a/libraries/aws_vpc.rb b/libraries/aws_vpc.rb index f8453dc..1459434 100644 --- a/libraries/aws_vpc.rb +++ b/libraries/aws_vpc.rb @@ -16,12 +16,25 @@ def to_s "VPC #{vpc_id}" end - [:cidr_block, :dhcp_options_id, :state, :vpc_id, :instance_tenancy, :is_default].each do |property| + [:cidr_block, :dhcp_options_id, :state, :vpc_id, :instance_tenancy, :is_default,].each do |property| define_method(property) do @vpc[property] end end + def flow_logs + return unless @exists + backend = AwsVpc::BackendFactory.create + filter = { name: "resource-id", values: [@vpc_id],} + resp = backend.describe_flow_logs({filter: [filter]}) + end + + def flow_logs_enabled? + return unless @exists + !flow_logs.empty? + end + + alias default? is_default private @@ -64,6 +77,10 @@ class AwsClientApi def describe_vpcs(query) AWSConnection.new.ec2_client.describe_vpcs(query) end + + def describe_flow_logs(query) + AWSConnection.new.ec2_client.describe_flow_logs(query) + end end end end diff --git a/libraries/aws_vpcs.rb b/libraries/aws_vpcs.rb index a9891be..760e567 100644 --- a/libraries/aws_vpcs.rb +++ b/libraries/aws_vpcs.rb @@ -13,6 +13,9 @@ class AwsVpcs < Inspec.resource(1) filter = FilterTable.create filter.add_accessor(:entries) .add(:exists?) { |x| !x.entries.empty? } + .add(:vpc_ids, field: :vpc_id) + .add(:is_default, field: :is_default) + .add(:state, field: :state) filter.connect(self, :vpc_data) def vpc_data diff --git a/test/integration/cis/verify b/test/integration/cis/verify index 787b4bd..6429645 160000 --- a/test/integration/cis/verify +++ b/test/integration/cis/verify @@ -1 +1 @@ -Subproject commit 787b4bd5479944d2399e37f5fae2ae077c8e340a +Subproject commit 6429645c7bb5b214ee3b74dbd4b02fab9ad0ada6 From b275f1cca341a9b6ed11593beb4c692e3376938d Mon Sep 17 00:00:00 2001 From: Rony Xavier Date: Wed, 31 Jan 2018 23:34:16 -0500 Subject: [PATCH 09/12] Resource Updates Signed-off-by: Rony Xavier --- libraries/_aws_connection.rb | 4 + libraries/aws_config_delivery_channel.rb | 71 ++++++++++++++++++ libraries/aws_config_recorder.rb | 96 ++++++++++++++++++++++++ libraries/aws_route_table.rb | 69 +++++++++++++++++ libraries/aws_sns_subscription.rb | 75 ++++++++++++++++++ libraries/aws_sns_topic.rb | 21 +++++- libraries/aws_sns_topics.rb | 45 +++++++++++ test/integration/cis/verify | 2 +- 8 files changed, 379 insertions(+), 4 deletions(-) create mode 100644 libraries/aws_config_delivery_channel.rb create mode 100644 libraries/aws_config_recorder.rb create mode 100644 libraries/aws_route_table.rb create mode 100644 libraries/aws_sns_subscription.rb create mode 100644 libraries/aws_sns_topics.rb diff --git a/libraries/_aws_connection.rb b/libraries/_aws_connection.rb index f723fa5..6ea3724 100644 --- a/libraries/_aws_connection.rb +++ b/libraries/_aws_connection.rb @@ -64,4 +64,8 @@ def s3_client def kms_client @kms_client ||= Aws::KMS::Client.new end + + def configservice_client + @kms_client ||= Aws::ConfigService::Client.new + end end diff --git a/libraries/aws_config_delivery_channel.rb b/libraries/aws_config_delivery_channel.rb new file mode 100644 index 0000000..09110c7 --- /dev/null +++ b/libraries/aws_config_delivery_channel.rb @@ -0,0 +1,71 @@ +require '_aws' + +class AwsConfigurationDeliveryChannel < Inspec.resource(1) + name 'aws_config_delivery_channel' + desc 'Verifies settings for AWS Configuration Delivery Channel' + example " + describe aws_config_delivery_channel do + it { should exist } + it { should be_default } + its('s3_bucket_name') { should_not be_nil } + its('sns_topic_arn') { should_not be_nil } + end + " + + include AwsResourceMixin + attr_reader :channel_name , :s3_bucket_name, :s3_key_prefix, :sns_topic_arn + + def to_s + "Configuration_Delivery_Channel: #{@channel_name}" + end + + def default? + @channel_name.eql?('default') + end + + private + + def validate_params(raw_params) + validated_params = check_resource_param_names( + raw_params: raw_params, + allowed_params: [:channel_name], + allowed_scalar_name: :channel_name, + allowed_scalar_type: String, + ) + + validated_params + end + + def fetch_from_aws + backend = AwsConfigurationDeliveryChannel::BackendFactory.create + + if @recorder_name.nil? + query = { delivery_channel_names: ['default'] } + else + query = { delivery_channel_names: [@channel_name] } + end + + @resp = backend.describe_delivery_channels(query) + @exists = !@resp.empty? + return unless @exists + + @channel = @resp.delivery_channels.first.to_h + @channel_name = @channel[:name] + @s3_bucket_name = @channel[:s3_bucket_name] + @s3_key_prefix = @channel[:s3_key_prefix] + @sns_topic_arn = @channel[:sns_topic_arn] + end + + class Backend + class AwsClientApi + BackendFactory.set_default_backend(self) + + def describe_delivery_channels(query) + AWSConnection.new.configservice_client.describe_delivery_channels(query) + rescue Aws::ConfigService::Errors::NoSuchDeliveryChannelException + return {} + end + + end + end +end diff --git a/libraries/aws_config_recorder.rb b/libraries/aws_config_recorder.rb new file mode 100644 index 0000000..4c3b8c1 --- /dev/null +++ b/libraries/aws_config_recorder.rb @@ -0,0 +1,96 @@ +require '_aws' + +class AwsConfigurationRecorder < Inspec.resource(1) + name 'aws_config_recorder' + desc 'Verifies settings for AWS Configuration Recorder' + example " + describe aws_config_recorder do + it { should exist } + it { should be_default } + it { should be_recording } + it { should be_all_supported } + it { should have_include_global_resource_types } + end + " + + include AwsResourceMixin + attr_reader :role_arn , :resource_types, :recorder_name, :resp + + def to_s + "Configuration_Recorder: #{@recorder_name}" + end + + def default? + @recorder_name.eql?('default') + end + + def all_supported? + @all_supported + end + + def has_include_global_resource_types? + @include_global_resource_types + end + + def status + return unless @exists + backend = AwsConfigurationRecorder::BackendFactory.create + @resp = backend.describe_configuration_recorder_status(@query) + @status = @resp.configuration_recorders_status.first.to_h + end + + def recording? + return unless @exists + status[:recording] + end + + private + + def validate_params(raw_params) + validated_params = check_resource_param_names( + raw_params: raw_params, + allowed_params: [:recorder_name], + allowed_scalar_name: :recorder_name, + allowed_scalar_type: String, + ) + + validated_params + end + + def fetch_from_aws + backend = AwsConfigurationRecorder::BackendFactory.create + + if @recorder_name.nil? + @query = { configuration_recorder_names: ['default'] } + else + @query = { configuration_recorder_names: [@recorder_name] } + end + + @resp = backend.describe_configuration_recorders(@query) + @exists = !@resp.empty? + return unless @exists + + @recorder = @resp.configuration_recorders.first.to_h + @recorder_name = @recorder[:name] + @role_arn = @recorder[:role_arn] + @all_supported = @recorder[:recording_group][:all_supported] + @include_global_resource_types = @recorder[:recording_group][:include_global_resource_types] + @resource_types = @recorder[:recording_group][:resource_types] + end + + class Backend + class AwsClientApi + BackendFactory.set_default_backend(self) + + def describe_configuration_recorders(query) + AWSConnection.new.configservice_client.describe_configuration_recorders(query) + rescue Aws::ConfigService::Errors::NoSuchConfigurationRecorderException + return {} + end + + def describe_configuration_recorder_status(query) + AWSConnection.new.configservice_client.describe_configuration_recorder_status(query) + end + end + end +end diff --git a/libraries/aws_route_table.rb b/libraries/aws_route_table.rb new file mode 100644 index 0000000..c145255 --- /dev/null +++ b/libraries/aws_route_table.rb @@ -0,0 +1,69 @@ +class AwsRouteTable < Inspec.resource(1) + name 'aws_route_table' + desc 'Verifies settings for an AWS Route Table' + example " + describe aws_route_table do + its('route_table_id') { should cmp 'rtb-2c60ec44' } + end + " + + include AwsResourceMixin + + def to_s + "Route Table #{@route_table_id}" + end + + attr_reader :associations, :propagating_vgws, :route_table_id, :routes, :tags, :vpc_id + + private + + def validate_params(raw_params) + validated_params = check_resource_param_names( + raw_params: raw_params, + allowed_params: [:route_table_id], + allowed_scalar_name: :route_table_id, + allowed_scalar_type: String, + ) + + if validated_params.key?(:route_table_id) && validated_params[:route_table_id] !~ /^rtb\-[0-9a-f]{8}/ + raise ArgumentError, 'aws_route_table Route Table ID must be in the' \ + ' format "rtb-" followed by 8 hexadecimal characters.' + end + + validated_params + end + + def fetch_from_aws + backend = AwsRouteTable::BackendFactory.create + + if @route_table_id.nil? + args = nil + else + args = { filters: [{ name: 'route-table-id', values: [@route_table_id] }] } + end + + resp = backend.describe_route_tables(args) + @routetable = resp.to_h[:route_tables] + + unless @routetable.empty? + r = @routetable.first + @associations = r[:associations] + @propagating_vgws = r[:propagating_vgws] + @route_table_id = r[:route_table_id] + @routes = r[:routes] + @tags = r[:tags] + @vpc_id = r[:vpc_id] + end + @exists = !@routetable.empty? + end + + class Backend + class AwsClientApi + BackendFactory.set_default_backend(self) + + def describe_route_tables(query) + AWSConnection.new.ec2_client.describe_route_tables(query) + end + end + end +end \ No newline at end of file diff --git a/libraries/aws_sns_subscription.rb b/libraries/aws_sns_subscription.rb new file mode 100644 index 0000000..b6f0a56 --- /dev/null +++ b/libraries/aws_sns_subscription.rb @@ -0,0 +1,75 @@ +require '_aws' + +class AwsSnsSubscription < Inspec.resource(1) + name 'aws_sns_subscription' + desc 'Verifies settings for an SNS Subscription' + example " + describe aws_sns_subscription('arn:aws:sns:us-east-1::test-topic-01:b214aff5-a2c7-438f-a753-8494493f2ff6') do + it { should_not have_raw_message_delivery } + it { should be_confirmation_authenticated } + its('owner') { should cmp '12345678' } + its('topic_arn') { should cmp 'arn:aws:sns:us-east-1::test-topic-01' } + its('endpoint') { should cmp 'arn:aws:sqs:us-east-1::test-queue-01' } + its('protocol') { should cmp 'sqs' } + end + " + + include AwsResourceMixin + attr_reader :arn, :owner, :raw_message_delivery, :topic_arn, :endpoint, :protocol, :confirmation_was_authenticated, :aws_response + + alias confirmation_authenticated? confirmation_was_authenticated + alias raw_message_delivery? raw_message_delivery + + def has_raw_message_delivery? + raw_message_delivery + end + + def to_s + 'SNS Subscription' + end + + private + + def validate_params(raw_params) + validated_params = check_resource_param_names( + raw_params: raw_params, + allowed_params: [:arn], + allowed_scalar_name: :arn, + allowed_scalar_type: String, + ) + # Validate the ARN + unless validated_params[:arn] =~ /^arn:aws:sns:[\w\-]+:\d{12}:[\S]+$/ + raise ArgumentError, 'Malformed ARN for SNS Subscriptions. Expected an ARN of the form ' \ + "'arn:aws:sns:REGION:ACCOUNT-ID:TOPIC-NAME'" + end + validated_params + end + + def fetch_from_aws + @aws_response = AwsSnsSubscription::BackendFactory.create.get_subscription_attributes(subscription_arn: @arn).attributes + @exists = true + @owner = @aws_response['Owner'] + @raw_message_delivery = @aws_response['RawMessageDelivery'].eql?('true') + @topic_arn = @aws_response['TopicArn'] + @endpoint = @aws_response['Endpoint'] + @protocol = @aws_response['Protocol'] + @confirmation_was_authenticated = @aws_response['ConfirmationWasAuthenticated'].eql?('true') + + rescue Aws::SNS::Errors::NotFound + @exists = false + end + + class Backend + class AwsClientApi + BackendFactory.set_default_backend(self) + + def get_subscription_attributes(criteria) + AWSConnection.new.sns_client.get_subscription_attributes(criteria) + end + + def list_subscriptions_by_topic(criteria) + AWSConnection.new.sns_client.list_subscriptions_by_topic(criteria) + end + end + end +end diff --git a/libraries/aws_sns_topic.rb b/libraries/aws_sns_topic.rb index d9c790a..b3020e7 100644 --- a/libraries/aws_sns_topic.rb +++ b/libraries/aws_sns_topic.rb @@ -11,7 +11,16 @@ class AwsSnsTopic < Inspec.resource(1) " include AwsResourceMixin - attr_reader :arn, :confirmed_subscription_count + attr_reader :arn, :confirmed_subscription_count, :region, :owner, :aws_response + + def subscriptions + return unless @exists + AwsSnsTopic::BackendFactory.create.list_subscriptions_by_topic(topic_arn: @arn).subscriptions.map(&:subscription_arn) + end + + def to_s + 'SNS Topic' + end private @@ -31,11 +40,13 @@ def validate_params(raw_params) end def fetch_from_aws - aws_response = AwsSnsTopic::BackendFactory.create.get_topic_attributes(topic_arn: @arn).attributes + @aws_response = AwsSnsTopic::BackendFactory.create.get_topic_attributes(topic_arn: @arn).attributes @exists = true # The response has a plain hash with CamelCase plain string keys and string values - @confirmed_subscription_count = aws_response['SubscriptionsConfirmed'].to_i + @owner = @aws_response['Owner'] + @region = @aws_response['TopicArn'].scan(/^arn:aws:sns:([\w\-]+):\d{12}:[\S]+$/).flatten.first + @confirmed_subscription_count = @aws_response['SubscriptionsConfirmed'].to_i rescue Aws::SNS::Errors::NotFound @exists = false end @@ -48,6 +59,10 @@ class AwsClientApi def get_topic_attributes(criteria) AWSConnection.new.sns_client.get_topic_attributes(criteria) end + + def list_subscriptions_by_topic(criteria) + AWSConnection.new.sns_client.list_subscriptions_by_topic(criteria) + end end end end diff --git a/libraries/aws_sns_topics.rb b/libraries/aws_sns_topics.rb new file mode 100644 index 0000000..47cb7b8 --- /dev/null +++ b/libraries/aws_sns_topics.rb @@ -0,0 +1,45 @@ +require '_aws' + +class AwsSnsTopics < Inspec.resource(1) + name 'aws_sns_topics' + desc 'Verifies settings for AWS VPCs in bulk' + example ' + describe aws_sns_topics do + it { should exist } + end + ' + + # Underlying FilterTable implementation. + filter = FilterTable.create + filter.add_accessor(:entries) + .add(:exists?) { |x| !x.entries.empty? } + .add(:topic_arns, field: :topic_arn) + filter.connect(self, :sns_topics) + + def sns_topics + @table + end + + def to_s + 'SNS Topics' + end + + def initialize + backend = AwsSnsTopics::BackendFactory.create + @table = backend.list_topics.to_h[:topics] + end + + class BackendFactory + extend AwsBackendFactoryMixin + end + + class Backend + class AwsClientApi + BackendFactory.set_default_backend(self) + + def list_topics(query = {}) + AWSConnection.new.sns_client.list_topics(query) + end + end + end +end diff --git a/test/integration/cis/verify b/test/integration/cis/verify index 6429645..34b7594 160000 --- a/test/integration/cis/verify +++ b/test/integration/cis/verify @@ -1 +1 @@ -Subproject commit 6429645c7bb5b214ee3b74dbd4b02fab9ad0ada6 +Subproject commit 34b7594caebf73b173aa189584666a8a51de79f0 From 7e9ad20691b374c960f44542defb9a8bb19287b2 Mon Sep 17 00:00:00 2001 From: Rony Xavier Date: Mon, 5 Feb 2018 19:31:21 -0500 Subject: [PATCH 10/12] Resource Updates Signed-off-by: Rony Xavier --- libraries/_aws_connection.rb | 12 +++++---- libraries/aws_iam_policy.rb | 4 +-- libraries/aws_iam_role.rb | 42 ++++++++++++++++++++++++++++++- libraries/aws_iam_root_user.rb | 4 +++ libraries/aws_route_table.rb | 6 ++--- libraries/aws_sns_subscription.rb | 8 +++--- test/integration/cis/verify | 2 +- 7 files changed, 62 insertions(+), 16 deletions(-) diff --git a/libraries/_aws_connection.rb b/libraries/_aws_connection.rb index 6ea3724..04921ab 100644 --- a/libraries/_aws_connection.rb +++ b/libraries/_aws_connection.rb @@ -11,12 +11,14 @@ def initialize creds = nil if ENV['AWS_PROFILE'] creds = Aws::SharedCredentials.new(profile_name: ENV['AWS_PROFILE']) - else + elsif ENV['AWS_ACCESS_KEY_ID'] and ENV['AWS_SECRET_ACCESS_KEY'] creds = Aws::Credentials.new( ENV['AWS_ACCESS_KEY_ID'], ENV['AWS_SECRET_ACCESS_KEY'], ENV['AWS_SESSION_TOKEN'], ) + else + creds = Aws::InstanceProfileCredentials.new end opts = { region: ENV['AWS_REGION'] || ENV['AWS_DEFAULT_REGION'], @@ -36,6 +38,10 @@ def cloudwatch_client def cloudwatch_logs_client @cloudwatch_logs_client ||= Aws::CloudWatchLogs::Client.new end + + def configservice_client + @configservice_client ||= Aws::ConfigService::Client.new + end def cloudtrail_client @cloudtrail_client ||= Aws::CloudTrail::Client.new @@ -64,8 +70,4 @@ def s3_client def kms_client @kms_client ||= Aws::KMS::Client.new end - - def configservice_client - @kms_client ||= Aws::ConfigService::Client.new - end end diff --git a/libraries/aws_iam_policy.rb b/libraries/aws_iam_policy.rb index f637caa..5f2227f 100644 --- a/libraries/aws_iam_policy.rb +++ b/libraries/aws_iam_policy.rb @@ -50,7 +50,7 @@ def attached_to_role?(role_name) end def document - return PolicyDocumentFilter.new({}) unless @exists + return PolicyDocumentFilter.new unless @exists policy_data = CGI.unescape(AwsIamPolicy::BackendFactory.create.get_policy_version( { @@ -150,7 +150,7 @@ def to_s end attr_reader :document - def initialize(document, policy_name) + def initialize(document=nil, policy_name=nil) @document = document @policy_name = policy_name end diff --git a/libraries/aws_iam_role.rb b/libraries/aws_iam_role.rb index 4dd7eee..56c0cc8 100644 --- a/libraries/aws_iam_role.rb +++ b/libraries/aws_iam_role.rb @@ -10,7 +10,7 @@ class AwsIamRole < Inspec.resource(1) " include AwsResourceMixin - attr_reader :role_name, :description + attr_reader :role_name, :description, def inline_policies return [] unless @exists @@ -22,6 +22,22 @@ def attached_policies AwsIamRole::BackendFactory.create.list_attached_role_policies(role_name: role_name).attached_policies.map(&:policy_name) end + + def assume_role_policy_document + return AssumePolicyDocumentFilter.new({}) unless @exists + + policy_data = CGI.unescape(@assume_role_policy_document) #CGI.unescape(AwsIamPolicy::BackendFactory.create.get_policy_version( + # { + # policy_arn: @arn, + # version_id: @default_version_id, + # }, + # ).policy_version.document) + + document = JSON.parse(policy_data, symbolize_names: true)[:Statement] + + AssumePolicyDocumentFilter.new(document) + end + private def validate_params(raw_params) @@ -47,6 +63,7 @@ def fetch_from_aws end @exists = true @description = role_info.role.description + @assume_role_policy_document = role_info.role.assume_role_policy_document end # Uses the SDK API to really talk to AWS @@ -67,3 +84,26 @@ def list_attached_role_policies(query) end end end + +class AssumePolicyDocumentFilter + filter = FilterTable.create + filter.add_accessor(:entries) + .add_accessor(:where) + .add(:exists?) { |x| !x.entries.empty? } + .add(:effects, field: :Effect) + .add(:actions, field: :Action) + .add(:resources, field: :Resource) + .add(:conditions, field: :Condition) + .add(:sids, field: :Sid) + .add(:principals, field: :Principal) + filter.connect(self, :document) + + def to_s + "Assume Role Rolicy" + end + + attr_reader :document + def initialize(document) + @document = document + end +end diff --git a/libraries/aws_iam_root_user.rb b/libraries/aws_iam_root_user.rb index 1ee32e1..0a28eef 100644 --- a/libraries/aws_iam_root_user.rb +++ b/libraries/aws_iam_root_user.rb @@ -41,4 +41,8 @@ def to_s def summary_account @summary_account ||= @client.get_account_summary.summary_map end + + def virtual_mfa_devices + @client.list_virtual_mfa_devices.virtual_mfa_devices + end end diff --git a/libraries/aws_route_table.rb b/libraries/aws_route_table.rb index c145255..5a8a62e 100644 --- a/libraries/aws_route_table.rb +++ b/libraries/aws_route_table.rb @@ -13,7 +13,7 @@ def to_s "Route Table #{@route_table_id}" end - attr_reader :associations, :propagating_vgws, :route_table_id, :routes, :tags, :vpc_id + attr_reader :associations, :propagating_vgws, :route_table_id, :routes, :tags, :vpc_id, :resp private @@ -42,8 +42,8 @@ def fetch_from_aws args = { filters: [{ name: 'route-table-id', values: [@route_table_id] }] } end - resp = backend.describe_route_tables(args) - @routetable = resp.to_h[:route_tables] + @resp = backend.describe_route_tables(args) + @routetable = @resp.to_h[:route_tables] unless @routetable.empty? r = @routetable.first diff --git a/libraries/aws_sns_subscription.rb b/libraries/aws_sns_subscription.rb index b6f0a56..1aa66fa 100644 --- a/libraries/aws_sns_subscription.rb +++ b/libraries/aws_sns_subscription.rb @@ -38,10 +38,10 @@ def validate_params(raw_params) allowed_scalar_type: String, ) # Validate the ARN - unless validated_params[:arn] =~ /^arn:aws:sns:[\w\-]+:\d{12}:[\S]+$/ - raise ArgumentError, 'Malformed ARN for SNS Subscriptions. Expected an ARN of the form ' \ - "'arn:aws:sns:REGION:ACCOUNT-ID:TOPIC-NAME'" - end + # unless validated_params[:arn] =~ /^arn:aws:sns:[\w\-]+:\d{12}:[\S]+$/ + # raise ArgumentError, 'Malformed ARN for SNS Subscriptions. Expected an ARN of the form ' \ + # "'arn:aws:sns:REGION:ACCOUNT-ID:TOPIC-NAME'" + # end validated_params end diff --git a/test/integration/cis/verify b/test/integration/cis/verify index 34b7594..39c0b1a 160000 --- a/test/integration/cis/verify +++ b/test/integration/cis/verify @@ -1 +1 @@ -Subproject commit 34b7594caebf73b173aa189584666a8a51de79f0 +Subproject commit 39c0b1a71749969e282046df7ba797cfa6e95984 From bfec3ba7666f3de464e15af9ebde5e883c323a8a Mon Sep 17 00:00:00 2001 From: Rony Xavier Date: Mon, 5 Feb 2018 19:31:27 -0500 Subject: [PATCH 11/12] Resource Updates Signed-off-by: Rony Xavier --- libraries/aws_iam_group.rb | 74 +++++++++++++++++++++++++++++++++++ libraries/aws_iam_groups.rb | 45 +++++++++++++++++++++ libraries/aws_route_tables.rb | 44 +++++++++++++++++++++ 3 files changed, 163 insertions(+) create mode 100644 libraries/aws_iam_group.rb create mode 100644 libraries/aws_iam_groups.rb create mode 100644 libraries/aws_route_tables.rb diff --git a/libraries/aws_iam_group.rb b/libraries/aws_iam_group.rb new file mode 100644 index 0000000..649a10e --- /dev/null +++ b/libraries/aws_iam_group.rb @@ -0,0 +1,74 @@ +require '_aws' + +class AwsIamGroup < Inspec.resource(1) + name 'aws_iam_group' + desc 'Verifies settings for AWS IAM Group' + example " + describe aws_iam_group('mygroup') do + it { should exist } + end + " + + include AwsResourceMixin + attr_reader :group_name, :users + + def to_s + "IAM Group #{group_name}" + end + + def inline_policies + return [] unless @exists + AwsIamGroup::BackendFactory.create.list_group_policies(group_name: group_name).policy_names + end + + def attached_policies + return [] unless @exists + AwsIamGroup::BackendFactory.create.list_attached_group_policies(group_name: group_name).attached_policies.map(&:policy_name) + end + + private + + def validate_params(raw_params) + validated_params = check_resource_param_names( + raw_params: raw_params, + allowed_params: [:group_name], + allowed_scalar_name: :group_name, + allowed_scalar_type: String, + ) + + if validated_params.empty? + raise ArgumentError, 'You must provide a group_name to aws_iam_group.' + end + + validated_params + end + + def fetch_from_aws + backend = AwsIamGroup::BackendFactory.create + + begin + resp = backend.get_group(group_name: group_name) + @exists = true + @aws_group_struct = resp[:group] + @users = resp[:users].map(&:user_name) + rescue Aws::IAM::Errors::NoSuchEntity + @exists = false + end + end + + class Backend + BackendFactory.set_default_backend(self) + + def get_group(query) + AWSConnection.new.iam_client.get_group(query) + end + + def list_group_policies(query) + AWSConnection.new.iam_client.list_group_policies(query) + end + + def list_attached_group_policies(query) + AWSConnection.new.iam_client.list_attached_group_policies(query) + end + end +end diff --git a/libraries/aws_iam_groups.rb b/libraries/aws_iam_groups.rb new file mode 100644 index 0000000..0ab76fe --- /dev/null +++ b/libraries/aws_iam_groups.rb @@ -0,0 +1,45 @@ +class AwsIamGroups < Inspec.resource(1) + name 'aws_iam_groups' + desc 'Verifies settings for AWS IAM groups in bulk' + example ' + describe aws_iam_groups do + it { should exist } + end + ' + + # Underlying FilterTable implementation. + filter = FilterTable.create + filter.add_accessor(:entries) + .add(:exists?) { |x| !x.entries.empty? } + .add(:group_names, field: :group_name) + .add(:group_ids, field: :group_id) + .add(:arns, field: :arn) + filter.connect(self, :group_data) + + def group_data + @table + end + + def to_s + 'IAM Groups' + end + + def initialize + backend = AwsIamGroups::BackendFactory.create + @table = backend.list_groups.to_h[:groups] + end + + class BackendFactory + extend AwsBackendFactoryMixin + end + + class Backend + class AwsClientApi + BackendFactory.set_default_backend(self) + + def list_groups(query = {}) + AWSConnection.new.iam_client.list_groups(query) + end + end + end +end diff --git a/libraries/aws_route_tables.rb b/libraries/aws_route_tables.rb new file mode 100644 index 0000000..b9a02b2 --- /dev/null +++ b/libraries/aws_route_tables.rb @@ -0,0 +1,44 @@ +class AwsRouteTables < Inspec.resource(1) + name 'aws_route_tables' + desc 'Verifies settings for AWS Route Tables in bulk' + example ' + describe aws_route_tables do + it { should exist } + end + ' + + # Underlying FilterTable implementation. + filter = FilterTable.create + filter.add_accessor(:entries) + .add(:exists?) { |x| !x.entries.empty? } + .add(:vpc_ids, field: :vpc_id) + .add(:route_table_ids, field: :route_table_id) + filter.connect(self, :routes_data) + + def routes_data + @table + end + + def to_s + 'Route Tables' + end + + def initialize + backend = AwsRouteTables::BackendFactory.create + @table = backend.describe_route_tables({}).to_h[:route_tables] # max value for limit is 1000 + end + + class BackendFactory + extend AwsBackendFactoryMixin + end + + class Backend + class AwsClientApi + BackendFactory.set_default_backend(self) + + def describe_route_tables(query = {}) + AWSConnection.new.ec2_client.describe_route_tables(query) + end + end + end +end From e61c661116a59e43951d12a4b714791743add541 Mon Sep 17 00:00:00 2001 From: Rony Xavier Date: Fri, 16 Feb 2018 03:49:39 -0500 Subject: [PATCH 12/12] updates Signed-off-by: Rony Xavier --- libraries/aws_ec2_security_groups.rb | 1 + libraries/aws_s3_bucket.rb | 18 ++++++++++++++++++ libraries/aws_vpc.rb | 1 + test/integration/cis/verify | 2 +- 4 files changed, 21 insertions(+), 1 deletion(-) diff --git a/libraries/aws_ec2_security_groups.rb b/libraries/aws_ec2_security_groups.rb index 4200153..6db0d99 100644 --- a/libraries/aws_ec2_security_groups.rb +++ b/libraries/aws_ec2_security_groups.rb @@ -27,6 +27,7 @@ def initialize(raw_criteria = {}) .add_accessor(:entries) .add(:exists?) { |x| !x.entries.empty? } .add(:group_ids, field: :group_id) + .add(:group_name, field: :group_name) filter.connect(self, :access_key_data) def access_key_data diff --git a/libraries/aws_s3_bucket.rb b/libraries/aws_s3_bucket.rb index e8ad98d..23c6323 100644 --- a/libraries/aws_s3_bucket.rb +++ b/libraries/aws_s3_bucket.rb @@ -35,6 +35,24 @@ def public? bucket_policy.any? { |s| s.effect == 'Allow' && s.principal == '*' } end + def has_acl_public_read? + puts 'AllUsers' ,bucket_acl.select { |g| g.grantee.type == 'Group' && g.grantee.uri =~ /AllUsers/ }.map(&:permission) + puts 'AuthenticatedUsers', bucket_acl.select { |g| g.grantee.type == 'Group' && g.grantee.uri =~ /AuthenticatedUsers/ }.map(&:permission) + # first line just for formatting + false || \ + bucket_acl.select { |g| g.grantee.type == 'Group' && g.grantee.uri =~ /AllUsers/ }.map(&:permission).include?('READ') || \ + bucket_acl.select { |g| g.grantee.type == 'Group' && g.grantee.uri =~ /AuthenticatedUsers/ }.map(&:permission).include?('READ') + end + + def has_acl_public_write? + puts 'AllUsers', bucket_acl.select { |g| g.grantee.type == 'Group' && g.grantee.uri =~ /AllUsers/ }.map(&:permission) + puts 'AuthenticatedUsers', bucket_acl.select { |g| g.grantee.type == 'Group' && g.grantee.uri =~ /AuthenticatedUsers/ }.map(&:permission) + # first line just for formatting + false || \ + bucket_acl.select { |g| g.grantee.type == 'Group' && g.grantee.uri =~ /AllUsers/ }.map(&:permission).include?('WRITE') || \ + bucket_acl.select { |g| g.grantee.type == 'Group' && g.grantee.uri =~ /AuthenticatedUsers/ }.map(&:permission).include?('WRITE') + end + def has_access_logging_enabled? return unless @exists # This is simple enough to inline it. diff --git a/libraries/aws_vpc.rb b/libraries/aws_vpc.rb index 1459434..fe1e1da 100644 --- a/libraries/aws_vpc.rb +++ b/libraries/aws_vpc.rb @@ -27,6 +27,7 @@ def flow_logs backend = AwsVpc::BackendFactory.create filter = { name: "resource-id", values: [@vpc_id],} resp = backend.describe_flow_logs({filter: [filter]}) + resp.flow_logs end def flow_logs_enabled? diff --git a/test/integration/cis/verify b/test/integration/cis/verify index 39c0b1a..beab41d 160000 --- a/test/integration/cis/verify +++ b/test/integration/cis/verify @@ -1 +1 @@ -Subproject commit 39c0b1a71749969e282046df7ba797cfa6e95984 +Subproject commit beab41dd60a8ba3ad33e68e706097471ec760a4a