diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 548b603a91..468a87868f 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,23 +1,17 @@ # Contributing to CloudSploit - Thank you for your interest in contributing to CloudSploit! We welcome your PRs, issues, feedback, and other contributions to this open source repository. To keep things moving smoothly, please use the following guidelines when working with the CloudSploit source code. ## Code of Conduct - The CloudSploit project, maintainers, and contributors are governed by the [CloudSploit Code of Conduct](CODE_OF_CONDUCT.md). By contributing, you are agreeing to uphold this code in your interactions with the CloudSploit community. ## License - -By contributing code to CloudSploit, you attest that you have the rights to all code and that you are assigning these rights to CloudSploit LLC for use within its projects. +By contributing code to CloudSploit, you attest that you have the rights to all code and that you are assigning these rights to Aqua Security, Ltd. for use within its projects. ## Getting Started - -Please read our [README](../README.md#installation) for information on getting setup to use and develop CloudSploit scans locally. We also have a [guide for writing new plugins](../README.md#writing-a-plugin). +Please read our [README](../README.md#installation) for information on getting setup to use and develop CloudSploit scans locally. We also have a [guide for writing new plugins](../docs/writing-plugins.md). ## Proposing Large Changes - While we welcome all contributions, large pull requests that make significant changes to the codebase are difficult to review are merge without prior discussion. Please open an issue to discuss these changes before beginning work on them. ## Questions? - Please ask all questions in the form of GitHub issues. diff --git a/.gitignore b/.gitignore index d348a1d269..b12a7ce315 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ vscode/* .idea coverage/ .nyc_output +config.js \ No newline at end of file diff --git a/README.md b/README.md index 4e037fcdec..349b529523 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,59 @@ -[](https://cloudsploit.com) +[](https://cloud.aquasec.com/signup) [![Build Status](https://travis-ci.org/cloudsploit/scans.svg?branch=master)](https://travis-ci.org/cloudsploit/scans) -CloudSploit Scans +CloudSploit by Aqua - Cloud Security Scans ================= +[](https://cloud.aquasec.com/signup) + +## Quick Start +``` +$ git clone git@github.com:cloudsploit/scans.git +$ cd scans +$ npm install +$ ./index.js -h +``` + +## Documentation +* [Background](#background) +* [Deployment Options](#deployment-options) + + [Self-Hosted](#self-hosted) + + [Hosted at Aqua Wave](#hosted-at-aqua-wave) +* [Installation](#installation) +* [Configuration](#configuration) + + [Amazon Web Services](docs/aws.md#cloud-provider-configuration) + + [Microsoft Azure](docs/azure.md#cloud-provider-configuration) + + [Google Cloud Platform](docs/gcp.md#cloud-provider-configuration) + + [Oracle Cloud Infrastructure](docs/oracle.md#cloud-provider-configuration) + + [CloudSploit Config File](#cloudsploit-config-file) + + [Credential Files](#credential-files) + + [AWS](#aws) + + [Azure](#azur) + + [GCP](#gcp) + + [Oracle OCI](#oracle-oci) + + [Environment Variables](#environment-variables) +* [Running](#running) +* [CLI Options](#cli-options) +* [Compliance](#compliance) + + [HIPAA](#hipaa) + + [PCI](#pci) + + [CIS Benchmarks](#cis-benchmarks) +* [Output Formats](#output-formats) + + [Console Output](#console-output) + + [Ignoring Passing Results](#ignoring-passing-results) + + [CSV](#csv) + + [JSON](#json) + + [JUnit XML](#junit-xml) + + [Collection Output](#collection-output) +* [Suppressions](#suppressions) +* [Running a Single Plugin](#running-a-single-plugin) +* [Architecture](#architecture) +* [Writing a Plugin](#writing-a-plugin) +* [Other Notes](#other-notes) + ## Background -CloudSploit scans is an open-source project designed to allow detection of security risks in cloud infrastructure accounts, including: Amazon Web Services (AWS), Microsoft Azure, Google Cloud Platform (GCP), and Oracle Cloud Infrastructure (OCI). These scripts are designed to return a series of potential misconfigurations and security risks. +CloudSploit by Aqua is an open-source project designed to allow detection of security risks in cloud infrastructure accounts, including: Amazon Web Services (AWS), Microsoft Azure, Google Cloud Platform (GCP), Oracle Cloud Infrastructure (OCI), and GitHub. These scripts are designed to return a series of potential misconfigurations and security risks. ## Deployment Options CloudSploit is available in two deployment options: @@ -14,939 +61,281 @@ CloudSploit is available in two deployment options: ### Self-Hosted Follow the instructions below to deploy the open-source version of CloudSploit on your machine in just a few simple steps. -### Hosted at Aqua Cloud -CloudSploit by Aqua, hosted in the Aqua Cloud, is a fully managed service CSPM solution maintained and updated by the cloud security experts at Aqua. Our hosted scanner handles the scheduling and running of background scans, aggregation of data into dashboards, tools, and visualizations, and integrates with popular third-party services for alerts. - -Sign up for [Aqua Cloud](https://cloud.aquasec.com/signup) today! +### Hosted at Aqua Wave +A commercial version of CloudSploit hosted at Aqua Wave. Try [Aqua Wave](https://cloud.aquasec.com/signup) today! ## Installation Ensure that NodeJS is installed. If not, install it from [here](https://nodejs.org/download/). ``` -git clone git@github.com:cloudsploit/scans.git -``` - -``` -npm install +$ git clone git@github.com:cloudsploit/scans.git +$ npm install ``` ## Configuration -To begin using the scanner, edit the `index.js` file with the corresponding settings. You can use any of these three options: - * Enter your settings [inline](https://github.com/cloudsploit/scans/blob/master/index.js#L13-L53). - * Create a json [file](https://github.com/cloudsploit/scans/blob/master/index.js#L57-L61). - * Use [environment variables](https://github.com/cloudsploit/scans/blob/master/index.js#L64-L109). - -Cloud Infrastructure configuration steps: - -* [AWS](#aws) -* [Azure](#azure) -* [GCP](#gcp) -* [Oracle](#oracle) - -#### AWS - -Create a "cloudsploit" user, with the `SecurityAudit` policy. - -1. Navigate to the [IAM console](https://console.aws.amazon.com/iam/home). -1. Go to Users -1. Create a new user (Add user) -1. Set the username to "cloudsploit" -1. Set the access type to "Programmatic access", click Next. -1. Select one of your preferred options, if you have a group with SecurityAudit role assign the new user to that group. -1. If not select the "Attach existing policies directly" and select the SecurityAudit policy, click Next. -1. Set tags as needed and then click on "Create user". -1. Make sure you safely store the Access key ID and Secret access key. -1. Paste them into the corresponding AWS credentials section of the `index.js` file. +CloudSploit requires read-only permission to your cloud account. Follow the guides below to provision this access: - -If using environment variables, the same ones expected by the aws sdks, namely `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, and `AWS_SESSION_TOKEN`, can be used. +* [Amazon Web Services](docs/aws.md#cloud-provider-configuration) +* [Microsoft Azure](docs/azure.md#cloud-provider-configuration) +* [Google Cloud Platform](docs/gcp.md#cloud-provider-configuration) +* [Oracle Cloud Infrastructure](docs/oracle.md#cloud-provider-configuration) -For more information on using our hosted scanner, [click here](#other-notes) +For AWS, you can run CloudSploit directly and it will detect credentials using the default [AWS credential chain](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/CredentialProviderChain.html). -#### Azure - -1. Log into your Azure Portal and navigate to the Azure Active Directory service. -1. Select App registrations and then click on New registration. -1. Enter "CloudSploit" and/or a descriptive name in the Name field, take note of it, it will be used again in step 3. -1. Leave the "Supported account types" default: "Accounts in this organizational directory only (YOURDIRECTORYNAME)". -1. Click on Register. -1. Copy the Application ID and Paste it below. -1. Copy the Directory ID and Paste it below. -1. Click on Certificates & secrets. -1. Under Client secrets, click on New client secret. -1. Enter a Description (i.e. Cloudsploit-2019) and select Expires "In 1 year". -1. Click on Add. -1. The Client secret value appears only once, make sure you store it safely. -1. Navigate to Subscriptions. -1. Click on the relevant Subscription ID, copy and paste the ID below. -1. Click on "Access Control (IAM)". -1. Go to the Role assignments tab. -1. Click on "Add", then "Add role assignment". -1. In the "Role" drop-down, select "Security Reader". -1. Leave the "Assign access to" default value. -1. In the "Select" drop-down, type the name of the app registration (e.g. "CloudSploit") you created and select it. -1. Click "Save". -1. Repeat the process for the role "Log Analytics Reader" - -#### GCP - -1. Log into your Google Cloud console and navigate to IAM Admin > Service Accounts. -1. Click on "Create Service Account". -1. Enter "CloudSploit" in the "Service account name", then enter "CloudSploit API Access" in the description. -1. Click on Continue. -1. Select the role: Project > Viewer. -1. Click on Continue. -1. Click on "Create Key". -1. Leave the default JSON selected. -1. Click on "Create". -1. The key will be downloaded to your machine. -1. Open the JSON key file, in a text editor and copy the Project Id, Client Email and Private Key values into the `index.js` file. -1. Enter the APIs & Services category. -1. Select Enable APIS & SERVICES at the top of the page -1. Search for DNS, then Select the option that appears and Enable it. -1. Enable all the APIs used to run scans, they are as follows: Stackdriver Monitoring, Stackdriver Logging, Compute, Cloud Key Management, Cloud SQL Admin, Kubernetes, Service Management, and Service Networking. - -#### Oracle - -1. Log into your Oracle Cloud console and navigate to Administration > Tenancy Details. -1. Copy your Tenancy OCID and paste it in the index file. -1. Navigate to Identity > Users. -1. Click on Create User. -1. Enter "CloudSploit", then enter "CloudSploit API Access" in the description. -1. Click on Create. -1. Copy the User OCID and paste it in the index file. -1. Follow the steps to Generate an API Signing Key listed on Oracle's Cloud Doc(https://docs.cloud.oracle.com/iaas/Content/API/Concepts/apisigningkey.htm#How). -1. Open the public key (oci_api_key_public.pem) in your preferred text editor and copy the plain text (everything). Click on Add Public Key, then click on Add. -1. Copy the public key fingerprint and paste it in the index file. -1. Open the private key (oci_api_key.pem) in your preferred text editor and paste it in the index file. -1. Navigate to Identity > Groups. -1. Click on Create Group. -1. Enter "SecurityAudit" in the Name field, then enter "CloudSploit Security Audit Access" in the description. -1. Click on Submit. -1. Click on the SecurityAudit group in the Groups List and Add the CloudSploit API User to the group. -1. Navigate to Identity > Policies. -1. Click on Create Policy. -1. Enter "SecurityAudit" in the Name field, then enter "CloudSploit Security Audit Policy" in the description. -1. Copy and paste the following statement: -1. ALLOW GROUP SecurityAudit to READ all-resources in tenancy -1. Click on Create. -1. Navigate to Identity > Compartments. -1. Select your root compartment or the compartment being audited. -1. Click on "Copy" by your Compartment OCID. - -## Running - -To run a standard scan, showing all outputs and results, simply run: +### CloudSploit Config File +The CloudSploit config file allows you to pass cloud provider credentials by: +1. A JSON file on your file system +1. Environment variables +1. Hard-coding (not recommended) +Start by copying the example config file: ``` -node index.js +$ cp config_example.js config.js ``` -In the list of plugins in the `exports.js` file, comment out any plugins you do not wish to run. You can also skip entire regions by modifying the `skipRegions` array. - - -## Compliance - -CloudSploit also supports mapping of its plugins to particular compliance policies. To run the compliance scan, use the `--compliance` flag. For example: -``` -node index.js --compliance=hipaa -node index.js --compliance=pci -``` - -CloudSploit currently supports the following compliance mappings: - -### HIPAA - -HIPAA scans map CloudSploit plugins to the Health Insurance Portability and Accountability Act of 1996. - -### PCI - -PCI scans map CloudSploit plugins to the Payment Card Industry Data Security Standard. - -## Output Formats - -CloudSploit supports output in several formats for consumption by other tools. -If you do not specify otherwise, CloudSploit writes output to standard output -(the console). - -You can ignore results from output that return an OK status by passing a `--ignore-ok` commandline argument. - -You can specify one or more output formats as follows: - +Edit the config file by uncommenting the relevant sections for the cloud provider you are testing. Each cloud has both a `credential_file` option, as well as inline options. For example: ``` -# Output results in CSV (suppressing the console output) -node index.js --csv=./out.csv - -# Output results in JSON (suppressing the console output) -node index.js --json=./out.json - -# Output results in JUnit XML (suppressing the console output) -node index.js --junit=./out.xml - -# Output collection results in JSON -node index.js --collection=./collection.json - -# Output results only to the console (default if omitted) -node index.js --console - -# Output results in all supported formats -node index.js --console --junit=./out.xml --csv=./out.csv - -# Output results in all supported formats for any test that is not OK. -node index.js --console --junit=./out.xml --csv=./out.csv --ignore-ok +azure: { + // OPTION 1: If using a credential JSON file, enter the path below + // credential_file: '/path/to/file.json', + // OPTION 2: If using hard-coded credentials, enter them below + // application_id: process.env.AZURE_APPLICATION_ID || '', + // key_value: process.env.AZURE_KEY_VALUE || '', + // directory_id: process.env.AZURE_DIRECTORY_ID || '', + // subscription_id: process.env.AZURE_SUBSCRIPTION_ID || '' +} ``` +### Credential Files +If you use the `credential_file` option, point to a file in your file system that follows the correct format for the cloud you are using. - -## Architecture - -CloudSploit works in two phases. First, it queries the cloud infrastructure APIs for various metadata about your account, namely the "collection" phase. Once all the necessary data is collected, the result is passed to the "scanning" phase. The scan uses the collected data to search for potential misconfigurations, risks, and other security issues, which are the resulting output. -## Writing a Plugin - -### Collection Phase - -To write a plugin, you want to understand which data is needed and how your cloud infrastructure provides them via their API calls. Once you have identified the API calls needed, you can add them to the collect.js file for your cloud infrastructure provider. This file determines the cloud infrastructure API calls and their run-order. - -### Collectors - -* [AWS Collection](#aws-collection) -* [Azure Collection](#azure-collection) -* [GCP Collection](#gcp-collection) -* [Oracle Collection](#oracle-collection) - -#### AWS Collection - -The following declaration tells the CloudSploit collection engine to query the CloudFront service using the `listDistributions` call and then save the results returned under `DistributionList.Items`. - +#### AWS ``` -CloudFront: { - listDistributions: { - property: 'DistributionList', - secondProperty: 'Items' - } -}, +{ + "accessKeyId": "YOURACCESSKEY", + "secretAccessKey": "YOURSECRETKEY" +} ``` -The second section in `collect.js` is `postcalls`, which is an array of objects defining API calls that rely on other calls first returned. For example, if you need to query for all `CloudFront distributions`, and then loop through each one and run a more detailed call, you would add the `CloudFront:listDistributions` call in the [`calls`](https://github.com/cloudsploit/scans/blob/master/collectors/aws/collector.js#L58-L64) section and then the more detailed call in [`postcalls`](https://github.com/cloudsploit/scans/blob/master/collectors/aws/collector.js#L467-L473), setting it to rely on the output of `listDistributions` call. - -An example: - +#### Azure ``` -getGroup: { - reliesOnService: 'iam', - reliesOnCall: 'listGroups', - filterKey: 'GroupName', - filterValue: 'GroupName' -}, +{ + "ApplicationID": "YOURAZUREAPPLICATIONID", + "KeyValue": "YOURAZUREKEYVALUE", + "DirectoryID": "YOURAZUREDIRECTORYID", + "SubscriptionID": "YOURAZURESUBSCRIPTIONID" +} ``` -This section tells CloudSploit to wait until the `IAM:listGroups` call has been made, and then loop through the data that is returned. The `filterKey` tells CloudSploit the name of the key from the original response, while `filterValue` tells it which property to set in the `getGroup` call filter. For example: `iam.getGroup({GroupName:abc})` where `abc` is the `GroupName` from the returned list. CloudSploit will loop through each response, re-invoking `getGroup` for each element. - -You can find the [AWS Collector here.](https://github.com/cloudsploit/scans/blob/master/collectors/aws/collector.js) - -#### Azure Collection - -The following declaration tells the Cloudsploit collection engine to query the Compute Management Service using the virtualMachines:listAll call. - +#### GCP +Note: For GCP, you [generate a JSON file](docs/gcp.md) directly from the GCP console, which you should not edit. ``` -virtualMachines: { - listAll: { - api: "ComputeManagementClient", - arm: true - } -}, +{ + "type": "service_account", + "project": "GCPPROJECTNAME", + "client_email": "GCPCLIENTEMAIL", + "private_key": "GCPPRIVATEKEY" +} ``` -The second section in `collect.js` is `postcalls`, which is an array of objects defining API calls that rely on other calls first returned. For example, if you need to query for all `Virtual Machine instances`, and then loop through each one and run a more detailed call, you would add the `virtualMachines:listAll` call in the [`calls`](https://github.com/cloudsploit/scans/blob/master/collectors/azure/collector.js#L50-L55) section and then the more detailed call in [`postcalls`](https://github.com/cloudsploit/scans/blob/master/collectors/azure/collector.js#L293-L302), setting it to rely on the output of `listDistributions` call. - +#### Oracle OCI ``` -virtualMachineExtensions: { - list: { - api: "ComputeManagementClient", - reliesOnService: ['resourceGroups', 'virtualMachines'], - reliesOnCall: ['list', 'listAll'], - filterKey: ['resourceGroupName', 'name'], - filterValue: ['resourceGroupName', 'name'], - arm: true - } -}, +{ + "tenancyId": "YOURORACLETENANCYID", + "compartmentId": "YOURORACLECOMPARTMENTID", + "userId": "YOURORACLEUSERID", + "keyFingerprint": "YOURORACLEKEYFINGERPRINT", + "keyValue": "YOURORACLEKEYVALUE", +} ``` -You can find the [Azure Collector here.](https://github.com/cloudsploit/scans/blob/master/collectors/azure/collector.js) - -#### GCP Collection - -The following declaration tells the Cloudsploit collection engine to query the Compute Management Service using the buckets:list call. +### Environment Variables +CloudSploit supports passing environment variables, but you must first uncomment the section of your `config.js` file relevant to the cloud provider being scanned. +You can then pass the variables listed in each section. For example, for AWS: ``` -buckets: { - list: { - api: 'storage', - version: 'v1', - location: null, - } -}, +{ + access_key: process.env.AWS_ACCESS_KEY_ID || '', + secret_access_key: process.env.AWS_SECRET_ACCESS_KEY || '', + session_token: process.env.AWS_SESSION_TOKEN || '', +} ``` -The second section in `collect.js` is `postcalls`, which is an array of objects defining API calls that rely on other calls first returned. For example, if you need to query for all `Storage Buckets`, and then loop through each one and run a more detailed call, you would add the `buckets:list` call in the [`calls`](https://github.com/cloudsploit/scans/blob/master/collectors/google/collector.js#L103-L109) section and then the more detailed call in [`postcalls`](https://github.com/cloudsploit/scans/blob/master/collectors/google/collector.js#L213-L223), setting it to rely on the output of `getIamPolicy` call. - -``` -buckets: { - getIamPolicy: { - api: 'storage', - version: 'v1', - location: null, - reliesOnService: ['buckets'], - reliesOnCall: ['list'], - filterKey: ['bucket'], - filterValue: ['name'], - } -}, +## Running +To run a standard scan, showing all outputs and results, simply run: ``` +$ ./index.js +``` + +## CLI Options +CloudSploit supports many options to customize the run time. Some popular options include: +* AWS GovCloud support: `--govcloud` +* AWS China support: `--china` +* Save the raw cloud provider response data: `--collection=file.json` +* Ignore passing (OK) results: `--ignore-ok` +* Exit with a non-zero code if non-passing results are found: `--exit-code` + * This is a good option for CI/CD systems +* Change the output from a table to raw text: `--console=text` + +See [Output Formats](#output-formates) below for more output options. + +
+ Click for a full list of options + + ``` + $ ./index.js -h + + _____ _ _ _____ _ _ _ + / ____| | | |/ ____| | | (_) | + | | | | ___ _ _ __| | (___ _ __ | | ___ _| |_ + | | | |/ _ \| | | |/ _` |\___ \| '_ \| |/ _ \| | __| + | |____| | (_) | |_| | (_| |____) | |_) | | (_) | | |_ + \_____|_|\___/ \__,_|\__,_|_____/| .__/|_|\___/|_|\__| + | | + |_| + + CloudSploit by Aqua Security, Ltd. + Cloud security auditing for AWS, Azure, GCP, Oracle, and GitHub + + usage: index.js [-h] --config CONFIG [--compliance {hipaa,cis,cis1,cis2,pci}] [--plugin PLUGIN] [--govcloud] [--china] [--csv CSV] [--json JSON] [--junit JUNIT] + [--table] [--console {none,text,table}] [--collection COLLECTION] [--ignore-ok] [--exit-code] [--skip-paginate] [--suppress SUPPRESS] + + optional arguments: + -h, --help show this help message and exit + --config CONFIG + The path to a cloud provider credentials file. + --compliance {hipaa,cis,cis1,cis2,pci} + Compliance mode. Only return results applicable to the selected program. + --plugin PLUGIN A specific plugin to run. If none provided, all plugins will be run. Obtain from the exports.js file. E.g. acmValidation + --govcloud AWS only. Enables GovCloud mode. + --china AWS only. Enables AWS China mode. + --csv CSV Output: CSV file + --json JSON Output: JSON file + --junit JUNIT Output: Junit file + --table Output: table + --console {none,text,table} + Console output format. Default: table + --collection COLLECTION + Output: full collection JSON as file + --ignore-ok Ignore passing (OK) results + --exit-code Exits with a non-zero status code if non-passing results are found + --skip-paginate AWS only. Skips pagination (for debugging). + --suppress SUPPRESS Suppress results matching the provided Regex. Format: pluginId:region:resourceId + ``` +
-You can find the [GCP Collector here.](https://github.com/cloudsploit/scans/blob/master/collectors/google/collector.js) - -#### Oracle Collection - -The following declaration tells the Cloudsploit collection engine to query the Compute Management Service using the vcn:list call. +## Compliance +CloudSploit supports mapping of its plugins to particular compliance policies. To run the compliance scan, use the `--compliance` flag. For example: ``` -vcn: { - list: { - api: "core", - filterKey: ['compartmentId'], - filterValue: ['compartmentId'], - } -}, +$ ./index.js --compliance=hipaa +$ ./index.js --compliance=pci ``` -The second section in `collect.js` is `postcalls`, which is an array of objects defining API calls that rely on other calls first returned. For example, if you need to query for all `VCNs`, and then loop through each one and run a more detailed call, you would add the `vcn:list` call in the [`calls`](https://github.com/cloudsploit/scans/blob/master/collectors/oracle/collector.js#L41-L47) section and then the more detailed call in [`postcalls`](https://github.com/cloudsploit/scans/blob/master/collectors/oracle/collector.js#L243-L251), setting it to rely on the output of `get` call. - +Multiple compliance modes can be run at the same time: ``` -vcn: { - get: { - api: "core", - reliesOnService: ['vcn'], - reliesOnCall: ['list'], - filterKey: ['vcnId'], - filterValue: ['id'], - } -}, +$ ./index.js --compliance=cis1 --compliance=cis2 ``` -You can find the [Oracle Collector here.](https://github.com/cloudsploit/scans/blob/master/collectors/oracle/collector.js) - -### Scanning Phase - -After the data has been collected, it is passed to the scanning engine when the results are analyzed for risks. Each plugin must export the following: - -* Exports the following: - * ```title``` (string): a user-friendly title for the plugin - * ```category``` (string): the cloud infrastructure category (i.e.: **_AWS:_** EC2, RDS, ELB, etc. **_Azure:_** ) - * ```description``` (string): a description of what the plugin does - * ```more_info``` (string): a more detailed description of the risk being tested for - * ```link``` (string): an cloud infrastructure help URL describing the service or risk, preferably with mitigation methods - * ```recommended_action``` (string): what the user should do to mitigate the risk found - * ```run``` (function): a function that runs the test (see below) -* Accepts a ```collection``` object via the run function containing the full collection object obtained in the first phase. -* Calls back with the results and the data source. - -### Result Codes -Each test has a result code that is used to determine if the test was successful and its risk level. The following codes are used: - -* 0: PASS: No risks -* 1: WARN: The result represents a potential misconfiguration or issue but is not an immediate risk -* 2: FAIL: The result presents an immediate risk to the security of the account -* 3: UNKNOWN: The results could not be determined (API failure, wrong permissions, etc.) - -### Tips for Writing Plugins -* Many security risks can be detected using the same API calls. To minimize the number of API calls being made, utilize the `cache` helper function to cache the results of an API call made in one test for future tests. For example, two plugins: "s3BucketPolicies" and "s3BucketPreventDelete" both call APIs to list every S3 bucket. These can be combined into a single plugin "s3Buckets" which exports two tests called "bucketPolicies" and "preventDelete". This way, the API is called once, but multiple tests are run on the same results. -* Ensure cloud infrastructure API calls are being used optimally. For example, call describeInstances with empty parameters to get all instances, instead of calling describeInstances multiple times looping through each instance name. -* Use async.eachLimit to reduce the number of simultaneous API calls. Instead of using a for loop on 100 requests, spread them out using async's each limit. - -### Example -#### AWS -To more clearly illustrate writing a new plugin, let's consider the "IAM Empty Groups" plugin. First, we know that we will need to query for a list of groups via `listGroups`, then loop through each group and query for the more detailed set of data via `getGroup`. - -We'll add these API calls to `collect.js`. First, under `calls` add: -``` -IAM: { - listGroups: { - property: 'Groups' - } -}, -``` -The `property` tells CloudSploit which property to read in the response from AWS. +CloudSploit currently supports the following compliance mappings: -Then, under `postCalls`, add: +### HIPAA ``` -IAM: { - getGroup: { - reliesOnService: 'iam', - reliesOnCall: 'listGroups', - filterKey: 'GroupName', - filterValue: 'GroupName' - } -}, +$ ./index.js --compliance=hipaa ``` -CloudSploit will first get the list of groups, then, it will loop through each one, using the group name to get more detailed info via `getGroup`. - -Next, we'll write the plugin. Create a new file in the `plugins/iam` folder called `emptyGroups.js` (this plugin already exists, but you can create a similar one for the purposes of this example). +HIPAA scans map CloudSploit plugins to the Health Insurance Portability and Accountability Act of 1996. -In the file, we'll be sure to export the plugin's title, category, description, link, and more information about it. Additionally, we will add any API calls it makes: -``` -apis: ['IAM:listGroups', 'IAM:getGroup'], -``` -In the `run` function, we can obtain the output of the collection phase from earlier by doing: -``` -var listGroups = helpers.addSource(cache, source, - ['iam', 'listGroups', region]); -``` -Then, we can loop through each of the results and do: +### PCI ``` -var getGroup = helpers.addSource(cache, source, - ['iam', 'getGroup', region, group.GroupName]); +$ ./index.js --compliance=pci ``` -The `helpers` function ensures that the proper results are returned from the collection and that they are saved into a "source" variable which can be returned with the results. +PCI scans map CloudSploit plugins to the Payment Card Industry Data Security Standard. -Now, we can write the plugin functionality by checking for the data relevant to our requirements: -``` -if (!getGroup || getGroup.err || !getGroup.data || !getGroup.data.Users) { - helpers.addResult(results, 3, 'Unable to query for group: ' + group.GroupName, 'global', group.Arn); -} else if (!getGroup.data.Users.length) { - helpers.addResult(results, 0, 'Group: ' + group.GroupName + ' does not contain any users', 'global', group.Arn); - return cb(); -} else { - helpers.addResult(results, 0, 'Group: ' + group.GroupName + ' contains ' + getGroup.data.Users.length + ' user(s)', 'global', group.Arn); -} +### CIS Benchmarks ``` -The `addResult` function ensures we are adding the results to the `results` array in the proper format. This function accepts the following: +$ ./index.js --compliance=cis +$ ./index.js --compliance=cis1 +$ ./index.js --compliance=cis2 ``` -(results array, score, message, region, resource) -``` -The `resource` is optional, and the `score` must be between 0 and 3 to indicate PASS, WARN, FAIL, or UNKNOWN. -#### Azure -To more clearly illustrate writing a new plugin, let us consider the Virtual Machines VM Endpoint Protection plugin `plugins/azure/virtualmachines/vmEndpointProtection.js` . First, we know that we will need to query for a list of virtual machines via `virtualMachines:listAll`, then loop through each group and query for the more detailed set of data via `virtualMachineExtensions:list`. +CIS Benchmarks are supported, both for Level 1 and Level 2 controls. Passing `--compliance=cis` will run both level 1 and level 2 controls. -We'll add these API calls to `collect.js`. First, under `calls` add: +## Output Formats +CloudSploit supports output in several formats for consumption by other tools. If you do not specify otherwise, CloudSploit writes output to standard output (the console) as a table. +Note: You can pass multiple output formats and combine options for further customization. For example: ``` -virtualMachines: { - listAll: { - url: 'https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.Compute/virtualMachines?api-version=2019-12-01' - } -}, -``` +# Print a table to the console and save a CSV file +$ ./index.js --csv=file.csv --console=table -Then, under `postcalls`, add: -``` -virtualMachineExtensions: { - list: { - reliesOnPath: 'virtualMachines.listAll', - properties: ['id'], - url: 'https://management.azure.com/{id}/extensions?api-version=2019-12-01' - } -}, +# Print text to the console and save a JSON and JUnit file while ignoring passing results +$ ./index.js --json=file.json --junit=file.xml --console=text --ignore-ok ``` -CloudSploit will first get the list of virtual machines, then, it will loop through each one, using the virtual machine name to get more detailed info via `virtualMachineExtensions`. -Next, we'll write the plugin. Create a new file in the `plugins/virtualmachines` folder called `vmEndpointProtection.js` (this plugin already exists, but you can create a similar one for the purposes of this example). - -In the file, we'll be sure to export the plugin's title, category, description, link, and more information about it. Additionally, we will add any API calls it makes: -``` -apis: ['virtualMachines:listAll', 'virtualMachineExtensions:list'], -``` -In the `run` function, we can obtain the output of the collection phase from earlier by doing: -``` -var virtualMachines = helpers.addSource(cache, source, - ['virtualMachines', 'listAll', location]); -``` -Then, we can loop through each of the results and do: +### Console Output +By default, CloudSploit results are printed to the console in a table format (with colors). You can override this and use plain text instead, by running: ``` -var virtualMachineExtensions = helpers.addSource(cache, source, ['virtualMachineExtensions', 'list', location, virtualMachine.id]); +$ ./index.js --console=text ``` -The `helpers` function ensures that the proper results are returned from the collection and that they are saved into a "source" variable which can be returned with the results. -Now, we can write the plugin functionality by checking for the data relevant to our requirements: +Alternatively, you can suppress the console output entirely by running: ``` -if (virtualMachineExtensions.err || !virtualMachineExtensions.data) { - helpers.addResult(results, 3, - Unable to query for VM Extensions: ' + helpers.addError(virtualMachineExtensions), location); - return rcb(); -} -if (!virtualMachineExtensions.data.length) { - helpers.addResult(results, 0, 'No VM Extensions found', location); -} -``` -The `addResult` function ensures we are adding the results to the `results` array in the proper format. This function accepts the following: +$ ./index.js --console=none ``` -(results array, score, message, region, resource) -``` -The `resource` is optional, and the `score` must be between 0 and 3 to indicate PASS, WARN, FAIL, or UNKNOWN. - -#### GCP -To more clearly illustrate writing a new plugin, let us consider the Storage Bucket All Users Policy plugin `plugins/google/storage/bucketAllUsersPolicy.js` . First, we know that we will need to query for a list of buckets via `buckets:list`, then loop through each group and query for the more detailed set of data via `buckets:getIamPolicy`. -We'll add these API calls to `collect.js`. First, under `calls` add: +### Ignoring Passing Results +You can ignore results from output that return an OK status by passing a `--ignore-ok` commandline argument. +### CSV ``` -buckets: { - list: { - api: 'storage', - version: 'v1', - location: null, - } -}, +$ ./index.js --csv=file.csv ``` -Then, under `postcalls`, add: +### JSON ``` -buckets: { - getIamPolicy: { - api: 'storage', - version: 'v1', - location: null, - reliesOnService: ['buckets'], - reliesOnCall: ['list'], - filterKey: ['bucket'], - filterValue: ['name'], - } -}, +$ ./index.js --json=file.json ``` -CloudSploit will first get the list of buckets, then, it will loop through each one, using the bucket name to get more detailed info via `getIamPolicy`. - -Next, we'll write the plugin. Create a new file in the `plugins/google/storage` folder called `bucketAllUsersPolicy.js` (this plugin already exists, but you can create a similar one for the purposes of this example). -In the file, we'll be sure to export the plugin's title, category, description, link, and more information about it. Additionally, we will add any API calls it makes: +### JUnit XML ``` -apis: ['buckets:list', 'buckets:getIamPolicy'], +$ ./index.js --junit=file.xml ``` -In the `run` function, we can obtain the output of the collection phase from earlier by doing: -``` -let bucketPolicyPolicies = helpers.addSource(cache, source, - ['buckets', 'getIamPolicy', region]); -``` -The `helpers` function ensures that the proper results are returned from the collection and that they are saved into a "source" variable which can be returned with the results. -Now, we can write the plugin functionality by checking for the data relevant to our requirements: +### Collection Output +CloudSploit saves the data queried from the cloud provider APIs in JSON format, which can be saved alongside other files for debugging or historical purposes. ``` -if (bucketPolicyPolicies.err || !bucketPolicyPolicies.data) { - helpers.addResult(results, 3, 'Unable to query storage buckets: ' + helpers.addError(bucketPolicyPolicies), region); - return rcb(); -} - -if (!bucketPolicyPolicies.data.length) { - helpers.addResult(results, 0, 'No storage buckets found', region); - return rcb(); -} -``` -The `addResult` function ensures we are adding the results to the `results` array in the proper format. This function accepts the following: +$ ./index.js --collection=file.json ``` -(results array, score, message, region, resource) -``` -The `resource` is optional, and the `score` must be between 0 and 3 to indicate PASS, WARN, FAIL, or UNKNOWN. - -#### Oracle -To more clearly illustrate writing a new plugin, let us consider the Networking Subnet Multi AD plugin `plugins/oracle/networking/subnetMultiAd.js` . First, we know that we will need to query for a list of VCNs via `vcn:list`, then loop through each group and query for the more detailed set of data via `subnet:list`. - -We'll add these API calls to `collect.js`. First, under `calls` add: +## Suppressions +Results can be suppressed by passing the `--suppress` flag (multiple options are supported) with the following format: ``` -vcn: { - list: { - api: "core", - filterKey: ['compartmentId'], - filterValue: ['compartmentId'], - } -}, +--suppress pluginId:region:resourceId ``` -Then, under `postcalls`, add: -``` -subnet: { - list: { - api: "core", - reliesOnService: ['vcn'], - reliesOnCall: ['list'], - filterKey: ['compartmentId', 'vcnId'], - filterValue: ['compartmentId', 'id'], - filterConfig: [true, false], - } -}, +For example: ``` -CloudSploit will first get the list of vcns, then, it will loop through each one, using the vcn id to get more detailed info via `subnet:list`. +# Suppress all results for the acmValidation plugin +$ ./index.js --suppress acmValidation:*:* -Next, we'll write the plugin. Create a new file in the `plugins/oracle/networking` folder called `subnetMultiAd.js` (this plugin already exists, but you can create a similar one for the purposes of this example). +# Suppress all us-east-1 region results +$ ./index.js --suppress *:us-east-1:* -In the file, we'll be sure to export the plugin's title, category, description, link, and more information about it. Additionally, we will add any API calls it makes: -``` -apis: ['vcn:list','subnet:list'] +# Suppress all results matching the regex "certificate/*" in all regions for all plugins +$ ./index.js --suppress *:*:certificate/* ``` -In the `run` function, we can obtain the output of the collection phase from earlier by doing: -``` -var subnets = helpers.addSource(cache, source, - ['subnet', 'list', region]); -``` -The `helpers` function ensures that the proper results are returned from the collection and that they are saved into a "source" variable which can be returned with the results. -Now, we can write the plugin functionality by checking for the data relevant to our requirements: +## Running a Single Plugin +The `--plugin` flag can be used if you only wish to run one plugin. ``` -if ((subnets.err && subnets.err.length) || !subnets.data) { - helpers.addResult(results, 3, - 'Unable to query for subnets: ' + helpers.addError(subnets), region); - return rcb(); -} - -if (!subnets.data.length) { - helpers.addResult(results, 0, 'No subnets found', region); - return rcb(); -} -``` -The `addResult` function ensures we are adding the results to the `results` array in the proper format. This function accepts the following: +$ ./index.js --plugin acmValidation ``` -(results array, score, message, region, resource) -``` -The `resource` is optional, and the `score` must be between 0 and 3 to indicate PASS, WARN, FAIL, or UNKNOWN. -## Other Notes - -When using the [hosted scanner](https://cloudsploit.com/scan), you will be able to see an intuitive visual representation of the scan results. In CloudSploit's console, printable scan results look as follows: - -[](https://console.cloudsploit.com/signup) - -### Cross-account IAM role - -Cross-account roles enable you to share access to your account with another AWS account using the same policy model that you're used to within AWS services' scope. - -The advantage is that cross-account roles are much more secure than key-based access, since an attacker who steals a cross-account role ARN still cannot make API calls unless he/she also infiltrates the AWS account that has been authorized to use the role in question. - -To create a cross-account role: - -``` -1. Navigate to the [IAM console](https://console.aws.amazon.com/iam/home). -2. Log into your AWS account and navigate to the IAM console. -3. Create a new IAM role. -4. When prompted for a trusted entity select: "Another AWS account". -5. Enter "057012691312" for the account to trust (Account ID). -6. Check the box to "Require external ID" and enter the external ID displayed below. -7. Ensure that MFA token is not selected. -8. Select the "SecurityAudit" managed policy. -9. Enter a memorable role name and create the role. -10. Then click on the role name and copy the role ARN for use in the next step. -``` - -### CloudSploit Supplemental Policy -Allows read only accesss to services not included in the SecurityAudit AWS Managed policy but that are also tested by the CSPM scans. - -```$xslt -{ - "Version": "2012-10-17", - "Statement": [ - { - "Action": [ - "ses:DescribeActiveReceiptRuleSet", - "athena:GetWorkGroup", - "logs:DescribeLogGroups", - "logs:DescribeMetricFilters", - "elastictranscoder:ListPipelines", - "elasticfilesystem:DescribeFileSystems", - "servicequotas:ListServiceQuotas" - ], - "Resource": "*", - "Effect": "Allow" - } - ] -} -``` -### AWS Inline Policy (Not Recommended) - -If you'd prefer to be more restrictive, the following IAM policy contains the exact permissions used by the scan. +## Architecture +CloudSploit works in two phases. First, it queries the cloud infrastructure APIs for various metadata about your account, namely the "collection" phase. Once all the necessary data is collected, the result is passed to the "scanning" phase. The scan uses the collected data to search for potential misconfigurations, risks, and other security issues, which are the resulting output. -**WARNING:** This policy will likely change as more plugins are written. If a test returns "UNKNOWN" it is likely missing a required permission. The preferred method is to use the "SecurityAudit" policy. +## Writing a Plugin +Please see our [contribution guidelines](.github/CONTRIBUTING.md) and [complete guide](docs/writing-plugins.md) to writing CloudSploit plugins. -``` -{ - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Resource": "*", - "Action": [ - "acm:Describe*", - "acm:List*", - "application-autoscaling:Describe*", - "appmesh:Describe*", - "appmesh:List*", - "appsync:List*", - "athena:List*", - "athena:GetWorkGroup", - "autoscaling:Describe*", - "batch:DescribeComputeEnvironments", - "batch:DescribeJobDefinitions", - "chime:List*", - "cloud9:Describe*", - "cloud9:ListEnvironments", - "clouddirectory:ListDirectories", - "cloudformation:DescribeStack*", - "cloudformation:GetTemplate", - "cloudformation:ListStack*", - "cloudformation:GetStackPolicy", - "cloudfront:Get*", - "cloudfront:List*", - "cloudhsm:ListHapgs", - "cloudhsm:ListHsms", - "cloudhsm:ListLunaClients", - "cloudsearch:DescribeDomains", - "cloudsearch:DescribeServiceAccessPolicies", - "cloudtrail:DescribeTrails", - "cloudtrail:GetEventSelectors", - "cloudtrail:GetTrailStatus", - "cloudtrail:ListTags", - "cloudtrail:LookupEvents", - "cloudwatch:Describe*", - "codebuild:ListProjects", - "codecommit:BatchGetRepositories", - "codecommit:GetBranch", - "codecommit:GetObjectIdentifier", - "codecommit:GetRepository", - "codecommit:List*", - "codedeploy:Batch*", - "codedeploy:Get*", - "codedeploy:List*", - "codepipeline:ListPipelines", - "codestar:Describe*", - "codestar:List*", - "cognito-identity:ListIdentityPools", - "cognito-idp:ListUserPools", - "cognito-sync:Describe*", - "cognito-sync:List*", - "comprehend:Describe*", - "comprehend:List*", - "config:BatchGetAggregateResourceConfig", - "config:BatchGetResourceConfig", - "config:Deliver*", - "config:Describe*", - "config:Get*", - "config:List*", - "datapipeline:DescribeObjects", - "datapipeline:DescribePipelines", - "datapipeline:EvaluateExpression", - "datapipeline:GetPipelineDefinition", - "datapipeline:ListPipelines", - "datapipeline:QueryObjects", - "datapipeline:ValidatePipelineDefinition", - "datasync:Describe*", - "datasync:List*", - "dax:Describe*", - "dax:ListTags", - "directconnect:Describe*", - "dms:Describe*", - "dms:ListTagsForResource", - "ds:DescribeDirectories", - "dynamodb:DescribeContinuousBackups", - "dynamodb:DescribeGlobalTable", - "dynamodb:DescribeTable", - "dynamodb:DescribeTimeToLive", - "dynamodb:ListBackups", - "dynamodb:ListGlobalTables", - "dynamodb:ListStreams", - "dynamodb:ListTables", - "ec2:Describe*", - "ecr:DescribeRepositories", - "ecr:GetRepositoryPolicy", - "ecs:Describe*", - "ecs:List*", - "eks:DescribeCluster", - "eks:ListClusters", - "elasticache:Describe*", - "elasticbeanstalk:Describe*", - "elasticfilesystem:DescribeFileSystems", - "elasticfilesystem:DescribeMountTargetSecurityGroups", - "elasticfilesystem:DescribeMountTargets", - "elasticloadbalancing:Describe*", - "elasticmapreduce:Describe*", - "elasticmapreduce:ListClusters", - "elasticmapreduce:ListInstances", - "elastictranscoder:ListPipelines", - "es:Describe*", - "es:ListDomainNames", - "events:Describe*", - "events:List*", - "firehose:Describe*", - "firehose:List*", - "fms:ListComplianceStatus", - "fms:ListPolicies", - "fsx:Describe*", - "fsx:List*", - "gamelift:ListBuilds", - "gamelift:ListFleets", - "glacier:DescribeVault", - "glacier:GetVaultAccessPolicy", - "glacier:ListVaults", - "globalaccelerator:Describe*", - "globalaccelerator:List*", - "greengrass:List*", - "guardduty:Get*", - "guardduty:List*", - "iam:GenerateCredentialReport", - "iam:GenerateServiceLastAccessedDetails", - "iam:Get*", - "iam:List*", - "iam:SimulateCustomPolicy", - "iam:SimulatePrincipalPolicy", - "inspector:Describe*", - "inspector:Get*", - "inspector:List*", - "inspector:Preview*", - "iot:Describe*", - "iot:GetPolicy", - "iot:GetPolicyVersion", - "iot:List*", - "kinesis:DescribeStream", - "kinesis:ListStreams", - "kinesis:ListTagsForStream", - "kinesisanalytics:ListApplications", - "kms:Describe*", - "kms:Get*", - "kms:List*", - "lambda:GetAccountSettings", - "lambda:GetFunctionConfiguration", - "lambda:GetLayerVersionPolicy", - "lambda:GetPolicy", - "lambda:List*", - "license-manager:List*", - "lightsail:GetInstances", - "lightsail:GetLoadBalancers", - "logs:Describe*", - "logs:ListTagsLogGroup", - "machinelearning:DescribeMLModels", - "mediaconnect:Describe*", - "mediaconnect:List*", - "mediastore:GetContainerPolicy", - "mediastore:ListContainers", - "opsworks:DescribeStacks", - "opsworks-cm:DescribeServers", - "organizations:List*", - "organizations:Describe*", - "quicksight:Describe*", - "quicksight:List*", - "ram:List*", - "rds:Describe*", - "rds:DownloadDBLogFilePortion", - "rds:ListTagsForResource", - "redshift:Describe*", - "rekognition:Describe*", - "rekognition:List*", - "robomaker:Describe*", - "robomaker:List*", - "route53:Get*", - "route53:List*", - "route53domains:GetDomainDetail", - "route53domains:GetOperationDetail", - "route53domains:ListDomains", - "route53domains:ListOperations", - "route53domains:ListTagsForDomain", - "route53resolver:List*", - "route53resolver:Get*", - "s3:GetAccelerateConfiguration", - "s3:GetAccountPublicAccessBlock", - "s3:GetAnalyticsConfiguration", - "s3:GetBucket*", - "s3:GetEncryptionConfiguration", - "s3:GetInventoryConfiguration", - "s3:GetLifecycleConfiguration", - "s3:GetMetricsConfiguration", - "s3:GetObjectAcl", - "s3:GetObjectVersionAcl", - "s3:GetReplicationConfiguration", - "s3:ListAllMyBuckets", - "sagemaker:Describe*", - "sagemaker:List*", - "sdb:DomainMetadata", - "sdb:ListDomains", - "secretsmanager:GetResourcePolicy", - "secretsmanager:ListSecrets", - "secretsmanager:ListSecretVersionIds", - "securityhub:Describe*", - "securityhub:Get*", - "securityhub:List*", - "serverlessrepo:GetApplicationPolicy", - "serverlessrepo:List*", - "servicequotas:ListServiceQuotas", - "ses:GetIdentityDkimAttributes", - "ses:GetIdentityPolicies", - "ses:GetIdentityVerificationAttributes", - "ses:ListIdentities", - "ses:ListIdentityPolicies", - "ses:ListVerifiedEmailAddresses", - "ses:DescribeActiveReceiptRuleSet", - "shield:Describe*", - "shield:List*", - "snowball:ListClusters", - "snowball:ListJobs", - "sns:GetTopicAttributes", - "sns:ListSubscriptionsByTopic", - "sns:ListTopics", - "sqs:GetQueueAttributes", - "sqs:ListDeadLetterSourceQueues", - "sqs:ListQueues", - "sqs:ListQueueTags", - "ssm:Describe*", - "ssm:GetAutomationExecution", - "ssm:ListDocuments", - "sso:DescribePermissionsPolicies", - "sso:List*", - "states:ListStateMachines", - "storagegateway:DescribeBandwidthRateLimit", - "storagegateway:DescribeCache", - "storagegateway:DescribeCachediSCSIVolumes", - "storagegateway:DescribeGatewayInformation", - "storagegateway:DescribeMaintenanceStartTime", - "storagegateway:DescribeNFSFileShares", - "storagegateway:DescribeSnapshotSchedule", - "storagegateway:DescribeStorediSCSIVolumes", - "storagegateway:DescribeTapeArchives", - "storagegateway:DescribeTapeRecoveryPoints", - "storagegateway:DescribeTapes", - "storagegateway:DescribeUploadBuffer", - "storagegateway:DescribeVTLDevices", - "storagegateway:DescribeWorkingStorage", - "storagegateway:List*", - "tag:GetResources", - "tag:GetTagKeys", - "transfer:Describe*", - "transfer:List*", - "translate:List*", - "trustedadvisor:Describe*", - "waf:ListWebACLs", - "waf-regional:ListWebACLs", - "workspaces:Describe*", - "xray:Get*" - ] - }, - { - "Effect": "Allow", - "Action": [ - "apigateway:GET" - ], - "Resource": [ - "arn:aws:apigateway:*::/apis", - "arn:aws:apigateway:*::/apis/*/stages", - "arn:aws:apigateway:*::/apis/*/stages/*", - "arn:aws:apigateway:*::/apis/*/routes", - "arn:aws:apigateway:*::/restapis", - "arn:aws:apigateway:*::/restapis/*/authorizers", - "arn:aws:apigateway:*::/restapis/*/authorizers/*", - "arn:aws:apigateway:*::/restapis/*/documentation/versions", - "arn:aws:apigateway:*::/restapis/*/resources", - "arn:aws:apigateway:*::/restapis/*/resources/*", - "arn:aws:apigateway:*::/restapis/*/resources/*/methods/*", - "arn:aws:apigateway:*::/restapis/*/stages", - "arn:aws:apigateway:*::/restapis/*/stages/*", - "arn:aws:apigateway:*::/vpclinks" - ] - } - ] -} -``` +## Other Notes +For other details about the Aqua Wave SaaS product, AWS security policies, and more, [click here](docs/notes.md). diff --git a/collectors/aws/collector.js b/collectors/aws/collector.js index d3a73639d2..fa30b93079 100644 --- a/collectors/aws/collector.js +++ b/collectors/aws/collector.js @@ -400,6 +400,10 @@ var calls = { describeDBSnapshots: { property: 'DBSnapshots', paginate: 'Marker' + }, + describeDBParameterGroups: { + property: 'DBParameterGroups', + paginate: 'Marker' } }, Redshift: { @@ -812,6 +816,14 @@ var postcalls = [ filterValue: 'FunctionArn' } }, + RDS: { + describeDBParameters: { + reliesOnService: 'rds', + reliesOnCall: 'describeDBParameterGroups', + filterKey: 'DBParameterGroupName', + filterValue: 'DBParameterGroupName' + } + }, SageMaker: { describeNotebookInstance: { reliesOnService: 'sagemaker', @@ -890,6 +902,12 @@ var postcalls = [ reliesOnService: 'iam', reliesOnCall: 'listRoles', override: true + }, + getRole: { + reliesOnService: 'iam', + reliesOnCall: 'listRoles', + filterKey: 'RoleName', + filterValue: 'RoleName' } } } diff --git a/compliance/all.js b/compliance/all.js deleted file mode 100644 index b7a3730724..0000000000 --- a/compliance/all.js +++ /dev/null @@ -1,12 +0,0 @@ -// Defines a way of filters that includes all rules. This is the default -// compliance filter if there is no other defined filter. -module.exports = { - describe: function() { - return ''; - }, - - includes: function() { - // We include all plugins, so just return true - return true; - } -}; diff --git a/compliance/cis.js b/compliance/cis.js deleted file mode 100644 index e75bf1b55d..0000000000 --- a/compliance/cis.js +++ /dev/null @@ -1,233 +0,0 @@ -// These rule mappings are based on CIS Amazon Web Services Foundation v1.2.0 -// dated 05-23-2018 - -var controls = { - rootAccountInUse: { - awsid: '1.1', - profile: 1, - scored: true, - title: 'Avoid the use of the "root" account' - }, - - usersMfaEnabled: { - awsid: '1.2', - profile: 1, - scored: true, - title: ' Ensure multi-factor authentication (MFA) is enabled for all ' - + 'IAM users that have a console password' - }, - - usersPasswordLastUsed: { - awsid: '1.3', - profile: 1, - scored: true, - title: 'Ensure credentials unused for 90 days or greater are disabled' - }, - - accessKeysLastUsed: { - awsid: '1.3', - profile: 1, - scored: true, - title: 'Ensure credentials unused for 90 days or greater are disabled' - }, - - accessKeysRotated: { - awsid: '1.4', - profile: 1, - scored: true, - title: 'Ensure access keys are rotated every 90 days or less' - }, - - passwordRequiresUppercase: { - awsid: '1.5', - profile: 1, - scored: true, - title: 'Ensure IAM password policy requires at least one uppercase ' - + 'letter' - }, - - passwordRequiresLowercase: { - awsid: '1.6', - profile: 1, - scored: true, - title: ' Ensure IAM password policy require at least one lowercase ' - + 'letter' - }, - - passwordRequiresSymbols: { - awsid: '1.7', - profile: 1, - scored: true, - title: ' Ensure IAM password policy require at least one symbol' - }, - - passwordRequiresNumbers: { - awsid: '1.8', - profile: 1, - scored: true, - title: 'Ensure IAM password policy require at least one number' - }, - - minPasswordLength: { - awsid: '1.9', - profile: 1, - scored: true, - title: 'Ensure IAM password policy requires minimum length of 14 or ' - + 'greater' - }, - - passwordReusePrevention: { - awsid: '1.10', - profile: 1, - scored: true, - title: 'Ensure IAM password policy prevents password reuse' - }, - - passwordExpiration: { - awsid: '1.11', - profile: 1, - scored: true, - title: 'Ensure IAM password policy expires passwords within 90 days or ' - + 'less' - }, - - rootAccessKeys: { - awsid: '1.12', - profile: 1, - scored: true, - title: 'Ensure no root account access key exists' - }, - - rootMfaEnabled: { - awsid: '1.13', - profile: 1, - scored: true, - title: 'Ensure MFA is enabled for the "root" account' - }, - - noUserIamPolicies: { - awsid: '1.16', - profile: 1, - scored: true, - title: 'Ensure IAM policies are attached only to groups or roles' - }, - - cloudtrailEnabled: { - awsid: '2.1', - profile: 1, - scored: true, - title: 'Ensure CloudTrail is enabled in all regions' - }, - - cloudtrailFileValidation: { - awsid: '2.2', - profile: 2, - scored: true, - title: 'Ensure CloudTrail log file validation is enabled' - }, - - cloudtrailBucketPrivate: { - awsid: '2.3', - profile: 1, - scored: true, - title: 'Ensure the S3 bucket used to store CloudTrail logs is not ' - + 'publicly accessible' - }, - - cloudtrailToCloudwatch: { - awsid: '2.4', - profile: 1, - scored: true, - title: 'Ensure CloudTrail trails are integrated with CloudWatch Logs' - }, - - configServiceEnabled: { - awsid: '2.5', - profile: 1, - scored: true, - title: ' Ensure AWS Config is enabled in all regions' - }, - - cloudtrailBucketAccessLogging: { - awsid: '2.6', - profile: 1, - scored: true, - title: ' Ensure AWS Config is enabled in all regions' - }, - - cloudtrailEncryption: { - awsid: '2.7', - profile: 2, - scored: true, - title: 'Ensure CloudTrail logs are encrypted at rest using KMS CMKs' - }, - - kmsKeyRotation: { - awsid: '2.8', - profile: 2, - scored: true, - title: 'Ensure rotation for customer created CMKs is enabled' - }, - - flowLogsEnabled: { - awsid: '2.8', - profile: 2, - scored: true, - title: 'Ensure VPC flow logging is enabled in all VPCs' - }, - - monitoringMetrics: { - awsid: '3', - profile: 1, - scored: true, - title: 'Monitoring' - }, - - openSSH: { - awsid: '4.1', - profile: 1, - scored: true, - title: 'Ensure no security groups allow ingress from 0.0.0.0/0 to ' - + 'port 22' - }, - - openRDP: { - awsid: '4.2', - profile: 1, - scored: true, - title: 'Ensure no security groups allow ingress from 0.0.0.0/0 to ' - + 'port 3389' - }, - - defaultSecurityGroup: { - awsid: '4.3', - profile: 2, - scored: true, - title: 'Ensure the default security group of every VPC restricts all ' - + 'traffic' - } -}; - -var maxProfileLevel = -1; - -// Defines a way of filtering plugins for those plugins that are related to -// PCI controls. The PCI information is defined inline, so this compliance -// checks for that information on the plugin. -module.exports = { - describe: function(pluginId) { - return controls[pluginId].title; - }, - - includes: function(pluginId) { - if (maxProfileLevel <= 0) { - return Object.prototype.hasOwnProperty.call(controls, pluginId); - } - - return Object.prototype.hasOwnProperty.call(controls, pluginId) && - controls[pluginId].profile <= maxProfileLevel; - }, - - setMaxProfile: function(level) { - maxProfileLevel = level; - } -}; diff --git a/compliance/controls.js b/compliance/controls.js deleted file mode 100644 index 67992334ef..0000000000 --- a/compliance/controls.js +++ /dev/null @@ -1,31 +0,0 @@ -module.exports = { - create: function(names) { - // We we don't have a specified compliance, then include all plugins - if (names.length === 0) { - return require('./all.js'); - } - - if (names.includes('hipaa')) { - console.log('INFO: Compliance mode: HIPAA'); - return require('./hipaa.js'); - } else if (names.includes('pci')) { - console.log('INFO: Compliance mode: PCI'); - return require('./pci.js'); - } else if (names.includes('cis')) { - console.log('INFO: Compliance mode: CIS'); - return require('./cis.js'); - } else if (names.includes('cis-1')) { - console.log('INFO: Compliance mode: CIS Profile 1'); - var cis1 = require('./cis.js'); - cis1.setMaxProfile(1); - return cis1; - } else if (names.includes('cis-2')) { - console.log('INFO: Compliance mode: CIS Profile 2'); - var cis2 = require('./cis.js'); - cis2.setMaxProfile(2); - return cis2; - } - - return null; - } -}; diff --git a/compliance/hipaa.js b/compliance/hipaa.js deleted file mode 100644 index 984f309e53..0000000000 --- a/compliance/hipaa.js +++ /dev/null @@ -1,12 +0,0 @@ -// Defines a way of filtering plugins for those plugins that are related to -// HIPAA controls. The HIPAA information is defined inline, so this compliance -// checks for that information on the plugin. -module.exports = { - describe: function(pluginId, plugin) { - return plugin.compliance && plugin.compliance.hipaa; - }, - - includes: function(pluginId, plugin) { - return plugin.compliance && plugin.compliance.hipaa; - } -}; diff --git a/compliance/pci.js b/compliance/pci.js deleted file mode 100644 index 0e5c613a27..0000000000 --- a/compliance/pci.js +++ /dev/null @@ -1,12 +0,0 @@ -// Defines a way of filtering plugins for those plugins that are related to -// PCI controls. The PCI information is defined inline, so this compliance -// checks for that information on the plugin. -module.exports = { - describe: function(pluginId, plugin) { - return plugin.compliance && plugin.compliance.pci; - }, - - includes: function(pluginId, plugin) { - return plugin.compliance && plugin.compliance.pci; - } -}; diff --git a/config/_oracle/keys/Readme.md b/config/_oracle/keys/Readme.md deleted file mode 100644 index 7204905078..0000000000 --- a/config/_oracle/keys/Readme.md +++ /dev/null @@ -1,21 +0,0 @@ -#Instructions - -## Create an API user - -In your Oracle Cloud Infrastructure Console, under Identity > Users: - -* Click on "Create User" -* Set the Name to "CloudSploitAPI" -* Set the Description to "CloudSploit API Read Only Access" -* Click on "Create" - -## Generate an API Signing Key - -Please follow the instructions on: https://docs.cloud.oracle.com/iaas/Content/API/Concepts/apisigningkey.htm - -You will need: -* Private un-encrypted key: openssl genrsa -out ~/.oci/oci_api_key.pem 2048 -* Public Key: openssl rsa -pubout -in ~/.oci/oci_api_key.pem -out ~/.oci/oci_api_key_public.pem -* Key Fingerprint: openssl rsa -pubout -outform DER -in ~/.oci/oci_api_key.pem | openssl md5 -c - -## Save the Private un-encrypted key in this directory to run your scans \ No newline at end of file diff --git a/config_example.js b/config_example.js new file mode 100644 index 0000000000..3bf02f4424 --- /dev/null +++ b/config_example.js @@ -0,0 +1,50 @@ +// CloudSploit config file + +module.exports = { + credentials: { + aws: { + // OPTION 1: If using a credential JSON file, enter the path below + // credential_file: '/path/to/file.json', + // OPTION 2: If using hard-coded credentials, enter them below + // access_key: process.env.AWS_ACCESS_KEY_ID || '', + // secret_access_key: process.env.AWS_SECRET_ACCESS_KEY || '', + // session_token: process.env.AWS_SESSION_TOKEN || '', + }, + azure: { + // OPTION 1: If using a credential JSON file, enter the path below + // credential_file: '/path/to/file.json', + // OPTION 2: If using hard-coded credentials, enter them below + // application_id: process.env.AZURE_APPLICATION_ID || '', + // key_value: process.env.AZURE_KEY_VALUE || '', + // directory_id: process.env.AZURE_DIRECTORY_ID || '', + // subscription_id: process.env.AZURE_SUBSCRIPTION_ID || '' + }, + google: { + // OPTION 1: If using a credential JSON file, enter the path below + // credential_file: process.env.GOOGLE_APPLICATION_CREDENTIALS || '/path/to/file.json', + // OPTION 2: If using hard-coded credentials, enter them below + // project: process.env.GOOGLE_PROJECT_ID || 'my-project', + // client_email: process.env.GOOGLE_CLIENT_EMAIL || 'cloudsploit@your-project-name.iam.gserviceaccount.com', + // private_key: process.env.GOOGLE_PRIVATE_KEY || '-----BEGIN PRIVATE KEY-----\nYOUR-PRIVATE-KEY-GOES-HERE\n-----END PRIVATE KEY-----\n' + }, + oracle: { + // OPTION 1: If using a credential JSON file, enter the path below + // credential_file: '/path/to/file.json', + // OPTION 2: If using hard-coded credentials, enter them below + // tenancy_id: process.env.ORACLE_TENANCY_ID || 'ocid1.tenancy.oc1..', + // compartment_id: process.env.ORACLE_COMPARTMENT_ID || 'ocid1.compartment.oc1..', + // user_id: process.env.ORACLE_USER_ID || 'ocid1.user.oc1..', + // key_fingerprint: process.env.ORACLE_KEY_FINGERPRINT || 'YOURKEYFINGERPRINT', + // key_value: process.env.ORACLE_KEY_VALUE || '-----BEGIN PRIVATE KEY-----\nYOUR-PRIVATE-KEY-GOES-HERE\n-----END PRIVATE KEY-----\n' + }, + github: { + // OPTION 1: If using a credential JSON file, enter the path below + // credential_file: '/path/to/file.json', + // OPTION 2: If using hard-coded credentials, enter them below + // token: process.env.GITHUB_TOKEN || '', + // url: process.env.GITHUB_URL || 'https://api.github.com', + // login: process.env.GITHUB_LOGIN || 'myusername', + // organization: process.env.GITHUB_ORG || false + } + } +}; \ No newline at end of file diff --git a/docs/aws.md b/docs/aws.md new file mode 100644 index 0000000000..cb44efa46a --- /dev/null +++ b/docs/aws.md @@ -0,0 +1,42 @@ +# CloudSploit For Amazon Web Services (AWS) + +## Cloud Provider Configuration +Create a "cloudsploit" user, with the `SecurityAudit` policy. + +1. Log into your AWS account as an admin or with permission to create IAM resources. +1. Navigate to the [IAM console](https://console.aws.amazon.com/iam/home). +1. Click on [Users](https://console.aws.amazon.com/iam/home?region=us-east-1#/users) +1. [Create a new user (Add user)](https://console.aws.amazon.com/iam/home?region=us-east-1#/users$new?step=details) +1. Set the username to `cloudsploit` +1. Set the access type to "Programmatic access", click Next. +1. Select "Attach existing policies directly" and select the SecurityAudit policy. +1. Click "Create policy" to create a supplemental policy (some permissions are not included in SecurityAudit). +1. Click the "JSON" tab and paste the following permission set. + ``` + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "ses:DescribeActiveReceiptRuleSet", + "athena:GetWorkGroup", + "logs:DescribeLogGroups", + "logs:DescribeMetricFilters", + "elastictranscoder:ListPipelines", + "elasticfilesystem:DescribeFileSystems", + "servicequotas:ListServiceQuotas" + ], + "Resource": "*" + } + ] + } + ``` +1. Click "Review policy." +1. Provide a name (`CloudSploitSupplemental`) and click "Create policy." +1. Return to the "Create user" page and attach the newly-created policy. Click "Next: tags." +1. Set tags as needed and then click on "Create user". +1. Make sure you safely store the Access key ID and Secret access key. +1. Paste them into the corresponding AWS credentials section of the `index.js` file. + +If using environment variables, the same ones expected by the aws sdks, namely `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, and `AWS_SESSION_TOKEN`, can be used. diff --git a/docs/azure.md b/docs/azure.md new file mode 100644 index 0000000000..5fe8997096 --- /dev/null +++ b/docs/azure.md @@ -0,0 +1,25 @@ +# CloudSploit For Microsoft Azure + +## Cloud Provider Configuration +1. Log into your Azure Portal and navigate to the Azure Active Directory service. +1. Select App registrations and then click on New registration. +1. Enter "CloudSploit" and/or a descriptive name in the Name field, take note of it, it will be used again in step 3. +1. Leave the "Supported account types" default: "Accounts in this organizational directory only (YOURDIRECTORYNAME)". +1. Click on Register. +1. Copy the Application ID and Paste it below. +1. Copy the Directory ID and Paste it below. +1. Click on Certificates & secrets. +1. Under Client secrets, click on New client secret. +1. Enter a Description (i.e. Cloudsploit-2019) and select Expires "In 1 year". +1. Click on Add. +1. The Client secret value appears only once, make sure you store it safely. +1. Navigate to Subscriptions. +1. Click on the relevant Subscription ID, copy and paste the ID below. +1. Click on "Access Control (IAM)". +1. Go to the Role assignments tab. +1. Click on "Add", then "Add role assignment". +1. In the "Role" drop-down, select "Security Reader". +1. Leave the "Assign access to" default value. +1. In the "Select" drop-down, type the name of the app registration (e.g. "CloudSploit") you created and select it. +1. Click "Save". +1. Repeat the process for the role "Log Analytics Reader" diff --git a/docs/console.png b/docs/console.png new file mode 100644 index 0000000000..5b5d34f749 Binary files /dev/null and b/docs/console.png differ diff --git a/docs/gcp.md b/docs/gcp.md new file mode 100644 index 0000000000..6d77271c58 --- /dev/null +++ b/docs/gcp.md @@ -0,0 +1,19 @@ +# CloudSploit For Google Cloud Platform (GCP) + +## Cloud Provider Configuration + +1. Log into your Google Cloud console and navigate to IAM Admin > Service Accounts. +1. Click on "Create Service Account". +1. Enter "CloudSploit" in the "Service account name", then enter "CloudSploit API Access" in the description. +1. Click on Continue. +1. Select the role: Project > Viewer. +1. Click on Continue. +1. Click on "Create Key". +1. Leave the default JSON selected. +1. Click on "Create". +1. The key will be downloaded to your machine. +1. Open the JSON key file, in a text editor and copy the Project Id, Client Email and Private Key values into the `index.js` file. +1. Enter the APIs & Services category. +1. Select Enable APIS & SERVICES at the top of the page +1. Search for DNS, then Select the option that appears and Enable it. +1. Enable all the APIs used to run scans, they are as follows: Stackdriver Monitoring, Stackdriver Logging, Compute, Cloud Key Management, Cloud SQL Admin, Kubernetes, Service Management, and Service Networking. \ No newline at end of file diff --git a/config/_github/README.md b/docs/github.md similarity index 100% rename from config/_github/README.md rename to docs/github.md diff --git a/docs/notes.md b/docs/notes.md new file mode 100644 index 0000000000..f92de1597b --- /dev/null +++ b/docs/notes.md @@ -0,0 +1,341 @@ +# Notes + +## Hosted SaaS +When using the [hosted scanner](https://cloud.aquasec.com/signup), you will be able to see an intuitive visual representation of the scan results. In the Aqua Wave console, printable scan results look as follows: + +[](https://cloud.aquasec.com/signup) + +## Cross-Account IAM Role +Cross-account roles enable you to share access to your account with another AWS account using the same policy model that you're used to within AWS services' scope. + +The advantage is that cross-account roles are much more secure than key-based access, since an attacker who steals a cross-account role ARN still cannot make API calls unless he/she also infiltrates the AWS account that has been authorized to use the role in question. + +To create a cross-account role: + +``` +1. Navigate to the [IAM console](https://console.aws.amazon.com/iam/home). +2. Log into your AWS account and navigate to the IAM console. +3. Create a new IAM role. +4. When prompted for a trusted entity select: "Another AWS account". +5. Enter "057012691312" for the account to trust (Account ID). +6. Check the box to "Require external ID" and enter the external ID displayed below. +7. Ensure that MFA token is not selected. +8. Select the "SecurityAudit" managed policy. +9. Enter a memorable role name and create the role. +10. Then click on the role name and copy the role ARN for use in the next step. +``` + +## CloudSploit Supplemental Policy +Allows read only accesss to services not included in the SecurityAudit AWS Managed policy but that are also tested by the CSPM scans. + +```$xslt +{ + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "ses:DescribeActiveReceiptRuleSet", + "athena:GetWorkGroup", + "logs:DescribeLogGroups", + "logs:DescribeMetricFilters", + "elastictranscoder:ListPipelines", + "elasticfilesystem:DescribeFileSystems", + "servicequotas:ListServiceQuotas" + ], + "Resource": "*", + "Effect": "Allow" + } + ] +} +``` + +## AWS Inline Policy (Not Recommended) +If you'd prefer to be more restrictive, the following IAM policy contains the exact permissions used by the scan. + +**WARNING:** This policy will likely change as more plugins are written. If a test returns "UNKNOWN" it is likely missing a required permission. The preferred method is to use the "SecurityAudit" policy. + +``` +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Resource": "*", + "Action": [ + "acm:Describe*", + "acm:List*", + "application-autoscaling:Describe*", + "appmesh:Describe*", + "appmesh:List*", + "appsync:List*", + "athena:List*", + "athena:GetWorkGroup", + "autoscaling:Describe*", + "batch:DescribeComputeEnvironments", + "batch:DescribeJobDefinitions", + "chime:List*", + "cloud9:Describe*", + "cloud9:ListEnvironments", + "clouddirectory:ListDirectories", + "cloudformation:DescribeStack*", + "cloudformation:GetTemplate", + "cloudformation:ListStack*", + "cloudformation:GetStackPolicy", + "cloudfront:Get*", + "cloudfront:List*", + "cloudhsm:ListHapgs", + "cloudhsm:ListHsms", + "cloudhsm:ListLunaClients", + "cloudsearch:DescribeDomains", + "cloudsearch:DescribeServiceAccessPolicies", + "cloudtrail:DescribeTrails", + "cloudtrail:GetEventSelectors", + "cloudtrail:GetTrailStatus", + "cloudtrail:ListTags", + "cloudtrail:LookupEvents", + "cloudwatch:Describe*", + "codebuild:ListProjects", + "codecommit:BatchGetRepositories", + "codecommit:GetBranch", + "codecommit:GetObjectIdentifier", + "codecommit:GetRepository", + "codecommit:List*", + "codedeploy:Batch*", + "codedeploy:Get*", + "codedeploy:List*", + "codepipeline:ListPipelines", + "codestar:Describe*", + "codestar:List*", + "cognito-identity:ListIdentityPools", + "cognito-idp:ListUserPools", + "cognito-sync:Describe*", + "cognito-sync:List*", + "comprehend:Describe*", + "comprehend:List*", + "config:BatchGetAggregateResourceConfig", + "config:BatchGetResourceConfig", + "config:Deliver*", + "config:Describe*", + "config:Get*", + "config:List*", + "datapipeline:DescribeObjects", + "datapipeline:DescribePipelines", + "datapipeline:EvaluateExpression", + "datapipeline:GetPipelineDefinition", + "datapipeline:ListPipelines", + "datapipeline:QueryObjects", + "datapipeline:ValidatePipelineDefinition", + "datasync:Describe*", + "datasync:List*", + "dax:Describe*", + "dax:ListTags", + "directconnect:Describe*", + "dms:Describe*", + "dms:ListTagsForResource", + "ds:DescribeDirectories", + "dynamodb:DescribeContinuousBackups", + "dynamodb:DescribeGlobalTable", + "dynamodb:DescribeTable", + "dynamodb:DescribeTimeToLive", + "dynamodb:ListBackups", + "dynamodb:ListGlobalTables", + "dynamodb:ListStreams", + "dynamodb:ListTables", + "ec2:Describe*", + "ecr:DescribeRepositories", + "ecr:GetRepositoryPolicy", + "ecs:Describe*", + "ecs:List*", + "eks:DescribeCluster", + "eks:ListClusters", + "elasticache:Describe*", + "elasticbeanstalk:Describe*", + "elasticfilesystem:DescribeFileSystems", + "elasticfilesystem:DescribeMountTargetSecurityGroups", + "elasticfilesystem:DescribeMountTargets", + "elasticloadbalancing:Describe*", + "elasticmapreduce:Describe*", + "elasticmapreduce:ListClusters", + "elasticmapreduce:ListInstances", + "elastictranscoder:ListPipelines", + "es:Describe*", + "es:ListDomainNames", + "events:Describe*", + "events:List*", + "firehose:Describe*", + "firehose:List*", + "fms:ListComplianceStatus", + "fms:ListPolicies", + "fsx:Describe*", + "fsx:List*", + "gamelift:ListBuilds", + "gamelift:ListFleets", + "glacier:DescribeVault", + "glacier:GetVaultAccessPolicy", + "glacier:ListVaults", + "globalaccelerator:Describe*", + "globalaccelerator:List*", + "greengrass:List*", + "guardduty:Get*", + "guardduty:List*", + "iam:GenerateCredentialReport", + "iam:GenerateServiceLastAccessedDetails", + "iam:Get*", + "iam:List*", + "iam:SimulateCustomPolicy", + "iam:SimulatePrincipalPolicy", + "inspector:Describe*", + "inspector:Get*", + "inspector:List*", + "inspector:Preview*", + "iot:Describe*", + "iot:GetPolicy", + "iot:GetPolicyVersion", + "iot:List*", + "kinesis:DescribeStream", + "kinesis:ListStreams", + "kinesis:ListTagsForStream", + "kinesisanalytics:ListApplications", + "kms:Describe*", + "kms:Get*", + "kms:List*", + "lambda:GetAccountSettings", + "lambda:GetFunctionConfiguration", + "lambda:GetLayerVersionPolicy", + "lambda:GetPolicy", + "lambda:List*", + "license-manager:List*", + "lightsail:GetInstances", + "lightsail:GetLoadBalancers", + "logs:Describe*", + "logs:ListTagsLogGroup", + "machinelearning:DescribeMLModels", + "mediaconnect:Describe*", + "mediaconnect:List*", + "mediastore:GetContainerPolicy", + "mediastore:ListContainers", + "opsworks:DescribeStacks", + "opsworks-cm:DescribeServers", + "organizations:List*", + "organizations:Describe*", + "quicksight:Describe*", + "quicksight:List*", + "ram:List*", + "rds:Describe*", + "rds:DownloadDBLogFilePortion", + "rds:ListTagsForResource", + "redshift:Describe*", + "rekognition:Describe*", + "rekognition:List*", + "robomaker:Describe*", + "robomaker:List*", + "route53:Get*", + "route53:List*", + "route53domains:GetDomainDetail", + "route53domains:GetOperationDetail", + "route53domains:ListDomains", + "route53domains:ListOperations", + "route53domains:ListTagsForDomain", + "route53resolver:List*", + "route53resolver:Get*", + "s3:GetAccelerateConfiguration", + "s3:GetAccountPublicAccessBlock", + "s3:GetAnalyticsConfiguration", + "s3:GetBucket*", + "s3:GetEncryptionConfiguration", + "s3:GetInventoryConfiguration", + "s3:GetLifecycleConfiguration", + "s3:GetMetricsConfiguration", + "s3:GetObjectAcl", + "s3:GetObjectVersionAcl", + "s3:GetReplicationConfiguration", + "s3:ListAllMyBuckets", + "sagemaker:Describe*", + "sagemaker:List*", + "sdb:DomainMetadata", + "sdb:ListDomains", + "secretsmanager:GetResourcePolicy", + "secretsmanager:ListSecrets", + "secretsmanager:ListSecretVersionIds", + "securityhub:Describe*", + "securityhub:Get*", + "securityhub:List*", + "serverlessrepo:GetApplicationPolicy", + "serverlessrepo:List*", + "servicequotas:ListServiceQuotas", + "ses:GetIdentityDkimAttributes", + "ses:GetIdentityPolicies", + "ses:GetIdentityVerificationAttributes", + "ses:ListIdentities", + "ses:ListIdentityPolicies", + "ses:ListVerifiedEmailAddresses", + "ses:DescribeActiveReceiptRuleSet", + "shield:Describe*", + "shield:List*", + "snowball:ListClusters", + "snowball:ListJobs", + "sns:GetTopicAttributes", + "sns:ListSubscriptionsByTopic", + "sns:ListTopics", + "sqs:GetQueueAttributes", + "sqs:ListDeadLetterSourceQueues", + "sqs:ListQueues", + "sqs:ListQueueTags", + "ssm:Describe*", + "ssm:GetAutomationExecution", + "ssm:ListDocuments", + "sso:DescribePermissionsPolicies", + "sso:List*", + "states:ListStateMachines", + "storagegateway:DescribeBandwidthRateLimit", + "storagegateway:DescribeCache", + "storagegateway:DescribeCachediSCSIVolumes", + "storagegateway:DescribeGatewayInformation", + "storagegateway:DescribeMaintenanceStartTime", + "storagegateway:DescribeNFSFileShares", + "storagegateway:DescribeSnapshotSchedule", + "storagegateway:DescribeStorediSCSIVolumes", + "storagegateway:DescribeTapeArchives", + "storagegateway:DescribeTapeRecoveryPoints", + "storagegateway:DescribeTapes", + "storagegateway:DescribeUploadBuffer", + "storagegateway:DescribeVTLDevices", + "storagegateway:DescribeWorkingStorage", + "storagegateway:List*", + "tag:GetResources", + "tag:GetTagKeys", + "transfer:Describe*", + "transfer:List*", + "translate:List*", + "trustedadvisor:Describe*", + "waf:ListWebACLs", + "waf-regional:ListWebACLs", + "workspaces:Describe*", + "xray:Get*" + ] + }, + { + "Effect": "Allow", + "Action": [ + "apigateway:GET" + ], + "Resource": [ + "arn:aws:apigateway:*::/apis", + "arn:aws:apigateway:*::/apis/*/stages", + "arn:aws:apigateway:*::/apis/*/stages/*", + "arn:aws:apigateway:*::/apis/*/routes", + "arn:aws:apigateway:*::/restapis", + "arn:aws:apigateway:*::/restapis/*/authorizers", + "arn:aws:apigateway:*::/restapis/*/authorizers/*", + "arn:aws:apigateway:*::/restapis/*/documentation/versions", + "arn:aws:apigateway:*::/restapis/*/resources", + "arn:aws:apigateway:*::/restapis/*/resources/*", + "arn:aws:apigateway:*::/restapis/*/resources/*/methods/*", + "arn:aws:apigateway:*::/restapis/*/stages", + "arn:aws:apigateway:*::/restapis/*/stages/*", + "arn:aws:apigateway:*::/vpclinks" + ] + } + ] +} +``` diff --git a/docs/oracle.md b/docs/oracle.md new file mode 100644 index 0000000000..178e5156be --- /dev/null +++ b/docs/oracle.md @@ -0,0 +1,50 @@ +# CloudSploit For Oracle Cloud Infrastructure (OCI) + +## Cloud Provider Configuration + +1. Log into your Oracle Cloud console and navigate to Administration > Tenancy Details. +1. Copy your Tenancy OCID and paste it in the index file. +1. Navigate to Identity > Users. +1. Click on Create User. +1. Enter "CloudSploit", then enter "CloudSploit API Access" in the description. +1. Click on Create. +1. Copy the User OCID and paste it in the index file. +1. Follow the steps to Generate an API Signing Key listed on Oracle's Cloud Doc(https://docs.cloud.oracle.com/iaas/Content/API/Concepts/apisigningkey.htm#How). +1. Open the public key (oci_api_key_public.pem) in your preferred text editor and copy the plain text (everything). Click on Add Public Key, then click on Add. +1. Copy the public key fingerprint and paste it in the index file. +1. Open the private key (oci_api_key.pem) in your preferred text editor and paste it in the index file. +1. Navigate to Identity > Groups. +1. Click on Create Group. +1. Enter "SecurityAudit" in the Name field, then enter "CloudSploit Security Audit Access" in the description. +1. Click on Submit. +1. Click on the SecurityAudit group in the Groups List and Add the CloudSploit API User to the group. +1. Navigate to Identity > Policies. +1. Click on Create Policy. +1. Enter "SecurityAudit" in the Name field, then enter "CloudSploit Security Audit Policy" in the description. +1. Copy and paste the following statement: +1. ALLOW GROUP SecurityAudit to READ all-resources in tenancy +1. Click on Create. +1. Navigate to Identity > Compartments. +1. Select your root compartment or the compartment being audited. +1. Click on "Copy" by your Compartment OCID. + +## Create an API user + +In your Oracle Cloud Infrastructure Console, under Identity > Users: + +* Click on "Create User" +* Set the Name to "CloudSploitAPI" +* Set the Description to "CloudSploit API Read Only Access" +* Click on "Create" + +## Generate an API Signing Key + +Please follow the instructions on: https://docs.cloud.oracle.com/iaas/Content/API/Concepts/apisigningkey.htm + +You will need: +* Private un-encrypted key: openssl genrsa -out ~/.oci/oci_api_key.pem 2048 +* Public Key: openssl rsa -pubout -in ~/.oci/oci_api_key.pem -out ~/.oci/oci_api_key_public.pem +* Key Fingerprint: openssl rsa -pubout -outform DER -in ~/.oci/oci_api_key.pem | openssl md5 -c + +## Save +Save the private un-encrypted key in this directory to run your scans diff --git a/docs/saas.png b/docs/saas.png new file mode 100644 index 0000000000..d353d50b58 Binary files /dev/null and b/docs/saas.png differ diff --git a/docs/upgrading.md b/docs/upgrading.md new file mode 100644 index 0000000000..2ff6fd1b07 --- /dev/null +++ b/docs/upgrading.md @@ -0,0 +1,46 @@ +# Upgrading CloudSploit +CloudSploit version 2.0.0 introduced a number of changes from the original CloudSploit release, designed to make running CloudSploit easier in multiple environment types, including command line and CI/CD systems. + +## Notable Changes +* The addition of the `argparse` library to enhance CLI option support +* Formalizing several previously-hidden settings and options (e.g. saving the JSON collection, multiple output formats, suppressions, etc.) +* The addition of the `tty-table` library for pretty-print CLI output of results. This is now the default output, but it can be changed to text-only via the `--console=text` flag. +* Improved documentation across the AWS, Azure, GCP, and OCI providers. +* The use of a `config.js` file for storing cloud provider configuration options, making it easier to run CloudSploit against multiple accounts by passing the `--config` flag. +* Fallback to the AWS credential chain, allowing users to get started running CloudSploit more quickly. +* Addition of an .eslint file for developers of CloudSploit and CloudSploit plugins. +* Formalizing CIS Benchmark options in the plugins using the `compliance` property. +* Added the ability to run a single plugin directly from the CLI, without editing the `exports.js` file by passing the flag `--plugin pluginName`. + +## Preparing Your Environment +If you previously used CloudSploit, you may need to make some changes as part of 2.0. Consider the following steps: +1. If you previously edited the `index.js` file, copy your cloud provider credentials to a new `config.js` file instead. You can do this by: + ``` + $ cp config_example.js config.js + // Edit your config.js file and pass either a path to a cloud credential file or the credentials themselves. + $ ./index.js --config=./config.js + ``` +1. If you are using AWS, you may now use the default credential handler by simply running CloudSploit with no config flag: + ``` + $ ./index.js + ``` +1. If you were running CloudSploit as part of a CI/CD process, the following flags may be helpful: + ``` + // Ignore passing results + $ ./index.js --ignore-ok + + // Exit with a non-zero code if non-passing results found + $ ./index.js --exit-code + + // Prints raw text output instead of the pretty-print tables + $ ./index.js --console=text + + // Suppresses the output (only recommended if using a file output) + $ ./index.js --console=none + + // Creates a JUnit XML file + $ ./index.js --junit=file.xml + ``` +1. If you are running CloudSploit in a place where pretty-print tables, with colors, are not usable, you can revert to raw text output with the `--console=text` flag. +1. The text output has changed. The previous format contained too much information and created unreadable output. The new text output puts each result on its own line, and includes the plugin name, description, and other useful information. +1. If you are using CloudSploit as source input to other systems, we strongly recommend using the JSON output option to create a standardized output file (do not try to parse the output text format). Use `--json=file.json` to create results in a JSON structure. diff --git a/docs/writing-plugins.md b/docs/writing-plugins.md new file mode 100644 index 0000000000..18330a94df --- /dev/null +++ b/docs/writing-plugins.md @@ -0,0 +1,405 @@ +# Writing CloudSploit Plugins + +## Collection Phase +To write a plugin, you want to understand which data is needed and how your cloud infrastructure provides them via their API calls. Once you have identified the API calls needed, you can add them to the collect.js file for your cloud infrastructure provider. This file determines the cloud infrastructure API calls and their run-order. + +### Collectors + +* [AWS Collection](#aws-collection) +* [Azure Collection](#azure-collection) +* [GCP Collection](#gcp-collection) +* [Oracle Collection](#oracle-collection) + +#### AWS Collection +The following declaration tells the CloudSploit collection engine to query the CloudFront service using the `listDistributions` call and then save the results returned under `DistributionList.Items`. + +``` +CloudFront: { + listDistributions: { + property: 'DistributionList', + secondProperty: 'Items' + } +}, +``` + +The second section in `collect.js` is `postcalls`, which is an array of objects defining API calls that rely on other calls first returned. For example, if you need to query for all `CloudFront distributions`, and then loop through each one and run a more detailed call, you would add the `CloudFront:listDistributions` call in the [`calls`](https://github.com/cloudsploit/scans/blob/master/collectors/aws/collector.js#L58-L64) section and then the more detailed call in [`postcalls`](https://github.com/cloudsploit/scans/blob/master/collectors/aws/collector.js#L467-L473), setting it to rely on the output of `listDistributions` call. + +An example: + +``` +getGroup: { + reliesOnService: 'iam', + reliesOnCall: 'listGroups', + filterKey: 'GroupName', + filterValue: 'GroupName' +}, +``` + +This section tells CloudSploit to wait until the `IAM:listGroups` call has been made, and then loop through the data that is returned. The `filterKey` tells CloudSploit the name of the key from the original response, while `filterValue` tells it which property to set in the `getGroup` call filter. For example: `iam.getGroup({GroupName:abc})` where `abc` is the `GroupName` from the returned list. CloudSploit will loop through each response, re-invoking `getGroup` for each element. + +You can find the [AWS Collector here.](https://github.com/cloudsploit/scans/blob/master/collectors/aws/collector.js) + +#### Azure Collection + +The following declaration tells the Cloudsploit collection engine to query the Compute Management Service using the virtualMachines:listAll call. + +``` +virtualMachines: { + listAll: { + api: "ComputeManagementClient", + arm: true + } +}, +``` + +The second section in `collect.js` is `postcalls`, which is an array of objects defining API calls that rely on other calls first returned. For example, if you need to query for all `Virtual Machine instances`, and then loop through each one and run a more detailed call, you would add the `virtualMachines:listAll` call in the [`calls`](https://github.com/cloudsploit/scans/blob/master/collectors/azure/collector.js#L50-L55) section and then the more detailed call in [`postcalls`](https://github.com/cloudsploit/scans/blob/master/collectors/azure/collector.js#L293-L302), setting it to rely on the output of `listDistributions` call. + +``` +virtualMachineExtensions: { + list: { + api: "ComputeManagementClient", + reliesOnService: ['resourceGroups', 'virtualMachines'], + reliesOnCall: ['list', 'listAll'], + filterKey: ['resourceGroupName', 'name'], + filterValue: ['resourceGroupName', 'name'], + arm: true + } +}, +``` + +You can find the [Azure Collector here.](https://github.com/cloudsploit/scans/blob/master/collectors/azure/collector.js) + +#### GCP Collection + +The following declaration tells the Cloudsploit collection engine to query the Compute Management Service using the buckets:list call. + +``` +buckets: { + list: { + api: 'storage', + version: 'v1', + location: null, + } +}, +``` + +The second section in `collect.js` is `postcalls`, which is an array of objects defining API calls that rely on other calls first returned. For example, if you need to query for all `Storage Buckets`, and then loop through each one and run a more detailed call, you would add the `buckets:list` call in the [`calls`](https://github.com/cloudsploit/scans/blob/master/collectors/google/collector.js#L103-L109) section and then the more detailed call in [`postcalls`](https://github.com/cloudsploit/scans/blob/master/collectors/google/collector.js#L213-L223), setting it to rely on the output of `getIamPolicy` call. + +``` +buckets: { + getIamPolicy: { + api: 'storage', + version: 'v1', + location: null, + reliesOnService: ['buckets'], + reliesOnCall: ['list'], + filterKey: ['bucket'], + filterValue: ['name'], + } +}, +``` + +You can find the [GCP Collector here.](https://github.com/cloudsploit/scans/blob/master/collectors/google/collector.js) + +#### Oracle Collection + +The following declaration tells the Cloudsploit collection engine to query the Compute Management Service using the vcn:list call. + +``` +vcn: { + list: { + api: "core", + filterKey: ['compartmentId'], + filterValue: ['compartmentId'], + } +}, +``` + +The second section in `collect.js` is `postcalls`, which is an array of objects defining API calls that rely on other calls first returned. For example, if you need to query for all `VCNs`, and then loop through each one and run a more detailed call, you would add the `vcn:list` call in the [`calls`](https://github.com/cloudsploit/scans/blob/master/collectors/oracle/collector.js#L41-L47) section and then the more detailed call in [`postcalls`](https://github.com/cloudsploit/scans/blob/master/collectors/oracle/collector.js#L243-L251), setting it to rely on the output of `get` call. + +``` +vcn: { + get: { + api: "core", + reliesOnService: ['vcn'], + reliesOnCall: ['list'], + filterKey: ['vcnId'], + filterValue: ['id'], + } +}, +``` + +You can find the [Oracle Collector here.](https://github.com/cloudsploit/scans/blob/master/collectors/oracle/collector.js) + +## Scanning Phase + +After the data has been collected, it is passed to the scanning engine when the results are analyzed for risks. Each plugin must export the following: + +* Exports the following: + * ```title``` (string): a user-friendly title for the plugin + * ```category``` (string): the cloud infrastructure category (i.e.: **_AWS:_** EC2, RDS, ELB, etc. **_Azure:_** ) + * ```description``` (string): a description of what the plugin does + * ```more_info``` (string): a more detailed description of the risk being tested for + * ```link``` (string): an cloud infrastructure help URL describing the service or risk, preferably with mitigation methods + * ```recommended_action``` (string): what the user should do to mitigate the risk found + * ```run``` (function): a function that runs the test (see below) +* Accepts a ```collection``` object via the run function containing the full collection object obtained in the first phase. +* Calls back with the results and the data source. + +## Result Codes +Each test has a result code that is used to determine if the test was successful and its risk level. The following codes are used: + +* 0: PASS: No risks +* 1: WARN: The result represents a potential misconfiguration or issue but is not an immediate risk +* 2: FAIL: The result presents an immediate risk to the security of the account +* 3: UNKNOWN: The results could not be determined (API failure, wrong permissions, etc.) + +## Tips for Writing Plugins +* Many security risks can be detected using the same API calls. To minimize the number of API calls being made, utilize the `cache` helper function to cache the results of an API call made in one test for future tests. For example, two plugins: "s3BucketPolicies" and "s3BucketPreventDelete" both call APIs to list every S3 bucket. These can be combined into a single plugin "s3Buckets" which exports two tests called "bucketPolicies" and "preventDelete". This way, the API is called once, but multiple tests are run on the same results. +* Ensure cloud infrastructure API calls are being used optimally. For example, call describeInstances with empty parameters to get all instances, instead of calling describeInstances multiple times looping through each instance name. +* Use async.eachLimit to reduce the number of simultaneous API calls. Instead of using a for loop on 100 requests, spread them out using async's each limit. + +## Example +### AWS +To more clearly illustrate writing a new plugin, let's consider the "IAM Empty Groups" plugin. First, we know that we will need to query for a list of groups via `listGroups`, then loop through each group and query for the more detailed set of data via `getGroup`. + +We'll add these API calls to `collect.js`. First, under `calls` add: +``` +IAM: { + listGroups: { + property: 'Groups' + } +}, +``` +The `property` tells CloudSploit which property to read in the response from AWS. + +Then, under `postCalls`, add: +``` +IAM: { + getGroup: { + reliesOnService: 'iam', + reliesOnCall: 'listGroups', + filterKey: 'GroupName', + filterValue: 'GroupName' + } +}, +``` +CloudSploit will first get the list of groups, then, it will loop through each one, using the group name to get more detailed info via `getGroup`. + +Next, we'll write the plugin. Create a new file in the `plugins/iam` folder called `emptyGroups.js` (this plugin already exists, but you can create a similar one for the purposes of this example). + +In the file, we'll be sure to export the plugin's title, category, description, link, and more information about it. Additionally, we will add any API calls it makes: +``` +apis: ['IAM:listGroups', 'IAM:getGroup'], +``` +In the `run` function, we can obtain the output of the collection phase from earlier by doing: +``` +var listGroups = helpers.addSource(cache, source, + ['iam', 'listGroups', region]); +``` +Then, we can loop through each of the results and do: +``` +var getGroup = helpers.addSource(cache, source, + ['iam', 'getGroup', region, group.GroupName]); +``` +The `helpers` function ensures that the proper results are returned from the collection and that they are saved into a "source" variable which can be returned with the results. + +Now, we can write the plugin functionality by checking for the data relevant to our requirements: +``` +if (!getGroup || getGroup.err || !getGroup.data || !getGroup.data.Users) { + helpers.addResult(results, 3, 'Unable to query for group: ' + group.GroupName, 'global', group.Arn); +} else if (!getGroup.data.Users.length) { + helpers.addResult(results, 0, 'Group: ' + group.GroupName + ' does not contain any users', 'global', group.Arn); + return cb(); +} else { + helpers.addResult(results, 0, 'Group: ' + group.GroupName + ' contains ' + getGroup.data.Users.length + ' user(s)', 'global', group.Arn); +} +``` +The `addResult` function ensures we are adding the results to the `results` array in the proper format. This function accepts the following: +``` +(results array, score, message, region, resource) +``` +The `resource` is optional, and the `score` must be between 0 and 3 to indicate PASS, WARN, FAIL, or UNKNOWN. + +### Azure +To more clearly illustrate writing a new plugin, let us consider the Virtual Machines VM Endpoint Protection plugin `plugins/azure/virtualmachines/vmEndpointProtection.js` . First, we know that we will need to query for a list of virtual machines via `virtualMachines:listAll`, then loop through each group and query for the more detailed set of data via `virtualMachineExtensions:list`. + +We'll add these API calls to `collect.js`. First, under `calls` add: + +``` +virtualMachines: { + listAll: { + url: 'https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.Compute/virtualMachines?api-version=2019-12-01' + } +}, +``` + +Then, under `postcalls`, add: +``` +virtualMachineExtensions: { + list: { + reliesOnPath: 'virtualMachines.listAll', + properties: ['id'], + url: 'https://management.azure.com/{id}/extensions?api-version=2019-12-01' + } +}, +``` +CloudSploit will first get the list of virtual machines, then, it will loop through each one, using the virtual machine name to get more detailed info via `virtualMachineExtensions`. + +Next, we'll write the plugin. Create a new file in the `plugins/virtualmachines` folder called `vmEndpointProtection.js` (this plugin already exists, but you can create a similar one for the purposes of this example). + +In the file, we'll be sure to export the plugin's title, category, description, link, and more information about it. Additionally, we will add any API calls it makes: +``` +apis: ['virtualMachines:listAll', 'virtualMachineExtensions:list'], +``` +In the `run` function, we can obtain the output of the collection phase from earlier by doing: +``` +var virtualMachines = helpers.addSource(cache, source, + ['virtualMachines', 'listAll', location]); +``` +Then, we can loop through each of the results and do: +``` +var virtualMachineExtensions = helpers.addSource(cache, source, ['virtualMachineExtensions', 'list', location, virtualMachine.id]); +``` +The `helpers` function ensures that the proper results are returned from the collection and that they are saved into a "source" variable which can be returned with the results. + +Now, we can write the plugin functionality by checking for the data relevant to our requirements: +``` +if (virtualMachineExtensions.err || !virtualMachineExtensions.data) { + helpers.addResult(results, 3, + Unable to query for VM Extensions: ' + helpers.addError(virtualMachineExtensions), location); + return rcb(); +} +if (!virtualMachineExtensions.data.length) { + helpers.addResult(results, 0, 'No VM Extensions found', location); +} +``` +The `addResult` function ensures we are adding the results to the `results` array in the proper format. This function accepts the following: +``` +(results array, score, message, region, resource) +``` +The `resource` is optional, and the `score` must be between 0 and 3 to indicate PASS, WARN, FAIL, or UNKNOWN. + +### GCP +To more clearly illustrate writing a new plugin, let us consider the Storage Bucket All Users Policy plugin `plugins/google/storage/bucketAllUsersPolicy.js` . First, we know that we will need to query for a list of buckets via `buckets:list`, then loop through each group and query for the more detailed set of data via `buckets:getIamPolicy`. + +We'll add these API calls to `collect.js`. First, under `calls` add: + +``` +buckets: { + list: { + api: 'storage', + version: 'v1', + location: null, + } +}, +``` + +Then, under `postcalls`, add: +``` +buckets: { + getIamPolicy: { + api: 'storage', + version: 'v1', + location: null, + reliesOnService: ['buckets'], + reliesOnCall: ['list'], + filterKey: ['bucket'], + filterValue: ['name'], + } +}, +``` +CloudSploit will first get the list of buckets, then, it will loop through each one, using the bucket name to get more detailed info via `getIamPolicy`. + +Next, we'll write the plugin. Create a new file in the `plugins/google/storage` folder called `bucketAllUsersPolicy.js` (this plugin already exists, but you can create a similar one for the purposes of this example). + +In the file, we'll be sure to export the plugin's title, category, description, link, and more information about it. Additionally, we will add any API calls it makes: +``` +apis: ['buckets:list', 'buckets:getIamPolicy'], +``` +In the `run` function, we can obtain the output of the collection phase from earlier by doing: +``` +let bucketPolicyPolicies = helpers.addSource(cache, source, + ['buckets', 'getIamPolicy', region]); +``` +The `helpers` function ensures that the proper results are returned from the collection and that they are saved into a "source" variable which can be returned with the results. + +Now, we can write the plugin functionality by checking for the data relevant to our requirements: +``` +if (bucketPolicyPolicies.err || !bucketPolicyPolicies.data) { + helpers.addResult(results, 3, 'Unable to query storage buckets: ' + helpers.addError(bucketPolicyPolicies), region); + return rcb(); +} + +if (!bucketPolicyPolicies.data.length) { + helpers.addResult(results, 0, 'No storage buckets found', region); + return rcb(); +} +``` +The `addResult` function ensures we are adding the results to the `results` array in the proper format. This function accepts the following: +``` +(results array, score, message, region, resource) +``` +The `resource` is optional, and the `score` must be between 0 and 3 to indicate PASS, WARN, FAIL, or UNKNOWN. + +### Oracle +To more clearly illustrate writing a new plugin, let us consider the Networking Subnet Multi AD plugin `plugins/oracle/networking/subnetMultiAd.js` . First, we know that we will need to query for a list of VCNs via `vcn:list`, then loop through each group and query for the more detailed set of data via `subnet:list`. + +We'll add these API calls to `collect.js`. First, under `calls` add: + +``` +vcn: { + list: { + api: "core", + filterKey: ['compartmentId'], + filterValue: ['compartmentId'], + } +}, +``` + +Then, under `postcalls`, add: +``` +subnet: { + list: { + api: "core", + reliesOnService: ['vcn'], + reliesOnCall: ['list'], + filterKey: ['compartmentId', 'vcnId'], + filterValue: ['compartmentId', 'id'], + filterConfig: [true, false], + } +}, +``` +CloudSploit will first get the list of vcns, then, it will loop through each one, using the vcn id to get more detailed info via `subnet:list`. + +Next, we'll write the plugin. Create a new file in the `plugins/oracle/networking` folder called `subnetMultiAd.js` (this plugin already exists, but you can create a similar one for the purposes of this example). + +In the file, we'll be sure to export the plugin's title, category, description, link, and more information about it. Additionally, we will add any API calls it makes: +``` +apis: ['vcn:list','subnet:list'] +``` +In the `run` function, we can obtain the output of the collection phase from earlier by doing: +``` +var subnets = helpers.addSource(cache, source, + ['subnet', 'list', region]); +``` +The `helpers` function ensures that the proper results are returned from the collection and that they are saved into a "source" variable which can be returned with the results. + +Now, we can write the plugin functionality by checking for the data relevant to our requirements: +``` +if ((subnets.err && subnets.err.length) || !subnets.data) { + helpers.addResult(results, 3, + 'Unable to query for subnets: ' + helpers.addError(subnets), region); + return rcb(); +} + +if (!subnets.data.length) { + helpers.addResult(results, 0, 'No subnets found', region); + return rcb(); +} +``` +The `addResult` function ensures we are adding the results to the `results` array in the proper format. This function accepts the following: +``` +(results array, score, message, region, resource) +``` +The `resource` is optional, and the `score` must be between 0 and 3 to indicate PASS, WARN, FAIL, or UNKNOWN. diff --git a/engine.js b/engine.js index 04ba6c65e3..46e5c69c70 100644 --- a/engine.js +++ b/engine.js @@ -1,201 +1,155 @@ var async = require('async'); -var plugins = require('./exports.js'); -var complianceControls = require('./compliance/controls.js'); +var exports = require('./exports.js'); var suppress = require('./postprocess/suppress.js'); var output = require('./postprocess/output.js'); /** * The main function to execute CloudSploit scans. - * @param AWSConfig The configuration for AWS. If undefined, then don't run. - * @param AzureConfig The configuration for Azure. If undefined, then don't run. - * @param GitHubConfig The configuration for Github. If undefined, then don't run. - * @param OracleConfig The configuration for Oracle. If undefined, then don't run. - * @param GoogleConfig The configuration for Google. If undefined, then don't run. - + * @param cloudConfig The configuration for the cloud provider. * @param settings General purpose settings. */ -var engine = function(AWSConfig, AzureConfig, GitHubConfig, OracleConfig, GoogleConfig, settings) { - // Determine if scan is a compliance scan - var complianceArgs = process.argv - .filter(function(arg) { - return arg.startsWith('--compliance='); - }) - .map(function(arg) { - return arg.substring(13); - }); - var compliance = complianceControls.create(complianceArgs); - if (!compliance) { - console.log('ERROR: Unsupported compliance mode. Please use one of the following:'); - console.log(' --compliance=hipaa'); - console.log(' --compliance=pci'); - console.log(' --compliance=cis'); - console.log(' --compliance=cis-1'); - console.log(' --compliance=cis-2'); - process.exit(); - } - +var engine = function(cloudConfig, settings) { // Initialize any suppression rules based on the the command line arguments - var suppressionFilter = suppress.create(process.argv); + var suppressionFilter = suppress.create(settings.suppress); // Initialize the output handler - var outputHandler = output.create(process.argv); - - // The original cloudsploit always has a 0 exit code. With this option, we can have - // the exit code depend on the results (useful for integration with CI systems) - var useStatusExitCode = process.argv.includes('--statusExitCode'); - - // Configure Service Provider Collectors - var serviceProviders = { - aws : { - name: 'aws', - collector: require('./collectors/aws/collector.js'), - config: AWSConfig, - apiCalls: [], - skipRegions: [] // Add any regions you wish to skip here. Ex: 'us-east-2' - }, - azure : { - name: 'azure', - collector: require('./collectors/azure/collector.js'), - config: AzureConfig, - apiCalls: [], - skipRegions: [] // Add any locations you wish to skip here. Ex: 'East US' - }, - github: { - name: 'github', - collector: require('./collectors/github/collector.js'), - config: GitHubConfig, - apiCalls: [] - }, - oracle: { - name: 'oracle', - collector: require('./collectors/oracle/collector.js'), - config: OracleConfig, - apiCalls: [] - }, - google: { - name: 'google', - collector: require('./collectors/google/collector.js'), - config: GoogleConfig, - apiCalls: [] - } - }; - - // Ignore Service Providers without a Config Object - for (var provider in serviceProviders){ - if (serviceProviders[provider].config == undefined) delete serviceProviders[provider]; + var outputHandler = output.create(settings); + + // Configure Service Provider Collector + var collector = require(`./collectors/${settings.cloud}/collector.js`); + var plugins = exports[settings.cloud]; + var apiCalls = []; + + // Print customization options + if (settings.compliance) console.log(`INFO: Using compliance modes: ${settings.compliance.join(', ')}`); + if (settings.govcloud) console.log('INFO: Using AWS GovCloud mode'); + if (settings.china) console.log('INFO: Using AWS China mode'); + if (settings.ignore_ok) console.log('INFO: Ignoring passing results'); + if (settings.skip_paginate) console.log('INFO: Skipping AWS pagination mode'); + if (settings.suppress && settings.suppress.length) console.log('INFO: Suppressing results based on suppress flags'); + if (settings.plugin) { + if (!plugins[settings.plugin]) return console.log(`ERROR: Invalid plugin: ${settings.plugin}`); + console.log(`INFO: Testing plugin: ${plugins[settings.plugin].title}`); } // STEP 1 - Obtain API calls to make console.log('INFO: Determining API calls to make...'); - function getMapValue(obj, key) { - if (Object.prototype.hasOwnProperty.call(obj, key)) - return obj[key]; - throw new Error('Invalid map key.'); - } + var skippedPlugins = []; + + Object.entries(plugins).forEach(function(p){ + var pluginId = p[0]; + var plugin = p[1]; + + // Skip plugins that don't match the ID flag + var skip = false; + if (settings.plugin && settings.plugin !== pluginId) { + skip = true; + } else { + // Skip GitHub plugins that do not match the run type + if (settings.cloud == 'github') { + if (cloudConfig.organization && + plugin.types.indexOf('org') === -1) { + skip = true; + console.debug(`DEBUG: Skipping GitHub plugin ${plugin.title} because it is not for Organization accounts`); + } else if (!cloudConfig.organization && + plugin.types.indexOf('org') === -1) { + skip = true; + console.debug(`DEBUG: Skipping GitHub plugin ${plugin.title} because it is not for User accounts`); + } + } - for (var p in plugins) { - if (!plugins[p]) continue; - for (var sp in serviceProviders) { - var serviceProviderPlugins = getMapValue(plugins, serviceProviders[sp].name); - var serviceProviderAPICalls = serviceProviders[sp].apiCalls; - var serviceProviderConfig = serviceProviders[sp].config; - for (var spp in serviceProviderPlugins) { - var plugin = getMapValue(serviceProviderPlugins, spp); - // Skip GitHub plugins that do not match the run type - if (sp == 'github' && serviceProviderConfig.organization && - plugin.types.indexOf('org') === -1) continue; - - if (sp == 'github' && !serviceProviderConfig.organization && - plugin.types.indexOf('user') === -1) continue; - - // Skip if our compliance set says don't run the rule - if (!compliance.includes(spp, plugin)) continue; - - for (var pac in plugin.apis) { - if (serviceProviderAPICalls.indexOf(plugin.apis[pac]) === -1) { - serviceProviderAPICalls.push(plugin.apis[pac]); + if (settings.compliance && settings.compliance.length) { + if (!plugin.compliance || !Object.keys(plugin.compliance).length) { + skip = true; + console.debug(`DEBUG: Skipping plugin ${plugin.title} because it is not used for compliance programs`); + } else { + // Compare + var cMatch = false; + settings.compliance.forEach(function(c){ + if (plugin.compliance[c]) cMatch = true; + }); + if (!cMatch) { + skip = true; + console.debug(`DEBUG: Skipping plugin ${plugin.title} because it did not match compliance programs ${settings.compliance.join(', ')}`); } } } } - } - console.log('INFO: API calls determined.'); - console.log('INFO: Collecting metadata. This may take several minutes...'); - - // STEP 2 - Collect API Metadata from Service Providers - async.map(serviceProviders, function(serviceProviderObj, serviceProviderDone) { - - settings.api_calls = serviceProviderObj.apiCalls; - settings.skip_regions = serviceProviderObj.skipRegions; - - serviceProviderObj.collector(serviceProviderObj.config, settings, function(err, collection) { - if (err || !collection) return console.log(`ERROR: Unable to obtain API metadata: ${err}`); - outputHandler.writeCollection(collection, serviceProviderObj.name); - - console.log(''); - console.log('-----------------------'); - console.log(serviceProviderObj.name.toUpperCase()); - console.log('-----------------------'); - console.log(''); - console.log(''); - console.log('INFO: Metadata collection complete. Analyzing...'); - console.log('INFO: Analysis complete. Scan report to follow...\n'); - console.log(''); - - var serviceProviderPlugins = getMapValue(plugins, serviceProviderObj.name); - - async.mapValuesLimit(serviceProviderPlugins, 10, function(plugin, key, pluginDone) { - if (!compliance.includes(key, plugin)) { - return pluginDone(null, 0); - } - - // Skip GitHub plugins that do not match the run type - if (serviceProviderObj.name == 'github' && - serviceProviderObj.config.organization && - plugin.types.indexOf('org') === -1) return pluginDone(null, 0); - - if (serviceProviderObj.name == 'github' && - !serviceProviderObj.config.organization && - plugin.types.indexOf('user') === -1) return pluginDone(null, 0); - - var maximumStatus = 0; - plugin.run(collection, settings, function(err, results) { - outputHandler.startCompliance(plugin, key, compliance); + if (skip) { + skippedPlugins.push(pluginId); + } else { + plugin.apis.forEach(function(api) { + if (apiCalls.indexOf(api) === -1) apiCalls.push(api); + }); + } + }); - for (var r in results) { - // If we have suppressed this result, then don't process it - // so that it doesn't affect the return code. - if (suppressionFilter([key, results[r].region || 'any', results[r].resource || 'any'].join(':'))) { - continue; - } + if (!apiCalls.length) return console.log('ERROR: Nothing to collect.'); - // Write out the result (to console or elsewhere) - outputHandler.writeResult(results[r], plugin, key); + console.log(`INFO: Found ${apiCalls.length} API calls to make for ${settings.cloud} plugins`); + console.log('INFO: Collecting metadata. This may take several minutes...'); - // Add this to our tracking fo the worst status to calculate - // the exit code - maximumStatus = Math.max(maximumStatus, results[r].status); + // STEP 2 - Collect API Metadata from Service Providers + collector(cloudConfig, { + api_calls: apiCalls, + paginate: settings.skip_paginate, + govcloud: settings.govcloud, + china: settings.china + }, function(err, collection) { + if (err || !collection || !Object.keys(collection).length) return console.log(`ERROR: Unable to obtain API metadata: ${err || 'No data returned'}`); + outputHandler.writeCollection(collection, settings.cloud); + + console.log('INFO: Metadata collection complete. Analyzing...'); + console.log('INFO: Analysis complete. Scan report to follow...'); + + var maximumStatus = 0; + + async.mapValuesLimit(plugins, 10, function(plugin, key, pluginDone) { + if (skippedPlugins.indexOf(key) > -1) return pluginDone(null, 0); + + plugin.run(collection, settings, function(err, results) { + for (var r in results) { + // If we have suppressed this result, then don't process it + // so that it doesn't affect the return code. + if (suppressionFilter([key, results[r].region || 'any', results[r].resource || 'any'].join(':'))) { + continue; + } + + var complianceMsg = []; + if (settings.compliance && settings.compliance.length) { + settings.compliance.forEach(function(c){ + if (plugin.compliance && plugin.compliance[c]) { + complianceMsg.push(`${c.toUpperCase()}: ${plugin.compliance[c]}`); + } + }); } + complianceMsg = complianceMsg.join('; '); + if (!complianceMsg.length) complianceMsg = null; + + // Write out the result (to console or elsewhere) + outputHandler.writeResult(results[r], plugin, key, complianceMsg); - outputHandler.endCompliance(plugin, key, compliance); + // Add this to our tracking fo the worst status to calculate + // the exit code + maximumStatus = Math.max(maximumStatus, results[r].status); + } - setTimeout(function() { pluginDone(err, maximumStatus); }, 0); - }); - }, function(err, results){ - if (err) return console.log(err); - var summaryStatus = Math.max(...Object.values(results)); - serviceProviderDone(err, summaryStatus); + setTimeout(function() { pluginDone(err, maximumStatus); }, 0); }); + }, function(err) { + if (err) return console.log(err); + // console.log(JSON.stringify(collection, null, 2)); + outputHandler.close(); + if (settings.exit_code) { + // The original cloudsploit always has a 0 exit code. With this option, we can have + // the exit code depend on the results (useful for integration with CI systems) + console.log(`INFO: Exiting with exit code: ${maximumStatus}`); + process.exitCode = maximumStatus; + } + console.log('INFO: Scan complete'); }); - }, function(err, results) { - // console.log(JSON.stringify(collection, null, 2)); - outputHandler.close(); - if (useStatusExitCode) { - process.exitCode = Math.max(results); - } - console.log('Done'); }); }; diff --git a/engine.spec.js b/engine.spec.js index 3de372c221..4faabcf89f 100644 --- a/engine.spec.js +++ b/engine.spec.js @@ -5,6 +5,6 @@ describe('engine', function () { it('should run with no arguments', function () { // Although we don't pass in anything, this is enough to test // that our dependencies are actually installed. - engine(undefined, undefined, undefined, undefined, undefined, {}); + engine({}, {cloud: 'aws'}); }) }); diff --git a/exports.js b/exports.js index 5a338cfa5e..b50c9d9a75 100644 --- a/exports.js +++ b/exports.js @@ -5,6 +5,7 @@ module.exports = { 'acmValidation' : require(__dirname + '/plugins/aws/acm/acmValidation.js'), 'acmCertificateExpiry' : require(__dirname + '/plugins/aws/acm/acmCertificateExpiry.js'), 'asgMultiAz' : require(__dirname + '/plugins/aws/autoscaling/asgMultiAz.js'), + 'emptyASG' : require(__dirname + '/plugins/aws/autoscaling/emptyASG.js'), 'workgroupEncrypted' : require(__dirname + '/plugins/aws/athena/workgroupEncrypted.js'), 'workgroupEnforceConfiguration' : require(__dirname + '/plugins/aws/athena/workgroupEnforceConfiguration.js'), 'publicS3Origin' : require(__dirname + '/plugins/aws/cloudfront/publicS3Origin.js'), @@ -116,6 +117,7 @@ module.exports = { 'iamUserAdmins' : require(__dirname + '/plugins/aws/iam/iamUserAdmins.js'), 'iamUserNameRegex' : require(__dirname + '/plugins/aws/iam/iamUserNameRegex.js'), 'iamRolePolicies' : require(__dirname + '/plugins/aws/iam/iamRolePolicies.js'), + 'iamRoleLastUsed' : require(__dirname + '/plugins/aws/iam/iamRoleLastUsed.js'), 'maxPasswordAge' : require(__dirname + '/plugins/aws/iam/maxPasswordAge.js'), 'minPasswordLength' : require(__dirname + '/plugins/aws/iam/minPasswordLength.js'), 'noUserIamPolicies' : require(__dirname + '/plugins/aws/iam/noUserIamPolicies.js'), @@ -126,6 +128,7 @@ module.exports = { 'passwordRequiresUppercase' : require(__dirname + '/plugins/aws/iam/passwordRequiresUppercase.js'), 'passwordReusePrevention' : require(__dirname + '/plugins/aws/iam/passwordReusePrevention.js'), 'rootAccessKeys' : require(__dirname + '/plugins/aws/iam/rootAccessKeys.js'), + 'rootSigningCertificate' : require(__dirname + '/plugins/aws/iam/rootSigningCertificate.js'), 'rootAccountInUse' : require(__dirname + '/plugins/aws/iam/rootAccountInUse.js'), 'rootHardwareMfa' : require(__dirname + '/plugins/aws/iam/rootHardwareMfa.js'), 'rootMfaEnabled' : require(__dirname + '/plugins/aws/iam/rootMfaEnabled.js'), @@ -149,6 +152,7 @@ module.exports = { 'rdsMultiAz' : require(__dirname + '/plugins/aws/rds/rdsMultiAz.js'), 'rdsSnapshotEncryption' : require(__dirname + '/plugins/aws/rds/rdsSnapshotEncryption.js'), 'rdsMinorVersionUpgrade' : require(__dirname + '/plugins/aws/rds/rdsMinorVersionUpgrade.js'), + 'sqlServerTLSVersion' : require(__dirname + '/plugins/aws/rds/sqlServerTLSVersion'), 'domainAutoRenew' : require(__dirname + '/plugins/aws/route53/domainAutoRenew.js'), 'domainExpiry' : require(__dirname + '/plugins/aws/route53/domainExpiry.js'), diff --git a/index.js b/index.js old mode 100644 new mode 100755 index 6f61d149a8..26e612ef56 --- a/index.js +++ b/index.js @@ -1,130 +1,204 @@ #!/usr/bin/env node -var engine = require('./engine'); - -var AWSConfig; -var AzureConfig; -var GitHubConfig; -var OracleConfig; -var GoogleConfig; - -// OPTION 1: Configure service provider credentials through hard-coded config objects - -// AWSConfig = { -// accessKeyId: '', -// secretAccessKey: '', -// sessionToken: '', -// region: 'us-east-1' -// }; - -// AzureConfig = { -// ApplicationID: '', // A.K.A ClientID -// KeyValue: '', // Secret -// DirectoryID: '', // A.K.A TenantID or Domain -// SubscriptionID: '', -// location: 'East US' -// }; - -// GitHubConfig = { -// token: '', // GitHub app token -// url: 'https://api.github.com', // BaseURL if not using public GitHub -// organization: false, // Set to true if the login is an organization -// login: '' // The login id for the user or organization -// }; - -// Oracle Important Note: -// Please read Oracle API's key generation instructions: config/_oracle/keys/Readme.md -// You will want an API signing key to fill the keyFingerprint and privateKey params -// OracleConfig = { -// RESTversion: '/20160918', -// tenancyId: 'ocid1.tenancy.oc1..', -// compartmentId: 'ocid1.compartment.oc1..', -// userId: 'ocid1.user.oc1..', -// keyFingerprint: 'YOURKEYFINGERPRINT', -// keyValue: "-----BEGIN PRIVATE KEY-----\nYOUR-PRIVATE-KEY-GOES-HERE\n-----END PRIVATE KEY-----\n", -// region: 'us-ashburn-1', -// }; - -// GoogleConfig = { -// "type": "service_account", -// "project": "your-project-name", -// "client_email": "cloudsploit@your-project-name.iam.gserviceaccount.com", -// "private_key": "-----BEGIN PRIVATE KEY-----\nYOUR-PRIVATE-KEY-GOES-HERE\n-----END PRIVATE KEY-----\n", -// }; - -// OPTION 2: Import a service provider config file containing credentials - -// AWSConfig = require(__dirname + '/aws_credentials.json'); -// AzureConfig = require(__dirname + '/azure_credentials.json'); -// GitHubConfig = require(__dirname + '/github_credentials.json'); -// OracleConfig = require(__dirname + '/oracle_credentials.json'); -// GoogleConfig = require(__dirname + '/google_credentials.json'); - -// OPTION 3: ENV configuration with service provider env vars -if(process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY){ - AWSConfig = { - accessKeyId: process.env.AWS_ACCESS_KEY_ID, - secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, - sessionToken: process.env.AWS_SESSION_TOKEN, - region: process.env.AWS_DEFAULT_REGION || 'us-east-1' - }; -} +const { ArgumentParser } = require('argparse'); +const engine = require('./engine'); + +console.log(` + _____ _ _ _____ _ _ _ + / ____| | | |/ ____| | | (_) | + | | | | ___ _ _ __| | (___ _ __ | | ___ _| |_ + | | | |/ _ \\| | | |/ _\` |\\___ \\| '_ \\| |/ _ \\| | __| + | |____| | (_) | |_| | (_| |____) | |_) | | (_) | | |_ + \\_____|_|\\___/ \\__,_|\\__,_|_____/| .__/|_|\\___/|_|\\__| + | | + |_| + + CloudSploit by Aqua Security, Ltd. + Cloud security auditing for AWS, Azure, GCP, Oracle, and GitHub +`); + +const parser = new ArgumentParser({}); + +parser.add_argument('--config', { + help: 'The path to a CloudSploit config file containing cloud credentials. See config_example.js' +}); + +parser.add_argument('--compliance', { + help: 'Compliance mode. Only return results applicable to the selected program.', + choices: ['hipaa', 'cis', 'cis1', 'cis2', 'pci'], + action: 'append' +}); +parser.add_argument('--plugin', { + help: 'A specific plugin to run. If none provided, all plugins will be run. Obtain from the exports.js file. E.g. acmValidation' +}); +parser.add_argument('--govcloud', { + help: 'AWS only. Enables GovCloud mode.', + action: 'store_true' +}); +parser.add_argument('--china', { + help: 'AWS only. Enables AWS China mode.', + action: 'store_true' +}); +parser.add_argument('--csv', { help: 'Output: CSV file' }); +parser.add_argument('--json', { help: 'Output: JSON file' }); +parser.add_argument('--junit', { help: 'Output: Junit file' }); +parser.add_argument('--console', { + help: 'Console output format. Default: table', + choices: ['none', 'text', 'table'], + default: 'table' +}); +parser.add_argument('--collection', { help: 'Output: full collection JSON as file' }); +parser.add_argument('--ignore-ok', { + help: 'Ignore passing (OK) results', + action: 'store_true' +}); +parser.add_argument('--exit-code', { + help: 'Exits with a non-zero status code if non-passing results are found', + action: 'store_true' +}); +parser.add_argument('--skip-paginate', { + help: 'AWS only. Skips pagination (for debugging).', + action: 'store_false' +}); +parser.add_argument('--suppress', { + help: 'Suppress results matching the provided Regex. Format: pluginId:region:resourceId', + action: 'append' +}); + +let settings = parser.parse_args(); +let cloudConfig = {}; + +settings.cloud = 'aws'; -if(process.env.AZURE_APPLICATION_ID && process.env.AZURE_KEY_VALUE){ - AzureConfig = { - ApplicationID: process.env.AZURE_APPLICATION_ID, - KeyValue: process.env.AZURE_KEY_VALUE, - DirectoryID: process.env.AZURE_DIRECTORY_ID, - SubscriptionID: process.env.AZURE_SUBSCRIPTION_ID, - region: process.env.AZURE_LOCATION || 'eastus' - }; +// Now execute the scans using the defined configuration information. +if (!settings.config) { + // AWS will handle the default credential chain without needing a credential file + console.log('INFO: No config file provided, using default AWS credential chain.'); + return engine(cloudConfig, settings); } -if(process.env.GITHUB_LOGIN){ - GitHubConfig = { - url: process.env.GITHUB_URL || 'https://api.github.com', - login: process.env.GITHUB_LOGIN, - organization: process.env.GITHUB_ORG ? true : false - }; +// If "compliance=cis" is passed, turn into "compliance=cis1 and compliance=cis2" +if (settings.compliance && settings.compliance.indexOf('cis') > -1) { + if (settings.compliance.indexOf('cis1') === -1) { + settings.compliance.push('cis1'); + } + if (settings.compliance.indexOf('cis2') === -1) { + settings.compliance.push('cis2'); + } + settings.compliance = settings.compliance.filter(function(e) { return e !== 'cis'; }); } -if(process.env.ORACLE_TENANCY_ID && process.env.ORACLE_USER_ID){ - OracleConfig = { - RESTversion: process.env.ORACLE_REST_VERSION, - tenancyId: process.env.ORACLE_TENANCY_ID, - compartmentId: process.env.ORACLE_COMPARTMENT_ID, - userId: process.env.ORACLE_USER_ID, - keyFingerprint: process.env.ORACLE_KEY_FINGERPRINT, - region: process.env.ORACLE_REGION || 'us-ashburn-1' - }; -} +console.log(`INFO: Using CloudSploit config file: ${settings.config}`); -if(process.env.GOOGLE_PROJECT_ID && process.env.GOOGLE_API_KEY){ - GoogleConfig = { - project: process.env.GOOGLE_PROJECT_ID, - API_KEY: process.env.GOOGLE_API_KEY, - serviceId: process.env.GOOGLE_SERVICE_ID, - region: process.env.GOOGLE_DEFAULT_REGION || 'us-east1' - }; +try { + var config = require(settings.config); +} catch(e) { + console.error('ERROR: Config file could not be loaded. Please ensure you have copied the config_example.js file to config.js'); + process.exit(1); } -if(process.env.GOOGLE_APPLICATION_CREDENTIALS){ - GoogleConfig = require(process.env.GOOGLE_APPLICATION_CREDENTIALS); - GoogleConfig.project = GoogleConfig.project_id; -} - -// Custom settings - place plugin-specific settings here -var settings = {}; -// If running in GovCloud, uncomment the following -// settings.govcloud = true; - -// If running in AWS China, uncomment the following -// settings.china = true; +function loadHelperFile(path) { + try { + var contents = require(path); + } catch (e) { + console.error(`ERROR: The credential file could not be loaded ${path}`); + console.error(e); + process.exit(1); + } + return contents; +} -// If you want to disable AWS pagination, set the setting to false here -settings.paginate = true; +function checkRequiredKeys(obj, keys) { + keys.forEach(function(key){ + if (!obj[key] || !obj[key].length) { + console.error(`ERROR: The credential config did not contain a valid value for: ${key}`); + process.exit(1); + } + }); +} -settings.debugTime = false; +if (config.credentials.aws.credential_file) { + cloudConfig = loadHelperFile(config.credentials.aws.credential_file); + if (!cloudConfig || !cloudConfig.accessKeyId || !cloudConfig.secretAccessKey) { + console.error('ERROR: AWS credential file does not have accessKeyId or secretAccessKey properties'); + process.exit(1); + } +} else if (config.credentials.aws.access_key) { + checkRequiredKeys(config.credentials.aws, ['secret_access_key']); + cloudConfig = { + accessKeyId: config.credentials.aws.access_key, + secretAccessKey: config.credentials.aws.secret_access_key, + sessionToken: config.credentials.aws.session_token, + region: 'us-east-1' + }; +} else if (config.credentials.azure.credential_file) { + settings.cloud = 'azure'; + cloudConfig = loadHelperFile(config.credentials.azure.credential_file); + if (!cloudConfig || !cloudConfig.ApplicationID || !cloudConfig.KeyValue || !cloudConfig.DirectoryID || !cloudConfig.SubscriptionID) { + console.error('ERROR: Azure credential file does not have ApplicationID, KeyValue, DirectoryID, or SubscriptionID'); + process.exit(1); + } + cloudConfig.location = 'East US'; +} else if (config.credentials.azure.application_id) { + settings.cloud = 'azure'; + checkRequiredKeys(config.credentials.azure, ['key_value', 'directory_id', 'subscription_id']); + cloudConfig = { + ApplicationID: config.credentials.azure.application_id, + KeyValue: config.credentials.azure.key_value, + DirectoryID: config.credentials.azure.directory_id, + SubscriptionID: config.credentials.azure.subscription_id, + location: 'East US' + }; +} else if (config.credentials.google.credential_file) { + settings.cloud = 'google'; + cloudConfig = loadHelperFile(config.credentials.google.credential_file); +} else if (config.credentials.google.project) { + settings.cloud = 'google'; + checkRequiredKeys(config.credentials.google, ['client_email', 'private_key']); + cloudConfig = { + type: 'service_account', + project: config.credentials.google.project, + client_email: config.credentials.google.client_email, + private_key: config.credentials.google.private_key, + }; +} else if (config.credentials.oracle.credential_file) { + settings.cloud = 'oracle'; + cloudConfig = loadHelperFile(config.credentials.oracle.credential_file); + if (!cloudConfig || !cloudConfig.tenancyId || !cloudConfig.compartmentId || !cloudConfig.userId || !cloudConfig.keyValue) { + console.error('ERROR: Oracle credential file does not have tenancyId, compartmentId, userId, or keyValue'); + process.exit(1); + } + + cloudConfig.RESTversion = '/20160918'; + cloudConfig.region = 'us-ashburn-1'; +} else if (config.credentials.oracle.tenancy_id) { + settings.cloud = 'oracle'; + checkRequiredKeys(config.credentials.oracle, ['compartment_id', 'user_id', 'key_fingerprint', 'key_value']); + cloudConfig = { + RESTversion: '/20160918', + tenancyId: config.credentials.oracle.tenancy_id, + compartmentId: config.credentials.oracle.compartment_id, + userId: config.credentials.oracle.user_id, + keyFingerprint: config.credentials.oracle.key_fingerprint, + keyValue: config.credentials.oracle.key_value, + region: 'us-ashburn-1', + }; +} else if (config.credentials.github.credential_file) { + settings.cloud = 'github'; + cloudConfig = loadHelperFile(config.credentials.github.credential_file); +} else if (config.credentials.github.token) { + settings.cloud = 'github'; + checkRequiredKeys(config.credentials.github, ['url', 'login']); + cloudConfig = { + token: config.credentials.github.token, + url: config.credentials.github.url, + organization: config.credentials.github.organization, + login: config.credentials.github.login + }; +} else { + console.error('ERROR: Config file does not contain any valid credential configs.'); + process.exit(1); +} // Now execute the scans using the defined configuration information. -engine(AWSConfig, AzureConfig, GitHubConfig, OracleConfig, GoogleConfig, settings); +engine(cloudConfig, settings); diff --git a/package-lock.json b/package-lock.json index e40b8b87f4..3a3bfd5893 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "cloudsploit", - "version": "0.0.1-dev5", + "version": "2.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -327,6 +327,11 @@ "@types/node": ">= 8" } }, + "@types/color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==" + }, "@types/node": { "version": "13.11.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-13.11.0.tgz", @@ -346,6 +351,18 @@ "event-target-shim": "^5.0.0" } }, + "acorn": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.0.tgz", + "integrity": "sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w==", + "dev": true + }, + "acorn-jsx": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.2.0.tgz", + "integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==", + "dev": true + }, "adal-node": { "version": "0.1.28", "resolved": "https://registry.npmjs.org/adal-node/-/adal-node-0.1.28.tgz", @@ -403,6 +420,23 @@ "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==", "dev": true }, + "ansi-escapes": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", + "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", + "dev": true, + "requires": { + "type-fest": "^0.11.0" + }, + "dependencies": { + "type-fest": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", + "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==", + "dev": true + } + } + }, "ansi-regex": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", @@ -455,12 +489,9 @@ "dev": true }, "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "requires": { - "sprintf-js": "~1.0.2" - } + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.0.tgz", + "integrity": "sha512-mEKF1/WpTsblaqx7NIkcsTxwDzvJuGH5sdUqDNcJS+vXCWe+yM/o4cs/Q2/GFESAYg+O7UouEmz+iBqmKofI/Q==" }, "arr-diff": { "version": "4.0.0", @@ -486,6 +517,15 @@ "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", "dev": true }, + "array.prototype.flat": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.3.tgz", + "integrity": "sha512-gBlRZV0VSmfPIeWfuuy56XZMvbVfbEUnOXUvt3F/eUUUSyzlgLxhEX4YAEpxNAogRGehPSnfXyPtYyKAhkzQhQ==", + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1" + } + }, "arrify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", @@ -516,6 +556,12 @@ "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", "dev": true }, + "astral-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", + "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "dev": true + }, "async": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", @@ -547,11 +593,11 @@ "integrity": "sha1-D+9a1G8b16hQLGVyfwNn1e5D1pY=" }, "aws-sdk": { - "version": "2.653.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.653.0.tgz", - "integrity": "sha512-vtpHfoAKoudNa5kknUgQeXzdnmkI63hqKYHuk5u7mx0HelP8iybTxmKfKENlOvkfKtBdCEbcmJRa3DxZUbQPHQ==", + "version": "2.740.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.740.0.tgz", + "integrity": "sha512-cSedIe7g5/S5o23jHvm9+vWhcYysmuKrmbML1Z0pO9KxlqOA9s4Z5f6Il7ZmvAktfmrxu1SyQu4YEoP5DL4/zw==", "requires": { - "buffer": "4.9.1", + "buffer": "4.9.2", "events": "1.1.1", "ieee754": "1.1.13", "jmespath": "0.15.0", @@ -572,159 +618,6 @@ "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.1.tgz", "integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==" }, - "azure-arm-authorization": { - "version": "8.3.1", - "resolved": "https://registry.npmjs.org/azure-arm-authorization/-/azure-arm-authorization-8.3.1.tgz", - "integrity": "sha512-s6XqNSpBhh7gdhVD9syCNlwkWG7VcN4PllwmNf/K9zEBpj8a2k46z9xbdNtZEGcNlBgUl8Tqeeu4d19WPktVSA==", - "requires": { - "ms-rest": "^2.3.3", - "ms-rest-azure": "^2.5.5" - } - }, - "azure-arm-cdn": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/azure-arm-cdn/-/azure-arm-cdn-4.2.0.tgz", - "integrity": "sha512-DkpLntvqHtCLbf7p/qqLS0eJluZtsb8gU65deJYiMz4OFQco+InP9giCVnY8gElW3QbMaqKyHOJCDK7NllKMoA==", - "requires": { - "ms-rest": "^2.5.0", - "ms-rest-azure": "^2.5.5" - } - }, - "azure-arm-compute": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/azure-arm-compute/-/azure-arm-compute-9.1.0.tgz", - "integrity": "sha512-Xn/kTHmU+5JKm6LjPOk/B3ph1M6UBiJCMFjB8NeMdvzWwoIe0xDiwdXpBJX6iskxXqEZoc5g2er1v12vyWgZJQ==", - "requires": { - "ms-rest": "^2.3.3", - "ms-rest-azure": "^2.5.5" - } - }, - "azure-arm-containerregistry": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/azure-arm-containerregistry/-/azure-arm-containerregistry-5.1.0.tgz", - "integrity": "sha512-kDJSVYAgYWTkYeRcQznnOzk0e9p0nOhrDSTVDgO2Bf+2uh1u3hq+aEvauWqb8X+rnVAOIjNgrOSltRsonCZohA==", - "requires": { - "ms-rest": "^2.5.0", - "ms-rest-azure": "^2.5.5" - } - }, - "azure-arm-containerservice": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/azure-arm-containerservice/-/azure-arm-containerservice-8.0.0.tgz", - "integrity": "sha512-GujKeAfe3pqd9FkAyJ0Afxr5f4GGaHiwkSgbgqIOkgdL4mDbWUvDI4EO/TakIKU+fMJMSmoGtKQLR/H32E3ldQ==", - "requires": { - "ms-rest": "^2.5.0", - "ms-rest-azure": "^2.5.5" - } - }, - "azure-arm-keyvault": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/azure-arm-keyvault/-/azure-arm-keyvault-1.2.0.tgz", - "integrity": "sha512-P1QgUHTSpKvko/mX2u7LDPf5yYinTOtIvMHJeZluQGTSRToMF9mBKqt1TKEzaHyS0eQDErDoLHyBSs7D9E1Qaw==", - "requires": { - "ms-rest": "^2.3.3", - "ms-rest-azure": "^2.5.5" - } - }, - "azure-arm-monitor": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/azure-arm-monitor/-/azure-arm-monitor-5.4.0.tgz", - "integrity": "sha512-1IvpqNx2XnOzXJruEJ2Pf66mvbsBnTsb3IAyryQNEVvLCNDZUK4xM/J0d39NPg/c6IoEmtZwIUSdM68KwvkQGA==", - "requires": { - "ms-rest": "^2.5.0", - "ms-rest-azure": "^2.5.5" - } - }, - "azure-arm-mysql": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/azure-arm-mysql/-/azure-arm-mysql-3.2.0.tgz", - "integrity": "sha512-L/KbAggszefQGNvmPm3f98lRm9ZVAqbqTZNSYb59Bxr3xiZsdkOwPbl7rK9/cD4mxhc9czS1kbZ8HbvbupMd/A==", - "requires": { - "ms-rest": "^2.3.3", - "ms-rest-azure": "^2.5.5" - } - }, - "azure-arm-network": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/azure-arm-network/-/azure-arm-network-13.0.0.tgz", - "integrity": "sha512-6qQq+Pswnf9Y6Q/a8jDTThwSD7sjAu4EG/NnpJdYwVcwuu7YeBMZapromPZcArCStqft58/lLIlzlzvTghWTDg==", - "requires": { - "ms-rest": "^2.5.0", - "ms-rest-azure": "^2.5.5" - } - }, - "azure-arm-postgresql": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/azure-arm-postgresql/-/azure-arm-postgresql-4.3.0.tgz", - "integrity": "sha512-x4S5GaSJ2MRUntpnDipIPJxA2XuAQ5qUTbgGRVt05shyOg29k3s8gleyveqd3oWKPWaGoW3Nf/kBj6RE2B3v/Q==", - "requires": { - "ms-rest": "^2.5.0", - "ms-rest-azure": "^2.5.5" - } - }, - "azure-arm-resource": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/azure-arm-resource/-/azure-arm-resource-7.3.0.tgz", - "integrity": "sha512-2K+ps1Iwa4PBQFwdCn1X8kAVIRLH5M7nlNZtfOWaYd7DXJ131qJpwW8ul6gKZgG7DAI3PBodrGsHFvPdgA+AzQ==", - "requires": { - "ms-rest": "^2.3.3", - "ms-rest-azure": "^2.5.5" - } - }, - "azure-arm-security": { - "version": "1.2.0-preview", - "resolved": "https://registry.npmjs.org/azure-arm-security/-/azure-arm-security-1.2.0-preview.tgz", - "integrity": "sha512-2Cx9WHCXlU9g9CNcyI0b9Uax1bO2ZLzGK5H5a1dfvaIdI7vylADuntCk1FHT8d2R1YkRNUq2QPw35MhdqxmnIw==", - "requires": { - "ms-rest": "^2.3.3", - "ms-rest-azure": "^2.5.5" - } - }, - "azure-arm-sql": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/azure-arm-sql/-/azure-arm-sql-5.7.0.tgz", - "integrity": "sha512-qHkv8RXmvLsrYwRql+udnindBq50j8Uhf1r8Mjqfh5Hssy4oZM0JvQCdOyJUSIzCpbipUgYUQp4TKkmqkAJxRw==", - "requires": { - "ms-rest": "^2.5.0", - "ms-rest-azure": "^2.5.5" - } - }, - "azure-arm-storage": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/azure-arm-storage/-/azure-arm-storage-6.3.0.tgz", - "integrity": "sha512-P7xJpGTU3v7vItaCMvRsGxWvXwVbPPmu1lGwN0w4vcY+UmX5wRidbDM9DKXwzysoGqCNSJ0DZBw3GeUsLh1ouA==", - "requires": { - "ms-rest": "^2.3.3", - "ms-rest-azure": "^2.5.5" - } - }, - "azure-arm-website": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/azure-arm-website/-/azure-arm-website-5.7.0.tgz", - "integrity": "sha512-GnwqaelTIhv40YI3Ch8+Q272X6XXWTq99Y1aYWZb1cejSP4gjrWWeppwor4HtjlVU9i9YIvYO91TRjQt8FrHVA==", - "requires": { - "ms-rest": "^2.3.3", - "ms-rest-azure": "^2.5.5" - } - }, - "azure-graph": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/azure-graph/-/azure-graph-4.3.0.tgz", - "integrity": "sha512-P5gAcKwJW44Z3VjQJ1MW3TNyMZFGdUD9DNH27MM0TmPXEfqIS8quPsF3OW1vthyT06PAiGo48u3InfCjbn4WLQ==", - "requires": { - "ms-rest": "^2.3.3", - "ms-rest-azure": "^2.5.5" - } - }, - "azure-keyvault": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/azure-keyvault/-/azure-keyvault-3.0.5.tgz", - "integrity": "sha512-59fzKRq9dnzv03lEuImvgXc3QjRJoSJtK0gv1WXoqCivBuPdFNK+x6hAjoEDS2WEOXG+7m3uiJWqpMh/8NW3ow==", - "requires": { - "ms-rest": "^2.5.0", - "ms-rest-azure": "^2.6.0" - } - }, "azure-storage": { "version": "2.10.3", "resolved": "https://registry.npmjs.org/azure-storage/-/azure-storage-2.10.3.tgz", @@ -766,8 +659,7 @@ "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, "base": { "version": "0.11.2", @@ -896,7 +788,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -937,6 +828,14 @@ } } }, + "breakword": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/breakword/-/breakword-1.0.5.tgz", + "integrity": "sha512-ex5W9DoOQ/LUEU3PMdLs9ua/CYZl1678NUkKOdUSi8Aw5F1idieaiRURCBFJCwVcrD1J8Iy3vfWSloaMwO2qFg==", + "requires": { + "wcwidth": "^1.0.1" + } + }, "browser-stdout": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", @@ -954,9 +853,9 @@ "integrity": "sha1-M3dm2hWAEhD92VbCLpxokaudAzc=" }, "buffer": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", - "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", "requires": { "base64-js": "^1.0.2", "ieee754": "^1.1.4", @@ -1023,11 +922,16 @@ } } }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, "camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" }, "capture-stack-trace": { "version": "1.0.1", @@ -1076,6 +980,12 @@ } } }, + "chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, "check-error": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", @@ -1143,6 +1053,21 @@ "integrity": "sha1-T6kXw+WclKAEzWH47lCdplFocUM=", "dev": true }, + "cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "requires": { + "restore-cursor": "^3.1.0" + } + }, + "cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "dev": true + }, "cliui": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", @@ -1182,6 +1107,11 @@ } } }, + "clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=" + }, "collection-visit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", @@ -1230,8 +1160,7 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "configstore": { "version": "3.1.2", @@ -1333,6 +1262,32 @@ "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=", "dev": true }, + "csv": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/csv/-/csv-5.3.2.tgz", + "integrity": "sha512-odDyucr9OgJTdGM2wrMbJXbOkJx3nnUX3Pt8SFOwlAMOpsUQlz1dywvLMXJWX/4Ib0rjfOsaawuuwfI5ucqBGQ==", + "requires": { + "csv-generate": "^3.2.4", + "csv-parse": "^4.8.8", + "csv-stringify": "^5.3.6", + "stream-transform": "^2.0.1" + } + }, + "csv-generate": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/csv-generate/-/csv-generate-3.2.4.tgz", + "integrity": "sha512-qNM9eqlxd53TWJeGtY1IQPj90b563Zx49eZs8e0uMyEvPgvNVmX1uZDtdzAcflB3PniuH9creAzcFOdyJ9YGvA==" + }, + "csv-parse": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-4.12.0.tgz", + "integrity": "sha512-wPQl3H79vWLPI8cgKFcQXl0NBgYYEqVnT1i6/So7OjMpsI540oD7p93r3w6fDSyPvwkTepG05F69/7AViX2lXg==" + }, + "csv-stringify": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-5.5.1.tgz", + "integrity": "sha512-HM0/86Ks8OwFbaYLd495tqTs1NhscZL52dC4ieKYumy8+nawQYC0xZ63w1NqLf0M148T2YLYqowoImc1giPn0g==" + }, "csv-write-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/csv-write-stream/-/csv-write-stream-2.0.0.tgz", @@ -1341,6 +1296,16 @@ "argparse": "^1.0.7", "generate-object-property": "^1.0.0", "ndjson": "^1.3.0" + }, + "dependencies": { + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "requires": { + "sprintf-js": "~1.0.2" + } + } } }, "dashdash": { @@ -1367,8 +1332,7 @@ "decamelize": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "dev": true + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" }, "decode-uri-component": { "version": "0.2.0", @@ -1391,6 +1355,12 @@ "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "dev": true }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, "default-require-extensions": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-2.0.0.tgz", @@ -1400,11 +1370,18 @@ "strip-bom": "^3.0.0" } }, + "defaults": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", + "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", + "requires": { + "clone": "^1.0.2" + } + }, "define-properties": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "dev": true, "requires": { "object-keys": "^1.0.12" } @@ -1472,10 +1449,19 @@ "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", "dev": true }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, "dot-prop": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", - "integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.1.tgz", + "integrity": "sha512-l0p4+mIuJIua0mhxGoh4a+iNL9bmeK5DvnSVQa6T0OhrVmaEa1XScX5Etc673FePCJOArq/4Pa2cLGODUWTPOQ==", "dev": true, "requires": { "is-obj": "^1.0.0" @@ -1536,7 +1522,6 @@ "version": "1.17.5", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz", "integrity": "sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==", - "dev": true, "requires": { "es-to-primitive": "^1.2.1", "function-bind": "^1.1.1", @@ -1555,7 +1540,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", - "dev": true, "requires": { "is-callable": "^1.1.4", "is-date-object": "^1.0.1", @@ -1574,12 +1558,178 @@ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", "dev": true }, + "eslint": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz", + "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "ajv": "^6.10.0", + "chalk": "^2.1.0", + "cross-spawn": "^6.0.5", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^1.4.3", + "eslint-visitor-keys": "^1.1.0", + "espree": "^6.1.2", + "esquery": "^1.0.1", + "esutils": "^2.0.2", + "file-entry-cache": "^5.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.0.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "inquirer": "^7.0.0", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.3.0", + "lodash": "^4.17.14", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "optionator": "^0.8.3", + "progress": "^2.0.0", + "regexpp": "^2.0.1", + "semver": "^6.1.2", + "strip-ansi": "^5.2.0", + "strip-json-comments": "^3.0.1", + "table": "^5.2.3", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "glob-parent": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "globals": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "dev": true, + "requires": { + "type-fest": "^0.8.1" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + } + } + }, + "eslint-scope": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.0.tgz", + "integrity": "sha512-iiGRvtxWqgtx5m8EyQUJihBloE4EnYeGE/bz1wSPwJE6tZuJUtHlhqDM4Xj2ukE8Dyy1+HCZ4hE0fzIVMzb58w==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "eslint-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", + "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + }, + "espree": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz", + "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==", + "dev": true, + "requires": { + "acorn": "^7.1.1", + "acorn-jsx": "^5.2.0", + "eslint-visitor-keys": "^1.1.0" + } + }, "esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", "dev": true }, + "esquery": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz", + "integrity": "sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + }, + "dependencies": { + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true + } + } + }, + "esrecurse": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", + "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "dev": true, + "requires": { + "estraverse": "^4.1.0" + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, "event-target-shim": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", @@ -1695,6 +1845,17 @@ } } }, + "external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "requires": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + } + }, "extglob": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", @@ -1775,6 +1936,12 @@ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, "fast-safe-stringify": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz", @@ -1785,6 +1952,24 @@ "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.1.tgz", "integrity": "sha512-x4FEgaz3zNRtJfLFqJmHWxkMDDvXVtaznj2V9jiP8ACUJrUgist4bP9FmDL2Vew2Y9mEQI/tG4GqabaitYp9CQ==" }, + "figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "file-entry-cache": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", + "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", + "dev": true, + "requires": { + "flat-cache": "^2.0.1" + } + }, "file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -1870,6 +2055,34 @@ } } }, + "flat-cache": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", + "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", + "dev": true, + "requires": { + "flatted": "^2.0.0", + "rimraf": "2.6.3", + "write": "1.0.3" + }, + "dependencies": { + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "flatted": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", + "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", + "dev": true + }, "for-in": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", @@ -2564,7 +2777,12 @@ "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", "dev": true }, "gaxios": { @@ -2606,8 +2824,7 @@ "get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" }, "get-func-name": { "version": "2.0.0", @@ -2772,6 +2989,11 @@ "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==", "dev": true }, + "grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==" + }, "growl": { "version": "1.10.5", "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", @@ -2807,7 +3029,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, "requires": { "function-bind": "^1.1.1" } @@ -2821,8 +3042,7 @@ "has-symbols": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", - "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", - "dev": true + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==" }, "has-value": { "version": "1.0.0", @@ -2919,17 +3139,42 @@ "debug": "4" } }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, "ieee754": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, "ignore-by-default": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", "integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk=", "dev": true }, + "import-fresh": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", + "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, "import-lazy": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", @@ -2963,6 +3208,123 @@ "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", "dev": true }, + "inquirer": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", + "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==", + "dev": true, + "requires": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.19", + "mute-stream": "0.0.8", + "run-async": "^2.4.0", + "rxjs": "^6.6.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", + "dev": true + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, "is-accessor-descriptor": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", @@ -3006,8 +3368,7 @@ "is-callable": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", - "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==", - "dev": true + "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==" }, "is-ci": { "version": "1.2.1", @@ -3041,8 +3402,7 @@ "is-date-object": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", - "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==", - "dev": true + "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==" }, "is-descriptor": { "version": "0.1.6", @@ -3164,7 +3524,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", - "dev": true, "requires": { "has": "^1.0.3" } @@ -3184,7 +3543,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", - "dev": true, "requires": { "has-symbols": "^1.0.1" } @@ -3361,6 +3719,17 @@ "requires": { "argparse": "^1.0.7", "esprima": "^4.0.0" + }, + "dependencies": { + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + } } }, "jsbn": { @@ -3406,6 +3775,12 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, "json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", @@ -3469,6 +3844,11 @@ "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "dev": true }, + "kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==" + }, "latest-version": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-3.1.0.tgz", @@ -3478,6 +3858,16 @@ "package-json": "^4.0.0" } }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, "load-json-file": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", @@ -3501,9 +3891,9 @@ } }, "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" }, "lodash.flattendeep": { "version": "4.4.0", @@ -3678,11 +4068,16 @@ "mime-db": "1.43.0" } }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, "requires": { "brace-expansion": "^1.1.7" } @@ -3728,6 +4123,11 @@ } } }, + "mixme": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/mixme/-/mixme-0.3.5.tgz", + "integrity": "sha512-SyV9uPETRig5ZmYev0ANfiGeB+g6N2EnqqEfBbCGmmJ6MgZ3E4qv5aPbnHVdZ60KAHHXV+T3sXopdrnIXQdmjQ==" + }, "mkdirp": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.4.tgz", @@ -3833,6 +4233,12 @@ } } }, + "mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true + }, "nan": { "version": "2.14.0", "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", @@ -3859,6 +4265,12 @@ "to-regex": "^3.0.1" } }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, "ndjson": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/ndjson/-/ndjson-1.5.0.tgz", @@ -4064,14 +4476,12 @@ "object-inspect": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz", - "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==", - "dev": true + "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==" }, "object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" }, "object-visit": { "version": "1.0.1", @@ -4094,7 +4504,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", - "dev": true, "requires": { "define-properties": "^1.1.2", "function-bind": "^1.1.1", @@ -4142,6 +4551,29 @@ "wrappy": "1" } }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, "os-homedir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", @@ -4157,6 +4589,12 @@ "windows-release": "^3.1.0" } }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, "p-finally": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", @@ -4166,7 +4604,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, "requires": { "p-try": "^2.0.0" } @@ -4183,8 +4620,7 @@ "p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" }, "package-hash": { "version": "3.0.0", @@ -4210,6 +4646,15 @@ "semver": "^5.1.0" } }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, "parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", @@ -4302,6 +4747,12 @@ "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", "dev": true }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, "prepend-http": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", @@ -4313,6 +4764,12 @@ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true + }, "pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", @@ -4421,6 +4878,12 @@ "safe-regex": "^1.1.0" } }, + "regexpp": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", + "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", + "dev": true + }, "registry-auth-token": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.4.0.tgz", @@ -4497,14 +4960,12 @@ "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", - "dev": true + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" }, "require-main-filename": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "dev": true + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" }, "resolve": { "version": "1.15.1", @@ -4527,6 +4988,16 @@ "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", "dev": true }, + "restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "requires": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + } + }, "ret": { "version": "0.1.15", "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", @@ -4542,6 +5013,21 @@ "glob": "^7.1.3" } }, + "run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true + }, + "rxjs": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.2.tgz", + "integrity": "sha512-BHdBMVoWC2sL26w//BCu3YzKT4s2jip/WhwsGEDmeKYBhKDZeYezVUnHatYB7L85v5xs0BAQmg6BEYJEKxBabg==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, "safe-buffer": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", @@ -4583,8 +5069,7 @@ "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", - "dev": true + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" }, "set-value": { "version": "2.0.1", @@ -4642,6 +5127,164 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" }, + "slice-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", + "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "astral-regex": "^1.0.0", + "is-fullwidth-code-point": "^2.0.0" + } + }, + "smartwrap": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/smartwrap/-/smartwrap-2.0.1.tgz", + "integrity": "sha512-BoOIGADm5Q9OXGVmT899L1fx6dvOYciVUPB9yFlE1XkxRs3/6sqT/HYZoXOxNb4jj2zshtH+jhmGa6wGkNDPgA==", + "requires": { + "array.prototype.flat": "^1.2.3", + "breakword": "^1.0.5", + "grapheme-splitter": "^1.0.4", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1", + "yargs": "^15.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" + }, + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "requires": { + "p-limit": "^2.2.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "requires": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + } + }, + "yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, "snapdragon": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", @@ -4900,6 +5543,14 @@ } } }, + "stream-transform": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/stream-transform/-/stream-transform-2.0.2.tgz", + "integrity": "sha512-J+D5jWPF/1oX+r9ZaZvEXFbu7znjxSkbNAHJ9L44bt/tCVuOEWZlDqU9qJk7N2xBU1S+K2DPpSKeR/MucmCA1Q==", + "requires": { + "mixme": "^0.3.1" + } + }, "string-width": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", @@ -4914,7 +5565,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.0.tgz", "integrity": "sha512-EEJnGqa/xNfIg05SxiPSqRS7S9qwDhYts1TSLR1BQfYUfPe1stofgGKvwERK9+9yf+PpfBMlpBaCHucXGPQfUA==", - "dev": true, "requires": { "define-properties": "^1.1.3", "es-abstract": "^1.17.5" @@ -4924,7 +5574,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.2.tgz", "integrity": "sha512-gCA0tza1JBvqr3bfAIFJGqfdRTyPae82+KTnm3coDXkZN9wnuW3HjGgN386D7hfv5CHQYCI022/rJPVlqXyHSw==", - "dev": true, "requires": { "define-properties": "^1.1.3", "es-abstract": "^1.17.5", @@ -4935,7 +5584,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.2.tgz", "integrity": "sha512-ZNRQ7sY3KroTaYjRS6EbNiiHrOkjihL9aQE/8gfQ4DtAC/aEBRHFJa44OmoWxGGqXuJlfKkZW4WcXErGr+9ZFg==", - "dev": true, "requires": { "define-properties": "^1.1.3", "es-abstract": "^1.17.5", @@ -4946,7 +5594,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.0.tgz", "integrity": "sha512-iCP8g01NFYiiBOnwG1Xc3WZLyoo+RuBymwIlWncShXDDJYWN6DbnM3odslBJdgCdRlq94B5s63NWAZlcn2CS4w==", - "dev": true, "requires": { "define-properties": "^1.1.3", "es-abstract": "^1.17.5" @@ -4992,6 +5639,46 @@ "has-flag": "^3.0.0" } }, + "table": { + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", + "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", + "dev": true, + "requires": { + "ajv": "^6.10.2", + "lodash": "^4.17.14", + "slice-ansi": "^2.1.0", + "string-width": "^3.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, "term-size": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz", @@ -5063,6 +5750,12 @@ "require-main-filename": "^2.0.0" } }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, "through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -5117,6 +5810,15 @@ "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=", "dev": true }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.2" + } + }, "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -5190,6 +5892,182 @@ } } }, + "tslib": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", + "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==", + "dev": true + }, + "tty-table": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tty-table/-/tty-table-4.1.3.tgz", + "integrity": "sha512-Aireaxah5bTJkkJ7b2RI8tMZxogscWP4UCgi5dKuOqL8BWPjnnOebC7F+oFYWTLKUaiJm8h+CvsigBaaCtxqGw==", + "requires": { + "chalk": "^3.0.0", + "csv": "^5.3.2", + "kleur": "^3.0.3", + "smartwrap": "^2.0.1", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1", + "yargs": "^15.3.1" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" + }, + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "requires": { + "p-limit": "^2.2.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "requires": { + "has-flag": "^4.0.0" + } + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "requires": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + } + }, + "yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, "tunnel": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.5.tgz", @@ -5208,12 +6086,27 @@ "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" + } + }, "type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", "dev": true }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + }, "undefsafe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.3.tgz", @@ -5410,6 +6303,12 @@ "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" }, + "v8-compile-cache": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz", + "integrity": "sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ==", + "dev": true + }, "validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -5435,6 +6334,14 @@ "extsprintf": "^1.2.0" } }, + "wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", + "requires": { + "defaults": "^1.0.3" + } + }, "which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", @@ -5446,8 +6353,7 @@ "which-module": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", - "dev": true + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" }, "wide-align": { "version": "1.1.3", @@ -5475,6 +6381,12 @@ "execa": "^1.0.0" } }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, "wrap-ansi": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", @@ -5519,6 +6431,15 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, + "write": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", + "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", + "dev": true, + "requires": { + "mkdirp": "^0.5.1" + } + }, "write-file-atomic": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz", @@ -5568,8 +6489,7 @@ "y18n": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", - "dev": true + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==" }, "yallist": { "version": "3.1.1", diff --git a/package.json b/package.json index 44193010a5..e206ef6a32 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "cloudsploit", - "version": "0.0.1-dev5", - "description": "AWS security scanning scripts", + "version": "2.0.0", + "description": "AWS, Azure, GCP, Oracle, GitHub security scanning scripts", "main": "index.js", "scripts": { "test": "mocha './**/*.spec.js'", @@ -21,6 +21,11 @@ }, "keywords": [ "aws", + "azure", + "google", + "gcp", + "oracle", + "oci", "cloud", "security" ], @@ -29,7 +34,7 @@ "bugs": { "url": "https://github.com/cloudsploit/scans/issues" }, - "homepage": "https://cloudsploit.com", + "homepage": "https://cloud.aquasec.com", "publishConfig": { "access": "public" }, @@ -37,14 +42,16 @@ "@octokit/app": "^3.0.0", "@octokit/request": "^3.0.3", "@octokit/rest": "^16.3.2", + "argparse": "^2.0.0", "async": "^2.6.1", - "aws-sdk": "^2.648.0", + "aws-sdk": "^2.740.0", "azure-storage": "^2.10.3", "csv-write-stream": "^2.0.0", "fast-safe-stringify": "^2.0.6", "googleapis": "^40.0.1", "minimatch": "^3.0.4", - "ms-rest-azure": "^2.6.0" + "ms-rest-azure": "^2.6.0", + "tty-table": "^4.1.3" }, "devDependencies": { "chai": "^4.2.0", diff --git a/plugins/aws/acm/acmCertificateExpiry.js b/plugins/aws/acm/acmCertificateExpiry.js index e2e726af1f..1b37212ab4 100644 --- a/plugins/aws/acm/acmCertificateExpiry.js +++ b/plugins/aws/acm/acmCertificateExpiry.js @@ -9,6 +9,9 @@ module.exports = { link: 'https://docs.aws.amazon.com/acm/latest/userguide/managed-renewal.html', recommended_action: 'Ensure AWS is able to renew the certificate via email or DNS validation of the domain.', apis: ['ACM:listCertificates', 'ACM:describeCertificate'], + compliance: { + pci: 'PCI requires certificates to be kept up to date and rotated prior to expiry.' + }, settings: { acm_certificate_expiry_pass: { name: 'ACM Certificate Expiry Pass', diff --git a/plugins/aws/autoscaling/emptyASG.js b/plugins/aws/autoscaling/emptyASG.js new file mode 100644 index 0000000000..63f4bf660d --- /dev/null +++ b/plugins/aws/autoscaling/emptyASG.js @@ -0,0 +1,54 @@ +var async = require('async'); +var helpers = require('../../../helpers/aws'); + +module.exports = { + title: 'Empty AutoScaling Group', + category: 'AutoScaling', + description: 'Ensures all autoscaling groups contain at least 1 instance.', + more_info: 'AutoScaling groups that are no longer in use should be deleted to prevent accidental use.', + link: 'https://docs.aws.amazon.com/autoscaling/ec2/userguide/AutoScalingGroup.html', + recommended_action: 'Delete the unused AutoScaling group.', + apis: ['AutoScaling:describeAutoScalingGroups'], + + run: function(cache, settings, callback) { + var results = []; + var source = {}; + var regions = helpers.regions(settings); + + async.each(regions.autoscaling, function(region, rcb){ + var describeAutoScalingGroups = helpers.addSource(cache, source, + ['autoscaling', 'describeAutoScalingGroups', region]); + + if (!describeAutoScalingGroups) return rcb(); + + if (describeAutoScalingGroups.err || !describeAutoScalingGroups.data) { + helpers.addResult(results, 3, + 'Unable to query for auto scaling groups: ' + + helpers.addError(describeAutoScalingGroups), region); + return rcb(); + } + + if (!describeAutoScalingGroups.data.length) { + helpers.addResult(results, 0, 'No auto scaling groups found', region); + return rcb(); + } + + describeAutoScalingGroups.data.forEach(function(asg){ + var resource = asg.AutoScalingGroupARN; + if (!asg.Instances || !asg.Instances.length) { + helpers.addResult(results, 2, + 'Auto scaling group: ' + asg.AutoScalingGroupName + ' does not contain any instance', + region, resource); + } else { + helpers.addResult(results, 0, + 'Auto scaling group: ' + asg.AutoScalingGroupName + ' contains ' + asg.Instances.length + ' instance(s)', + region, resource); + } + }); + + rcb(); + }, function(){ + callback(null, results, source); + }); + } +}; diff --git a/plugins/aws/autoscaling/emptyASG.spec.js b/plugins/aws/autoscaling/emptyASG.spec.js new file mode 100644 index 0000000000..73bc84e1b7 --- /dev/null +++ b/plugins/aws/autoscaling/emptyASG.spec.js @@ -0,0 +1,168 @@ +var expect = require('chai').expect; +const emptyASG = require('./emptyASG'); + +const autoScalingGroups = [ + { + "AutoScalingGroupName": "auto-scaling-test-group", + "AutoScalingGroupARN": "arn:aws:autoscaling:us-east-1:111122223333:autoScalingGroup:e83ceb12-2760-4a92-a374-3df611331bdc:autoScalingGroupName/auto-scaling-test-group", + "LaunchTemplate": { + "LaunchTemplateId": "lt-0f1f6b356026abc86", + "LaunchTemplateName": "auto-scaling-template", + "Version": "$Default" + }, + "MinSize": 1, + "MaxSize": 1, + "DesiredCapacity": 1, + "DefaultCooldown": 300, + "AvailabilityZones": [ + "us-east-1a" + ], + "LoadBalancerNames": [], + "TargetGroupARNs": [], + "HealthCheckType": "EC2", + "HealthCheckGracePeriod": 300, + "Instances": [ + { + "InstanceId": "i-093267d7a579c4bee", + "InstanceType": "t2.micro", + "AvailabilityZone": "us-east-1a", + "LifecycleState": "InService", + "HealthStatus": "Healthy", + "LaunchTemplate": { + "LaunchTemplateId": "lt-0f1f6b356026abc86", + "LaunchTemplateName": "auto-scaling-template", + "Version": "1" + }, + "ProtectedFromScaleIn": false + } + ], + "CreatedTime": "2020-08-18T23:12:00.954Z", + "SuspendedProcesses": [], + "VPCZoneIdentifier": "subnet-06aa0f60", + "EnabledMetrics": [], + "Tags": [], + "TerminationPolicies": [ + "Default" + ], + "NewInstancesProtectedFromScaleIn": false, + "ServiceLinkedRoleARN": "arn:aws:iam::111122223333:role/aws-service-role/autoscaling.amazonaws.com/AWSServiceRoleForAutoScaling" + }, + { + "AutoScalingGroupName": "auto-scaling-test-group", + "AutoScalingGroupARN": "arn:aws:autoscaling:us-east-1:111122223333:autoScalingGroup:e83ceb12-2760-4a92-a374-3df611331bdc:autoScalingGroupName/auto-scaling-test-group", + "LaunchTemplate": { + "LaunchTemplateId": "lt-0f1f6b356026abc86", + "LaunchTemplateName": "auto-scaling-template", + "Version": "$Default" + }, + "MinSize": 1, + "MaxSize": 1, + "DesiredCapacity": 1, + "DefaultCooldown": 300, + "AvailabilityZones": [ + "us-east-1a" + ], + "LoadBalancerNames": [], + "TargetGroupARNs": [], + "HealthCheckType": "EC2", + "HealthCheckGracePeriod": 300, + "Instances": [], + "CreatedTime": "2020-08-18T23:12:00.954Z", + "SuspendedProcesses": [], + "VPCZoneIdentifier": "subnet-06aa0f60", + "EnabledMetrics": [], + "Tags": [], + "TerminationPolicies": [ + "Default" + ], + "NewInstancesProtectedFromScaleIn": false, + "ServiceLinkedRoleARN": "arn:aws:iam::111122223333:role/aws-service-role/autoscaling.amazonaws.com/AWSServiceRoleForAutoScaling" + }, +]; + + + +const createCache = (asgs, notifications) => { + return { + autoscaling: { + describeAutoScalingGroups: { + 'us-east-1': { + data: asgs + }, + }, + }, + }; +}; + +const createErrorCache = () => { + return { + autoscaling: { + describeAutoScalingGroups: { + 'us-east-1': { + err: { + message: 'error describing autoscaling groups' + }, + }, + }, + }, + }; +}; + +const createNullCache = () => { + return { + autoscaling: { + describeAutoScalingGroups: { + 'us-east-1': null, + }, + }, + }; +}; + +describe('emptyASG', function () { + describe('run', function () { + it('should PASS if autoscaling group contains instance(s)', function (done) { + const cache = createCache([autoScalingGroups[0]]); + emptyASG.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + done(); + }); + }); + + it('should FAIL if autoscaling group does not contain instance(s)', function (done) { + const cache = createCache([autoScalingGroups[1]]); + emptyASG.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(2); + done(); + }); + }); + + it('should PASS if no autoscaling group data found ', function (done) { + const cache = createCache([]); + emptyASG.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + done(); + }); + }); + + it('should UNKNOWN if unable to describe autoscaling group found', function (done) { + const cache = createErrorCache(); + emptyASG.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(3); + done(); + }); + }); + + it('should not return anything if no autoscaling group found', function (done) { + const cache = createNullCache(); + emptyASG.run(cache, {}, (err, results) => { + expect(results.length).to.equal(0); + done(); + }); + }); + + }); +}); \ No newline at end of file diff --git a/plugins/aws/cloudtrail/cloudtrailBucketAccessLogging.js b/plugins/aws/cloudtrail/cloudtrailBucketAccessLogging.js index 2c3b213ad8..4ea12c4ac1 100644 --- a/plugins/aws/cloudtrail/cloudtrailBucketAccessLogging.js +++ b/plugins/aws/cloudtrail/cloudtrailBucketAccessLogging.js @@ -14,7 +14,8 @@ module.exports = { 'verifying that the audit logs for the AWS environment are not modified.', pci: 'PCI requires tracking and monitoring of all access to environments ' + 'in which cardholder data is present. CloudTrail bucket access logging ' + - 'helps audit the bucket in which these logs are stored.' + 'helps audit the bucket in which these logs are stored.', + cis1: '2.6 Ensure CloudTrail bucket access logging is enabled' }, run: function(cache, settings, callback) { diff --git a/plugins/aws/cloudtrail/cloudtrailBucketPrivate.js b/plugins/aws/cloudtrail/cloudtrailBucketPrivate.js index 8d153475e5..53172b9ee2 100644 --- a/plugins/aws/cloudtrail/cloudtrailBucketPrivate.js +++ b/plugins/aws/cloudtrail/cloudtrailBucketPrivate.js @@ -9,6 +9,9 @@ module.exports = { recommended_action: 'Set the S3 bucket access policy for all CloudTrail buckets to only allow known users to access its files.', link: 'http://docs.aws.amazon.com/AmazonS3/latest/dev/example-bucket-policies.html', apis: ['CloudTrail:describeTrails', 'S3:getBucketAcl'], + compliance: { + cis1: '2.3 Ensure the S3 bucket used to store CloudTrail logs is not publicly accessible' + }, run: function(cache, settings, callback) { var results = []; diff --git a/plugins/aws/cloudtrail/cloudtrailEnabled.js b/plugins/aws/cloudtrail/cloudtrailEnabled.js index 6ca030e49b..a6dea402b5 100644 --- a/plugins/aws/cloudtrail/cloudtrailEnabled.js +++ b/plugins/aws/cloudtrail/cloudtrailEnabled.js @@ -15,7 +15,8 @@ module.exports = { 'logging and auditing solution for AWS since it is tightly ' + 'integrated into most AWS services and APIs.', pci: 'CloudTrail logs satisfy the PCI requirement to log all account activity ' + - 'within environments containing cardholder data.' + 'within environments containing cardholder data.', + cis1: '2.1 Ensure CloudTrail is enabled in all regions' }, run: function(cache, settings, callback) { var results = []; diff --git a/plugins/aws/cloudtrail/cloudtrailEncryption.js b/plugins/aws/cloudtrail/cloudtrailEncryption.js index 1182c98b48..3798d71028 100644 --- a/plugins/aws/cloudtrail/cloudtrailEncryption.js +++ b/plugins/aws/cloudtrail/cloudtrailEncryption.js @@ -9,6 +9,9 @@ module.exports = { recommended_action: 'Enable CloudTrail log encryption through the CloudTrail console or API', link: 'http://docs.aws.amazon.com/awscloudtrail/latest/userguide/encrypting-cloudtrail-log-files-with-aws-kms.html', apis: ['CloudTrail:describeTrails'], + compliance: { + cis2: '2.7 Ensure CloudTrail logs are encrypted at rest using KMS CMKs' + }, run: function(cache, settings, callback) { var results = []; diff --git a/plugins/aws/cloudtrail/cloudtrailFileValidation.js b/plugins/aws/cloudtrail/cloudtrailFileValidation.js index 86cdb0113a..0644893e3a 100644 --- a/plugins/aws/cloudtrail/cloudtrailFileValidation.js +++ b/plugins/aws/cloudtrail/cloudtrailFileValidation.js @@ -13,7 +13,8 @@ module.exports = { hipaa: 'The auditing requirements of HIPAA require logs to be kept securely ' + 'in a manner that prevents tampering. CloudTrail log validation ' + 'provides a mechanism of validating that the logs generated by ' + - 'AWS have not been modified, ensuring the integrity of the log data.' + 'AWS have not been modified, ensuring the integrity of the log data.', + cis2: '2.2 Ensure CloudTrail log file validation is enabled' }, run: function(cache, settings, callback) { diff --git a/plugins/aws/cloudtrail/cloudtrailToCloudwatch.js b/plugins/aws/cloudtrail/cloudtrailToCloudwatch.js index 12b4100066..d49fc5930f 100644 --- a/plugins/aws/cloudtrail/cloudtrailToCloudwatch.js +++ b/plugins/aws/cloudtrail/cloudtrailToCloudwatch.js @@ -9,6 +9,9 @@ module.exports = { recommended_action: 'Enable CloudTrail CloudWatch integration for all regions', link: 'http://docs.aws.amazon.com/awscloudtrail/latest/userguide/send-cloudtrail-events-to-cloudwatch-logs.html', apis: ['CloudTrail:describeTrails'], + compliance: { + cis1: '2.4 Ensure CloudTrail trails are integrated with CloudWatch Logs' + }, run: function(cache, settings, callback) { var results = []; diff --git a/plugins/aws/cloudwatchlogs/monitoringMetrics.js b/plugins/aws/cloudwatchlogs/monitoringMetrics.js index 3003b335b6..a95351ec5a 100644 --- a/plugins/aws/cloudwatchlogs/monitoringMetrics.js +++ b/plugins/aws/cloudwatchlogs/monitoringMetrics.js @@ -68,6 +68,9 @@ module.exports = { recommended_action: 'Enable metric filters to detect malicious activity in CloudTrail logs sent to CloudWatch.', link: 'http://docs.aws.amazon.com/awscloudtrail/latest/userguide/send-cloudtrail-events-to-cloudwatch-logs.html', apis: ['CloudTrail:describeTrails', 'CloudWatchLogs:describeMetricFilters'], + compliance: { + cis1: '3.0 Monitoring metrics are enabled' + }, run: function(cache, settings, callback) { var results = []; diff --git a/plugins/aws/configservice/configServiceEnabled.js b/plugins/aws/configservice/configServiceEnabled.js index fcc8fd5949..46847b725b 100644 --- a/plugins/aws/configservice/configServiceEnabled.js +++ b/plugins/aws/configservice/configServiceEnabled.js @@ -13,7 +13,8 @@ module.exports = { pci: 'PCI requires the development and maintenance of secure applications. ' + 'While ConfigService cannot assist in developing secure applications, ' + 'it can be used to detect application and environment changes that ' + - 'could introduce security risks.' + 'could introduce security risks.', + cis1: '2.5 Ensure AWS Config is enabled in all regions' }, run: function(cache, settings, callback) { diff --git a/plugins/aws/ec2/defaultSecurityGroup.js b/plugins/aws/ec2/defaultSecurityGroup.js index aede271ab7..35fcb73973 100644 --- a/plugins/aws/ec2/defaultSecurityGroup.js +++ b/plugins/aws/ec2/defaultSecurityGroup.js @@ -13,7 +13,8 @@ module.exports = { pci: 'PCI has strict requirements to segment networks using firewalls. ' + 'Security groups are a software-layer firewall that should be used ' + 'to isolate resources. Ensure default security groups to not allow ' + - 'unintended traffic to cross these isolation boundaries.' + 'unintended traffic to cross these isolation boundaries.', + cis2: '4.3 Ensure the default security group of every VPC restricts all traffic' }, run: function(cache, settings, callback) { diff --git a/plugins/aws/ec2/flowLogsEnabled.js b/plugins/aws/ec2/flowLogsEnabled.js index 1c6d74128b..3aa2e384fc 100644 --- a/plugins/aws/ec2/flowLogsEnabled.js +++ b/plugins/aws/ec2/flowLogsEnabled.js @@ -15,7 +15,8 @@ module.exports = { 'containing HIPAA data. Flow Logs should be enabled to satisfy ' + 'the audit controls of the HIPAA framework.', pci: 'PCI requires logging of all network access to environments containing ' + - 'cardholder data. Enable VPC flow logs to log these network requests.' + 'cardholder data. Enable VPC flow logs to log these network requests.', + cis2: '2.9 Ensure VPC flow logging is enabled in all VPCs' }, run: function(cache, settings, callback) { diff --git a/plugins/aws/ec2/openRDP.js b/plugins/aws/ec2/openRDP.js index ea07c72bac..6a83709d91 100644 --- a/plugins/aws/ec2/openRDP.js +++ b/plugins/aws/ec2/openRDP.js @@ -9,6 +9,9 @@ module.exports = { link: 'http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/authorizing-access-to-an-instance.html', recommended_action: 'Restrict TCP port 3389 to known IP addresses', apis: ['EC2:describeSecurityGroups'], + compliance: { + cis1: '4.2 Ensure no security groups allow ingress from 0.0.0.0/0 to port 3389' + }, remediation_description: 'The impacted security group rule will be deleted if no input is provided. Otherwise, any input will replace the open CIDR rule.', remediation_min_version: '202006020730', apis_remediate: ['EC2:describeSecurityGroups'], diff --git a/plugins/aws/ec2/openSSH.js b/plugins/aws/ec2/openSSH.js index 281c55a222..d5d472171e 100644 --- a/plugins/aws/ec2/openSSH.js +++ b/plugins/aws/ec2/openSSH.js @@ -9,6 +9,9 @@ module.exports = { link: 'http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/authorizing-access-to-an-instance.html', recommended_action: 'Restrict TCP port 22 to known IP addresses', apis: ['EC2:describeSecurityGroups'], + compliance: { + cis1: '4.1 Ensure no security groups allow ingress from 0.0.0.0/0 to port 22' + }, remediation_description: 'The impacted security group rule will be deleted if no input is provided. Otherwise, any input will replace the open CIDR rule.', remediation_min_version: '202006020730', apis_remediate: ['EC2:describeSecurityGroups'], diff --git a/plugins/aws/iam/accessKeysLastUsed.js b/plugins/aws/iam/accessKeysLastUsed.js index c5af7e2082..b95d0c8b51 100644 --- a/plugins/aws/iam/accessKeysLastUsed.js +++ b/plugins/aws/iam/accessKeysLastUsed.js @@ -11,7 +11,8 @@ module.exports = { apis: ['IAM:generateCredentialReport'], compliance: { pci: 'PCI requires that all users be removed if they are inactive for 90 days. ' + - 'If a user access key is inactive, it should be removed.' + 'If a user access key is inactive, it should be removed.', + cis1: '1.3 Ensure credentials unused for 90 days or greater are disabled' }, settings: { access_keys_last_used_fail: { diff --git a/plugins/aws/iam/accessKeysRotated.js b/plugins/aws/iam/accessKeysRotated.js index 04a84fcb7c..846b82e82c 100644 --- a/plugins/aws/iam/accessKeysRotated.js +++ b/plugins/aws/iam/accessKeysRotated.js @@ -15,7 +15,8 @@ module.exports = { 'users or systems accessing HIPAA-compliant environments.', pci: 'PCI requires that all user credentials are rotated every 90 days. While ' + 'IAM roles handle rotation automatically, access keys need to be manually ' + - 'rotated.' + 'rotated.', + cis1: '1.4 Ensure access keys are rotated every 90 days or less' }, settings: { access_keys_rotated_fail: { diff --git a/plugins/aws/iam/iamRoleLastUsed.js b/plugins/aws/iam/iamRoleLastUsed.js new file mode 100644 index 0000000000..7c57044be8 --- /dev/null +++ b/plugins/aws/iam/iamRoleLastUsed.js @@ -0,0 +1,93 @@ +var async = require('async'); +var helpers = require('../../../helpers/aws'); + +module.exports = { + title: 'IAM Role Last Used', + category: 'IAM', + description: 'Ensures IAM roles that have not been used within the given time frame are deleted.', + more_info: 'IAM roles that have not been used for a long period may contain old access policies that could allow unintended access to resources if accidentally attached to new services. These roles should be deleted.', + link: 'https://aws.amazon.com/about-aws/whats-new/2019/11/identify-unused-iam-roles-easily-and-remove-them-confidently-by-using-the-last-used-timestamp/', + recommended_action: 'Delete IAM roles that have not been used within the expected time frame.', + apis: ['IAM:listRoles', 'IAM:getRole'], + settings: { + iam_role_last_used_fail: { + name: 'IAM Role Last Used Fail', + description: 'Return a failing result when IAM roles exceed this number of days without being used', + regex: '^[1-9]{1}[0-9]{0,3}$', + default: 180 + }, + iam_role_last_used_warn: { + name: 'IAM Role Last Used Warn', + description: 'Return a warning result when IAM roles exceed this number of days without being used', + regex: '^[1-9]{1}[0-9]{0,3}$', + default: 90 + } + }, + + run: function(cache, settings, callback) { + var config = { + iam_role_last_used_fail: settings.iam_role_last_used_fail || this.settings.iam_role_last_used_fail.default, + iam_role_last_used_warn: settings.iam_role_last_used_warn || this.settings.iam_role_last_used_warn.default + }; + + var custom = helpers.isCustom(settings, this.settings); + + var results = []; + var source = {}; + + var region = helpers.defaultRegion(settings); + + var listRoles = helpers.addSource(cache, source, + ['iam', 'listRoles', region]); + + if (!listRoles) return callback(null, results, source); + + if (listRoles.err || !listRoles.data) { + helpers.addResult(results, 3, + 'Unable to query for IAM roles: ' + helpers.addError(listRoles)); + return callback(null, results, source); + } + + if (!listRoles.data.length) { + helpers.addResult(results, 0, 'No IAM roles found'); + return callback(null, results, source); + } + + async.each(listRoles.data, function(role, cb){ + if (!role.RoleName) return cb(); + + // Get role details + var getRole = helpers.addSource(cache, source, + ['iam', 'getRole', region, role.RoleName]); + + if (!getRole || getRole.err || !getRole.data) { + helpers.addResult(results, 3, + 'Unable to query for IAM role details: ' + role.RoleName + ': ' + helpers.addError(getRole), 'global', role.Arn); + return cb(); + } + + if (!getRole.data.Role || !getRole.data.Role.RoleLastUsed || + !getRole.data.Role.RoleLastUsed.LastUsedDate) { + helpers.addResult(results, 2, + 'IAM role: ' + role.RoleName + ' has not been used', 'global', role.Arn); + return cb(); + } + + var daysAgo = helpers.daysAgo(getRole.data.Role.RoleLastUsed.LastUsedDate); + + var returnCode = 0; + var returnMsg = `IAM role was last used ${daysAgo} days ago in the ${getRole.data.Role.RoleLastUsed.Region || 'unknown'} region`; + if (daysAgo > config.iam_role_last_used_fail) { + returnCode = 2; + } else if (daysAgo > config.iam_role_last_used_warn) { + returnCode = 1; + } + + helpers.addResult(results, returnCode, returnMsg, 'global', role.Arn, custom); + + cb(); + }, function(){ + callback(null, results, source); + }); + } +}; \ No newline at end of file diff --git a/plugins/aws/iam/iamRoleLastUsed.spec.js b/plugins/aws/iam/iamRoleLastUsed.spec.js new file mode 100644 index 0000000000..d78eab6814 --- /dev/null +++ b/plugins/aws/iam/iamRoleLastUsed.spec.js @@ -0,0 +1,119 @@ +var assert = require('assert'); +var expect = require('chai').expect; +var iamRoleLastUsed = require('./iamRoleLastUsed'); + +const cache = { + "iam": { + "listRoles": { + "us-east-1": { + "data": [ + { + "Path": "/", + "RoleName": "SampleRole1", + "RoleId": "ABCDEFG", + "Arn": "arn:aws:iam::01234567819101:role/SampleRole1", + "CreateDate": "2019-11-19T14:52:01.000Z", + "AssumeRolePolicyDocument": "" + }, + { + "Path": "/", + "RoleName": "SampleRole2", + "RoleId": "ABCDEFG", + "Arn": "arn:aws:iam::01234567819101:role/SampleRole2", + "CreateDate": "2019-11-19T14:52:01.000Z", + "AssumeRolePolicyDocument": "" + }, + { + "Path": "/", + "RoleName": "SampleRole3", + "RoleId": "ABCDEFG", + "Arn": "arn:aws:iam::01234567819101:role/SampleRole3", + "CreateDate": "2019-11-19T14:52:01.000Z", + "AssumeRolePolicyDocument": "" + } + ] + } + }, + "getRole": { + "us-east-1": { + "SampleRole1": { + "data": { + "Role": { + "Path": "/", + "RoleName": "SampleRole1", + "RoleId": "ABCDEFG", + "Arn": "arn:aws:iam::01234567819101:role/SampleRole1", + "CreateDate": "2019-11-19T14:52:01.000Z", + "AssumeRolePolicyDocument": "", + "RoleLastUsed": {} + } + } + }, + "SampleRole2": { + "data": { + "Role": { + "Path": "/", + "RoleName": "SampleRole2", + "RoleId": "ABCDEFG", + "Arn": "arn:aws:iam::01234567819101:role/SampleRole2", + "CreateDate": "2019-11-19T14:52:01.000Z", + "AssumeRolePolicyDocument": "", + "RoleLastUsed": { + "LastUsedDate": new Date(), + "Region": "us-east-1" + } + } + } + }, + "SampleRole3": { + "data": { + "Role": { + "Path": "/", + "RoleName": "SampleRole3", + "RoleId": "ABCDEFG", + "Arn": "arn:aws:iam::01234567819101:role/SampleRole3", + "CreateDate": "2019-11-19T14:52:01.000Z", + "AssumeRolePolicyDocument": "", + "RoleLastUsed": { + "LastUsedDate": "2019-05-18T14:42:29.000Z", + "Region": "us-east-1" + } + } + } + }, + } + } + } +} + + +describe('iamRoleLastUsed', function() { + describe('run', function() { + it('should FAIL when no last used date present', function(done) { + const callback = (err, results) => { + expect(results[0].status).to.equal(2) + done() + } + + iamRoleLastUsed.run(cache, {}, callback) + }) + + it('should PASS when last used date is recent', function(done) { + const callback = (err, results) => { + expect(results[1].status).to.equal(0) + done() + } + + iamRoleLastUsed.run(cache, {}, callback) + }) + + it('should FAIL when last used date is old', function(done) { + const callback = (err, results) => { + expect(results[2].status).to.equal(2) + done() + } + + iamRoleLastUsed.run(cache, {}, callback) + }) + }) +}) diff --git a/plugins/aws/iam/minPasswordLength.js b/plugins/aws/iam/minPasswordLength.js index ca23cb9da7..96d8bdbb3d 100644 --- a/plugins/aws/iam/minPasswordLength.js +++ b/plugins/aws/iam/minPasswordLength.js @@ -15,7 +15,8 @@ module.exports = { permissions: {remediate: ['iam:UpdateAccountPasswordPolicy'], rollback: ['iam:UpdateAccountPasswordPolicy']}, compliance: { pci: 'PCI requires that passwords have a minimum length of at least 7 characters. ' + - 'Setting an IAM password length policy enforces this requirement.' + 'Setting an IAM password length policy enforces this requirement.', + cis1: '1.9 Ensure IAM password policy requires minimum length of 14 or greater' }, settings: { min_password_length_fail: { diff --git a/plugins/aws/iam/noUserIamPolicies.js b/plugins/aws/iam/noUserIamPolicies.js index e6b20c1fc2..eba2abb47a 100644 --- a/plugins/aws/iam/noUserIamPolicies.js +++ b/plugins/aws/iam/noUserIamPolicies.js @@ -9,6 +9,9 @@ module.exports = { link: 'http://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#use-groups-for-permissions', recommended_action: 'Create groups with the required policies, move the IAM users to the applicable groups, and then remove the inline and directly attached policies from the IAM user.', apis: ['IAM:listUsers', 'IAM:listUserPolicies', 'IAM:listAttachedUserPolicies'], + compliance: { + cis1: '1.16 Ensure IAM policies are attached only to groups or roles' + }, run: function(cache, settings, callback) { var results = []; diff --git a/plugins/aws/iam/passwordExpiration.js b/plugins/aws/iam/passwordExpiration.js index caed94e4c4..39e620b678 100644 --- a/plugins/aws/iam/passwordExpiration.js +++ b/plugins/aws/iam/passwordExpiration.js @@ -15,7 +15,8 @@ module.exports = { permissions: {remediate: ['iam:UpdateAccountPasswordPolicy'], rollback: ['iam:UpdateAccountPasswordPolicy']}, compliance: { pci: 'PCI requires that user passwords are rotated every 90 days. Forcing ' + - 'password expirations enforces this policy.' + 'password expirations enforces this policy.', + cis1: '1.11 Ensure IAM password policy expires passwords within 90 days or less' }, run: function(cache, settings, callback) { diff --git a/plugins/aws/iam/passwordRequiresLowercase.js b/plugins/aws/iam/passwordRequiresLowercase.js index 18b54a0e16..eadea430f7 100644 --- a/plugins/aws/iam/passwordRequiresLowercase.js +++ b/plugins/aws/iam/passwordRequiresLowercase.js @@ -15,7 +15,8 @@ module.exports = { permissions: {remediate: ['iam:UpdateAccountPasswordPolicy'], rollback: ['iam:UpdateAccountPasswordPolicy']}, compliance: { pci: 'PCI requires a strong password policy. Setting IAM password ' + - 'requirements enforces this policy.' + 'requirements enforces this policy.', + cis1: '1.6 Ensure IAM password policy require at least one lowercase letter' }, run: function(cache, settings, callback) { diff --git a/plugins/aws/iam/passwordRequiresNumbers.js b/plugins/aws/iam/passwordRequiresNumbers.js index aa82ec1ad3..d0384f3cf7 100644 --- a/plugins/aws/iam/passwordRequiresNumbers.js +++ b/plugins/aws/iam/passwordRequiresNumbers.js @@ -15,7 +15,8 @@ module.exports = { permissions: {remediate: ['iam:UpdateAccountPasswordPolicy'], rollback: ['iam:UpdateAccountPasswordPolicy']}, compliance: { pci: 'PCI requires a strong password policy. Setting IAM password ' + - 'requirements enforces this policy.' + 'requirements enforces this policy.', + cis1: '1.8 Ensure IAM password policy require at least one number' }, run: function(cache, settings, callback) { diff --git a/plugins/aws/iam/passwordRequiresSymbols.js b/plugins/aws/iam/passwordRequiresSymbols.js index 49e18874a7..6969c6c14b 100644 --- a/plugins/aws/iam/passwordRequiresSymbols.js +++ b/plugins/aws/iam/passwordRequiresSymbols.js @@ -15,7 +15,8 @@ module.exports = { permissions: {remediate: ['iam:UpdateAccountPasswordPolicy'], rollback: ['iam:UpdateAccountPasswordPolicy']}, compliance: { pci: 'PCI requires a strong password policy. Setting IAM password ' + - 'requirements enforces this policy.' + 'requirements enforces this policy.', + cis1: '1.7 Ensure IAM password policy require at least one symbol' }, run: function(cache, settings, callback) { diff --git a/plugins/aws/iam/passwordRequiresUppercase.js b/plugins/aws/iam/passwordRequiresUppercase.js index dbc1dd83fd..32c126d2a4 100644 --- a/plugins/aws/iam/passwordRequiresUppercase.js +++ b/plugins/aws/iam/passwordRequiresUppercase.js @@ -15,7 +15,8 @@ module.exports = { permissions: {remediate: ['iam:UpdateAccountPasswordPolicy'], rollback: ['iam:UpdateAccountPasswordPolicy']}, compliance: { pci: 'PCI requires a strong password policy. Setting IAM password ' + - 'requirements enforces this policy.' + 'requirements enforces this policy.', + cis1: '1.5 Ensure IAM password policy requires at least one uppercase letter' }, run: function(cache, settings, callback) { diff --git a/plugins/aws/iam/passwordReusePrevention.js b/plugins/aws/iam/passwordReusePrevention.js index ef30fba41f..0a086b067d 100644 --- a/plugins/aws/iam/passwordReusePrevention.js +++ b/plugins/aws/iam/passwordReusePrevention.js @@ -15,7 +15,8 @@ module.exports = { permissions: {remediate: ['iam:UpdateAccountPasswordPolicy'], rollback: ['iam:UpdateAccountPasswordPolicy']}, compliance: { pci: 'PCI requires that the previous 4 passwords not be reused. ' + - 'Restricting IAM password reuse enforces this policy.' + 'Restricting IAM password reuse enforces this policy.', + cis1: '1.10 Ensure IAM password policy prevents password reuse' }, settings: { password_reuse_fail: { diff --git a/plugins/aws/iam/rootAccessKeys.js b/plugins/aws/iam/rootAccessKeys.js index 79541e597d..100915ccf5 100644 --- a/plugins/aws/iam/rootAccessKeys.js +++ b/plugins/aws/iam/rootAccessKeys.js @@ -12,7 +12,8 @@ module.exports = { hipaa: 'HIPAA requires strong auditing controls surrounding actions ' + 'taken in the environment. The root user lacks these controls ' + 'since it is not tied to a specific user. The root access keys ' + - 'should not be used.' + 'should not be used.', + cis1: '1.12 Ensure no root account access key exists' }, run: function(cache, settings, callback) { diff --git a/plugins/aws/iam/rootAccountInUse.js b/plugins/aws/iam/rootAccountInUse.js index 0e32fc19b8..7e99a0653d 100644 --- a/plugins/aws/iam/rootAccountInUse.js +++ b/plugins/aws/iam/rootAccountInUse.js @@ -15,7 +15,8 @@ module.exports = { 'should not be used.', pci: 'PCI requires that cardholder data can only be accessed by those with ' + 'a legitimate business need. Restricting root access prevents access ' + - 'to these environments from users who may not be identified.' + 'to these environments from users who may not be identified.', + cis1: '1.1 Avoid the use of the root account' }, settings: { root_account_in_use_days: { diff --git a/plugins/aws/iam/rootMfaEnabled.js b/plugins/aws/iam/rootMfaEnabled.js index 2399450167..572d5f0031 100644 --- a/plugins/aws/iam/rootMfaEnabled.js +++ b/plugins/aws/iam/rootMfaEnabled.js @@ -11,7 +11,8 @@ module.exports = { compliance: { pci: 'PCI requires MFA for all access to cardholder environments. ' + 'Create an MFA key for the root account and then lock it in ' + - 'a safe location for use as backup for named IAM users.' + 'a safe location for use as backup for named IAM users.', + cis1: '1.13 Ensure MFA is enabled for the "root" account' }, run: function(cache, settings, callback) { diff --git a/plugins/aws/iam/rootSigningCertificate.js b/plugins/aws/iam/rootSigningCertificate.js new file mode 100644 index 0000000000..a9a5cc917c --- /dev/null +++ b/plugins/aws/iam/rootSigningCertificate.js @@ -0,0 +1,60 @@ +var helpers = require('../../../helpers/aws'); + +module.exports = { + title: 'Root Account Active Signing Certificates', + category: 'IAM', + description: 'Ensures the root user is not using x509 singing certificates', + more_info: 'AWS supports using x509 signing certificates for API access, but these should not be attached to the root user, which has full access to the account.', + link: 'https://docs.aws.amazon.com/whitepapers/latest/aws-overview-security-processes/x.509-certificates.html', + recommended_action: 'Delete the x509 certificates associated with the root account.', + apis: ['IAM:generateCredentialReport'], + compliance: { + hipaa: 'HIPAA requires strong auditing controls surrounding actions ' + + 'taken in the environment. The root user lacks these controls ' + + 'since it is not tied to a specific user. The root signing keys ' + + 'should not be used.' + }, + + run: function(cache, settings, callback) { + var results = []; + var source = {}; + + var region = helpers.defaultRegion(settings); + + var generateCredentialReport = helpers.addSource(cache, source, + ['iam', 'generateCredentialReport', region]); + + if (!generateCredentialReport) return callback(null, results, source); + + if (generateCredentialReport.err || !generateCredentialReport.data) { + helpers.addResult(results, 3, + 'Unable to query for root user: ' + helpers.addError(generateCredentialReport)); + return callback(null, results, source); + } + + var found = false; + for (var r in generateCredentialReport.data) { + var obj = generateCredentialReport.data[r]; + const resource = obj.arn; + + if (obj && obj.user && obj.user === '') { + found = true; + + if (obj.cert_1_active || + obj.cert_2_active) { + helpers.addResult(results, 2, 'The root user uses x509 singing certificates.', 'global', resource); + } else { + helpers.addResult(results, 0, 'The root user does not use x509 singing certificates.', 'global', resource); + } + + break; + } + } + + if (!found) { + helpers.addResult(results, 3, 'Unable to query for root user'); + } + + callback(null, results, source); + } +}; diff --git a/plugins/aws/iam/rootSigningCertificate.spec.js b/plugins/aws/iam/rootSigningCertificate.spec.js new file mode 100644 index 0000000000..a05ffa0118 --- /dev/null +++ b/plugins/aws/iam/rootSigningCertificate.spec.js @@ -0,0 +1,162 @@ +var expect = require('chai').expect; +const rootSigningCertificate = require('./rootSigningCertificate'); + +const credentialReports = [ + { + user: '', + arn: 'arn:aws:iam::111122223333:root', + user_creation_time: '2020-08-09T16:55:28+00:00', + password_enabled: 'not_supported', + password_last_used: '2020-08-17T21:13:44+00:00', + password_last_changed: 'not_supported', + password_next_rotation: 'not_supported', + mfa_active: false, + access_key_1_active: true, + access_key_1_last_rotated: '2020-08-17T21:15:23+00:00', + access_key_1_last_used_date: null, + access_key_1_last_used_region: null, + access_key_1_last_used_service: null, + access_key_2_active: false, + access_key_2_last_rotated: null, + access_key_2_last_used_date: null, + access_key_2_last_used_region: null, + access_key_2_last_used_service: null, + cert_1_active: false, + cert_1_last_rotated: null, + cert_2_active: false, + cert_2_last_rotated: null + }, + { + user: '', + arn: 'arn:aws:iam::111122223333:user/cloudsploit', + user_creation_time: '2020-08-17T09:07:27+00:00', + password_enabled: true, + password_last_used: 'no_information', + password_last_changed: '2020-08-17T09:07:29+00:00', + password_next_rotation: null, + mfa_active: false, + access_key_1_active: true, + access_key_1_last_rotated: '2020-08-17T09:07:29+00:00', + access_key_1_last_used_date: null, + access_key_1_last_used_region: null, + access_key_1_last_used_service: null, + access_key_2_active: false, + access_key_2_last_rotated: null, + access_key_2_last_used_date: null, + access_key_2_last_used_region: null, + access_key_2_last_used_service: null, + cert_1_active: true, + cert_1_last_rotated: null, + cert_2_active: false, + cert_2_last_rotated: null + }, + { + user: 'codesploit', + arn: 'arn:aws:iam::111122223333:user/cloudsploit', + user_creation_time: '2020-08-17T09:07:27+00:00', + password_enabled: true, + password_last_used: 'no_information', + password_last_changed: '2020-08-17T09:07:29+00:00', + password_next_rotation: null, + mfa_active: false, + access_key_1_active: true, + access_key_1_last_rotated: '2020-08-17T09:07:29+00:00', + access_key_1_last_used_date: null, + access_key_1_last_used_region: null, + access_key_1_last_used_service: null, + access_key_2_active: false, + access_key_2_last_rotated: null, + access_key_2_last_used_date: null, + access_key_2_last_used_region: null, + access_key_2_last_used_service: null, + cert_1_active: true, + cert_1_last_rotated: null, + cert_2_active: false, + cert_2_last_rotated: null + } +] + +const createCache = (credentialReports) => { + return { + iam: { + generateCredentialReport: { + 'us-east-1': { + data: credentialReports + }, + }, + }, + }; +}; + +const createErrorCache = () => { + return { + iam: { + generateCredentialReport: { + 'us-east-1': { + err: { + message: 'error describing cloudformation stacks' + }, + }, + }, + }, + }; +}; + +const createNullCache = () => { + return { + iam: { + generateCredentialReport: { + 'us-east-1': null, + }, + }, + }; +}; + +describe('rootSigningCertificate', function () { + describe('run', function () { + + it('should PASS if the root user is not using x509 singing certificates', function (done) { + const cache = createCache([credentialReports[0]]); + rootSigningCertificate.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + done(); + }); + }); + + it('should FAIL if the root user is using x509 singing certificates', function (done) { + const cache = createCache([credentialReports[1]]); + rootSigningCertificate.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(2); + done(); + }); + }); + + it('should UNKNOWN if the root user is not found', function (done) { + const cache = createCache([credentialReports[2]]); + rootSigningCertificate.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(3); + done(); + }); + }); + + it('should not return any results if unable to fetch credential reports', function (done) { + const cache = createNullCache(); + rootSigningCertificate.run(cache, {}, (err, results) => { + expect(results.length).to.equal(0); + done(); + }); + }); + + it('should UNKNOWN if error occurs while fetching credential reports', function (done) { + const cache = createErrorCache(); + rootSigningCertificate.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(3); + done(); + }); + }); + }); +}); \ No newline at end of file diff --git a/plugins/aws/iam/usersMfaEnabled.js b/plugins/aws/iam/usersMfaEnabled.js index be0c3b6a5e..1e9df89f8f 100644 --- a/plugins/aws/iam/usersMfaEnabled.js +++ b/plugins/aws/iam/usersMfaEnabled.js @@ -14,7 +14,9 @@ module.exports = { 'strong controls around entity authentication which can be ' + 'enhanced through the use of MFA.', pci: 'PCI requires MFA for all access to cardholder environments. ' + - 'Create an MFA key for user accounts.' + 'Create an MFA key for user accounts.', + cis1: '1.2 Ensure multi-factor authentication (MFA) is enabled for all ' + + 'IAM users that have a console password' }, run: function(cache, settings, callback) { diff --git a/plugins/aws/iam/usersPasswordLastUsed.js b/plugins/aws/iam/usersPasswordLastUsed.js index ff9d4a8ee8..40dbd14ecb 100644 --- a/plugins/aws/iam/usersPasswordLastUsed.js +++ b/plugins/aws/iam/usersPasswordLastUsed.js @@ -14,7 +14,8 @@ module.exports = { compliance: { pci: 'PCI requires that all user credentials are rotated every 90 days. ' + 'If the user password has not been used in the last 90 days, the ' + - 'user should be deactivated.' + 'user should be deactivated.', + cis1: '1.3 Ensure credentials unused for 90 days or greater are disabled' }, settings: { users_password_last_used_fail: { diff --git a/plugins/aws/kms/kmsKeyRotation.js b/plugins/aws/kms/kmsKeyRotation.js index 2ef8dcc952..8601b377d2 100644 --- a/plugins/aws/kms/kmsKeyRotation.js +++ b/plugins/aws/kms/kmsKeyRotation.js @@ -13,7 +13,8 @@ module.exports = { pci: 'PCI has strict requirements regarding the use of encryption keys ' + 'to protect cardholder data. These requirements include rotating ' + 'the key periodically. KMS provides key rotation capabilities that ' + - 'should be enabled.' + 'should be enabled.', + cis2: '2.8 Ensure rotation for customer created CMKs is enabled' }, run: function(cache, settings, callback) { diff --git a/plugins/aws/rds/sqlServerTLSVersion.js b/plugins/aws/rds/sqlServerTLSVersion.js new file mode 100644 index 0000000000..43be030839 --- /dev/null +++ b/plugins/aws/rds/sqlServerTLSVersion.js @@ -0,0 +1,104 @@ +var async = require('async'); +var helpers = require('../../../helpers/aws'); + +module.exports = { + title: 'SQL Server TLS Version', + category: 'RDS', + description: 'Ensures RDS SQL Servers do not allow outdated TLS certificate versions', + more_info: 'TLS 1.2 or higher should be used for all TLS connections to RDS. A parameter group can be used to enforce this connection type.', + link: 'https://aws.amazon.com/about-aws/whats-new/2020/07/amazon-rds-for-sql-server-supports-disabling-old-versions-of-tls-and-ciphers/', + recommended_action: 'Create a parameter group that contains the TLS version restriction and limit access to TLS 1.2 or higher', + apis: ['RDS:describeDBParameterGroups', 'RDS:describeDBParameters'], + + run: function(cache, settings, callback) { + var results = []; + var source = {}; + var regions = helpers.regions(settings); + async.each(regions.rds, function(region, rcb){ + var describeDBParameterGroups = helpers.addSource(cache, source, + ['rds', 'describeDBParameterGroups', region]); + + if (!describeDBParameterGroups) return rcb(); + + if (describeDBParameterGroups.err || !describeDBParameterGroups.data) { + helpers.addResult(results, 3, + 'Unable to query for parameter groups: ' + helpers.addError(describeDBParameterGroups)); + return rcb(); + } + + if (!describeDBParameterGroups.data.length) { + helpers.addResult(results, 0, 'No parameter groups found'); + return rcb(); + } + + async.each(describeDBParameterGroups.data, function(group, paramcb){ + if (!group.DBParameterGroupName) return paramcb(); + + var resource = group.DBParameterGroupArn; + + if(group.DBParameterGroupFamily && !group.DBParameterGroupFamily.startsWith('sqlserver')) return paramcb(); + + var parameters = helpers.addSource(cache, source, + ['rds', 'describeDBParameters', region, group.DBParameterGroupName]); + + if (!parameters) return paramcb(); + + if (parameters.err || !parameters.data){ + helpers.addResult(results, 3, + 'Unable to query for parameters: ' + helpers.addError(parameters), + region, resource); + return paramcb(); + } + + if (!parameters.data.Parameters || !parameters.data.Parameters.length) { + helpers.addResult(results, 0, 'No parameters found', region, resource); + return paramcb(); + } + + var tls10 = null; + var tls11 = null; + var tls12 = null; + + for (var param in parameters.data.Parameters) { + if (parameters.data.Parameters[param] && + parameters.data.Parameters[param].ParameterName && + parameters.data.Parameters[param].ParameterName === 'rds.tls10') { + + tls10 = parameters.data.Parameters[param].ParameterValue; + } + else if (parameters.data.Parameters[param] && + parameters.data.Parameters[param].ParameterName && + parameters.data.Parameters[param].ParameterName === 'rds.tls11') { + + tls11 = parameters.data.Parameters[param].ParameterValue; + } + else if (parameters.data.Parameters[param] && + parameters.data.Parameters[param].ParameterName && + parameters.data.Parameters[param].ParameterName === 'rds.tls12') { + + tls12 = parameters.data.Parameters[param].ParameterValue; + } + if (!tls10 || !tls11 || !tls12) continue; + } + + if (tls10 === 'disabled' && tls11 === 'disabled' && tls12 != 'disabled') { + helpers.addResult(results, 0, + 'DB parameter group ' + (group.DBParameterGroupName) + ' uses TLS 1.2', + region, resource); + } + else { + helpers.addResult(results, 2, + 'DB parameter group ' + (group.DBParameterGroupName) + ' does not use TLS 1.2', + region, resource); + } + + paramcb(); + }); + + rcb(); + + }, function(){ + callback(null, results, source); + }); + } +}; diff --git a/plugins/aws/rds/sqlServerTLSVersion.spec.js b/plugins/aws/rds/sqlServerTLSVersion.spec.js new file mode 100644 index 0000000000..3fdd9f2a6e --- /dev/null +++ b/plugins/aws/rds/sqlServerTLSVersion.spec.js @@ -0,0 +1,201 @@ +var expect = require('chai').expect; +var sqlServerTLSVersion = require('./sqlServerTLSVersion.js'); + +const parameterGroups = [ + { + "DBParameterGroupName": "default.sqlserver-ex-14.0", + "DBParameterGroupFamily": "sqlserver-ex-14.0", + "Description": "Default parameter group for sqlserver-ex-14.0", + "DBParameterGroupArn": "arn:aws:rds:us-east-1:23424531345:pg:default.sqlserver-ex-14.0" + }, + { + "DBParameterGroupName": "ex-g", + "DBParameterGroupFamily": "sqlserver-ex-14.0", + "Description": "abv", + "DBParameterGroupArn": "arn:aws:rds:us-east-1:23424531345:pg:ex-g" + } +]; + +const groupParameters = [ + [ + { + ParameterName: 'rds.tls10', + ParameterValue: 'disabled', + Description: 'TLS 1.0.', + Source: 'user', + ApplyType: 'static', + DataType: 'string', + AllowedValues: 'default, enabled, disabled', + IsModifiable: true, + ApplyMethod: 'pending-reboot', + SupportedEngineModes: [] + }, + { + ParameterName: 'rds.tls11', + ParameterValue: 'default', + Description: 'TLS 1.1.', + Source: 'user', + ApplyType: 'static', + DataType: 'string', + AllowedValues: 'default, enabled, disabled', + IsModifiable: true, + ApplyMethod: 'pending-reboot', + SupportedEngineModes: [] + }, + { + ParameterName: 'rds.tls12', + ParameterValue: 'default', + Description: 'TLS 1.2.', + Source: 'system', + ApplyType: 'static', + DataType: 'string', + AllowedValues: 'default, enabled, disabled', + IsModifiable: false, + ApplyMethod: 'pending-reboot', + SupportedEngineModes: [] + } + ], + [ + { + ParameterName: 'rds.tls10', + ParameterValue: 'disabled', + Description: 'TLS 1.0.', + Source: 'user', + ApplyType: 'static', + DataType: 'string', + AllowedValues: 'default, enabled, disabled', + IsModifiable: true, + ApplyMethod: 'pending-reboot', + SupportedEngineModes: [] + }, + { + ParameterName: 'rds.tls11', + ParameterValue: 'disabled', + Description: 'TLS 1.1.', + Source: 'user', + ApplyType: 'static', + DataType: 'string', + AllowedValues: 'default, enabled, disabled', + IsModifiable: true, + ApplyMethod: 'pending-reboot', + SupportedEngineModes: [] + }, + { + ParameterName: 'rds.tls12', + ParameterValue: 'default', + Description: 'TLS 1.2.', + Source: 'system', + ApplyType: 'static', + DataType: 'string', + AllowedValues: 'default, enabled, disabled', + IsModifiable: false, + ApplyMethod: 'pending-reboot', + SupportedEngineModes: [] + } + ] +]; + +const createCache = (parameterGroups, groupParameters) => { + if (parameterGroups.length) var dbParameterGroupName = parameterGroups[0]['DBParameterGroupName']; + return { + rds: { + describeDBParameterGroups: { + 'us-east-1': { + data: parameterGroups + }, + }, + describeDBParameters: { + 'us-east-1': { + [dbParameterGroupName]: { + data: { + Parameters: groupParameters + } + } + }, + }, + }, + }; +}; + + +const createErrorCache = () => { + return { + rds: { + describeDBParameterGroups: { + 'us-east-1': { + err: { + message: 'error describing parameter groups' + }, + }, + }, + }, + }; +}; + +const createNullCache = () => { + return { + rds: { + describeDBParameterGroups: { + 'us-east-1': null, + }, + }, + }; +}; + +describe('sqlServerTLSVersion', function () { + describe('run', function () { + + it('should PASS if unable to get parameter groups', function (done) { + const cache = createCache([]); + sqlServerTLSVersion.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + done(); + }); + }); + + it('should PASS if unable to get group parameters', function (done) { + const cache = createCache([parameterGroups[0]],[]); + sqlServerTLSVersion.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + done(); + }); + }); + + it('should FAIL if parameter group does not use TLS version 1.2', function (done) { + const cache = createCache([parameterGroups[0]], groupParameters[0]); + sqlServerTLSVersion.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(2); + done(); + }); + }); + + it('should PASS if parameter group uses TLS version 1.2', function (done) { + const cache = createCache([parameterGroups[1]], groupParameters[1]); + sqlServerTLSVersion.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + done(); + }); + }); + + it('should not return any results if unable to get parameter groups', function (done) { + const cache = createNullCache(); + sqlServerTLSVersion.run(cache, {}, (err, results) => { + expect(results.length).to.equal(0); + done(); + }); + }); + + it('should UNKNOWN if error occurs while fetching parameter groups', function (done) { + const cache = createErrorCache(); + sqlServerTLSVersion.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(3); + done(); + }); + }); + }); +}); \ No newline at end of file diff --git a/plugins/google/vpcnetwork/openRDP.js b/plugins/google/vpcnetwork/openRDP.js index 90378da61b..c4038fccbb 100644 --- a/plugins/google/vpcnetwork/openRDP.js +++ b/plugins/google/vpcnetwork/openRDP.js @@ -7,7 +7,7 @@ module.exports = { description: 'Determines if TCP port 3389 for RDP is open to the public', more_info: "While some ports such as HTTP and HTTPS are required to be open to the public to function properly, more sensitive services such as RDP should be restricted to known IP addresses.", link: 'https://cloud.google.com/vpc/docs/using-firewalls', - recommended_action: 'Restrict TCP port 5432 to known IP addresses.', + recommended_action: 'Restrict TCP port 3389 to known IP addresses.', apis: ['firewalls:list'], run: function(cache, settings, callback) { @@ -45,4 +45,4 @@ module.exports = { callback(null, results, source); }); } -} \ No newline at end of file +} diff --git a/postprocess/output.js b/postprocess/output.js index 9652355644..3be8bb7a33 100644 --- a/postprocess/output.js +++ b/postprocess/output.js @@ -1,44 +1,77 @@ var csvWriter = require('csv-write-stream'); var fs = require('fs'); +var ttytable = require('tty-table'); + +function exchangeStatusWord(result) { + if (result.status === 0) return 'OK'; + if (result.status === 1) return 'WARN'; + if (result.status === 2) return 'FAIL'; + return 'UNKNOWN'; +} + +function commaSafe(str) { + if (!str) return ''; + return str.replace(/,/g, ' '); +} + +function log(msg, settings) { + if (!settings.mocha) console.log(msg); +} // For the console output, we don't need any state since we can write // directly to the console. +var tableHeaders = []; +var tableRows = []; + var consoleOutputHandler = { - startCompliance: function(plugin, pluginKey, compliance) { - var complianceDesc = compliance.describe(pluginKey, plugin); - if (complianceDesc) { - console.log(''); - console.log('-----------------------'); - console.log(plugin.title); - console.log('-----------------------'); - console.log(complianceDesc); - console.log(''); - } - }, + writeResult: function(result, plugin, pluginKey, complianceMsg) { + var toWrite = { + Category: plugin.category, + Plugin: plugin.title, + Description: plugin.description, + Resource: (result.resource || 'N/A'), + Region: (result.region || 'global'), + Status: exchangeStatusWord(result), + Message: result.message || 'N/A' + }; - endCompliance: function() { - // For console output, we don't do anything + if (complianceMsg) { + if (tableHeaders.length !== 8) { + tableHeaders.push({ + value: 'Compliance' + }); + } + toWrite.Compliance = complianceMsg; + } + + tableRows.push(toWrite); }, - writeResult: function(result, plugin) { - var statusWord; - if (result.status === 0) { - statusWord = 'OK'; - } else if (result.status === 1) { - statusWord = 'WARN'; - } else if (result.status === 2) { - statusWord = 'FAIL'; + close: function(settings) { + // For console output, print the table + if (settings.console == 'none') { + console.log('INFO: Console output suppressed because "console" setting was "none"'); + } else if (settings.console == 'text') { + tableRows.forEach(function(row){ + Object.entries(row).forEach(function(entry){ + console.log(`${entry[0]}: ${entry[1]}`); + }); + console.log('\n'); + }); } else { - statusWord = 'UNKNOWN'; + const t1 = ttytable(tableHeaders, tableRows, null, { + borderStyle: 'solid', + borderColor: 'white', + paddingBottom: 0, + headerAlign: 'center', + headerColor: 'white', + align: 'left', + color: 'white', + width: '100%' + }).render(); + if (process.argv.join('').indexOf('mocha') === -1) console.log(t1); } - - console.log(plugin.category + '\t' + plugin.title + '\t' + - (result.resource || 'N/A') + '\t' + - (result.region || 'Global') + '\t\t' + - statusWord + '\t' + result.message); - }, - - close: function() {} + } }; module.exports = { @@ -46,41 +79,32 @@ module.exports = { * Creates an output handler that writes output in the CSV format. * @param {fs.WriteSteam} stream The stream to write to or an object that * obeys the writeable stream contract. + * @param {Object} settings The source settings object */ - createCsv: function(stream) { - var writer = csvWriter({headers: ['category', 'title', 'resource', - 'region', 'statusWord', 'message']}); + createCsv: function(stream, settings) { + var headers = ['category', 'title', 'description', + 'resource', 'region', 'statusWord', 'message']; + if (settings.compliance) headers.push('compliance'); + var writer = csvWriter({headers: headers}); writer.pipe(stream); return { writer: writer, - startCompliance: function() { - }, - - endCompliance: function() { - }, - - writeResult: function(result, plugin) { - var statusWord; - if (result.status === 0) { - statusWord = 'OK'; - } else if (result.status === 1) { - statusWord = 'WARN'; - } else if (result.status === 2) { - statusWord = 'FAIL'; - } else { - statusWord = 'UNKNOWN'; - } - - this.writer.write([plugin.category, plugin.title, + writeResult: function(result, plugin, pluginKey, complianceMsg) { + var toWrite = [plugin.category, plugin.title, commaSafe(plugin.description), (result.resource || 'N/A'), (result.region || 'Global'), - statusWord, result.message]); + exchangeStatusWord(result), commaSafe(result.message)]; + + if (settings.compliance) toWrite.push(complianceMsg || ''); + + this.writer.write(toWrite); }, close: function() { this.writer.end(); + log(`INFO: CSV file written to ${settings.csv}`, settings); } }; }, @@ -90,43 +114,31 @@ module.exports = { * @param {fs.WriteSteam} stream The stream to write to or an object that * obeys the writeable stream contract. */ - createJson: function(stream) { + createJson: function(stream, settings) { var results = []; return { stream: stream, - - startCompliance: function() { - }, - - endCompliance: function() { - }, - writeResult: function(result, plugin, pluginKey) { - var statusWord; - if (result.status === 0) { - statusWord = 'OK'; - } else if (result.status === 1) { - statusWord = 'WARN'; - } else if (result.status === 2) { - statusWord = 'FAIL'; - } else { - statusWord = 'UNKNOWN'; - } - - results.push({ + writeResult: function(result, plugin, pluginKey, complianceMsg) { + var toWrite = { plugin: pluginKey, category: plugin.category, title: plugin.title, + description: plugin.description, resource: result.resource || 'N/A', region: result.region || 'Global', - status: statusWord, + status: exchangeStatusWord(result), message: result.message - }); + }; + + if (complianceMsg) toWrite.compliance = complianceMsg; + results.push(toWrite); }, close: function() { - this.stream.write(JSON.stringify(results)); + this.stream.write(JSON.stringify(results, null, 2)); this.stream.end(); + log(`INFO: JSON file written to ${settings.json}`, settings); } }; }, @@ -141,7 +153,7 @@ module.exports = { * @param {fs.WriteStream} stream The stream to write to or an object that * obeys the writeable stream contract. */ - createJunit: function(stream) { + createJunit: function(stream, settings) { return { stream: stream, @@ -151,12 +163,6 @@ module.exports = { * we group tests based on the plugin key. */ testSuites: {}, - - startCompliance: function() { - }, - - endCompliance: function() { - }, /** * Adds the result to be written to the output file. @@ -223,6 +229,7 @@ module.exports = { this.stream.write('\n'); this.stream.end(); + log(`INFO: JUnit file written to ${settings.junit}`, settings); }, /** @@ -280,18 +287,19 @@ module.exports = { * @param {fs.WriteSteam} stream The stream to write to or an object that * obeys the writeable stream contract. */ - createCollection: function(stream) { + createCollection: function(stream, settings) { var results = {}; return { stream: stream, - write: function(collection, providerName) { - results[providerName] = collection; + write: function(collection) { + results = collection; }, close: function() { - this.stream.write(JSON.stringify(results)); + this.stream.write(JSON.stringify(results, null, 2)); this.stream.end(); + log(`INFO: Collection file written to ${settings.collection}`, settings); } }; }, @@ -308,47 +316,73 @@ module.exports = { * one output handler or one that forwards function calls to a group of * output handlers. */ - create: function(argv) { + create: function(settings) { var outputs = []; var collectionOutput; + tableHeaders = [ + { + value: 'Category', + width: '10%', + }, + { + value: 'Plugin' + }, + { + value: 'Description' + }, + { + value: 'Resource' + }, + { + value: 'Region' + }, + { + value: 'Status', + width: '10%', + formatter: function(value) { + if (value === 'OK') { + value = this.style(value, 'bgGreen', 'black'); + } else if (value === 'FAIL') { + value = this.style(value, 'bgRed', 'white'); + } else if (value === 'WARN') { + value = this.style(value, 'bgYellow', 'black'); + } else { + value = this.style(value, 'bgGray', 'white'); + } + return value; + } + }, + { + value: 'Message' + } + ]; + + tableRows = []; + // Creates the handlers for writing output. - var addCsvOutput = argv.find(function(arg) { - return arg.startsWith('--csv='); - }); - if (addCsvOutput) { - var stream = fs.createWriteStream(addCsvOutput.substr(6)); - outputs.push(this.createCsv(stream)); + if (settings.csv) { + var stream = fs.createWriteStream(settings.csv); + outputs.push(this.createCsv(stream, settings)); } - var addJunitOutput = argv.find(function(arg) { - return arg.startsWith('--junit='); - }); - if (addJunitOutput) { - var streamJunit = fs.createWriteStream(addJunitOutput.substr(8)); - outputs.push(this.createJunit(streamJunit)); + if (settings.junit) { + var streamJunit = fs.createWriteStream(settings.junit); + outputs.push(this.createJunit(streamJunit, settings)); } - var addJsonOutput = argv.find(function(arg) { - return arg.startsWith('--json='); - }); - if (addJsonOutput) { - var streamJson = fs.createWriteStream(addJsonOutput.substr(7)); - outputs.push(this.createJson(streamJson)); + if (settings.json) { + var streamJson = fs.createWriteStream(settings.json); + outputs.push(this.createJson(streamJson, settings)); } - var addConsoleOutput = argv.find(function(arg) { - return arg.startsWith('--console'); - }); - - var addCollectionOutput = argv.find(function(arg) { - return arg.startsWith('--collection='); - }); - if(addCollectionOutput) { - var streamColl = fs.createWriteStream(addCollectionOutput.substr(13)); - collectionOutput = this.createCollection(streamColl); + if (settings.collection) { + var streamColl = fs.createWriteStream(settings.collection); + collectionOutput = this.createCollection(streamColl, settings); } + var addConsoleOutput = settings.console; + // Write to console if specified or by default if there is not // other output handler specified. if (addConsoleOutput || outputs.length == 0) { @@ -356,48 +390,30 @@ module.exports = { } // Ignore any "OK" results - only report issues - var ignoreOkStatus = argv.find(function(arg) { - return arg.startsWith('--ignore-ok'); - }); + var ignoreOkStatus = settings.ignore_ok; // This creates a multiplexer-like object that forwards the // call onto any output handler that has been defined. This // allows us to simply send the output to multiple handlers // and the caller doesn't need to worry about that part. return { - startCompliance: function(plugin, pluginKey, compliance) { - for (var output of outputs) { - output.startCompliance(plugin, pluginKey, compliance); - } - }, - - endCompliance: function(plugin, pluginKey, compliance) { - for (var output of outputs) { - output.endCompliance(plugin, pluginKey, compliance); - } - }, - - writeResult: function(result, plugin, pluginKey) { - for (var output of outputs) { + writeResult: function(result, plugin, pluginKey, complianceMsg) { + outputs.forEach(function(output) { if (!(ignoreOkStatus && result.status === 0)) { - output.writeResult(result, plugin, pluginKey); + output.writeResult(result, plugin, pluginKey, complianceMsg); } - } + }); }, writeCollection: function(collection, providerName) { - if(collectionOutput) { - collectionOutput.write(collection, providerName); - } + if (collectionOutput) collectionOutput.write(collection, providerName); }, close: function() { - if(collectionOutput) { - collectionOutput.close(); - } - for (var output of outputs) { - output.close(); - } + if (collectionOutput) collectionOutput.close(); + outputs.forEach(function(output) { + output.close(settings); + }); } }; } diff --git a/postprocess/output.spec.js b/postprocess/output.spec.js index 7046fb8230..ccec9c3a02 100644 --- a/postprocess/output.spec.js +++ b/postprocess/output.spec.js @@ -25,7 +25,7 @@ describe('output', function () { describe('junit', function () { it('should generate empty junit when no results', function () { var buffer = createOutputBuffer(); - var handler = output.createJunit(buffer); + var handler = output.createJunit(buffer, {mocha: true, junit: 'test.junit'}); handler.close(); expect(buffer.cache).to.equal( '\n' + @@ -34,7 +34,7 @@ describe('output', function () { it('should indicate one pass there is one passing result', function () { var buffer = createOutputBuffer(); - var handler = output.createJunit(buffer); + var handler = output.createJunit(buffer, { mocha: true, junit: 'test.junit' }); handler.writeResult({status: 0}, {title:'myTitle'}, 'key'); handler.close(); @@ -45,7 +45,7 @@ describe('output', function () { it('should indicate one failure there is one failing result', function () { var buffer = createOutputBuffer(); - var handler = output.createJunit(buffer); + var handler = output.createJunit(buffer, { mocha: true, junit: 'test.junit' }); handler.writeResult({status: 2, message: 'fail message'}, {title:'myTitle'}, 'key'); handler.close(); @@ -57,7 +57,7 @@ describe('output', function () { it('should indicate one error there is one failing error', function () { var buffer = createOutputBuffer(); - var handler = output.createJunit(buffer); + var handler = output.createJunit(buffer, { mocha: true, junit: 'test.junit' }); handler.writeResult({status: 3, message: 'error message'}, {title:'myTitle'}, 'key'); handler.close(); @@ -71,34 +71,34 @@ describe('output', function () { describe('csv', function () { it('should generate only header if no results', function () { var buffer = createOutputBuffer(); - var handler = output.createCsv(buffer); + var handler = output.createCsv(buffer, { mocha: true, junit: 'test.csv' }); handler.close(); expect(buffer.cache).to.equal(''); }) it('should indicate one pass there is one passing result', function () { var buffer = createOutputBuffer(); - var handler = output.createCsv(buffer); - handler.writeResult({status: 0}, {title:'myTitle'}, 'key'); + var handler = output.createCsv(buffer, { mocha: true, junit: 'test.csv' }); + handler.writeResult({status: 0}, {title:'myTitle', description: 'myDescription'}, 'key'); handler.close(); - expect(buffer.cache).to.equal('category,title,resource,region,statusWord,message\n,myTitle,N/A,Global,OK,\n'); + expect(buffer.cache).to.equal('category,title,description,resource,region,statusWord,message\n,myTitle,myDescription,N/A,Global,OK,\n'); }) }) describe('json', function () { it('should generate empty array if no results', function () { var buffer = createOutputBuffer(); - var handler = output.createJson(buffer); + var handler = output.createJson(buffer, { mocha: true, junit: 'test.json' }); handler.close(); expect(buffer.cache).to.equal('[]'); }) it('should indicate one pass there is one passing result', function () { var buffer = createOutputBuffer(); - var handler = output.createJson(buffer); - handler.writeResult({status: 0}, {title:'myTitle'}, 'key'); + var handler = output.createJson(buffer, { mocha: true, junit: 'test.json' }); + handler.writeResult({ status: 0 }, { title: 'myTitle', description: 'myDescription' }, 'key'); handler.close(); - expect(buffer.cache).to.equal('[{"plugin":"key","title":"myTitle","resource":"N/A","region":"Global","status":"OK"}]'); + expect(JSON.stringify(JSON.parse(buffer.cache))).to.equal('[{"plugin":"key","title":"myTitle","description":"myDescription","resource":"N/A","region":"Global","status":"OK"}]'); }) }) @@ -108,7 +108,11 @@ describe('output', function () { // default, which is console output. var handler = output.create([]) - handler.writeResult({status: 0}, {title:'myTitle'}, 'key'); + handler.writeResult({status: 0, message: 'Certificate has validation enabled'}, { + category: 'ACM', + title:'ACM Certificate Validation', + description: 'Testing the ACM certificate which must have DNS validation enabled' + }, 'key'); handler.close(); // No expect here because in the current structure, we cannot // capture the standard output @@ -119,21 +123,11 @@ describe('output', function () { // default, which is console output. var handler = output.create([]); - // Create the information about the compliance rule - for this - // test, it doesn't have to be anything fancy - var complianceRule = { - describe: function (pluginKey, plugin) { - return 'desc'; - } - }; - var plugin = { - title: 'title' - }; - var pluginKey = 'someIdentifier'; - - handler.startCompliance(plugin, pluginKey, complianceRule); - handler.writeResult({status: 0}, {title:'myTitle'}, 'key'); - handler.endCompliance(plugin, pluginKey, complianceRule); + handler.writeResult({ status: 0, message: 'Certificate has validation enabled'}, { + category: 'ACM', + title: 'ACM Certificate Validation', + description: 'Testing the ACM certificate which must have DNS validation enabled' + }, 'key2', 'Compliance message'); handler.close(); // No expect here because in the current structure, we cannot // capture the standard output diff --git a/postprocess/suppress.js b/postprocess/suppress.js index 97c2665700..bccea55237 100644 --- a/postprocess/suppress.js +++ b/postprocess/suppress.js @@ -1,18 +1,13 @@ module.exports = { - create: function(argv) { + create: function(suppressions) { // Creates an object that can post process results to suppress rules // This allows the client to set to ignore particular failures so that // they don't affect the overall score // Suppressions have the format pluginId:region:resourceId, where any // of the items can be * to indicate match all. + if (!suppressions) suppressions = []; - var expressions = argv - .filter(function(arg) { - return arg.startsWith('--suppress='); - }) - .map(function(arg) { - return arg.substring(11); - }) + var expressions = suppressions .map(function(expr) { return [ expr, diff --git a/postprocess/suppress.spec.js b/postprocess/suppress.spec.js index a88c6a0e4f..fad473faea 100644 --- a/postprocess/suppress.spec.js +++ b/postprocess/suppress.spec.js @@ -9,20 +9,20 @@ describe('create', function () { }); it('should return the filter if matches', function () { - var filter = suppress.create(['--suppress=*n*']); + var filter = suppress.create(['*n*']); expect(filter('any')).to.equal('*n*'); }); it('should return the filter if matches whole word', function () { - var filter = suppress.create(['--suppress=*longer*']); + var filter = suppress.create(['*longer*']); expect(filter('longer')).to.equal('*longer*'); }); it('should return the filter if multiple and second matches', function () { - var filter = suppress.create(['--suppress=*first*', - '--suppress=second']); + var filter = suppress.create(['*first*', + 'second']); expect(filter('second')).to.equal('second'); });