diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 6cc86a237..149d87c74 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -5,7 +5,7 @@ Thank you for your interest in contributing to CloudExploit! We welcome your PRs
The CloudExploit project, maintainers, and contributors are governed by the [CloudExploit Code of Conduct](CODE_OF_CONDUCT.md). By contributing, you are agreeing to uphold this code in your interactions with the CloudExploit community.
## License
-By contributing code to CloudExploit, you attest that you have the rights to all code and that you are assigning these rights to Khulnasoft Security, Ltd. for use within its projects.
+By contributing code to CloudExploit, you attest that you have the rights to all code and that you are assigning these rights to KhulnaSoft 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 CloudExploit scans locally. We also have a [guide for writing new plugins](../docs/writing-plugins.md).
diff --git a/.github/pull_request_templates/pull_request_template.md b/.github/pull_request_templates/pull_request_template.md
deleted file mode 100644
index c6a382e3e..000000000
--- a/.github/pull_request_templates/pull_request_template.md
+++ /dev/null
@@ -1,28 +0,0 @@
-# Pull Request
-
-## Description
-
-
-## Related Issues
-
-
-## Type of Change
-- [ ] Bug fix (non-breaking change that fixes an issue)
-- [ ] New feature (non-breaking change that adds functionality)
-- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
-- [ ] Documentation update
-
-## Testing
-- [ ] I have added/updated tests for the changes.
-- [ ] All tests pass locally with my changes.
-- [ ] I have tested the changes in the following environments:
- - [ ] Local development
- - [ ] Staging
- - [ ] Production (if applicable)
-
-## Documentation
-- [ ] I have updated the documentation to reflect the changes.
-- [ ] The changes are documented in the README or relevant documentation files.
-
-## Additional Notes
-
diff --git a/.github/workflows/scans_ci.yml b/.github/workflows/scans_ci.yml
index 6338651f7..52815e234 100644
--- a/.github/workflows/scans_ci.yml
+++ b/.github/workflows/scans_ci.yml
@@ -1,44 +1,25 @@
-name: CI/CD Pipeline
-
-on:
- push:
- branches: [ main, master ]
- pull_request:
- branches: [ main, master ]
- schedule:
- - cron: '0 0 * * 0' # Weekly run on Sunday at midnight
+name:
+on: [push, pull_request, create, delete, issue_comment]
jobs:
- test:
- name: Test on Node.js ${{ matrix.node-version }}
+ build:
runs-on: ubuntu-latest
- strategy:
- matrix:
- node-version: [12.x, 14.x, 16.x] # Test on multiple Node.js versions
+
steps:
- uses: actions/checkout@v2
-
- - name: Use Node.js ${{ matrix.node-version }}
- uses: actions/setup-node@v2
+ - name: Use Node.js
+ uses: actions/setup-node@v1
with:
- node-version: ${{ matrix.node-version }}
- cache: 'npm'
-
- - name: Check for common typos
- uses: khulnasoft/codetypo-actions@master
+ node-version: '12.x'
+ - uses: codespell-project/actions-codespell@master
with:
check_filenames: true
skip: ./.github/*,.git,./package.json,./package-lock.json,./node_modules,./tests,./config,*.png,Dockerfile,./scripts,*.spec.js,./plugins/azure/storageaccounts/storageAccountsAADEnabled.js,./plugins/aws/cloudtrail/cloudtrailBucketAccessLogging.js,./helpers/google/index.js,*zip
ignore_words_list: iam,\"tRe\",AKS,aks,optin,callInt,callInt
-
- - name: Install Dependencies
- run: npm ci
-
+ - run: npm install
+
- name: Lint
run: npm run lint
-
- - name: Run Tests
+
+ - name: NPM Test
run: npm test
-
- - name: Build
- run: npm run build --if-present
diff --git a/Dockerfile b/Dockerfile
index 0599d6f16..e931b5c90 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -9,21 +9,30 @@ FROM node:lts-alpine3.12
# You could also use this to specify a particular version number.
ARG PACKAGENAME=cloudexploit
+# Create a non-root user and group
+RUN addgroup -S cloudexploit && adduser -S cloudexploit -G cloudexploit
+
COPY . /var/scan/cloudexploit/
+# Set the working directory to /var/scan
+WORKDIR /var/scan
+
# Install cloudexploit/scan into the container using npm from NPM
-RUN cd /var/scan \
-&& npm init --yes \
+RUN npm init --yes \
&& npm install ${PACKAGENAME} \
-&& npm link /var/scan/cloudexploit
+&& npm link /var/scan/cloudexploit \
+&& chown -R cloudexploit:cloudexploit /var/scan
# Setup the container's path so that you can run cloudexploit directly
# in case someone wants to customize it when running the container.
ENV PATH "$PATH:/var/scan/node_modules/.bin"
+# Switch to non-root user
+USER cloudexploit
+
# By default, run the scan. CMD allows consumers of the container to supply
# command line arguments to the run command to control how this executes.
# Thus, you can use the parameters that you would normally give to index.js
# when running in a container.
-ENTRYPOINT ["cloudexploitscan"]
+ENTRYPOINT ["cloudexploit"]
CMD []
diff --git a/README.md b/README.md
index bd60fd6e2..f4a0387fb 100644
--- a/README.md
+++ b/README.md
@@ -1,408 +1,309 @@
-[](https://travis-ci.org/khulnasoft/cloudexploit)
+# ☁️ CloudExploit by Khulnasoft — Next‑Gen Cloud Security Scanning Platform
-[](https://travis-ci.com/khulnasoft/cloudexploit)
+---
-CloudExploit by Khulnasoft - Cloud Security Scans
-=================
+## 🔥 Overview
-[
](https://cloud.khulnasoft.com/signup)
+**CloudExploit** is a powerful **multi‑cloud security scanning engine** built for:
-## Quick Start
-### Generic
-```
-$ git clone https://github.com/khulnasoft/cloudexploit.git
-$ cd cloudexploit
-$ npm install
-$ ./index.js -h
-```
+* ☁️ AWS, Azure, GCP, Oracle Cloud
+* 🔐 Compliance auditing (HIPAA, PCI, CIS)
+* ⚙️ CI/CD security automation
+* 🤖 AI‑driven security workflows (future‑ready)
-### Docker
-```
-$ git clone https://github.com/khulnasoft/cloudexploit.git
-$ cd cloudexploit
-$ docker build . -t cloudexploit:0.0.1
-$ docker run cloudexploit:0.0.1 -h
-$ docker run -e AWS_ACCESS_KEY_ID=XX -e AWS_SECRET_ACCESS_KEY=YY cloudexploit:0.0.1 --compliance=pci
-```
+It detects **misconfigurations, vulnerabilities, and risky policies** across your cloud infrastructure.
-## Documentation
-* [Background](#background)
-* [Deployment Options](#deployment-options)
- + [Self-Hosted](#self-hosted)
- + [Hosted at Khulnasoft Wave](#hosted-at-khulnasoft-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)
- + [CloudExploit Config File](#cloudexploit-config-file)
- + [Credential Files](#credential-files)
- + [AWS](#aws)
- + [Azure](#azure)
- + [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
-CloudExploit by Khulnasoft 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
-CloudExploit is available in two deployment options:
-
-### Self-Hosted
-Follow the instructions below to deploy the open-source version of CloudExploit on your machine in just a few simple steps.
-
-### Hosted at Khulnasoft Wave
-A commercial version of CloudExploit hosted at Khulnasoft Wave. Try [Khulnasoft Wave](https://cloud.khulnasoft.com/signup) today!
-
-## Installation
-Ensure that NodeJS is installed. If not, install it from [here](https://nodejs.org/download/).
+---
-```bash
-$ git clone git@github.com:cloudexploit/scans.git
-$ npm install
-```
+# 🚀 New Capabilities (Enhanced Runtime Platform)
+
+## ⚡ Accelerated Runtime Modes
-## Development and Testing
+CloudExploit supports multiple execution strategies:
-### Running Tests
+| Mode | Description |
+| ------------ | ---------------------------------- |
+| `standard` | Full scan, all plugins |
+| `fast` | Parallel scan, optimized for speed |
+| `targeted` | Plugin‑specific scanning |
+| `compliance` | Compliance‑only execution |
+| `ci` | Optimized for CI/CD pipelines |
+| `low-memory` | Reduced resource usage |
-To run the test suite, use the following command:
+**Example**
```bash
-npm test
+./index.js --mode=fast
```
-### Running Specific Tests
+---
-To run a specific test file or test suite, you can use the following command:
+## 🧪 Emulator / Sandbox Execution Mode
+
+Run scans **without touching real cloud accounts**:
```bash
-npm test --
+./index.js --emulator=local
```
-### Test Coverage
+**Use cases**
+
+* 🔍 Plugin development
+* 🧪 Security testing
+* 🎓 Training environments
+* 🧱 CI sandbox validation
+
+---
+
+## 🌐 Web Frontend Dashboard
+
+CloudExploit includes an optional **web UI** for real‑time visibility.
+
+### Features
-To generate a test coverage report, run:
+* 📊 Live scan monitoring
+* 🧠 Risk scoring visualization
+* 📁 Historical scan explorer
+* 📉 Compliance dashboards
+* 🔐 Multi‑account view
+
+**Run the frontend**
```bash
-npm run test:coverage
+cd web
+npm install
+npm run dev
```
-### Linting
+Open: **[http://localhost:3000](http://localhost:3000)**
+
+---
-To check for code style issues, run:
+# ⚡ Quick Start
+
+## 🧰 Local Install
```bash
-npm run lint
+git clone https://github.com/khulnasoft/cloudexploit.git
+cd cloudexploit
+npm install
+./index.js -h
```
-### CI/CD
+## 🐳 Docker
-The project uses GitHub Actions for continuous integration. The following workflows are defined:
+```bash
+docker build . -t cloudexploit:latest
-- **CI Pipeline**: Runs on every push and pull request to the `main` or `master` branch. It includes:
- - Linting
- - Unit tests across multiple Node.js versions (12.x, 14.x, 16.x)
- - Build verification
+docker run cloudexploit:latest -h
-### Pull Requests
+docker run \
+ -e AWS_ACCESS_KEY_ID=XX \
+ -e AWS_SECRET_ACCESS_KEY=YY \
+ cloudexploit:latest --compliance=pci
+```
-When submitting a pull request, please ensure that:
+---
-1. All tests pass
-2. The code is properly linted
-3. New features include appropriate tests
-4. Documentation is updated if necessary
+# ⚙️ Runtime Examples
-Use the provided pull request template to ensure consistency in code reviews.
+### Full Scan
-## Configuration
-CloudExploit requires read-only permission to your cloud account. Follow the guides below to provision this access:
+```bash
+./index.js
+```
-* [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)
+### Fast Parallel Scan
-For AWS, you can run CloudExploit directly and it will detect credentials using the default [AWS credential chain](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/CredentialProviderChain.html).
+```bash
+./index.js --mode=fast
+```
-### CloudExploit Config File
-The CloudExploit 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)
+### Compliance Scan
-Start by copying the example config file:
+```bash
+./index.js --compliance=pci
```
-$ cp config_example.js config.js
+
+### CI/CD Fail on Risk
+
+```bash
+./index.js --exit-code --ignore-ok
```
-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:
+### Save Output
+
+```bash
+./index.js --json=report.json --csv=report.csv
```
+
+---
+
+# 🔐 Supported Cloud Providers
+
+* Amazon Web Services
+* Microsoft Azure
+* Google Cloud Platform
+* Oracle Cloud Infrastructure
+* GitHub
+
+---
+
+# ⚙️ Configuration
+
+CloudExploit requires **read‑only security audit access**.
+
+### Setup credentials
+
+* AWS → `docs/aws.md`
+* Azure → `docs/azure.md`
+* GCP → `docs/gcp.md`
+* OCI → `docs/oracle.md`
+
+**Example config**
+
+```js
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 || ''
+ 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.
+---
-#### AWS
-```
-{
- "accessKeyId": "YOURACCESSKEY",
- "secretAccessKey": "YOURSECRETKEY"
-}
-```
+# 🧠 Architecture
-#### Azure
-```
-{
- "ApplicationID": "YOURAZUREAPPLICATIONID",
- "KeyValue": "YOURAZUREKEYVALUE",
- "DirectoryID": "YOURAZUREDIRECTORYID",
- "SubscriptionID": "YOURAZURESUBSCRIPTIONID"
-}
-```
+CloudExploit uses a **two‑phase scanning pipeline**:
-#### GCP
-Note: For GCP, you [generate a JSON file](docs/gcp.md) directly from the GCP console, which you should not edit.
```
-{
- "type": "service_account",
- "project": "GCPPROJECTNAME",
- "client_email": "GCPCLIENTEMAIL",
- "private_key": "GCPPRIVATEKEY"
-}
+[ Collection Engine ]
+ ↓
+[ Data Normalizer ]
+ ↓
+[ Plugin Scanner Engine ]
+ ↓
+[ Risk Scoring + Compliance Mapper ]
+ ↓
+[ Output Engine / API / Web UI ]
```
-#### Oracle OCI
-```
-{
- "tenancyId": "YOURORACLETENANCYID",
- "compartmentId": "YOURORACLECOMPARTMENTID",
- "userId": "YOURORACLEUSERID",
- "keyFingerprint": "YOURORACLEKEYFINGERPRINT",
- "keyValue": "YOURORACLEKEYVALUE",
-}
-```
+---
-### Environment Variables
-CloudExploit supports passing environment variables, but you must first uncomment the section of your `config.js` file relevant to the cloud provider being scanned.
+# 🧩 Plugin System
-You can then pass the variables listed in each section. For example, for AWS:
-```
-{
- access_key: process.env.AWS_ACCESS_KEY_ID || '',
- secret_access_key: process.env.AWS_SECRET_ACCESS_KEY || '',
- session_token: process.env.AWS_SESSION_TOKEN || '',
-}
-```
+Each plugin represents:
-## Running
-To run a standard scan, showing all outputs and results, simply run:
-```
-$ ./index.js
-```
+* A security control
+* A misconfiguration detection rule
+* A compliance mapping
-## CLI Options
-CloudExploit 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
-
- _____ _ _ ______ _ _ _
- / ____| | | | ____| | | (_) |
- | | | | ___ _ _ __| | |__ __ ___ __ | | ___ _| |_
- | | | |/ _ \\| | | |/ _\` | __| \\ \\/ / '_ \\| |/ _ \\| | __|
- | |____| | (_) | |_| | (_| | |____ > <| |_) | | (_) | | |_
- \\_____|_|\\___/ \\__,_|\\__,_|______/_/\\_\\ .__/|_|\\___/|_|\\__|
- | |
- |_|
-
- CloudExploit by Khulnasoft 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
- ```
-
-
-## Compliance
-
-CloudExploit supports mapping of its plugins to particular compliance policies. To run the compliance scan, use the `--compliance` flag. For example:
-```
-$ ./index.js --compliance=hipaa
-$ ./index.js --compliance=pci
-```
+**Run a single plugin**
-Multiple compliance modes can be run at the same time:
-```
-$ ./index.js --compliance=cis1 --compliance=cis2
+```bash
+./index.js --plugin acmValidation
```
-CloudExploit currently supports the following compliance mappings:
+---
-### HIPAA
-```
-$ ./index.js --compliance=hipaa
-```
-HIPAA scans map CloudExploit plugins to the Health Insurance Portability and Accountability Act of 1996.
+# 🛠 Writing Plugins
-### PCI
-```
-$ ./index.js --compliance=pci
-```
-PCI scans map CloudExploit plugins to the Payment Card Industry Data Security Standard.
+See:
-### CIS Benchmarks
-```
-$ ./index.js --compliance=cis
-$ ./index.js --compliance=cis1
-$ ./index.js --compliance=cis2
-```
+* `docs/writing-plugins.md`
+* `docs/writing-remediation.md`
-CIS Benchmarks are supported, both for Level 1 and Level 2 controls. Passing `--compliance=cis` will run both level 1 and level 2 controls.
+---
-## Output Formats
-CloudExploit supports output in several formats for consumption by other tools. If you do not specify otherwise, CloudExploit writes output to standard output (the console) as a table.
+# 📤 Output Formats
-Note: You can pass multiple output formats and combine options for further customization. For example:
-```
-# Print a table to the console and save a CSV file
-$ ./index.js --csv=file.csv --console=table
+| Format | Usage |
+| -------------- | ------------------------ |
+| Console Table | default |
+| JSON | `--json=file.json` |
+| CSV | `--csv=file.csv` |
+| JUnit XML | `--junit=file.xml` |
+| Raw Collection | `--collection=data.json` |
-# 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
-```
+---
-### Console Output
-By default, CloudExploit results are printed to the console in a table format (with colors). You can override this and use plain text instead, by running:
-```
-$ ./index.js --console=text
-```
+# 🔕 Suppression System
-Alternatively, you can suppress the console output entirely by running:
-```
-$ ./index.js --console=none
+Suppress known acceptable risks:
+
+```bash
+--suppress pluginId:region:resourceId
```
-### Ignoring Passing Results
-You can ignore results from output that return an OK status by passing a `--ignore-ok` commandline argument.
+**Example**
-### CSV
-```
-$ ./index.js --csv=file.csv
+```bash
+--suppress *:*:certificate/*
```
-### JSON
-```
-$ ./index.js --json=file.json
-```
+---
-### JUnit XML
-```
-$ ./index.js --junit=file.xml
-```
+# 🧪 CI/CD Integration
-### Collection Output
-CloudExploit saves the data queried from the cloud provider APIs in JSON format, which can be saved alongside other files for debugging or historical purposes.
-```
-$ ./index.js --collection=file.json
-```
+Perfect for:
-## Suppressions
-Results can be suppressed by passing the `--suppress` flag (multiple options are supported) with the following format:
-```
---suppress pluginId:region:resourceId
-```
+* GitHub Actions
+* GitLab CI
+* Jenkins
+* DevSecOps pipelines
+
+**Example**
-For example:
+```bash
+./index.js --exit-code --ignore-ok --json=report.json
```
-# Suppress all results for the acmValidation plugin
-$ ./index.js --suppress acmValidation:*:*
-# Suppress all us-east-1 region results
-$ ./index.js --suppress *:us-east-1:*
+---
-# Suppress all results matching the regex "certificate/*" in all regions for all plugins
-$ ./index.js --suppress *:*:certificate/*
-```
+# ☁️ Hosted Version
-## Running a Single Plugin
-The `--plugin` flag can be used if you only wish to run one plugin.
-```
-$ ./index.js --plugin acmValidation
-```
+Use the fully managed SaaS platform:
+
+👉 [https://cloud.khulnasoft.com/signup](https://cloud.khulnasoft.com/signup)
+
+---
+
+# 🧭 Roadmap
+
+* 🤖 AI risk scoring engine
+* 🧠 LLM‑based remediation suggestions
+* 🔗 SIEM & SOAR integrations
+* 📡 Real‑time cloud event scanning
+* 🛰 Attack path simulation engine
+
+---
+
+# 🤝 Contributing
+
+We welcome contributions:
+
+* Plugins
+* Remediation scripts
+* New cloud providers
+* Performance optimizations
+
+See `.github/CONTRIBUTING.md`
+
+---
+
+# ⚠️ Disclaimer
+
+CloudExploit is intended for:
+
+* Security auditing
+* Compliance validation
+* Defensive security research
+
+Users must ensure **legal authorization** before scanning cloud environments.
-## Architecture
-CloudExploit 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
-Please see our [contribution guidelines](.github/CONTRIBUTING.md) and [complete guide](docs/writing-plugins.md) to writing CloudExploit plugins.
+# 💙 Credits
-## Writing a remediation
-The `--remediate` flag can be used if you want to run remediation for the plugins mentioned as part of this argument. This takes a list of plugin names.
-Please see our [developing remediation guide](docs/writing-remediation.md) for more details.
-## Other Notes
-For other details about the Khulnasoft Wave SaaS product, AWS security policies, and more, [click here](docs/notes.md).
+Built with ❤️ by Khulnasoft Security Team
diff --git a/bannerGenerator.js b/bannerGenerator.js
new file mode 100644
index 000000000..3aeefdd4f
--- /dev/null
+++ b/bannerGenerator.js
@@ -0,0 +1,23 @@
+#!/usr/bin/env node
+
+const figlet = require('figlet');
+const chalk = require('chalk');
+const gradient = require('gradient-string');
+const boxen = require('boxen').default; // <-- fix here
+
+// Generate FIGlet banner
+const banner = figlet.textSync('CloudSploit', { font: 'Slant' });
+
+// Apply gradient
+const colored = gradient.rainbow(banner);
+
+// Wrap in a box
+const boxed = boxen(colored, {
+ padding: 1,
+ margin: 1,
+ borderStyle: 'round',
+ borderColor: 'magenta'
+});
+
+// Print to console
+console.log(boxed);
diff --git a/collectors/aws/accessanalyzer/listFindingsV2.js b/collectors/aws/accessanalyzer/listFindingsV2.js
new file mode 100644
index 000000000..3f9240b56
--- /dev/null
+++ b/collectors/aws/accessanalyzer/listFindingsV2.js
@@ -0,0 +1,49 @@
+var AWS = require('aws-sdk');
+var async = require('async');
+var helpers = require(__dirname + '/../../../helpers/aws');
+
+module.exports = function(AWSConfig, collection, retries, callback) {
+ var accessanalyzer = new AWS.AccessAnalyzer(AWSConfig);
+ async.eachLimit(collection.accessanalyzer.listAnalyzers[AWSConfig.region].data, 15, function(analyzer, cb) {
+ collection.accessanalyzer.listFindingsV2[AWSConfig.region][analyzer.arn] = {};
+ var params = {
+ analyzerArn: analyzer.arn
+ };
+
+ var paginating = false;
+ var paginateCb = function(err, data) {
+ if (err) collection.accessanalyzer.listFindingsV2[AWSConfig.region][analyzer.arn].err = err;
+
+ if (!data) return cb();
+
+ if (paginating && data.findings && data.findings.length &&
+ collection.accessanalyzer.listFindingsV2[AWSConfig.region][analyzer.arn].data.findings &&
+ collection.accessanalyzer.listFindingsV2[AWSConfig.region][analyzer.arn].data.findings.length) {
+ collection.accessanalyzer.listFindingsV2[AWSConfig.region][analyzer.arn].data.findings = collection.accessanalyzer.listFindingsV2[AWSConfig.region][analyzer.arn].data.findings.concat(data.findings);
+ } else {
+ collection.accessanalyzer.listFindingsV2[AWSConfig.region][analyzer.arn].data = data;
+ }
+
+ if (data.nextToken && data.nextToken.length) {
+ paginating = true;
+ return execute(data.nextToken);
+ }
+
+ cb();
+ };
+
+ function execute(nextToken) { // eslint-disable-line no-inner-declarations
+ var localParams = JSON.parse(JSON.stringify(params || {}));
+ if (nextToken) localParams['nextToken'] = nextToken;
+ if (nextToken) {
+ helpers.makeCustomCollectorCall(accessanalyzer, 'listFindingsV2', localParams, retries, null, null, null, paginateCb);
+ } else {
+ helpers.makeCustomCollectorCall(accessanalyzer, 'listFindingsV2', params, retries, null, null, null, paginateCb);
+ }
+ }
+
+ execute();
+ }, function(){
+ callback();
+ });
+};
\ No newline at end of file
diff --git a/collectors/aws/guardduty/getDetector.js b/collectors/aws/guardduty/getDetector.js
index cf635c980..b04b7c3d7 100644
--- a/collectors/aws/guardduty/getDetector.js
+++ b/collectors/aws/guardduty/getDetector.js
@@ -14,7 +14,10 @@ module.exports = function(AWSConfig, collection, retries, callback) {
if (err) {
collection.guardduty.getDetector[AWSConfig.region][detectorId].err = err;
}
- if (data) collection.guardduty.getDetector[AWSConfig.region][detectorId].data = data;
+ if (data) {
+ data.id = detectorId;
+ collection.guardduty.getDetector[AWSConfig.region][detectorId].data = data;
+ }
cb();
});
}, function(){
diff --git a/collectors/aws/ses/getIdentityDkimAttributes.js b/collectors/aws/ses/getIdentityDkimAttributes.js
index ee188d427..3e5106497 100644
--- a/collectors/aws/ses/getIdentityDkimAttributes.js
+++ b/collectors/aws/ses/getIdentityDkimAttributes.js
@@ -27,10 +27,11 @@ module.exports = function(AWSConfig, collection, retries, callback) {
if (err) {
collection.ses.getIdentityDkimAttributes[AWSConfig.region].err = err;
} else if (data && data.DkimAttributes) {
- allDkimAttributes = {
- ...allDkimAttributes,
- ...data.DkimAttributes
- };
+ var processedIdentities = Object.keys(data.DkimAttributes).map((key) => ({
+ identityName: key,
+ ...data.DkimAttributes[key],
+ }));
+ allDkimAttributes = allDkimAttributes.concat(processedIdentities);
}
processIdentityChunk(chunkIndex + 1);
});
diff --git a/collectors/azure/blobService/listContainersSegmented.js b/collectors/azure/blobService/listContainersSegmented.js
index d9579e61f..ab6583178 100644
--- a/collectors/azure/blobService/listContainersSegmented.js
+++ b/collectors/azure/blobService/listContainersSegmented.js
@@ -1,58 +1,59 @@
+const { BlobServiceClient, StorageSharedKeyCredential } = require('@azure/storage-blob');
var async = require('async');
module.exports = function(collection, reliesOn, callback) {
if (!reliesOn['storageAccounts.listKeys']) return callback();
- var azureStorage = require('azure-storage');
-
if (!collection['blobService']['listContainersSegmented']) collection['blobService']['listContainersSegmented'] = {};
if (!collection['blobService']['getContainerAcl']) collection['blobService']['getContainerAcl'] = {};
- // Loop through regions and properties in reliesOn
- async.eachOfLimit(reliesOn['storageAccounts.listKeys'], 10,function(regionObj, region, cb) {
+ async.eachOfLimit(reliesOn['storageAccounts.listKeys'], 10, function(regionObj, region, cb) {
collection['blobService']['listContainersSegmented'][region] = {};
collection['blobService']['getContainerAcl'][region] = {};
async.eachOfLimit(regionObj, 10, function(subObj, resourceId, sCb) {
collection['blobService']['listContainersSegmented'][region][resourceId] = {};
- if (subObj && subObj.data && subObj.data.keys && subObj.data.keys[0] && subObj.data.keys[0].value) {
- // Extract storage account name from resourceId
- var storageAccountName = resourceId.substring(resourceId.lastIndexOf('/') + 1);
- var storageService = new azureStorage['BlobService'](storageAccountName, subObj.data.keys[0].value);
-
- storageService.listContainersSegmented(null, function(serviceErr, serviceResults) {
- if (serviceErr || !serviceResults) {
- collection['blobService']['listContainersSegmented'][region][resourceId].err = (serviceErr || 'No data returned');
- sCb();
- } else {
- collection['blobService']['listContainersSegmented'][region][resourceId].data = serviceResults.entries;
-
- // Add ACLs
- async.eachLimit(serviceResults.entries, 10, function(entryObj, entryCb) {
- var entryId = `${resourceId}/blobService/${entryObj.name}`;
- collection['blobService']['getContainerAcl'][region][entryId] = {};
-
- storageService.getContainerAcl(entryObj.name, function(getErr, getData) {
- if (getErr || !getData) {
- collection['blobService']['getContainerAcl'][region][entryId].err = (getErr || 'No data returned');
- } else {
- collection['blobService']['getContainerAcl'][region][entryId].data = getData;
- }
- entryCb();
- });
- }, function() {
- sCb();
- });
+ const key = subObj && subObj.data && subObj.data.keys && subObj.data.keys[0] && subObj.data.keys[0].value? subObj.data.keys[0].value : null;
+ if (!key) return sCb();
+
+ const storageAccountName = resourceId.substring(resourceId.lastIndexOf('/') + 1);
+ const credential = new StorageSharedKeyCredential(storageAccountName, key);
+ const blobServiceClient = new BlobServiceClient(
+ `https://${storageAccountName}.blob.core.windows.net`,
+ credential
+ );
+
+ const containers = [];
+
+ (async() => {
+ try {
+ for await (const container of blobServiceClient.listContainers()) {
+ containers.push(container);
}
- });
- } else {
- sCb();
- }
- }, function() {
- cb();
- });
- }, function() {
- callback();
- });
+
+ collection['blobService']['listContainersSegmented'][region][resourceId].data = containers;
+
+ // Get ACLs for each container
+ async.eachLimit(containers, 10, async(entryObj, entryCb) => {
+ const entryId = `${resourceId}/blobService/${entryObj.name}`;
+ collection['blobService']['getContainerAcl'][region][entryId] = {};
+
+ try {
+ const containerClient = blobServiceClient.getContainerClient(entryObj.name);
+ const aclResponse = await containerClient.getAccessPolicy();
+ collection['blobService']['getContainerAcl'][region][entryId].data = aclResponse;
+ } catch (getErr) {
+ collection['blobService']['getContainerAcl'][region][entryId].err = getErr.message || getErr;
+ }
+
+ entryCb();
+ }, sCb);
+ } catch (serviceErr) {
+ collection['blobService']['listContainersSegmented'][region][resourceId].err = serviceErr.message || serviceErr;
+ sCb();
+ }
+ })();
+ }, cb);
+ }, callback);
};
diff --git a/collectors/azure/collector.js b/collectors/azure/collector.js
index 6ebcd89d4..a78e1f59b 100644
--- a/collectors/azure/collector.js
+++ b/collectors/azure/collector.js
@@ -10,8 +10,8 @@
- api_calls: (Optional) If provided, will only query these APIs.
- Example:
{
- "skip_locations": ["eastus", "westus"],
- "api_calls": ["storageAccounts:list", "resourceGroups:list"]
+ "skip_locations": ["eastus", "westus"],
+ "api_calls": ["storageAccounts:list", "resourceGroups:list"]
}
- callback: Function to call when the collection is complete
*********************/
@@ -62,14 +62,59 @@ let collect = function(AzureConfig, settings, callback, fargateFlag) {
var collection = {};
let makeCall = function(localUrl, obj, cb, localData) {
- helpers.call({
- url: localUrl,
- post: obj.post,
- token: obj.graph ? loginData.graphToken : (obj.vault ? loginData.vaultToken : loginData.token),
- govcloud : AzureConfig.Govcloud
- }, function(err, data, response) {
- if (err) return cb(err, null, response);
+ const makeApiCall = async(retryAttempt = 0) => {
+ try {
+ const response = await new Promise((resolve, reject) => {
+ helpers.call({
+ url: localUrl,
+ post: obj.post,
+ token: obj.graph ? loginData.graphToken : (obj.vault ? loginData.vaultToken : loginData.token),
+ govcloud: AzureConfig.Govcloud
+ }, (err, data, apiResponse) => {
+ if (err) {
+ reject({ error: err, response: apiResponse });
+ } else {
+ resolve({ data, response: apiResponse });
+ }
+ });
+ });
+
+ handleResponse(response.data);
+ } catch (error) {
+ if (error.error && error.error.includes('ECONNRESET') && retryAttempt < 3) {
+ // Refresh token using callback patterns
+ helpers.login(AzureConfig, function(refreshErr, refreshedLoginData) {
+ if (refreshErr) {
+ return cb(`Failed to refresh token: ${refreshErr}`, null, error.response);
+ }
+
+ // Update loginData with refreshed tokens
+ loginData = refreshedLoginData;
+ console.log('Token refreshed successfully. New token data:', JSON.stringify({
+ environment: loginData.environment,
+ token: loginData.token ? '***' + loginData.token.slice(-8) : null,
+ graphToken: loginData.graphToken ? '***' + loginData.graphToken.slice(-8) : null,
+ vaultToken: loginData.vaultToken ? '***' + loginData.vaultToken.slice(-8) : null
+ }, null, 2));
+
+ // Retry with new token
+ makeApiCall(retryAttempt + 1)
+ .then(() => cb(null, null, null))
+ .catch(err => cb(err, null, null));
+ });
+ } else {
+ return cb(error.error, null, error.response);
+ }
+ }
+ };
+
+ // Start the async process
+ makeApiCall().catch(error => {
+ console.log(`Unexpected error in makeApiCall: ${error}`);
+ cb(error, null, null);
+ });
+ function handleResponse(data) {
// If a new nextLink is provided, this will be updated. There shouldn't
// be a need to hold on to the previous value
if (data && obj.hasListResponse && data.length) data.value = data;
@@ -85,17 +130,24 @@ let collect = function(AzureConfig, settings, callback, fargateFlag) {
return cb(null, localData);
}
- let resData = localData || data;
+ const resData = localData || data;
if (data && ((obj.paginate && data[obj.paginate]) || data['nextLink']) && (!obj.limit || (obj.limit && resData && resData.value && resData.value.length < obj.limit))) {
obj.nextUrl = data['nextLink'] || data[obj.paginate];
processCall(obj, cb, localData || data);
} else {
return cb(null, localData || data || []);
}
- });
+ }
};
let processCall = function(obj, cb, localData) {
+ let callbackCalled = false;
+ const wrappedCallback = (err, data, response) => {
+ if (callbackCalled) return;
+ callbackCalled = true;
+ cb(err, data, response);
+ };
+
if (fargateFlag) {
const maxApiRetryAttempts = 15;
let initialResponse = null;
@@ -108,38 +160,64 @@ let collect = function(AzureConfig, settings, callback, fargateFlag) {
return retryAfter;
},
errorFilter: function(err) {
- return err.includes('TooManyRequests');
+ const errorMessage = typeof err === 'string' ? err : err.message || err.toString();
+
+ // Azure throttling patterns
+ const throttlingPatterns = [
+ 'TooManyRequests',
+ 'RateLimitExceeded',
+ 'Throttling',
+ 'Throttled',
+ 'RequestThrottled',
+ 'RequestLimitExceeded',
+ 'ServerBusy',
+ 'ServiceBusy',
+ 'toomanyrequests',
+ 'ratelimitexceeded',
+ 'throttling',
+ 'throttled',
+ 'requestthrottled',
+ 'requestlimitexceeded',
+ 'serverbusy',
+ 'servicebusy',
+ 'too many requests',
+ 'rate limit',
+ 'retry after',
+ 'the request is being throttled',
+ 'request rate is large',
+ 'rate exceeded'
+ ];
+
+ const errorMatch = throttlingPatterns.some(pattern => errorMessage.includes(pattern));
+ return errorMatch;
}
}, function(retryCallback) {
let localUrl = obj.nextUrl || obj.url.replace(/\{subscriptionId\}/g, AzureConfig.SubscriptionID);
- // Temporary fix to add additional buffer of 6 seconds before each call
var rateLimit = obj.rateLimit && obj.rateLimit == 3000? 6000: obj.rateLimit;
if (rateLimit) {
setTimeout(function() {
console.log(`Fargate collector rate limited: url: ${localUrl}`);
makeCall(localUrl, obj, function(err, data, response) {
initialResponse = response;
- return retryCallback(err, data, response);
+ retryCallback(err, data, response);
}, localData);
}, rateLimit);
} else {
makeCall(localUrl, obj, function(err, data, response) {
initialResponse = response;
- return retryCallback(err, data, response);
+ retryCallback(err, data, response);
}, localData);
}
- }, function(err, data, response) {
- cb(err, data, response);
- });
+ }, wrappedCallback);
} else {
let localUrl = obj.nextUrl || obj.url.replace(/\{subscriptionId\}/g, AzureConfig.SubscriptionID);
if (obj.rateLimit) {
setTimeout(function() {
console.log(`url: ${localUrl}`);
- makeCall(localUrl, obj, cb, localData);
+ makeCall(localUrl, obj, wrappedCallback, localData);
}, obj.rateLimit);
} else {
- makeCall(localUrl, obj, cb, localData);
+ makeCall(localUrl, obj, wrappedCallback, localData);
}
}
};
@@ -531,5 +609,4 @@ let collect = function(AzureConfig, settings, callback, fargateFlag) {
});
};
-module.exports = collect;
-
+module.exports = collect;
\ No newline at end of file
diff --git a/collectors/azure/queueService/listQueuesSegmented.js b/collectors/azure/queueService/listQueuesSegmented.js
index a594798bd..787986b01 100644
--- a/collectors/azure/queueService/listQueuesSegmented.js
+++ b/collectors/azure/queueService/listQueuesSegmented.js
@@ -1,58 +1,57 @@
+const { TableServiceClient, AzureNamedKeyCredential } = require('@azure/data-tables');
var async = require('async');
module.exports = function(collection, reliesOn, callback) {
if (!reliesOn['storageAccounts.listKeys']) return callback();
- var azureStorage = require('azure-storage');
+ if (!collection['tableService']['listTablesSegmented']) collection['tableService']['listTablesSegmented'] = {};
+ if (!collection['tableService']['getTableAcl']) collection['tableService']['getTableAcl'] = {};
- if (!collection['queueService']['listQueuesSegmented']) collection['queueService']['listQueuesSegmented'] = {};
- if (!collection['queueService']['getQueueAcl']) collection['queueService']['getQueueAcl'] = {};
-
- // Loop through regions and properties in reliesOn
- async.eachOfLimit(reliesOn['storageAccounts.listKeys'], 10,function(regionObj, region, cb) {
- collection['queueService']['listQueuesSegmented'][region] = {};
- collection['queueService']['getQueueAcl'][region] = {};
+ async.eachOfLimit(reliesOn['storageAccounts.listKeys'], 10, function(regionObj, region, cb) {
+ collection['tableService']['listTablesSegmented'][region] = {};
+ collection['tableService']['getTableAcl'][region] = {};
async.eachOfLimit(regionObj, 10, function(subObj, resourceId, sCb) {
- collection['queueService']['listQueuesSegmented'][region][resourceId] = {};
-
- if (subObj && subObj.data && subObj.data.keys && subObj.data.keys[0] && subObj.data.keys[0].value) {
- // Extract storage account name from resourceId
- var storageAccountName = resourceId.substring(resourceId.lastIndexOf('/') + 1);
- var storageService = new azureStorage['QueueService'](storageAccountName, subObj.data.keys[0].value);
-
- storageService.listQueuesSegmented(null, function(serviceErr, serviceResults) {
- if (serviceErr || !serviceResults) {
- collection['queueService']['listQueuesSegmented'][region][resourceId].err = (serviceErr || 'No data returned');
- sCb();
- } else {
- collection['queueService']['listQueuesSegmented'][region][resourceId].data = serviceResults.entries;
-
- // Add ACLs
- async.eachLimit(serviceResults.entries, 10, function(entryObj, entryCb) {
- var entryId = `${resourceId}/queueService/${entryObj.name}`;
- collection['queueService']['getQueueAcl'][region][entryId] = {};
-
- storageService.getQueueAcl(entryObj.name, function(getErr, getData) {
- if (getErr || !getData) {
- collection['queueService']['getQueueAcl'][region][entryId].err = (getErr || 'No data returned');
- } else {
- collection['queueService']['getQueueAcl'][region][entryId].data = getData;
- }
- entryCb();
- });
- }, function() {
- sCb();
- });
+ collection['tableService']['listTablesSegmented'][region][resourceId] = {};
+
+ const key = subObj && subObj.data && subObj.data.keys && subObj.data.keys[0] && subObj.data.keys[0].value? subObj.data.keys[0].value:null;
+ if (!key) return sCb();
+
+ const storageAccountName = resourceId.substring(resourceId.lastIndexOf('/') + 1);
+ const credential = new AzureNamedKeyCredential(storageAccountName, key);
+ const serviceClient = new TableServiceClient(
+ `https://${storageAccountName}.table.core.windows.net`,
+ credential
+ );
+
+ const tables = [];
+
+ (async() => {
+ try {
+ for await (const table of serviceClient.listTables()) {
+ tables.push(table.name);
}
- });
- } else {
- sCb();
- }
- }, function() {
- cb();
- });
- }, function() {
- callback();
- });
+
+ collection['tableService']['listTablesSegmented'][region][resourceId].data = tables;
+
+ async.eachLimit(tables, 10, async(tableName, tableCb) => {
+ const tableId = `${resourceId}/tableService/${tableName}`;
+ collection['tableService']['getTableAcl'][region][tableId] = {};
+
+ try {
+ const aclResponse = await serviceClient.getAccessPolicy(tableName);
+ collection['tableService']['getTableAcl'][region][tableId].data = aclResponse;
+ } catch (getErr) {
+ collection['tableService']['getTableAcl'][region][tableId].err = getErr.message || getErr;
+ }
+
+ tableCb();
+ }, sCb);
+ } catch (tableErr) {
+ collection['tableService']['listTablesSegmented'][region][resourceId].err = tableErr.message || tableErr;
+ sCb();
+ }
+ })();
+ }, cb);
+ }, callback);
};
diff --git a/collectors/azure/tableService/listTablesSegmented.js b/collectors/azure/tableService/listTablesSegmented.js
index 0522c6a8a..787986b01 100644
--- a/collectors/azure/tableService/listTablesSegmented.js
+++ b/collectors/azure/tableService/listTablesSegmented.js
@@ -1,58 +1,57 @@
+const { TableServiceClient, AzureNamedKeyCredential } = require('@azure/data-tables');
var async = require('async');
module.exports = function(collection, reliesOn, callback) {
if (!reliesOn['storageAccounts.listKeys']) return callback();
- var azureStorage = require('azure-storage');
-
if (!collection['tableService']['listTablesSegmented']) collection['tableService']['listTablesSegmented'] = {};
if (!collection['tableService']['getTableAcl']) collection['tableService']['getTableAcl'] = {};
- // Loop through regions and properties in reliesOn
- async.eachOfLimit(reliesOn['storageAccounts.listKeys'], 10,function(regionObj, region, cb) {
+ async.eachOfLimit(reliesOn['storageAccounts.listKeys'], 10, function(regionObj, region, cb) {
collection['tableService']['listTablesSegmented'][region] = {};
collection['tableService']['getTableAcl'][region] = {};
async.eachOfLimit(regionObj, 10, function(subObj, resourceId, sCb) {
collection['tableService']['listTablesSegmented'][region][resourceId] = {};
- if (subObj && subObj.data && subObj.data.keys && subObj.data.keys[0] && subObj.data.keys[0].value) {
- // Extract storage account name from resourceId
- var storageAccountName = resourceId.substring(resourceId.lastIndexOf('/') + 1);
- var storageService = new azureStorage['TableService'](storageAccountName, subObj.data.keys[0].value);
-
- storageService.listTablesSegmented(null, function(tableErr, tableResults) {
- if (tableErr || !tableResults) {
- collection['tableService']['listTablesSegmented'][region][resourceId].err = (tableErr || 'No data returned');
- sCb();
- } else {
- collection['tableService']['listTablesSegmented'][region][resourceId].data = tableResults.entries;
-
- // Add table ACLs
- async.eachLimit(tableResults.entries, 10, function(tableName, tableCb){
- var tableId = `${resourceId}/tableService/${tableName}`;
- collection['tableService']['getTableAcl'][region][tableId] = {};
-
- storageService.getTableAcl(tableName, function(getErr, getData){
- if (getErr || !getData) {
- collection['tableService']['getTableAcl'][region][tableId].err = (getErr || 'No data returned');
- } else {
- collection['tableService']['getTableAcl'][region][tableId].data = getData;
- }
- tableCb();
- });
- }, function(){
- sCb();
- });
+ const key = subObj && subObj.data && subObj.data.keys && subObj.data.keys[0] && subObj.data.keys[0].value? subObj.data.keys[0].value:null;
+ if (!key) return sCb();
+
+ const storageAccountName = resourceId.substring(resourceId.lastIndexOf('/') + 1);
+ const credential = new AzureNamedKeyCredential(storageAccountName, key);
+ const serviceClient = new TableServiceClient(
+ `https://${storageAccountName}.table.core.windows.net`,
+ credential
+ );
+
+ const tables = [];
+
+ (async() => {
+ try {
+ for await (const table of serviceClient.listTables()) {
+ tables.push(table.name);
}
- });
- } else {
- sCb();
- }
- }, function() {
- cb();
- });
- }, function() {
- callback();
- });
+
+ collection['tableService']['listTablesSegmented'][region][resourceId].data = tables;
+
+ async.eachLimit(tables, 10, async(tableName, tableCb) => {
+ const tableId = `${resourceId}/tableService/${tableName}`;
+ collection['tableService']['getTableAcl'][region][tableId] = {};
+
+ try {
+ const aclResponse = await serviceClient.getAccessPolicy(tableName);
+ collection['tableService']['getTableAcl'][region][tableId].data = aclResponse;
+ } catch (getErr) {
+ collection['tableService']['getTableAcl'][region][tableId].err = getErr.message || getErr;
+ }
+
+ tableCb();
+ }, sCb);
+ } catch (tableErr) {
+ collection['tableService']['listTablesSegmented'][region][resourceId].err = tableErr.message || tableErr;
+ sCb();
+ }
+ })();
+ }, cb);
+ }, callback);
};
diff --git a/docs/writing-plugins.md b/docs/writing-plugins.md
index 6e318ee33..b6487a8ac 100644
--- a/docs/writing-plugins.md
+++ b/docs/writing-plugins.md
@@ -22,7 +22,7 @@ CloudFront: {
},
```
-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/khulnasoft/cloudexploit/blob/master/collectors/aws/collector.js#L58-L64) section and then the more detailed call in [`postcalls`](https://github.com/khulnasoft/cloudexploit/blob/master/collectors/aws/collector.js#L467-L473), setting it to rely on the output of `listDistributions` call.
+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/cloudexploit/scans/blob/master/collectors/aws/collector.js#L58-L64) section and then the more detailed call in [`postcalls`](https://github.com/cloudexploit/scans/blob/master/collectors/aws/collector.js#L467-L473), setting it to rely on the output of `listDistributions` call.
An example:
@@ -37,7 +37,7 @@ getGroup: {
This section tells CloudExploit to wait until the `IAM:listGroups` call has been made, and then loop through the data that is returned. The `filterKey` tells CloudExploit 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. CloudExploit will loop through each response, re-invoking `getGroup` for each element.
-You can find the [AWS Collector here.](https://github.com/khulnasoft/cloudexploit/blob/master/collectors/aws/collector.js)
+You can find the [AWS Collector here.](https://github.com/cloudexploit/scans/blob/master/collectors/aws/collector.js)
#### Azure Collection
@@ -52,7 +52,7 @@ virtualMachines: {
},
```
-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/khulnasoft/cloudexploit/blob/master/collectors/azure/collector.js#L50-L55) section and then the more detailed call in [`postcalls`](https://github.com/khulnasoft/cloudexploit/blob/master/collectors/azure/collector.js#L293-L302), setting it to rely on the output of `listDistributions` call.
+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/cloudexploit/scans/blob/master/collectors/azure/collector.js#L50-L55) section and then the more detailed call in [`postcalls`](https://github.com/cloudexploit/scans/blob/master/collectors/azure/collector.js#L293-L302), setting it to rely on the output of `listDistributions` call.
```
virtualMachineExtensions: {
@@ -67,7 +67,7 @@ virtualMachineExtensions: {
},
```
-You can find the [Azure Collector here.](https://github.com/khulnasoft/cloudexploit/blob/master/collectors/azure/collector.js)
+You can find the [Azure Collector here.](https://github.com/cloudexploit/scans/blob/master/collectors/azure/collector.js)
#### GCP Collection
@@ -83,7 +83,7 @@ buckets: {
},
```
-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/khulnasoft/cloudexploit/blob/master/collectors/google/collector.js#L103-L109) section and then the more detailed call in [`postcalls`](https://github.com/khulnasoft/cloudexploit/blob/master/collectors/google/collector.js#L213-L223), setting it to rely on the output of `getIamPolicy` call.
+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/cloudexploit/scans/blob/master/collectors/google/collector.js#L103-L109) section and then the more detailed call in [`postcalls`](https://github.com/cloudexploit/scans/blob/master/collectors/google/collector.js#L213-L223), setting it to rely on the output of `getIamPolicy` call.
```
buckets: {
@@ -99,7 +99,7 @@ buckets: {
},
```
-You can find the [GCP Collector here.](https://github.com/khulnasoft/cloudexploit/blob/master/collectors/google/collector.js)
+You can find the [GCP Collector here.](https://github.com/cloudexploit/scans/blob/master/collectors/google/collector.js)
#### Oracle Collection
@@ -115,7 +115,7 @@ vcn: {
},
```
-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/khulnasoft/cloudexploit/blob/master/collectors/oracle/collector.js#L41-L47) section and then the more detailed call in [`postcalls`](https://github.com/khulnasoft/cloudexploit/blob/master/collectors/oracle/collector.js#L243-L251), setting it to rely on the output of `get` call.
+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/cloudexploit/scans/blob/master/collectors/oracle/collector.js#L41-L47) section and then the more detailed call in [`postcalls`](https://github.com/cloudexploit/scans/blob/master/collectors/oracle/collector.js#L243-L251), setting it to rely on the output of `get` call.
```
vcn: {
@@ -129,7 +129,7 @@ vcn: {
},
```
-You can find the [Oracle Collector here.](https://github.com/khulnasoft/cloudexploit/blob/master/collectors/oracle/collector.js)
+You can find the [Oracle Collector here.](https://github.com/cloudexploit/scans/blob/master/collectors/oracle/collector.js)
## Scanning Phase
diff --git a/exports.js b/exports.js
index 7aaa27bd9..261e4485f 100644
--- a/exports.js
+++ b/exports.js
@@ -51,6 +51,7 @@ module.exports = {
'webTierIamRole' : require(__dirname + '/plugins/aws/autoscaling/webTierIamRole.js'),
'appTierIamRole' : require(__dirname + '/plugins/aws/autoscaling/appTierIamRole.js'),
'asgUnusedLaunchConfiguration' : require(__dirname + '/plugins/aws/autoscaling/asgUnusedLaunchConfiguration.js'),
+ 'asgTagPropagation' : require(__dirname + '/plugins/aws/autoscaling/asgTagPropagation.js'),
'workgroupEncrypted' : require(__dirname + '/plugins/aws/athena/workgroupEncrypted.js'),
'workgroupEnforceConfiguration' : require(__dirname + '/plugins/aws/athena/workgroupEnforceConfiguration.js'),
@@ -187,6 +188,8 @@ module.exports = {
'publicAmi' : require(__dirname + '/plugins/aws/ec2/publicAmi.js'),
'encryptedAmi' : require(__dirname + '/plugins/aws/ec2/encryptedAmi.js'),
'amiHasTags' : require(__dirname + '/plugins/aws/ec2/amiHasTags.js'),
+ 'amiNamingConvention' : require(__dirname + '/plugins/aws/ec2/amiNamingConvention.js'),
+ 'oldAmi' : require(__dirname + '/plugins/aws/ec2/oldAmi.js'),
'instanceIamRole' : require(__dirname + '/plugins/aws/ec2/instanceIamRole.js'),
'ebsBackupEnabled' : require(__dirname + '/plugins/aws/ec2/ebsBackupEnabled.js'),
'ebsEncryptionEnabled' : require(__dirname + '/plugins/aws/ec2/ebsEncryptionEnabled.js'),
@@ -243,6 +246,8 @@ module.exports = {
'openAllPortsProtocolsEgress' : require(__dirname + '/plugins/aws/ec2/openAllPortsProtocolsEgress.js'),
'defaultSecurityGroupInUse' : require(__dirname + '/plugins/aws/ec2/defaultSecurityGroupInUse.js'),
'ec2NetworkExposure' : require(__dirname + '/plugins/aws/ec2/ec2NetworkExposure.js'),
+ 'ec2PrivilegeAnalysis' : require(__dirname + '/plugins/aws/ec2/ec2PrivilegeAnalysis.js'),
+
'efsCmkEncrypted' : require(__dirname + '/plugins/aws/efs/efsCmkEncrypted.js'),
'efsEncryptionEnabled' : require(__dirname + '/plugins/aws/efs/efsEncryptionEnabled.js'),
@@ -269,6 +274,8 @@ module.exports = {
'eksLatestPlatformVersion' : require(__dirname + '/plugins/aws/eks/eksLatestPlatformVersion.js'),
'eksClusterHasTags' : require(__dirname + '/plugins/aws/eks/eksClusterHasTags.js'),
'eksNetworkExposure' : require(__dirname + '/plugins/aws/eks/eksNetworkExposure.js'),
+ 'eksPrivilegeAnalysis' : require(__dirname + '/plugins/aws/eks/eksPrivilegeAnalysis.js'),
+
'kendraIndexEncrypted' : require(__dirname + '/plugins/aws/kendra/kendraIndexEncrypted.js'),
@@ -514,6 +521,8 @@ module.exports = {
'lambdaDeadLetterQueue' : require(__dirname + '/plugins/aws/lambda/lambdaDeadLetterQueue.js'),
'lambdaEnhancedMonitoring' : require(__dirname + '/plugins/aws/lambda/lambdaEnhancedMonitoring.js'),
'lambdaUniqueExecutionRole' : require(__dirname + '/plugins/aws/lambda/lambdaUniqueExecutionRole.js'),
+ 'lambdaNetworkExposure' : require(__dirname + '/plugins/aws/lambda/lambdaNetworkExposure.js'),
+ 'lambdaPrivilegeAnalysis' : require(__dirname + '/plugins/aws/lambda/lambdaPrivilegeAnalysis.js'),
'webServerPublicAccess' : require(__dirname + '/plugins/aws/mwaa/webServerPublicAccess.js'),
'environmentAdminPrivileges' : require(__dirname + '/plugins/aws/mwaa/environmentAdminPrivileges.js'),
@@ -650,6 +659,8 @@ module.exports = {
'docdbClusterBackupRetention' : require(__dirname + '/plugins/aws/documentDB/docdbClusterBackupRetention.js'),
'docdbCertificateRotated' : require(__dirname + '/plugins/aws/documentDB/docdbCertificateRotated.js'),
'docdbClusterProfilerEnabled' : require(__dirname + '/plugins/aws/documentDB/docdbClusterProfilerEnabled.js'),
+ 'docdbEncryptionInTransit' : require(__dirname + '/plugins/aws/documentDB/docdbEncryptionInTransit.js'),
+ 'docdbAuditLoggingEnabled' : require(__dirname + '/plugins/aws/documentDB/docdbAuditLoggingEnabled.js'),
'instanceMediaStreamsEncrypted' : require(__dirname + '/plugins/aws/connect/instanceMediaStreamsEncrypted.js'),
'instanceTranscriptsEncrypted' : require(__dirname + '/plugins/aws/connect/instanceTranscriptsEncrypted.js'),
@@ -696,6 +707,8 @@ module.exports = {
'ecsClustersHaveTags' : require(__dirname + '/plugins/aws/ecs/ecsClustersHaveTags.js'),
'ecsClusterWithActiveTask' : require(__dirname + '/plugins/aws/ecs/ecsClusterWithActiveTask.js'),
'ecsClusterActiveService' : require(__dirname + '/plugins/aws/ecs/ecsClusterActiveService.js'),
+ 'ecsServicePublicIpDisabled' : require(__dirname + '/plugins/aws/ecs/ecsServicePublicIpDisabled.js'),
+ 'ecsFargatePlatformVersion' : require(__dirname + '/plugins/aws/ecs/ecsFargatePlatformVersion.js'),
'cognitoHasWafEnabled' : require(__dirname + '/plugins/aws/cognito/cognitoHasWafEnabled.js'),
'cognitoMFAEnabled' : require(__dirname + '/plugins/aws/cognito/cognitoMFAEnabled.js'),
@@ -818,6 +831,7 @@ module.exports = {
'vmDiskCMKRotation' : require(__dirname + '/plugins/azure/virtualmachines/vmDiskCMKRotation.js'),
'vmDiskPublicAccess' : require(__dirname + '/plugins/azure/virtualmachines/vmDiskPublicAccess.js'),
'computeGalleryRbacSharing' : require(__dirname + '/plugins/azure/virtualmachines/computeGalleryRbacSharing.js'),
+ 'vmPrivilegeAnalysis' : require(__dirname + '/plugins/azure/virtualmachines/vmPrivilegeAnalysis.js'),
'vmNetworkExposure' : require(__dirname + '/plugins/azure/virtualmachines/vmNetworkExposure.js'),
'bastionHostExists' : require(__dirname + '/plugins/azure/bastion/bastionHostExists.js'),
@@ -888,6 +902,8 @@ module.exports = {
'postgresqlPrivateEndpoints' : require(__dirname + '/plugins/azure/postgresqlserver/postgresqlPrivateEndpoints.js'),
'azureServicesAccessDisabled' : require(__dirname + '/plugins/azure/postgresqlserver/azureServicesAccessDisabled.js'),
'postgresqlTlsVersion' : require(__dirname + '/plugins/azure/postgresqlserver/postgresqlTlsVersion.js'),
+ 'postgresqlServerPublicAccess' : require(__dirname + '/plugins/azure/postgresqlserver/postgresqlServerPublicAccess.js'),
+ 'postgresqlFlexibleServerPublicAccess': require(__dirname + '/plugins/azure/postgresqlserver/postgresqlFlexibleServerPublicAccess.js'),
'flexibleServerPrivateAccess' : require(__dirname + '/plugins/azure/postgresqlserver/flexibleServerPrivateAccess'),
'diagnosticLoggingEnabled' : require(__dirname + '/plugins/azure/postgresqlserver/diagnosticLoggingEnabled.js'),
'flexibleServerLogDisconnections': require(__dirname + '/plugins/azure/postgresqlserver/flexibleServerLogDisconnections.js'),
@@ -1003,6 +1019,9 @@ module.exports = {
'disableFTPDeployments' : require(__dirname + '/plugins/azure/appservice/disableFTPDeployments.js'),
'accessControlAllowCredential' : require(__dirname + '/plugins/azure/appservice/accessControlAllowCredential.js'),
'appServiceDiagnosticLogs' : require(__dirname + '/plugins/azure/appservice/appServiceDiagnosticLogs.js'),
+ 'functionPrivilegeAnalysis' : require(__dirname + '/plugins/azure/appservice/functionPrivilegeAnalysis.js'),
+ 'functionAppNetworkExposure' : require(__dirname + '/plugins/azure/appservice/functionAppNetworkExposure.js'),
+ 'appServicePublicAccess' : require(__dirname + '/plugins/azure/appservice/appServicePublicAccess.js'),
'rbacEnabled' : require(__dirname + '/plugins/azure/kubernetesservice/rbacEnabled.js'),
'aksManagedIdentity' : require(__dirname + '/plugins/azure/kubernetesservice/aksManagedIdentity.js'),
@@ -1015,6 +1034,7 @@ module.exports = {
'aksHostBasedEncryption' : require(__dirname + '/plugins/azure/kubernetesservice/aksHostBasedEncryption.js'),
'aksApiAuthorizedIpRanges' : require(__dirname + '/plugins/azure/kubernetesservice/aksApiAuthorizedIpRanges.js'),
'aksNetworkExposure' : require(__dirname + '/plugins/azure/kubernetesservice/aksNetworkExposure.js'),
+ 'aksPrivilegeAnalysis' : require(__dirname + '/plugins/azure/kubernetesservice/aksPrivilegeAnalysis.js'),
'acrAdminUser' : require(__dirname + '/plugins/azure/containerregistry/acrAdminUser.js'),
'acrHasTags' : require(__dirname + '/plugins/azure/containerregistry/acrHasTags.js'),
@@ -1029,14 +1049,14 @@ module.exports = {
'endpointLoggingEnabled' : require(__dirname + '/plugins/azure/cdnprofiles/endpointLoggingEnabled.js'),
'detectInsecureCustomOrigin' : require(__dirname + '/plugins/azure/cdnprofiles/detectInsecureCustomOrigin.js'),
- 'passwordRequiresLowercase' : require(__dirname + '/plugins/azure/activedirectory/passwordRequiresLowercase.js'),
- 'passwordRequiresNumbers' : require(__dirname + '/plugins/azure/activedirectory/passwordRequiresNumbers.js'),
- 'passwordRequiresSymbols' : require(__dirname + '/plugins/azure/activedirectory/passwordRequiresSymbols.js'),
- 'passwordRequiresUppercase' : require(__dirname + '/plugins/azure/activedirectory/passwordRequiresUppercase.js'),
- 'minPasswordLength' : require(__dirname + '/plugins/azure/activedirectory/minPasswordLength.js'),
- 'ensureNoGuestUser' : require(__dirname + '/plugins/azure/activedirectory/ensureNoGuestUser.js'),
- 'noCustomOwnerRoles' : require(__dirname + '/plugins/azure/activedirectory/noCustomOwnerRoles.js'),
- 'appOrgnaizationalDirectoryAccess' : require(__dirname + '/plugins/azure/activedirectory/appOrgnaizationalDirectoryAccess.js'),
+ 'passwordRequiresLowercase' : require(__dirname + '/plugins/azure/entraid/passwordRequiresLowercase.js'),
+ 'passwordRequiresNumbers' : require(__dirname + '/plugins/azure/entraid/passwordRequiresNumbers.js'),
+ 'passwordRequiresSymbols' : require(__dirname + '/plugins/azure/entraid/passwordRequiresSymbols.js'),
+ 'passwordRequiresUppercase' : require(__dirname + '/plugins/azure/entraid/passwordRequiresUppercase.js'),
+ 'minPasswordLength' : require(__dirname + '/plugins/azure/entraid/minPasswordLength.js'),
+ 'ensureNoGuestUser' : require(__dirname + '/plugins/azure/entraid/ensureNoGuestUser.js'),
+ 'noCustomOwnerRoles' : require(__dirname + '/plugins/azure/entraid/noCustomOwnerRoles.js'),
+ 'appOrgnaizationalDirectoryAccess' : require(__dirname + '/plugins/azure/entraid/appOrgnaizationalDirectoryAccess.js'),
'dbAuditingEnabled' : require(__dirname + '/plugins/azure/sqldatabases/dbAuditingEnabled.js'),
'dbDataMaskingEnabled' : require(__dirname + '/plugins/azure/sqldatabases/dbDataMaskingEnabled.js'),
@@ -1076,6 +1096,7 @@ module.exports = {
'keyVaultHasTags' : require(__dirname + '/plugins/azure/keyvaults/keyVaultHasTags.js'),
'keyVaultsPrivateEndpoint' : require(__dirname + '/plugins/azure/keyvaults/keyVaultsPrivateEndpoint.js'),
'kvLogAnalyticsEnabled' : require(__dirname + '/plugins/azure/keyvaults/kvLogAnalyticsEnabled.js'),
+ 'keyVaultPublicAccess' : require(__dirname + '/plugins/azure/keyvaults/keyVaultPublicAccess.js'),
'advancedThreatProtection' : require(__dirname + '/plugins/azure/cosmosdb/advancedThreatProtection.js'),
'cosmosdbDiagnosticLogs' : require(__dirname + '/plugins/azure/cosmosdb/cosmosdbDiagnosticLogs.js'),
@@ -1460,7 +1481,7 @@ module.exports = {
'imagesCMKEncrypted' : require(__dirname + '/plugins/google/compute/imagesCMKEncrypted.js'),
'snapshotEncryption' : require(__dirname + '/plugins/google/compute/snapshotEncryption.js'),
'instanceNetworkExposure' : require(__dirname + '/plugins/google/compute/instanceNetworkExposure.js'),
-
+ 'computePrivilegeAnalysis' : require(__dirname + '/plugins/google/compute/computePrivilegeAnalysis.js'),
'keyRotation' : require(__dirname + '/plugins/google/cryptographickeys/keyRotation.js'),
'keyProtectionLevel' : require(__dirname + '/plugins/google/cryptographickeys/keyProtectionLevel.js'),
'kmsPublicAccess' : require(__dirname + '/plugins/google/cryptographickeys/kmsPublicAccess.js'),
@@ -1569,7 +1590,7 @@ module.exports = {
'binaryAuthorizationEnabled' : require(__dirname + '/plugins/google/kubernetes/binaryAuthorizationEnabled.js'),
'clientCertificateDisabled' : require(__dirname + '/plugins/google/kubernetes/clientCertificateDisabled.js'),
'clusterNetworkExposure' : require(__dirname + '/plugins/google/kubernetes/clusterNetworkExposure.js'),
-
+ 'kubernetesPrivilegeAnalysis' : require(__dirname + '/plugins/google/kubernetes/kubernetesPrivilegeAnalysis.js'),
'dnsSecEnabled' : require(__dirname + '/plugins/google/dns/dnsSecEnabled.js'),
'dnsSecSigningAlgorithm' : require(__dirname + '/plugins/google/dns/dnsSecSigningAlgorithm.js'),
'dnsZoneLabelsAdded' : require(__dirname + '/plugins/google/dns/dnsZoneLabelsAdded.js'),
@@ -1608,8 +1629,16 @@ module.exports = {
'cloudFunctionLabelsAdded' : require(__dirname + '/plugins/google/cloudfunctions/cloudFunctionLabelsAdded.js'),
'cloudFunctionOldRuntime' : require(__dirname + '/plugins/google/cloudfunctions/cloudFunctionOldRuntime.js'),
'functionAllUsersPolicy' : require(__dirname + '/plugins/google/cloudfunctions/functionAllUsersPolicy.js'),
-
'serverlessVPCAccess' : require(__dirname + '/plugins/google/cloudfunctions/serverlessVPCAccess.js'),
+ 'cloudFunctionNetworkExposure' : require(__dirname + '/plugins/google/cloudfunctions/cloudFunctionNetworkExposure.js'),
+ 'cloudFunctionsPrivilegeAnalysis': require(__dirname + '/plugins/google/cloudfunctions/cloudFunctionsPrivilegeAnalysis.js'),
+
+ 'cloudFunctionV2HttpsOnly' : require(__dirname + '/plugins/google/cloudfunctionsv2/cloudFunctionV2HttpsOnly.js'),
+ 'functionV2DefaultServiceAccount': require(__dirname + '/plugins/google/cloudfunctionsv2/functionV2DefaultServiceAccount.js'),
+ 'cloudFunctionV2IngressSettings': require(__dirname + '/plugins/google/cloudfunctionsv2/cloudFunctionV2IngressSettings.js'),
+ 'cloudFunctionV2LabelsAdded' : require(__dirname + '/plugins/google/cloudfunctionsv2/cloudFunctionV2LabelsAdded.js'),
+ 'cloudFunctionV2OldRuntime' : require(__dirname + '/plugins/google/cloudfunctionsv2/cloudFunctionV2OldRuntime.js'),
+ 'cloudFunctionV2VPCConnector' : require(__dirname + '/plugins/google/cloudfunctionsv2/cloudFunctionV2VPCConnector.js'),
'computeAllowedExternalIPs' : require(__dirname + '/plugins/google/cloudresourcemanager/computeAllowedExternalIPs.js'),
'disableAutomaticIAMGrants' : require(__dirname + '/plugins/google/cloudresourcemanager/disableAutomaticIAMGrants.js'),
diff --git a/helpers/asl/asl-1.js b/helpers/asl/asl-1.js
index ac8420dca..67569135b 100644
--- a/helpers/asl/asl-1.js
+++ b/helpers/asl/asl-1.js
@@ -1,30 +1,44 @@
var parse = function(obj, path, region, cloud, accountId, resourceId) {
- //(Array.isArray(obj)) return [obj];
- if (typeof path == 'string' && path.includes('.')) path = path.split('.');
- if (Array.isArray(path) && path.length && typeof obj === 'object') {
+ // Enhanced path splitting: ensure [*] is always its own segment
+ if (typeof path === 'string') {
+ // Split on . but keep [*] as its own segment
+ // Example: networkAcls.ipRules[*].value => ['networkAcls', 'ipRules', '[*]', 'value']
+ path = path
+ .replace(/\[\*\]/g, '.[$*].') // temporarily mark wildcards
+ .split('.')
+ .filter(Boolean)
+ .map(seg => seg === '[$*]' ? '[*]' : seg); // restore wildcard
+ }
+
+ if (Array.isArray(path) && path.length) {
var localPath = path.shift();
- if (localPath.includes('[*]')){
- localPath = localPath.split('[')[0];
- if (obj[localPath] && obj[localPath].length && obj[localPath].length === 1) {
- if (!path || !path.length) {
- return [obj[localPath][0], path];
- } else if (path.length === 1){
- return [obj[localPath],path[0]];
- //return parse(obj[localPath][0], path[0]);
- }
- }
- if (path.length && path.join('.').includes('[*]')) {
- return parse(obj[localPath], path);
- } else if (!obj[localPath] || !obj[localPath].length) {
- return ['not set'];
+ // Handle array wildcard syntax [*]
+ if (localPath === '[*]') {
+ if (Array.isArray(obj)) {
+ var results = [];
+ obj.forEach(function(item) {
+ var pathCopy = path.slice();
+ var result = parse(item, pathCopy, region, cloud, accountId, resourceId);
+ if (Array.isArray(result)) {
+ results = results.concat(result);
+ } else if (result !== 'not set') {
+ results.push(result);
+ }
+ });
+ return results.length > 0 ? results : 'not set';
+ } else {
+ return 'not set';
}
- return [obj[localPath], path];
}
- if (obj[localPath] || typeof obj[localPath] === 'boolean') {
- return parse(obj[localPath], path);
- } else return ['not set'];
+ if (obj && Object.prototype.hasOwnProperty.call(obj, localPath)) {
+ return parse(obj[localPath], path, region, cloud, accountId, resourceId);
+ } else {
+ return 'not set';
+ }
+ } else if (Array.isArray(path) && path.length === 0) {
+ return obj;
} else if (!Array.isArray(obj) && path && path.length) {
- if (obj[path]) return [obj[path]];
+ if (obj[path] || typeof obj[path] === 'boolean') return obj[path];
else {
if (cloud==='aws' && path.startsWith('arn:aws')) {
const template_string = path;
@@ -50,15 +64,191 @@ var parse = function(obj, path, region, cloud, accountId, resourceId) {
}
});
path = converted_string;
- return [path];
- } else return ['not set'];
+ return path;
+ } else return 'not set';
}
} else if (Array.isArray(obj)) {
- return [obj];
+ return obj;
+ } else {
+ return obj;
+ }
+};
+
+var inCidr = function(ip, cidr) {
+ if (!ip || !cidr || typeof ip !== 'string' || typeof cidr !== 'string') {
+ return { result: false, error: 'Malformed IP' };
+ }
+
+ ip = ip.trim();
+ cidr = cidr.trim();
+
+ var isIpv6Cidr = cidr.includes(':');
+ var isIpv6Ip = ip.includes(':');
+
+ if (isIpv6Cidr && !isIpv6Ip) {
+ return { result: false, error: 'Cannot check IPv4 address against IPv6 CIDR' };
+ }
+ if (!isIpv6Cidr && isIpv6Ip) {
+ return { result: false, error: 'Cannot check IPv6 address against IPv4 CIDR' };
+ }
+
+ if (isIpv6Cidr && isIpv6Ip) {
+ return inCidrIPv6(ip, cidr);
} else {
- return [obj];
+ return inCidrIPv4(ip, cidr);
+ }
+};
+
+var inCidrIPv4 = function(ip, cidr) {
+ var cidrMatch = cidr.match(/^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\/(\d{1,2})$/);
+ if (!cidrMatch) {
+ return { result: false, error: 'Malformed IP' };
+ }
+
+ var cidrIp = cidrMatch[1];
+ var prefixLength = parseInt(cidrMatch[2]);
+
+ var cidrParts = cidrIp.split('.').map(function(part) { return parseInt(part); });
+ if (cidrParts.some(function(part) { return isNaN(part) || part < 0 || part > 255; }) || prefixLength < 0 || prefixLength > 32) {
+ return { result: false, error: 'Malformed IP' };
+ }
+
+ var ipMatch = ip.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})(\/\d{1,2})?$/);
+ if (!ipMatch) {
+ return { result: false, error: 'Malformed IP' };
+ }
+
+ var ipParts = ipMatch.slice(1, 5).map(function(part) { return parseInt(part); });
+ if (ipParts.some(function(part) { return isNaN(part) || part < 0 || part > 255; })) {
+ return { result: false, error: 'Malformed IP' };
+ }
+
+ var cidrInt = ((cidrParts[0] << 24) + (cidrParts[1] << 16) + (cidrParts[2] << 8) + cidrParts[3]) >>> 0;
+ var ipInt = ((ipParts[0] << 24) + (ipParts[1] << 16) + (ipParts[2] << 8) + ipParts[3]) >>> 0;
+
+ var mask = (0xFFFFFFFF << (32 - prefixLength)) >>> 0;
+ var networkInt = (cidrInt & mask) >>> 0;
+ var broadcastInt = (networkInt | (0xFFFFFFFF >>> prefixLength)) >>> 0;
+
+ var isInRange = ipInt >= networkInt && ipInt <= broadcastInt;
+
+ var result = {
+ result: isInRange,
+ error: null,
+ message: isInRange ? 'IP in range' : 'IP not in range'
+ };
+
+ return result;
+};
+
+var inCidrIPv6 = function(ip, cidr) {
+ var cidrMatch = cidr.match(/^([0-9a-fA-F:]+)\/(\d{1,3})$/);
+ if (!cidrMatch) {
+ return { result: false, error: 'Malformed IP' };
+ }
+
+ var cidrIp = cidrMatch[1];
+ var prefixLength = parseInt(cidrMatch[2]);
+
+ if (prefixLength < 0 || prefixLength > 128) {
+ return { result: false, error: 'Malformed IP' };
+ }
+
+ var ipv6Pattern = /^[0-9a-fA-F:]+$/;
+ if (!ipv6Pattern.test(ip) || !ipv6Pattern.test(cidrIp)) {
+ return { result: false, error: 'Malformed IP' };
+ }
+
+ try {
+ var expandedCidr = expandIPv6Simple(cidrIp);
+ var expandedIp = expandIPv6Simple(ip);
+
+ if (!expandedCidr || !expandedIp) {
+ return { result: false, error: 'Malformed IP' };
+ }
+
+ var prefixChars = Math.floor(prefixLength / 4);
+ var cidrPrefix = expandedCidr.substring(0, prefixChars);
+ var ipPrefix = expandedIp.substring(0, prefixChars);
+
+ var isInRange = ipPrefix === cidrPrefix;
+
+ var result = {
+ result: isInRange,
+ error: null,
+ message: isInRange ? 'IP in range' : 'IP not in range'
+ };
+
+ return result;
+ } catch (e) {
+ return { result: false, error: 'Malformed IP' };
+ }
+};
+
+var expandIPv6Simple = function(ip) {
+ try {
+ // Handle :: notation (simplified)
+ if (ip.includes('::')) {
+ var parts = ip.split('::');
+ if (parts.length > 2) return null;
+
+ var left = parts[0] ? parts[0].split(':') : [];
+ var right = parts[1] ? parts[1].split(':') : [];
+
+ var totalParts = left.length + right.length;
+ var missingParts = 8 - totalParts;
+
+ if (missingParts < 0) return null;
+
+ var expanded = left.concat(Array(missingParts).fill('0000')).concat(right);
+ return expanded.map(function(part) { return part.padStart(4, '0'); }).join('');
+ } else {
+ var ipParts = ip.split(':');
+ if (ipParts.length !== 8) return null;
+ return ipParts.map(function(part) { return part.padStart(4, '0'); }).join('');
+ }
+ } catch (e) {
+ return null;
+ }
+};
+
+var transformToIpRange = function(val) {
+ if (typeof val !== 'string') {
+ return { error: 'Value must be a string for IPRANGE transformation' };
+ }
+
+ var trimmedVal = val.trim();
+
+ var ipv4CidrPattern = /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\/(\d{1,2})$/;
+ var ipv4SinglePattern = /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/;
+ var ipv6CidrPattern = /^([0-9a-fA-F:]+)\/(\d{1,3})$/;
+ var ipv6SinglePattern = /^[0-9a-fA-F:]+$/;
+
+ var isValidFormat = ipv4CidrPattern.test(trimmedVal) ||
+ ipv4SinglePattern.test(trimmedVal) ||
+ ipv6CidrPattern.test(trimmedVal) ||
+ ipv6SinglePattern.test(trimmedVal);
+
+ if (!isValidFormat) {
+ return { error: 'Value must be a valid IP or CIDR format (e.g., "192.168.1.100" or "192.168.1.0/24")' };
}
+
+ var processedVal = trimmedVal;
+ if (ipv4SinglePattern.test(trimmedVal)) {
+ processedVal = trimmedVal + '/32';
+ } else if (ipv6SinglePattern.test(trimmedVal) && !trimmedVal.includes('/')) {
+ processedVal = trimmedVal + '/128';
+ }
+
+ var result = {
+ type: 'iprange',
+ original: val,
+ cidr: processedVal
+ };
+
+ return result;
};
+
var transform = function(val, transformation) {
if (transformation == 'DATE') {
return new Date(val);
@@ -79,6 +269,8 @@ var transform = function(val, transformation) {
return val;
} else if (transformation == 'TOLOWERCASE') {
return val.toLowerCase();
+ } else if (transformation == 'IPRANGE') {
+ return transformToIpRange(val);
} else {
return val;
}
@@ -87,23 +279,55 @@ var transform = function(val, transformation) {
var compositeResult = function(inputResultsArr, resource, region, results, logical) {
let failingResults = [];
let passingResults = [];
+
+ // No results to process, exit early
+ if (!inputResultsArr || !inputResultsArr.length) {
+ results.push({
+ status: 2,
+ resource: resource,
+ message: 'No results to evaluate',
+ region: region
+ });
+ return;
+ }
+
+ // If only one result, always use its status and message
+ if (inputResultsArr.length === 1) {
+ results.push({
+ status: inputResultsArr[0].status,
+ resource: resource,
+ message: inputResultsArr[0].message,
+ region: region
+ });
+ return;
+ }
+
inputResultsArr.forEach(localResult => {
if (localResult.status === 2) {
failingResults.push(localResult.message);
}
-
if (localResult.status === 0) {
passingResults.push(localResult.message);
}
});
if (!logical) {
- results.push({
- status: inputResultsArr[0].status,
- resource: resource,
- message: inputResultsArr[0].message,
- region: region
- });
+ // Default behavior: if any resource fails, overall result is FAIL
+ if (failingResults && failingResults.length) {
+ results.push({
+ status: 2,
+ resource: resource,
+ message: failingResults.join(' and '),
+ region: region
+ });
+ } else {
+ results.push({
+ status: 0,
+ resource: resource,
+ message: passingResults.join(' and '),
+ region: region
+ });
+ }
} else if (logical === 'AND') {
if (failingResults && failingResults.length) {
results.push({
@@ -139,204 +363,12 @@ var compositeResult = function(inputResultsArr, resource, region, results, logic
}
};
-var validate = function(condition, conditionResult, inputResultsArr, message, property, parsed) {
-
- if (Array.isArray(property)){
- property = property[property.length-1];
- }
- if (parsed && typeof parsed === 'object' && parsed[property]) {
- condition.parsed = parsed[property];
- }
-
- if (condition.transform) {
- try {
- condition.parsed = transform(condition.parsed, condition.transform);
- } catch (e) {
- conditionResult = 2;
- message.push(`${property}: unable to perform transformation`);
- let resultObj = {
- status: conditionResult,
- message: message.join(', ')
- };
-
- inputResultsArr.push(resultObj);
- return resultObj;
- }
- }
-
- // Compare the property with the operator
- if (condition.op) {
- let userRegex;
- if (condition.op === 'MATCHES' || condition.op === 'NOTMATCHES') {
- userRegex = new RegExp(condition.value);
- }
- if (condition.transform && condition.transform == 'EACH' && condition) {
- if (condition.op == 'CONTAINS') {
- var stringifiedCondition = JSON.stringify(condition.parsed);
- if (condition.value && condition.value.includes(':')) {
- var key = condition.value.split(/:(?!.*:)/)[0];
- var value = condition.value.split(/:(?!.*:)/)[1];
-
- if (stringifiedCondition.includes(key) && stringifiedCondition.includes(value)){
- message.push(`${property}: ${condition.value} found in ${stringifiedCondition}`);
- return 0;
- } else {
- message.push(`${condition.value} not found in ${stringifiedCondition}`);
- return 2;
- }
- } else if (stringifiedCondition && stringifiedCondition.includes(condition.value)) {
- message.push(`${property}: ${condition.value} found in ${stringifiedCondition}`);
- return 0;
- } else if (stringifiedCondition && stringifiedCondition.length){
- message.push(`${condition.value} not found in ${stringifiedCondition}`);
- return 2;
- } else {
- message.push(`${condition.parsed} is not the right property type for this operation`);
- return 2;
- }
- } else if (condition.op == 'NOTCONTAINS') {
- var conditionStringified = JSON.stringify(condition.parsed);
- if (condition.value && condition.value.includes(':')) {
-
- var conditionKey = condition.value.split(/:(?!.*:)/)[0];
- var conditionValue = condition.value.split(/:(?!.*:)/)[1];
-
- if (conditionStringified.includes(conditionKey) && !conditionStringified.includes(conditionValue)){
- message.push(`${property}: ${condition.value} not found in ${conditionStringified}`);
- return 0;
- } else {
- message.push(`${condition.value} found in ${conditionStringified}`);
- return 2;
- }
- } else if (conditionStringified && !conditionStringified.includes(condition.value)) {
- message.push(`${property}: ${condition.value} not found in ${conditionStringified}`);
- return 0;
- } else if (conditionStringified && conditionStringified.length){
- message.push(`${condition.value} found in ${conditionStringified}`);
- return 2;
- } else {
- message.push(`${condition.parsed} is not the right property type for this operation`);
- return 2;
- }
- } else {
- // Recurse into the same function
- var subProcessed = [];
- if (!condition.parsed.length) {
- conditionResult = 2;
- message.push(`${property}: is not iterable using EACH transformation`);
- } else {
- condition.parsed.forEach(function(parsed) {
- subProcessed.push(runValidation(parsed, condition, inputResultsArr));
- });
- subProcessed.forEach(function(sub) {
- if (sub.status) conditionResult = sub.status;
- if (sub.message) message.push(sub.message);
- });
- }
- }
- } else if (condition.op == 'EQ') {
- if (condition.parsed == condition.value) {
- message.push(`${property}: ${condition.parsed} matched: ${condition.value}`);
- return 0;
- } else {
- message.push(`${property}: ${condition.parsed} did not match: ${condition.value}`);
- return 2;
- }
- } else if (condition.op == 'GT') {
- if (condition.parsed > condition.value) {
- message.push(`${property}: count of ${condition.parsed} was greater than: ${condition.value}`);
- } else {
- conditionResult = 2;
- message.push(`${property}: count of ${condition.parsed} was not greater than: ${condition.value}`);
- }
- } else if (condition.op == 'NE') {
- if (condition.parsed !== condition.value) {
- message.push(`${property}: ${condition.parsed} is not: ${condition.value}`);
- } else {
- conditionResult = 2;
- message.push(`${property}: ${condition.parsed} is: ${condition.value}`);
- }
- } else if (condition.op == 'MATCHES') {
- if (userRegex.test(condition.parsed)) {
- message.push(`${property}: ${condition.parsed} matches the regex: ${condition.value}`);
- } else {
- conditionResult = 2;
- message.push(`${property}: ${condition.parsed} does not match the regex: ${condition.value}`);
- }
- } else if (condition.op == 'NOTMATCHES') {
- if (!userRegex.test(condition.parsed)) {
- message.push(`${condition.property}: ${condition.parsed} does not match the regex: ${condition.value}`);
- } else {
- conditionResult = 2;
- message.push(`${condition.property}: ${condition.parsed} matches the regex : ${condition.value}`);
- }
- } else if (condition.op == 'EXISTS') {
- if (condition.parsed !== 'not set') {
- message.push(`${property}: set to ${condition.parsed}`);
- return 0;
- } else {
- message.push(`${property}: ${condition.parsed}`);
- return 2;
- }
- } else if (condition.op == 'ISTRUE') {
- if (typeof condition.parsed == 'boolean' && condition.parsed) {
- message.push(`${property} is true`);
- return 0;
- } else if (typeof condition.parsed == 'boolean' && !condition.parsed) {
- conditionResult = 2;
- message.push(`${property} is false`);
- return 2;
- } else {
- message.push(`${property} is not a boolean value`);
- return 2;
- }
- } else if (condition.op == 'ISFALSE') {
- if (typeof condition.parsed == 'boolean' && !condition.parsed) {
- message.push(`${property} is false`);
- return 0;
- } else if (typeof condition.parsed == 'boolean' && condition.parsed) {
- conditionResult = 2;
- message.push(`${property} is true`);
- return 2;
- } else {
- message.push(`${property} is not a boolean value`);
- return 2;
- }
- } else if (condition.op == 'CONTAINS') {
- if (condition.parsed && condition.parsed.length && condition.parsed.includes(condition.value)) {
- message.push(`${property}: ${condition.value} found in ${condition.parsed}`);
- return 0;
- } else if (condition.parsed && condition.parsed.length){
- message.push(`${condition.value} not found in ${condition.parsed}`);
- return 2;
- } else {
- message.push(`${condition.parsed} is not the right property type for this operation`);
- return 2;
- }
- } else if (condition.op == 'NOTCONTAINS') {
- if (condition.parsed && condition.parsed.length && !condition.parsed.includes(condition.value)) {
- message.push(`${property}: ${condition.value} not found in ${condition.parsed}`);
- return 0;
- } else if (condition.parsed && condition.parsed.length){
- message.push(`${condition.value} found in ${condition.parsed}`);
- return 2;
- } else {
- message.push(`${condition.parsed} is not the right property type for this operation`);
- return 2;
- }
- }
- return conditionResult;
- }
-};
-
-var runValidation = function(obj, condition, inputResultsArr, nestedResultArr) {
- let result = 0;
+var runValidation = function(obj, condition, inputResultsArr, nestedResultArr, region, cloud, accountId, resourceId) {
let message = [];
+ let conditionResult = 0; // Initialize conditionResult at function level
// Extract the values for the conditions
if (condition.property) {
-
- let conditionResult = 0;
let property;
if (Array.isArray(condition.property)) {
if (condition.property.length === 1) {
@@ -347,87 +379,404 @@ var runValidation = function(obj, condition, inputResultsArr, nestedResultArr) {
} else {
property = condition.property;
}
- condition.parsed = parse(obj, condition.property)[0];
+
+ // Handle AliasTarget special cases
+ if (typeof property === 'string' && property.includes('AliasTarget')) {
+ const propertyParts = property.split('.');
+ const aliasProperty = propertyParts.length > 1 ? propertyParts[1] : null;
+
+ if (obj && obj.AliasTarget) {
+ if (aliasProperty && obj.AliasTarget[aliasProperty] !== undefined) {
+ condition.parsed = obj.AliasTarget[aliasProperty];
+ } else if (!aliasProperty) {
+ condition.parsed = obj.AliasTarget;
+ } else {
+ condition.parsed = 'not set';
+ }
+ } else {
+ condition.parsed = 'not set';
+ }
+ } else {
+ const parseResult = parse(obj, condition.property, region, cloud, accountId, resourceId);
+ condition.parsed = parseResult;
+ }
- // if ( Array.isArray(obj)) {
- // condition.parsed = obj;
- // } else {
- // condition.parsed = parse(obj, condition.property)[0];
- // }
+ // Normalize: if property is wildcard and parse returned 'not set', treat as ['not set']
+ if ((Array.isArray(condition.property) ? condition.property.join('.') : condition.property).includes('[*]') && condition.parsed === 'not set') {
+ condition.parsed = ['not set'];
+ }
- if ((typeof condition.parsed !== 'boolean' && !condition.parsed)|| condition.parsed === 'not set'){
- conditionResult = 2;
- message.push(`${property}: not set to any value`);
+ // Transform the property if required (except for IPRANGE which transforms the value)
+ if (condition.transform && condition.transform !== 'IPRANGE') {
+ try {
+ condition.parsed = transform(condition.parsed, condition.transform);
+ } catch (e) {
+ conditionResult = 2;
+ message.push(`${property}: unable to perform transformation`);
+ let resultObj = {
+ status: conditionResult,
+ message: message.join(', ')
+ };
- let resultObj = {
- status: conditionResult,
- message: message.join(', ')
- };
+ inputResultsArr.push(resultObj);
+ return resultObj;
+ }
+ }
- inputResultsArr.push(resultObj);
- return resultObj;
+ if (condition.parsed === 'not set'){
+ conditionResult = 2;
+ message.push(`${condition.property}: not set to any value`);
+ } else if ((typeof condition.parsed !== 'boolean' && !condition.parsed) && !Array.isArray(condition.parsed)){
+ conditionResult = 2;
+ message.push(`${property}: not set to any value`);
}
- if (property.includes('[*]')) {
+ // Compare the property with the operator
+ if (condition.op) {
+ let userRegex;
+ if (condition.op === 'MATCHES' || condition.op === 'NOTMATCHES') {
+ userRegex = new RegExp(condition.value);
+ }
+
+ // Handle arrays returned by parse function (from wildcard paths)
if (Array.isArray(condition.parsed)) {
- if (!Array.isArray(nestedResultArr)) nestedResultArr = [];
- let propertyArr = property.split('.');
- propertyArr.shift();
- property = propertyArr.join('.');
- condition.property = property;
- if (condition.op !== 'CONTAINS' || condition.op !== 'NOTCONTAINS') {
- condition.parsed.forEach(parsed => {
- if (property.includes('[*]')) {
- runValidation(parsed, condition, inputResultsArr, nestedResultArr);
- } else {
- let localConditionResult = validate(condition, conditionResult, inputResultsArr, message, property, parsed);
- nestedResultArr.push(localConditionResult);
- }
- // [0,2,0,2,0,0,2,2]
- });
- } else {
- runValidation(condition.parsed, condition, inputResultsArr, nestedResultArr);
- }
- // NestedCompositeResult
- if (nestedResultArr && nestedResultArr.length) {
- if (!condition.nested) condition.nested = 'ONE';
- let resultObj;
- if ((condition.nested.toUpperCase() === 'ONE' && nestedResultArr.indexOf(0) > -1) || (condition.nested.toUpperCase() === 'ALL' && nestedResultArr.indexOf(2) === 0)) {
- resultObj = {
- status: 0,
- message: message.join(', ')
- };
+ let anyMatch = false;
+ let anyNotSet = false;
+ let allNotSet = true;
+ let arrayMessages = [];
+ condition.parsed.forEach(function(item, index) {
+ let itemMatch = false;
+ if (item === 'not set') {
+ arrayMessages.push(`Item ${index}: not set`);
+ anyNotSet = true;
} else {
- resultObj = {
- status: 2,
- message: message.join(', ')
- };
+ allNotSet = false;
}
+ if (condition.op && item !== 'not set') {
+ if (condition.op == 'EQ') {
+ itemMatch = (item == condition.value);
+ } else if (condition.op == 'NE') {
+ itemMatch = (item !== condition.value);
+ } else if (condition.op == 'CONTAINS') {
+ if (condition.transform == 'IPRANGE') {
+ var valueRange = transformToIpRange(condition.value);
+ if (valueRange.error) {
+ arrayMessages.push('Item ' + index + ': ' + valueRange.error);
+ itemMatch = false;
+ } else {
+ var cidrResult = inCidr(condition.value, item);
+ if (cidrResult.error) {
+ arrayMessages.push('Item ' + index + ': ' + cidrResult.error);
+ itemMatch = false;
+ } else {
+ itemMatch = cidrResult.result;
+ var resultMsg = cidrResult.result ? 'allows access from ' + condition.value : 'does not allow access from ' + condition.value;
+ arrayMessages.push('Item ' + index + ': ' + item + ' ' + resultMsg);
+ }
+ }
+ } else {
+ itemMatch = (item && item.includes && item.includes(condition.value));
+ }
+ } else if (condition.op == 'MATCHES') {
+ let userRegex = RegExp(condition.value);
+ itemMatch = userRegex.test(item);
+ } else if (condition.op == 'EXISTS') {
+ itemMatch = (item !== 'not set');
+ } else if (condition.op == 'ISTRUE') {
+ itemMatch = !!item;
+ } else if (condition.op == 'ISFALSE') {
+ itemMatch = !item;
+ } else if (condition.op == 'ISEMPTY') {
+ if (item === 'not set') {
+ itemMatch = false;
+ arrayMessages.push(`Item ${index}: not set`);
+ } else if (typeof item === 'boolean' || typeof item === 'number') {
+ itemMatch = false;
+ arrayMessages.push(`Item ${index}: is of type ${typeof item}, which cannot be empty`);
+ } else {
+ itemMatch = (item === '' || (Array.isArray(item) && item.length === 0) ||
+ (typeof item === 'object' && item !== null && Object.keys(item).length === 0));
+ }
+ }
+ }
+ if (itemMatch) {
+ arrayMessages.push(`Item ${index}: ${item} matched condition`);
+ anyMatch = true;
+ } else if (item !== 'not set') {
+ arrayMessages.push(`Item ${index}: ${item} did not match condition`);
+ }
+ });
+ if (condition.parsed.length === 0 || allNotSet) {
+ message.push(`${condition.property}: ${arrayMessages.join(', ')}`);
+ let resultObj = {
+ status: 2, // FAIL if array is empty or all items are not set (property missing everywhere)
+ message: message.join(', ')
+ };
+ inputResultsArr.push(resultObj);
+ return resultObj;
+ } else if (anyMatch) {
+ message.push(`${condition.property}: ${arrayMessages.join(', ')}`);
+ let resultObj = {
+ status: 0, // PASS if any item matches and at least one is set
+ message: message.join(', ')
+ };
+ inputResultsArr.push(resultObj);
+ return resultObj;
+ } else if (anyNotSet) {
+ message.push(`${condition.property}: ${arrayMessages.join(', ')}`);
+ let resultObj = {
+ status: 2, // FAIL if any item is not set
+ message: message.join(', ')
+ };
+ inputResultsArr.push(resultObj);
+ return resultObj;
+ } else {
+ message.push(`${condition.property}: ${arrayMessages.join(', ')}`);
+ let resultObj = {
+ status: 2, // FAIL if none match and all are set
+ message: message.join(', ')
+ };
inputResultsArr.push(resultObj);
return resultObj;
}
- } else {
- if (!Array.isArray(nestedResultArr)) nestedResultArr = [];
- let propertyArr = property.split('.');
- propertyArr.shift();
- property = propertyArr.join('.');
- let localConditionResult = validate(condition, conditionResult, inputResultsArr, message, condition.property, condition.parsed);
-
- let resultObj = {
- status: localConditionResult,
- message: message.join(', ')
- };
+ }
+ if (condition.transform && condition.transform == 'EACH' && condition) {
+ if (condition.op == 'CONTAINS') {
+ var stringifiedCondition = JSON.stringify(condition.parsed);
+ if (condition.value && condition.value.includes(':')) {
+ var key = condition.value.split(/:(?!.*:)/)[0];
+ var value = condition.value.split(/:(?!.*:)/)[1];
+ if (stringifiedCondition.includes(key) && stringifiedCondition.includes(value)){
+ message.push(`${property}: ${condition.value} found in ${stringifiedCondition}`);
+ conditionResult = 0;
+ } else {
+ message.push(`${condition.value} not found in ${stringifiedCondition}`);
+ conditionResult = 2;
+ }
+ } else if (stringifiedCondition && stringifiedCondition.includes(condition.value)) {
+ message.push(`${property}: ${condition.value} found in ${stringifiedCondition}`);
+ conditionResult = 0;
+ } else if (stringifiedCondition && stringifiedCondition.length){
+ message.push(`${condition.value} not found in ${stringifiedCondition}`);
+ conditionResult = 2;
+ } else {
+ message.push(`${condition.parsed} is not the right property type for this operation`);
+ conditionResult = 2;
+ }
+ } else if (condition.op == 'NOTCONTAINS') {
+ var conditionStringified = JSON.stringify(condition.parsed);
+ if (condition.value && condition.value.includes(':')) {
- inputResultsArr.push(resultObj);
- return resultObj;
+ var conditionKey = condition.value.split(/:(?!.*:)/)[0];
+ var conditionValue = condition.value.split(/:(?!.*:)/)[1];
+ if (conditionStringified.includes(conditionKey) && !conditionStringified.includes(conditionValue)){
+ message.push(`${property}: ${condition.value} not found in ${conditionStringified}`);
+ return 0;
+ } else {
+ message.push(`${condition.value} found in ${conditionStringified}`);
+ return 2;
+ }
+ } else if (conditionStringified && !conditionStringified.includes(condition.value)) {
+ message.push(`${property}: ${condition.value} not found in ${conditionStringified}`);
+ return 0;
+ } else if (conditionStringified && conditionStringified.length){
+ message.push(`${condition.value} found in ${conditionStringified}`);
+ return 2;
+ } else {
+ message.push(`${condition.parsed} is not the right property type for this operation`);
+ return 2;
+ }
+ } else {
+ // Recurse into the same function
+ var subProcessed = [];
+ if (!condition.parsed.length) {
+ conditionResult = 2;
+ message.push(`${property}: is not iterable using EACH transformation`);
+ } else {
+ condition.parsed.forEach(function(parsed) {
+ subProcessed.push(runValidation(parsed, condition, inputResultsArr, null, region, cloud, accountId, resourceId));
+ });
+ subProcessed.forEach(function(sub) {
+ if (sub.status) conditionResult = sub.status;
+ if (sub.message) message.push(sub.message);
+ });
+ }
+ }
+ } else if (condition.op == 'EQ') {
+ if (condition.parsed == condition.value) {
+ message.push(`${property}: ${condition.parsed} matched: ${condition.value}`);
+ conditionResult = 0;
+ } else {
+ // Check if we're comparing an object to a string - common user error
+ if (typeof condition.parsed === 'object' && condition.parsed !== null && typeof condition.value === 'string') {
+ message.push(`${property}: is an object but compared to string "${condition.value}". Consider using a more specific property path like "${property}.propertyName"`);
+ } else {
+ message.push(`${property}: ${condition.parsed} did not match: ${condition.value}`);
+ }
+ conditionResult = 2;
+ }
+ } else if (condition.op == 'GT') {
+ // Convert to numbers for comparison if they are numeric strings
+ let parsedVal = condition.parsed;
+ let comparisonVal = condition.value;
+
+ // Force numeric conversion
+ parsedVal = Number(parsedVal);
+ comparisonVal = Number(comparisonVal);
+
+ if (parsedVal > comparisonVal) {
+ message.push(`${property}: count of ${condition.parsed} was greater than: ${condition.value}`);
+ conditionResult = 0;
+ } else {
+ conditionResult = 2;
+ message.push(`${property}: count of ${condition.parsed} was not greater than: ${condition.value}`);
+ }
+ } else if (condition.op == 'LT') {
+ // Convert to numbers for comparison if they are numeric strings
+ let parsedVal = condition.parsed;
+ let comparisonVal = condition.value;
+
+ // Force numeric conversion
+ parsedVal = Number(parsedVal);
+ comparisonVal = Number(comparisonVal);
+
+ if (parsedVal < comparisonVal) {
+ message.push(`${property}: count of ${condition.parsed} was less than: ${condition.value}`);
+ conditionResult = 0;
+ } else {
+ conditionResult = 2;
+ message.push(`${property}: count of ${condition.parsed} was not less than: ${condition.value}`);
+ }
+ } else if (condition.op == 'NE') {
+ if (condition.parsed !== condition.value) {
+ message.push(`${property}: ${condition.parsed} is not: ${condition.value}`);
+ conditionResult = 0;
+ } else {
+ conditionResult = 2;
+ // Check if we're comparing an object to a string - common user error
+ if (typeof condition.parsed === 'object' && condition.parsed !== null && typeof condition.value === 'string') {
+ message.push(`${property}: is an object but compared to string "${condition.value}". Consider using a more specific property path like "${property}.propertyName"`);
+ } else {
+ message.push(`${property}: ${condition.parsed} is: ${condition.value}`);
+ }
+ }
+ } else if (condition.op == 'MATCHES') {
+ if (userRegex.test(condition.parsed)) {
+ message.push(`${property}: ${condition.parsed} matches the regex: ${condition.value}`);
+ conditionResult = 0;
+ } else {
+ conditionResult = 2;
+ message.push(`${property}: ${condition.parsed} does not match the regex: ${condition.value}`);
+ }
+ } else if (condition.op == 'NOTMATCHES') {
+ if (!userRegex.test(condition.parsed)) {
+ message.push(`${condition.property}: ${condition.parsed} does not match the regex: ${condition.value}`);
+ conditionResult = 0;
+ } else {
+ conditionResult = 2;
+ message.push(`${condition.property}: ${condition.parsed} matches the regex : ${condition.value}`);
+ }
+ } else if (condition.op == 'EXISTS') {
+ if (condition.parsed !== 'not set') {
+ message.push(`${property}: set to ${condition.parsed}`);
+ conditionResult = 0;
+ } else {
+ message.push(`${property}: ${condition.parsed}`);
+ conditionResult = 2;
+ }
+ } else if (condition.op == 'ISTRUE') {
+ if (typeof condition.parsed == 'boolean' && condition.parsed) {
+ message.push(`${property} is true`);
+ conditionResult = 0;
+ } else if (typeof condition.parsed == 'boolean' && !condition.parsed) {
+ conditionResult = 2;
+ message.push(`${property} is false`);
+ } else {
+ message.push(`${property} is not a boolean value`);
+ conditionResult = 2;
+ }
+ } else if (condition.op == 'ISFALSE') {
+ if (typeof condition.parsed == 'boolean' && !condition.parsed) {
+ message.push(`${property} is false`);
+ conditionResult = 0;
+ } else if (typeof condition.parsed == 'boolean' && condition.parsed) {
+ conditionResult = 2;
+ message.push(`${property} is true`);
+ } else {
+ message.push(`${property} is not a boolean value`);
+ conditionResult = 2;
+ }
+ } else if (condition.op == 'ISEMPTY') {
+ if (condition.parsed === 'not set') {
+ message.push(`${property} is not set`);
+ conditionResult = 2;
+ } else if (typeof condition.parsed === 'boolean' || typeof condition.parsed === 'number') {
+ message.push(`${property} is of type ${typeof condition.parsed}, which cannot be empty`);
+ conditionResult = 2;
+ } else if (condition.parsed === '' ||
+ (Array.isArray(condition.parsed) && condition.parsed.length === 0) ||
+ (typeof condition.parsed === 'object' && condition.parsed !== null && Object.keys(condition.parsed).length === 0)) {
+ message.push(`${property} is empty`);
+ conditionResult = 0;
+ } else {
+ message.push(`${property} is not empty`);
+ conditionResult = 2;
+ }
+ } else if (condition.op == 'CONTAINS' && condition.transform == 'IPRANGE') {
+ if (typeof condition.parsed !== 'string') {
+ message.push(property + ': IPRANGE requires property to be an IP address string, got ' + typeof condition.parsed);
+ conditionResult = 2;
+ } else {
+ var valueRange = transformToIpRange(condition.value);
+ if (valueRange.error) {
+ message.push(property + ': ' + valueRange.error);
+ conditionResult = 2;
+ } else {
+ var cidrResult = inCidr(condition.value, condition.parsed);
+
+ if (cidrResult.error) {
+ message.push(property + ': ' + cidrResult.error);
+ conditionResult = 2;
+ } else if (cidrResult.result) {
+ message.push(property + ': ' + cidrResult.message + ' (' + condition.parsed + ' allows access from ' + condition.value + ')');
+ conditionResult = 0;
+ } else {
+ message.push(property + ': ' + cidrResult.message + ' (' + condition.parsed + ' does not allow access from ' + condition.value + ')');
+ conditionResult = 2;
+ }
+ }
+ }
+ } else if (condition.op == 'CONTAINS') {
+ if (condition.parsed && condition.parsed.length && condition.parsed.includes(condition.value)) {
+ message.push(`${property}: ${condition.value} found in ${condition.parsed}`);
+ conditionResult = 0;
+ } else if (condition.parsed && condition.parsed.length){
+ message.push(`${condition.value} not found in ${condition.parsed}`);
+ conditionResult = 2;
+ } else {
+ // Check if we're trying to use CONTAINS on an object - common user error
+ if (typeof condition.parsed === 'object' && condition.parsed !== null && !Array.isArray(condition.parsed)) {
+ message.push(`${property}: is an object, not a string or array. CONTAINS operation requires a string or array. Consider using a more specific property path like "${property}.propertyName"`);
+ } else {
+ message.push(`${condition.parsed} is not the right property type for this operation`);
+ }
+ conditionResult = 2;
+ }
+ } else if (condition.op == 'NOTCONTAINS') {
+ if (condition.parsed && condition.parsed.length && !condition.parsed.includes(condition.value)) {
+ message.push(`${property}: ${condition.value} not found in ${condition.parsed}`);
+ conditionResult = 0;
+ } else if (condition.parsed && condition.parsed.length){
+ message.push(`${condition.value} found in ${condition.parsed}`);
+ conditionResult = 2;
+ } else {
+ message.push(`${condition.parsed} is not the right property type for this operation`);
+ conditionResult = 2;
+ }
}
- } else {
- // Transform the property if required
- conditionResult = validate(condition, conditionResult, inputResultsArr, message, property);
- if (conditionResult) result = conditionResult;
}
}
@@ -436,7 +785,7 @@ var runValidation = function(obj, condition, inputResultsArr, nestedResultArr) {
}
let resultObj = {
- status: result,
+ status: conditionResult,
message: message.join(', ')
};
@@ -445,92 +794,281 @@ var runValidation = function(obj, condition, inputResultsArr, nestedResultArr) {
};
var runConditions = function(input, data, results, resourcePath, resourceName, region, cloud, accountId) {
- let dataToValidate;
- let newPath;
- let newData;
- let validated;
let parsedResource = resourceName;
-
let inputResultsArr = [];
let logical;
let localInput = JSON.parse(JSON.stringify(input));
// to check if top level * matches. ex: Instances[*] should be
// present in each condition if not its impossible to compare resources
- let resourceConditionArr = [];
+
localInput.conditions.forEach(condition => {
logical = condition.logical;
- var conditionPropArr = condition.property.split('.');
- if (condition.property && condition.property.includes('[*]')) {
- if (conditionPropArr.length > 1 && conditionPropArr[1].includes('[*]')) {
- resourceConditionArr.push(conditionPropArr[0]);
- var firstProperty = conditionPropArr.shift();
- dataToValidate = parse(data, firstProperty.split('[*]')[0])[0];
- condition.property = conditionPropArr.join('.');
- if (dataToValidate.length) {
- dataToValidate.forEach(newData => {
- condition.validated = runValidation(newData, condition, inputResultsArr);
- parsedResource = parse(newData, resourcePath, region, cloud, accountId, resourceName)[0];
- if (typeof parsedResource !== 'string' || parsedResource === 'not set') parsedResource = resourceName;
- });
- } else {
- condition.validated = runValidation([], condition, inputResultsArr);
- parsedResource = parse([], resourcePath, region, cloud, accountId, resourceName)[0];
- if (typeof parsedResource !== 'string' || parsedResource === 'not set') parsedResource = resourceName;
- }
- // result per resource
- } else {
- dataToValidate = parse(data, condition.property);
- newPath = dataToValidate[1];
- newData = dataToValidate[0];
- if (newPath && newData.length){
- newData.forEach(dataElm =>{
- if (newPath) condition.property = JSON.parse(JSON.stringify(newPath));
- condition.validated = runValidation(dataElm, condition, inputResultsArr);
- parsedResource = parse(dataElm, resourcePath, region, cloud, accountId, resourceName)[0];
- if (typeof parsedResource !== 'string' || parsedResource === 'not set') parsedResource = resourceName;
- });
- } else if (newPath && !newData.length) {
- condition.property = JSON.parse(JSON.stringify(newPath));
- condition.validated = runValidation(newData, condition, inputResultsArr);
- parsedResource = parse(newData, resourcePath, region, cloud, accountId, resourceName)[0];
- if (parsedResource === 'not set' || typeof parsedResource !== 'string') parsedResource = resourceName;
- } else if (!newPath) {
- // no path returned. means it has fully parsed and got the value.
- // save the value
- newPath = JSON.parse(JSON.stringify(condition.property));
- if (condition.property.includes('.')){
- condition.property = condition.property.split('.')[condition.property.split('.').length -1 ];
+
+ // Special handling for ResourceRecordSets[*].AliasTarget.* properties
+ if (condition.property && condition.property.includes('ResourceRecordSets[*].AliasTarget')) {
+ let foundMatch = false;
+ let matchResults = [];
+ let nonMatchResults = [];
+
+ if (data && data.ResourceRecordSets && Array.isArray(data.ResourceRecordSets)) {
+ // Directly access ResourceRecordSets if it exists at the top level
+ for (let i = 0; i < data.ResourceRecordSets.length; i++) {
+ let record = data.ResourceRecordSets[i];
+ if (record && record.AliasTarget) {
+ // Extract just the AliasTarget part of the property path
+ const aliasProperty = condition.property.split('AliasTarget.')[1];
+
+ if (aliasProperty && record.AliasTarget[aliasProperty]) {
+ let propValue = record.AliasTarget[aliasProperty];
+ let result = 2; // Default to fail
+ let message = '';
+
+ // Perform the actual comparison
+ if (condition.op === 'CONTAINS' && propValue.includes(condition.value)) {
+ result = 0;
+ message = `${aliasProperty}: ${condition.value} found in ${propValue}`;
+ foundMatch = true;
+ matchResults.push({
+ status: result,
+ message: message,
+ resource: record.Name || resourceName
+ });
+ } else if (condition.op === 'NOTCONTAINS' && !propValue.includes(condition.value)) {
+ result = 0;
+ message = `${aliasProperty}: ${condition.value} not found in ${propValue}`;
+ foundMatch = true;
+ matchResults.push({
+ status: result,
+ message: message,
+ resource: record.Name || resourceName
+ });
+ } else if (condition.op === 'EQ' && propValue === condition.value) {
+ result = 0;
+ message = `${aliasProperty}: ${propValue} matched: ${condition.value}`;
+ foundMatch = true;
+ matchResults.push({
+ status: result,
+ message: message,
+ resource: record.Name || resourceName
+ });
+ } else if (condition.op === 'NE' && propValue !== condition.value) {
+ result = 0;
+ message = `${aliasProperty}: ${propValue} is not: ${condition.value}`;
+ foundMatch = true;
+ matchResults.push({
+ status: result,
+ message: message,
+ resource: record.Name || resourceName
+ });
+ } else if (condition.op === 'GT') {
+ // Convert to numbers for comparison if they are numeric strings
+ let parsedVal = Number(propValue);
+ let comparisonVal = Number(condition.value);
+
+ if (!isNaN(parsedVal) && !isNaN(comparisonVal) && parsedVal > comparisonVal) {
+ result = 0;
+ message = `${aliasProperty}: ${propValue} was greater than: ${condition.value}`;
+ foundMatch = true;
+ matchResults.push({
+ status: result,
+ message: message,
+ resource: record.Name || resourceName
+ });
+ } else {
+ message = `${aliasProperty}: ${propValue} was not greater than: ${condition.value}`;
+ nonMatchResults.push({
+ status: 2,
+ message: message,
+ resource: record.Name || resourceName
+ });
+ }
+ } else if (condition.op === 'LT') {
+ // Convert to numbers for comparison if they are numeric strings
+ let parsedVal = Number(propValue);
+ let comparisonVal = Number(condition.value);
+
+ if (!isNaN(parsedVal) && !isNaN(comparisonVal) && parsedVal < comparisonVal) {
+ result = 0;
+ message = `${aliasProperty}: ${propValue} was less than: ${condition.value}`;
+ foundMatch = true;
+ matchResults.push({
+ status: result,
+ message: message,
+ resource: record.Name || resourceName
+ });
+ } else {
+ message = `${aliasProperty}: ${propValue} was not less than: ${condition.value}`;
+ nonMatchResults.push({
+ status: 2,
+ message: message,
+ resource: record.Name || resourceName
+ });
+ }
+ } else if (condition.op === 'ISTRUE') {
+ if (typeof propValue === 'boolean' && propValue === true) {
+ result = 0;
+ message = `${aliasProperty} is true`;
+ foundMatch = true;
+ matchResults.push({
+ status: result,
+ message: message,
+ resource: record.Name || resourceName
+ });
+ } else if (typeof propValue === 'string' &&
+ (propValue.toLowerCase() === 'true' || propValue === '1')) {
+ result = 0;
+ message = `${aliasProperty} is true (${propValue})`;
+ foundMatch = true;
+ matchResults.push({
+ status: result,
+ message: message,
+ resource: record.Name || resourceName
+ });
+ } else {
+ message = `${aliasProperty} is not true`;
+ nonMatchResults.push({
+ status: 2,
+ message: message,
+ resource: record.Name || resourceName
+ });
+ }
+ } else if (condition.op === 'ISFALSE') {
+ if (typeof propValue === 'boolean' && propValue === false) {
+ result = 0;
+ message = `${aliasProperty} is false`;
+ foundMatch = true;
+ matchResults.push({
+ status: result,
+ message: message,
+ resource: record.Name || resourceName
+ });
+ } else if (typeof propValue === 'string' &&
+ (propValue.toLowerCase() === 'false' || propValue === '0')) {
+ result = 0;
+ message = `${aliasProperty} is false (${propValue})`;
+ foundMatch = true;
+ matchResults.push({
+ status: result,
+ message: message,
+ resource: record.Name || resourceName
+ });
+ } else {
+ message = `${aliasProperty} is not false`;
+ nonMatchResults.push({
+ status: 2,
+ message: message,
+ resource: record.Name || resourceName
+ });
+ }
+ } else if (condition.op === 'EXISTS') {
+ result = 0;
+ message = `${aliasProperty}: set to ${propValue}`;
+ foundMatch = true;
+ matchResults.push({
+ status: result,
+ message: message,
+ resource: record.Name || resourceName
+ });
+ } else if (condition.op === 'MATCHES' && new RegExp(condition.value).test(propValue)) {
+ result = 0;
+ message = `${aliasProperty}: ${propValue} matches the regex: ${condition.value}`;
+ foundMatch = true;
+ matchResults.push({
+ status: result,
+ message: message,
+ resource: record.Name || resourceName
+ });
+ } else if (condition.op === 'NOTMATCHES' && !new RegExp(condition.value).test(propValue)) {
+ result = 0;
+ message = `${aliasProperty}: ${propValue} does not match the regex: ${condition.value}`;
+ foundMatch = true;
+ matchResults.push({
+ status: result,
+ message: message,
+ resource: record.Name || resourceName
+ });
+ } else {
+ if (condition.op === 'CONTAINS') {
+ message = `${condition.value} not found in ${propValue}`;
+ } else if (condition.op === 'NOTCONTAINS') {
+ message = `${condition.value} found in ${propValue}`;
+ } else if (condition.op === 'EQ') {
+ message = `${aliasProperty}: ${propValue} did not match: ${condition.value}`;
+ } else if (condition.op === 'NE') {
+ message = `${aliasProperty}: ${propValue} is: ${condition.value}`;
+ } else if (condition.op === 'GT') {
+ message = `${aliasProperty}: ${propValue} was not greater than: ${condition.value}`;
+ } else if (condition.op === 'LT') {
+ message = `${aliasProperty}: ${propValue} was not less than: ${condition.value}`;
+ } else if (condition.op === 'ISTRUE') {
+ message = `${aliasProperty} is not true`;
+ } else if (condition.op === 'ISFALSE') {
+ message = `${aliasProperty} is not false`;
+ } else if (condition.op === 'MATCHES') {
+ message = `${aliasProperty}: ${propValue} does not match the regex: ${condition.value}`;
+ } else if (condition.op === 'NOTMATCHES') {
+ message = `${aliasProperty}: ${propValue} matches the regex: ${condition.value}`;
+ }
+
+ nonMatchResults.push({
+ status: 2,
+ message: message,
+ resource: record.Name || resourceName
+ });
+ }
+ } else if (!aliasProperty) {
+ // Handle the entire AliasTarget object
+ matchResults.push({
+ status: 0,
+ message: `AliasTarget: exists for record ${record.Name}`,
+ resource: record.Name || resourceName
+ });
+ foundMatch = true;
+ }
}
- condition.validated = runValidation(newData, condition, inputResultsArr);
- condition.property = JSON.parse(JSON.stringify(newPath));
- parsedResource = parse(newData, resourcePath, region, cloud, accountId, resourceName)[0];
- if (parsedResource === 'not set' || typeof parsedResource !== 'string') parsedResource = resourceName;
}
}
- } else {
- dataToValidate = parse(data, condition.property);
- if (dataToValidate.length === 1) {
- validated = runValidation(data, condition, inputResultsArr);
- parsedResource = parse(data, resourcePath, region, cloud, accountId, resourceName)[0];
- if (typeof parsedResource !== 'string' || parsedResource === 'not set') parsedResource = resourceName;
- } else {
- newPath = dataToValidate[1];
- newData = dataToValidate[0];
- condition.property = newPath;
- newData.forEach(element =>{
- condition.validated = runValidation(element, condition, inputResultsArr);
- parsedResource = parse(data, resourcePath, region, cloud, accountId, resourceName)[0];
- if (typeof parsedResource !== 'string' || parsedResource === 'not set') parsedResource = null;
-
- results.push({
- status: validated.status,
- resource: parsedResource ? parsedResource : resourceName,
- message: validated.message,
- region: region
+
+ // After checking all records, add the appropriate results to inputResultsArr
+ if (foundMatch) {
+ // If any record matched, add all matching results
+ matchResults.forEach(result => {
+ inputResultsArr.push({
+ status: result.status,
+ message: result.message
});
+ parsedResource = result.resource;
});
+ } else {
+ // If no records matched, add a failure result
+ if (nonMatchResults.length > 0) {
+ // Use the first non-matching result as the representative failure
+ inputResultsArr.push({
+ status: 2,
+ message: nonMatchResults[0].message
+ });
+ parsedResource = nonMatchResults[0].resource;
+ } else {
+ // No records with AliasTarget found
+ inputResultsArr.push({
+ status: 2,
+ message: `No matching records with AliasTarget.${condition.property.split('AliasTarget.')[1] || ''} found`
+ });
+ }
}
+ } else if (condition.property && condition.property.includes('[*]')) {
+ // For wildcard properties, parse once and validate the result
+ const parseResult = parse(data, condition.property, region, cloud, accountId, resourceName);
+ condition.parsed = parseResult;
+ condition.validated = runValidation(data, condition, inputResultsArr, null, region, cloud, accountId, resourceName);
+ parsedResource = parse(data, resourcePath, region, cloud, accountId, resourceName);
+ if (typeof parsedResource !== 'string' || parsedResource === 'not set') parsedResource = resourceName;
+ } else {
+ // For non-wildcard properties, use the same logic as wildcard
+ condition.validated = runValidation(data, condition, inputResultsArr, null, region, cloud, accountId, resourceName);
+ parsedResource = parse(data, resourcePath, region, cloud, accountId, resourceName);
+ if (typeof parsedResource !== 'string' || parsedResource === 'not set') parsedResource = resourceName;
}
});
@@ -568,13 +1106,13 @@ var asl = function(source, input, resourceMap, cloud, accountId, callback) {
message: regionVal.err.message || 'Error',
region: region
});
- } else if (regionVal.data && regionVal.data.length) {
+ } else if (regionVal.data && regionVal.data.length) {
regionVal.data.forEach(function(regionData) {
var resourceName = parse(regionData, resourcePath, region, cloud, accountId)[0];
runConditions(input, regionData, results, resourcePath, resourceName, region, cloud, accountId);
});
} else if (regionVal.data && Object.keys(regionVal.data).length) {
- runConditions(input, regionVal.data, results, resourcePath, '', region);
+ runConditions(input, regionVal.data, results, resourcePath, '', region, cloud, accountId);
} else {
if (!Object.keys(regionVal).length || (regionVal.data && (!regionVal.data.length || !Object.keys(regionVal.data).length))) {
results.push({
@@ -599,7 +1137,6 @@ var asl = function(source, input, resourceMap, cloud, accountId, callback) {
runConditions(input, regionData, results, resourcePath, resourceName, region, cloud, accountId);
});
} else {
-
runConditions(input, resourceObj.data, results, resourcePath, resourceName, region, cloud, accountId);
}
}
diff --git a/helpers/asl/asl-old.js b/helpers/asl/asl-old.js
new file mode 100644
index 000000000..835392bd0
--- /dev/null
+++ b/helpers/asl/asl-old.js
@@ -0,0 +1,937 @@
+var parse = function(obj, path, region, cloud, accountId, resourceId) {
+ //(Array.isArray(obj)) return [obj];
+ if (typeof path == 'string' && path.includes('.')) path = path.split('.');
+ if (Array.isArray(path) && path.length && typeof obj === 'object') {
+ var localPath = path.shift();
+ if (localPath.includes('[*]')){
+ localPath = localPath.split('[')[0];
+ if (obj[localPath] && obj[localPath].length && obj[localPath].length === 1) {
+ if (!path || !path.length) {
+ return [obj[localPath][0], path];
+ } else if (path.length === 1){
+ return [obj[localPath],path[0]];
+ //return parse(obj[localPath][0], path[0]);
+ }
+ }
+ if (path.length && path.join('.').includes('[*]')) {
+ return parse(obj[localPath], path);
+ } else if (!obj[localPath] || !obj[localPath].length) {
+ return ['not set'];
+ }
+ return [obj[localPath], path];
+ }
+ if (obj[localPath] || typeof obj[localPath] === 'boolean') {
+ return parse(obj[localPath], path);
+ } else return ['not set'];
+ } else if (!Array.isArray(obj) && path && path.length) {
+ if (obj[path] || typeof obj[path] === 'boolean') return [obj[path]];
+ else {
+ if (cloud==='aws' && path.startsWith('arn:aws')) {
+ const template_string = path;
+ const placeholders = template_string.match(/{([^{}]+)}/g);
+ let extracted_values = [];
+ if (placeholders) {
+ extracted_values = placeholders.map(placeholder => {
+ const key = placeholder.slice(1, -1);
+ if (key === 'value') return [obj][0];
+ else return obj[key];
+ });
+ }
+ // Replace other variables
+ let converted_string = template_string
+ .replace(/\{region\}/g, region)
+ .replace(/\{cloudAccount\}/g, accountId)
+ .replace(/\{resourceId\}/g, resourceId);
+ placeholders.forEach((placeholder, index) => {
+ if (index === placeholders.length - 1) {
+ converted_string = converted_string.replace(placeholder, extracted_values.pop());
+ } else {
+ converted_string = converted_string.replace(placeholder, extracted_values.shift());
+ }
+ });
+ path = converted_string;
+ return [path];
+ } else return ['not set'];
+ }
+ } else if (Array.isArray(obj)) {
+ return [obj];
+ } else {
+ return [obj];
+ }
+};
+var transform = function(val, transformation) {
+ if (transformation == 'DATE') {
+ return new Date(val);
+ } else if (transformation == 'INTEGER') {
+ return parseInt(val);
+ } else if (transformation == 'STRING') {
+ return val.toString();
+ } else if (transformation == 'DAYSFROM') {
+ // Return the number of days between the date and now
+ var now = new Date();
+ var then = new Date(val);
+ var timeDiff = then.getTime() - now.getTime();
+ var diff = (Math.round(timeDiff / (1000 * 3600 * 24)));
+ return diff;
+ } else if (transformation == 'COUNT') {
+ return val.length;
+ } else if (transformation == 'EACH') {
+ return val;
+ } else if (transformation == 'TOLOWERCASE') {
+ return val.toLowerCase();
+ } else {
+ return val;
+ }
+};
+
+var compositeResult = function(inputResultsArr, resource, region, results, logical) {
+ let failingResults = [];
+ let passingResults = [];
+
+ // No results to process, exit early
+ if (!inputResultsArr || !inputResultsArr.length) {
+ results.push({
+ status: 2,
+ resource: resource,
+ message: 'No results to evaluate',
+ region: region
+ });
+ return;
+ }
+
+ inputResultsArr.forEach(localResult => {
+ if (localResult.status === 2) {
+ failingResults.push(localResult.message);
+ }
+
+ if (localResult.status === 0) {
+ passingResults.push(localResult.message);
+ }
+ });
+
+ if (!logical) {
+ results.push({
+ status: inputResultsArr[0].status,
+ resource: resource,
+ message: inputResultsArr[0].message,
+ region: region
+ });
+ } else if (logical === 'AND') {
+ if (failingResults && failingResults.length) {
+ results.push({
+ status: 2,
+ resource: resource,
+ message: failingResults.join(' and '),
+ region: region
+ });
+ } else {
+ results.push({
+ status: 0,
+ resource: resource,
+ message: passingResults.join(' and '),
+ region: region
+ });
+ }
+ } else {
+ if (passingResults && passingResults.length) {
+ results.push({
+ status: 0,
+ resource: resource,
+ message: passingResults.join(' and '),
+ region: region
+ });
+ } else {
+ results.push({
+ status: 2,
+ resource: resource,
+ message: failingResults.join(' and '),
+ region: region
+ });
+ }
+ }
+};
+
+var validate = function(condition, conditionResult, inputResultsArr, message, property, parsed) {
+ if (Array.isArray(property)){
+ property = property[property.length-1];
+ }
+
+ // Special case for AliasTarget properties
+ if (property && property.includes('AliasTarget') && parsed && typeof parsed === 'object') {
+ // Handle the AliasTarget object which has HostedZoneId, DNSName, and EvaluateTargetHealth
+ if (condition.property && condition.property.includes('AliasTarget')) {
+ // Extract the specific AliasTarget sub-property if specified
+ const aliasProperty = condition.property.split('.')[1]; // Get the part after AliasTarget.
+ if (aliasProperty && parsed.AliasTarget && parsed.AliasTarget[aliasProperty]) {
+ condition.parsed = parsed.AliasTarget[aliasProperty];
+ } else if (!aliasProperty && parsed.AliasTarget) {
+ condition.parsed = parsed.AliasTarget;
+ }
+ }
+ } else if (parsed && typeof parsed === 'object' && parsed[property]) {
+ condition.parsed = parsed[property];
+ }
+
+ if (condition.transform) {
+ try {
+ condition.parsed = transform(condition.parsed, condition.transform);
+ } catch (e) {
+ conditionResult = 2;
+ message.push(`${property}: unable to perform transformation`);
+ let resultObj = {
+ status: conditionResult,
+ message: message.join(', ')
+ };
+
+ inputResultsArr.push(resultObj);
+ return resultObj;
+ }
+ }
+
+ // Compare the property with the operator
+ if (condition.op) {
+ let userRegex;
+ if (condition.op === 'MATCHES' || condition.op === 'NOTMATCHES') {
+ userRegex = new RegExp(condition.value);
+ }
+ if (condition.transform && condition.transform == 'EACH' && condition) {
+ if (condition.op == 'CONTAINS') {
+ var stringifiedCondition = JSON.stringify(condition.parsed);
+ if (condition.value && condition.value.includes(':')) {
+ var key = condition.value.split(/:(?!.*:)/)[0];
+ var value = condition.value.split(/:(?!.*:)/)[1];
+
+ if (stringifiedCondition.includes(key) && stringifiedCondition.includes(value)){
+ message.push(`${property}: ${condition.value} found in ${stringifiedCondition}`);
+ return 0;
+ } else {
+ message.push(`${condition.value} not found in ${stringifiedCondition}`);
+ return 2;
+ }
+ } else if (stringifiedCondition && stringifiedCondition.includes(condition.value)) {
+ message.push(`${property}: ${condition.value} found in ${stringifiedCondition}`);
+ return 0;
+ } else if (stringifiedCondition && stringifiedCondition.length){
+ message.push(`${condition.value} not found in ${stringifiedCondition}`);
+ return 2;
+ } else {
+ message.push(`${condition.parsed} is not the right property type for this operation`);
+ return 2;
+ }
+ } else if (condition.op == 'NOTCONTAINS') {
+ var conditionStringified = JSON.stringify(condition.parsed);
+ if (condition.value && condition.value.includes(':')) {
+
+ var conditionKey = condition.value.split(/:(?!.*:)/)[0];
+ var conditionValue = condition.value.split(/:(?!.*:)/)[1];
+
+ if (conditionStringified.includes(conditionKey) && !conditionStringified.includes(conditionValue)){
+ message.push(`${property}: ${condition.value} not found in ${conditionStringified}`);
+ return 0;
+ } else {
+ message.push(`${condition.value} found in ${conditionStringified}`);
+ return 2;
+ }
+ } else if (conditionStringified && !conditionStringified.includes(condition.value)) {
+ message.push(`${property}: ${condition.value} not found in ${conditionStringified}`);
+ return 0;
+ } else if (conditionStringified && conditionStringified.length){
+ message.push(`${condition.value} found in ${conditionStringified}`);
+ return 2;
+ } else {
+ message.push(`${condition.parsed} is not the right property type for this operation`);
+ return 2;
+ }
+ } else {
+ // Recurse into the same function
+ var subProcessed = [];
+ if (!condition.parsed.length) {
+ conditionResult = 2;
+ message.push(`${property}: is not iterable using EACH transformation`);
+ } else {
+ condition.parsed.forEach(function(parsed) {
+ subProcessed.push(runValidation(parsed, condition, inputResultsArr));
+ });
+ subProcessed.forEach(function(sub) {
+ if (sub.status) conditionResult = sub.status;
+ if (sub.message) message.push(sub.message);
+ });
+ }
+ }
+ } else if (condition.op == 'EQ') {
+ if (condition.parsed == condition.value) {
+ message.push(`${property}: ${condition.parsed} matched: ${condition.value}`);
+ return 0;
+ } else {
+ message.push(`${property}: ${condition.parsed} did not match: ${condition.value}`);
+ return 2;
+ }
+ } else if (condition.op == 'GT') {
+ // Convert to numbers for comparison if they are numeric strings
+ let parsedVal = condition.parsed;
+ let comparisonVal = condition.value;
+
+ // Force numeric conversion
+ parsedVal = Number(parsedVal);
+ comparisonVal = Number(comparisonVal);
+
+ if (parsedVal > comparisonVal) {
+ message.push(`${property}: count of ${condition.parsed} was greater than: ${condition.value}`);
+ return 0;
+ } else {
+ conditionResult = 2;
+ message.push(`${property}: count of ${condition.parsed} was not greater than: ${condition.value}`);
+ return 2;
+ }
+ } else if (condition.op == 'LT') {
+ // Convert to numbers for comparison if they are numeric strings
+ let parsedVal = condition.parsed;
+ let comparisonVal = condition.value;
+
+ // Force numeric conversion
+ parsedVal = Number(parsedVal);
+ comparisonVal = Number(comparisonVal);
+
+ if (parsedVal < comparisonVal) {
+ message.push(`${property}: count of ${condition.parsed} was less than: ${condition.value}`);
+ return 0;
+ } else {
+ conditionResult = 2;
+ message.push(`${property}: count of ${condition.parsed} was not less than: ${condition.value}`);
+ return 2;
+ }
+ } else if (condition.op == 'NE') {
+ if (condition.parsed !== condition.value) {
+ message.push(`${property}: ${condition.parsed} is not: ${condition.value}`);
+ return 0;
+ } else {
+ conditionResult = 2;
+ message.push(`${property}: ${condition.parsed} is: ${condition.value}`);
+ return 2;
+ }
+ } else if (condition.op == 'MATCHES') {
+ if (userRegex.test(condition.parsed)) {
+ message.push(`${property}: ${condition.parsed} matches the regex: ${condition.value}`);
+ return 0;
+ } else {
+ conditionResult = 2;
+ message.push(`${property}: ${condition.parsed} does not match the regex: ${condition.value}`);
+ return 2;
+ }
+ } else if (condition.op == 'NOTMATCHES') {
+ if (!userRegex.test(condition.parsed)) {
+ message.push(`${condition.property}: ${condition.parsed} does not match the regex: ${condition.value}`);
+ return 0;
+ } else {
+ conditionResult = 2;
+ message.push(`${condition.property}: ${condition.parsed} matches the regex : ${condition.value}`);
+ return 2;
+ }
+ } else if (condition.op == 'EXISTS') {
+ if (condition.parsed !== 'not set') {
+ message.push(`${property}: set to ${condition.parsed}`);
+ return 0;
+ } else {
+ message.push(`${property}: ${condition.parsed}`);
+ return 2;
+ }
+ } else if (condition.op == 'ISTRUE') {
+ if (typeof condition.parsed == 'boolean' && condition.parsed) {
+ message.push(`${property} is true`);
+ return 0;
+ } else if (typeof condition.parsed == 'boolean' && !condition.parsed) {
+ conditionResult = 2;
+ message.push(`${property} is false`);
+ return 2;
+ } else {
+ message.push(`${property} is not a boolean value`);
+ return 2;
+ }
+ } else if (condition.op == 'ISFALSE') {
+ if (typeof condition.parsed == 'boolean' && !condition.parsed) {
+ message.push(`${property} is false`);
+ return 0;
+ } else if (typeof condition.parsed == 'boolean' && condition.parsed) {
+ conditionResult = 2;
+ message.push(`${property} is true`);
+ return 2;
+ } else {
+ message.push(`${property} is not a boolean value`);
+ return 2;
+ }
+ } else if (condition.op == 'CONTAINS') {
+ if (condition.parsed && condition.parsed.length && condition.parsed.includes(condition.value)) {
+ message.push(`${property}: ${condition.value} found in ${condition.parsed}`);
+ return 0;
+ } else if (condition.parsed && condition.parsed.length){
+ message.push(`${condition.value} not found in ${condition.parsed}`);
+ return 2;
+ } else {
+ message.push(`${condition.parsed} is not the right property type for this operation`);
+ return 2;
+ }
+ } else if (condition.op == 'NOTCONTAINS') {
+ if (condition.parsed && condition.parsed.length && !condition.parsed.includes(condition.value)) {
+ message.push(`${property}: ${condition.value} not found in ${condition.parsed}`);
+ return 0;
+ } else if (condition.parsed && condition.parsed.length){
+ message.push(`${condition.value} found in ${condition.parsed}`);
+ return 2;
+ } else {
+ message.push(`${condition.parsed} is not the right property type for this operation`);
+ return 2;
+ }
+ }
+ return conditionResult;
+ }
+};
+
+var runValidation = function(obj, condition, inputResultsArr, nestedResultArr) {
+ let result = 0;
+ let message = [];
+
+ // Extract the values for the conditions
+ if (condition.property) {
+
+ let conditionResult = 0;
+ let property;
+ if (Array.isArray(condition.property)) {
+ if (condition.property.length === 1) {
+ property = condition.property[0];
+ } else if (condition.property.length > 1) {
+ property = condition.property.slice(0);
+ }
+ } else {
+ property = condition.property;
+ }
+
+ // Handle AliasTarget special cases
+ let isAliasTargetProperty = false;
+ if (typeof property === 'string' && property.includes('AliasTarget')) {
+ isAliasTargetProperty = true;
+ const propertyParts = property.split('.');
+ const aliasProperty = propertyParts.length > 1 ? propertyParts[1] : null;
+
+ if (obj && obj.AliasTarget) {
+ if (aliasProperty && obj.AliasTarget[aliasProperty] !== undefined) {
+ condition.parsed = obj.AliasTarget[aliasProperty];
+ } else if (!aliasProperty) {
+ condition.parsed = obj.AliasTarget;
+ } else {
+ condition.parsed = 'not set';
+ }
+ } else {
+ condition.parsed = 'not set';
+ }
+ } else {
+ condition.parsed = parse(obj, condition.property)[0];
+ }
+
+ if ((typeof condition.parsed !== 'boolean' && !condition.parsed) || condition.parsed === 'not set'){
+ conditionResult = 2;
+ message.push(`${property}: not set to any value`);
+
+ let resultObj = {
+ status: conditionResult,
+ message: message.join(', ')
+ };
+
+ inputResultsArr.push(resultObj);
+ return resultObj;
+ }
+
+ if (property.includes('[*]') && !isAliasTargetProperty) {
+ if (Array.isArray(condition.parsed)) {
+ if (!Array.isArray(nestedResultArr)) nestedResultArr = [];
+ let propertyArr = property.split('.');
+ propertyArr.shift();
+ property = propertyArr.join('.');
+ condition.property = property;
+ if (condition.op !== 'CONTAINS' || condition.op !== 'NOTCONTAINS') {
+ condition.parsed.forEach(parsed => {
+ if (property.includes('[*]')) {
+ runValidation(parsed, condition, inputResultsArr, nestedResultArr);
+ } else {
+ let localConditionResult = validate(condition, conditionResult, inputResultsArr, message, property, parsed);
+ nestedResultArr.push(localConditionResult);
+ }
+ });
+ } else {
+ runValidation(condition.parsed, condition, inputResultsArr, nestedResultArr);
+ }
+ // NestedCompositeResult
+ if (nestedResultArr && nestedResultArr.length) {
+ if (!condition.nested) condition.nested = 'ONE';
+ let resultObj;
+ if ((condition.nested.toUpperCase() === 'ONE' && nestedResultArr.indexOf(0) > -1) || (condition.nested.toUpperCase() === 'ALL' && nestedResultArr.indexOf(2) === 0)) {
+ resultObj = {
+ status: 0,
+ message: message.join(', ')
+ };
+ } else {
+ resultObj = {
+ status: 2,
+ message: message.join(', ')
+ };
+ }
+
+ inputResultsArr.push(resultObj);
+ return resultObj;
+ }
+ } else {
+ if (!Array.isArray(nestedResultArr)) nestedResultArr = [];
+ let propertyArr = property.split('.');
+ propertyArr.shift();
+ property = propertyArr.join('.');
+ let localConditionResult = validate(condition, conditionResult, inputResultsArr, message, condition.property, condition.parsed);
+
+ let resultObj = {
+ status: localConditionResult,
+ message: message.join(', ')
+ };
+
+ inputResultsArr.push(resultObj);
+ return resultObj;
+ }
+ } else {
+ // Transform the property if required
+ conditionResult = validate(condition, conditionResult, inputResultsArr, message, property, obj);
+ if (conditionResult) result = conditionResult;
+ }
+ }
+
+ if (!message.length) {
+ message = ['The resource matched all required conditions'];
+ }
+
+ let resultObj = {
+ status: result,
+ message: message.join(', ')
+ };
+
+ inputResultsArr.push(resultObj);
+ return resultObj;
+};
+
+var runConditions = function(input, data, results, resourcePath, resourceName, region, cloud, accountId) {
+ let dataToValidate;
+ let newPath;
+ let newData;
+ let validated;
+ let parsedResource = resourceName;
+
+ let inputResultsArr = [];
+ let logical;
+ let localInput = JSON.parse(JSON.stringify(input));
+
+ // to check if top level * matches. ex: Instances[*] should be
+ // present in each condition if not its impossible to compare resources
+ let resourceConditionArr = [];
+
+ localInput.conditions.forEach(condition => {
+ logical = condition.logical;
+ var conditionPropArr = condition.property.split('.');
+
+ // Special handling for ResourceRecordSets[*].AliasTarget.* properties
+ if (condition.property && condition.property.includes('ResourceRecordSets[*].AliasTarget')) {
+ let foundMatch = false;
+ let matchResults = [];
+ let nonMatchResults = [];
+
+ if (data && data.ResourceRecordSets && Array.isArray(data.ResourceRecordSets)) {
+ // Directly access ResourceRecordSets if it exists at the top level
+ for (let i = 0; i < data.ResourceRecordSets.length; i++) {
+ let record = data.ResourceRecordSets[i];
+ if (record && record.AliasTarget) {
+ // Extract just the AliasTarget part of the property path
+ const aliasProperty = condition.property.split('AliasTarget.')[1];
+
+ if (aliasProperty && record.AliasTarget[aliasProperty]) {
+ let propValue = record.AliasTarget[aliasProperty];
+ let result = 2; // Default to fail
+ let message = '';
+
+ // Perform the actual comparison
+ if (condition.op === 'CONTAINS' && propValue.includes(condition.value)) {
+ result = 0;
+ message = `${aliasProperty}: ${condition.value} found in ${propValue}`;
+ foundMatch = true;
+ matchResults.push({
+ status: result,
+ message: message,
+ resource: record.Name || resourceName
+ });
+ } else if (condition.op === 'NOTCONTAINS' && !propValue.includes(condition.value)) {
+ result = 0;
+ message = `${aliasProperty}: ${condition.value} not found in ${propValue}`;
+ foundMatch = true;
+ matchResults.push({
+ status: result,
+ message: message,
+ resource: record.Name || resourceName
+ });
+ } else if (condition.op === 'EQ' && propValue === condition.value) {
+ result = 0;
+ message = `${aliasProperty}: ${propValue} matched: ${condition.value}`;
+ foundMatch = true;
+ matchResults.push({
+ status: result,
+ message: message,
+ resource: record.Name || resourceName
+ });
+ } else if (condition.op === 'NE' && propValue !== condition.value) {
+ result = 0;
+ message = `${aliasProperty}: ${propValue} is not: ${condition.value}`;
+ foundMatch = true;
+ matchResults.push({
+ status: result,
+ message: message,
+ resource: record.Name || resourceName
+ });
+ } else if (condition.op === 'GT') {
+ // Convert to numbers for comparison if they are numeric strings
+ let parsedVal = Number(propValue);
+ let comparisonVal = Number(condition.value);
+
+ if (!isNaN(parsedVal) && !isNaN(comparisonVal) && parsedVal > comparisonVal) {
+ result = 0;
+ message = `${aliasProperty}: ${propValue} was greater than: ${condition.value}`;
+ foundMatch = true;
+ matchResults.push({
+ status: result,
+ message: message,
+ resource: record.Name || resourceName
+ });
+ } else {
+ message = `${aliasProperty}: ${propValue} was not greater than: ${condition.value}`;
+ nonMatchResults.push({
+ status: 2,
+ message: message,
+ resource: record.Name || resourceName
+ });
+ }
+ } else if (condition.op === 'LT') {
+ // Convert to numbers for comparison if they are numeric strings
+ let parsedVal = Number(propValue);
+ let comparisonVal = Number(condition.value);
+
+ if (!isNaN(parsedVal) && !isNaN(comparisonVal) && parsedVal < comparisonVal) {
+ result = 0;
+ message = `${aliasProperty}: ${propValue} was less than: ${condition.value}`;
+ foundMatch = true;
+ matchResults.push({
+ status: result,
+ message: message,
+ resource: record.Name || resourceName
+ });
+ } else {
+ message = `${aliasProperty}: ${propValue} was not less than: ${condition.value}`;
+ nonMatchResults.push({
+ status: 2,
+ message: message,
+ resource: record.Name || resourceName
+ });
+ }
+ } else if (condition.op === 'ISTRUE') {
+ if (typeof propValue === 'boolean' && propValue === true) {
+ result = 0;
+ message = `${aliasProperty} is true`;
+ foundMatch = true;
+ matchResults.push({
+ status: result,
+ message: message,
+ resource: record.Name || resourceName
+ });
+ } else if (typeof propValue === 'string' &&
+ (propValue.toLowerCase() === 'true' || propValue === '1')) {
+ result = 0;
+ message = `${aliasProperty} is true (${propValue})`;
+ foundMatch = true;
+ matchResults.push({
+ status: result,
+ message: message,
+ resource: record.Name || resourceName
+ });
+ } else {
+ message = `${aliasProperty} is not true`;
+ nonMatchResults.push({
+ status: 2,
+ message: message,
+ resource: record.Name || resourceName
+ });
+ }
+ } else if (condition.op === 'ISFALSE') {
+ if (typeof propValue === 'boolean' && propValue === false) {
+ result = 0;
+ message = `${aliasProperty} is false`;
+ foundMatch = true;
+ matchResults.push({
+ status: result,
+ message: message,
+ resource: record.Name || resourceName
+ });
+ } else if (typeof propValue === 'string' &&
+ (propValue.toLowerCase() === 'false' || propValue === '0')) {
+ result = 0;
+ message = `${aliasProperty} is false (${propValue})`;
+ foundMatch = true;
+ matchResults.push({
+ status: result,
+ message: message,
+ resource: record.Name || resourceName
+ });
+ } else {
+ message = `${aliasProperty} is not false`;
+ nonMatchResults.push({
+ status: 2,
+ message: message,
+ resource: record.Name || resourceName
+ });
+ }
+ } else if (condition.op === 'EXISTS') {
+ result = 0;
+ message = `${aliasProperty}: set to ${propValue}`;
+ foundMatch = true;
+ matchResults.push({
+ status: result,
+ message: message,
+ resource: record.Name || resourceName
+ });
+ } else if (condition.op === 'MATCHES' && new RegExp(condition.value).test(propValue)) {
+ result = 0;
+ message = `${aliasProperty}: ${propValue} matches the regex: ${condition.value}`;
+ foundMatch = true;
+ matchResults.push({
+ status: result,
+ message: message,
+ resource: record.Name || resourceName
+ });
+ } else if (condition.op === 'NOTMATCHES' && !new RegExp(condition.value).test(propValue)) {
+ result = 0;
+ message = `${aliasProperty}: ${propValue} does not match the regex: ${condition.value}`;
+ foundMatch = true;
+ matchResults.push({
+ status: result,
+ message: message,
+ resource: record.Name || resourceName
+ });
+ } else {
+ if (condition.op === 'CONTAINS') {
+ message = `${condition.value} not found in ${propValue}`;
+ } else if (condition.op === 'NOTCONTAINS') {
+ message = `${condition.value} found in ${propValue}`;
+ } else if (condition.op === 'EQ') {
+ message = `${aliasProperty}: ${propValue} did not match: ${condition.value}`;
+ } else if (condition.op === 'NE') {
+ message = `${aliasProperty}: ${propValue} is: ${condition.value}`;
+ } else if (condition.op === 'GT') {
+ message = `${aliasProperty}: ${propValue} was not greater than: ${condition.value}`;
+ } else if (condition.op === 'LT') {
+ message = `${aliasProperty}: ${propValue} was not less than: ${condition.value}`;
+ } else if (condition.op === 'ISTRUE') {
+ message = `${aliasProperty} is not true`;
+ } else if (condition.op === 'ISFALSE') {
+ message = `${aliasProperty} is not false`;
+ } else if (condition.op === 'MATCHES') {
+ message = `${aliasProperty}: ${propValue} does not match the regex: ${condition.value}`;
+ } else if (condition.op === 'NOTMATCHES') {
+ message = `${aliasProperty}: ${propValue} matches the regex: ${condition.value}`;
+ }
+
+ nonMatchResults.push({
+ status: 2,
+ message: message,
+ resource: record.Name || resourceName
+ });
+ }
+ } else if (!aliasProperty) {
+ // Handle the entire AliasTarget object
+ matchResults.push({
+ status: 0,
+ message: `AliasTarget: exists for record ${record.Name}`,
+ resource: record.Name || resourceName
+ });
+ foundMatch = true;
+ }
+ }
+ }
+ }
+
+ // After checking all records, add the appropriate results to inputResultsArr
+ if (foundMatch) {
+ // If any record matched, add all matching results
+ matchResults.forEach(result => {
+ inputResultsArr.push({
+ status: result.status,
+ message: result.message
+ });
+ parsedResource = result.resource;
+ });
+ } else {
+ // If no records matched, add a failure result
+ if (nonMatchResults.length > 0) {
+ // Use the first non-matching result as the representative failure
+ inputResultsArr.push({
+ status: 2,
+ message: nonMatchResults[0].message
+ });
+ parsedResource = nonMatchResults[0].resource;
+ } else {
+ // No records with AliasTarget found
+ inputResultsArr.push({
+ status: 2,
+ message: `No matching records with AliasTarget.${condition.property.split('AliasTarget.')[1] || ''} found`
+ });
+ }
+ }
+ } else if (condition.property && condition.property.includes('[*]')) {
+ if (conditionPropArr.length > 1 && conditionPropArr[1].includes('[*]')) {
+ resourceConditionArr.push(conditionPropArr[0]);
+ var firstProperty = conditionPropArr.shift();
+ dataToValidate = parse(data, firstProperty.split('[*]')[0])[0];
+ condition.property = conditionPropArr.join('.');
+ if (dataToValidate && dataToValidate.length) {
+ dataToValidate.forEach(newData => {
+ condition.validated = runValidation(newData, condition, inputResultsArr);
+ parsedResource = parse(newData, resourcePath, region, cloud, accountId, resourceName)[0];
+ if (typeof parsedResource !== 'string' || parsedResource === 'not set') parsedResource = resourceName;
+ });
+ } else {
+ condition.validated = runValidation([], condition, inputResultsArr);
+ parsedResource = parse([], resourcePath, region, cloud, accountId, resourceName)[0];
+ if (typeof parsedResource !== 'string' || parsedResource === 'not set') parsedResource = resourceName;
+ }
+ // result per resource
+ } else {
+ dataToValidate = parse(data, condition.property);
+ newPath = dataToValidate[1];
+ newData = dataToValidate[0];
+ if (newPath && newData && newData.length){
+ newData.forEach(dataElm =>{
+ if (newPath) condition.property = JSON.parse(JSON.stringify(newPath));
+ condition.validated = runValidation(dataElm, condition, inputResultsArr);
+ // Use the Name property as resource if available (common in Route53)
+ parsedResource = dataElm.Name || parse(dataElm, resourcePath, region, cloud, accountId, resourceName)[0];
+ if (typeof parsedResource !== 'string' || parsedResource === 'not set') parsedResource = resourceName;
+ });
+ } else if (newPath && !newData.length) {
+ condition.property = JSON.parse(JSON.stringify(newPath));
+ condition.validated = runValidation(newData, condition, inputResultsArr);
+ parsedResource = parse(newData, resourcePath, region, cloud, accountId, resourceName)[0];
+ if (parsedResource === 'not set' || typeof parsedResource !== 'string') parsedResource = resourceName;
+ } else if (!newPath) {
+ // no path returned. means it has fully parsed and got the value.
+ // save the value
+ newPath = JSON.parse(JSON.stringify(condition.property));
+ if (condition.property.includes('.')){
+ condition.property = condition.property.split('.')[condition.property.split('.').length -1 ];
+ }
+ condition.validated = runValidation(newData, condition, inputResultsArr);
+ condition.property = JSON.parse(JSON.stringify(newPath));
+ parsedResource = parse(newData, resourcePath, region, cloud, accountId, resourceName)[0];
+ if (parsedResource === 'not set' || typeof parsedResource !== 'string') parsedResource = resourceName;
+ }
+ }
+ } else {
+ dataToValidate = parse(data, condition.property);
+ if (dataToValidate.length === 1) {
+ validated = runValidation(data, condition, inputResultsArr);
+ parsedResource = parse(data, resourcePath, region, cloud, accountId, resourceName)[0];
+ if (typeof parsedResource !== 'string' || parsedResource === 'not set') parsedResource = resourceName;
+ } else {
+ newPath = dataToValidate[1];
+ newData = dataToValidate[0];
+ condition.property = newPath;
+ newData.forEach(element =>{
+ condition.validated = runValidation(element, condition, inputResultsArr);
+ parsedResource = parse(data, resourcePath, region, cloud, accountId, resourceName)[0];
+ if (typeof parsedResource !== 'string' || parsedResource === 'not set') parsedResource = null;
+
+ results.push({
+ status: validated.status,
+ resource: parsedResource ? parsedResource : resourceName,
+ message: validated.message,
+ region: region
+ });
+ });
+ }
+ }
+ });
+
+ compositeResult(inputResultsArr, parsedResource, region, results, logical);
+};
+
+var asl = function(source, input, resourceMap, cloud, accountId, callback) {
+ if (!source || !input) return callback('No source or input provided');
+ if (!input.apis || !input.apis[0]) return callback('No APIs provided for input');
+ if (!input.conditions || !input.conditions.length) return callback('No conditions provided for input');
+ let service = input.conditions[0].service;
+ var subService = (input.conditions[0].subservice) ? input.conditions[0].subservice : null;
+ let api = input.conditions[0].api;
+ let resourcePath;
+ if (resourceMap &&
+ resourceMap[service] &&
+ resourceMap[service][api]) {
+ resourcePath = resourceMap[service][api];
+ }
+
+ if (!source[service]) return callback(`Source data did not contain service: ${service}`);
+ if (subService && !source[service][subService]) return callback(`Source data did not contain service: ${service}:${subService}`);
+ if (subService && !source[service][subService][api]) return callback(`Source data did not contain API: ${api}`);
+ if (!subService && !source[service][api]) return callback(`Source data did not contain API: ${api}`);
+
+ var results = [];
+ let data = subService ? source[service][subService][api] : source[service][api];
+
+ for (let region in data) {
+ let regionVal = data[region];
+ if (typeof regionVal !== 'object') continue;
+ if (regionVal.err) {
+ results.push({
+ status: 3,
+ message: regionVal.err.message || 'Error',
+ region: region
+ });
+ } else if (regionVal.data && regionVal.data.length) {
+ regionVal.data.forEach(function(regionData) {
+ var resourceName = parse(regionData, resourcePath, region, cloud, accountId)[0];
+ runConditions(input, regionData, results, resourcePath, resourceName, region, cloud, accountId);
+ });
+ } else if (regionVal.data && Object.keys(regionVal.data).length) {
+ runConditions(input, regionVal.data, results, resourcePath, '', region, cloud, accountId);
+ } else {
+ if (!Object.keys(regionVal).length || (regionVal.data && (!regionVal.data.length || !Object.keys(regionVal.data).length))) {
+ results.push({
+ status: 0,
+ message: 'No resources found in this region',
+ region: region
+ });
+ } else {
+ for (let resourceName in regionVal) {
+ let resourceObj = regionVal[resourceName];
+ if (resourceObj.err || !resourceObj.data) {
+ results.push({
+ status: 3,
+ resource: resourceName,
+ message: resourceObj.err.message || 'Error',
+ region: region
+ });
+ } else {
+ if (resourceObj.data && resourceObj.data.length){
+ resourceObj.data.forEach(function(regionData) {
+ var resourceName = parse(regionData, resourcePath, region, cloud, accountId)[0];
+ runConditions(input, regionData, results, resourcePath, resourceName, region, cloud, accountId);
+ });
+ } else {
+ runConditions(input, resourceObj.data, results, resourcePath, resourceName, region, cloud, accountId);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ callback(null, results, data);
+};
+
+module.exports = asl;
diff --git a/helpers/aws/api.js b/helpers/aws/api.js
index 3cc10d910..8f0dc0758 100644
--- a/helpers/aws/api.js
+++ b/helpers/aws/api.js
@@ -144,15 +144,6 @@ var serviceMap = {
BridgeResourceNameIdentifier: 'DomainName', BridgeExecutionService: 'ES',
BridgeCollectionService: 'es', DataIdentifier: 'DomainStatus',
},
- 'QLDB':
- {
- enabled: true, isSingleSource: true, InvAsset: 'ledger', InvService: 'qldb',
- InvResourceCategory: 'database', InvResourceType: 'qldb_ledger', BridgeServiceName: 'qldb',
- BridgePluginCategoryName: 'QLDB', BridgeProvider: 'aws', BridgeCall: 'describeLedger',
- BridgeArnIdentifier: 'Arn', BridgeIdTemplate: '', BridgeResourceType: 'ledger',
- BridgeResourceNameIdentifier: 'Name', BridgeExecutionService: 'QLDB',
- BridgeCollectionService: 'qldb', DataIdentifier: 'data',
- },
'DynamoDB':
{
enabled: true, isSingleSource: true, InvAsset: 'table', InvService: 'dynamodb',
@@ -216,6 +207,7 @@ var serviceMap = {
BridgeResourceNameIdentifier: 'logGroupName', BridgeExecutionService: 'CloudWatchLogs',
BridgeCollectionService: 'cloudwatchlogs', DataIdentifier: 'data',
},
+
'EventBridge':
{
enabled: true, isSingleSource: true, InvAsset: 'bus', InvService: 'eventbridge',
@@ -225,6 +217,15 @@ var serviceMap = {
BridgeResourceNameIdentifier: 'Name', BridgeExecutionService: 'EventBridge',
BridgeCollectionService: 'eventbridge', DataIdentifier: 'data',
},
+ 'ECR':
+ {
+ enabled: true, isSingleSource: true, InvAsset: 'registry', InvService: 'ecr',
+ InvResourceCategory: 'cloud_resources', InvResourceType: 'ecr_repository',
+ BridgeServiceName: 'ecr', BridgePluginCategoryName: 'ECR', BridgeProvider: 'aws', BridgeCall: 'describeRepositories',
+ BridgeArnIdentifier: 'repositoryArn', BridgeIdTemplate: '', BridgeResourceType: 'repository',
+ BridgeResourceNameIdentifier:'repositoryName' , BridgeExecutionService: 'ECR',
+ BridgeCollectionService: 'ecr', DataIdentifier: 'data',
+ },
'App Mesh':
{
enabled: true, isSingleSource: true, InvAsset: 'mesh', InvService: 'appmesh',
@@ -461,15 +462,6 @@ var serviceMap = {
BridgeResourceNameIdentifier: 'EnvironmentName', BridgeExecutionService: 'ElasticBeanstalk',
BridgeCollectionService: 'elasticbeanstalk', BridgeCall: 'describeEnvironments', DataIdentifier: 'data',
},
- 'Elastic Transcoder':
- {
- enabled: true, isSingleSource: true, InvAsset: 'transcoder', InvService: 'elasticTranscoder',
- InvResourceCategory: 'cloud_resources', InvResourceType: 'transcoder pipeline',
- BridgeProvider: 'aws', BridgeServiceName: 'elastictranscoder', BridgePluginCategoryName: 'Elastic Transcoder',
- BridgeArnIdentifier: 'Arn', BridgeIdTemplate: '', BridgeResourceType: 'pipeline',
- BridgeResourceNameIdentifier: 'Name', BridgeExecutionService: 'Elastic Transcoder',
- BridgeCollectionService: 'elastictranscoder', BridgeCall: 'listPipelines', DataIdentifier: 'data',
- },
'ELBv2':
{
enabled: true, isSingleSource: true, InvAsset: 'loadbalancer', InvService: 'elbv2',
@@ -557,7 +549,7 @@ var serviceMap = {
InvResourceCategory: 'ai&ml', InvResourceType: 'Lookout Metrics',
BridgeProvider: 'aws', BridgeServiceName: 'lookoutmetrics', BridgePluginCategoryName: 'AI & ML',
BridgeArnIdentifier: 'AnomalyDetectorArn', BridgeIdTemplate: '',
- BridgeResourceType: 'lookoutmetrics', BridgeResourceNameIdentifier: 'AnomalyDetectorName', BridgeExecutionService: 'AI & ML',
+ BridgeResourceType: 'AnomalyDetector', BridgeResourceNameIdentifier: 'AnomalyDetectorName', BridgeExecutionService: 'AI & ML',
BridgeCollectionService: 'lookoutmetrics', BridgeCall: 'listAnomalyDetectors', DataIdentifier: 'data',
},
{
@@ -569,6 +561,123 @@ var serviceMap = {
BridgeCollectionService: 'sagemaker', BridgeCall: 'describeNotebookInstance', DataIdentifier: 'data',
},
],
+ 'Guard Duty':
+ {
+ enabled: true, isSingleSource: true, InvAsset: 'detector', InvService: 'guardduty',
+ InvResourceCategory: 'cloud_resources', InvResourceType: 'Guardduty Detector',
+ BridgeProvider: 'aws', BridgeServiceName: 'guardduty', BridgePluginCategoryName: 'GuardDuty',
+ BridgeArnIdentifier: '', BridgeIdTemplate: 'arn:aws:guardduty:{region}:{cloudAccount}:detector/{id}', BridgeResourceType: 'detector',
+ BridgeResourceNameIdentifier: 'id', BridgeExecutionService: 'GuardDuty',
+ BridgeCollectionService: 'guardduty', BridgeCall: 'getDetector', DataIdentifier: 'data',
+ },
+ 'WorkSpaces':
+ {
+ enabled: true, isSingleSource: true, InvAsset: 'instance', InvService: 'workspaces',
+ InvResourceCategory: 'cloud_resources', InvResourceType: 'WorkSpace Instance',
+ BridgeProvider: 'aws', BridgeServiceName: 'workspaces', BridgePluginCategoryName: 'WorkSpaces',
+ BridgeArnIdentifier: '', BridgeIdTemplate: 'arn:aws:workspaces:{region}:{cloudAccount}:workspace/{name}', BridgeResourceType: 'workspace',
+ BridgeResourceNameIdentifier: 'WorkspaceId', BridgeExecutionService: 'WorkSpaces',
+ BridgeCollectionService: 'workspaces', BridgeCall: 'describeWorkspaces', DataIdentifier: 'data',
+ },
+ 'Transfer':
+ {
+ enabled: true, isSingleSource: true, InvAsset: 'server', InvService: 'transfer',
+ InvResourceCategory: 'cloud_resources', InvResourceType: 'Transfer Server',
+ BridgeProvider: 'aws', BridgeServiceName: 'transfer', BridgePluginCategoryName: 'Transfer',
+ BridgeArnIdentifier: 'Arn', BridgeIdTemplate: '', BridgeResourceType: 'server',
+ BridgeResourceNameIdentifier: 'ServerId', BridgeExecutionService: 'Transfer',
+ BridgeCollectionService: 'transfer', BridgeCall: 'listServers', DataIdentifier: 'data',
+ },
+ 'AppFlow':
+ {
+ enabled: true, isSingleSource: true, InvAsset: 'flow', InvService: 'appflow',
+ InvResourceCategory: 'cloud_resources', InvResourceType: 'Appflow',
+ BridgeProvider: 'aws', BridgeServiceName: 'appflow', BridgePluginCategoryName: 'AppFlow',
+ BridgeArnIdentifier: 'flowArn', BridgeIdTemplate: '', BridgeResourceType: 'flow',
+ BridgeResourceNameIdentifier: 'flowName', BridgeExecutionService: 'AppFlow',
+ BridgeCollectionService: 'appflow', BridgeCall: 'listFlows', DataIdentifier: 'data',
+ },
+ 'Cognito':
+ {
+ enabled: true, isSingleSource: true, InvAsset: 'userpool', InvService: 'cognitoidentityserviceprovider',
+ InvResourceCategory: 'cloud_resources', InvResourceType: 'Cognito Userpool',
+ BridgeProvider: 'aws', BridgeServiceName: 'cognitoidentityserviceprovider', BridgePluginCategoryName: 'Cognito',
+ BridgeArnIdentifier: '', BridgeIdTemplate: 'arn:aws:cognito-idp:{region}:{cloudAccount}:userpool/{id}', BridgeResourceType: 'userpool',
+ BridgeResourceNameIdentifier: 'Id', BridgeExecutionService: 'Cognito',
+ BridgeCollectionService: 'cognitoidentityserviceprovider', BridgeCall: 'listUserPools', DataIdentifier: 'data',
+ },
+ 'WAF':
+ {
+ enabled: true, isSingleSource: true, InvAsset: 'webacl', InvService: 'wafv2',
+ InvResourceCategory: 'cloud_resources', InvResourceType: 'Web ACL',
+ BridgeProvider: 'aws', BridgeServiceName: 'wafv2', BridgePluginCategoryName: 'WAF',
+ BridgeArnIdentifier: 'ARN', BridgeIdTemplate: '', BridgeResourceType: 'webacl',
+ BridgeResourceNameIdentifier: 'Id', BridgeExecutionService: 'WAF',
+ BridgeCollectionService: 'wafv2', BridgeCall: 'listWebACLs', DataIdentifier: 'data',
+ },
+ 'Glue':
+ {
+ enabled: true, isSingleSource: true, InvAsset: 'glue', InvService: 'glue',
+ InvResourceCategory: 'cloud_resources', InvResourceType: 'Glue SecurityConfigurations',
+ BridgeProvider: 'aws', BridgeServiceName: 'glue', BridgePluginCategoryName: 'Glue',
+ BridgeArnIdentifier: '', BridgeIdTemplate: 'arn:aws:glue:{region}:{cloudAccount}:/securityConfiguration/{name}', BridgeResourceType: 'securityConfiguration',
+ BridgeResourceNameIdentifier: 'Name', BridgeExecutionService: 'Glue',
+ BridgeCollectionService: 'glue', BridgeCall: 'getSecurityConfigurations', DataIdentifier: 'data',
+ },
+ 'ConfigService':
+ {
+ enabled: true, isSingleSource: true, InvAsset: 'configservice', InvService: 'configservice',
+ InvResourceCategory: 'cloud_resources', InvResourceType: 'ConfigService',
+ BridgeProvider: 'aws', BridgeServiceName: 'configservice', BridgePluginCategoryName: 'ConfigService',
+ BridgeArnIdentifier: 'ConfigRuleArn', BridgeIdTemplate: '', BridgeResourceType: 'config-rule',
+ BridgeResourceNameIdentifier: 'ConfigRuleName', BridgeExecutionService: 'ConfigService',
+ BridgeCollectionService: 'configservice', BridgeCall: 'describeConfigRules', DataIdentifier: 'data',
+ },
+ 'Firehose':
+ {
+ enabled: true, isSingleSource: true, InvAsset: 'firehose', InvService: 'firehose',
+ InvResourceCategory: 'cloud_resources', InvResourceType: 'Firehose',
+ BridgeProvider: 'aws', BridgeServiceName: 'firehose', BridgePluginCategoryName: 'Firehose',
+ BridgeArnIdentifier: 'DeliveryStreamARN', BridgeIdTemplate: '', BridgeResourceType: 'deliverystream',
+ BridgeResourceNameIdentifier: 'DeliveryStreamName', BridgeExecutionService: 'Firehose',
+ BridgeCollectionService: 'firehose', BridgeCall: 'describeDeliveryStream', DataIdentifier: 'DeliveryStreamDescription',
+ },
+ 'SES':
+ {
+ enabled: true, isSingleSource: true, InvAsset: 'ses', InvService: 'SES',
+ InvResourceCategory: 'cloud_resource', InvResourceType: 'ses_emails',
+ BridgeProvider: 'aws', BridgeServiceName: 'ses', BridgePluginCategoryName: 'SES',
+ BridgeArnIdentifier: '', BridgeIdTemplate: 'arn:aws:ses:{region}:{cloudAccount}:identity/{name}', BridgeResourceType: 'identity',
+ BridgeResourceNameIdentifier: 'identityName', BridgeExecutionService: 'SES',
+ BridgeCollectionService: 'ses', BridgeCall: 'getIdentityDkimAttributes', DataIdentifier: 'DkimAttributes',
+ },
+ 'FSx':
+ {
+ enabled: true, isSingleSource: true, InvAsset: 'filesystem', InvService: 'fsx',
+ InvResourceCategory: 'cloud_resources', InvResourceType: 'Fsx Filesystem',
+ BridgeProvider: 'aws', BridgeServiceName: 'fsx', BridgePluginCategoryName: 'FSx',
+ BridgeArnIdentifier: 'ResourceARN', BridgeIdTemplate: '', BridgeResourceType: 'file-system',
+ BridgeResourceNameIdentifier: 'FileSystemId', BridgeExecutionService: 'FSx',
+ BridgeCollectionService: 'fsx', BridgeCall: 'describeFileSystems', DataIdentifier: 'data',
+ },
+ 'OpenSearch': [
+ {
+ enabled: true, isSingleSource: true, InvAsset: 'domain', InvService: 'opensearch',
+ InvResourceCategory: 'database', InvResourceType: 'OpenSearch Domain',
+ BridgeProvider: 'aws', BridgeServiceName: 'opensearch', BridgePluginCategoryName: 'OpenSearch',
+ BridgeArnIdentifier: 'ARN', BridgeIdTemplate: '', BridgeResourceType: 'domain',
+ BridgeResourceNameIdentifier: 'DomainName', BridgeExecutionService: 'OpenSearch',
+ BridgeCollectionService: 'opensearch', BridgeCall: 'describeDomain', DataIdentifier: 'DomainStatus',
+ },
+ {
+ enabled: true, isSingleSource: true, InvAsset: 'collection', InvService: 'opensearch',
+ InvResourceCategory: 'database', InvResourceType: 'OpenSearch Serverless',
+ BridgeProvider: 'aws', BridgeServiceName: 'opensearchserverless', BridgePluginCategoryName: 'OpenSearch',
+ BridgeArnIdentifier: 'arn', BridgeIdTemplate: '', BridgeResourceType: 'collection',
+ BridgeResourceNameIdentifier: 'name', BridgeExecutionService: 'OpenSearch',
+ BridgeCollectionService: 'opensearchserverless', BridgeCall: 'listCollections', DataIdentifier: 'data',
+ },
+ ],
};
var calls = {
@@ -1146,14 +1255,6 @@ var calls = {
paginate: 'NextToken'
}
},
- ElasticTranscoder: {
- // TODO: Pagination via NextPageToken and PageToken
- listPipelines: {
- property: 'Pipelines',
- paginate: 'NextPageToken',
- paginateReqProp: 'PageToken'
- }
- },
ELB: {
describeLoadBalancers: {
property: 'LoadBalancerDescriptions',
@@ -1479,12 +1580,6 @@ var calls = {
paginate: 'nextToken'
}
},
- QLDB: {
- listLedgers: {
- property: 'Ledgers',
- paginate: 'NextToken'
- }
- },
RDS: {
describeDBInstances: {
property: 'DBInstances',
@@ -1818,6 +1913,21 @@ var postcalls = [
IoTSiteWise: {
sendIntegration: serviceMap['IoT SiteWise']
},
+ Workspaces: {
+ sendIntegration: serviceMap['WorkSpaces']
+ },
+ Transfer: {
+ sendIntegration: serviceMap['Transfer']
+ },
+ Glue: {
+ sendIntegration: serviceMap['Glue'],
+ },
+ SecurityHub: {
+ sendIntegration: serviceMap['SecurityHub']
+ },
+ FSx:{
+ sendIntegration: serviceMap['FSx']
+ },
ACM: {
describeCertificate: {
@@ -1825,7 +1935,10 @@ var postcalls = [
reliesOnCall: 'listCertificates',
filterKey: 'CertificateArn',
filterValue: 'CertificateArn'
- }
+ },
+ sendIntegration: {
+ enabled: true
+ },
},
AccessAnalyzer: {
listFindings: {
@@ -1913,7 +2026,8 @@ var postcalls = [
reliesOnCall: 'listFlows',
filterKey: 'flowName',
filterValue: 'flowName'
- }
+ },
+ sendIntegration: serviceMap['AppFlow']
},
Athena: {
getWorkGroup: {
@@ -2102,7 +2216,8 @@ var postcalls = [
reliesOnCall: 'describeConfigRules',
filterKey: 'ConfigRuleName',
filterValue: 'ConfigRuleName'
- }
+ },
+ sendIntegration: serviceMap['ConfigService']
},
CodeStar: {
describeProject: {
@@ -2178,6 +2293,12 @@ var postcalls = [
filterKey: 'ResourceName',
filterValue: 'DBClusterArn'
},
+ describeDBClusterParameters: {
+ reliesOnService: 'docdb',
+ reliesOnCall: 'describeDBClusters',
+ filterKey: 'DBClusterParameterGroupName',
+ filterValue: 'DBClusterParameterGroup'
+ },
sendIntegration: serviceMap['DocumentDB']
},
DynamoDB: {
@@ -2227,7 +2348,8 @@ var postcalls = [
reliesOnCall: 'listDomainNames',
filterKey: 'DomainName',
filterValue: 'DomainName'
- }
+ },
+ sendIntegration: serviceMap['OpenSearch'][0]
},
S3: {
getBucketLogging: {
@@ -2342,7 +2464,8 @@ var postcalls = [
reliesOnCall: 'listUserPools',
filterKey: 'UserPoolId',
filterValue: 'Id'
- }
+ },
+ sendIntegration: serviceMap['Cognito']
},
EC2: {
describeSubnets: {
@@ -2388,9 +2511,7 @@ var postcalls = [
filterKey: 'resourceArn',
filterValue: 'repositoryArn'
},
- sendIntegration: {
- enabled: true
- }
+ sendIntegration: serviceMap['ECR']
},
ECRPUBLIC: {
describeRepositories: {
@@ -2437,15 +2558,6 @@ var postcalls = [
},
sendIntegration: serviceMap['ElasticBeanstalk']
},
- ElasticTranscoder: {
- listJobsByPipeline: {
- reliesOnService: 'elastictranscoder',
- reliesOnCall: 'listPipelines',
- filterKey: 'PipelineId',
- filterValue: 'Id'
- },
- sendIntegration: serviceMap['Elastic Transcoder']
- },
ELB: {
describeLoadBalancerPolicies: {
reliesOnService: 'elb',
@@ -2645,7 +2757,8 @@ var postcalls = [
reliesOnService: 'firehose',
reliesOnCall: 'listDeliveryStreams',
override: true
- }
+ },
+ sendIntegration: serviceMap['Firehose'],
},
KMS: {
describeKey: {
@@ -2684,7 +2797,7 @@ var postcalls = [
reliesOnCall: 'listFunctions',
filterKey: 'FunctionName',
filterValue: 'FunctionName',
- rateLimit: 100, // it's not documented but experimentially 10/second works.
+ rateLimit: 100, // it's not documented but experimental 10/second works.
},
getFunction: {
reliesOnService: 'lambda',
@@ -2766,15 +2879,6 @@ var postcalls = [
filterValue: 'botAliasId'
}
},
- QLDB: {
- describeLedger: {
- reliesOnService: 'qldb',
- reliesOnCall: 'listLedgers',
- filterKey: 'Name',
- filterValue: 'Name'
- },
- sendIntegration: serviceMap['QLDB']
- },
ManagedBlockchain: {
listMembers: {
reliesOnService: 'managedblockchain',
@@ -2897,7 +3001,8 @@ var postcalls = [
reliesOnCall: 'listIdentities',
override: true,
rateLimit: 1000
- }
+ },
+ sendIntegration: serviceMap['SES']
},
SNS: {
getTopicAttributes: {
@@ -2951,7 +3056,8 @@ var postcalls = [
reliesOnService: 'wafv2',
reliesOnCall: 'listWebACLs',
override: true
- }
+ },
+ sendIntegration: serviceMap['WAF']
},
GuardDuty: {
getDetector: {
@@ -2974,6 +3080,7 @@ var postcalls = [
reliesOnCall: 'listDetectors',
override: true,
},
+ sendIntegration: serviceMap['Guard Duty'],
},
},
{
@@ -3130,7 +3237,8 @@ var postcalls = [
reliesOnService: 'opensearchserverless',
reliesOnCall: 'listNetworkSecurityPolicies',
override: true
- }
+ },
+ sendIntegration: serviceMap['OpenSearch'][1]
}
}
];
diff --git a/helpers/aws/api_multipart.js b/helpers/aws/api_multipart.js
index 4590b8ffb..b4576bf39 100644
--- a/helpers/aws/api_multipart.js
+++ b/helpers/aws/api_multipart.js
@@ -573,14 +573,6 @@ var calls = [
paginate: 'NextToken'
}
},
- ElasticTranscoder: {
- // TODO: Pagination via NextPageToken and PageToken
- listPipelines: {
- property: 'Pipelines',
- paginate: 'NextPageToken',
- paginateReqProp: 'PageToken'
- }
- },
ELB: {
describeLoadBalancers: {
property: 'LoadBalancerDescriptions',
@@ -862,12 +854,6 @@ var calls = [
paginate: 'nextToken'
}
},
- QLDB: {
- listLedgers: {
- property: 'Ledgers',
- paginate: 'NextToken'
- }
- },
RDS: {
describeDBInstances: {
property: 'DBInstances',
@@ -1444,6 +1430,12 @@ var postcalls = [
filterKey: 'ResourceName',
filterValue: 'DBClusterArn'
},
+ describeDBClusterParameters: {
+ reliesOnService: 'docdb',
+ reliesOnCall: 'describeDBClusters',
+ filterKey: 'DBClusterParameterGroupName',
+ filterValue: 'DBClusterParameterGroup'
+ },
},
DynamoDB: {
describeTable: {
@@ -1666,14 +1658,6 @@ var postcalls = [
override: true
}
},
- ElasticTranscoder: {
- listJobsByPipeline: {
- reliesOnService: 'elastictranscoder',
- reliesOnCall: 'listPipelines',
- filterKey: 'PipelineId',
- filterValue: 'Id'
- }
- },
ELB: {
describeLoadBalancerPolicies: {
reliesOnService: 'elb',
@@ -2018,7 +2002,7 @@ var postcalls = [
reliesOnCall: 'listFunctions',
filterKey: 'FunctionName',
filterValue: 'FunctionName',
- rateLimit: 500, // it's not documented but experimentally 10/second works.
+ rateLimit: 500, // it's not documented but experimental 10/second works.
},
getFunction: {
reliesOnService: 'lambda',
@@ -2092,14 +2076,6 @@ var postcalls = [
filterValue: 'botId'
}
},
- QLDB: {
- describeLedger: {
- reliesOnService: 'qldb',
- reliesOnCall: 'listLedgers',
- filterKey: 'Name',
- filterValue: 'Name'
- }
- },
ManagedBlockchain: {
listMembers: {
reliesOnService: 'managedblockchain',
diff --git a/helpers/aws/functions.js b/helpers/aws/functions.js
index cbbdce4f0..4824e94f8 100644
--- a/helpers/aws/functions.js
+++ b/helpers/aws/functions.js
@@ -32,7 +32,6 @@ function waitForCredentialReport(iam, callback, CREDENTIAL_DOWNLOAD_STARTED) {
//return callback(CREDENTIAL_REPORT_ERROR);
return callback('Error downloading report');
}
-
//CREDENTIAL_REPORT_DATA = reportData;
//callback(null, CREDENTIAL_REPORT_DATA);
callback(null, reportData);
@@ -335,6 +334,9 @@ function crossAccountPrincipal(principal, accountId, fetchPrincipals, settings={
}
function hasFederatedUserRole(policyDocument) {
+ if (!policyDocument || !Array.isArray(policyDocument)) {
+ return false;
+ }
// true iff every statement refers to federated user access
for (let statement of policyDocument) {
if (statement.Action &&
@@ -785,10 +787,20 @@ function remediateOpenPorts(putCall, pluginName, protocol, port, config, cache,
return rCb(err);
} else {
if (openIpv6Range && !localIpV6Exists) {
- remediation_file['remediate']['actions'][pluginName][resource]['steps'].push({
- 'inboundRule': '::1/128',
- 'action': 'ADDED'
- });
+ if (settings.input && settings.input[ipv6InputKey]) {
+ const newIpv6CidrRange = settings.input[ipv6InputKey].split(',');
+ for (const cidr of newIpv6CidrRange) {
+ remediation_file['remediate']['actions'][pluginName][resource]['steps'].push({
+ 'inboundRule': cidr,
+ 'action': 'ADDED'
+ });
+ }
+ } else {
+ remediation_file['remediate']['actions'][pluginName][resource]['steps'].push({
+ 'inboundRule': '::1/128',
+ 'action': 'ADDED'
+ });
+ }
} else if (openIpv6Range && localIpV6Exists) {
remediation_file['remediate']['actions'][pluginName][resource]['steps'].push({
'inboundRule': '::1/128',
@@ -797,10 +809,20 @@ function remediateOpenPorts(putCall, pluginName, protocol, port, config, cache,
}
if (openIpRange && !localIpExists) {
- remediation_file['remediate']['actions'][pluginName][resource]['steps'].push({
- 'inboundRule': '127.0.0.1/32',
- 'action': 'ADDED'
- });
+ if (settings.input && settings.input[ipv4InputKey]) {
+ const newIpCidrRange = settings.input[ipv4InputKey].split(',');
+ for (const cidr of newIpCidrRange) {
+ remediation_file['remediate']['actions'][pluginName][resource]['steps'].push({
+ 'inboundRule': cidr,
+ 'action': 'ADDED'
+ });
+ }
+ } else {
+ remediation_file['remediate']['actions'][pluginName][resource]['steps'].push({
+ 'inboundRule': '127.0.0.1/32',
+ 'action': 'ADDED'
+ });
+ }
} else if (openIpRange && localIpExists){
remediation_file['remediate']['actions'][pluginName][resource]['steps'].push({
'inboundRule': '127.0.0.1/32',
@@ -1126,8 +1148,10 @@ var checkTags = function(cache, resourceName, resourceList, region, results, set
['resourcegroupstaggingapi', 'getResources', region]);
if (!allResources || allResources.err || !allResources.data) {
- helpers.addResult(results, 3,
- 'Unable to query all resources from group tagging api:' + helpers.addError(allResources), region);
+ resourceList.map(arn => {
+ helpers.addResult(results, 3,
+ 'Unable to query all resources from group tagging api:' + helpers.addError(allResources), region, arn);
+ });
return;
}
var awsOrGov = defaultPartition(settings);
@@ -1239,7 +1263,6 @@ var getAttachedELBs = function(cache, source, region, resourceId, lbField, lbAt
return elbs;
};
-
var checkNetworkExposure = function(cache, source, subnets, securityGroups, elbs, region, results, resource) {
var internetExposed = '';
var isSubnetPrivate = false;
@@ -1249,41 +1272,41 @@ var checkNetworkExposure = function(cache, source, subnets, securityGroups, elbs
if (resource.functionUrlConfig && resource.functionUrlConfig.data) {
if (resource.functionUrlConfig.data.AuthType === 'NONE') {
internetExposed += 'public function URL';
- } else if (resource.functionUrlConfig.data.AuthType === 'AWS_IAM' &&
- resource.functionPolicy && resource.functionPolicy.data) {
+ } else if (resource.functionUrlConfig.data.AuthType === 'AWS_IAM' &&
+ resource.functionPolicy && resource.functionPolicy.data) {
let authConfig = resource.functionPolicy.data;
if (authConfig.Policy) {
let statements = normalizePolicyDocument(authConfig.Policy);
-
+
if (statements) {
let hasDenyAll = false;
let hasPublicAllow = false;
let hasRestrictiveConditions = false;
-
+
for (let statement of statements) {
// Check for explicit deny statements first
if (statement.Effect === 'Deny') {
// Check if there's a deny for all principals
- if ((!statement.Condition || Object.keys(statement.Condition).length === 0) &&
+ if ((!statement.Condition || Object.keys(statement.Condition).length === 0) &&
globalPrincipal(statement.Principal)) {
hasDenyAll = true;
break;
}
-
+
// Check for deny with IP restrictions
- if (statement.Condition &&
- (statement.Condition['NotIpAddress'] ||
- statement.Condition['IpAddress'])) {
+ if (statement.Condition &&
+ (statement.Condition['NotIpAddress'] ||
+ statement.Condition['IpAddress'])) {
hasRestrictiveConditions = true;
}
} else if (statement.Effect === 'Allow') {
// Skip if the statement doesn't include relevant Lambda actions
- if (!statement.Action ||
- (!Array.isArray(statement.Action) ?
+ if (!statement.Action ||
+ (!Array.isArray(statement.Action) ?
!statement.Action.includes('lambda:InvokeFunctionUrl') :
- !statement.Action.some(action =>
- action === '*' ||
- action === 'lambda:*' ||
+ !statement.Action.some(action =>
+ action === '*' ||
+ action === 'lambda:*' ||
action === 'lambda:InvokeFunctionUrl'
))) {
continue;
@@ -1303,17 +1326,17 @@ var checkNetworkExposure = function(cache, source, subnets, securityGroups, elbs
'aws:PrincipalArn',
'aws:SourceAccount'
];
-
- const hasRestriction = restrictiveConditions.some(condition =>
- Object.keys(statement.Condition).some(key =>
+
+ const hasRestriction = restrictiveConditions.some(condition =>
+ Object.keys(statement.Condition).some(key =>
key.toLowerCase().includes(condition.toLowerCase())
)
);
-
+
if (hasRestriction) {
hasRestrictiveConditions = true;
} else if (statement.Condition['StringEquals'] &&
- statement.Condition['StringEquals']['lambda:FunctionUrlAuthType'] === 'NONE') {
+ statement.Condition['StringEquals']['lambda:FunctionUrlAuthType'] === 'NONE') {
hasPublicAllow = true;
}
}
@@ -1323,8 +1346,8 @@ var checkNetworkExposure = function(cache, source, subnets, securityGroups, elbs
// Only mark as exposed if we have a public allow and no restrictions
if (hasPublicAllow && !hasDenyAll && !hasRestrictiveConditions) {
- internetExposed += internetExposed.length ?
- ', function URL with global IAM access' :
+ internetExposed += internetExposed.length ?
+ ', function URL with global IAM access' :
'function URL with global IAM access';
}
}
@@ -1352,19 +1375,19 @@ var checkNetworkExposure = function(cache, source, subnets, securityGroups, elbs
['apigateway', 'getIntegration', region, api.id]);
if (!getIntegration || getIntegration.err || !Object.keys(getIntegration).length) continue;
-
+
for (let apiResource of Object.values(getIntegration)) {
// Check if any integration points to this Lambda function
let lambdaIntegrations = Object.values(apiResource).filter(integration => {
- return integration && integration.data && (integration.data.type === 'AWS' || integration.data.type === 'AWS_PROXY') &&
- integration.data.uri &&
+ return integration && integration.data && (integration.data.type === 'AWS' || integration.data.type === 'AWS_PROXY') &&
+ integration.data.uri &&
integration.data.uri.includes(resource.functionArn);
});
if (lambdaIntegrations.length) {
internetExposed += internetExposed.length ? `, API Gateway ${api.name}` : `API Gateway ${api.name}`;
}
- }
+ }
}
}
}
@@ -1375,7 +1398,7 @@ var checkNetworkExposure = function(cache, source, subnets, securityGroups, elbs
}
if (!resource.functionArn) {
- // Scenario 1: check if resource is in a private subnet
+ // Scenario 1: check if resource is in a private subnet
let subnetRouteTableMap, privateSubnets;
var describeSubnets = helpers.addSource(cache, source,
['ec2', 'describeSubnets', region]);
@@ -1500,15 +1523,13 @@ var checkNetworkExposure = function(cache, source, subnets, securityGroups, elbs
if (elbs && elbs.length) {
if (!describeSecurityGroups || !describeSecurityGroups.data) {
describeSecurityGroups = helpers.addSource(cache, source,
- ['ec2', 'describeSecurityGroups', region]);
+ ['ec2', 'describeSecurityGroups', region]);
}
elbs.forEach(lb => {
let isLBPublic = false;
if (lb.Scheme && lb.Scheme.toLowerCase() === 'internet-facing') {
if (lb.SecurityGroups && lb.SecurityGroups.length) {
- var describeSecurityGroups = helpers.addSource(cache, source,
- ['ec2', 'describeSecurityGroups', region]);
if (describeSecurityGroups &&
!describeSecurityGroups.err && describeSecurityGroups.data && describeSecurityGroups.data.length) {
let elbSGs = describeSecurityGroups.data.filter(sg => lb.SecurityGroups.includes(sg.GroupId));
@@ -1533,7 +1554,7 @@ var checkNetworkExposure = function(cache, source, subnets, securityGroups, elbs
let getLambdaTargetELBs = function(cache, source, region) {
let lambdaELBMap = {};
-
+
var describeLoadBalancersv2 = helpers.addSource(cache, source,
['elbv2', 'describeLoadBalancers', region]);
@@ -1545,18 +1566,18 @@ let getLambdaTargetELBs = function(cache, source, region) {
var describeTargetGroups = helpers.addSource(cache, source,
['elbv2', 'describeTargetGroups', region, lb.DNSName]);
- if (!describeTargetGroups || describeTargetGroups.err || !describeTargetGroups.data ||
+ if (!describeTargetGroups || describeTargetGroups.err || !describeTargetGroups.data ||
!describeTargetGroups.data.TargetGroups) return;
describeTargetGroups.data.TargetGroups.forEach(tg => {
var describeTargetHealth = helpers.addSource(cache, source,
['elbv2', 'describeTargetHealth', region, tg.TargetGroupArn]);
- if (!describeTargetHealth || describeTargetHealth.err || !describeTargetHealth.data ||
+ if (!describeTargetHealth || describeTargetHealth.err || !describeTargetHealth.data ||
!describeTargetHealth.data.TargetHealthDescriptions) return;
describeTargetHealth.data.TargetHealthDescriptions.forEach(target => {
- if (target.Target && target.Target.Id &&
+ if (target.Target && target.Target.Id &&
target.Target.Id.startsWith('arn:aws:lambda')) {
if (!lambdaELBMap[target.Target.Id]) {
lambdaELBMap[target.Target.Id] = [];
@@ -1567,13 +1588,13 @@ let getLambdaTargetELBs = function(cache, source, region) {
targetGroupArn: tg.TargetGroupArn,
targets: [target.Target]
});
-
+
// Check if there's an active listener for this target group
let hasListener = false;
var describeListeners = helpers.addSource(cache, source,
['elbv2', 'describeListeners', region, lb.DNSName]);
-
- if (describeListeners && describeListeners.data &&
+
+ if (describeListeners && describeListeners.data &&
describeListeners.data.Listeners) {
hasListener = describeListeners.data.Listeners.some(listener =>
listener.DefaultActions.some(action =>
@@ -1581,7 +1602,7 @@ let getLambdaTargetELBs = function(cache, source, region) {
)
);
}
-
+
if (hasListener) {
lambdaELBMap[target.Target.Id].push(lb);
}
diff --git a/helpers/aws/regions.js b/helpers/aws/regions.js
index a1590baba..a7bf75855 100644
--- a/helpers/aws/regions.js
+++ b/helpers/aws/regions.js
@@ -158,7 +158,7 @@ module.exports = {
qldb: ['us-east-1', 'us-east-2', 'us-west-2', 'ap-northeast-2',
'ap-southeast-1', 'ap-southeast-2', 'ap-northeast-1', 'ca-central-1', 'eu-central-1',
'eu-west-1', 'eu-west-2'],
- finspace: ['us-east-1', 'us-east-2', 'us-west-2', 'ca-central-1','eu-central-1', 'eu-west-1', 'eu-west-2', 'ap-northeast-1', 'ap-southeast-1', 'ap-southeast-2'],
+ finspace: ['us-east-1', 'us-east-2', 'us-west-2', 'ca-central-1', 'eu-west-1'],
codepipeline: [...regions, 'ap-south-2', 'ap-southeast-4', 'eu-south-2', 'eu-central-2', 'il-central-1'],
codeartifact: ['us-east-1', 'us-east-2', 'us-west-2', 'eu-central-1',
'eu-west-1', 'eu-west-2', 'eu-west-3', 'eu-north-1', 'eu-south-1',
diff --git a/helpers/azure/api.js b/helpers/azure/api.js
index baf16480c..2f71d0678 100644
--- a/helpers/azure/api.js
+++ b/helpers/azure/api.js
@@ -53,7 +53,7 @@ var serviceMap = {
BridgeResourceNameIdentifier: 'name', BridgeExecutionService: 'Redis Cache',
BridgeCollectionService: 'rediscaches', DataIdentifier: 'data',
},
- 'CDN Profiles':
+ 'CDN Profiles': [
{
enabled: true, isSingleSource: true, InvAsset: 'cdnProfiles', InvService: 'cdnProfiles',
InvResourceCategory: 'cloud_resources', InvResourceType: 'CDN_Profiles', BridgeServiceName: 'profiles',
@@ -62,6 +62,15 @@ var serviceMap = {
BridgeResourceNameIdentifier: 'name', BridgeExecutionService: 'CDN Profiles',
BridgeCollectionService: 'profiles', DataIdentifier: 'data',
},
+ {
+ enabled: true, isSingleSource: true, InvAsset: 'endpoint', InvService: 'cdnProfiles',
+ InvResourceCategory: 'cloud_resources', InvResourceType: 'Endpoints', BridgeServiceName: 'endpoints',
+ BridgePluginCategoryName: 'CDN Profiles', BridgeProvider: 'Azure', BridgeCall: 'listByProfile',
+ BridgeArnIdentifier: '', BridgeIdTemplate: '', BridgeResourceType: 'endpoints',
+ BridgeResourceNameIdentifier: 'name', BridgeExecutionService: 'CDN Profiles',
+ BridgeCollectionService: 'endpoints', DataIdentifier: 'data',
+ }
+ ],
'Cosmos DB':
{
enabled: true, isSingleSource: true, InvAsset: 'cosmosdb', InvService: 'cosmosDB',
@@ -116,7 +125,7 @@ var serviceMap = {
BridgeResourceNameIdentifier: 'displayName', BridgeExecutionService: 'Azure Policy',
BridgeCollectionService: 'policyassignments', DataIdentifier: 'data',
},
- 'Virtual Networks':
+ 'Virtual Networks':[
{
enabled: true, isSingleSource: true, InvAsset: 'virtual_network', InvService: 'virtual_network',
InvResourceCategory: 'cloud_resources', InvResourceType: 'Virtual Network', BridgeServiceName: 'virtualnetworks',
@@ -125,6 +134,15 @@ var serviceMap = {
BridgeResourceNameIdentifier: 'name', BridgeExecutionService: 'Virtual Networks',
BridgeCollectionService: 'virtualnetworks', DataIdentifier: 'data',
},
+ {
+ enabled: true, isSingleSource: true, InvAsset: 'vn_routeTables', InvService: 'virtual_network',
+ InvResourceCategory: 'cloud_resources', InvResourceType: 'VN_RouteTables', BridgeServiceName: 'routetables',
+ BridgePluginCategoryName: 'Virtual Networks', BridgeProvider: 'Azure', BridgeCall: 'listAll',
+ BridgeArnIdentifier: '', BridgeIdTemplate: '', BridgeResourceType: 'routeTables',
+ BridgeResourceNameIdentifier: 'name', BridgeExecutionService: 'Virtual Networks',
+ BridgeCollectionService: 'routetables', DataIdentifier: 'data',
+ }
+ ],
'Queue Service':
{
enabled: true, isSingleSource: true, InvAsset: 'queueService', InvService: 'queueService',
@@ -170,6 +188,96 @@ var serviceMap = {
BridgeResourceNameIdentifier: 'name', BridgeExecutionService: 'AI & ML',
BridgeCollectionService: 'openai', BridgeCall: 'listAccounts', DataIdentifier: 'data',
},
+ 'Blob Service':
+ {
+ enabled: true, isSingleSource: true, InvAsset: 'blob_container', InvService: 'blobservice',
+ InvResourceCategory: 'cloud_resources', InvResourceType: 'blob_container', BridgeServiceName: 'blobcontainers',
+ BridgePluginCategoryName: 'Blob Service', BridgeProvider: 'Azure', BridgeCall: 'list',
+ BridgeArnIdentifier: '', BridgeIdTemplate: '', BridgeResourceType: 'containers',
+ BridgeResourceNameIdentifier: 'name', BridgeExecutionService: 'Blob Service',
+ BridgeCollectionService: 'blobcontainers', DataIdentifier: 'data',
+ },
+ 'Virtual Machines':
+ {
+ enabled: true, isSingleSource: true, InvAsset: 'vm_scaleset', InvService: 'virtualmachines',
+ InvResourceCategory: 'cloud_resources', InvResourceType: 'VM_ScaleSet', BridgeServiceName: 'virtualmachinescalesets',
+ BridgePluginCategoryName: 'Virtual Machines', BridgeProvider: 'Azure', BridgeCall: 'listAll',
+ BridgeArnIdentifier: '', BridgeIdTemplate: '', BridgeResourceType: 'virtualMachineScaleSets',
+ BridgeResourceNameIdentifier: 'name', BridgeExecutionService: 'Virtual Machines',
+ BridgeCollectionService: 'virtualmachinescalesets', DataIdentifier: 'data',
+ },
+ 'Event Grid':
+ {
+ enabled: true, isSingleSource: true, InvAsset: 'domain', InvService: 'eventgrid',
+ InvResourceCategory: 'cloud_resources', InvResourceType: 'EventGrid Domain', BridgeServiceName: 'eventgrid',
+ BridgePluginCategoryName: 'Event Grid', BridgeProvider: 'Azure', BridgeCall: 'listDomains',
+ BridgeArnIdentifier: '', BridgeIdTemplate: '', BridgeResourceType: 'domains',
+ BridgeResourceNameIdentifier: 'name', BridgeExecutionService: 'Event Grid',
+ BridgeCollectionService: 'eventgrid', DataIdentifier: 'data',
+ },
+ 'Event Hubs':
+ {
+ enabled: true, isSingleSource: true, InvAsset: 'namespace', InvService: 'eventhubs',
+ InvResourceCategory: 'cloud_resources', InvResourceType: 'Eventhubs Namespace', BridgeServiceName: 'eventhub',
+ BridgePluginCategoryName: 'Event Hubs', BridgeProvider: 'Azure', BridgeCall: 'listEventHub',
+ BridgeArnIdentifier: '', BridgeIdTemplate: '', BridgeResourceType: 'namespaces',
+ BridgeResourceNameIdentifier: 'name', BridgeExecutionService: 'Event Hubs',
+ BridgeCollectionService: 'eventhub', DataIdentifier: 'data',
+ },
+ 'Defender': [
+ {
+ enabled: true, isSingleSource: true, InvAsset: 'defender', InvService: 'defender',
+ InvResourceCategory: 'cloud_resources', InvResourceType: 'Defender', BridgeServiceName: 'pricings',
+ BridgePluginCategoryName: 'Defender', BridgeProvider: 'Azure', BridgeCall: 'list',
+ BridgeArnIdentifier: '', BridgeIdTemplate: '', BridgeResourceType: 'pricings',
+ BridgeResourceNameIdentifier: 'name', BridgeExecutionService: 'Defender',
+ BridgeCollectionService: 'pricings', DataIdentifier: 'data',
+ },
+ {
+ enabled: true, isSingleSource: true, InvAsset: 'defender', InvService: 'defender',
+ InvResourceCategory: 'cloud_resources', InvResourceType: 'Defender Settings', BridgeServiceName: 'securitycenter',
+ BridgePluginCategoryName: 'Defender', BridgeProvider: 'Azure', BridgeCall: 'list',
+ BridgeArnIdentifier: '', BridgeIdTemplate: '', BridgeResourceType: 'settings',
+ BridgeResourceNameIdentifier: 'name', BridgeExecutionService: 'Defender',
+ BridgeCollectionService: 'securitycenter', DataIdentifier: 'data',
+ }
+ ],
+ 'Application Gateway': [
+ {
+ enabled: true, isSingleSource: true, InvAsset: 'applicationGateway', InvService: 'applicationGateway',
+ InvResourceCategory: 'cloud_resources', InvResourceType: 'Application Gateway', BridgeServiceName: 'applicationgateway',
+ BridgePluginCategoryName: 'Application Gateway', BridgeProvider: 'Azure', BridgeCall: 'listAll',
+ BridgeArnIdentifier: '', BridgeIdTemplate: '', BridgeResourceType: 'applicationGateways',
+ BridgeResourceNameIdentifier: 'name', BridgeExecutionService: 'Application Gateway',
+ BridgeCollectionService: 'applicationgateway', DataIdentifier: 'data',
+ },
+ {
+ enabled: true, isSingleSource: true, InvAsset: 'policy', InvService: 'applicationGateway',
+ InvResourceCategory: 'cloud_resources', InvResourceType: 'wafpolicies', BridgeServiceName: 'wafpolicies',
+ BridgePluginCategoryName: 'Application Gateway', BridgeProvider: 'Azure', BridgeCall: 'listAll',
+ BridgeArnIdentifier: '', BridgeIdTemplate: '', BridgeResourceType: 'ApplicationGatewayWebApplicationFirewallPolicies',
+ BridgeResourceNameIdentifier: 'name', BridgeExecutionService: 'Application Gateway',
+ BridgeCollectionService: 'wafpolicies', DataIdentifier: 'data',
+ }
+ ],
+ 'Entra ID': [
+ {
+ enabled: true, isSingleSource: true, InvAsset: 'entraId', InvService: 'entraId',
+ InvResourceCategory: 'cloud_resources', InvResourceType: 'Roles', BridgeServiceName: 'roledefinitions',
+ BridgePluginCategoryName: 'Entra ID', BridgeProvider: 'Azure', BridgeCall: 'list',
+ BridgeArnIdentifier: '', BridgeIdTemplate: '', BridgeResourceType: 'roleDefinitions',
+ BridgeResourceNameIdentifier: 'name', BridgeExecutionService: 'Entra ID',
+ BridgeCollectionService: 'roledefinitions', DataIdentifier: 'data',
+ },
+ {
+ enabled: true, isSingleSource: true, InvAsset: 'entraId', InvService: 'entraId',
+ InvResourceCategory: 'cloud_resources', InvResourceType: 'Application', BridgeServiceName: 'applications',
+ BridgePluginCategoryName: 'Entra ID', BridgeProvider: 'Azure', BridgeCall: 'list',
+ BridgeArnIdentifier: '', BridgeIdTemplate: '', BridgeResourceType: '',
+ BridgeResourceNameIdentifier: 'name', BridgeExecutionService: 'Entra ID',
+ BridgeCollectionService: 'applications', DataIdentifier: 'data',
+ }
+ ]
};
// Standard calls that contain top-level operations
@@ -215,7 +323,7 @@ var calls = {
listAll: {
url: 'https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.Network/virtualNetworks?api-version=2020-03-01'
},
- sendIntegration: serviceMap['Virtual Networks']
+ sendIntegration: serviceMap['Virtual Networks'][0]
},
natGateways: {
listBySubscription: {
@@ -267,7 +375,7 @@ var calls = {
},
vaults: {
list: {
- url: 'https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.KeyVault/vaults?api-version=2019-09-01'
+ url: 'https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.KeyVault/vaults?api-version=2023-07-01'
},
sendIntegration: serviceMap['Key Vaults'],
},
@@ -290,7 +398,8 @@ var calls = {
routeTables: {
listAll: {
url: 'https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.Network/routeTables?api-version=2022-07-01'
- }
+ },
+ sendIntegration: serviceMap['Virtual Networks'][1]
},
managedClusters: {
list: {
@@ -338,7 +447,7 @@ var calls = {
list: {
url: 'https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.Cdn/profiles?api-version=2024-02-01'
},
- sendIntegration: serviceMap['CDN Profiles']
+ sendIntegration: serviceMap['CDN Profiles'][0]
},
autoProvisioningSettings: {
list: {
@@ -348,7 +457,8 @@ var calls = {
applicationGateway: {
listAll: {
url: 'https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.Network/applicationGateways?api-version=2022-07-01'
- }
+ },
+ sendIntegration: serviceMap['Application Gateway'][0]
},
securityContacts: {
list: {
@@ -375,7 +485,8 @@ var calls = {
roleDefinitions: {
list: {
url: 'https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.Authorization/roleDefinitions?api-version=2015-07-01'
- }
+ },
+ sendIntegration: serviceMap['Entra ID'][0]
},
managementLocks: {
listAtSubscriptionLevel: {
@@ -407,7 +518,8 @@ var calls = {
list: {
url: 'https://graph.microsoft.com/v1.0/applications/',
graph: true,
- }
+ },
+ sendIntegration: serviceMap['Entra ID'][1]
},
automationAccounts: {
list: {
@@ -422,7 +534,8 @@ var calls = {
pricings: {
list: {
url: 'https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.Security/pricings?api-version=2018-06-01'
- }
+ },
+ sendIntegration: serviceMap['Defender'][0]
},
availabilitySets: {
listBySubscription: {
@@ -432,7 +545,8 @@ var calls = {
virtualMachineScaleSets: {
listAll: {
url: 'https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.Compute/virtualMachineScaleSets?api-version=2023-07-01'
- }
+ },
+ sendIntegration: serviceMap['Virtual Machines']
},
bastionHosts: {
listAll: {
@@ -442,7 +556,8 @@ var calls = {
wafPolicies: {
listAll: {
url: 'https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.Network/ApplicationGatewayWebApplicationFirewallPolicies?api-version=2022-07-01'
- }
+ },
+ sendIntegration: serviceMap['Application Gateway'][1]
},
autoscaleSettings: {
listBySubscription: {
@@ -462,7 +577,7 @@ var calls = {
url: 'https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.DBforMySQL/servers?api-version=2017-12-01'
},
listMysqlFlexibleServer: {
- url : 'https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.DBforMySQL/flexibleServers?api-version=2021-05-01'
+ url : 'https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.DBforMySQL/flexibleServers?api-version=2023-12-30'
},
listPostgres: {
url: 'https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.DBforPostgreSQL/servers?api-version=2017-12-01'
@@ -480,7 +595,8 @@ var calls = {
securityCenter: {
list: {
url: 'https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.Security/settings?api-version=2021-06-01'
- }
+ },
+ sendIntegration: serviceMap['Defender'][1]
},
publicIPAddresses: {
listAll: {
@@ -500,12 +616,14 @@ var calls = {
eventGrid: {
listDomains: {
url: 'https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.EventGrid/domains?api-version=2023-12-15-preview'
- }
+ },
+ sendIntegration: serviceMap['Event Grid']
},
eventHub: {
listEventHub: {
url: 'https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.EventHub/namespaces?api-version=2022-10-01-preview'
- }
+ },
+ sendIntegration: serviceMap['Event Hubs']
},
serviceBus: {
listNamespacesBySubscription: {
@@ -815,7 +933,8 @@ var postcalls = {
url: 'https://management.azure.com/{id}/blobServices/default/containers?api-version=2019-06-01',
rateLimit: 3000,
limit: 20000
- }
+ },
+ sendIntegration: serviceMap['Blob Service']
},
blobServices: {
list: {
@@ -891,6 +1010,14 @@ var postcalls = {
properties: ['id'],
url: 'https://management.azure.com/{id}/config/backup/list?api-version=2021-02-01',
post: true
+ },
+ getWebAppDetails: {
+ reliesOnPath: 'webApps.list',
+ properties: ['id'],
+ url: 'https://management.azure.com/{id}?api-version=2022-03-01'
+ },
+ sendIntegration: {
+ enabled: true
}
},
containerApps: {
@@ -905,7 +1032,8 @@ var postcalls = {
reliesOnPath: 'profiles.list',
properties: ['id'],
url: 'https://management.azure.com/{id}/endpoints?api-version=2019-04-15'
- }
+ },
+ sendIntegration: serviceMap['CDN Profiles'][1]
},
customDomain: {
listByFrontDoorProfiles: {
@@ -976,6 +1104,11 @@ var postcalls = {
reliesOnPath: 'servers.listPostgresFlexibleServer',
properties: ['id'],
url: 'https://management.azure.com/{id}/firewallRules?api-version=2022-12-01'
+ },
+ listByFlexibleServerMysql: {
+ reliesOnPath: 'servers.listMysqlFlexibleServer',
+ properties: ['id'],
+ url: 'https://management.azure.com/{id}/firewallRules?api-version=2021-05-01'
}
},
outboundFirewallRules: {
@@ -1131,6 +1264,13 @@ var postcalls = {
url: 'https://management.azure.com/{id}/encryptionScopes?api-version=2023-01-01'
}
},
+ eventHub: {
+ listNetworkRuleSet: {
+ reliesOnPath: 'eventHub.listEventHub',
+ properties: ['id'],
+ url: 'https://management.azure.com/{id}/networkRuleSets/default?api-version=2022-10-01-preview'
+ }
+ }
};
var tertiarycalls = {
diff --git a/helpers/azure/auth.js b/helpers/azure/auth.js
index d4ad17a45..992794134 100644
--- a/helpers/azure/auth.js
+++ b/helpers/azure/auth.js
@@ -1,5 +1,5 @@
-var request = require('request');
var locations = require(__dirname + '/locations.js');
+var axios = require('axios');
var locations_gov = require(__dirname + '/locations_gov.js');
var dontReplace = {
@@ -36,57 +36,66 @@ module.exports = {
if (!azureConfig.KeyValue) return callback('No KeyValue provided');
if (!azureConfig.DirectoryID) return callback('No DirectoryID provided');
if (!azureConfig.SubscriptionID) return callback('No SubscriptionID provided');
+ var { ClientSecretCredential } = require('@azure/identity');
- var msRestAzure = require('ms-rest-azure');
-
- function performLogin(tokenAudience, cb) {
- msRestAzure.loginWithServicePrincipalSecret(
- azureConfig.ApplicationID,
- azureConfig.KeyValue,
- azureConfig.DirectoryID,
- tokenAudience, function(err, credentials) {
- if (err) return cb(err);
- if (!credentials) return cb('Unable to log into Azure using provided credentials.');
- if (!credentials.environment) return cb('Unable to obtain environment from Azure application');
- if (!credentials.tokenCache ||
- !credentials.tokenCache._entries ||
- !credentials.tokenCache._entries[0] ||
- !credentials.tokenCache._entries[0].accessToken) {
- return cb('Unable to obtain token from Azure.');
- }
-
- cb(null, credentials);
+ function getToken(credential, scopes, cb) {
+ credential.getToken(scopes)
+ .then(response => {
+ cb(null, response.token);
+ })
+ .catch(error => {
+ cb(error);
});
}
+ const credential = new ClientSecretCredential(
+ azureConfig.DirectoryID,
+ azureConfig.ApplicationID,
+ azureConfig.KeyValue
+ );
+
if (azureConfig.Govcloud) {
- performLogin({ environment: msRestAzure.AzureEnvironment.AzureUSGovernment }, function(err, credentials) {
+ const armScope = 'https://management.usgovcloudapi.net/.default';
+ const graphScope = 'https://graph.microsoft.us/.default';
+ const vaultScope = 'https://vault.azure.us/.default';
+
+ getToken(credential, [armScope], function(err, armToken) {
if (err) return callback(err);
- performLogin({ tokenAudience: 'https://graph.microsoft.us', environment: msRestAzure.AzureEnvironment.AzureUSGovernment }, function(graphErr, graphCredentials) {
+ getToken(credential, [graphScope], function(graphErr, graphToken) {
if (graphErr) return callback(graphErr);
- performLogin({ tokenAudience: 'https://vault.azure.us', environment: msRestAzure.AzureEnvironment.AzureUSGovernment }, function(vaultErr, vaultCredentials) {
+ getToken(credential, [vaultScope], function(vaultErr, vaultToken) {
if (vaultErr) console.log('No vault');
callback(null, {
- environment: credentials.environment,
- token: credentials.tokenCache._entries[0].accessToken,
- graphToken: graphCredentials ? graphCredentials.tokenCache._entries[0].accessToken : null,
- vaultToken: vaultCredentials ? vaultCredentials.tokenCache._entries[0].accessToken : null
+ environment: {
+ name: 'AzureUSGovernment',
+ portalUrl: 'https://portal.azure.us'
+ },
+ token: armToken,
+ graphToken: graphToken,
+ vaultToken: vaultToken
});
});
});
});
} else {
- performLogin(null, function(err, credentials) {
+ const armScope = 'https://management.azure.com/.default';
+ const graphScope = 'https://graph.microsoft.com/.default';
+ const vaultScope = 'https://vault.azure.net/.default';
+
+ getToken(credential, [armScope], function(err, armToken) {
if (err) return callback(err);
- performLogin({ tokenAudience: 'https://graph.microsoft.com' }, function(graphErr, graphCredentials) {
+ getToken(credential, [graphScope], function(graphErr, graphToken) {
if (graphErr) return callback(graphErr);
- performLogin({ tokenAudience: 'https://vault.azure.net' }, function(vaultErr, vaultCredentials) {
+ getToken(credential, [vaultScope], function(vaultErr, vaultToken) {
if (vaultErr) return callback(vaultErr);
callback(null, {
- environment: credentials.environment,
- token: credentials.tokenCache._entries[0].accessToken,
- graphToken: graphCredentials.tokenCache._entries[0].accessToken,
- vaultToken: vaultCredentials.tokenCache._entries[0].accessToken
+ environment: {
+ name: 'AzureCloud',
+ portalUrl: 'https://portal.azure.com'
+ },
+ token: armToken,
+ graphToken: graphToken,
+ vaultToken: vaultToken
});
});
});
@@ -99,89 +108,162 @@ module.exports = {
'Authorization': `Bearer ${params.token}`
};
+ var requestData = null;
if (params.body && Object.keys(params.body).length) {
- headers['Content-Length'] = JSON.stringify(params.body).length;
+ requestData = JSON.stringify(params.body);
+ headers['Content-Length'] = requestData.length;
headers['Content-Type'] = 'application/json;charset=UTF-8';
}
if (params.govcloud) params.url = params.url.replace('management.azure.com', 'management.usgovcloudapi.net');
-
- request({
+ var axiosOptions = {
method: params.method ? params.method : params.post ? 'POST' : 'GET',
- uri: params.url,
+ url: params.url,
headers: headers,
- body: params.body ? JSON.stringify(params.body) : null
- }, function(error, response, body) {
- if (response && [200, 202].includes(response.statusCode) && body) {
+ data: requestData,
+ // Handle response as text first, then parse manually to match original behavior
+ transformResponse: [(data) => data]
+ };
+
+ axios(axiosOptions)
+ .then(function(response) {
+ var body = response.data;
+
+ if (response && [200, 202].includes(response.status) && body) {
+ try {
+ body = JSON.parse(body);
+ } catch (e) {
+ return callback(`Error parsing response from Azure API: ${e}`);
+ }
+ return callback(null, body);
+ } else {
+ handleErrorResponse(body, response, callback);
+ }
+ })
+ .catch(function(error) {
+ if (error.response) {
+ // The request was made and the server responded with a status code outside 2xx
+ handleErrorResponse(error.response.data, error.response, callback);
+ } else if (error.request) {
+ // The request was made but no response was received
+ if (error.code === 'ECONNRESET') {
+ console.log('[ERROR] Unhandled error from Azure API: Error: ECONNRESET');
+ return callback('Unknown error occurred while calling the Azure API: ECONNRESET');
+ }
+ console.log(`[ERROR] Unhandled error from Azure API: Error: ${error}`);
+ return callback('Unknown error occurred while calling the Azure API');
+ } else {
+ // Something happened in setting up the request
+ console.log(`[ERROR] Unhandled error from Azure API: Error: ${error}`);
+ return callback('Unknown error occurred while calling the Azure API');
+ }
+ });
+
+ function handleErrorResponse(body, response, callback) {
+ if (body) {
try {
body = JSON.parse(body);
} catch (e) {
- return callback(`Error parsing response from Azure API: ${e}`);
+ return callback(`Error parsing error response from Azure API: ${e}`);
}
- return callback(null, body);
- } else {
- if (body) {
+
+ if (typeof body == 'string') {
+ // Need to double parse it
try {
body = JSON.parse(body);
} catch (e) {
- return callback(`Error parsing error response from Azure API: ${e}`);
+ return callback(`Error parsing error response string from Azure API: ${e}`);
}
+ }
- if (typeof body == 'string') {
- // Need to double parse it
- try {
- body = JSON.parse(body);
- } catch (e) {
- return callback(`Error parsing error response string from Azure API: ${e}`);
- }
+ if (response && ((response.statusCode && response.statusCode === 429) || (response.status && response.status === 429)) &&
+ body &&
+ body.error &&
+ body.error.message &&
+ typeof body.error.message == 'string') {
+ var errorMessage = `TooManyRequests: ${body.error.message}`;
+ return callback(errorMessage, null, response);
+ } else if (body &&
+ body.error &&
+ body.error.message &&
+ typeof body.error.message == 'string') {
+ return callback(body.error.message);
+ } else if (body &&
+ body['odata.error'] &&
+ body['odata.error'].message &&
+ body['odata.error'].message.value &&
+ typeof body['odata.error'].message.value == 'string') {
+ if (body['odata.error'].requestId) {
+ body['odata.error'].message.value += ` RequestId: ${body['odata.error'].requestId}`;
}
- if (response &&
- response.statusCode &&
- response.statusCode === 429 &&
- body &&
- body.error &&
- body.error.message &&
- typeof body.error.message == 'string') {
- var errorMessage = `TooManyRequests: ${body.error.message}`;
- return callback(errorMessage, null, response);
- } else if (body &&
- body.error &&
- body.error.message &&
- typeof body.error.message == 'string') {
- return callback(body.error.message);
- } else if (body &&
- body['odata.error'] &&
- body['odata.error'].message &&
- body['odata.error'].message.value &&
- typeof body['odata.error'].message.value == 'string') {
- if (body['odata.error'].requestId) {
- body['odata.error'].message.value += ` RequestId: ${body['odata.error'].requestId}`;
- }
- return callback(body['odata.error'].message.value);
- } else if (body &&
- body.message &&
- typeof body.message == 'string') {
- if (body.code && typeof body.code == 'string') {
- body.message = (body.code + ': ' + body.message);
- }
- return callback(body.message);
- } else if (body &&
- body.Message &&
- typeof body.Message == 'string') {
- if (body.Code && typeof body.Code == 'string') {
- body.Message = (body.Code + ': ' + body.Message);
- }
- return callback(body.Message);
+ return callback(body['odata.error'].message.value);
+ } else if (body &&
+ body.message &&
+ typeof body.message == 'string') {
+ if (body.code && typeof body.code == 'string') {
+ body.message = (body.code + ': ' + body.message);
+ }
+ return callback(body.message);
+ } else if (body &&
+ body.Message &&
+ typeof body.Message == 'string') {
+ if (body.Code && typeof body.Code == 'string') {
+ body.Message = (body.Code + ': ' + body.Message);
+ }
+ return callback(body.Message);
+ }
+ if (typeof body == 'string') {
+ // Need to double parse it
+ try {
+ body = JSON.parse(body);
+ } catch (e) {
+ return callback(`Error parsing error response string from Azure API: ${e}`);
}
-
- console.log(`[ERROR] Unhandled error from Azure API: Body: ${JSON.stringify(body)}`);
+ }
+ if (response && ((response.statusCode && response.statusCode === 429) || (response.status && response.status === 429)) &&
+ body &&
+ body.error &&
+ body.error.message &&
+ typeof body.error.message == 'string') {
+ errorMessage = `TooManyRequests: ${body.error.message}`;
+ return callback(errorMessage, null, response);
+ } else if (body &&
+ body.error &&
+ body.error.message &&
+ typeof body.error.message == 'string') {
+ return callback(body.error.message);
+ } else if (body &&
+ body['odata.error'] &&
+ body['odata.error'].message &&
+ body['odata.error'].message.value &&
+ typeof body['odata.error'].message.value == 'string') {
+ if (body['odata.error'].requestId) {
+ body['odata.error'].message.value += ` RequestId: ${body['odata.error'].requestId}`;
+ }
+ return callback(body['odata.error'].message.value);
+ } else if (body &&
+ body.message &&
+ typeof body.message == 'string') {
+ if (body.code && typeof body.code == 'string') {
+ body.message = (body.code + ': ' + body.message);
+ }
+ return callback(body.message);
+ } else if (body &&
+ body.Message &&
+ typeof body.Message == 'string') {
+ if (body.Code && typeof body.Code == 'string') {
+ body.Message = (body.Code + ': ' + body.Message);
+ }
+ return callback(body.Message);
}
- console.log(`[ERROR] Unhandled error from Azure API: Error: ${error}`);
- return callback('Unknown error occurred while calling the Azure API');
+ console.log(`[ERROR] Unhandled error from Azure API: Body: ${JSON.stringify(body)}`);
}
- });
+
+ console.log('[ERROR] Unhandled error from Azure API');
+ return callback('Unknown error occurred while calling the Azure API');
+ }
},
addLocations: function(obj, service, collection, err, data, skip_locations) {
@@ -207,7 +289,8 @@ module.exports = {
}
});
},
- addGovLocations: function(obj, service, collection, err, data , skip_locations) {
+
+ addGovLocations: function(obj, service, collection, err, data, skip_locations) {
if (!service || !locations_gov[service]) return;
locations_gov[service].forEach(function(location) {
if (skip_locations.includes(location)) return;
@@ -232,5 +315,4 @@ module.exports = {
},
reduceProperties: reduceProperties
-};
-
+};
\ No newline at end of file
diff --git a/helpers/azure/functions.js b/helpers/azure/functions.js
index 03212126e..1a390e58a 100644
--- a/helpers/azure/functions.js
+++ b/helpers/azure/functions.js
@@ -775,21 +775,29 @@ function checkSecurityGroup(securityGroups) {
return {exposed: true};
}
-function checkNetworkExposure(cache, source, networkInterfaces, securityGroups, location, results, lbNames) {
+function checkNetworkExposure(cache, source, networkInterfaces, securityGroups, location, results, attachedResources, resource) {
let exposedPath = '';
- if (securityGroups && securityGroups.length) {
- // Scenario 1: check if security group allow all inbound traffic
- let exposedSG = checkSecurityGroup(securityGroups);
- if (exposedSG && exposedSG.exposed) {
- if (exposedSG.nsg) {
- return `nsg ${exposedSG.nsg}`
- } else {
- return '';
+ const isFunctionApp = resource && resource.kind &&
+ resource.kind.toLowerCase().includes('functionapp');
+
+ if (!isFunctionApp) {
+ if (securityGroups && securityGroups.length) {
+ // Scenario 1: check if security group allow all inbound traffic
+ let exposedSG = checkSecurityGroup(securityGroups);
+ if (exposedSG && exposedSG.exposed) {
+ if (exposedSG.nsg) {
+ return `nsg ${exposedSG.nsg}`
+ } else {
+ return '';
+ }
}
}
}
+
+ const { applicationGateways, lbNames, frontDoors } = attachedResources;
+
if (lbNames && lbNames.length) {
const loadBalancers = shared.addSource(cache, source,
['loadBalancers', 'listAll', location]);
@@ -802,18 +810,51 @@ function checkNetworkExposure(cache, source, networkInterfaces, securityGroups,
if (lb.frontendIPConfigurations && lb.frontendIPConfigurations.length) {
isPublic = lb.frontendIPConfigurations.some(ipConfig => ipConfig.properties
&& ipConfig.properties.publicIPAddress && ipConfig.properties.publicIPAddress.id);
- if (isPublic && ((lb.nboundNatRules && nboundNatRules.length) || (lb.loadBalancingRules && lb.loadBalancingRules.length))) {
- exposedPath += `lb ${lb.name}`;
- break;
+ if (isPublic && ((lb.inboundNatRules && lb.inboundNatRules.length) || (lb.loadBalancingRules && lb.loadBalancingRules.length))) {
+ exposedPath += exposedPath.length ? `, lb ${lb.name}` : `lb ${lb.name}`;
}
}
}
}
}
}
+
+
+ if (applicationGateways && applicationGateways.length) {
+ for (const ag of applicationGateways) {
+ if (ag.frontendIPConfigurations && ag.frontendIPConfigurations.some(config => config.publicIPAddress && config.publicIPAddress.id)) {
+ exposedPath += exposedPath.length ? `, ag ${ag.name}` : `ag ${ag.name}`;
+ }
+ }
+ }
+
+ if (frontDoors && frontDoors.length) {
+ for (const fd of frontDoors) {
+ if (!fd.associatedWafPolicies || !fd.associatedWafPolicies.length) {
+ exposedPath += exposedPath.length ? `, fd ${fd.name}` : `fd ${fd.name}`;
+ continue;
+ }
+
+ // Check WAF policies
+ let hasSecureWaf = false;
+ for (const policy of fd.associatedWafPolicies) {
+ if (policy.policySettings &&
+ policy.policySettings.enabledState === 'Enabled' &&
+ policy.policySettings.mode === 'Prevention') {
+ hasSecureWaf = true;
+ break;
+ }
+ }
+
+ if (!hasSecureWaf) {
+ exposedPath += exposedPath.length ? `, fd ${fd.name}` : `fd ${fd.name}`;
+ }
+ }
+ }
+
+
return exposedPath;
}
-
module.exports = {
addResult: addResult,
findOpenPorts: findOpenPorts,
@@ -830,3 +871,4 @@ module.exports = {
checkNetworkExposure: checkNetworkExposure
};
+
diff --git a/helpers/google/api.js b/helpers/google/api.js
index 1d4f99870..31c467b61 100644
--- a/helpers/google/api.js
+++ b/helpers/google/api.js
@@ -190,6 +190,33 @@ var serviceMap = {
BridgeArnIdentifier: '', BridgeIdTemplate: '{name}', BridgeResourceType: 'models',
BridgeResourceNameIdentifier: 'name', BridgeExecutionService: 'gcp-AI & ML',
BridgeCollectionService: 'gcp-vertexai', DataIdentifier: 'data',
+ },
+ 'CloudBuild':
+ {
+ enabled: true, isSingleSource: true, InvAsset: 'trigger', InvService: 'CloudBuild',
+ InvResourceCategory: 'cloud_resources', InvResourceType: 'trigger', BridgeServiceName: 'cloudbuild',
+ BridgePluginCategoryName: 'gcp-CloudBuild', BridgeProvider: 'Google', BridgeCall: 'triggers',
+ BridgeArnIdentifier: '', BridgeIdTemplate: 'projects/{cloudAccount}/locations/{region}/triggers/{name}', BridgeResourceType: 'triggers',
+ BridgeResourceNameIdentifier: 'name', BridgeExecutionService: 'gcp-CloudBuild',
+ BridgeCollectionService: 'gcp-cloudbuild', DataIdentifier: 'data',
+ },
+ 'Cloud Composer':
+ {
+ enabled: true, isSingleSource: true, InvAsset: 'environment', InvService: 'Cloud Composer',
+ InvResourceCategory: 'cloud_resources', InvResourceType: 'composer_environment', BridgeServiceName: 'composer',
+ BridgePluginCategoryName: 'gcp-Cloud Composer', BridgeProvider: 'Google', BridgeCall: 'environments',
+ BridgeArnIdentifier: '', BridgeIdTemplate: '', BridgeResourceType: 'environments',
+ BridgeResourceNameIdentifier: 'name', BridgeExecutionService: 'gcp-Cloud Composer',
+ BridgeCollectionService: 'gcp-composer', DataIdentifier: 'data',
+ },
+ 'Resource Manager':
+ {
+ enabled: true, isSingleSource: true, InvAsset: 'organization', InvService: 'Resource Manager',
+ InvResourceCategory: 'cloud_resources', InvResourceType: 'Organization', BridgeServiceName: 'organizations',
+ BridgePluginCategoryName: 'gcp-Resource Manager', BridgeProvider: 'Google', BridgeCall: 'list',
+ BridgeArnIdentifier: '', BridgeIdTemplate: '', BridgeResourceType: 'organizations',
+ BridgeResourceNameIdentifier: 'name', BridgeExecutionService: 'gcp-Resource Manager',
+ BridgeCollectionService: 'gcp-organizations', DataIdentifier: 'data',
}
};
var calls = {
@@ -212,7 +239,8 @@ var calls = {
pagination: true,
paginationKey: 'pageToken',
dataFilterKey: 'environments'
- }
+ },
+ sendIntegration: serviceMap['Cloud Composer']
},
repositories: {
list: {
@@ -224,6 +252,22 @@ var calls = {
enabled: true
}
},
+ apiGateways: {
+ list: {
+ url: 'https://apigateway.googleapis.com/v1/projects/{projectId}/locations/{locationId}/gateways',
+ location: 'region',
+ dataKey: 'gateways',
+ isDataArray: true
+ }
+ },
+ api: {
+ list: {
+ url: 'https://apigateway.googleapis.com/v1/projects/{projectId}/locations/{locationId}/apis',
+ location: 'region',
+ dataKey: 'apis',
+ isDataArray: true
+ }
+ },
images: {
list: {
url: 'https://compute.googleapis.com/compute/v1/projects/{projectId}/global/images',
@@ -346,6 +390,18 @@ var calls = {
enabled: true
}
},
+ functionsv2: {
+ list: {
+ url: 'https://cloudfunctions.googleapis.com/v2/projects/{projectId}/locations/{locationId}/functions',
+ location: 'region',
+ paginationKey: 'pageSize',
+ pagination: true,
+ dataFilterKey: 'functions'
+ },
+ sendIntegration: {
+ enabled: true
+ }
+ },
keyRings: {
list: {
url: 'https://cloudkms.googleapis.com/v1/projects/{projectId}/locations/{locationId}/keyRings',
@@ -472,7 +528,8 @@ var calls = {
url: 'https://cloudbuild.clients6.google.com/v1/projects/{projectId}/locations/{locationId}/triggers',
location: 'region',
dataFilterKey: 'triggers'
- }
+ },
+ sendIntegration: serviceMap['CloudBuild']
},
managedZones: {
list: {
@@ -620,7 +677,8 @@ var calls = {
listDatasets: {
url: 'https://{locationId}-aiplatform.googleapis.com/v1/projects/{projectId}/locations/{locationId}/datasets',
location: 'region',
- dataKey: 'datasets'
+ dataKey: 'datasets',
+ isDataArray: true
},
listModels: {
url: 'https://{locationId}-aiplatform.googleapis.com/v1/projects/{projectId}/locations/{locationId}/models',
@@ -752,6 +810,24 @@ var postcalls = {
}
},
},
+ apiConfigs: {
+ list: {
+ url: 'https://apigateway.googleapis.com/v1/{name}/configs',
+ location: 'region',
+ reliesOnService: ['api'],
+ reliesOnCall: ['list'],
+ properties: ['name'],
+ }
+ },
+ apiGateways: {
+ getIamPolicy: {
+ url: 'https://apigateway.googleapis.com/v1/{name}:getIamPolicy',
+ location: 'region',
+ reliesOnService: ['apiGateways'],
+ reliesOnCall: ['list'],
+ properties: ['name'],
+ }
+ },
datasets: {
get: {
url: 'https://bigquery.googleapis.com/bigquery/v2/projects/{projectId}/datasets/{datasetId}',
@@ -786,6 +862,17 @@ var postcalls = {
properties: ['name']
}
},
+ functionsv2: {
+ getIamPolicy: {
+ url: 'https://cloudfunctions.googleapis.com/v2/{name}:getIamPolicy',
+ location: null,
+ method: 'POST',
+ reliesOnService: ['functionsv2'],
+ reliesOnCall: ['list'],
+ properties: ['name'],
+ body: { options: { requestedPolicyVersion: 3 } }
+ }
+ },
jobs: {
get: { //https://dataflow.googleapis.com/v1b3/projects/{projectId}/jobs/{jobId}
url: 'https://dataflow.googleapis.com/v1b3/projects/{projectId}/locations/{locationId}/jobs/{id}',
@@ -854,7 +941,8 @@ var postcalls = {
pagination: true,
paginationKey: 'pageSize',
dataKey: 'accessPolicies'
- }
+ },
+ sendIntegration: serviceMap['Resource Manager']
},
folders:{ // https://cloudresourcemanager.googleapis.com/v2/folders
list: {
diff --git a/helpers/google/functions.js b/helpers/google/functions.js
index 18fd8ac7d..f7e0a90b2 100644
--- a/helpers/google/functions.js
+++ b/helpers/google/functions.js
@@ -419,10 +419,10 @@ function checkFirewallRules(firewallRules) {
sourceAddressPrefix.includes('/0') ||
sourceAddressPrefix.toLowerCase() === 'internet' ||
sourceAddressPrefix.includes('/0')
- ): null;
+ ): false;
- var allowed = firewallRule.allowed? firewallRule.allowed.some(allow => !!allow.IPProtocol): null;
- var denied = firewallRule.denied? firewallRule.denied.some(deny => deny.IPProtocol === 'all'): null;
+ var allowed = firewallRule.allowed? firewallRule.allowed.some(allow => !!allow.IPProtocol): false;
+ var denied = firewallRule.denied? firewallRule.denied.some(deny => deny.IPProtocol === 'all'): false;
if (allSources && allowed) {
return {exposed: true, networkName: `vpc ${networkName}`};
}
@@ -450,14 +450,22 @@ function getForwardingRules(cache, source, region, resource) {
return [];
}
- backendServices = backendServices.filter(service => {
- if (service.backends && service.backends.length) {
- return service.backends.some(backend => {
- let group = backend.group.replace(/^.*?(\/projects\/.*)$/, '$1');
- return resource.selfLink.includes(group);
- });
- }
- });
+ if (resource.httpsTrigger && resource.httpsTrigger.url) {
+ backendServices = backendServices.filter(service => {
+ if (service.backends && service.backends.length) {
+ return service.backends.some(backend => backend.target && backend.target.includes(resource.httpsTrigger.url));
+ }
+ });
+ } else {
+ backendServices = backendServices.filter(service => {
+ if (service.backends && service.backends.length) {
+ return service.backends.some(backend => {
+ let group = backend.group.replace(/^.*?(\/projects\/.*)$/, '$1');
+ return (resource.selfLink && resource.selfLink.includes(group));
+ });
+ }
+ });
+ }
if (backendServices && backendServices.length) {
forwardingRules.forEach(rule => {
diff --git a/helpers/google/index.js b/helpers/google/index.js
index 47b1d601b..7654dec0f 100644
--- a/helpers/google/index.js
+++ b/helpers/google/index.js
@@ -306,7 +306,7 @@ var execute = async function(LocalGoogleConfig, collection, service, callObj, ca
resultItems = setData(collectionItems, data.data[callObj.dataKey], postCall, parent, {'service': service, 'callKey': callKey, maxLimit: callObj.maxLimit});
} else if (data.data.clusters && ['kubernetes', 'dataproc'].includes(service)) {
resultItems = setData(collectionItems, data.data['clusters'], postCall, parent, {'service': service, 'callKey': callKey, maxLimit: callObj.maxLimit});
- } else if (callObj.dataKey && data.data && data.data.length && service == 'vertexAI') {
+ } else if (callObj.dataKey && data.data && data.data.length && callObj.isDataArray) {
resultItems = setData(collectionItems, data.data[0][callObj.dataKey], postCall, parent, {'service': service, 'callKey': callKey, maxLimit: callObj.maxLimit});
} else if (callObj.dataFilterKey && data.data[callObj.dataFilterKey]) {
resultItems = setData(collectionItems, data.data[callObj.dataFilterKey], postCall, parent, {'service': service, 'callKey': callKey, maxLimit: callObj.maxLimit});
diff --git a/helpers/google/regions.js b/helpers/google/regions.js
index 69d2d233f..8c68e6948 100644
--- a/helpers/google/regions.js
+++ b/helpers/google/regions.js
@@ -104,7 +104,7 @@ module.exports = {
composer: [
'us-west1', 'us-west2', 'us-west3', 'us-west4', 'us-central1', 'us-east1', 'us-east4', 'northamerica-northeast1', 'southamerica-east1',
'europe-west2', 'europe-west1', 'europe-west6', 'europe-west3', 'europe-central2', 'asia-south1', 'asia-southeast1', 'asia-east2', 'asia-northeast1',
- 'asia-northeast2', 'australia-southeast1', 'asia-northeast3'
+ 'asia-northeast2', 'australia-southeast1', 'asia-northeast3','asia-east1'
],
instanceGroupManagers: regions,
functions: [
@@ -112,6 +112,11 @@ module.exports = {
'europe-west1', 'europe-west2', 'europe-west3', 'europe-west6', 'europe-central2', 'asia-south1', 'asia-southeast1', 'asia-southeast2',
'asia-east1', 'asia-east2', 'asia-northeast1', 'asia-northeast2', 'asia-northeast3', 'australia-southeast1'
],
+ functionsv2: [
+ 'us-east1', 'us-east4', 'us-west1', 'us-west2', 'us-west3', 'us-west4', 'us-central1', 'northamerica-northeast1', 'southamerica-east1',
+ 'europe-west1', 'europe-west2', 'europe-west3', 'europe-west6', 'europe-central2', 'asia-south1', 'asia-southeast1', 'asia-southeast2',
+ 'asia-east1', 'asia-east2', 'asia-northeast1', 'asia-northeast2', 'asia-northeast3', 'australia-southeast1'
+ ],
cloudbuild: ['global', 'us-east1', 'us-east4', 'us-west2', 'us-west3', 'us-west4', 'us-central1', 'us-west1',
'northamerica-northeast1', 'northamerica-northeast2', 'southamerica-east1', 'southamerica-west1', 'europe-west1', 'europe-west2',
'europe-west3', 'europe-west4', 'europe-west6', 'europe-central2', 'europe-north1', 'asia-south1', 'asia-south2', 'asia-southeast1', 'asia-southeast2',
@@ -157,6 +162,9 @@ module.exports = {
accessApproval: ['global'],
networkRoutes: ['global'],
roles: ['global'],
+ apiGateways: ['global','asia-northeast1', 'australia-southeast1', 'europe-west1', 'europe-west2', 'us-central1', 'us-east1', 'us-east4', 'us-west2', 'us-west3', 'us-west4'],
+ api: ['global','asia-northeast1', 'australia-southeast1', 'europe-west1', 'europe-west2', 'us-central1', 'us-east1', 'us-east4', 'us-west2', 'us-west3', 'us-west4'],
+ apiConfigs: ['global','asia-northeast1', 'australia-southeast1', 'europe-west1', 'europe-west2', 'us-central1', 'us-east1', 'us-east4', 'us-west2', 'us-west3', 'us-west4'],
vertexAI: ['us-west1', 'us-west2', 'us-west3', 'us-west4', 'us-central1', 'us-east1', 'us-east4', 'us-south1',
'northamerica-northeast1', 'northamerica-northeast2', 'southamerica-east1', 'southamerica-west1', 'europe-west1',
'europe-west2', 'europe-west3', 'europe-west4', 'europe-west6', 'europe-west8', 'europe-west9', 'europe-north1', 'europe-central2',
diff --git a/helpers/google/resources.js b/helpers/google/resources.js
index 54b7c6fee..005624471 100644
--- a/helpers/google/resources.js
+++ b/helpers/google/resources.js
@@ -52,6 +52,10 @@ module.exports = {
functions: {
list: 'name'
},
+ functionsv2: {
+ list: 'name',
+ getIamPolicy: 'name'
+ },
instanceGroups: {
aggregatedList: ''
},
diff --git a/helpers/shared.js b/helpers/shared.js
index 1222c84eb..f789dabb4 100644
--- a/helpers/shared.js
+++ b/helpers/shared.js
@@ -3,7 +3,6 @@ var async = require('async');
var ONE_DAY = 24*60*60*1000;
var ONE_HOUR = 60*60*1000;
-let identityServices = ['iam', 'aad'];
var daysBetween = function(date1, date2) {
return Math.round(Math.abs((new Date(date1).getTime() - new Date(date2).getTime())/(ONE_DAY)));
};
@@ -21,11 +20,6 @@ var processIntegration = function(serviceName, settings, collection, calls, post
let localSettings = {};
localSettings = settings;
- if (settings.identifier.new_inventory_enabled &&
- (identityServices.includes(serviceName.toLowerCase()) || (calls[serviceName] && calls[serviceName].sendIntegration && calls[serviceName].sendIntegration.isIdentity))) {
- console.log(`Not sending ${serviceName} because new inventory ff is enabled`);
- return iCb();
- }
if (settings.govcloud) {
localEvent.awsOrGov = 'aws-us-gov';
}
@@ -33,7 +27,7 @@ var processIntegration = function(serviceName, settings, collection, calls, post
localEvent.scanTriggeredFromEventsFlow = settings.scanTriggeredFromEventsFlow;
localEvent.collection = {};
localEvent.previousCollection = {};
-
+ localEvent.cloud_account_identifier = settings.identifier.cloud_account_identifier;
localEvent.lastScanId = settings.lastScanId;
localEvent.collection[serviceName.toLowerCase()] = {};
diff --git a/index.js b/index.js
index 65230eb16..05a994c11 100755
--- a/index.js
+++ b/index.js
@@ -5,17 +5,17 @@ const engine = require('./engine');
console.log(`
- _____ _ _ ______ _ _ _
- / ____| | | | ____| | | (_) |
- | | | | ___ _ _ __| | |__ __ ___ __ | | ___ _| |_
- | | | |/ _ \\| | | |/ _\` | __| \\ \\/ / '_ \\| |/ _ \\| | __|
- | |____| | (_) | |_| | (_| | |____ > <| |_) | | (_) | | |_
- \\_____|_|\\___/ \\__,_|\\__,_|______/_/\\_\\ .__/|_|\\___/|_|\\__|
- | |
- |_|
-
- CloudSploit by Khulnasoft Security, Ltd.
- Cloud security auditing for AWS, Azure, GCP, Oracle, and GitHub
+ ________ _______ __ _ __
+ / ____/ /___ __ ______/ / ___/____ / /___ (_) /_
+ / / / / __ \/ / / / __ /\__ \/ __ \/ / __ \/ / __/
+/ /___/ / /_/ / /_/ / /_/ /___/ / /_/ / / /_/ / / /_
+\____/_/\____/\__,_/\__,_//____/ .___/_/\____/_/\__/
+ /_/
+
+ ☁️ CloudExploit by KhulnaSoft Security, Ltd.
+ 🔐 Multi‑Cloud Security Auditing Platform
+ 🚀 AWS • Azure • GCP • Oracle • GitHub
+
`);
const parser = new ArgumentParser({});
@@ -276,4 +276,4 @@ if (settings.remediate && settings.remediate.length) {
}
// Now execute the scans using the defined configuration information.
-engine(cloudConfig, settings);
+engine(cloudConfig, settings);
\ No newline at end of file
diff --git a/package.json b/package.json
index ad99d5e59..e07db42a6 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "cloudexploit",
- "version": "2.0.1",
+ "version": "2.0.0",
"description": "AWS, Azure, GCP, Oracle, GitHub security scanning scripts",
"main": "index.js",
"scripts": {
@@ -13,7 +13,7 @@
"lint": "npx eslint ."
},
"bin": {
- "cloudexploit-scan": "./index.js"
+ "cloudexploit": "./index.js"
},
"repository": {
"type": "git",
@@ -29,7 +29,7 @@
"cloud",
"security"
],
- "author": "Khulnasoft Security",
+ "author": "KhulnaSoft Security",
"license": "GPL-3.0-or-later",
"bugs": {
"url": "https://github.com/khulnasoft/cloudexploit/issues"
diff --git a/plugins/alibaba/ram/accessKeysRotation.spec.js b/plugins/alibaba/ram/accessKeysRotation.spec.js
index daa9034a4..019e383a8 100644
--- a/plugins/alibaba/ram/accessKeysRotation.spec.js
+++ b/plugins/alibaba/ram/accessKeysRotation.spec.js
@@ -24,7 +24,7 @@ const getUserLoginProfile = [
AccessKey: [
{
Status: "Active",
- AccessKeyId: "LTAI5tD6ekrSssrWq5rNa4JQ",
+ AccessKeyId: "LTABCDEHJJH",
CreateDate: failDate,
},
],
@@ -35,7 +35,7 @@ const getUserLoginProfile = [
AccessKey: [
{
Status: "Active",
- AccessKeyId: "LTAI5tD6ekrSssrWq5rNa4JQ",
+ AccessKeyId: "LTABCDEHJJ",
CreateDate: passDate,
},
],
@@ -138,4 +138,4 @@ describe('accessKeysRotation', function () {
});
});
})
-})
\ No newline at end of file
+})
diff --git a/plugins/aws/accessanalyzer/accessAnalyzerActiveFindings.js b/plugins/aws/accessanalyzer/accessAnalyzerActiveFindings.js
index 93d9f22b3..ca801a7e3 100644
--- a/plugins/aws/accessanalyzer/accessAnalyzerActiveFindings.js
+++ b/plugins/aws/accessanalyzer/accessAnalyzerActiveFindings.js
@@ -11,15 +11,15 @@ module.exports = {
'You can view IAM Access Analyzer findings at any time. Work through all of the findings in your account until you have zero active findings.',
link: 'https://docs.aws.amazon.com/IAM/latest/UserGuide/access-analyzer-work-with-findings.html',
recommended_action: 'Investigate into active findings in your account and do the needful until you have zero active findings.',
- apis: ['AccessAnalyzer:listAnalyzers', 'AccessAnalyzer:listFindings'],
+ apis: ['AccessAnalyzer:listAnalyzers', 'AccessAnalyzer:listFindings', 'AccessAnalyzer:listFindingsV2'],
realtime_triggers: ['accessanalyzer:CreateAnalyzer','accessanalyzer:DeleteAnalyzer','accessanalyzer:CreateArchiveRule','accessanalyzer:StartResourceScan'],
-
- run: function(cache, settings, callback) {
- var results = [];
- var source = {};
+
+ run: function(cache, settings, callback) {
+ var results = [];
+ var source = {};
var regions = helpers.regions(settings);
- async.each(regions.accessanalyzer, function(region, rcb){
+ async.each(regions.accessanalyzer, function(region, rcb){
var listAnalyzers = helpers.addSource(cache, source,
['accessanalyzer', 'listAnalyzers', region]);
@@ -40,19 +40,32 @@ module.exports = {
if (!analyzer.arn) continue;
let resource = analyzer.arn;
+ let totalFiltered = [];
var listFindings = helpers.addSource(cache, source,
['accessanalyzer', 'listFindings', region, analyzer.arn]);
- if (!listFindings || listFindings.err || !listFindings.data) {
+ if (listFindings && !listFindings.err && listFindings.data) {
+ let filtered = listFindings.data.findings.filter(finding => finding.status === 'ACTIVE');
+ totalFiltered = totalFiltered.concat(filtered);
+ }
+
+ var listFindingsV2 = helpers.addSource(cache, source,
+ ['accessanalyzer', 'listFindingsV2', region, analyzer.arn]);
+
+ if (listFindingsV2 && !listFindingsV2.err && listFindingsV2.data) {
+ let filteredv2 = listFindingsV2.data.findings.filter(finding => finding.status === 'ACTIVE');
+ totalFiltered = totalFiltered.concat(filteredv2);
+ }
+
+ if ((!listFindings || listFindings.err || !listFindings.data) && (!listFindingsV2 || listFindingsV2.err || !listFindingsV2.data)) {
helpers.addResult(results, 3,
- `Unable to IAM Access Analyzer findings: ${helpers.addError(listFindings)}`,
+ `Unable to IAM Access Analyzer findings: ${helpers.addError(listFindings)} ${helpers.addError(listFindingsV2)}`,
region, resource);
continue;
- }
-
- let filtered = listFindings.data.findings.filter(finding => finding.status === 'ACTIVE');
- if (!filtered.length) {
+ }
+
+ if (!totalFiltered.length) {
helpers.addResult(results, 0,
'Amazon IAM Access Analyzer has no active findings',
region, resource);
diff --git a/plugins/aws/accessanalyzer/accessAnalyzerActiveFindings.spec.js b/plugins/aws/accessanalyzer/accessAnalyzerActiveFindings.spec.js
index ce47e5ea8..649f0572d 100644
--- a/plugins/aws/accessanalyzer/accessAnalyzerActiveFindings.spec.js
+++ b/plugins/aws/accessanalyzer/accessAnalyzerActiveFindings.spec.js
@@ -15,8 +15,8 @@ const listAnalyzers = [
];
const listFindings = [
-{
- "findings": [
+ {
+ "findings": [
{
"action": [
"kms:RetireGrant"
@@ -74,9 +74,9 @@ const listFindings = [
"updatedAt": "2022-01-12T13:48:20+00:00"
}
]
-},
-{
- "findings": [
+ },
+ {
+ "findings": [
{
"action": [
"kms:RetireGrant"
@@ -134,10 +134,87 @@ const listFindings = [
"updatedAt": "2022-01-12T13:48:20+00:00"
}
]
-}
-
+ }
+
];
+const listFindingsV2 = [
+ {
+ "findings": [
+ {
+ "analyzedAt": "2025-01-23T13:06:24+00:00",
+ "createdAt": "2025-01-23T13:06:56+00:00",
+ "id": "1a234567-bc6d-7yui-h5j7-4f5f9j8987y0",
+ "resource": "arn:aws:iam::123456789123:role/abcd-abcd-adfitoui-abcdefg-p1-AsdfghTfjdudnjkDkjg-Z9JgMyMzcxOZ",
+ "resourceType": "AWS::IAM::Role",
+ "resourceOwnerAccount": "123456789123",
+ "status": "ACTIVE",
+ "updatedAt": "2025-01-23T13:06:56+00:00",
+ "findingType": "UnusedIAMRole"
+ },
+ {
+ "analyzedAt": "2025-01-23T13:06:24+00:00",
+ "createdAt": "2025-01-23T13:06:56+00:00",
+ "id": "938r4848-4h4j-8449-76d8-8768dh5dhh4u",
+ "resource": "arn:aws:iam::123456789123:role/abcd-abcd-adfitoui-abcdefg-AsdfghTfjdudnjkDkjg-6vzrTVSqTaNe",
+ "resourceType": "AWS::IAM::Role",
+ "resourceOwnerAccount": "123456789123",
+ "status": "ACTIVE",
+ "updatedAt": "2025-01-23T13:06:56+00:00",
+ "findingType": "UnusedIAMRole"
+ },
+ {
+ "analyzedAt": "2025-01-23T13:06:55+00:00",
+ "createdAt": "2025-01-23T13:06:56+00:00",
+ "id": "7484f848-984j-498l-784s-yryh74748f45",
+ "resource": "arn:aws:iam::123456789123:role/service-role/sdfghyFj-FGH-njkkjg-plgd-6uhjn9ok",
+ "resourceType": "AWS::IAM::Role",
+ "resourceOwnerAccount": "123456789123",
+ "status": "ACTIVE",
+ "updatedAt": "2025-01-23T13:06:56+00:00",
+ "findingType": "UnusedPermission"
+ },
+ ]
+ },
+ {
+ "findings": [
+ {
+ "analyzedAt": "2025-01-23T13:06:24+00:00",
+ "createdAt": "2025-01-23T13:06:56+00:00",
+ "id": "1a234567-bc6d-7yui-h5j7-4f5f9j8987y0",
+ "resource": "arn:aws:iam::123456789123:role/abcd-abcd-adfitoui-abcdefg-p1-AsdfghTfjdudnjkDkjg-Z9JgMyMzcxOZ",
+ "resourceType": "AWS::IAM::Role",
+ "resourceOwnerAccount": "123456789123",
+ "status": "ARCHIVED",
+ "updatedAt": "2025-01-23T13:06:56+00:00",
+ "findingType": "UnusedIAMRole"
+ },
+ {
+ "analyzedAt": "2025-01-23T13:06:24+00:00",
+ "createdAt": "2025-01-23T13:06:56+00:00",
+ "id": "938r4848-4h4j-8449-76d8-8768dh5dhh4u",
+ "resource": "arn:aws:iam::123456789123:role/abcd-abcd-adfitoui-abcdefg-AsdfghTfjdudnjkDkjg-6vzrTVSqTaNe",
+ "resourceType": "AWS::IAM::Role",
+ "resourceOwnerAccount": "123456789123",
+ "status": "ARCHIVED",
+ "updatedAt": "2025-01-23T13:06:56+00:00",
+ "findingType": "UnusedIAMRole"
+ },
+ {
+ "analyzedAt": "2025-01-23T13:06:55+00:00",
+ "createdAt": "2025-01-23T13:06:56+00:00",
+ "id": "7484f848-984j-498l-784s-yryh74748f45",
+ "resource": "arn:aws:iam::123456789123:role/service-role/sdfghyFj-FGH-njkkjg-plgd-6uhjn9ok",
+ "resourceType": "AWS::IAM::Role",
+ "resourceOwnerAccount": "123456789123",
+ "status": "RESOLVED",
+ "updatedAt": "2025-01-23T13:06:56+00:00",
+ "findingType": "UnusedPermission"
+ },
+ ]
+ }
+
+]
const createCache = (analyzer, listFindings, analyzerErr, listFindingsErr) => {
var analyzerArn = (analyzer && analyzer.length) ? analyzer[0].arn: null;
@@ -163,7 +240,7 @@ const createCache = (analyzer, listFindings, analyzerErr, listFindingsErr) => {
describe('accessAnalyzerActiveFindings', function () {
describe('run', function () {
- it('should FAIL if Amazon IAM access analyzer has active findings.', function (done) {
+ it('should FAIL if Amazon IAM access analyzer V1 has active findings.', function (done) {
const cache = createCache(listAnalyzers, listFindings[0]);
accessAnalyzerActiveFindings.run(cache, {}, (err, results) => {
expect(results.length).to.equal(1);
@@ -174,14 +251,38 @@ describe('accessAnalyzerActiveFindings', function () {
});
});
- it('should PASS if Amazon IAM access analyzer have no active findings.', function (done) {
+ it('should FAIL if Amazon IAM access analyzer v2 has active findings.', function (done) {
+ const cache = createCache(listAnalyzers, listFindingsV2[0]);
+ accessAnalyzerActiveFindings.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(2);
+ expect(results[0].region).to.equal('us-east-1');
+ expect(results[0].message).to.include('Amazon IAM Access Analyzer has active findings');
+ done();
+ });
+ });
+
+ it('should PASS if Amazon IAM access analyzer V1 have no active findings.', function (done) {
const cache = createCache(listAnalyzers, listFindings[1]);
accessAnalyzerActiveFindings.run(cache, {}, (err, results) => {
expect(results.length).to.equal(1);
expect(results[0].status).to.equal(0);
expect(results[0].region).to.equal('us-east-1');
expect(results[0].message).to.include('Amazon IAM Access Analyzer has no active findings');
-
+
+ done();
+ });
+ });
+
+
+ it('should PASS if Amazon IAM access analyzer V2 have no active findings.', function (done) {
+ const cache = createCache(listAnalyzers, listFindingsV2[1]);
+ accessAnalyzerActiveFindings.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(0);
+ expect(results[0].region).to.equal('us-east-1');
+ expect(results[0].message).to.include('Amazon IAM Access Analyzer has no active findings');
+
done();
});
});
diff --git a/plugins/aws/apigateway/apigatewayV2Authorization.js b/plugins/aws/apigateway/apigatewayV2Authorization.js
index eb3ca1f12..974166533 100644
--- a/plugins/aws/apigateway/apigatewayV2Authorization.js
+++ b/plugins/aws/apigateway/apigatewayV2Authorization.js
@@ -14,7 +14,6 @@ module.exports = {
realtime_triggers: ['ApiGatewayV2:createApi','ApiGatewayV2:deleteApi','ApiGatewayV2:importApi','ApiGatewayV2:createAuthorizer','ApiGatewayV2:deleteAuthorizer'],
run: function(cache, settings, callback) {
- console.log('here');
var results = [];
var source = {};
var regions = helpers.regions(settings);
diff --git a/plugins/aws/autoscaling/asgTagPropagation.js b/plugins/aws/autoscaling/asgTagPropagation.js
new file mode 100644
index 000000000..7515e70ef
--- /dev/null
+++ b/plugins/aws/autoscaling/asgTagPropagation.js
@@ -0,0 +1,75 @@
+var async = require('async');
+var helpers = require('../../../helpers/aws');
+
+module.exports = {
+ title: 'ASG Tag Propagation',
+ category: 'AutoScaling',
+ domain: 'Availability',
+ severity: 'Medium',
+ description: 'Ensure that EC2 Auto Scaling Groups propagate tags to EC2 instances that it launches.',
+ more_info: 'Tags can help with managing, identifying, organizing, searching for, and filtering resources. Additionally, tags can help with security and compliance. Tags should be propagated from an Auto Scaling group to the EC2 instances that it launches.',
+ link: 'https://docs.aws.amazon.com/autoscaling/ec2/userguide/ec2-auto-scaling-tagging.html',
+ recommended_action: 'Enable tag propagation for all tags on Auto Scaling Groups by setting PropagateAtLaunch to true for each tag.',
+ apis: ['AutoScaling:describeAutoScalingGroups'],
+ realtime_triggers: ['autoscaling:CreateAutoScalingGroup', 'autoscaling:UpdateAutoScalingGroup', 'autoscaling:CreateOrUpdateTags', 'autoscaling:DeleteTags'],
+
+ 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 (!resource) return;
+
+ if (!asg.Tags || !asg.Tags.length) {
+ helpers.addResult(results, 0,
+ 'Auto scaling group has no tags configured',
+ region, resource);
+ return;
+ }
+
+ var tagsNotPropagating = [];
+ asg.Tags.forEach(function(tag) {
+ if (!tag.PropagateAtLaunch) {
+ tagsNotPropagating.push(tag.Key || 'unnamed');
+ }
+ });
+
+ if (!tagsNotPropagating.length ) {
+ helpers.addResult(results, 0,
+ 'Auto scaling group has all tags configured to propagate to EC2 instances',
+ region, resource);
+ } else {
+ helpers.addResult(results, 2,
+ 'Auto scaling group has ' + tagsNotPropagating.length +
+ ' tag(s) not configured to propagate to EC2 instances',
+ region, resource);
+ }
+ });
+ rcb();
+ }, function(){
+ callback(null, results, source);
+ });
+ }
+};
+
diff --git a/plugins/aws/autoscaling/asgTagPropagation.spec.js b/plugins/aws/autoscaling/asgTagPropagation.spec.js
new file mode 100644
index 000000000..53aa7c641
--- /dev/null
+++ b/plugins/aws/autoscaling/asgTagPropagation.spec.js
@@ -0,0 +1,236 @@
+var expect = require('chai').expect;
+const asgTagPropagation = require('./asgTagPropagation');
+
+const autoScalingGroups = [
+ {
+ "AutoScalingGroupName": "asg-all-tags-propagate",
+ "AutoScalingGroupARN": "arn:aws:autoscaling:us-east-1:111122223333:autoScalingGroup:e83ceb12-2760-4a92-a374-3df611331bdc:autoScalingGroupName/asg-all-tags-propagate",
+ "MinSize": 1,
+ "MaxSize": 3,
+ "DesiredCapacity": 2,
+ "DefaultCooldown": 300,
+ "AvailabilityZones": [
+ "us-east-1a",
+ "us-east-1b"
+ ],
+ "HealthCheckType": "EC2",
+ "HealthCheckGracePeriod": 300,
+ "Instances": [],
+ "CreatedTime": "2020-08-18T23:12:00.954Z",
+ "Tags": [
+ {
+ "ResourceId": "asg-all-tags-propagate",
+ "ResourceType": "auto-scaling-group",
+ "Key": "Environment",
+ "Value": "Production",
+ "PropagateAtLaunch": true
+ },
+ {
+ "ResourceId": "asg-all-tags-propagate",
+ "ResourceType": "auto-scaling-group",
+ "Key": "Owner",
+ "Value": "DevOps",
+ "PropagateAtLaunch": true
+ }
+ ],
+ "TerminationPolicies": ["Default"]
+ },
+ {
+ "AutoScalingGroupName": "asg-some-tags-propagate",
+ "AutoScalingGroupARN": "arn:aws:autoscaling:us-east-1:111122223333:autoScalingGroup:e83ceb12-2760-4a92-a374-3df611331bdc:autoScalingGroupName/asg-some-tags-propagate",
+ "MinSize": 1,
+ "MaxSize": 3,
+ "DesiredCapacity": 2,
+ "DefaultCooldown": 300,
+ "AvailabilityZones": [
+ "us-east-1a"
+ ],
+ "HealthCheckType": "EC2",
+ "HealthCheckGracePeriod": 300,
+ "Instances": [],
+ "CreatedTime": "2020-08-18T23:12:00.954Z",
+ "Tags": [
+ {
+ "ResourceId": "asg-some-tags-propagate",
+ "ResourceType": "auto-scaling-group",
+ "Key": "Environment",
+ "Value": "Production",
+ "PropagateAtLaunch": true
+ },
+ {
+ "ResourceId": "asg-some-tags-propagate",
+ "ResourceType": "auto-scaling-group",
+ "Key": "Owner",
+ "Value": "DevOps",
+ "PropagateAtLaunch": false
+ }
+ ],
+ "TerminationPolicies": ["Default"]
+ },
+ {
+ "AutoScalingGroupName": "asg-no-tags",
+ "AutoScalingGroupARN": "arn:aws:autoscaling:us-east-1:111122223333:autoScalingGroup:e83ceb12-2760-4a92-a374-3df611331bdc:autoScalingGroupName/asg-no-tags",
+ "MinSize": 1,
+ "MaxSize": 3,
+ "DesiredCapacity": 2,
+ "DefaultCooldown": 300,
+ "AvailabilityZones": [
+ "us-east-1a"
+ ],
+ "HealthCheckType": "EC2",
+ "HealthCheckGracePeriod": 300,
+ "Instances": [],
+ "CreatedTime": "2020-08-18T23:12:00.954Z",
+ "Tags": [],
+ "TerminationPolicies": ["Default"]
+ },
+ {
+ "AutoScalingGroupName": "asg-no-tags-property",
+ "AutoScalingGroupARN": "arn:aws:autoscaling:us-east-1:111122223333:autoScalingGroup:e83ceb12-2760-4a92-a374-3df611331bdc:autoScalingGroupName/asg-no-tags-property",
+ "MinSize": 1,
+ "MaxSize": 3,
+ "DesiredCapacity": 2,
+ "DefaultCooldown": 300,
+ "AvailabilityZones": [
+ "us-east-1a"
+ ],
+ "HealthCheckType": "EC2",
+ "HealthCheckGracePeriod": 300,
+ "Instances": [],
+ "CreatedTime": "2020-08-18T23:12:00.954Z",
+ "TerminationPolicies": ["Default"]
+ },
+ {
+ "AutoScalingGroupName": "asg-all-tags-not-propagate",
+ "AutoScalingGroupARN": "arn:aws:autoscaling:us-east-1:111122223333:autoScalingGroup:e83ceb12-2760-4a92-a374-3df611331bdc:autoScalingGroupName/asg-all-tags-not-propagate",
+ "MinSize": 1,
+ "MaxSize": 3,
+ "DesiredCapacity": 2,
+ "DefaultCooldown": 300,
+ "AvailabilityZones": [
+ "us-east-1a"
+ ],
+ "HealthCheckType": "EC2",
+ "HealthCheckGracePeriod": 300,
+ "Instances": [],
+ "CreatedTime": "2020-08-18T23:12:00.954Z",
+ "Tags": [
+ {
+ "ResourceId": "asg-all-tags-not-propagate",
+ "ResourceType": "auto-scaling-group",
+ "Key": "Environment",
+ "Value": "Production",
+ "PropagateAtLaunch": false
+ },
+ {
+ "ResourceId": "asg-all-tags-not-propagate",
+ "ResourceType": "auto-scaling-group",
+ "Key": "Owner",
+ "Value": "DevOps",
+ "PropagateAtLaunch": false
+ }
+ ],
+ "TerminationPolicies": ["Default"]
+ }
+];
+
+const createCache = (asgs) => {
+ return {
+ autoscaling: {
+ describeAutoScalingGroups: {
+ 'us-east-1': {
+ data: asgs
+ },
+ },
+ },
+ };
+};
+
+const createErrorCache = () => {
+ return {
+ autoscaling: {
+ describeAutoScalingGroups: {
+ 'us-east-1': {
+ err: {
+ message: 'error describing Auto Scaling groups'
+ },
+ },
+ },
+ },
+ };
+};
+
+describe('asgTagPropagation', function () {
+ describe('run', function () {
+ it('should PASS if all tags have PropagateAtLaunch set to true', function (done) {
+ const cache = createCache([autoScalingGroups[0]]);
+ asgTagPropagation.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(0);
+ expect(results[0].message).to.include('all tags configured to propagate');
+ done();
+ });
+ });
+
+ it('should FAIL if some tags do not have PropagateAtLaunch set to true', function (done) {
+ const cache = createCache([autoScalingGroups[1]]);
+ asgTagPropagation.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(2);
+ expect(results[0].message).to.include('1 tag(s) not configured to propagate');
+ done();
+ });
+ });
+
+ it('should PASS if Auto Scaling group has no tags', function (done) {
+ const cache = createCache([autoScalingGroups[2]]);
+ asgTagPropagation.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(0);
+ expect(results[0].message).to.include('has no tags configured');
+ done();
+ });
+ });
+
+ it('should PASS if Auto Scaling group has no Tags property', function (done) {
+ const cache = createCache([autoScalingGroups[3]]);
+ asgTagPropagation.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(0);
+ expect(results[0].message).to.include('has no tags configured');
+ done();
+ });
+ });
+
+ it('should FAIL if all tags have PropagateAtLaunch set to false', function (done) {
+ const cache = createCache([autoScalingGroups[4]]);
+ asgTagPropagation.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(2);
+ expect(results[0].message).to.include('2 tag(s) not configured to propagate');
+ done();
+ });
+ });
+
+ it('should PASS if no Auto Scaling groups found', function (done) {
+ const cache = createCache([]);
+ asgTagPropagation.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(0);
+ expect(results[0].message).to.include('No auto scaling groups found');
+ done();
+ });
+ });
+
+ it('should UNKNOWN if error describing Auto Scaling groups', function (done) {
+ const cache = createErrorCache();
+ asgTagPropagation.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(3);
+ expect(results[0].message).to.include('Unable to query');
+ done();
+ });
+ });
+ });
+});
+
diff --git a/plugins/aws/cloudfront/cloudfrontLoggingEnabled.js b/plugins/aws/cloudfront/cloudfrontLoggingEnabled.js
index a6bf25320..2fca857eb 100644
--- a/plugins/aws/cloudfront/cloudfrontLoggingEnabled.js
+++ b/plugins/aws/cloudfront/cloudfrontLoggingEnabled.js
@@ -5,14 +5,14 @@ module.exports = {
category: 'CloudFront',
domain: 'Content Delivery',
severity: 'Medium',
- description: 'Ensures CloudFront distributions have request logging enabled.',
- more_info: 'Logging requests to CloudFront ' +
+ description: 'Ensures CloudFront distributions have S3 legacy logging enabled.',
+ more_info: 'Logging S3 legacy to CloudFront ' +
'distributions is a helpful way of detecting and ' +
'investigating potential attacks, malicious activity, ' +
- 'or misuse of backend resources. Logs can be sent to S3 ' +
+ 'or misuse of backend resources. Logs can be sent to S3 ' +
'and processed for further analysis.',
- link: 'http://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/AccessLogs.html',
- recommended_action: 'Enable CloudFront request logging.',
+ link: 'https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/standard-logging-legacy-s3.html',
+ recommended_action: 'Enable CloudFront S3 legacy logging.',
apis: ['CloudFront:listDistributions', 'CloudFront:getDistribution'],
compliance: {
hipaa: 'As part of the audit control requirement for HIPAA, request logging for ' +
@@ -77,10 +77,10 @@ module.exports = {
var logging = getDistribution.data.Distribution.DistributionConfig.Logging;
if (logging.Enabled){
helpers.addResult(results, 0,
- 'Request logging is enabled', 'global', Distribution.ARN);
+ 'S3 legacy logging is enabled', 'global', Distribution.ARN);
} else {
helpers.addResult(results, 2,
- 'Request logging is not enabled', 'global', Distribution.ARN);
+ 'S3 legacy logging is not enabled', 'global', Distribution.ARN);
}
}
});
diff --git a/plugins/aws/cloudwatchlogs/monitoringMetrics.js b/plugins/aws/cloudwatchlogs/monitoringMetrics.js
index 732c952b9..4e98dae0b 100644
--- a/plugins/aws/cloudwatchlogs/monitoringMetrics.js
+++ b/plugins/aws/cloudwatchlogs/monitoringMetrics.js
@@ -70,7 +70,7 @@ module.exports = {
domain: 'Compliance',
severity: 'Medium',
description: 'Ensures metric filters are setup for CloudWatch logs to detect security risks from CloudTrail.',
- more_info: 'Sending CloudTrail logs to CloudWatch is only useful if metrics are setup to detect risky activity from those logs. There are numerous metrics that should be used. For the exact filter patterns, please see this plugin on GitHub: https://github.com/khulnasoft/cloudexploit/blob/master/plugins/aws/cloudwatchlogs/monitoringMetrics.js',
+ more_info: 'Sending CloudTrail logs to CloudWatch is only useful if metrics are setup to detect risky activity from those logs. There are numerous metrics that should be used. For the exact filter patterns, please see this plugin on GitHub: https://github.com/cloudexploit/scans/blob/master/plugins/aws/cloudwatchlogs/monitoringMetrics.js',
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'],
diff --git a/plugins/aws/configservice/configServiceMissingBucket.js b/plugins/aws/configservice/configServiceMissingBucket.js
index 29ea23c91..df3d5c603 100644
--- a/plugins/aws/configservice/configServiceMissingBucket.js
+++ b/plugins/aws/configservice/configServiceMissingBucket.js
@@ -19,6 +19,7 @@ module.exports = {
var source = {};
var regions = helpers.regions(settings);
var awsOrGov = helpers.defaultPartition(settings);
+ var defaultRegion = helpers.defaultRegion(settings);
async.each(regions.configservice, function(region, rcb) {
var describeDeliveryChannels = helpers.addSource(cache, source,
@@ -36,17 +37,22 @@ module.exports = {
helpers.addResult(results, 0, 'No Config delivery channels found', region);
return rcb();
}
+ var listBuckets = helpers.addSource(cache, source,
+ ['s3', 'listBuckets', defaultRegion]);
let deletedBuckets = [];
for (let record of describeDeliveryChannels.data) {
if (!record.s3BucketName) continue;
var headBucket = helpers.addSource(cache, source,
- ['s3', 'headBucket', region, record.s3BucketName]);
+ ['s3', 'headBucket', defaultRegion, record.s3BucketName]);
- if (headBucket && headBucket.err && headBucket.err.message &&
- headBucket.err.message.toLowerCase().includes('not found')){
- deletedBuckets.push(record);
+ var bucketFound = listBuckets && listBuckets.data && listBuckets.data.length
+ ? listBuckets.data.some(bucket => bucket.Name === record.s3BucketName) : false;
+
+ if (!bucketFound || (headBucket && headBucket.err && headBucket.err.message &&
+ headBucket.err.message.toLowerCase().includes('not found'))) {
+ deletedBuckets.push(record.s3BucketName);
} else if (!headBucket || headBucket.err) {
helpers.addResult(results, 3,
'Unable to query S3 bucket: ' + helpers.addError(headBucket), region, `arn:${awsOrGov}:s3:::` + record.s3BucketName);
diff --git a/plugins/aws/configservice/configServiceMissingBucket.spec.js b/plugins/aws/configservice/configServiceMissingBucket.spec.js
index 13ebc28a8..98520e376 100644
--- a/plugins/aws/configservice/configServiceMissingBucket.spec.js
+++ b/plugins/aws/configservice/configServiceMissingBucket.spec.js
@@ -14,7 +14,7 @@ const describeDeliveryChannels = [
}
];
-const createCache = (records, headBucket, recordsErr, headBucketErr) => {
+const createCache = (records, headBucket, recordsErr, headBucketErr, buckets) => {
var name = (records && records.length) ? records[0].s3BucketName : null;
return {
configservice: {
@@ -26,6 +26,12 @@ const createCache = (records, headBucket, recordsErr, headBucketErr) => {
},
},
s3: {
+ listBuckets: {
+ 'us-east-1': {
+ err: 'err',
+ data: buckets
+ }
+ },
headBucket: {
'us-east-1': {
[name]: {
@@ -42,7 +48,10 @@ const createCache = (records, headBucket, recordsErr, headBucketErr) => {
describe('configServiceMissingBucket', function () {
describe('run', function () {
it('should PASS if Config Service is not referencing any deleted bucket', function (done) {
- const cache = createCache([describeDeliveryChannels[1]], null);
+ const cache = createCache([describeDeliveryChannels[1]], null, null, null,[{
+ "Name": "amazon-connect-e39f272cf1f0",
+ "CreationDate": "November 22, 2021, 15:51:19 (UTC+05:00)",
+ }]);
configServiceMissingBucket.run(cache, {}, (err, results) => {
expect(results.length).to.equal(1);
expect(results[0].status).to.equal(0);
diff --git a/plugins/aws/connect/customerProfilesDomainEncrypted.js b/plugins/aws/connect/customerProfilesDomainEncrypted.js
index 76063936c..ca53ba859 100644
--- a/plugins/aws/connect/customerProfilesDomainEncrypted.js
+++ b/plugins/aws/connect/customerProfilesDomainEncrypted.js
@@ -34,10 +34,13 @@ module.exports = {
desiredEncryptionLevelString: settings.customer_profiles_desired_encryption_level || this.settings.customer_profiles_desired_encryption_level.default
};
+ // Skip encryption check if default awskms is set
+ var skipEncryptionCheck = config.desiredEncryptionLevelString === 'awskms';
+
var desiredEncryptionLevel = helpers.ENCRYPTION_LEVELS.indexOf(config.desiredEncryptionLevelString);
var currentEncryptionLevel;
- async.each(regions.customerprofiles, function(region, rcb){
+ async.each(regions.customerprofiles, function(region, rcb){
var listDomains = helpers.addSource(cache, source,
['customerprofiles', 'listDomains', region]);
@@ -76,44 +79,54 @@ module.exports = {
`Unable to get customerprofiles domain description: ${helpers.addError(getDomain)}`,
region, resource);
continue;
- }
-
- if (getDomain.data.DefaultEncryptionKey) {
- let DefaultEncryptionKey = getDomain.data.DefaultEncryptionKey;
- var keyId = DefaultEncryptionKey.split('/')[1] ? DefaultEncryptionKey.split('/')[1] : DefaultEncryptionKey;
-
- var describeKey = helpers.addSource(cache, source,
- ['kms', 'describeKey', region, keyId]);
-
- if (!describeKey || describeKey.err || !describeKey.data || !describeKey.data.KeyMetadata) {
- helpers.addResult(results, 3,
- `Unable to query KMS key: ${helpers.addError(describeKey)}`,
- region, DefaultEncryptionKey);
- continue;
- }
-
- currentEncryptionLevel = helpers.getEncryptionLevel(describeKey.data.KeyMetadata, helpers.ENCRYPTION_LEVELS);
- } else {
- helpers.addResult(results, 3,
- 'Unable to find Customer Profile domain encryption key', region, resource);
- continue;
}
-
- var currentEncryptionLevelString = helpers.ENCRYPTION_LEVELS[currentEncryptionLevel];
-
- if (currentEncryptionLevel >= desiredEncryptionLevel) {
+ if (skipEncryptionCheck) {
helpers.addResult(results, 0,
- `Customer Profile domain is encrypted with ${currentEncryptionLevelString} \
- which is greater than or equal to the desired encryption level ${config.desiredEncryptionLevelString}`,
+ 'Customer Profile domain is encrypted with desired encryption level.',
region, resource);
} else {
- helpers.addResult(results, 2,
- `Customer Profile domain is encrypted with ${currentEncryptionLevelString} \
- which is less than the desired encryption level ${config.desiredEncryptionLevelString}`,
- region, resource);
+ if (getDomain.data.DefaultEncryptionKey) {
+ let DefaultEncryptionKey = getDomain.data.DefaultEncryptionKey;
+ var keyId = DefaultEncryptionKey.split('/')[1] ? DefaultEncryptionKey.split('/')[1] : DefaultEncryptionKey;
+
+ var describeKey = helpers.addSource(cache, source,
+ ['kms', 'describeKey', region, keyId]);
+
+ if (!describeKey || describeKey.err || !describeKey.data || !describeKey.data.KeyMetadata) {
+ helpers.addResult(results, 3,
+ `Unable to query KMS key: ${helpers.addError(describeKey)}`,
+ region, DefaultEncryptionKey);
+ continue;
+ }
+
+ currentEncryptionLevel = helpers.getEncryptionLevel(describeKey.data.KeyMetadata, helpers.ENCRYPTION_LEVELS);
+ } else if (domain.DomainName.startsWith('amazon-connect') && getDomain.data.DefaultEncryptionKey == null) {
+ helpers.addResult(results, 2,
+ `Customer Profile domain is encrypted with awskms \
+ which is less than the desired encryption level ${config.desiredEncryptionLevelString}`,
+ region, resource);
+ continue;
+ } else {
+ helpers.addResult(results, 3,
+ 'Unable to find Customer Profile domain encryption key', region, resource);
+ continue;
+ }
+
+ var currentEncryptionLevelString = helpers.ENCRYPTION_LEVELS[currentEncryptionLevel];
+
+ if (currentEncryptionLevel >= desiredEncryptionLevel) {
+ helpers.addResult(results, 0,
+ `Customer Profile domain is encrypted with ${currentEncryptionLevelString} \
+ which is greater than or equal to the desired encryption level ${config.desiredEncryptionLevelString}`,
+ region, resource);
+ } else {
+ helpers.addResult(results, 2,
+ `Customer Profile domain is encrypted with ${currentEncryptionLevelString} \
+ which is less than the desired encryption level ${config.desiredEncryptionLevelString}`,
+ region, resource);
+ }
}
}
-
rcb();
}, function(){
callback(null, results, source);
diff --git a/plugins/aws/documentDB/docdbAuditLoggingEnabled.js b/plugins/aws/documentDB/docdbAuditLoggingEnabled.js
new file mode 100644
index 000000000..82d0d1914
--- /dev/null
+++ b/plugins/aws/documentDB/docdbAuditLoggingEnabled.js
@@ -0,0 +1,56 @@
+var async = require('async');
+var helpers = require('../../../helpers/aws');
+
+module.exports = {
+ title: 'DocumentDB Audit Logging Enabled',
+ category: 'DocumentDB',
+ domain: 'Databases',
+ severity: 'Medium',
+ description: 'Ensure that audit logging is enabled for DocumentDB clusters.',
+ more_info: 'Audit logging in Amazon DocumentDB provides visibility into authentication events, queries, and data changes. It helps detect unauthorized access, supports troubleshooting, and meets compliance requirements. Logs should be sent to CloudWatch or a SIEM for centralized monitoring and alerting.',
+ recommended_action: 'Modify DocumentDB cluster and enable audit logging feature.',
+ link: 'https://docs.aws.amazon.com/documentdb/latest/developerguide/profiling.html',
+ apis: ['DocDB:describeDBClusters'],
+ realtime_triggers: ['docdb:CreateDBCluster','docdb:ModifyDBCluster','docdb:DeleteDBCluster'],
+
+ run: function(cache, settings, callback) {
+ var results = [];
+ var source = {};
+ var regions = helpers.regions(settings);
+
+ async.each(regions.docdb, function(region, rcb){
+ var describeDBClusters = helpers.addSource(cache, source,
+ ['docdb', 'describeDBClusters', region]);
+
+ if (!describeDBClusters) return rcb();
+
+ if (describeDBClusters.err || !describeDBClusters.data) {
+ helpers.addResult(results, 3,
+ `Unable to list DocumentDB clusters: ${helpers.addError(describeDBClusters)}`, region);
+ return rcb();
+ }
+
+ if (!describeDBClusters.data.length) {
+ helpers.addResult(results, 0,
+ 'No DocumentDB clusters found', region);
+ return rcb();
+ }
+
+ for (let cluster of describeDBClusters.data) {
+ if (!cluster.DBClusterArn) continue;
+
+ if (cluster.EnabledCloudwatchLogsExports &&
+ cluster.EnabledCloudwatchLogsExports.length &&
+ cluster.EnabledCloudwatchLogsExports.includes('audit')) {
+ helpers.addResult(results, 0, 'DocumentDB cluster has audit logging enabled', region, cluster.DBClusterArn);
+ } else {
+ helpers.addResult(results, 2, 'DocumentDB cluster does not have audit logging enabled', region, cluster.DBClusterArn);
+ }
+ }
+
+ rcb();
+ }, function(){
+ callback(null, results, source);
+ });
+ }
+};
diff --git a/plugins/aws/documentDB/docdbAuditLoggingEnabled.spec.js b/plugins/aws/documentDB/docdbAuditLoggingEnabled.spec.js
new file mode 100644
index 000000000..dbac13c12
--- /dev/null
+++ b/plugins/aws/documentDB/docdbAuditLoggingEnabled.spec.js
@@ -0,0 +1,110 @@
+var expect = require('chai').expect;
+var docdbAuditLoggingEnabled = require('./docdbAuditLoggingEnabled');
+
+const describeDBClusters = [
+ {
+ AvailabilityZones: [],
+ BackupRetentionPeriod: 1,
+ DBClusterArn: 'arn:aws:rds:us-east-1:000011112222:cluster:docdb-2021-11-10-10-16-10',
+ DBClusterIdentifier: 'docdb-2021-11-10-10-16-10',
+ DBClusterParameterGroup: 'default.docdb4.0',
+ DBSubnetGroup: 'default-vpc-99de2fe4',
+ Status: 'available',
+ DeletionProtection: true,
+ EnabledCloudwatchLogsExports: [ "audit", "profiler"]
+ },
+ {
+ AvailabilityZones: [],
+ BackupRetentionPeriod: 10,
+ DBClusterArn: 'arn:aws:rds:us-east-1:000011112223:cluster:docdb-2021-11-10-10-16-10',
+ DBClusterIdentifier: 'docdb-2021-11-10-10-16-10',
+ DBClusterParameterGroup: 'default.docdb4.0',
+ DBSubnetGroup: 'default-vpc-99de2fe4',
+ Status: 'available',
+ DeletionProtection: false,
+ EnabledCloudwatchLogsExports: [ "profiler"]
+ },
+ {
+ AvailabilityZones: [],
+ BackupRetentionPeriod: 10,
+ DBClusterArn: 'arn:aws:rds:us-east-1:000011112224:cluster:docdb-2021-11-10-10-16-10',
+ DBClusterIdentifier: 'docdb-2021-11-10-10-16-10',
+ DBClusterParameterGroup: 'default.docdb4.0',
+ DBSubnetGroup: 'default-vpc-99de2fe4',
+ Status: 'available',
+ DeletionProtection: false,
+ EnabledCloudwatchLogsExports: []
+ }
+];
+
+const createCache = (clusters, clustersErr) => {
+ return {
+ docdb: {
+ describeDBClusters: {
+ 'us-east-1': {
+ err: clustersErr,
+ data: clusters
+ },
+ },
+ }
+ };
+};
+
+describe('docdbAuditLoggingEnabled', function () {
+ describe('run', function () {
+ it('should PASS if DocumentDB Cluster has audit logging enabled', function (done) {
+ const cache = createCache([describeDBClusters[0]]);
+ docdbAuditLoggingEnabled.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(0);
+ expect(results[0].message).to.include('DocumentDB cluster has audit logging enabled');
+ expect(results[0].region).to.equal('us-east-1');
+ done();
+ });
+ });
+
+ it('should FAIL if DocumentDB Cluster does not have audit logging enabled', function (done) {
+ const cache = createCache([describeDBClusters[1]]);
+ docdbAuditLoggingEnabled.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(2);
+ expect(results[0].message).to.include('DocumentDB cluster does not have audit logging enabled');
+ expect(results[0].region).to.equal('us-east-1');
+ done();
+ });
+ });
+
+ it('should FAIL if DocumentDB Cluster has empty EnabledCloudwatchLogsExports', function (done) {
+ const cache = createCache([describeDBClusters[2]]);
+ docdbAuditLoggingEnabled.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(2);
+ expect(results[0].message).to.include('DocumentDB cluster does not have audit logging enabled');
+ expect(results[0].region).to.equal('us-east-1');
+ done();
+ });
+ });
+
+ it('should PASS if no DocumentDB Clusters found', function (done) {
+ const cache = createCache([]);
+ docdbAuditLoggingEnabled.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(0);
+ expect(results[0].message).to.include('No DocumentDB clusters found');
+ expect(results[0].region).to.equal('us-east-1');
+ done();
+ });
+ });
+
+ it('should UNKNOWN if unable to list DocumentDB Clusters', function (done) {
+ const cache = createCache(null, { message: "Unable to list DocumentDB Clusters" });
+ docdbAuditLoggingEnabled.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(3);
+ expect(results[0].message).to.include('Unable to list DocumentDB clusters:');
+ expect(results[0].region).to.equal('us-east-1');
+ done();
+ });
+ });
+ });
+});
\ No newline at end of file
diff --git a/plugins/aws/documentDB/docdbEncryptionInTransit.js b/plugins/aws/documentDB/docdbEncryptionInTransit.js
new file mode 100644
index 000000000..5b4f65418
--- /dev/null
+++ b/plugins/aws/documentDB/docdbEncryptionInTransit.js
@@ -0,0 +1,99 @@
+var async = require('async');
+var helpers = require('../../../helpers/aws');
+
+module.exports = {
+ title: 'DocumentDB Encryption In Transit',
+ category: 'DocumentDB',
+ domain: 'Databases',
+ severity: 'High',
+ description: 'Ensure that DocumentDB clusters have TLS/SSL encryption in transit enabled.',
+ more_info: 'DocumentDB uses TLS/SSL to encrypt data during transit. The TLS parameter in the cluster parameter group should be set to enabled to require encrypted connections. This ensures that all data transmitted between clients and the DocumentDB cluster is encrypted.',
+ recommended_action: 'Modify the cluster parameter group to set the tls parameter to enabled, or create a custom parameter group with TLS enabled and associate it with the cluster.',
+ link: 'https://docs.aws.amazon.com/documentdb/latest/developerguide/security.encryption.ssl.html',
+ apis: ['DocDB:describeDBClusters', 'DocDB:describeDBClusterParameters'],
+ realtime_triggers: [ 'docdb:CreateDBCluster', 'docdb:ModifyDBCluster', 'docdb:ModifyDBClusterParameterGroup', 'docdb:CreateDBClusterParameterGroup','docdb:DeleteDBCluster' ],
+
+ run: function(cache, settings, callback) {
+ var results = [];
+ var source = {};
+ var regions = helpers.regions(settings);
+
+ async.each(regions.docdb, function(region, rcb){
+ var describeDBClusters = helpers.addSource(cache, source,
+ ['docdb', 'describeDBClusters', region]);
+
+ if (!describeDBClusters) return rcb();
+
+ if (describeDBClusters.err || !describeDBClusters.data) {
+ helpers.addResult(results, 3,
+ `Unable to list DocumentDB clusters: ${helpers.addError(describeDBClusters)}`, region);
+ return rcb();
+ }
+
+ if (!describeDBClusters.data.length) {
+ helpers.addResult(results, 0,
+ 'No DocumentDB clusters found', region);
+ return rcb();
+ }
+
+ async.each(describeDBClusters.data, function(cluster, ccb){
+ if (!cluster.DBClusterArn || !cluster.DBClusterIdentifier) return ccb();
+
+ var resource = cluster.DBClusterArn;
+ var tlsEnabled = false;
+
+ if (!cluster.DBClusterParameterGroup) {
+ helpers.addResult(results, 2,
+ 'DocumentDB cluster does not have a parameter group associated',
+ region, resource);
+ return ccb();
+ }
+
+ var parameterGroupName = cluster.DBClusterParameterGroup;
+
+
+ var parameters = helpers.addSource(cache, source,
+ ['docdb', 'describeDBClusterParameters', region, parameterGroupName]);
+
+ if (!parameters || parameters.err || !parameters.data) {
+ helpers.addResult(results, 3,
+ `Unable to query cluster parameters: ${helpers.addError(parameters)}`,
+ region, resource);
+ return ccb();
+ }
+
+ if (!parameters.data.Parameters) {
+ helpers.addResult(results, 2,
+ 'DocumentDB cluster does not have TLS encryption in transit enabled',
+ region, resource);
+ return ccb();
+ }
+
+ for (var param of parameters.data.Parameters) {
+ if (param.ParameterName && param.ParameterName === 'tls' &&
+ param.ParameterValue &&
+ (param.ParameterValue.toLowerCase() === 'enabled' || param.ParameterValue === '1')) {
+ tlsEnabled = true;
+ break;
+ }
+ }
+
+ if (tlsEnabled) {
+ helpers.addResult(results, 0,
+ 'DocumentDB cluster has TLS encryption in transit enabled',
+ region, resource);
+ } else {
+ helpers.addResult(results, 2,
+ 'DocumentDB cluster does not have TLS encryption in transit enabled',
+ region, resource);
+ }
+
+ ccb();
+ }, function(){
+ rcb();
+ });
+ }, function(){
+ callback(null, results, source);
+ });
+ }
+};
diff --git a/plugins/aws/documentDB/docdbEncryptionInTransit.spec.js b/plugins/aws/documentDB/docdbEncryptionInTransit.spec.js
new file mode 100644
index 000000000..5408d2464
--- /dev/null
+++ b/plugins/aws/documentDB/docdbEncryptionInTransit.spec.js
@@ -0,0 +1,233 @@
+var expect = require('chai').expect;
+var docdbEncryptionInTransit = require('./docdbEncryptionInTransit');
+
+const describeDBClusters = [
+ {
+ AvailabilityZones: [],
+ BackupRetentionPeriod: 7,
+ DBClusterArn: 'arn:aws:rds:us-east-1:000011112222:cluster:docdb-2021-11-10-10-16-10',
+ DBClusterIdentifier: 'docdb-2021-11-10-10-16-10',
+ DBClusterParameterGroup: 'custom-docdb-param-group',
+ DBSubnetGroup: 'default-vpc-99de2fe4',
+ Status: 'available',
+ Engine: 'docdb',
+ EngineVersion: '4.0.0'
+ },
+ {
+ AvailabilityZones: [],
+ BackupRetentionPeriod: 7,
+ DBClusterArn: 'arn:aws:rds:us-east-1:000011112223:cluster:docdb-2021-11-10-10-16-11',
+ DBClusterIdentifier: 'docdb-2021-11-10-10-16-11',
+ DBClusterParameterGroup: 'custom-docdb-param-group-disabled',
+ DBSubnetGroup: 'default-vpc-99de2fe4',
+ Status: 'available',
+ Engine: 'docdb',
+ EngineVersion: '4.0.0'
+ },
+ {
+ AvailabilityZones: [],
+ BackupRetentionPeriod: 7,
+ DBClusterArn: 'arn:aws:rds:us-east-1:000011112224:cluster:docdb-2021-11-10-10-16-12',
+ DBClusterIdentifier: 'docdb-2021-11-10-10-16-12',
+ DBClusterParameterGroup: 'default.docdb4.0',
+ DBSubnetGroup: 'default-vpc-99de2fe4',
+ Status: 'available',
+ Engine: 'docdb',
+ EngineVersion: '4.0.0'
+ },
+ {
+ AvailabilityZones: [],
+ BackupRetentionPeriod: 7,
+ DBClusterArn: 'arn:aws:rds:us-east-1:000011112225:cluster:docdb-2021-11-10-10-16-13',
+ DBClusterIdentifier: 'docdb-2021-11-10-10-16-13',
+ DBSubnetGroup: 'default-vpc-99de2fe4',
+ Status: 'available',
+ Engine: 'docdb',
+ EngineVersion: '4.0.0'
+ }
+];
+
+const clusterParameters = {
+ 'custom-docdb-param-group': {
+ Parameters: [
+ {
+ ParameterName: 'tls',
+ ParameterValue: 'enabled',
+ Description: 'Enable TLS encryption',
+ Source: 'user',
+ ApplyType: 'static',
+ DataType: 'string',
+ AllowedValues: 'enabled,disabled',
+ IsModifiable: true
+ }
+ ]
+ },
+ 'custom-docdb-param-group-disabled': {
+ Parameters: [
+ {
+ ParameterName: 'tls',
+ ParameterValue: 'disabled',
+ Description: 'Enable TLS encryption',
+ Source: 'user',
+ ApplyType: 'static',
+ DataType: 'string',
+ AllowedValues: 'enabled,disabled',
+ IsModifiable: true
+ }
+ ]
+ }
+};
+
+const createCache = (clusters, clustersErr, parameters, parametersErr) => {
+ var cache = {
+ docdb: {
+ describeDBClusters: {
+ 'us-east-1': {
+ err: clustersErr,
+ data: clusters
+ },
+ },
+ }
+ };
+
+ if (parameters) {
+ cache.docdb = cache.docdb || {};
+ cache.docdb.describeDBClusterParameters = {
+ 'us-east-1': {}
+ };
+ for (var groupName in parameters) {
+ cache.docdb.describeDBClusterParameters['us-east-1'][groupName] = {
+ err: parametersErr,
+ data: parameters[groupName]
+ };
+ }
+ }
+
+ return cache;
+};
+
+describe('docdbEncryptionInTransit', function () {
+ describe('run', function () {
+ it('should PASS if DocumentDB cluster has TLS enabled in custom parameter group', function (done) {
+ const cache = createCache([describeDBClusters[0]], null, clusterParameters);
+ docdbEncryptionInTransit.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(0);
+ expect(results[0].message).to.include('has TLS encryption in transit enabled');
+ expect(results[0].region).to.equal('us-east-1');
+ done();
+ });
+ });
+
+ it('should FAIL if DocumentDB cluster has TLS disabled in parameter group', function (done) {
+ const cache = createCache([describeDBClusters[1]], null, clusterParameters);
+ docdbEncryptionInTransit.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(2);
+ expect(results[0].message).to.include('does not have TLS encryption in transit enabled');
+ expect(results[0].region).to.equal('us-east-1');
+ done();
+ });
+ });
+
+ it('should FAIL if DocumentDB cluster uses default parameter group', function (done) {
+ const cache = createCache([describeDBClusters[2]], null, {
+ 'default.docdb4.0': {
+ Parameters: [
+ {
+ ParameterName: 'tls',
+ ParameterValue: 'disabled',
+ Description: 'Enable TLS encryption',
+ Source: 'system',
+ ApplyType: 'static',
+ DataType: 'string',
+ AllowedValues: 'enabled,disabled',
+ IsModifiable: false
+ }
+ ]
+ }
+ });
+ docdbEncryptionInTransit.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(2);
+ expect(results[0].message).to.include('does not have TLS encryption in transit enabled');
+ expect(results[0].region).to.equal('us-east-1');
+ done();
+ });
+ });
+
+ it('should FAIL if DocumentDB cluster has no parameter group', function (done) {
+ const cache = createCache([describeDBClusters[3]], null);
+ docdbEncryptionInTransit.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(2);
+ expect(results[0].message).to.include('does not have a parameter group associated');
+ expect(results[0].region).to.equal('us-east-1');
+ done();
+ });
+ });
+
+ it('should PASS if no DocumentDB clusters found', function (done) {
+ const cache = createCache([]);
+ docdbEncryptionInTransit.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(0);
+ expect(results[0].message).to.include('No DocumentDB clusters found');
+ expect(results[0].region).to.equal('us-east-1');
+ done();
+ });
+ });
+
+ it('should UNKNOWN if unable to list DocumentDB clusters', function (done) {
+ const cache = createCache(null, { message: "Unable to list DocumentDB Clusters" });
+ docdbEncryptionInTransit.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(3);
+ expect(results[0].message).to.include('Unable to list DocumentDB clusters:');
+ expect(results[0].region).to.equal('us-east-1');
+ done();
+ });
+ });
+
+ it('should UNKNOWN if unable to query cluster parameters', function (done) {
+ const cache = createCache([describeDBClusters[0]], null, null, { message: "Unable to query parameters" });
+ docdbEncryptionInTransit.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(3);
+ expect(results[0].message).to.include('Unable to query cluster parameters');
+ expect(results[0].region).to.equal('us-east-1');
+ done();
+ });
+ });
+
+ it('should FAIL if cluster parameters data is null', function (done) {
+ const cache = createCache([describeDBClusters[0]], null, {
+ 'custom-docdb-param-group': {
+ Parameters: null
+ }
+ });
+ docdbEncryptionInTransit.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(2);
+ expect(results[0].message).to.include('does not have TLS encryption in transit enabled');
+ expect(results[0].region).to.equal('us-east-1');
+ done();
+ });
+ });
+
+ it('should FAIL if cluster parameters array is empty', function (done) {
+ const cache = createCache([describeDBClusters[0]], null, {
+ 'custom-docdb-param-group': {
+ Parameters: []
+ }
+ });
+ docdbEncryptionInTransit.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(2);
+ expect(results[0].message).to.include('does not have TLS encryption in transit enabled');
+ expect(results[0].region).to.equal('us-east-1');
+ done();
+ });
+ });
+ });
+});
diff --git a/plugins/aws/ec2/amiNamingConvention.js b/plugins/aws/ec2/amiNamingConvention.js
new file mode 100644
index 000000000..d3af4fe89
--- /dev/null
+++ b/plugins/aws/ec2/amiNamingConvention.js
@@ -0,0 +1,84 @@
+var async = require('async');
+var helpers = require('../../../helpers/aws');
+
+module.exports = {
+ title: 'AMI Naming Conventions',
+ category: 'EC2',
+ domain: 'Compute',
+ severity: 'Low',
+ description: 'Ensure that Amazon Machine Images (AMIs) follow organizational naming conventions for tagging',
+ more_info: 'AMIs should follow a consistent naming convention using the Name tag to identify their purpose, environment, and region. This helps prevent accidental use of incorrect images, reduces operational errors, and improves resource management. Without proper naming conventions, teams may deploy instances with outdated or inappropriate AMIs, leading to security vulnerabilities or configuration issues.',
+ link: 'https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Using_Tags.html',
+ recommended_action: 'Update AMI Name tags to follow organizational naming conventions.',
+ apis: ['EC2:describeImages'],
+ settings: {
+ ami_naming_pattern: {
+ name: 'AMI Naming Pattern',
+ description: 'A regex pattern to validate AMI Name tag values. Default: ^ami-(ue1|uw1|uw2|ew1|ec1|an1|an2|as1|as2|se1)-(d|t|s|p)-([a-z0-9\\-]+)$',
+ regex: '^.*$',
+ default: '^ami-(ue1|uw1|uw2|ew1|ec1|an1|an2|as1|as2|se1)-(d|t|s|p)-([a-z0-9\\-]+)$'
+ }
+ },
+ realtime_triggers: ['ec2:CreateImage', 'ec2:CreateTags', 'ec2:DeleteTags', 'ec2:DeregisterImage'],
+
+ run: function(cache, settings, callback) {
+ var results = [];
+ var source = {};
+ var regions = helpers.regions(settings);
+ var awsOrGov = helpers.defaultPartition(settings);
+
+ var config = {
+ ami_naming_pattern: settings.ami_naming_pattern || this.settings.ami_naming_pattern.default
+ };
+
+ var namingPattern = new RegExp(config.ami_naming_pattern);
+
+ async.each(regions.ec2, function(region, rcb){
+ var describeImages = helpers.addSource(cache, source,
+ ['ec2', 'describeImages', region]);
+
+ if (!describeImages) return rcb();
+
+ if (describeImages.err || !describeImages.data) {
+ helpers.addResult(results, 3,
+ 'Unable to query for AMIs: ' + helpers.addError(describeImages), region);
+ return rcb();
+ }
+
+ if (!describeImages.data.length) {
+ helpers.addResult(results, 0, 'No AMIs found', region);
+ return rcb();
+ }
+
+ for (var ami of describeImages.data) {
+ if (!ami.ImageId) continue;
+
+ const arn = 'arn:' + awsOrGov + ':ec2:' + region + '::image/' + ami.ImageId;
+
+ if (!ami.Tags || !ami.Tags.length) {
+ helpers.addResult(results, 2,
+ 'AMI does not have a name tag', region, arn);
+ continue;
+ }
+
+ var nameTag = ami.Tags.find(tag => tag.Key === 'Name');
+
+ if (!nameTag || !nameTag.Value) {
+ helpers.addResult(results, 2,
+ 'AMI does not have a name tag', region, arn);
+ } else if (!namingPattern.test(nameTag.Value)) {
+ helpers.addResult(results, 2,
+ `AMI Name tag "${nameTag.Value}" does not follow organizational naming convention`, region, arn);
+ } else {
+ helpers.addResult(results, 0,
+ `AMI Name tag "${nameTag.Value}" follows organizational naming convention`, region, arn);
+ }
+ }
+
+ rcb();
+ }, function(){
+ callback(null, results, source);
+ });
+ }
+};
+
diff --git a/plugins/aws/ec2/amiNamingConvention.spec.js b/plugins/aws/ec2/amiNamingConvention.spec.js
new file mode 100644
index 000000000..e36c7b1ed
--- /dev/null
+++ b/plugins/aws/ec2/amiNamingConvention.spec.js
@@ -0,0 +1,132 @@
+var expect = require('chai').expect;
+const amiNamingConvention = require('./amiNamingConvention');
+
+const describeImages = [
+ {
+ ImageId: 'ami-046b09f5340dfd8gb',
+ Tags: [
+ { Key: 'Name', Value: 'ami-ue1-p-nodejs' }
+ ]
+ },
+ {
+ ImageId: 'ami-046b09f5340dfd8gc',
+ Tags: [
+ { Key: 'Name', Value: 'ami-uw2-d-apache-spark' }
+ ]
+ },
+ {
+ ImageId: 'ami-046b09f5340dfd8gd',
+ Tags: [
+ { Key: 'Name', Value: 'MyCustomAMI' }
+ ]
+ },
+ {
+ ImageId: 'ami-046b09f5340dfd8ge',
+ Tags: [
+ { Key: 'Environment', Value: 'Production' }
+ ]
+ },
+ {
+ ImageId: 'ami-046b09f5340dfd8gf',
+ Tags: []
+ }
+];
+
+const createCache = (instances) => {
+ return {
+ ec2: {
+ describeImages: {
+ 'us-east-1': {
+ data: instances
+ },
+ },
+ },
+ };
+};
+
+const createErrorCache = () => {
+ return {
+ ec2: {
+ describeImages: {
+ 'us-east-1': {
+ err: {
+ message: 'error describing AMIs'
+ }
+ },
+ },
+ },
+ };
+};
+
+
+describe('amiNamingConvention', function () {
+ describe('run', function () {
+
+ it('should return UNKNOWN if unable to query for AMIs', function (done) {
+ const cache = createErrorCache();
+ amiNamingConvention.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(3);
+ expect(results[0].region).to.equal('us-east-1');
+ expect(results[0].message).to.include('Unable to query for AMIs');
+ done();
+ });
+ });
+
+ it('should return PASS if no AMIs found', function (done) {
+ const cache = createCache([]);
+ amiNamingConvention.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(0);
+ expect(results[0].region).to.equal('us-east-1');
+ expect(results[0].message).to.include('No AMIs found');
+ done();
+ });
+ });
+
+ it('should return PASS if AMI Name tag follows naming convention', function (done) {
+ const cache = createCache([describeImages[0]]);
+ amiNamingConvention.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(0);
+ expect(results[0].region).to.equal('us-east-1');
+ expect(results[0].message).to.include('follows organizational naming convention');
+ done();
+ });
+ });
+
+ it('should return FAIL if AMI Name tag does not follow naming convention', function (done) {
+ const cache = createCache([describeImages[2]]);
+ amiNamingConvention.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(2);
+ expect(results[0].region).to.equal('us-east-1');
+ expect(results[0].message).to.include('does not follow organizational naming convention');
+ done();
+ });
+ });
+
+ it('should return FAIL if AMI does not have a name tag', function (done) {
+ const cache = createCache([describeImages[3]]);
+ amiNamingConvention.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(2);
+ expect(results[0].region).to.equal('us-east-1');
+ expect(results[0].message).to.include('AMI does not have a name tag');
+ done();
+ });
+ });
+
+ it('should return FAIL if AMI has empty tags array', function (done) {
+ const cache = createCache([describeImages[4]]);
+ amiNamingConvention.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(2);
+ expect(results[0].region).to.equal('us-east-1');
+ expect(results[0].message).to.include('AMI does not have a name tag');
+ done();
+ });
+ });
+ });
+});
+
diff --git a/plugins/aws/ec2/ebsRecentSnapshots.js b/plugins/aws/ec2/ebsRecentSnapshots.js
index 4e8d9016c..6f875b5d3 100644
--- a/plugins/aws/ec2/ebsRecentSnapshots.js
+++ b/plugins/aws/ec2/ebsRecentSnapshots.js
@@ -6,14 +6,26 @@ module.exports = {
category: 'EC2',
domain: 'Compute',
severity: 'Medium',
- description: 'Ensures that EBS volume has had a snapshot within the last 7 days',
+ description: 'Ensures that EBS volume has had a recent snapshot within the configured time period',
more_info: 'EBS volumes without recent snapshots may be at risk of data loss or recovery issues.',
link: 'https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSSnapshots.html',
- recommended_action: 'Create a new snapshot for EBS volume weekly.',
+ recommended_action: 'Create a new snapshot for EBS volume within the configured time period.',
apis: ['EC2:describeSnapshots','STS:getCallerIdentity'],
+ settings: {
+ ebs_recent_snapshot_days: {
+ name: 'EBS Recent Snapshot Days',
+ description: 'Number of days to consider a snapshot as recent. Snapshots older than this will be flagged as FAIL.',
+ regex: '^[1-9]{1}[0-9]{0,2}$',
+ default: '7'
+ }
+ },
realtime_triggers: ['ec2:CreateSnapshot', 'ec2:DeleteSnapshot'],
run: function(cache, settings, callback) {
+ var config = {
+ ebs_recent_snapshot_days: parseInt(settings.ebs_recent_snapshot_days || this.settings.ebs_recent_snapshot_days.default)
+ };
+
var results = [];
var source = {};
var regions = helpers.regions(settings);
@@ -44,7 +56,7 @@ module.exports = {
var snapshotTime = new Date(snapshot.StartTime);
var difference = Math.floor((today -snapshotTime) / (1000 * 60 * 60 * 24));
- if (difference > 7){
+ if (difference > config.ebs_recent_snapshot_days){
helpers.addResult(results, 2,
'EBS volume does not have a recent snapshot', region,resource);
} else {
diff --git a/plugins/aws/ec2/ebsRecentSnapshots.spec.js b/plugins/aws/ec2/ebsRecentSnapshots.spec.js
index 686316e9b..0f4bd7dcb 100644
--- a/plugins/aws/ec2/ebsRecentSnapshots.spec.js
+++ b/plugins/aws/ec2/ebsRecentSnapshots.spec.js
@@ -7,6 +7,9 @@ snapshotPass.setDate(snapshotPass.getDate() - 1);
var snapshotFail = new Date();
snapshotFail.setDate(snapshotFail.getDate() - 10);
+var snapshotCustom = new Date();
+snapshotCustom.setDate(snapshotCustom.getDate() - 15);
+
const describeSnapshots = [
{
"Description": "",
@@ -47,6 +50,18 @@ const describeSnapshots = [
"VolumeId": "vol-02c402f5a6a02c6e7",
"VolumeSize": 1,
"Tags": []
+ },
+ {
+ "Description": "Custom test snapshot",
+ "Encrypted": false,
+ "OwnerId": "112233445566",
+ "Progress": "100%",
+ "SnapshotId": "snap-04custom567890abc",
+ "StartTime": snapshotCustom,
+ "State": "completed",
+ "VolumeId": "vol-03custom567890def",
+ "VolumeSize": 10,
+ "Tags": []
}
];
@@ -135,5 +150,40 @@ describe('ebsRecentSnapshots', function () {
done();
});
});
+
+ it('should use custom snapshot age threshold when setting is provided', function (done) {
+ const cache = createCache([describeSnapshots[3]]); // 15-day old snapshot
+ const settings = { ebs_recent_snapshot_days: '20' };
+ ebsRecentSnapshots.run(cache, settings, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(0);
+ expect(results[0].region).to.equal('us-east-1');
+ expect(results[0].message).to.include('EBS volume has a recent snapshot');
+ done();
+ });
+ });
+
+ it('should FAIL when snapshot is older than custom threshold', function (done) {
+ const cache = createCache([describeSnapshots[3]]); // 15-day old snapshot
+ const settings = { ebs_recent_snapshot_days: '10' };
+ ebsRecentSnapshots.run(cache, settings, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(2);
+ expect(results[0].region).to.equal('us-east-1');
+ expect(results[0].message).to.include('EBS volume does not have a recent snapshot');
+ done();
+ });
+ });
+
+ it('should use default 7 days when no setting is provided', function (done) {
+ const cache = createCache([describeSnapshots[1]]); // 10-day old snapshot
+ ebsRecentSnapshots.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(2);
+ expect(results[0].region).to.equal('us-east-1');
+ expect(results[0].message).to.include('EBS volume does not have a recent snapshot');
+ done();
+ });
+ });
});
});
\ No newline at end of file
diff --git a/plugins/aws/ec2/ec2NetworkExposure.js b/plugins/aws/ec2/ec2NetworkExposure.js
index 47b969f9d..1892d5c6f 100644
--- a/plugins/aws/ec2/ec2NetworkExposure.js
+++ b/plugins/aws/ec2/ec2NetworkExposure.js
@@ -2,7 +2,7 @@ var async = require('async');
var helpers = require('../../../helpers/aws');
module.exports = {
- title: 'Network Exposure',
+ title: 'Internet Exposure',
category: 'EC2',
domain: 'Compute',
severity: 'Info',
@@ -64,3 +64,4 @@ module.exports = {
});
}
};
+
diff --git a/plugins/aws/ec2/ec2PrivilegeAnalysis.js b/plugins/aws/ec2/ec2PrivilegeAnalysis.js
new file mode 100644
index 000000000..997c448e7
--- /dev/null
+++ b/plugins/aws/ec2/ec2PrivilegeAnalysis.js
@@ -0,0 +1,19 @@
+module.exports = {
+ title: 'Privilege Analysis',
+ category: 'EC2',
+ domain: 'Compute',
+ severity: 'Info',
+ description: 'Check if EC2 instances are overly permissive.',
+ more_info: 'EC2 instances exposed to the internet are at a higher risk of unauthorized access, data breaches, and cyberattacks. It’s crucial to limit exposure by securing access through proper configuration of security groups, NACLs, and route tables.',
+ link: 'https://docs.aws.amazon.com/vpc/latest/userguide/VPC_Security.html',
+ recommended_action: 'Secure EC2 instances by restricting access with properly configured security groups and NACLs.',
+ apis: [],
+ realtime_triggers: ['ec2:RunInstances','ec2:TerminateInstances'],
+
+ run: function(cache, settings, callback) {
+ var results = [];
+ var source = {};
+ callback(null, results, source);
+
+ }
+};
diff --git a/plugins/aws/ec2/oldAmi.js b/plugins/aws/ec2/oldAmi.js
new file mode 100644
index 000000000..64df9a9ce
--- /dev/null
+++ b/plugins/aws/ec2/oldAmi.js
@@ -0,0 +1,82 @@
+var async = require('async');
+var helpers = require('../../../helpers/aws');
+
+module.exports = {
+ title: 'Old Amazon Machine Images',
+ category: 'EC2',
+ domain: 'Compute',
+ severity: 'Low',
+ description: 'Ensure that Amazon Machine Images (AMIs) are not older than a specified number of days.',
+ more_info: 'Amazon Machine Images that are too old may contain outdated software, security vulnerabilities, or deprecated configurations. Regularly updating and replacing old AMIs helps maintain security and operational efficiency.',
+ link: 'https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/AMIs.html',
+ recommended_action: 'Review and replace AMIs that are older than the specified threshold with newer versions.',
+ apis: ['EC2:describeImages'],
+ settings: {
+ ami_age_fail: {
+ name: 'AMI Age Fail',
+ description: 'Return a failing result when AMI exceeds this number of days',
+ regex: '^[1-9]{1}[0-9]{0,3}$',
+ default: 90
+ }
+ },
+ realtime_triggers: ['ec2:CreateImage', 'ec2:DeregisterImage'],
+
+ run: function(cache, settings, callback) {
+ var results = [];
+ var source = {};
+ var regions = helpers.regions(settings);
+ var awsOrGov = helpers.defaultPartition(settings);
+
+ var config = {
+ ami_age_fail: parseInt(settings.ami_age_fail || this.settings.ami_age_fail.default),
+ };
+
+ async.each(regions.ec2, function(region, rcb){
+ var describeImages = helpers.addSource(cache, source,
+ ['ec2', 'describeImages', region]);
+
+ if (!describeImages) return rcb();
+
+ if (describeImages.err || !describeImages.data) {
+ helpers.addResult(results, 3,
+ 'Unable to query for AMIs: ' + helpers.addError(describeImages), region);
+ return rcb();
+ }
+
+ if (!describeImages.data.length) {
+ helpers.addResult(results, 0, 'No AMIs found', region);
+ return rcb();
+ }
+
+ var now = new Date();
+
+ for (var ami of describeImages.data) {
+ if (!ami.ImageId) continue;
+
+ const arn = 'arn:' + awsOrGov + ':ec2:' + region + '::image/' + ami.ImageId;
+
+ if (!ami.CreationDate) {
+ helpers.addResult(results, 3,
+ 'AMI does not have a creation date', region, arn);
+ continue;
+ }
+
+ var creationDate = new Date(ami.CreationDate);
+ var difference = helpers.daysBetween(creationDate, now);
+
+ if (difference > config.ami_age_fail) {
+ helpers.addResult(results, 2,
+ `AMI is ${Math.floor(difference)} days old`, region, arn);
+ } else {
+ helpers.addResult(results, 0,
+ `AMI is ${Math.floor(difference)} days old`, region, arn);
+ }
+ }
+
+ rcb();
+ }, function(){
+ callback(null, results, source);
+ });
+ }
+};
+
diff --git a/plugins/aws/ec2/oldAmi.spec.js b/plugins/aws/ec2/oldAmi.spec.js
new file mode 100644
index 000000000..330191929
--- /dev/null
+++ b/plugins/aws/ec2/oldAmi.spec.js
@@ -0,0 +1,108 @@
+var expect = require('chai').expect;
+const oldAmi = require('./oldAmi');
+
+const describeImages = [
+ {
+ ImageId: 'ami-046b09f5340dfd8gb',
+ CreationDate: new Date(Date.now() - 100 * 24 * 60 * 60 * 1000).toISOString() // 100 days old
+ },
+ {
+ ImageId: 'ami-046b09f5340dfd8gc',
+ CreationDate: new Date(Date.now() - 70 * 24 * 60 * 60 * 1000).toISOString() // 70 days old
+ },
+ {
+ ImageId: 'ami-046b09f5340dfd8gd',
+ CreationDate: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString() // 30 days old
+ },
+ {
+ ImageId: 'ami-046b09f5340dfd8ge',
+ // No CreationDate
+ }
+];
+
+const createCache = (images) => {
+ return {
+ ec2: {
+ describeImages: {
+ 'us-east-1': {
+ data: images
+ },
+ },
+ },
+ };
+};
+
+const createErrorCache = () => {
+ return {
+ ec2: {
+ describeImages: {
+ 'us-east-1': {
+ err: {
+ message: 'error describing AMIs'
+ }
+ },
+ },
+ },
+ };
+};
+
+describe('oldAmi', function () {
+ describe('run', function () {
+
+ it('should return UNKNOWN if unable to query for AMIs', function (done) {
+ const cache = createErrorCache();
+ oldAmi.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(3);
+ expect(results[0].region).to.equal('us-east-1');
+ expect(results[0].message).to.include('Unable to query for AMIs');
+ done();
+ });
+ });
+
+ it('should return PASS if no AMIs found', function (done) {
+ const cache = createCache([]);
+ oldAmi.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(0);
+ expect(results[0].region).to.equal('us-east-1');
+ expect(results[0].message).to.include('No AMIs found');
+ done();
+ });
+ });
+
+ it('should return FAIL if AMI is older than fail threshold (90 days)', function (done) {
+ const cache = createCache([describeImages[0]]);
+ oldAmi.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(2);
+ expect(results[0].region).to.equal('us-east-1');
+ expect(results[0].message).to.include('100 days old');
+ done();
+ });
+ });
+
+ it('should return PASS if AMI is newer than warn threshold', function (done) {
+ const cache = createCache([describeImages[2]]);
+ oldAmi.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(0);
+ expect(results[0].region).to.equal('us-east-1');
+ expect(results[0].message).to.include('30 days old');
+ done();
+ });
+ });
+
+ it('should return UNKNOWN if AMI does not have a creation date', function (done) {
+ const cache = createCache([describeImages[3]]);
+ oldAmi.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(3);
+ expect(results[0].region).to.equal('us-east-1');
+ expect(results[0].message).to.include('AMI does not have a creation date');
+ done();
+ });
+ });
+ });
+});
+
diff --git a/plugins/aws/ec2/securityGroupRfc1918.js b/plugins/aws/ec2/securityGroupRfc1918.js
index 12b6eff30..abeab2151 100644
--- a/plugins/aws/ec2/securityGroupRfc1918.js
+++ b/plugins/aws/ec2/securityGroupRfc1918.js
@@ -15,7 +15,7 @@ module.exports = {
private_cidrs: {
name: 'EC2 RFC 1918 CIDR Addresses',
description: 'A comma-separated list of CIDRs that indicates reserved private addresses',
- regex: '/^(?=.*[^.]$)((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?).?){4}$/',
+ regex: '^(?:(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?).?){4}/(?:[0-9]|[1-2][0-9]|3[0-2])(?:,(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?).?){4}/(?:[0-9]|[1-2][0-9]|3[0-2]))*)?$',
default: '10.0.0.0/8,172.16.0.0/12,192.168.0.0/16'
}
},
@@ -70,16 +70,15 @@ module.exports = {
}
}
}
-
- if (!privateCidrsFound.length) {
- helpers.addResult(results, 0,
- 'Security group "' + group.GroupName + '" is not configured to allow inbound access from any source IP address within any reserved private addresses',
- region, resource);
- } else {
- helpers.addResult(results, 2,
- 'Security group "' + group.GroupName + '" is configured to allow inbound access from any source IP address within these reserved private addresses: ' + privateCidrsFound.join(', '),
- region, resource);
- }
+ }
+ if (!privateCidrsFound.length) {
+ helpers.addResult(results, 0,
+ 'Security group "' + group.GroupName + '" is not configured to allow inbound access from any source IP address within any reserved private addresses',
+ region, resource);
+ } else {
+ helpers.addResult(results, 2,
+ 'Security group "' + group.GroupName + '" is configured to allow inbound access from any source IP address within these reserved private addresses: ' + privateCidrsFound.join(', '),
+ region, resource);
}
}
rcb();
diff --git a/plugins/aws/ecs/ecsFargatePlatformVersion.js b/plugins/aws/ecs/ecsFargatePlatformVersion.js
new file mode 100644
index 000000000..6d73e9bdf
--- /dev/null
+++ b/plugins/aws/ecs/ecsFargatePlatformVersion.js
@@ -0,0 +1,114 @@
+var async = require('async');
+var helpers = require('../../../helpers/aws');
+
+module.exports = {
+ title: 'ECS Fargate Latest Platform Version',
+ category: 'ECS',
+ domain: 'Containers',
+ severity: 'Medium',
+ description: 'Ensure that Amazon ECS Fargate services are using the latest Fargate platform version.',
+ more_info: 'Using the latest Fargate platform version ensures services benefit from up-to-date security patches, performance improvements, and feature updates.',
+ link: 'https://docs.aws.amazon.com/AmazonECS/latest/developerguide/platform_versions.html',
+ recommended_action: 'Update ECS Fargate services to use the latest platform version.',
+ apis: ['ECS:listClusters', 'ECS:listServices', 'ECS:describeServices'],
+ realtime_triggers: ['ecs:CreateCluster', 'ecs:CreateService', 'ecs:UpdateService', 'ecs:DeleteService', 'ecs:DeleteCluster'],
+ run: function(cache, settings, callback){
+ var results = [];
+ var source = {};
+ var regions = helpers.regions(settings);
+
+ async.each(regions.ecs, function(region, rcb){
+
+ var listClusters = helpers.addSource(cache, source,
+ ['ecs', 'listClusters', region]);
+ if (!listClusters) return rcb();
+
+ if (listClusters.err || !listClusters.data) {
+ helpers.addResult(results, 3,
+ 'Unable to query for ECS clusters: ' + helpers.addError(listClusters), region);
+ return rcb();
+ }
+
+ if (!listClusters.data.length) {
+ helpers.addResult(results, 0, 'No ECS clusters found', region);
+ return rcb();
+ }
+
+ for (var clusterArn of listClusters.data) {
+ var listServices = helpers.addSource(cache, source,
+ ['ecs', 'listServices', region, clusterArn]);
+
+ if (!listServices || listServices.err || !listServices.data) {
+ helpers.addResult(results, 3,
+ 'Unable to query for ECS services: ' + helpers.addError(listServices), region, clusterArn);
+ continue;
+ }
+
+ if (!listServices.data.length) {
+ helpers.addResult(results, 0,
+ 'No ECS Fargate services found in cluster', region, clusterArn);
+ continue;
+ }
+
+ var hasFargateServices = false;
+
+ for (var serviceArn of listServices.data) {
+ var describeServices = helpers.addSource(cache, source,
+ ['ecs', 'describeServices', region, serviceArn]);
+
+ if (!describeServices || describeServices.err || !describeServices.data) {
+ helpers.addResult(results, 3,
+ 'Unable to describe ECS service: ' + helpers.addError(describeServices), region, serviceArn);
+ continue;
+ }
+
+ var service = null;
+ if (describeServices.data.services && describeServices.data.services.length > 0) {
+ service = describeServices.data.services[0];
+ }
+ if (!service) continue;
+
+ var isFargate = false;
+
+ if (service.launchType && service.launchType.toLowerCase() === 'fargate') {
+ isFargate = true;
+ } else if (service.capacityProviderStrategy && service.capacityProviderStrategy.length > 0) {
+ for (var cp of service.capacityProviderStrategy) {
+ if (cp.capacityProvider && cp.capacityProvider.toLowerCase().indexOf('fargate') !== -1) {
+ isFargate = true;
+ break;
+ }
+ }
+ }
+
+ if (!isFargate) continue;
+
+ hasFargateServices = true;
+ var platformVersion = service.platformVersion;
+ var platformVersionLower = platformVersion ? platformVersion.toLowerCase() : '';
+
+ if (platformVersionLower !== 'latest') {
+ helpers.addResult(results, 2,
+ 'ECS Fargate service is not using the latest platform version',
+ region, serviceArn);
+ } else {
+ helpers.addResult(results, 0,
+ 'ECS Fargate service is using the latest platform version',
+ region, serviceArn);
+ }
+ }
+
+ if (!hasFargateServices) {
+ helpers.addResult(results, 0,
+ 'No ECS Fargate services found in cluster',
+ region, clusterArn);
+ }
+ }
+ rcb();
+ },
+ function(){
+ callback(null, results, source);
+ });
+ }
+};
+
diff --git a/plugins/aws/ecs/ecsFargatePlatformVersion.spec.js b/plugins/aws/ecs/ecsFargatePlatformVersion.spec.js
new file mode 100644
index 000000000..fb97e625a
--- /dev/null
+++ b/plugins/aws/ecs/ecsFargatePlatformVersion.spec.js
@@ -0,0 +1,215 @@
+var expect = require('chai').expect;
+const ecsFargatePlatformVersion = require('./ecsFargatePlatformVersion');
+
+const listClusters = [
+ 'arn:aws:ecs:us-east-1:112233445566:cluster/test-cluster',
+ 'arn:aws:ecs:us-east-1:112233445566:cluster/another-cluster'
+];
+
+const createCache = (clusters, servicesMap, describeServicesMap) => {
+ var cache = {
+ ecs: {
+ listClusters: {
+ 'us-east-1': {
+ data: clusters || []
+ }
+ },
+ listServices: {
+ 'us-east-1': {}
+ },
+ describeServices: {
+ 'us-east-1': {}
+ }
+ }
+ };
+
+ if (clusters && clusters.length) {
+ for (var clusterArn of clusters) {
+ if (servicesMap && servicesMap[clusterArn]) {
+ cache.ecs.listServices['us-east-1'][clusterArn] = {
+ data: servicesMap[clusterArn]
+ };
+ } else {
+ cache.ecs.listServices['us-east-1'][clusterArn] = {
+ data: []
+ };
+ }
+ }
+ }
+
+ if (describeServicesMap) {
+ for (var serviceArn in describeServicesMap) {
+ cache.ecs.describeServices['us-east-1'][serviceArn] = {
+ data: describeServicesMap[serviceArn]
+ };
+ }
+ }
+
+ return cache;
+};
+
+const createErrorCache = () => {
+ return {
+ ecs: {
+ listClusters: {
+ 'us-east-1': {
+ err: {
+ message: 'error listing clusters'
+ }
+ }
+ }
+ }
+ };
+};
+
+describe('ecsFargatePlatformVersion', function () {
+ describe('run', function () {
+ it('should PASS if no clusters found', function (done) {
+ const cache = createCache([], {}, {});
+ ecsFargatePlatformVersion.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(0);
+ expect(results[0].message).to.include('No ECS clusters found');
+ done();
+ });
+ });
+
+ it('should PASS if no Fargate services found', function (done) {
+ const servicesMap = {
+ 'arn:aws:ecs:us-east-1:112233445566:cluster/test-cluster': [
+ 'arn:aws:ecs:us-east-1:112233445566:service/test-cluster/my-service'
+ ]
+ };
+ const describeServicesMap = {
+ 'arn:aws:ecs:us-east-1:112233445566:service/test-cluster/my-service': {
+ services: [{
+ serviceName: 'my-service',
+ serviceArn: 'arn:aws:ecs:us-east-1:112233445566:service/test-cluster/my-service',
+ launchType: 'EC2'
+ }]
+ }
+ };
+ const cache = createCache([listClusters[0]], servicesMap, describeServicesMap);
+ ecsFargatePlatformVersion.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(0);
+ expect(results[0].message).to.include('No ECS Fargate services found');
+ done();
+ });
+ });
+
+ it('should PASS if Linux Fargate service uses LATEST platform version', function (done) {
+ const servicesMap = {
+ 'arn:aws:ecs:us-east-1:112233445566:cluster/test-cluster': [
+ 'arn:aws:ecs:us-east-1:112233445566:service/test-cluster/my-service'
+ ]
+ };
+ const describeServicesMap = {
+ 'arn:aws:ecs:us-east-1:112233445566:service/test-cluster/my-service': {
+ services: [{
+ serviceName: 'my-service',
+ serviceArn: 'arn:aws:ecs:us-east-1:112233445566:service/test-cluster/my-service',
+ launchType: 'FARGATE',
+ platformVersion: 'LATEST',
+ platformFamily: 'LINUX'
+ }]
+ }
+ };
+ const cache = createCache([listClusters[0]], servicesMap, describeServicesMap);
+ ecsFargatePlatformVersion.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(0);
+ expect(results[0].message).to.include('using the latest platform version');
+ done();
+ });
+ });
+
+ it('should FAIL if Linux Fargate service uses 1.3.0 platform version', function (done) {
+ const servicesMap = {
+ 'arn:aws:ecs:us-east-1:112233445566:cluster/test-cluster': [
+ 'arn:aws:ecs:us-east-1:112233445566:service/test-cluster/my-service'
+ ]
+ };
+ const describeServicesMap = {
+ 'arn:aws:ecs:us-east-1:112233445566:service/test-cluster/my-service': {
+ services: [{
+ serviceName: 'my-service',
+ serviceArn: 'arn:aws:ecs:us-east-1:112233445566:service/test-cluster/my-service',
+ launchType: 'FARGATE',
+ platformVersion: '1.3.0',
+ platformFamily: 'LINUX'
+ }]
+ }
+ };
+ const cache = createCache([listClusters[0]], servicesMap, describeServicesMap);
+ ecsFargatePlatformVersion.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(2);
+ expect(results[0].message).to.include('is not using the latest platform version');
+ done();
+ });
+ });
+
+ it('should PASS if Windows Fargate service uses LATEST platform version', function (done) {
+ const servicesMap = {
+ 'arn:aws:ecs:us-east-1:112233445566:cluster/test-cluster': [
+ 'arn:aws:ecs:us-east-1:112233445566:service/test-cluster/my-service'
+ ]
+ };
+ const describeServicesMap = {
+ 'arn:aws:ecs:us-east-1:112233445566:service/test-cluster/my-service': {
+ services: [{
+ serviceName: 'my-service',
+ serviceArn: 'arn:aws:ecs:us-east-1:112233445566:service/test-cluster/my-service',
+ launchType: 'FARGATE',
+ platformVersion: 'LATEST',
+ platformFamily: 'WINDOWS_SERVER'
+ }]
+ }
+ };
+ const cache = createCache([listClusters[0]], servicesMap, describeServicesMap);
+ ecsFargatePlatformVersion.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(0);
+ expect(results[0].message).to.include('using the latest platform version');
+ done();
+ });
+ });
+
+ it('should FAIL if Fargate service has no platform version configured', function (done) {
+ const servicesMap = {
+ 'arn:aws:ecs:us-east-1:112233445566:cluster/test-cluster': [
+ 'arn:aws:ecs:us-east-1:112233445566:service/test-cluster/my-service'
+ ]
+ };
+ const describeServicesMap = {
+ 'arn:aws:ecs:us-east-1:112233445566:service/test-cluster/my-service': {
+ services: [{
+ serviceName: 'my-service',
+ serviceArn: 'arn:aws:ecs:us-east-1:112233445566:service/test-cluster/my-service',
+ launchType: 'FARGATE',
+ platformFamily: 'LINUX'
+ }]
+ }
+ };
+ const cache = createCache([listClusters[0]], servicesMap, describeServicesMap);
+ ecsFargatePlatformVersion.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(2);
+ expect(results[0].message).to.include('is not using the latest platform version');
+ done();
+ });
+ });
+
+ it('should UNKNOWN if unable to list clusters', function (done) {
+ const cache = createErrorCache();
+ ecsFargatePlatformVersion.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(3);
+ expect(results[0].message).to.include('Unable to query');
+ done();
+ });
+ });
+ });
+});
+
diff --git a/plugins/aws/ecs/ecsServicePublicIpDisabled.js b/plugins/aws/ecs/ecsServicePublicIpDisabled.js
new file mode 100644
index 000000000..e3f65eb3a
--- /dev/null
+++ b/plugins/aws/ecs/ecsServicePublicIpDisabled.js
@@ -0,0 +1,96 @@
+var async = require('async');
+var helpers = require('../../../helpers/aws');
+
+module.exports = {
+ title: 'ECS Service Public IP Disabled',
+ category: 'ECS',
+ domain: 'Containers',
+ severity: 'High',
+ description: 'Ensure that Amazon ECS services have assignPublicIp set to disabled.',
+ more_info: 'Enabling public IP assignment could expose container application servers to unintended or unauthorized access. Services should use private networking with NAT gateways or VPC endpoints for outbound internet access.',
+ link: 'https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-networking.html',
+ recommended_action: 'Modify ECS services to set assignPublicIp to disabled in the network configuration.',
+ apis: ['ECS:listClusters', 'ECS:listServices', 'ECS:describeServices'],
+ realtime_triggers: ['ecs:CreateService', 'ecs:UpdateService', 'ecs:DeleteService', 'ecs:CreateCluster', 'ecs:DeleteCluster', 'ecs:UpdateCluster'],
+
+ run: function(cache, settings, callback) {
+ var results = [];
+ var source = {};
+ var regions = helpers.regions(settings);
+
+ async.each(regions.ecs, function(region, rcb) {
+ var listClusters = helpers.addSource(cache, source,
+ ['ecs', 'listClusters', region]);
+
+ if (!listClusters) return rcb();
+
+ if (listClusters.err || !listClusters.data) {
+ helpers.addResult(results, 3,
+ 'Unable to query for ECS clusters: ' + helpers.addError(listClusters), region);
+ return rcb();
+ }
+
+ if (!listClusters.data.length) {
+ helpers.addResult(results, 0, 'No ECS clusters found', region);
+ return rcb();
+ }
+
+ for (var clusterArn of listClusters.data) {
+ var listServices = helpers.addSource(cache, source,
+ ['ecs', 'listServices', region, clusterArn]);
+
+ if (!listServices || listServices.err || !listServices.data) {
+ helpers.addResult(results, 3,
+ 'Unable to query for ECS services: ' + helpers.addError(listServices), region, clusterArn);
+ continue;
+ }
+
+ if (!listServices.data.length) {
+ helpers.addResult(results, 0,
+ 'No ECS services found in cluster', region, clusterArn);
+ continue;
+ }
+
+ for (var serviceArn of listServices.data) {
+ var describeServices = helpers.addSource(cache, source,
+ ['ecs', 'describeServices', region, serviceArn]);
+
+ if (!describeServices || describeServices.err || !describeServices.data) {
+ helpers.addResult(results, 3,
+ 'Unable to describe ECS service: ' + helpers.addError(describeServices), region, serviceArn);
+ continue;
+ }
+
+
+ var service = null;
+ if (describeServices.data.services && describeServices.data.services.length > 0) {
+ service = describeServices.data.services[0];
+ }
+ if (!service) continue;
+
+ var networkMode = service.networkConfiguration;
+ var assignPublicIp = null;
+
+ if (networkMode && networkMode.awsvpcConfiguration) {
+ assignPublicIp = networkMode.awsvpcConfiguration.assignPublicIp;
+ var assignPublicIpLower = assignPublicIp ? assignPublicIp.toLowerCase() : '';
+ if (assignPublicIpLower !== 'disabled') {
+ helpers.addResult(results, 2,
+ 'ECS service does not have assignPublicIp set to disabled',
+ region, serviceArn);
+ } else {
+ helpers.addResult(results, 0,
+ 'ECS service has assignPublicIp set to disabled',
+ region, serviceArn);
+ }
+ }
+ }
+ }
+ rcb();
+ }, function() {
+ callback(null, results, source);
+ });
+ }
+};
+
+
diff --git a/plugins/aws/ecs/ecsServicePublicIpDisabled.spec.js b/plugins/aws/ecs/ecsServicePublicIpDisabled.spec.js
new file mode 100644
index 000000000..3bbaeb4d8
--- /dev/null
+++ b/plugins/aws/ecs/ecsServicePublicIpDisabled.spec.js
@@ -0,0 +1,158 @@
+var expect = require('chai').expect;
+const ecsServicePublicIpDisabled = require('./ecsServicePublicIpDisabled');
+
+const listClusters = [
+ 'arn:aws:ecs:us-east-1:112233445566:cluster/test-cluster',
+ 'arn:aws:ecs:us-east-1:112233445566:cluster/another-cluster'
+];
+
+const createCache = (clusters, servicesMap, describeServicesMap) => {
+ var cache = {
+ ecs: {
+ listClusters: {
+ 'us-east-1': {
+ data: clusters || []
+ }
+ },
+ listServices: {
+ 'us-east-1': {}
+ },
+ describeServices: {
+ 'us-east-1': {}
+ }
+ }
+ };
+
+ if (clusters && clusters.length) {
+ for (var clusterArn of clusters) {
+ if (servicesMap && servicesMap[clusterArn]) {
+ cache.ecs.listServices['us-east-1'][clusterArn] = {
+ data: servicesMap[clusterArn]
+ };
+ } else {
+ cache.ecs.listServices['us-east-1'][clusterArn] = {
+ data: []
+ };
+ }
+ }
+ }
+
+ if (describeServicesMap) {
+ for (var serviceArn in describeServicesMap) {
+ cache.ecs.describeServices['us-east-1'][serviceArn] = {
+ data: describeServicesMap[serviceArn]
+ };
+ }
+ }
+
+ return cache;
+};
+
+const createErrorCache = () => {
+ return {
+ ecs: {
+ listClusters: {
+ 'us-east-1': {
+ err: {
+ message: 'error listing clusters'
+ }
+ }
+ }
+ }
+ };
+};
+
+describe('ecsServicePublicIpDisabled', function () {
+ describe('run', function () {
+ it('should PASS if no clusters found', function (done) {
+ const cache = createCache([], {}, {});
+ ecsServicePublicIpDisabled.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(0);
+ expect(results[0].message).to.include('No ECS clusters found');
+ done();
+ });
+ });
+
+ it('should PASS if no services found', function (done) {
+ const cache = createCache(listClusters, {}, {});
+ ecsServicePublicIpDisabled.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(2);
+ expect(results[0].status).to.equal(0);
+ expect(results[0].message).to.include('No ECS services found in cluster');
+ done();
+ });
+ });
+
+ it('should PASS if service has assignPublicIp set to disabled', function (done) {
+ const servicesMap = {
+ 'arn:aws:ecs:us-east-1:112233445566:cluster/test-cluster': [
+ 'arn:aws:ecs:us-east-1:112233445566:service/test-cluster/my-service'
+ ]
+ };
+ const describeServicesMap = {
+ 'arn:aws:ecs:us-east-1:112233445566:service/test-cluster/my-service': {
+ services: [{
+ serviceName: 'my-service',
+ serviceArn: 'arn:aws:ecs:us-east-1:112233445566:service/test-cluster/my-service',
+ networkConfiguration: {
+ awsvpcConfiguration: {
+ assignPublicIp: 'DISABLED',
+ subnets: ['subnet-12345'],
+ securityGroups: ['sg-12345']
+ }
+ }
+ }]
+ }
+ };
+ const cache = createCache([listClusters[0]], servicesMap, describeServicesMap);
+ ecsServicePublicIpDisabled.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(0);
+ expect(results[0].message).to.include('has assignPublicIp set to disabled');
+ done();
+ });
+ });
+
+ it('should FAIL if service has assignPublicIp set to ENABLED', function (done) {
+ const servicesMap = {
+ 'arn:aws:ecs:us-east-1:112233445566:cluster/test-cluster': [
+ 'arn:aws:ecs:us-east-1:112233445566:service/test-cluster/my-service'
+ ]
+ };
+ const describeServicesMap = {
+ 'arn:aws:ecs:us-east-1:112233445566:service/test-cluster/my-service': {
+ services: [{
+ serviceName: 'my-service',
+ serviceArn: 'arn:aws:ecs:us-east-1:112233445566:service/test-cluster/my-service',
+ networkConfiguration: {
+ awsvpcConfiguration: {
+ assignPublicIp: 'ENABLED',
+ subnets: ['subnet-12345'],
+ securityGroups: ['sg-12345']
+ }
+ }
+ }]
+ }
+ };
+ const cache = createCache([listClusters[0]], servicesMap, describeServicesMap);
+ ecsServicePublicIpDisabled.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(2);
+ expect(results[0].message).to.include('does not have assignPublicIp set to disabled');
+ done();
+ });
+ });
+
+ it('should UNKNOWN if unable to list clusters', function (done) {
+ const cache = createErrorCache();
+ ecsServicePublicIpDisabled.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(3);
+ expect(results[0].message).to.include('Unable to query');
+ done();
+ });
+ });
+ });
+});
+
diff --git a/plugins/aws/eks/eksKubernetesVersion.js b/plugins/aws/eks/eksKubernetesVersion.js
index 478e4993a..26d362c82 100644
--- a/plugins/aws/eks/eksKubernetesVersion.js
+++ b/plugins/aws/eks/eksKubernetesVersion.js
@@ -42,7 +42,10 @@ module.exports = {
'1.26': '2024-06-11',
'1.27': '2024-07-24',
'1.28': '2024-11-26',
- '1.29': '2025-03-23'
+ '1.29': '2025-03-23',
+ '1.30': '2025-07-23',
+ '1.31': '2025-11-26',
+ '1.32': '2026-03-23'
};
var outdatedVersions = {
diff --git a/plugins/aws/eks/eksKubernetesVersion.spec.js b/plugins/aws/eks/eksKubernetesVersion.spec.js
index bf84f1ebd..3d36ed45d 100644
--- a/plugins/aws/eks/eksKubernetesVersion.spec.js
+++ b/plugins/aws/eks/eksKubernetesVersion.spec.js
@@ -82,7 +82,7 @@ describe('eksKubernetesVersion', function () {
"cluster": {
"name": "mycluster",
"arn": "arn:aws:eks:us-east-1:012345678911:cluster/mycluster",
- "version": "1.30",
+ "version": "1.32",
}
}
);
diff --git a/plugins/aws/eks/eksNetworkExposure.js b/plugins/aws/eks/eksNetworkExposure.js
index aeadeb324..d2306e27a 100644
--- a/plugins/aws/eks/eksNetworkExposure.js
+++ b/plugins/aws/eks/eksNetworkExposure.js
@@ -2,7 +2,7 @@ var async = require('async');
var helpers = require('../../../helpers/aws');
module.exports = {
- title: 'Network Exposure',
+ title: 'Internet Exposure',
category: 'EKS',
domain: 'Containers',
severity: 'Info',
diff --git a/plugins/aws/eks/eksPrivilegeAnalysis.js b/plugins/aws/eks/eksPrivilegeAnalysis.js
new file mode 100644
index 000000000..cae32738d
--- /dev/null
+++ b/plugins/aws/eks/eksPrivilegeAnalysis.js
@@ -0,0 +1,20 @@
+
+module.exports = {
+ title: 'Privilege Analysis',
+ category: 'EKS',
+ domain: 'Containers',
+ severity: 'Info',
+ description: 'Ensures no EKS cluster available in your AWS account is overly-permissive.',
+ more_info: 'Overly-permissive EKS clusters can expose your infrastructure to unauthorized access or accidental misconfigurations. Regular analysis of permissions helps maintain a secure cluster setup.',
+ link: 'https://docs.aws.amazon.com/eks/latest/userguide/cluster-endpoint.html',
+ recommended_action: 'Audit the IAM roles and policies associated with your EKS cluster. Restrict access to the minimum necessary permissions and configure security groups and endpoint access control appropriately.',
+ apis: [''],
+ realtime_triggers: ['eks:CreateCluster', 'eks:updateClusterConfig', 'eks:DeleteCluster'],
+
+ run: function(cache, settings, callback) {
+ var results = [];
+ var source = {};
+
+ callback(null, results, source);
+ }
+};
diff --git a/plugins/aws/guardduty/noActiveFindings.spec.js b/plugins/aws/guardduty/noActiveFindings.spec.js
index 9ada2f7ca..ef01aab0d 100644
--- a/plugins/aws/guardduty/noActiveFindings.spec.js
+++ b/plugins/aws/guardduty/noActiveFindings.spec.js
@@ -55,8 +55,8 @@ const getFindings = [
"Region": "us-east-1",
"Resource": {
"AccessKeyDetails": {
- "AccessKeyId": "ASIARPGOCGXS5IZZUZNG",
- "PrincipalId": "AIDARPGOCGXSQEYQAT545",
+ "AccessKeyId": "ABCDEFGHI",
+ "PrincipalId": "000011112222",
"UserName": "sadeed",
"UserType": "IAMUser"
},
@@ -118,7 +118,7 @@ const getFindings = [
"Region": "us-east-1",
"Resource": {
"AccessKeyDetails": {
- "AccessKeyId": "ASIARPGOCGXSYK2EKS4B",
+ "AccessKeyId": "ABCDEFGHI",
"PrincipalId": "000011112222",
"UserName": "aws-viteace",
"UserType": "Root"
@@ -263,4 +263,4 @@ describe('noActiveFindings', function () {
});
});
});
-});
\ No newline at end of file
+});
diff --git a/plugins/aws/iam/iamRolePolicies.js b/plugins/aws/iam/iamRolePolicies.js
index 6d3dd14d2..9180eeb59 100644
--- a/plugins/aws/iam/iamRolePolicies.js
+++ b/plugins/aws/iam/iamRolePolicies.js
@@ -60,7 +60,7 @@ module.exports = {
},
ignore_customer_managed_iam_policies: {
name: 'Ignore Customer-Managed IAM Policies',
- description: 'If set to true, skip customer-managed policies attached to the role',
+ description: 'If set to true, skip customer-managed policies attached to the role.',
regex: '^(true|false)$',
default: 'false'
},
@@ -81,6 +81,12 @@ module.exports = {
description: 'Enable this setting to ignore resource wildcards i.e. \'"Resource": "*"\' in the IAM policy, which by default, are being flagged.',
regex: '^(true|false)$',
default: 'false'
+ },
+ iam_policy_message_format: {
+ name: 'IAM Policy Message Format',
+ description: 'Enable this setting to include policy names in the failure messages',
+ regex: '^(true|false)$',
+ default: 'false'
}
},
realtime_triggers: ['iam:CreateRole','iam:DeleteRole','iam:AttachRolePolicy','iam:DetachRolePolicy','iam:PutRolePolicy','iam:DeleteRolePolicy'],
@@ -94,7 +100,8 @@ module.exports = {
ignore_customer_managed_iam_policies: settings.ignore_customer_managed_iam_policies || this.settings.ignore_customer_managed_iam_policies.default,
iam_role_policies_ignore_tag: settings.iam_role_policies_ignore_tag || this.settings.iam_role_policies_ignore_tag.default,
iam_policy_resource_specific_wildcards: settings.iam_policy_resource_specific_wildcards || this.settings.iam_policy_resource_specific_wildcards.default,
- ignore_iam_policy_resource_wildcards: settings.ignore_iam_policy_resource_wildcards || this.settings.ignore_iam_policy_resource_wildcards.default
+ ignore_iam_policy_resource_wildcards: settings.ignore_iam_policy_resource_wildcards || this.settings.ignore_iam_policy_resource_wildcards.default,
+ iam_policy_message_format: settings.iam_policy_message_format || this.settings.iam_policy_message_format.default
};
config.ignore_service_specific_wildcards = (config.ignore_service_specific_wildcards === 'true');
@@ -102,7 +109,7 @@ module.exports = {
config.ignore_aws_managed_iam_policies = (config.ignore_aws_managed_iam_policies === 'true');
config.ignore_customer_managed_iam_policies = (config.ignore_customer_managed_iam_policies === 'true');
config.ignore_iam_policy_resource_wildcards = (config.ignore_iam_policy_resource_wildcards === 'true');
-
+ config.iam_policy_message_format = (config.iam_policy_message_format === 'true');
var allowedRegex = RegExp(config.iam_policy_resource_specific_wildcards);
@@ -196,7 +203,8 @@ module.exports = {
return cb();
}
- var roleFailures = [];
+ var roleFailures = config.iam_policy_message_format ? {} : [];
+
// See if role has admin managed policy
if (listAttachedRolePolicies.data &&
@@ -204,7 +212,11 @@ module.exports = {
for (var policy of listAttachedRolePolicies.data.AttachedPolicies) {
if (policy.PolicyArn === managedAdminPolicy) {
- roleFailures.push('Role has managed AdministratorAccess policy');
+ if (config.iam_policy_message_format) {
+ roleFailures.admin = 'managedAdminPolicy';
+ } else {
+ roleFailures.push('Role has managed AdministratorAccess policy');
+ }
break;
}
@@ -230,7 +242,11 @@ module.exports = {
getPolicyVersion.data.PolicyVersion.Document);
if (!statements) break;
- addRoleFailures(roleFailures, statements, 'managed', config.ignore_service_specific_wildcards, allowedRegex, config.ignore_iam_policy_resource_wildcards);
+ if (config.iam_policy_message_format) {
+ addRoleFailuresPolicyName(roleFailures, statements, 'managed', policy.PolicyName, config.ignore_service_specific_wildcards, allowedRegex, config.ignore_iam_policy_resource_wildcards);
+ } else {
+ addRoleFailures(roleFailures, statements, 'managed', config.ignore_service_specific_wildcards, allowedRegex, config.ignore_iam_policy_resource_wildcards);
+ }
}
}
}
@@ -249,21 +265,22 @@ module.exports = {
var statements = getRolePolicy[policyName].data.PolicyDocument;
if (!statements) break;
- addRoleFailures(roleFailures, statements, 'inline', config.ignore_service_specific_wildcards, allowedRegex, config.ignore_iam_policy_resource_wildcards);
+ if (config.iam_policy_message_format) {
+ addRoleFailuresPolicyName(roleFailures, statements, 'inline', policyName, config.ignore_service_specific_wildcards, allowedRegex, config.ignore_iam_policy_resource_wildcards);
+ } else {
+ addRoleFailures(roleFailures, statements, 'inline', config.ignore_service_specific_wildcards, allowedRegex, config.ignore_iam_policy_resource_wildcards);
+ }
}
}
}
- if (roleFailures.length) {
- helpers.addResult(results, 2,
- roleFailures.join(', '),
- 'global', role.Arn, custom);
+ if (config.iam_policy_message_format) {
+ compileFormattedResults(roleFailures, role, results, custom);
} else {
- helpers.addResult(results, 0,
- 'Role does not have overly-permissive policy',
- 'global', role.Arn, custom);
+ compileSimpleResults(roleFailures, role, results, custom);
}
+
cb();
}, function(){
callback(null, results, source);
@@ -308,4 +325,173 @@ function addRoleFailures(roleFailures, statements, policyType, ignoreServiceSpec
if (failMsg && roleFailures.indexOf(failMsg) === -1) roleFailures.push(failMsg);
}
}
+}
+
+function addRoleFailuresPolicyName(roleFailures, statements, policyType, policyName, ignoreServiceSpecific, regResource, ignoreResourceSpecific) {
+ // Initialize roleFailures as an object for the first time
+ if (!roleFailures.managed) {
+ roleFailures.managed = {
+ allActionsAllResources: [],
+ allActionsSelectedResources: [],
+ actionsAllResources: [],
+ wildcardActions: {},
+ regexMismatch: {}
+ };
+ }
+ if (!roleFailures.inline) {
+ roleFailures.inline = {
+ allActionsAllResources: [],
+ allActionsSelectedResources: [],
+ actionsAllResources: [],
+ wildcardActions: {},
+ regexMismatch: {}
+ };
+ }
+ if (!roleFailures.admin) roleFailures.admin = false;
+
+ for (var statement of statements) {
+ if (statement.Effect === 'Allow' && !statement.Condition) {
+ let targetObj = roleFailures[policyType];
+
+ if (statement.Action &&
+ statement.Action.indexOf('*') > -1 &&
+ statement.Resource &&
+ statement.Resource.indexOf('*') > -1) {
+ targetObj.allActionsAllResources.push(policyName);
+ } else if (statement.Action.indexOf('*') > -1) {
+ targetObj.allActionsSelectedResources.push(policyName);
+ } else if (!ignoreResourceSpecific && statement.Resource && statement.Resource == '*') {
+ targetObj.actionsAllResources.push(policyName);
+ } else if (!ignoreServiceSpecific && statement.Action && statement.Action.length) {
+ // Check each action for wildcards
+ let wildcards = [];
+ for (var a in statement.Action) {
+ if (/^.+:[a-zA-Z]?\*.?$/.test(statement.Action[a])) {
+ wildcards.push(statement.Action[a]);
+ }
+ }
+ if (wildcards.length) {
+ if (!targetObj.wildcardActions[wildcards.join(', ')]) {
+ targetObj.wildcardActions[wildcards.join(', ')] = [];
+ }
+ if (!targetObj.wildcardActions[wildcards.join(', ')].includes(policyName)) {
+ targetObj.wildcardActions[wildcards.join(', ')].push(policyName);
+ }
+ }
+ } else if (statement.Resource && statement.Resource.length) {
+ // Check each resource for wildcard
+ let wildcards = [];
+ for (var resource of statement.Resource) {
+ if (!regResource.test(resource)) {
+ wildcards.push(resource);
+ }
+ }
+ if (wildcards.length) {
+ if (!targetObj.regexMismatch[wildcards.join(', ')]) {
+ targetObj.regexMismatch[wildcards.join(', ')] = [];
+ }
+ if (!targetObj.regexMismatch[wildcards.join(', ')].includes(policyName)) {
+ targetObj.regexMismatch[wildcards.join(', ')].push(policyName);
+ }
+ }
+ }
+ }
+ }
+}
+
+function hasFailures(roleFailures) {
+ if (roleFailures.admin) return true;
+
+ if (roleFailures.managed) {
+ if (roleFailures.managed.allActionsAllResources.length) return true;
+ if (roleFailures.managed.allActionsSelectedResources.length) return true;
+ if (roleFailures.managed.actionsAllResources.length) return true;
+ if (Object.keys(roleFailures.managed.wildcardActions).length) return true;
+ if (roleFailures.managed.regexMismatch.length) return true;
+ }
+
+ if (roleFailures.inline) {
+ if (roleFailures.inline.allActionsAllResources.length) return true;
+ if (roleFailures.inline.allActionsSelectedResources.length) return true;
+ if (roleFailures.inline.actionsAllResources.length) return true;
+ if (Object.keys(roleFailures.inline.wildcardActions).length) return true;
+ if (roleFailures.inline.regexMismatch.length) return true;
+ }
+
+ return false;
+}
+
+function formatPolicyNames(policyArray) {
+ if (policyArray.length <= 5) {
+ return [...new Set(policyArray)].join('", "');
+ }
+ return [...new Set(policyArray)].slice(0, 5).join('", "') + '" and so on...';
+}
+
+function compileSimpleResults(roleFailures, role, results, custom) {
+ if (roleFailures.length) {
+ helpers.addResult(results, 2,
+ roleFailures.join(', '),
+ 'global', role.Arn, custom);
+ } else {
+ helpers.addResult(results, 0,
+ 'Role does not have overly-permissive policy',
+ 'global', role.Arn, custom);
+ }
+}
+
+function compileFormattedResults(roleFailures, role, results, custom) {
+ if (hasFailures(roleFailures)) {
+ let messages = [];
+
+ if (roleFailures.admin == 'managedAdminPolicy') {
+ messages.push('Role has managed AdministratorAccess policy');
+ }
+
+ // Format managed policies
+ if (roleFailures.managed) {
+ if (roleFailures.managed.allActionsAllResources.length) {
+ messages.push(`Role managed policy "${formatPolicyNames(roleFailures.managed.allActionsAllResources)}" allows all actions on all resources`);
+ }
+ if (roleFailures.managed.allActionsSelectedResources.length) {
+ messages.push(`Role managed policy "${formatPolicyNames(roleFailures.managed.allActionsSelectedResources)}" allows all actions on selected resources`);
+ }
+ if (roleFailures.managed.actionsAllResources.length) {
+ messages.push(`Role managed policy "${formatPolicyNames(roleFailures.managed.actionsAllResources)}" allows actions on all resources`);
+ }
+ for (let action in roleFailures.managed.wildcardActions) {
+ messages.push(`Role managed policy "${roleFailures.managed.wildcardActions[action].join('", "')}" allows wildcard actions: ${action}`);
+ }
+ for (let resource in roleFailures.managed.regexMismatch) {
+ messages.push(`Role managed policy "${roleFailures.managed.regexMismatch[resource].join('", "')}" does not match provided regex: ${resource}`);
+ }
+ }
+
+ // Format inline policies
+ if (roleFailures.inline) {
+ if (roleFailures.inline.allActionsAllResources.length) {
+ messages.push(`Role inline policy "${formatPolicyNames(roleFailures.inline.allActionsAllResources)}" allows all actions on all resources`);
+ }
+ if (roleFailures.inline.allActionsSelectedResources.length) {
+ messages.push(`Role inline policy "${formatPolicyNames(roleFailures.inline.allActionsSelectedResources)}" allows all actions on selected resources`);
+ }
+ if (roleFailures.inline.actionsAllResources.length) {
+ messages.push(`Role inline policy "${formatPolicyNames(roleFailures.inline.actionsAllResources)}" allows actions on all resources`);
+ }
+ for (let action in roleFailures.inline.wildcardActions) {
+ messages.push(`Role inline policy "${roleFailures.inline.wildcardActions[action].join('", "')}" allows wildcard actions: ${action}`);
+ }
+ for (let resource in roleFailures.inline.regexMismatch) {
+ messages.push(`Role inline policy "${roleFailures.inline.regexMismatch[resource].join('", "')}" does not match provided regex: ${resource}`);
+ }
+ }
+
+ helpers.addResult(results, 2,
+ messages.join('\n'),
+ 'global', role.Arn, custom);
+ } else {
+ helpers.addResult(results, 0,
+ 'Role does not have overly-permissive policy',
+ 'global', role.Arn, custom);
+ }
}
\ No newline at end of file
diff --git a/plugins/aws/iam/iamRolePolicies.spec.js b/plugins/aws/iam/iamRolePolicies.spec.js
index 3ec8ebcfa..927aff99a 100644
--- a/plugins/aws/iam/iamRolePolicies.spec.js
+++ b/plugins/aws/iam/iamRolePolicies.spec.js
@@ -393,7 +393,7 @@ describe('iamRolePolicies', function () {
const cache = createCache([listRoles[0]],getRole[0], {}, listRolePolicies[1], getRolePolicy[4]);
iamRolePolicies.run(cache, {}, (err, results) => {
expect(results.length).to.equal(1);
- expect(results[0].message).to.include('policy allows all actions on selected resources');
+ expect(results[0].message).to.include('allows all actions on selected resources');
expect(results[0].status).to.equal(2);
done();
});
@@ -403,7 +403,7 @@ describe('iamRolePolicies', function () {
const cache = createCache([listRoles[1]],getRole[0], {}, listRolePolicies[1], getRolePolicy[3]);
iamRolePolicies.run(cache, {}, (err, results) => {
expect(results.length).to.equal(1);
- expect(results[0].message).to.include('policy allows all actions on all resources');
+ expect(results[0].message).to.include('allows all actions on all resources');
expect(results[0].status).to.equal(2);
done();
});
diff --git a/plugins/aws/kinesisvideo/videostreamDataEncrypted.js b/plugins/aws/kinesisvideo/videostreamDataEncrypted.js
index a1146e422..940cefd8c 100644
--- a/plugins/aws/kinesisvideo/videostreamDataEncrypted.js
+++ b/plugins/aws/kinesisvideo/videostreamDataEncrypted.js
@@ -11,7 +11,7 @@ module.exports = {
'It is recommended to use customer-managed keys (CMKs) for encryption in order to gain more granular control over encryption/decryption process.',
recommended_action: 'Encrypt Kinesis Video Streams data with customer-manager keys (CMKs).',
link: 'https://docs.aws.amazon.com/kinesisvideostreams/latest/dg/how-kms.html',
- apis: ['KinesisVideo:listStreams', 'KMS:describeKey', 'KMS:listKeys'],
+ apis: ['KinesisVideo:listStreams', 'KMS:describeKey', 'KMS:listKeys', 'KMS:listAliases'],
settings: {
video_stream_data_desired_encryption_level: {
name: 'Kinesis Video Streams Data Target Encryption Level',
@@ -59,16 +59,39 @@ module.exports = {
return rcb();
}
+ var listAliases = helpers.addSource(cache, source,
+ ['kms', 'listAliases', region]);
+
+ if (!listAliases || listAliases.err || !listAliases.data) {
+ helpers.addResult(results, 3,
+ 'Unable to query for KMS aliases: ' + helpers.addError(listAliases),
+ region);
+ return rcb();
+ }
+
+ var keyArn;
+ var kmsAliasArnMap = {};
+ listAliases.data.forEach(function(alias) {
+ keyArn = alias.AliasArn.replace(/:alias\/.*/, ':key/' + alias.TargetKeyId);
+ kmsAliasArnMap[alias.AliasName] = keyArn;
+ });
+
for (let streamData of listStreams.data) {
if (!streamData.StreamARN) continue;
let resource = streamData.StreamARN;
if (streamData.KmsKeyId) {
- var kmsKeyId = streamData.KmsKeyId.split('/')[1] ? streamData.KmsKeyId.split('/')[1] : streamData.KmsKeyId;
+
+ let aliasKey = streamData.KmsKeyId.includes('alias/') ? streamData.KmsKeyId.split(':').pop() : streamData.KmsKeyId;
+ let kmsKeyArn = (aliasKey.startsWith('alias/'))
+ ? (kmsAliasArnMap[aliasKey] ? kmsAliasArnMap[aliasKey] : streamData.KmsKeyId)
+ : streamData.KmsKeyId;
+ var kmsKeyId = kmsKeyArn.split('/')[1] ? kmsKeyArn.split('/')[1] : kmsKeyArn;
+
var describeKey = helpers.addSource(cache, source,
- ['kms', 'describeKey', region, kmsKeyId]);
+ ['kms', 'describeKey', region, kmsKeyId]);
if (!describeKey || describeKey.err || !describeKey.data || !describeKey.data.KeyMetadata) {
helpers.addResult(results, 3,
diff --git a/plugins/aws/kinesisvideo/videostreamDataEncrypted.spec.js b/plugins/aws/kinesisvideo/videostreamDataEncrypted.spec.js
index 3dc7073d3..0c7177eec 100644
--- a/plugins/aws/kinesisvideo/videostreamDataEncrypted.spec.js
+++ b/plugins/aws/kinesisvideo/videostreamDataEncrypted.spec.js
@@ -21,6 +21,14 @@ const listKeys = [
}
];
+const listAliases = [
+ {
+ "AliasName": "alias/my-kinesis-key",
+ "AliasArn": "arn:aws:kms:us-east-1:000011112222:alias/my-kinesis-key",
+ "TargetKeyId": "ad013a33-b01d-4d88-ac97-127399c18b3e"
+ }
+];
+
const describeKey = [
{
"KeyMetadata": {
@@ -60,7 +68,7 @@ const describeKey = [
}
];
-const createCache = (streamData, keys, describeKey, streamDataErr, keysErr, describeKeyErr) => {
+const createCache = (streamData, keys, aliases, describeKey, streamDataErr, keysErr, aliasesErr, describeKeyErr) => {
var keyId = (keys && keys.length ) ? keys[0].KeyId : null;
return {
kinesisvideo: {
@@ -78,6 +86,12 @@ const createCache = (streamData, keys, describeKey, streamDataErr, keysErr, desc
err: keysErr
}
},
+ listAliases: {
+ 'us-east-1': {
+ data: aliases,
+ err: aliasesErr
+ }
+ },
describeKey: {
'us-east-1': {
[keyId]: {
@@ -95,8 +109,8 @@ const createCache = (streamData, keys, describeKey, streamDataErr, keysErr, desc
describe('videostreamDataEncrypted', function () {
describe('run', function () {
- it('should PASS if Kinesis Video Streams data is using desired encryption level', function (done) {
- const cache = createCache(listStreams, listKeys, describeKey[0]);
+ it('should PASS if Kinesis Video Streams data is using customer-managed encryption (awscmk)', function (done) {
+ const cache = createCache(listStreams, listKeys, listAliases, describeKey[0]);
videostreamDataEncrypted.run(cache, { video_stream_data_desired_encryption_level: 'awscmk' }, (err, results) => {
expect(results.length).to.equal(1);
expect(results[0].status).to.equal(0);
@@ -106,9 +120,9 @@ describe('videostreamDataEncrypted', function () {
});
- it('should FAIL if Kinesis Video Streams data is using desired encyption level', function (done) {
- const cache = createCache(listStreams, listKeys, describeKey[1]);
- videostreamDataEncrypted.run(cache, { video_stream_data_desired_encryption_level:'awscmk' }, (err, results) => {
+ it('should FAIL if Kinesis Video Streams data is using AWS managed encryption (awskms)', function (done) {
+ const cache = createCache(listStreams, listKeys, listAliases, describeKey[1]);
+ videostreamDataEncrypted.run(cache, { video_stream_data_desired_encryption_level: 'awscmk' }, (err, results) => {
expect(results.length).to.equal(1);
expect(results[0].status).to.equal(2);
expect(results[0].message).to.include('Kinesis Video Streams data is using awskms');
@@ -117,7 +131,7 @@ describe('videostreamDataEncrypted', function () {
});
- it('should PASS if no Kinesis Video Streams found', function (done) {
+ it('should PASS if no Kinesis Video Streams are found', function (done) {
const cache = createCache([]);
videostreamDataEncrypted.run(cache, {}, (err, results) => {
expect(results.length).to.equal(1);
@@ -128,7 +142,7 @@ describe('videostreamDataEncrypted', function () {
});
it('should UNKNOWN if unable to list Kinesis Video Streams', function (done) {
- const cache = createCache(null, null, null, { message: "Unable to list Kinesis Video Streams encryption" });
+ const cache = createCache(null, null, null, null, { message: "Unable to list Kinesis Video Streams" });
videostreamDataEncrypted.run(cache, {}, (err, results) => {
expect(results.length).to.equal(1);
expect(results[0].status).to.equal(3);
@@ -138,7 +152,16 @@ describe('videostreamDataEncrypted', function () {
});
it('should UNKNOWN if unable to list KMS keys', function (done) {
- const cache = createCache(null, null, null, null, { message: "Unable to list KMS keys" });
+ const cache = createCache(null, null, null, null, null, { message: "Unable to list KMS keys" });
+ videostreamDataEncrypted.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(3);
+ done();
+ });
+ });
+
+ it('should UNKNOWN if unable to retrieve KMS alias data', function (done) {
+ const cache = createCache(listStreams, listKeys, null, describeKey[0], null, null, { message: "Unable to list KMS aliases" });
videostreamDataEncrypted.run(cache, {}, (err, results) => {
expect(results.length).to.equal(1);
expect(results[0].status).to.equal(3);
diff --git a/plugins/aws/kms/kmsDefaultKeyUsage.js b/plugins/aws/kms/kmsDefaultKeyUsage.js
index 8a309c865..495613bb0 100644
--- a/plugins/aws/kms/kmsDefaultKeyUsage.js
+++ b/plugins/aws/kms/kmsDefaultKeyUsage.js
@@ -11,7 +11,7 @@ module.exports = {
link: 'http://docs.aws.amazon.com/kms/latest/developerguide/concepts.html',
recommended_action: 'Avoid using the default KMS key',
apis: ['KMS:listKeys', 'KMS:describeKey', 'KMS:listAliases', 'CloudTrail:describeTrails',
- 'EC2:describeVolumes', 'ElasticTranscoder:listPipelines', 'RDS:describeDBInstances',
+ 'EC2:describeVolumes', 'RDS:describeDBInstances',
'Redshift:describeClusters', 'S3:listBuckets', 'S3:getBucketEncryption',
'SES:describeActiveReceiptRuleSet', 'Workspaces:describeWorkspaces', 'Lambda:listFunctions',
'CloudWatchLogs:describeLogGroups', 'EFS:describeFileSystems', 'STS:getCallerIdentity'],
@@ -21,7 +21,7 @@ module.exports = {
'passwords, it is still strongly encouraged to use a ' +
'customer-provided CMK rather than the default KMS key.'
},
- realtime_triggers: ['cloudtrail:CreateTrail','cloudtrail:UpdateTrail','cloudtrail:DeleteTrail','ec2:CreateVolume','ec2:DeleteVolume','elastictranscoder:UpdatePipeline','elastictranscoder:CreatePipeline','elastictranscoder:DeletePipeline','rds:CreateDBInstance','rds:ModifyDBInstance','rds:DeleteDBInstance','redshift:CreateCluster','redshift:ModifyCluster','redshift:DeleteCluster','s3:CreateBucket','s3:DeleteBucket','s3:PutBucketEncryption','ses:CreateReceiptRule','ses:DeleteReceiptRule','ses:UpdateReceiptRule','workspaces:CreateWorkspaces','workspaces:TerminateWorkspaces','lambda:UpdateFunctionConfiguration','lambda:CreateFunction','lambda:DeleteFunction','cloudwatchlogs:CreateLogGroup','cloudwatchlogs:DeleteLogGroup','cloudwatchlogs:AssociateKmsKey','efs:CreateFileSystem',':efs:DeleteFileSystem'],
+ realtime_triggers: ['cloudtrail:CreateTrail','cloudtrail:UpdateTrail','cloudtrail:DeleteTrail','ec2:CreateVolume','ec2:DeleteVolume','rds:CreateDBInstance','rds:ModifyDBInstance','rds:DeleteDBInstance','redshift:CreateCluster','redshift:ModifyCluster','redshift:DeleteCluster','s3:CreateBucket','s3:DeleteBucket','s3:PutBucketEncryption','ses:CreateReceiptRule','ses:DeleteReceiptRule','ses:UpdateReceiptRule','workspaces:CreateWorkspaces','workspaces:TerminateWorkspaces','lambda:UpdateFunctionConfiguration','lambda:CreateFunction','lambda:DeleteFunction','cloudwatchlogs:CreateLogGroup','cloudwatchlogs:DeleteLogGroup','cloudwatchlogs:AssociateKmsKey','efs:CreateFileSystem',':efs:DeleteFileSystem'],
run: function(cache, settings, callback) {
var results = [];
@@ -100,28 +100,6 @@ module.exports = {
}
}
}
- }
-
- // For ElasticTranscoder
- if (region in regions.elastictranscoder) {
- var listPipelines = helpers.addSource(cache, source, ['elastictranscoder', 'listPipelines', region]);
-
- if (listPipelines) {
- if (listPipelines.err || !listPipelines.data) {
- helpers.addResult(results, 3,
- 'Unable to query for ElasticTranscoder pipelines: ' + helpers.addError(listPipelines), region);
- } else {
- for (var k in listPipelines.data){
- if (listPipelines.data[k].AwsKmsKeyArn) {
- services.push({
- serviceName: 'ElasticTranscoder',
- resource: listPipelines.data[k].Arn,
- KMSKey: listPipelines.data[k].AwsKmsKeyArn
- });
- }
- }
- }
- }
}
// For RDS
diff --git a/plugins/aws/lambda/lambdaNetworkExposure.js b/plugins/aws/lambda/lambdaNetworkExposure.js
index d552ca172..891b16e8d 100644
--- a/plugins/aws/lambda/lambdaNetworkExposure.js
+++ b/plugins/aws/lambda/lambdaNetworkExposure.js
@@ -2,7 +2,7 @@ var async = require('async');
var helpers = require('../../../helpers/aws');
module.exports = {
- title: 'Network Exposure',
+ title: 'Internet Exposure',
category: 'Lambda',
domain: 'Serverless',
severity: 'Info',
@@ -10,8 +10,8 @@ module.exports = {
more_info: 'Lambda functions can be exposed to the internet through Function URLs with public access policies or through API Gateway integrations. It\'s important to ensure these endpoints are properly secured.',
link: 'https://docs.aws.amazon.com/lambda/latest/dg/lambda-urls.html',
recommended_action: 'Ensure Lambda Function URLs have proper authorization configured and API Gateway integrations use appropriate security measures.',
- apis: ['Lambda:listFunctions', 'Lambda:getFunctionUrlConfig', 'Lambda:getPolicy',
- 'APIGateway:getRestApis','APIGateway:getResources', 'APIGateway:getStages', 'APIGateway:getIntegration', 'ELBv2:describeLoadBalancers', 'ELBv2:describeTargetGroups',
+ apis: ['Lambda:listFunctions', 'Lambda:getFunctionUrlConfig', 'Lambda:getPolicy',
+ 'APIGateway:getRestApis','APIGateway:getResources', 'APIGateway:getStages', 'APIGateway:getIntegration', 'ELBv2:describeLoadBalancers', 'ELBv2:describeTargetGroups',
'ELBv2:describeTargetHealth', 'ELBv2:describeListeners', 'EC2:describeSecurityGroups'],
realtime_triggers: ['lambda:CreateFunctionUrlConfig', 'lambda:UpdateFunctionUrlConfig', 'lambda:DeleteFunctionUrlConfig',
'lambda:AddPermission', 'lambda:RemovePermission',
@@ -49,7 +49,7 @@ module.exports = {
// Get function URL config and policy for Lambda-specific checks
var getFunctionUrlConfig = helpers.addSource(cache, source,
['lambda', 'getFunctionUrlConfig', region, lambda.FunctionName]);
-
+
var getPolicy = helpers.addSource(cache, source,
['lambda', 'getPolicy', region, lambda.FunctionName]);
@@ -79,4 +79,4 @@ module.exports = {
callback(null, results, source);
});
}
-};
\ No newline at end of file
+};
diff --git a/plugins/aws/lambda/lambdaOldRuntimes.js b/plugins/aws/lambda/lambdaOldRuntimes.js
index 19a11a73f..994360ce2 100644
--- a/plugins/aws/lambda/lambdaOldRuntimes.js
+++ b/plugins/aws/lambda/lambdaOldRuntimes.js
@@ -40,6 +40,7 @@ module.exports = {
{ 'id':'nodejs12.x', 'name': 'Node.js 12', 'endOfLifeDate': '2023-03-31'},
{ 'id':'nodejs14.x', 'name': 'Node.js 14', 'endOfLifeDate': '2023-12-04'},
{ 'id':'nodejs16.x', 'name': 'Node.js 16', 'endOfLifeDate': '2024-06-12'},
+ { 'id':'nodejs18.x', 'name': 'Node.js 18', 'endOfLifeDate': '2025-04-30'},
{ 'id':'dotnetcore3.1', 'name': '.Net Core 3.1', 'endOfLifeDate': '2023-04-03' },
{ 'id':'dotnetcore2.1', 'name': '.Net Core 2.1', 'endOfLifeDate': '2022-01-05' },
{ 'id':'dotnetcore2.0', 'name': '.Net Core 2.0', 'endOfLifeDate': '2019-05-30' },
diff --git a/plugins/aws/lambda/lambdaOldRuntimes.spec.js b/plugins/aws/lambda/lambdaOldRuntimes.spec.js
index d49bcdf98..6087987a5 100644
--- a/plugins/aws/lambda/lambdaOldRuntimes.spec.js
+++ b/plugins/aws/lambda/lambdaOldRuntimes.spec.js
@@ -5,7 +5,7 @@ const listFunctions = [
{
"FunctionName": "test-lambda",
"FunctionArn": "arn:aws:lambda:us-east-1:000011112222:function:test-lambda",
- "Runtime": "nodejs18.x",
+ "Runtime": "nodejs22.x",
"Role": "arn:aws:iam::000011112222:role/lambda-role",
"Handler": "index.handler",
"TracingConfig": { "Mode": "PassThrough" }
diff --git a/plugins/aws/lambda/lambdaPrivilegeAnalysis.js b/plugins/aws/lambda/lambdaPrivilegeAnalysis.js
new file mode 100644
index 000000000..4856c1be8
--- /dev/null
+++ b/plugins/aws/lambda/lambdaPrivilegeAnalysis.js
@@ -0,0 +1,20 @@
+module.exports = {
+ title: 'Privilege Analysis',
+ category: 'Lambda',
+ domain: 'Serverless',
+ severity: 'Info',
+ description: 'Ensures no Lambda function available in your AWS account is overly-permissive.',
+ more_info: 'AWS Lambda Function should have most-restrictive IAM permissions for Lambda security best practices.',
+ link: 'https://docs.aws.amazon.com/lambda/latest/dg/lambda-permissions.html',
+ recommended_action: 'Modify IAM role attached with Lambda function to provide the minimal amount of access required to perform its tasks',
+ apis: [''],
+ realtime_triggers: ['lambda:CreateFunction','lambda:UpdateFunctionConfiguration', 'lambda:DeleteFunction'],
+
+ run: function(cache, settings, callback) {
+ var results = [];
+ var source = {};
+
+ callback(null, results, source);
+
+ }
+};
diff --git a/plugins/aws/neptune/neptuneDBIamAuth.js b/plugins/aws/neptune/neptuneDBIamAuth.js
index e4476cfc5..5187ad106 100644
--- a/plugins/aws/neptune/neptuneDBIamAuth.js
+++ b/plugins/aws/neptune/neptuneDBIamAuth.js
@@ -37,7 +37,7 @@ module.exports = {
}
for (let cluster of describeDBClusters.data) {
- if (!cluster.DBClusterArn) continue;
+ if (!cluster.DBClusterArn || cluster.Engine !== 'neptune') continue;
if (cluster.IAMDatabaseAuthenticationEnabled) {
helpers.addResult(results, 0, 'Neptune database instance has IAM authentication enabled', region, cluster.DBClusterArn);
diff --git a/plugins/aws/neptune/neptuneDBIamAuth.spec.js b/plugins/aws/neptune/neptuneDBIamAuth.spec.js
index 931edf0b6..5a9752635 100644
--- a/plugins/aws/neptune/neptuneDBIamAuth.spec.js
+++ b/plugins/aws/neptune/neptuneDBIamAuth.spec.js
@@ -4,6 +4,7 @@ var neptuneDBIamAuth = require('./neptuneDBIamAuth');
const describeDBClusters = [
{
"AllocatedStorage": 1,
+ "Engine": "neptune",
"BackupRetentionPeriod": 1,
"DbClusterResourceId": "cluster-WNY2ZTZWH4RQ2CTKEEP4GVCPU4",
"DBClusterArn": "arn:aws:rds:us-east-1:000111222333:cluster:database-2",
@@ -12,6 +13,7 @@ const describeDBClusters = [
},
{
"AllocatedStorage": 1,
+ "Engine": "neptune",
"BackupRetentionPeriod": 1,
"DbClusterResourceId": "cluster-WNY2ZTZWH4RQ2CTKEEP4GVCPU9",
"DBClusterArn": "arn:aws:rds:us-east-1:000111222334:cluster:database-3",
diff --git a/plugins/aws/neptune/neptuneDBInstanceEncrypted.js b/plugins/aws/neptune/neptuneDBInstanceEncrypted.js
index 5eea32e9c..5ae37b307 100644
--- a/plugins/aws/neptune/neptuneDBInstanceEncrypted.js
+++ b/plugins/aws/neptune/neptuneDBInstanceEncrypted.js
@@ -63,7 +63,7 @@ module.exports = {
}
for (let cluster of describeDBClusters.data) {
- if (!cluster.DBClusterArn) continue;
+ if (!cluster.DBClusterArn || cluster.Engine !== 'neptune') continue;
let resource = cluster.DBClusterArn;
diff --git a/plugins/aws/neptune/neptuneDBMultiAz.js b/plugins/aws/neptune/neptuneDBMultiAz.js
index b6b843766..01c49ea40 100644
--- a/plugins/aws/neptune/neptuneDBMultiAz.js
+++ b/plugins/aws/neptune/neptuneDBMultiAz.js
@@ -38,7 +38,7 @@ module.exports = {
}
for (let cluster of describeDBClusters.data) {
- if (!cluster.DBClusterArn) continue;
+ if (!cluster.DBClusterArn || cluster.Engine !== 'neptune') continue;
let resource = cluster.DBClusterArn;
diff --git a/plugins/aws/neptune/neptuneDBMultiAz.spec.js b/plugins/aws/neptune/neptuneDBMultiAz.spec.js
index e5335b5f5..0c7c827a9 100644
--- a/plugins/aws/neptune/neptuneDBMultiAz.spec.js
+++ b/plugins/aws/neptune/neptuneDBMultiAz.spec.js
@@ -8,6 +8,7 @@ const describeDBClusters = [
"DbClusterResourceId": "cluster-WNY2ZTZWH4RQ2CTKEEP4GVCPU4",
"DBClusterArn": "arn:aws:rds:us-east-1:000111222333:cluster:database-2",
"AssociatedRoles": [],
+ "Engine": "neptune",
"MultiAZ": true
},
{
@@ -16,6 +17,7 @@ const describeDBClusters = [
"DbClusterResourceId": "cluster-WNY2ZTZWH4RQ2CTKEEP4GVCPU9",
"DBClusterArn": "arn:aws:rds:us-east-1:000111222334:cluster:database-3",
"AssociatedRoles": [],
+ "Engine": "neptune",
"MultiAZ": false
}
];
diff --git a/plugins/aws/neptune/neptuneDbDeletionProtection.js b/plugins/aws/neptune/neptuneDbDeletionProtection.js
index d7f021183..0f333a043 100644
--- a/plugins/aws/neptune/neptuneDbDeletionProtection.js
+++ b/plugins/aws/neptune/neptuneDbDeletionProtection.js
@@ -37,8 +37,7 @@ module.exports = {
}
for (let cluster of describeDBClusters.data) {
- if (!cluster.DBClusterArn) continue;
-
+ if (!cluster.DBClusterArn || cluster.Engine != 'neptune') continue;
if (cluster.DeletionProtection) {
helpers.addResult(results, 0, 'Neptune database instance has deletion protection enabled', region, cluster.DBClusterArn);
} else {
diff --git a/plugins/aws/neptune/neptuneDbDeletionProtection.spec.js b/plugins/aws/neptune/neptuneDbDeletionProtection.spec.js
index a82c6ca31..082b291ec 100644
--- a/plugins/aws/neptune/neptuneDbDeletionProtection.spec.js
+++ b/plugins/aws/neptune/neptuneDbDeletionProtection.spec.js
@@ -8,6 +8,7 @@ const describeDBClusters = [
"DbClusterResourceId": "cluster-WNY2ZTZWH4RQ2CTKEEP4GVCPU4",
"DBClusterArn": "arn:aws:rds:us-east-1:000111222333:cluster:database-2",
"AssociatedRoles": [],
+ "Engine": "neptune",
"DeletionProtection": true
},
{
@@ -16,6 +17,7 @@ const describeDBClusters = [
"DbClusterResourceId": "cluster-WNY2ZTZWH4RQ2CTKEEP4GVCPU9",
"DBClusterArn": "arn:aws:rds:us-east-1:000111222334:cluster:database-3",
"AssociatedRoles": [],
+ "Engine": "neptune",
"DeletionProtection": false
}
];
diff --git a/plugins/aws/neptune/neptuneInstanceBackupRetention.js b/plugins/aws/neptune/neptuneInstanceBackupRetention.js
index 3b10aca31..c491a872e 100644
--- a/plugins/aws/neptune/neptuneInstanceBackupRetention.js
+++ b/plugins/aws/neptune/neptuneInstanceBackupRetention.js
@@ -46,7 +46,7 @@ module.exports = {
}
for (let cluster of describeDBClusters.data) {
- if (!cluster.DBClusterArn) continue;
+ if (!cluster.DBClusterArn || cluster.Engine !== 'neptune') continue;
let resource = cluster.DBClusterArn;
@@ -67,3 +67,4 @@ module.exports = {
});
}
};
+
diff --git a/plugins/aws/rds/rdsPublicSubnet.js b/plugins/aws/rds/rdsPublicSubnet.js
index 43d808a4c..20249149c 100644
--- a/plugins/aws/rds/rdsPublicSubnet.js
+++ b/plugins/aws/rds/rdsPublicSubnet.js
@@ -8,7 +8,7 @@ module.exports = {
severity: 'High',
description: 'Ensures RDS database instances are not deployed in public subnet.',
more_info: 'RDS instances should not be deployed in public subnets to prevent direct exposure to the internet and reduce the risk of unauthorized access.',
- link: 'https://docs.aws.amazon.com/config/latest/developerguide/rds-instance-public-access-check.html',
+ link: 'https://docs.aws.amazon.com/config/latest/developerguide/rds-instance-subnet-igw-check.html',
recommended_action: 'Replace the subnet groups of rds instance with the private subnets.',
apis: ['RDS:describeDBInstances', 'EC2:describeRouteTables', 'EC2:describeSubnets'],
realtime_triggers: ['rds:CreateDBInstance', 'rds:ModifyDBInstance', 'rds:RestoreDBInstanceFromDBSnapshot', 'rds:RestoreDBInstanceFromS3','rds:DeleteDBInstance'],
diff --git a/plugins/aws/route53/domainExpiry.js b/plugins/aws/route53/domainExpiry.js
index cf2a9869e..0fd5ba80e 100644
--- a/plugins/aws/route53/domainExpiry.js
+++ b/plugins/aws/route53/domainExpiry.js
@@ -42,7 +42,7 @@ module.exports = {
if (difference > 35) {
helpers.addResult(results, 0, returnMsg, 'global', domain.DomainName);
- } else if (domain.DomainName.endsWith(('.com.ar, .com.br, .jp')) && difference > 30) {
+ } else if (['.com.ar', '.com.br', '.jp'].some(suffix => domain.DomainName.endsWith(suffix)) && difference > 30){
helpers.addResult(results, 0, returnMsg, 'global', domain.DomainName);
} else if (difference > 0) {
helpers.addResult(results, 2, returnMsg, 'global', domain.DomainName);
diff --git a/plugins/aws/s3/bucketSecureTransportEnabled.spec.js b/plugins/aws/s3/bucketSecureTransportEnabled.spec.js
index 7694f8597..dbbf055e9 100644
--- a/plugins/aws/s3/bucketSecureTransportEnabled.spec.js
+++ b/plugins/aws/s3/bucketSecureTransportEnabled.spec.js
@@ -21,7 +21,7 @@ const getBucketPolicy = [
Policy: '{"Version":"2012-10-17","Id":"ExamplePolicy","Statement":[{"Sid":"","Effect":"Allow","Principal":{"AWS":"arn:aws:iam::00000011111:root"},"Action":["s3:PutObject"],"Resource":["arn:aws:s3:::staging-01-sd-logs/*"]},{"Sid":"","Effect":"Deny","Principal":"*","Action":"s3:*","Resource":["arn:aws:s3:::staging-01-sd-logs/*","arn:aws:s3:::staging-01-sd-logs"],"Condition":{"Bool":{"aws:SecureTransport":"false"}}}]}'
},
{
- Policy: '{"Version":"2008-10-17","Statement":[{"Sid":"Stmt1537431944913","Effect":"Allow","Principal":{"AWS":"arn:aws:iam::00001111122:root"},"Action":["s3:PutObject"],"Resource":["arn:aws:s3:::alqemy-upwork/*"]},{"Sid":"Stmt1537431944211","Effect":"Deny","Principal":"*","Action":"s3:*","Resource":["arn:aws:s3:::alqemy-upwork/*","arn:aws:s3:::alqemy-upwork"],"Condition":{"Bool":{"aws:SecureTransport":"false"}}}]}'
+ Policy: '{"Version":"2008-10-17","Statement":[{"Sid":"Stmt1537431944913","Effect":"Allow","Principal":{"AWS":"arn:aws:iam::00001111122:root"},"Action":["s3:PutObject"],"Resource":["arn:aws:s3:::cloudexploit-test-secure-transport/*"]},{"Sid":"Stmt1537431944211","Effect":"Deny","Principal":"*","Action":"s3:*","Resource":["arn:aws:s3:::cloudexploit-test-secure-transport/*","arn:aws:s3:::cloudexploit-test-secure-transport"],"Condition":{"Bool":{"aws:SecureTransport":"false"}}}]}'
},
{
Policy: '{"Version":"2012-10-17","Id":"ExamplePolicy","Statement":[]}'
diff --git a/plugins/aws/s3/s3BucketHasTags.js b/plugins/aws/s3/s3BucketHasTags.js
index d225153de..155497837 100644
--- a/plugins/aws/s3/s3BucketHasTags.js
+++ b/plugins/aws/s3/s3BucketHasTags.js
@@ -9,7 +9,7 @@ module.exports = {
more_info: 'Tags help you to group resources together that are related to or associated with each other. It is a best practice to tag cloud resources to better organize and gain visibility into their usage.',
recommended_action: 'Modify S3 buckets and add tags.',
link: 'https://docs.aws.amazon.com/AmazonS3/latest/userguide/CostAllocTagging.html',
- apis: ['S3:listBuckets', 'ResourceGroupsTaggingAPI:getResources', 'S3:getBucketLocation'],
+ apis: ['S3:listBuckets', 'S3:getBucketTagging', 'S3:getBucketLocation'],
realtime_triggers: ['s3:CreateBucket', 's3:PutBucketTagging','s3:DeleteBucket'],
run: function(cache, settings, callback) {
@@ -32,20 +32,43 @@ module.exports = {
return callback(null, results, source);
}
- var bucketsByRegion= {};
listBuckets.data.forEach(function(bucket) {
if (!bucket.Name) return;
+
var bucketLocation = helpers.getS3BucketLocation(cache, defaultRegion, bucket.Name);
- if (!bucketsByRegion[bucketLocation]) {
- bucketsByRegion[bucketLocation] = [];
+ var bucketArn = `arn:${awsOrGov}:s3:::${bucket.Name}`;
+
+ // Try the bucket's actual region first, then fall back to default region
+ var getBucketTagging = helpers.addSource(cache, source,
+ ['s3', 'getBucketTagging', bucketLocation, bucket.Name]);
+
+ // If not found in bucket's region, try default region (where collector runs)
+ if (!getBucketTagging) {
+ getBucketTagging = helpers.addSource(cache, source,
+ ['s3', 'getBucketTagging', defaultRegion, bucket.Name]);
+ }
+
+
+ if (!getBucketTagging || getBucketTagging.err) {
+ if (getBucketTagging && getBucketTagging.err &&
+ (getBucketTagging.err.code === 'NoSuchTagSet' ||
+ getBucketTagging.err.message && getBucketTagging.err.message.includes('does not exist'))) {
+ // No tags exist for this bucket
+ helpers.addResult(results, 2, 'S3 bucket does not have any tags', bucketLocation, bucketArn);
+ } else {
+ helpers.addResult(results, 3,
+ 'Unable to query S3 bucket tags: ' + helpers.addError(getBucketTagging),
+ bucketLocation, bucketArn);
+ }
+ return;
+ }
+
+ if (getBucketTagging.data && getBucketTagging.data.TagSet && getBucketTagging.data.TagSet.length > 0) {
+ helpers.addResult(results, 0, 'S3 bucket has tags', bucketLocation, bucketArn);
+ } else {
+ helpers.addResult(results, 2, 'S3 bucket does not have any tags', bucketLocation, bucketArn);
}
- bucketsByRegion[bucketLocation].push(`arn:${awsOrGov}:s3:::${bucket.Name}`);
});
-
- for (var region in bucketsByRegion) {
- var bucketNames = bucketsByRegion[region] || [];
- helpers.checkTags(cache, 'S3 bucket', bucketNames, region, results, settings);
- }
callback(null, results, source);
}
};
diff --git a/plugins/aws/s3/s3BucketHasTags.spec.js b/plugins/aws/s3/s3BucketHasTags.spec.js
index c5d16a432..5eefe1109 100644
--- a/plugins/aws/s3/s3BucketHasTags.spec.js
+++ b/plugins/aws/s3/s3BucketHasTags.spec.js
@@ -1,7 +1,7 @@
var expect = require('chai').expect;
var s3BucketHasTags = require('./s3BucketHasTags');
-const createCache = (bucketData, bucketDataErr, rgData, rgDataErr) => {
+const createCache = (bucketData, bucketDataErr, bucketTaggingData, bucketTaggingErr) => {
var bucketName = (bucketData && bucketData.length) ? bucketData[0].Name : null;
return {
s3: {
@@ -19,13 +19,13 @@ const createCache = (bucketData, bucketDataErr, rgData, rgDataErr) => {
}
}
}
- }
- },
- resourcegroupstaggingapi: {
- getResources: {
- 'us-east-1':{
- err: rgDataErr,
- data: rgData
+ },
+ getBucketTagging: {
+ 'us-east-1': {
+ [bucketName]: {
+ err: bucketTaggingErr,
+ data: bucketTaggingData
+ }
}
}
}
@@ -58,19 +58,46 @@ describe('s3BucketHasTags', function () {
s3BucketHasTags.run(cache, {}, callback);
});
- it('should give unknown result if unable to query resource group tagging api', function (done) {
+ it('should give unknown result if unable to query bucket tagging', function (done) {
const callback = (err, results) => {
expect(results.length).to.equal(1);
expect(results[0].status).to.equal(3);
- expect(results[0].message).to.include('Unable to query all resources from group tagging api:');
+ expect(results[0].message).to.include('Unable to query S3 bucket tags:');
done();
};
- const cache = createCache([{
- "Name": "test-bucket",
- "CreationDate": "November 22, 2021, 15:51:19 (UTC+05:00)",
- }], null, [],{
- message: "Unable to query for Resource group tags"
- });
+ // Create cache with error in both potential lookup locations (bucket region and default region)
+ const cache = {
+ s3: {
+ listBuckets: {
+ 'us-east-1': {
+ err: null,
+ data: [{
+ "Name": "test-bucket",
+ "CreationDate": "November 22, 2021, 15:51:19 (UTC+05:00)",
+ }]
+ }
+ },
+ getBucketLocation: {
+ 'us-east-1': {
+ 'test-bucket': {
+ data: {
+ LocationConstraint: null // us-east-1
+ }
+ }
+ }
+ },
+ getBucketTagging: {
+ 'us-east-1': {
+ 'test-bucket': {
+ err: {
+ message: "Unable to query bucket tags"
+ },
+ data: null
+ }
+ }
+ }
+ }
+ };
s3BucketHasTags.run(cache, {}, callback);
});
@@ -86,11 +113,13 @@ describe('s3BucketHasTags', function () {
[{
"Name": "test-bucket",
"CreationDate": "November 22, 2021, 15:51:19 (UTC+05:00)",
- }],null,
- [{
- "ResourceARN": "arn:aws:s3:::test-bucket",
- "Tags": [{key:"key1", value:"value"}],
- }],null
+ }], null,
+ {
+ "TagSet": [
+ {"Key": "key1", "Value": "value1"},
+ {"Key": "key2", "Value": "value2"}
+ ]
+ }, null
);
s3BucketHasTags.run(cache, {}, callback);
});
@@ -107,11 +136,32 @@ describe('s3BucketHasTags', function () {
[{
"Name": "test-bucket",
"CreationDate": "November 22, 2021, 15:51:19 (UTC+05:00)",
- }],null,
+ }], null,
+ null, {
+ code: "NoSuchTagSet",
+ message: "The TagSet does not exist"
+ }
+ );
+
+ s3BucketHasTags.run(cache, {}, callback);
+ });
+
+ it('should give failing result if s3 has empty tag set', function (done) {
+ const callback = (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(2);
+ expect(results[0].message).to.include('S3 bucket does not have any tags');
+ done();
+ };
+
+ const cache = createCache(
[{
- "ResourceARN": "arn:aws:s3:::test-bucket",
- "Tags": [],
- }],null
+ "Name": "test-bucket",
+ "CreationDate": "November 22, 2021, 15:51:19 (UTC+05:00)",
+ }], null,
+ {
+ "TagSet": []
+ }, null
);
s3BucketHasTags.run(cache, {}, callback);
diff --git a/plugins/aws/s3/s3Encryption.js b/plugins/aws/s3/s3Encryption.js
index 628967770..35ebea1df 100644
--- a/plugins/aws/s3/s3Encryption.js
+++ b/plugins/aws/s3/s3Encryption.js
@@ -155,7 +155,7 @@ module.exports = {
if (encryptionLevel.level) return encryptionLevel.level;
if (encryptionLevel.key) {
const keyId = encryptionLevel.key.split('/')[1];
- const describeKey = helpers.addSource(cache, source, ['kms', 'describeKey', region, keyId]);
+ const describeKey = helpers.addSource(cache, source, ['kms', 'describeKey', bucketLocation, keyId]);
if (!describeKey || describeKey.err || !describeKey.data) {
helpers.addResult(results, 3, `Unable to query for KMS Key: ${helpers.addError(describeKey)}`, region, keyId);
return 0;
diff --git a/plugins/aws/securityhub/securityHubActiveFindings.js b/plugins/aws/securityhub/securityHubActiveFindings.js
index 64ffc5707..ccf32f750 100644
--- a/plugins/aws/securityhub/securityHubActiveFindings.js
+++ b/plugins/aws/securityhub/securityHubActiveFindings.js
@@ -43,10 +43,10 @@ module.exports = {
var resource = describeHub.data.HubArn;
const getFindings = helpers.addSource(cache, source, ['securityhub', 'getFindings', region]);
- if (!getFindings) {
+ if (!getFindings || !getFindings.data) {
helpers.addResult(results, 0, 'No active findings available', region, resource);
return rcb();
- } else if (getFindings.err || !getFindings.data ) {
+ } else if (getFindings.err) {
helpers.addResult(results, 3, `Unable to get SecurityHub findings: ${helpers.addError(getFindings)}`, region, resource);
} else if (!getFindings.data.length) {
helpers.addResult(results, 0, 'No active findings available', region, resource);
diff --git a/plugins/aws/ses/dkimEnabled.js b/plugins/aws/ses/dkimEnabled.js
index 9e753b3f5..cf3937c77 100644
--- a/plugins/aws/ses/dkimEnabled.js
+++ b/plugins/aws/ses/dkimEnabled.js
@@ -51,9 +51,9 @@ module.exports = {
return rcb();
}
- for (var i in getIdentityDkimAttributes.data.DkimAttributes) {
- var resource = `arn:${awsOrGov}:ses:${region}:${accountId}:identity/${i}`;
- var identity = getIdentityDkimAttributes.data.DkimAttributes[i];
+ for (var identity of getIdentityDkimAttributes.data.DkimAttributes) {
+ if (!identity.identityName) continue;
+ var resource = `arn:${awsOrGov}:ses:${region}:${accountId}:identity/${identity.identityName}`;
if (!identity.DkimEnabled) {
helpers.addResult(results, 2, 'DKIM is not enabled', region, resource);
diff --git a/plugins/aws/ses/dkimEnabled.spec.js b/plugins/aws/ses/dkimEnabled.spec.js
index 60ff44c7c..b5ff05cba 100644
--- a/plugins/aws/ses/dkimEnabled.spec.js
+++ b/plugins/aws/ses/dkimEnabled.spec.js
@@ -9,6 +9,7 @@ const listIdentities = [
const getIdentityDkimAttributes = [
{
"DkimEnabled": true,
+ "identityName": 'abc.com',
"DkimVerificationStatus": "Pending",
"DkimTokens": [
"otux44vv2jf7bme4j6y7qyagkni466lo",
@@ -18,6 +19,7 @@ const getIdentityDkimAttributes = [
},
{
"DkimEnabled": true,
+ "identityName": 'test.com',
"DkimVerificationStatus": "Success",
"DkimTokens": [
"otux44vv2jf7bme4j6y7qyagkni466lo",
@@ -27,6 +29,7 @@ const getIdentityDkimAttributes = [
},
{
"DkimEnabled": false,
+ "identityName": 'test.com',
"DkimVerificationStatus": "Pending",
"DkimTokens": [
"otux44vv2jf7bme4j6y7qyagkni466lo",
diff --git a/plugins/aws/workspaces/unusedWorkspaces.js b/plugins/aws/workspaces/unusedWorkspaces.js
index a17a62fc4..4259660f5 100644
--- a/plugins/aws/workspaces/unusedWorkspaces.js
+++ b/plugins/aws/workspaces/unusedWorkspaces.js
@@ -7,10 +7,18 @@ module.exports = {
domain: 'Identity and Access Management',
severity: 'High',
description: 'Ensure that there are no unused AWS WorkSpaces instances available within your AWS account.',
- more_info: 'An AWS WorkSpaces instance is considered unused if it has 0 known user connections registered within the past 30 days. Remove these instances to avoid unnecessary billing.',
+ more_info: 'An AWS WorkSpaces instance is considered unused if it has 0 known user connections registered within the configured inactivity threshold. Remove these instances to avoid unnecessary billing.',
link: 'https://aws.amazon.com/workspaces/pricing/',
recommended_action: 'Identify and remove unused Workspaces instance',
apis: ['WorkSpaces:describeWorkspacesConnectionStatus','STS:getCallerIdentity'],
+ settings: {
+ workspaces_inactivity_threshold_days: {
+ name: 'WorkSpaces Inactivity Threshold (Days)',
+ description: 'Number of days of inactivity before a WorkSpace is considered unused',
+ regex: '^[0-9]{1,4}$',
+ default: '30'
+ }
+ },
realtime_triggers: ['workspace:CreateWorkSpaces','workspace:TerminateWorkspaces'],
run: function(cache, settings, callback) {
@@ -18,6 +26,10 @@ module.exports = {
var source = {};
var regions = helpers.regions(settings);
+ var config = {
+ workspaces_inactivity_threshold_days: parseInt(settings.workspaces_inactivity_threshold_days || this.settings.workspaces_inactivity_threshold_days.default)
+ };
+
var awsOrGov = helpers.defaultPartition(settings);
var acctRegion = helpers.defaultRegion(settings);
var accountId = helpers.addSource(cache, source, ['sts', 'getCallerIdentity', acctRegion , 'data']);
@@ -47,14 +59,16 @@ module.exports = {
if (!workspace.LastKnownUserConnectionTimestamp) {
helpers.addResult(results, 2,
'WorkSpace does not have any known user connection', region, resource);
- } else if (workspace.LastKnownUserConnectionTimestamp &&
- (helpers.daysBetween(new Date(), workspace.LastKnownUserConnectionTimestamp)) > 30) {
- helpers.addResult(results, 2,
- `WorkSpace is not in use for last ${helpers.daysBetween(new Date(), workspace.LastKnownUserConnectionTimestamp)}`,
- region, resource);
} else {
- helpers.addResult(results, 0,
- 'WorkSpace is in use', region, resource);
+ var daysSinceLastConnection = helpers.daysBetween(new Date(), workspace.LastKnownUserConnectionTimestamp);
+ if (daysSinceLastConnection > config.workspaces_inactivity_threshold_days) {
+ helpers.addResult(results, 2,
+ `WorkSpace is not in use for last ${daysSinceLastConnection} days (threshold: ${config.workspaces_inactivity_threshold_days} days)`,
+ region, resource);
+ } else {
+ helpers.addResult(results, 0,
+ 'WorkSpace is in use', region, resource);
+ }
}
});
diff --git a/plugins/aws/workspaces/unusedWorkspaces.spec.js b/plugins/aws/workspaces/unusedWorkspaces.spec.js
index dc41a2cc9..9cc134007 100644
--- a/plugins/aws/workspaces/unusedWorkspaces.spec.js
+++ b/plugins/aws/workspaces/unusedWorkspaces.spec.js
@@ -12,10 +12,16 @@ const describeWorkspacesConnectionStatus = [
WorkspaceId: "test02",
ConnectionState:"DISCONNECTED",
ConnectionStateCheckTimestamp:"2021-10-04T08:56:18.935Z",
- LastKnownUserConnectionTimestamp: "2020-10-04T08:56:18.935Z"
+ LastKnownUserConnectionTimestamp: new Date(Date.now() - 35 * 24 * 60 * 60 * 1000).toISOString()
},
{
- WorkspaceId: "test02",
+ WorkspaceId: "test03",
+ ConnectionState:"DISCONNECTED",
+ ConnectionStateCheckTimestamp:"2021-10-04T08:56:18.935Z",
+ LastKnownUserConnectionTimestamp: new Date(Date.now() - 150 * 24 * 60 * 60 * 1000).toISOString()
+ },
+ {
+ WorkspaceId: "test04",
ConnectionState:"DISCONNECTED",
ConnectionStateCheckTimestamp:"2021-10-04T08:56:18.935Z"
},
@@ -70,12 +76,27 @@ describe('unusedWorkspaces', function () {
expect(results.length).to.equal(1);
expect(results[0].status).to.equal(2);
expect(results[0].region).to.equal('us-east-1');
+ expect(results[0].message).to.include('threshold: 30 days');
done();
});
});
- it('should FAIL if WorkSpace does not have any known user connection', function (done) {
+ it('should FAIL if Workspace is not in use for 150 days with 120 day threshold', function (done) {
const cache = createCache([describeWorkspacesConnectionStatus[2]]);
+ const settings = {
+ workspaces_inactivity_threshold_days: '120'
+ };
+ unusedWorkspaces.run(cache, settings, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(2);
+ expect(results[0].region).to.equal('us-east-1');
+ expect(results[0].message).to.include('threshold: 120 days');
+ done();
+ });
+ });
+
+ it('should FAIL if WorkSpace does not have any known user connection', function (done) {
+ const cache = createCache([describeWorkspacesConnectionStatus[3]]);
unusedWorkspaces.run(cache, {}, (err, results) => {
expect(results.length).to.equal(1);
expect(results[0].status).to.equal(2);
diff --git a/plugins/aws/workspaces/workspacesIpAccessControl.js b/plugins/aws/workspaces/workspacesIpAccessControl.js
index 06ebf4201..33def0c3c 100644
--- a/plugins/aws/workspaces/workspacesIpAccessControl.js
+++ b/plugins/aws/workspaces/workspacesIpAccessControl.js
@@ -3,7 +3,7 @@ var helpers = require('../../../helpers/aws');
module.exports = {
title: 'Workspaces IP Access Control',
- category: 'Workspaces',
+ category: 'WorkSpaces',
domain: 'Identity and Access Management',
severity: 'Medium',
description: 'Ensures enforced IP Access Control on Workspaces',
diff --git a/plugins/azure/advisor/checkAdvisorRecommendations.js b/plugins/azure/advisor/checkAdvisorRecommendations.js
index 242fd116c..99bd9cf34 100644
--- a/plugins/azure/advisor/checkAdvisorRecommendations.js
+++ b/plugins/azure/advisor/checkAdvisorRecommendations.js
@@ -8,7 +8,7 @@ module.exports = {
severity: 'Medium',
description: 'Ensure that all Microsoft Azure Advisor recommendations found are implemented to optimize your cloud deployments, increase security, and reduce costs.',
more_info: 'Advisor service analyzes your Azure cloud configurations and resource usage telemetry to provide personalized and actionable recommendations that can help you optimize your cloud resources for security, reliability and high availability, operational excellence, performance efficiency, and cost.',
- recommended_action: 'Implement all Microsoft Azurer Adivsor recommendations.',
+ recommended_action: 'Implement all Microsoft Azure Advisor recommendations.',
link: 'https://learn.microsoft.com/en-us/azure/advisor/advisor-get-started',
apis: ['advisor:recommendationsList'],
diff --git a/plugins/azure/advisor/checkAdvisorRecommendations.spec.js b/plugins/azure/advisor/checkAdvisorRecommendations.spec.js
index c8443c839..54753ec2a 100644
--- a/plugins/azure/advisor/checkAdvisorRecommendations.spec.js
+++ b/plugins/azure/advisor/checkAdvisorRecommendations.spec.js
@@ -40,7 +40,7 @@ const createCache = (err, recommendationsList) => {
describe('checkAdvisorRecommendations', function() {
describe('run', function() {
- it('should give passing result if no Adivsor Recommendations are found', function(done) {
+ it('should give passing result if no Advisor Recommendations are found', function(done) {
const callback = (err, results) => {
expect(results.length).to.equal(1)
expect(results[0].status).to.equal(0)
diff --git a/plugins/azure/apiManagement/apiInstanceManagedIdentity.js b/plugins/azure/apiManagement/apiInstanceManagedIdentity.js
index 57557b018..b08f064e4 100644
--- a/plugins/azure/apiManagement/apiInstanceManagedIdentity.js
+++ b/plugins/azure/apiManagement/apiInstanceManagedIdentity.js
@@ -7,7 +7,7 @@ module.exports = {
domain: 'Developer Tools',
severity: 'Medium',
description: 'Ensures that Azure API Management instance has managed identity enabled.',
- more_info: 'Enabling managed identities eliminate the need for developers having to manage credentials by providing an identity for the Azure resource in Azure AD and using it to obtain Azure Active Directory (Azure AD) tokens.',
+ more_info: 'Enabling managed identities eliminate the need for developers having to manage credentials by providing an identity for the Azure resource in Azure AD and using it to obtain Azure Entra ID tokens.',
link: 'https://learn.microsoft.com/en-us/azure/api-management/api-management-howto-use-managed-service-identity',
recommended_action: 'Modify API Management instance and add managed identity.',
apis: ['apiManagementService:list'],
diff --git a/plugins/azure/appConfigurations/appConfigAccessKeyAuthDisabled.js b/plugins/azure/appConfigurations/appConfigAccessKeyAuthDisabled.js
index 922bc1e66..6ba03cc4b 100644
--- a/plugins/azure/appConfigurations/appConfigAccessKeyAuthDisabled.js
+++ b/plugins/azure/appConfigurations/appConfigAccessKeyAuthDisabled.js
@@ -7,7 +7,7 @@ module.exports = {
domain: 'Developer Tools',
severity: 'Low',
description: 'Ensures that access key authentication is disabled for App Configuration.',
- more_info: 'By default, requests can be authenticated with either Microsoft Entra credentials, or by using an access key. For enhanced security, centralized identity management, and seamless integration with Azure\'s authentication and authorization services, it is recommended to rely on Azure Active Directory (Azure AD) and disable local authentication for Azure App Configurations.',
+ more_info: 'By default, requests can be authenticated with either Microsoft Entra credentials, or by using an access key. For enhanced security, centralized identity management, and seamless integration with Azure\'s authentication and authorization services, it is recommended to rely on Azure Entra ID and disable local authentication for Azure App Configurations.',
link: 'https://learn.microsoft.com/en-us/azure/azure-app-configuration/howto-disable-access-key-authentication',
recommended_action: 'Ensure that Azure App Configurations have access key authentication disabled.',
apis: ['appConfigurations:list'],
diff --git a/plugins/azure/appConfigurations/appConfigManagedIdentity.js b/plugins/azure/appConfigurations/appConfigManagedIdentity.js
index 1989c003f..2bd8e7b51 100644
--- a/plugins/azure/appConfigurations/appConfigManagedIdentity.js
+++ b/plugins/azure/appConfigurations/appConfigManagedIdentity.js
@@ -7,7 +7,7 @@ module.exports = {
domain: 'Developer Tools',
severity: 'Medium',
description: 'Ensures that Azure App Configurations have managed identity enabled.',
- more_info: 'Enabling managed identities eliminate the need for developers having to manage credentials by providing an identity for the Azure resource in Azure AD and using it to obtain Azure Active Directory (Azure AD) tokens.',
+ more_info: 'Enabling managed identities eliminate the need for developers having to manage credentials by providing an identity for the Azure resource in Azure AD and using it to obtain Azure Entra ID tokens.',
link: 'https://learn.microsoft.com/en-us/azure/azure-app-configuration/overview-managed-identity',
recommended_action: 'Modify App Configuration store and add managed identity.',
apis: ['appConfigurations:list'],
diff --git a/plugins/azure/appservice/appServiceAccessRestriction.js b/plugins/azure/appservice/appServiceAccessRestriction.js
index da71ba7b2..e11e94802 100644
--- a/plugins/azure/appservice/appServiceAccessRestriction.js
+++ b/plugins/azure/appservice/appServiceAccessRestriction.js
@@ -7,8 +7,7 @@ module.exports = {
domain: 'Application Integration',
severity: 'Medium',
description: 'Ensure that Azure App Services have access restriction configured to control network access to your app.',
- more_info: 'By setting up access restrictions, you can define a priority-ordered allow/deny list that controls network access to your app. ' +
- 'The list can include IP addresses or Azure Virtual Network subnets. When there are one or more entries, an implicit deny all exists at the end of the list.',
+ more_info: 'By setting up access restrictions, you can define a priority-ordered allow/deny list that controls network access to your app. The list can include IP addresses or Azure Virtual Network subnets. When there are one or more entries, an implicit deny all exists at the end of the list. The most secure configuration is to disable public network access entirely. If public access is enabled, this plugin checks for explicit access restrictions with an "Any" IP address and "Deny" action rule.',
recommended_action: 'Add access restriction rules under network settings for the app services',
link: 'https://learn.microsoft.com/en-us/azure/app-service/app-service-ip-restrictions#set-up-azure-functions-access-restrictions',
apis: ['webApps:list', 'webApps:listConfigurations'],
@@ -50,20 +49,30 @@ module.exports = {
'Unable to query App Service configuration: ' + helpers.addError(webConfigs),
location, webApp.id);
} else {
- let denyAllIp;
- if (webConfigs.data[0].ipSecurityRestrictions && webConfigs.data[0].ipSecurityRestrictions.length) {
- denyAllIp = webConfigs.data[0].ipSecurityRestrictions.find(ipSecurityRestriction =>
- ipSecurityRestriction.ipAddress && ipSecurityRestriction.ipAddress.toUpperCase() === 'ANY' &&
- ipSecurityRestriction.action && ipSecurityRestriction.action.toUpperCase() === 'DENY'
- );
- }
+ const config = webConfigs.data[0];
- if (denyAllIp) {
+ if (config.publicNetworkAccess && config.publicNetworkAccess.toLowerCase() === 'disabled') {
helpers.addResult(results, 0,
'App Service has access restriction enabled',
location, webApp.id);
} else {
- helpers.addResult(results, 2, 'App Service does not have access restriction enabled', location, webApp.id);
+ let denyAllIp;
+ if (config.ipSecurityRestrictions && config.ipSecurityRestrictions.length) {
+ denyAllIp = config.ipSecurityRestrictions.find(ipSecurityRestriction =>
+ ipSecurityRestriction.ipAddress && ipSecurityRestriction.ipAddress.toUpperCase() === 'ANY' &&
+ ipSecurityRestriction.action && ipSecurityRestriction.action.toUpperCase() === 'DENY'
+ );
+ }
+
+ if (denyAllIp) {
+ helpers.addResult(results, 0,
+ 'App Service has access restriction enabled',
+ location, webApp.id);
+ } else {
+ helpers.addResult(results, 2,
+ 'App Service does not have access restriction enabled',
+ location, webApp.id);
+ }
}
}
});
diff --git a/plugins/azure/appservice/appServiceAccessRestriction.spec.js b/plugins/azure/appservice/appServiceAccessRestriction.spec.js
index 993770048..d6c7520d7 100644
--- a/plugins/azure/appservice/appServiceAccessRestriction.spec.js
+++ b/plugins/azure/appservice/appServiceAccessRestriction.spec.js
@@ -11,6 +11,7 @@ const webApps = [
const configurations = [
{
'id': '/subscriptions/123/resourceGroups/khulnasoft-resource-group/providers/Microsoft.Web/sites/app1/config/web',
+ 'publicNetworkAccess': 'Enabled',
'ipSecurityRestrictions': [
{
'ipAddress': 'Any',
@@ -23,6 +24,7 @@ const configurations = [
},
{
'id': '/subscriptions/123/resourceGroups/khulnasoft-resource-group/providers/Microsoft.Web/sites/app1/config/web',
+ 'publicNetworkAccess': 'Enabled',
'ipSecurityRestrictions': [
{
'ipAddress': '208.130.0.0/16',
@@ -39,6 +41,22 @@ const configurations = [
'description': 'Deny all access'
}
]
+ },
+ {
+ 'id': '/subscriptions/123/resourceGroups/khulnasoft-resource-group/providers/Microsoft.Web/sites/app1/config/web',
+ 'publicNetworkAccess': 'Disabled'
+ },
+ {
+ 'id': '/subscriptions/123/resourceGroups/khulnasoft-resource-group/providers/Microsoft.Web/sites/app1/config/web',
+ 'publicNetworkAccess': 'Enabled',
+ 'ipSecurityRestrictions': [
+ {
+ 'ipAddress': '192.168.1.0/24',
+ 'action': 'Allow',
+ 'priority': 100,
+ 'name': 'Office Network'
+ }
+ ],
}
];
@@ -123,7 +141,18 @@ describe('appServiceAccessRestriction', function() {
});
});
- it('should give passing result if app Service has access restriction enabled', function(done) {
+ it('should give passing result if public network access is disabled (most secure)', function(done) {
+ const cache = createCache([webApps[0]], [configurations[2]]);
+ appServiceAccessRestriction.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(0);
+ expect(results[0].message).to.include('App Service has access restriction enabled');
+ expect(results[0].region).to.equal('eastus');
+ done();
+ });
+ });
+
+ it('should give passing result if app Service has explicit Any/Deny rule', function(done) {
const cache = createCache([webApps[0]], [configurations[1]]);
appServiceAccessRestriction.run(cache, {}, (err, results) => {
expect(results.length).to.equal(1);
@@ -134,7 +163,7 @@ describe('appServiceAccessRestriction', function() {
});
});
- it('should give failing result if App Service does not have access restriction enabled', function(done) {
+ it('should give failing result if App Service has allow all rule', function(done) {
const cache = createCache([webApps[0]], [configurations[0]]);
appServiceAccessRestriction.run(cache, {}, (err, results) => {
expect(results.length).to.equal(1);
@@ -144,5 +173,16 @@ describe('appServiceAccessRestriction', function() {
done();
});
});
+
+ it('should give failing result if App Service has specific IP restrictions but no Any/Deny rule', function(done) {
+ const cache = createCache([webApps[0]], [configurations[3]]);
+ appServiceAccessRestriction.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(2);
+ expect(results[0].message).to.include('App Service does not have access restriction enabled');
+ expect(results[0].region).to.equal('eastus');
+ done();
+ });
+ });
});
});
\ No newline at end of file
diff --git a/plugins/azure/appservice/appServicePublicAccess.js b/plugins/azure/appservice/appServicePublicAccess.js
new file mode 100644
index 000000000..246ca91c2
--- /dev/null
+++ b/plugins/azure/appservice/appServicePublicAccess.js
@@ -0,0 +1,69 @@
+var async = require('async');
+var helpers = require('../../../helpers/azure');
+
+module.exports = {
+ title: 'App Service Public Network Access Disabled',
+ category: 'App Service',
+ domain: 'Application Integration',
+ severity: 'High',
+ description: 'Ensure that Azure App Services have public network access disabled to prevent exposure of the application to the internet.',
+ more_info: 'By default, App Services may allow public network traffic unless explicitly disabled. Public network access can be disabled using the publicNetworkAccess property or by configuring a private endpoint. Disabling public network access ensures that your applications are only reachable through secure private endpoints and not exposed to the public internet.',
+ recommended_action: 'Set the Public network access setting to Disabled in the App Service Networking configuration, or configure a private endpoint to restrict access. You can do this via the Azure Portal, CLI, or ARM template.',
+ link: 'https://learn.microsoft.com/en-us/azure/app-service/overview-access-restrictions#ip-based-access-restriction-rules',
+ apis: ['webApps:list', 'webApps:listConfigurations'],
+ realtime_triggers: ['microsoftweb:sites:write', 'microsoftweb:sites:delete', 'microsoftweb:sites:config:write', 'microsoftweb:sites:config:delete'],
+
+ run: function(cache, settings, callback) {
+ var results = [];
+ var source = {};
+ var locations = helpers.locations(settings.govcloud);
+
+ async.each(locations.webApps, function(location, rcb) {
+ var webApps = helpers.addSource(cache, source,
+ ['webApps', 'list', location]);
+
+ if (!webApps) return rcb();
+
+ if (webApps.err || !webApps.data) {
+ helpers.addResult(results, 3,
+ 'Unable to query for App Services: ' + helpers.addError(webApps), location);
+ return rcb();
+ }
+
+ if (!webApps.data.length) {
+ helpers.addResult(results, 0, 'No existing App Services found', location);
+ return rcb();
+ }
+
+ webApps.data.forEach(function(webApp) {
+ if (!webApp.id) return;
+
+ var webConfigs = helpers.addSource(cache, source,
+ ['webApps', 'listConfigurations', location, webApp.id]);
+
+ if (!webConfigs || webConfigs.err || !webConfigs.data || !webConfigs.data.length) {
+ helpers.addResult(results, 3,
+ 'Unable to query App Service configuration: ' + helpers.addError(webConfigs),
+ location, webApp.id);
+ return;
+ }
+
+ var config = webConfigs.data[0];
+
+ if (config.publicNetworkAccess && config.publicNetworkAccess.toLowerCase() === 'disabled') {
+ helpers.addResult(results, 0,
+ 'App Service has public network access disabled',
+ location, webApp.id);
+ } else {
+ helpers.addResult(results, 2,
+ 'App Service does not have public network access disabled',
+ location, webApp.id);
+ }
+ });
+
+ rcb();
+ }, function() {
+ callback(null, results, source);
+ });
+ }
+};
\ No newline at end of file
diff --git a/plugins/azure/appservice/appServicePublicAccess.spec.js b/plugins/azure/appservice/appServicePublicAccess.spec.js
new file mode 100644
index 000000000..cd732733d
--- /dev/null
+++ b/plugins/azure/appservice/appServicePublicAccess.spec.js
@@ -0,0 +1,151 @@
+var expect = require('chai').expect;
+var appServicePublicAccess = require('./appServicePublicAccess');
+
+const webApps = [
+ {
+ 'id': '/subscriptions/123/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app-1',
+ 'name': 'test-app-1',
+ 'type': 'Microsoft.Web/sites',
+ 'kind': 'app',
+ 'location': 'eastus'
+ },
+ {
+ 'id': '/subscriptions/123/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app-2',
+ 'name': 'test-app-2',
+ 'type': 'Microsoft.Web/sites',
+ 'kind': 'app',
+ 'location': 'eastus'
+ },
+ {
+ 'id': '/subscriptions/123/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app-3',
+ 'name': 'test-app-3',
+ 'type': 'Microsoft.Web/sites',
+ 'kind': 'functionapp',
+ 'location': 'eastus'
+ }
+];
+
+const listConfigurations = [
+ {
+ 'id': '/subscriptions/123/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app-1/config/web',
+ 'name': 'web',
+ 'publicNetworkAccess': 'Disabled'
+ },
+ {
+ 'id': '/subscriptions/123/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app-2/config/web',
+ 'name': 'web',
+ 'publicNetworkAccess': 'Enabled'
+ },
+ {
+ 'id': '/subscriptions/123/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app-3/config/web',
+ 'name': 'web'
+ }
+];
+
+const createCache = (webApps, configurations, webAppsErr, configurationsErr) => {
+ const appId = (webApps && webApps.length) ? webApps[0].id : null;
+ return {
+ webApps: {
+ list: {
+ 'eastus': {
+ err: webAppsErr,
+ data: webApps
+ }
+ },
+ listConfigurations: {
+ 'eastus': {
+ [appId]: {
+ err: configurationsErr,
+ data: configurations
+ }
+ }
+ }
+ }
+ };
+};
+
+describe('appServicePublicAccess', function () {
+ describe('run', function () {
+ it('should give passing result if no web apps found', function (done) {
+ const cache = createCache([], null);
+ appServicePublicAccess.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(0);
+ expect(results[0].message).to.include('No existing App Services found');
+ expect(results[0].region).to.equal('eastus');
+ done();
+ });
+ });
+
+ it('should give unknown result if unable to query for web apps', function (done) {
+ const cache = createCache(null, null, { message: 'Unable to query Web Apps' });
+ appServicePublicAccess.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(3);
+ expect(results[0].message).to.include('Unable to query for App Services');
+ expect(results[0].region).to.equal('eastus');
+ done();
+ });
+ });
+
+ it('should give unknown result if unable to query web app configuration', function (done) {
+ const cache = createCache([webApps[0]], null, null, { message: 'Unable to query configuration' });
+ appServicePublicAccess.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(3);
+ expect(results[0].message).to.include('Unable to query App Service configuration');
+ expect(results[0].region).to.equal('eastus');
+ done();
+ });
+ });
+
+ it('should give passing result if App Service has public network access disabled', function (done) {
+ const cache = createCache([webApps[0]], [listConfigurations[0]]);
+ appServicePublicAccess.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(0);
+ expect(results[0].message).to.include('App Service has public network access disabled');
+ expect(results[0].region).to.equal('eastus');
+ done();
+ });
+ });
+
+ it('should give failing result if App Service has public network access enabled', function (done) {
+ const cache = createCache([webApps[1]], [listConfigurations[1]]);
+ appServicePublicAccess.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(2);
+ expect(results[0].message).to.include('App Service does not have public network access disabled');
+ expect(results[0].region).to.equal('eastus');
+ done();
+ });
+ });
+
+ it('should give failing result if App Service publicNetworkAccess property is not set', function (done) {
+ const cache = createCache([webApps[2]], [listConfigurations[2]]);
+ appServicePublicAccess.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(2);
+ expect(results[0].message).to.include('App Service does not have public network access disabled');
+ expect(results[0].region).to.equal('eastus');
+ done();
+ });
+ });
+
+ it('should give passing result for App Service with case-insensitive disabled value', function (done) {
+ const config = [{
+ 'id': '/subscriptions/123/resourceGroups/test-rg/providers/Microsoft.Web/sites/test-app-1/config/web',
+ 'name': 'web',
+ 'publicNetworkAccess': 'disabled'
+ }];
+ const cache = createCache([webApps[0]], config);
+ appServicePublicAccess.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(0);
+ expect(results[0].message).to.include('App Service has public network access disabled');
+ expect(results[0].region).to.equal('eastus');
+ done();
+ });
+ });
+ });
+});
\ No newline at end of file
diff --git a/plugins/azure/appservice/authEnabled.js b/plugins/azure/appservice/authEnabled.js
index 6c2cec0d3..8695a4146 100644
--- a/plugins/azure/appservice/authEnabled.js
+++ b/plugins/azure/appservice/authEnabled.js
@@ -7,7 +7,7 @@ module.exports = {
domain: 'Application Integration',
severity: 'Medium',
description: 'Ensures Authentication is enabled for App Services, redirecting unauthenticated users to the login page.',
- more_info: 'Enabling authentication will redirect all unauthenticated requests to the login page. It also handles authentication of users with specific providers (Azure Active Directory, Facebook, Google, Microsoft Account, and Twitter).',
+ more_info: 'Enabling authentication will redirect all unauthenticated requests to the login page. It also handles authentication of users with specific providers (Azure Entra ID, Facebook, Google, Microsoft Account, and Twitter).',
recommended_action: 'Enable App Service Authentication for all App Services.',
link: 'https://learn.microsoft.com/en-us/azure/app-service/overview-authentication-authorization',
apis: ['webApps:list', 'webApps:getAuthSettings'],
diff --git a/plugins/azure/appservice/automatedBackupsEnabled.js b/plugins/azure/appservice/automatedBackupsEnabled.js
index c4517bf65..4448bb7b1 100644
--- a/plugins/azure/appservice/automatedBackupsEnabled.js
+++ b/plugins/azure/appservice/automatedBackupsEnabled.js
@@ -2,13 +2,13 @@ var async = require('async');
var helpers = require('../../../helpers/azure');
module.exports = {
- title: 'Web Apps Backup Enabled',
+ title: 'Web Apps Custom Backup Enabled',
category: 'App Service',
domain: 'Application Integration',
severity: 'Medium',
- description: 'Ensures that Azure Web Apps have automated backups enabled.',
+ description: 'Ensures that Azure Web Apps have custom automated backups enabled.',
more_info: 'Protect your Azure App Services web applications against accidental deletion and/or corruption, you can configure application backups to create restorable copies of your app content.',
- recommended_action: 'Configure backup for Azure Web Apps',
+ recommended_action: 'Configure custom automated backup for Azure Web Apps.',
link: 'https://learn.microsoft.com/en-us/azure/app-service/manage-backup',
apis: ['webApps:list', 'webApps:getBackupConfiguration'],
realtime_triggers: ['microsoftweb:sites:write','microsoftweb:sites:delete','microsoftweb:sites:config:write','microsoftweb:sites:config:delete'],
@@ -44,10 +44,10 @@ module.exports = {
['webApps', 'getBackupConfiguration', location, webApp.id]);
if (configs && configs.err && configs.err.includes('NotFound')) {
- helpers.addResult(results, 2, 'Backups are not configured for WebApp', location, webApp.id);
+ helpers.addResult(results, 2, 'Custom Backups are not configured for WebApp', location, webApp.id);
} else if (!configs || configs.err || !configs.data) {
helpers.addResult(results, 3, 'Unable to query for Web App backup configs: ' + helpers.addError(configs), location, webApp.id);
- } else helpers.addResult(results, 0, 'Backups are configured for WebApp', location, webApp.id);
+ } else helpers.addResult(results, 0, 'Custom Backups are configured for WebApp', location, webApp.id);
});
rcb();
@@ -55,4 +55,4 @@ module.exports = {
callback(null, results, source);
});
}
-};
+};
\ No newline at end of file
diff --git a/plugins/azure/appservice/automatedBackupsEnabled.spec.js b/plugins/azure/appservice/automatedBackupsEnabled.spec.js
index c257c8b9c..cd3bda1d6 100644
--- a/plugins/azure/appservice/automatedBackupsEnabled.spec.js
+++ b/plugins/azure/appservice/automatedBackupsEnabled.spec.js
@@ -27,12 +27,12 @@ const backupConfigs = {
enabled: true,
storageAccountUrl: 'https://akhtarrgdiag.blob.core.windows.net/appbackup?sp=rwdl&st=2022-03-16T07:51:37Z&se=2295-12-29T08:51:37Z&sv=2020-08-04&sr=c&sig=FeC0hGUrqJb6b%2Bh5qbIif84725sMjeqyNUzWa4tL3L4%3D',
backupSchedule: {
- frequencyInterval: 7,
- frequencyUnit: 'Day',
- keepAtLeastOneBackup: true,
- retentionPeriodInDays: 7,
- startTime: '2022-03-16T07:51:38.699',
- lastExecutionTime: '2022-03-16T07:53:38.4131659'
+ frequencyInterval: 7,
+ frequencyUnit: 'Day',
+ keepAtLeastOneBackup: true,
+ retentionPeriodInDays: 7,
+ startTime: '2022-03-16T07:51:38.699',
+ lastExecutionTime: '2022-03-16T07:53:38.4131659'
},
databases: [],
mySqlDumpParams: null
@@ -128,7 +128,7 @@ describe('automatedBackupsEnabled', function() {
automatedBackupsEnabled.run(cache, {}, (err, results) => {
expect(results.length).to.equal(1);
expect(results[0].status).to.equal(2);
- expect(results[0].message).to.include('Backups are not configured for WebApp');
+ expect(results[0].message).to.include('Custom Backups are not configured for WebApp');
expect(results[0].region).to.equal('eastus');
done();
});
@@ -139,10 +139,10 @@ describe('automatedBackupsEnabled', function() {
automatedBackupsEnabled.run(cache, {}, (err, results) => {
expect(results.length).to.equal(1);
expect(results[0].status).to.equal(0);
- expect(results[0].message).to.include('Backups are configured for WebApp');
+ expect(results[0].message).to.include('Custom Backups are configured for WebApp');
expect(results[0].region).to.equal('eastus');
done();
});
});
});
-});
+});
\ No newline at end of file
diff --git a/plugins/azure/appservice/functionAppNetworkExposure.js b/plugins/azure/appservice/functionAppNetworkExposure.js
new file mode 100644
index 000000000..77fec5e94
--- /dev/null
+++ b/plugins/azure/appservice/functionAppNetworkExposure.js
@@ -0,0 +1,131 @@
+var async = require('async');
+var helpers = require('../../../helpers/azure');
+
+module.exports = {
+ title: 'Internet Exposure',
+ category: 'App Service',
+ domain: 'Application Integration',
+ severity: 'Info',
+ description: 'Ensures that Azure function apps are not exposed to the internet.',
+ more_info: 'Azure Functions exposed to the internet are at higher risk of unauthorized access and exploitation. Securing access through proper configuration of authorization levels, IP restrictions, private endpoints, or service-specific security settings is critical to minimize vulnerabilities.',
+ recommended_action: 'Restrict Azure Function exposure by implementing secure access controls, such as authorization levels, IP restrictions, private endpoints, or integrating with VNETs.',
+ link: 'https://learn.microsoft.com/en-us/azure/azure-functions/functions-networking-options',
+ apis: ['webApps:list', 'applicationGateways:list', 'loadBalancers:list', 'classicFrontDoors:list', 'afdWafPolicies:listAll'],
+ realtime_triggers: ['microsoftweb:sites:write','microsoftweb:sites:delete', 'microsoftnetwork:applicationgateways:write', 'microsoftnetwork:applicationgateways:delete', 'microsoftnetwork:loadbalancers:write', 'microsoftnetwork:loadbalancers:delete',
+ 'microsoftnetwork:frontdoors:write', 'microsoftnetwork:frontdoors:delete', 'microsoftnetwork:frontdoorwebapplicationfirewallpolicies:write', 'microsoftnetwork:frontdoorwebapplicationfirewallpolicies:delete'],
+
+ run: function(cache, settings, callback) {
+ const results = [];
+ const source = {};
+ const locations = helpers.locations(settings.govcloud);
+
+ async.each(locations.webApps, function(location, rcb) {
+ const webApps = helpers.addSource(cache, source,
+ ['webApps', 'list', location]);
+
+ if (!webApps) return rcb();
+
+ if (webApps.err || !webApps.data) {
+ helpers.addResult(results, 3,
+ 'Unable to query for Function Apps: ' + helpers.addError(webApps), location);
+ return rcb();
+ }
+
+ if (webApps.data && webApps.data.length) {
+ webApps.data = webApps.data.filter(app => app.id && app.kind && app.kind.toLowerCase().includes('functionapp'));
+ }
+
+ if (!webApps.data.length) {
+ helpers.addResult(results, 0, 'No existing Function Apps found', location);
+ return rcb();
+ }
+
+ const appGateways = helpers.addSource(cache, source,
+ ['applicationGateways', 'list', location]);
+
+ const loadBalancers = helpers.addSource(cache, source,
+ ['loadBalancers', 'list', location]);
+
+
+ const frontDoors = helpers.addSource(cache, source,
+ ['classicFrontDoors', 'list', 'global']);
+
+
+ const wafPolicies = helpers.addSource(cache, source,
+ ['afdWafPolicies', 'listAll', 'global']);
+
+
+ for (let functionApp of webApps.data) {
+ let internetExposed = '';
+ if (functionApp.publicNetworkAccess && functionApp.publicNetworkAccess === 'Enabled') {
+ internetExposed = 'public network access';
+ } else {
+ let attachedResources = {
+ appGateways: [],
+ lbNames: [],
+ frontDoors: []
+ };
+
+ // list attached app gateways
+ if (appGateways && !appGateways.err && appGateways.data && appGateways.data.length) {
+ attachedResources.appGateways = appGateways.data.filter(ag =>
+ ag.backendAddressPools && ag.backendAddressPools.some(pool =>
+ pool.backendAddresses && pool.backendAddresses.some(addr =>
+ addr.fqdn === functionApp.properties.defaultHostName)));
+ }
+
+ //list attached load balancers
+ if (loadBalancers && !loadBalancers.err && loadBalancers.data && loadBalancers.data.length) {
+ attachedResources.lbNames = loadBalancers.data.filter(lb =>
+ lb.backendAddressPools && lb.backendAddressPools.some(pool =>
+ pool.properties.backendIPConfigurations &&
+ pool.properties.backendIPConfigurations.some(config =>
+ config.id.toLowerCase().includes(functionApp.id.toLowerCase()))));
+
+ attachedResources.lbNames = attachedResources.lbNames.map(lb => lb.name);
+ }
+
+ // list attached front doors
+ if (frontDoors && !frontDoors.err && frontDoors.data && frontDoors.data.length) {
+ frontDoors.data.forEach(fd => {
+ const isFunctionAppBackend = fd.backendPools && fd.backendPools.some(pool =>
+ pool.backends && pool.backends.some(backend =>
+ backend.address === functionApp.properties.defaultHostName));
+
+ if (isFunctionAppBackend) {
+ fd.associatedWafPolicies = [];
+
+ if (fd.frontendEndpoints && wafPolicies && !wafPolicies.err && wafPolicies.data && wafPolicies.data.length) {
+ fd.frontendEndpoints.forEach(endpoint => {
+ if (endpoint.webApplicationFirewallPolicyLink) {
+ const policyId = endpoint.webApplicationFirewallPolicyLink.id.toLowerCase();
+ const matchingPolicy = wafPolicies.data.find(policy =>
+ policy.id && policy.id.toLowerCase() === policyId);
+ if (matchingPolicy) {
+ fd.associatedWafPolicies.push(matchingPolicy);
+ }
+ }
+ });
+ }
+
+ attachedResources.frontDoors.push(fd);
+ }
+ });
+ }
+
+ internetExposed = helpers.checkNetworkExposure(cache, source, [], [], location, results, attachedResources, functionApp);
+ }
+
+ if (internetExposed && internetExposed.length) {
+ helpers.addResult(results, 2, `Function App is exposed to the internet through ${internetExposed}`, location, functionApp.id);
+ } else {
+ helpers.addResult(results, 0, 'Function App is not exposed to the internet', location, functionApp.id);
+ }
+ }
+
+ rcb();
+ }, function() {
+ callback(null, results, source);
+ });
+ }
+};
diff --git a/plugins/azure/appservice/functionPrivilegeAnalysis.js b/plugins/azure/appservice/functionPrivilegeAnalysis.js
new file mode 100644
index 000000000..eba8673b9
--- /dev/null
+++ b/plugins/azure/appservice/functionPrivilegeAnalysis.js
@@ -0,0 +1,24 @@
+module.exports = {
+ title: 'Privilege Analysis',
+ category: 'App Service',
+ domain: 'Web Apps',
+ severity: 'Info',
+ description: 'Ensures that no Azure Functions in your environment have excessive permissions.',
+ more_info: 'Azure Functions that use managed identities or service principals with excessive Azure AD permissions may pose security risks. It is a best practice to assign only the necessary permissions to the identities attached to functions.',
+ link: 'https://docs.microsoft.com/en-us/azure/app-service/overview-managed-identity',
+ recommended_action: 'Review and restrict the Azure AD roles associated with managed identities used by Azure Functions to follow the principle of least privilege.',
+ apis: [''],
+ realtime_triggers: [
+ 'Microsoft.Web/sites/write',
+ 'Microsoft.Web/sites/delete',
+ 'Microsoft.Web/sites/functions/write',
+ 'Microsoft.Web/sites/functions/delete',
+ ],
+
+ run: function(cache, settings, callback) {
+ var results = [];
+ var source = {};
+
+ callback(null, results, source);
+ },
+};
diff --git a/plugins/azure/appservice/privateEndpointsEnabled.js b/plugins/azure/appservice/privateEndpointsEnabled.js
index 454216400..17f8f002f 100644
--- a/plugins/azure/appservice/privateEndpointsEnabled.js
+++ b/plugins/azure/appservice/privateEndpointsEnabled.js
@@ -6,11 +6,11 @@ module.exports = {
category: 'App Service',
domain: 'Application Integration',
severity: 'Medium',
- description: 'Ensures that Web Apps are accessible only through private endpoints.',
- more_info: 'Enabling private endpoints for Azure App Service enhances security by allowing access exclusively through a private network, minimizing the risk of public internet exposure and protecting against external attacks.',
- recommended_action: 'Ensure that Private Endpoints are configured properly and Public Network Access is disabled for Web Apps.',
+ description: 'Ensures that Web Apps and Function Apps are accessible only through private endpoints.',
+ more_info: 'Enabling private endpoints for Azure App Service and Function Apps enhances security by allowing access exclusively through a private network, minimizing the risk of public internet exposure and protecting against external attacks.',
+ recommended_action: 'Ensure that Private Endpoints are configured properly and Public Network Access is disabled for Web Apps and Function Apps.',
link: 'https://learn.microsoft.com/en-us/azure/app-service/overview-private-endpoint',
- apis: ['webApps:list'],
+ apis: ['webApps:list', 'webApps:getWebAppDetails'],
realtime_triggers: ['microsoftweb:sites:write', 'microsoftweb:sites:privateendpointconnectionproxies:write', 'microsoftweb:sites:privateendpointconnectionproxies:delete', 'microsoftweb:sites:delete'],
run: function(cache, settings, callback) {
@@ -35,17 +35,40 @@ module.exports = {
}
webApps.data.forEach(function(webApp) {
- if (webApp && webApp.kind && webApp.kind === 'functionapp') {
- helpers.addResult(results, 0, 'Private Endpoints can not be configured for function apps', location, webApp.id);
- } else if (webApp && webApp.privateLinkIdentifiers) {
- helpers.addResult(results, 0, 'App Service has Private Endpoints configured', location, webApp.id);
+ if (!webApp || !webApp.id) return;
+
+ const webAppDetails = helpers.addSource(cache, source,
+ ['webApps', 'getWebAppDetails', location, webApp.id]);
+
+ let hasPrivateEndpoints = false;
+
+ if (webAppDetails && !webAppDetails.err && webAppDetails.data && webAppDetails.data.privateEndpointConnections) {
+ if (Array.isArray(webAppDetails.data.privateEndpointConnections) && webAppDetails.data.privateEndpointConnections.length > 0) {
+ hasPrivateEndpoints = true;
+ }
+ }
+
+ if (!hasPrivateEndpoints && webApp.privateEndpointConnections && webApp.privateEndpointConnections.length > 0) {
+ hasPrivateEndpoints = true;
+ }
+
+ if (hasPrivateEndpoints) {
+ if (webApp.kind && webApp.kind.toLowerCase().includes('functionapp')) {
+ helpers.addResult(results, 0, 'Function App has Private Endpoints configured', location, webApp.id);
+ } else {
+ helpers.addResult(results, 0, 'App Service has Private Endpoints configured', location, webApp.id);
+ }
} else {
- helpers.addResult(results, 2, 'App Service does not have Private Endpoints configured', location, webApp.id);
+ // No private endpoints configured
+ if (webApp.kind && webApp.kind.toLowerCase().includes('functionapp')) {
+ helpers.addResult(results, 2, 'Function App does not have Private Endpoints configured', location, webApp.id);
+ } else {
+ helpers.addResult(results, 2, 'App Service does not have Private Endpoints configured', location, webApp.id);
+ }
}
});
rcb();
}, function() {
- // Global checking goes here
callback(null, results, source);
});
}
diff --git a/plugins/azure/appservice/privateEndpointsEnabled.spec.js b/plugins/azure/appservice/privateEndpointsEnabled.spec.js
index 2482ca00e..efce9f52f 100644
--- a/plugins/azure/appservice/privateEndpointsEnabled.spec.js
+++ b/plugins/azure/appservice/privateEndpointsEnabled.spec.js
@@ -5,25 +5,67 @@ const webApps = [
{
'id': '/subscriptions/123/resourceGroups/khulnasoft-resource-group/providers/Microsoft.Web/sites/app1',
'name': 'app1',
- 'privateLinkIdentifiers': '123456'
+ 'kind': 'app',
+ 'privateEndpointConnections': [
+ {
+ 'id': '/subscriptions/123/resourceGroups/khulnasoft-resource-group/providers/Microsoft.Web/sites/app1/privateEndpointConnections/test-endpoint',
+ 'name': 'test-endpoint'
+ }
+ ]
},
{
- 'id': '/subscriptions/123/resourceGroups/khulnasoft-resource-group/providers/Microsoft.Web/sites/app1',
- 'name': 'app1',
- 'privateLinkIdentifiers': ''
+ 'id': '/subscriptions/123/resourceGroups/khulnasoft-resource-group/providers/Microsoft.Web/sites/app2',
+ 'name': 'app2',
+ 'kind': 'app',
+ 'privateEndpointConnections': []
+ },
+ {
+ 'id': '/subscriptions/123/resourceGroups/khulnasoft-resource-group/providers/Microsoft.Web/sites/func1',
+ 'name': 'func1',
+ 'kind': 'functionapp',
+ 'privateEndpointConnections': [
+ {
+ 'id': '/subscriptions/123/resourceGroups/khulnasoft-resource-group/providers/Microsoft.Web/sites/func1/privateEndpointConnections/func-endpoint',
+ 'name': 'func-endpoint'
+ }
+ ]
+ },
+ {
+ 'id': '/subscriptions/123/resourceGroups/khulnasoft-resource-group/providers/Microsoft.Web/sites/func2',
+ 'name': 'func2',
+ 'kind': 'functionapp',
+ 'privateEndpointConnections': []
}
];
-const createCache = (webApps) => {
- return {
+const createCache = (webApps, privateEndpointConnections) => {
+ let cache = {
webApps: {
list: {
- 'eastus':{
+ 'eastus': {
data: webApps
}
}
}
};
+
+ if (privateEndpointConnections && webApps) {
+ cache.webApps.getWebAppDetails = {
+ 'eastus': {}
+ };
+ webApps.forEach((webApp, index) => {
+ if (webApp && webApp.id) {
+ cache.webApps.getWebAppDetails['eastus'][webApp.id] = {
+ data: {
+ ...webApp,
+ privateEndpointConnections: privateEndpointConnections[index] || []
+ }
+ };
+ }
+ });
+ }
+
+ return cache;
};
const createErrorCache = () => {
@@ -61,7 +103,7 @@ describe('privateEndpointsEnabled', function() {
});
it('should give passing result if app service has Private Endpoints configured', function(done) {
- const cache = createCache([webApps[0]]);
+ const cache = createCache([webApps[0]], [[{id: 'endpoint1', name: 'test-endpoint'}]]);
privateEndpointsEnabled.run(cache, {}, (err, results) => {
expect(results.length).to.equal(1);
expect(results[0].status).to.equal(0);
@@ -71,8 +113,8 @@ describe('privateEndpointsEnabled', function() {
});
});
- it('should give failing result if app service app service does not have Private Endpoints configured', function(done) {
- const cache = createCache([webApps[1]]);
+ it('should give failing result if app service does not have Private Endpoints configured', function(done) {
+ const cache = createCache([webApps[1]], [[]]);
privateEndpointsEnabled.run(cache, {}, (err, results) => {
expect(results.length).to.equal(1);
expect(results[0].status).to.equal(2);
@@ -81,5 +123,27 @@ describe('privateEndpointsEnabled', function() {
done();
});
});
+
+ it('should give passing result if function app has Private Endpoints configured', function(done) {
+ const cache = createCache([webApps[2]], [[{id: 'func-endpoint', name: 'func-test-endpoint'}]]);
+ privateEndpointsEnabled.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(0);
+ expect(results[0].message).to.include('Function App has Private Endpoints configured');
+ expect(results[0].region).to.equal('eastus');
+ done();
+ });
+ });
+
+ it('should give failing result if function app does not have Private Endpoints configured', function(done) {
+ const cache = createCache([webApps[3]], [[]]);
+ privateEndpointsEnabled.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(2);
+ expect(results[0].message).to.include('Function App does not have Private Endpoints configured');
+ expect(results[0].region).to.equal('eastus');
+ done();
+ });
+ });
});
});
\ No newline at end of file
diff --git a/plugins/azure/appservice/scmSiteAccessRestriction.js b/plugins/azure/appservice/scmSiteAccessRestriction.js
index 5539f199f..f897e5408 100644
--- a/plugins/azure/appservice/scmSiteAccessRestriction.js
+++ b/plugins/azure/appservice/scmSiteAccessRestriction.js
@@ -45,20 +45,28 @@ module.exports = {
'Unable to query App Service configuration: ' + helpers.addError(webConfigs),
location, webApp.id);
} else {
- let denyAllIp;
- if (webConfigs.data[0].scmIpSecurityRestrictions && webConfigs.data[0].scmIpSecurityRestrictions.length) {
- denyAllIp = webConfigs.data[0].scmIpSecurityRestrictions.find(ipSecurityRestriction =>
- ipSecurityRestriction.ipAddress && ipSecurityRestriction.ipAddress.toUpperCase() === 'ANY' &&
- ipSecurityRestriction.action && ipSecurityRestriction.action.toUpperCase() === 'DENY'
- );
- }
+ const config = webConfigs.data[0];
- if (denyAllIp) {
+ if (config.publicNetworkAccess && config.publicNetworkAccess.toLowerCase() === 'disabled') {
helpers.addResult(results, 0,
'App Service has access restriction enabled for scm site',
location, webApp.id);
} else {
- helpers.addResult(results, 2, 'App Service does not have access restriction enabled for scm site', location, webApp.id);
+ let denyAllIp;
+ if (config.scmIpSecurityRestrictions && config.scmIpSecurityRestrictions.length) {
+ denyAllIp = config.scmIpSecurityRestrictions.find(ipSecurityRestriction =>
+ ipSecurityRestriction.ipAddress && ipSecurityRestriction.ipAddress.toUpperCase() === 'ANY' &&
+ ipSecurityRestriction.action && ipSecurityRestriction.action.toUpperCase() === 'DENY'
+ );
+ }
+
+ if (denyAllIp) {
+ helpers.addResult(results, 0,
+ 'App Service has access restriction enabled for scm site',
+ location, webApp.id);
+ } else {
+ helpers.addResult(results, 2, 'App Service does not have access restriction enabled for scm site', location, webApp.id);
+ }
}
}
});
@@ -69,4 +77,6 @@ module.exports = {
callback(null, results, source);
});
}
-};
\ No newline at end of file
+};
+
+
diff --git a/plugins/azure/appservice/scmSiteAccessRestriction.spec.js b/plugins/azure/appservice/scmSiteAccessRestriction.spec.js
index a1e128da4..8d8847f10 100644
--- a/plugins/azure/appservice/scmSiteAccessRestriction.spec.js
+++ b/plugins/azure/appservice/scmSiteAccessRestriction.spec.js
@@ -39,6 +39,24 @@ const configurations = [
'description': 'Deny all access'
}
]
+ },
+ {
+ 'id': '/subscriptions/123/resourceGroups/khulnasoft-resource-group/providers/Microsoft.Web/sites/app1/config/web',
+ 'publicNetworkAccess': 'Disabled',
+ 'scmIpSecurityRestrictions': []
+ },
+ {
+ 'id': '/subscriptions/123/resourceGroups/khulnasoft-resource-group/providers/Microsoft.Web/sites/app1/config/web',
+ 'publicNetworkAccess': 'Disabled',
+ 'scmIpSecurityRestrictions': [
+ {
+ 'ipAddress': 'Any',
+ 'action': 'Allow',
+ 'priority': 1,
+ 'name': 'Allow all',
+ 'description': 'Allow all access'
+ }
+ ]
}
];
@@ -144,5 +162,27 @@ describe('scmSiteAccessRestriction', function() {
done();
});
});
+
+ it('should give passing result if App Service has public network access disabled with no IP restrictions', function(done) {
+ const cache = createCache([webApps[0]], [configurations[2]]);
+ scmSiteAccessRestriction.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(0);
+ expect(results[0].message).to.include('App Service has access restriction enabled for scm site');
+ expect(results[0].region).to.equal('eastus');
+ done();
+ });
+ });
+
+ it('should give passing result if App Service has public network access disabled even with allow all IP restrictions', function(done) {
+ const cache = createCache([webApps[0]], [configurations[3]]);
+ scmSiteAccessRestriction.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(0);
+ expect(results[0].message).to.include('App Service has access restriction enabled for scm site');
+ expect(results[0].region).to.equal('eastus');
+ done();
+ });
+ });
});
});
\ No newline at end of file
diff --git a/plugins/azure/appservice/webAppsADEnabled.js b/plugins/azure/appservice/webAppsADEnabled.js
index 6c4caf0da..a49de6d3c 100644
--- a/plugins/azure/appservice/webAppsADEnabled.js
+++ b/plugins/azure/appservice/webAppsADEnabled.js
@@ -2,13 +2,13 @@ var async = require('async');
var helpers = require('../../../helpers/azure');
module.exports = {
- title: 'Web Apps Active Directory Enabled',
+ title: 'Web Apps Entra ID Enabled',
category: 'App Service',
domain: 'Application Integration',
severity: 'Medium',
- description: 'Ensures that Azure Web Apps have registration with Azure Active Directory.',
- more_info: 'Registration with Azure Active Directory (AAD) enables App Service web applications to connect to other Azure cloud services securely without the need of access credentials such as user names and passwords.',
- recommended_action: 'Enable registration with Azure Active Directory for Azure Web Apps.',
+ description: 'Ensures that Azure Web Apps have registration with Azure Entra ID.',
+ more_info: 'Registration with Azure Entra ID enables App Service web applications to connect to other Azure cloud services securely without the need of access credentials such as user names and passwords.',
+ recommended_action: 'Enable registration with Azure Entra ID for Azure Web Apps.',
link: 'https://learn.microsoft.com/en-us/azure/app-service/overview-managed-identity?tabs=portal%2Chttp#add-a-system-assigned-identity',
apis: ['webApps:list'],
realtime_triggers: ['microsoftweb:sites:write','microsoftweb:sites:delete'],
@@ -36,9 +36,9 @@ module.exports = {
for (let app of webApps.data) {
if (app.identity && app.identity.principalId) {
- helpers.addResult(results, 0, 'Registration with Azure Active Directory is enabled for the Web App', location, app.id);
+ helpers.addResult(results, 0, 'Registration with Azure Entra ID is enabled for the Web App', location, app.id);
} else {
- helpers.addResult(results, 2, 'Registration with Azure Active Directory is disabled for the Web App', location, app.id);
+ helpers.addResult(results, 2, 'Registration with Azure Entra ID is disabled for the Web App', location, app.id);
}
}
diff --git a/plugins/azure/appservice/webAppsADEnabled.spec.js b/plugins/azure/appservice/webAppsADEnabled.spec.js
index 06b8be84f..26e86be42 100644
--- a/plugins/azure/appservice/webAppsADEnabled.spec.js
+++ b/plugins/azure/appservice/webAppsADEnabled.spec.js
@@ -72,23 +72,23 @@ describe('webAppsADEnabled', function() {
});
});
- it('should give passing result if Registration with Azure Active Directory is enabled', function(done) {
+ it('should give passing result if Registration with Azure Entra ID is enabled', function(done) {
const cache = createCache([webApps[1]]);
webAppsADEnabled.run(cache, {}, (err, results) => {
expect(results.length).to.equal(1);
expect(results[0].status).to.equal(0);
- expect(results[0].message).to.include('Registration with Azure Active Directory is enabled for the Web App');
+ expect(results[0].message).to.include('Registration with Azure Entra ID is enabled for the Web App');
expect(results[0].region).to.equal('eastus');
done();
});
});
- it('should give failing result if Registration with Azure Active Directory is disabled', function(done) {
+ it('should give failing result if Registration with Azure Entra ID is disabled', function(done) {
const cache = createCache([webApps[0]]);
webAppsADEnabled.run(cache, {}, (err, results) => {
expect(results.length).to.equal(1);
expect(results[0].status).to.equal(2);
- expect(results[0].message).to.include('Registration with Azure Active Directory is disabled for the Web App');
+ expect(results[0].message).to.include('Registration with Azure Entra ID is disabled for the Web App');
expect(results[0].region).to.equal('eastus');
done();
});
diff --git a/plugins/azure/automationAccounts/automationAcctExpiredWebhooks.spec.js b/plugins/azure/automationAccounts/automationAcctExpiredWebhooks.spec.js
index c34338c55..bd714f340 100644
--- a/plugins/azure/automationAccounts/automationAcctExpiredWebhooks.spec.js
+++ b/plugins/azure/automationAccounts/automationAcctExpiredWebhooks.spec.js
@@ -1,5 +1,7 @@
var expect = require('chai').expect;
var automationAcctExpiredWebhooks = require('./automationAcctExpiredWebhooks');
+var nextMonthExpiry = new Date();
+nextMonthExpiry.setMonth(nextMonthExpiry.getMonth() + 1);
const automationAccounts = [
{
@@ -31,8 +33,7 @@ const webhooks = [
"id": "/subscriptions/12345/resourceGroups/test-rg/providers/Microsoft.Automation/automationAccounts/test-automationacct/webhooks/test1",
"name": "test1",
"creationTime": "2024-01-22T13:33:52.1066667+00:00",
- // Set expiry to 1 year from now to ensure it's in the future
- "expiryTime": new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString(),
+ "expiryTime": nextMonthExpiry,
},
{
"id": "/subscriptions/12345/resourceGroups/test-rg/providers/Microsoft.Automation/automationAccounts/test-automationacct/webhooks/test2",
@@ -181,6 +182,6 @@ describe('automationAcctExpiredWebhooks', function () {
expect(results[0].region).to.equal('eastus');
done();
});
- });
+ });
});
});
\ No newline at end of file
diff --git a/plugins/azure/automationAccounts/automationAcctManagedIdentity.js b/plugins/azure/automationAccounts/automationAcctManagedIdentity.js
index bf3fbe446..8f620522f 100644
--- a/plugins/azure/automationAccounts/automationAcctManagedIdentity.js
+++ b/plugins/azure/automationAccounts/automationAcctManagedIdentity.js
@@ -7,7 +7,7 @@ module.exports = {
domain: 'Management and Governance',
severity: 'Medium',
description: 'Ensure that Azure Automation accounts have managed identity enabled.',
- more_info: 'Enabling managed identities eliminate the need for developers having to manage credentials by providing an identity for the Azure resource in Azure AD and using it to obtain Azure Active Directory (Azure AD) tokens.',
+ more_info: 'Enabling managed identities eliminate the need for developers having to manage credentials by providing an identity for the Azure resource in Azure AD and using it to obtain Azure Entra ID tokens.',
recommended_action: 'Modify automation account and enable managed identity.',
link: 'https://learn.microsoft.com/en-us/azure/automation/quickstarts/enable-managed-identity',
apis: ['automationAccounts:list'],
diff --git a/plugins/azure/automationAccounts/automationAcctPublicAccess.js b/plugins/azure/automationAccounts/automationAcctPublicAccess.js
index 311ac7155..47e9d730f 100644
--- a/plugins/azure/automationAccounts/automationAcctPublicAccess.js
+++ b/plugins/azure/automationAccounts/automationAcctPublicAccess.js
@@ -10,7 +10,7 @@ module.exports = {
more_info: 'Disabling public network access ensures that network traffic between the machines on the VNet and the Automation account traverses over the a private link, eliminating exposure from the public internet.',
recommended_action: 'Modify automation account and disable public access.',
link: 'https://learn.microsoft.com/en-us/azure/automation/how-to/private-link-security',
- apis: ['automationAccounts:list'],
+ apis: ['automationAccounts:list', 'automationAccounts:get'],
realtime_triggers: ['microsoftautomation:automationaccounts:write','microsoftautomation:automationaccounts:delete'],
run: function(cache, settings, callback) {
@@ -38,8 +38,20 @@ module.exports = {
for (var account of automationAccounts.data) {
if (!account.id) continue;
- if (!account.publicNetworkAccess) {
- helpers.addResult(results, 0, 'Automation account has public network access disabled', location, account.id);
+ var describeAcct = helpers.addSource(cache, source,
+ ['automationAccounts', 'get', location, account.id]);
+
+ if (!describeAcct || describeAcct.err || !describeAcct.data ) {
+ helpers.addResult(results, 3, 'Unable to query for Automation account: ' + helpers.addError(describeAcct), location);
+ continue;
+ }
+
+ if (Object.prototype.hasOwnProperty.call(describeAcct.data, 'publicNetworkAccess')) {
+ if (describeAcct.data.publicNetworkAccess) {
+ helpers.addResult(results, 2, 'Automation account does not have public network access disabled', location, account.id);
+ } else {
+ helpers.addResult(results, 0, 'Automation account has public network access disabled', location, account.id);
+ }
} else {
helpers.addResult(results, 2, 'Automation account does not have public network access disabled', location, account.id);
}
diff --git a/plugins/azure/automationAccounts/automationAcctPublicAccess.spec.js b/plugins/azure/automationAccounts/automationAcctPublicAccess.spec.js
index 80472b795..5d4ce4e11 100644
--- a/plugins/azure/automationAccounts/automationAcctPublicAccess.spec.js
+++ b/plugins/azure/automationAccounts/automationAcctPublicAccess.spec.js
@@ -2,6 +2,12 @@ var expect = require('chai').expect;
var automationAcctPublicAccess = require('./automationAcctPublicAccess.js');
const automationAccounts = [
+ {
+ "id": "/subscriptions/12345/resourceGroups/DefaultResourceGroup-EUS/providers/Microsoft.Automation/automationAccounts/Automate-12345-EUS2"
+ }
+];
+
+const account = [
{
"id": "/subscriptions/12345/resourceGroups/DefaultResourceGroup-EUS/providers/Microsoft.Automation/automationAccounts/Automate-12345-EUS2",
"location": "EastUS2",
@@ -22,7 +28,6 @@ const automationAccounts = [
}
},
"publicNetworkAccess": false,
-
},
{
"id": "/subscriptions/12345/resourceGroups/DefaultResourceGroup-CUS/providers/Microsoft.Automation/automationAccounts/Automate-12345-CUS",
@@ -34,17 +39,29 @@ const automationAccounts = [
}
];
-const createCache = (automationAccounts,err) => {
+const createCache = (automationAccounts, acct) => {
+ let automationacct = {};
+ let getacct = {};
+
+ if (automationAccounts) {
+ automationacct['data'] = automationAccounts;
+ if (automationAccounts && automationAccounts.length) {
+ getacct[automationAccounts[0].id] = {
+ 'data': acct
+ };
+ }
+ }
+
return {
automationAccounts: {
list: {
- 'eastus': {
- data: automationAccounts,
- err: err
- }
+ 'eastus': automationacct
+ },
+ get: {
+ 'eastus': getacct
}
}
- }
+ };
};
describe('automationAcctPublicAccess', function () {
@@ -62,7 +79,7 @@ describe('automationAcctPublicAccess', function () {
});
it('should give unknown result if Unable to query automation accounts:', function (done) {
- const cache = createCache(null, 'Error');
+ const cache = createCache();
automationAcctPublicAccess.run(cache, {}, (err, results) => {
expect(results.length).to.equal(1);
expect(results[0].status).to.equal(3);
@@ -73,7 +90,7 @@ describe('automationAcctPublicAccess', function () {
});
it('should give passing result if automation account has public network access disabled', function (done) {
- const cache = createCache([automationAccounts[0]]);
+ const cache = createCache(automationAccounts, account[0]);
automationAcctPublicAccess.run(cache, {}, (err, results) => {
expect(results.length).to.equal(1);
expect(results[0].status).to.equal(0);
@@ -84,7 +101,7 @@ describe('automationAcctPublicAccess', function () {
});
it('should give failing result if automation account does not have public network access disabled', function (done) {
- const cache = createCache([automationAccounts[1]]);
+ const cache = createCache(automationAccounts, account[1]);
automationAcctPublicAccess.run(cache, {}, (err, results) => {
expect(results.length).to.equal(1);
expect(results[0].status).to.equal(2);
diff --git a/plugins/azure/batchAccounts/batchAccountsAADEnabled.js b/plugins/azure/batchAccounts/batchAccountsAADEnabled.js
index cf9cabc3a..fc2839ecd 100644
--- a/plugins/azure/batchAccounts/batchAccountsAADEnabled.js
+++ b/plugins/azure/batchAccounts/batchAccountsAADEnabled.js
@@ -2,13 +2,13 @@ var async = require('async');
var helpers = require('../../../helpers/azure/');
module.exports = {
- title: 'Batch Account AAD Auth Enabled',
+ title: 'Batch Account Entra ID Auth Enabled',
category: 'Batch',
domain: 'Compute',
severity: 'Medium',
- description: 'Ensures that Batch account has Azure Active Directory (AAD) authentication mode enabled.',
- more_info: 'Enabling Azure Active Directory (AAD) authentication for Batch account ensures enhanced security by restricting the service API authentication to Microsoft Entra ID that prevents access through less secure shared key methods, thereby safeguarding batch resources from unauthorized access.',
- recommended_action: 'Enable Active Directory authentication mode for all Batch accounts.',
+ description: 'Ensures that Batch account has Azure Entra ID authentication mode enabled.',
+ more_info: 'Enabling Azure Entra ID authentication for Batch account ensures enhanced security by restricting the service API authentication to Microsoft Entra ID that prevents access through less secure shared key methods, thereby safeguarding batch resources from unauthorized access.',
+ recommended_action: 'Enable Entra ID authentication mode for all Batch accounts.',
link: 'https://learn.microsoft.com/en-us/azure/batch/batch-aad-auth',
apis: ['batchAccounts:list'],
realtime_triggers: ['microsoftbatch:batchaccounts:write', 'microsoftbatch:batchaccounts:delete'],
@@ -42,9 +42,9 @@ module.exports = {
batchAccount.allowedAuthenticationModes.some(mode => mode.toUpperCase() === 'AAD') : false;
if (found) {
- helpers.addResult(results, 0, 'Batch account has Active Directory authentication enabled', location, batchAccount.id);
+ helpers.addResult(results, 0, 'Batch account has Entra ID authentication enabled', location, batchAccount.id);
} else {
- helpers.addResult(results, 2, 'Batch account does not have Active Directory authentication enabled', location, batchAccount.id);
+ helpers.addResult(results, 2, 'Batch account does not have Entra ID authentication enabled', location, batchAccount.id);
}
}
diff --git a/plugins/azure/batchAccounts/batchAccountsAADEnabled.spec.js b/plugins/azure/batchAccounts/batchAccountsAADEnabled.spec.js
index cd106bc09..508c96d69 100644
--- a/plugins/azure/batchAccounts/batchAccountsAADEnabled.spec.js
+++ b/plugins/azure/batchAccounts/batchAccountsAADEnabled.spec.js
@@ -69,23 +69,23 @@ describe('batchAccountsAADEnabled', function () {
});
});
- it('should give passing result if Batch account is configured with AAD Authentication', function (done) {
+ it('should give passing result if Batch account is configured with Entra ID Authentication', function (done) {
const cache = createCache([batchAccounts[0]]);
batchAccountsAADEnabled.run(cache, {}, (err, results) => {
expect(results.length).to.equal(1);
expect(results[0].status).to.equal(0);
- expect(results[0].message).to.include('Batch account has Active Directory authentication enabled');
+ expect(results[0].message).to.include('Batch account has Entra ID authentication enabled');
expect(results[0].region).to.equal('eastus');
done();
});
});
- it('should give failing result if Batch account is not configured with AAD Authentication', function (done) {
+ it('should give failing result if Batch account is not configured with Entra ID Authentication', function (done) {
const cache = createCache([batchAccounts[1]]);
batchAccountsAADEnabled.run(cache, {}, (err, results) => {
expect(results.length).to.equal(1);
expect(results[0].status).to.equal(2);
- expect(results[0].message).to.include('Batch account does not have Active Directory authentication enabled');
+ expect(results[0].message).to.include('Batch account does not have Entra ID authentication enabled');
expect(results[0].region).to.equal('eastus');
done();
});
diff --git a/plugins/azure/batchAccounts/batchAccountsManagedIdentity.js b/plugins/azure/batchAccounts/batchAccountsManagedIdentity.js
index 767c81eb9..4b0821e75 100644
--- a/plugins/azure/batchAccounts/batchAccountsManagedIdentity.js
+++ b/plugins/azure/batchAccounts/batchAccountsManagedIdentity.js
@@ -7,7 +7,7 @@ module.exports = {
domain: 'Compute',
severity: 'Medium',
description: 'Ensures that Batch accounts have managed identity enabled.',
- more_info: 'Enabling managed identities eliminate the need for developers having to manage credentials by providing an identity for the Azure resource in Azure AD and using it to obtain Azure Active Directory (Azure AD) tokens.',
+ more_info: 'Enabling managed identities eliminate the need for developers having to manage credentials by providing an identity for the Azure resource in Azure and using it to obtain Azure Entra Id tokens.',
recommended_action: 'Modify Batch Account and enable managed identity.',
link: 'https://learn.microsoft.com/en-us/troubleshoot/azure/hpc/batch/use-managed-identities-azure-batch-account-pool',
apis: ['batchAccounts:list'],
diff --git a/plugins/azure/containerapps/containerAppManagedIdentity.js b/plugins/azure/containerapps/containerAppManagedIdentity.js
index 6fb376db7..978ecbd01 100644
--- a/plugins/azure/containerapps/containerAppManagedIdentity.js
+++ b/plugins/azure/containerapps/containerAppManagedIdentity.js
@@ -7,7 +7,7 @@ module.exports = {
domain: 'Containers',
severity: 'Medium',
description: 'Ensure that Azure Container Apps has managed identity enabled.',
- more_info: 'Enabling managed identities eliminate the need for developers having to manage credentials by providing an identity for the Azure resource in Azure AD and using it to obtain Azure Active Directory (Azure AD) tokens.',
+ more_info: 'Enabling managed identities eliminate the need for developers having to manage credentials by providing an identity for the Azure resource in Azure AD and using it to obtain Azure Entra ID tokens.',
recommended_action: 'Modify Container apps and add managed identity.',
link: 'https://learn.microsoft.com/en-us/azure/container-apps/managed-identity',
apis: ['containerApps:list'],
diff --git a/plugins/azure/containerregistry/acrManagedIdentityEnabled.js b/plugins/azure/containerregistry/acrManagedIdentityEnabled.js
index a5396cec2..32c72d930 100644
--- a/plugins/azure/containerregistry/acrManagedIdentityEnabled.js
+++ b/plugins/azure/containerregistry/acrManagedIdentityEnabled.js
@@ -7,7 +7,7 @@ module.exports = {
domain: 'Containers',
severity: 'Medium',
description: 'Ensure that Azure container registries have managed identity enabled.',
- more_info: 'Enabling managed identities eliminate the need for developers having to manage credentials by providing an identity for the Azure resource in Azure AD and using it to obtain Azure Active Directory (Azure AD) tokens.',
+ more_info: 'Enabling managed identities eliminate the need for developers having to manage credentials by providing an identity for the Azure resource in Azure AD and using it to obtain Azure Entra ID tokens.',
recommended_action: 'Modify container registry and enable managed identity.',
link: 'https://learn.microsoft.com/en-us/azure/container-registry/container-registry-authentication-managed-identity?tabs=azure-cli',
apis: ['registries:list'],
diff --git a/plugins/azure/cosmosdb/cosmosdbLocalAuth.js b/plugins/azure/cosmosdb/cosmosdbLocalAuth.js
index 8efe7ca7b..7090696b5 100644
--- a/plugins/azure/cosmosdb/cosmosdbLocalAuth.js
+++ b/plugins/azure/cosmosdb/cosmosdbLocalAuth.js
@@ -7,7 +7,7 @@ module.exports = {
domain: 'Databases',
severity: 'Low',
description: 'Ensures that local authentication is disabled for Cosmos DB accounts.',
- more_info: 'For enhanced security, centralized identity management and seamless integration with Azure\'s authentication and authorization services, it is recommended to rely on Azure Active Directory (Azure AD) and disable local authentication for Azure Cosmos DB accounts.',
+ more_info: 'For enhanced security, centralized identity management and seamless integration with Azure\'s authentication and authorization services, it is recommended to rely on Azure Entra ID and disable local authentication for Azure Cosmos DB accounts.',
recommended_action: 'Ensure that Cosmos DB accounts have local authentication disabled.',
link: 'https://learn.microsoft.com/en-us/azure/cosmos-db/how-to-setup-rbac#disable-local-auth',
apis: ['databaseAccounts:list'],
diff --git a/plugins/azure/cosmosdb/cosmosdbManagedIdentity.js b/plugins/azure/cosmosdb/cosmosdbManagedIdentity.js
index d5c9b2519..a23d70cb9 100644
--- a/plugins/azure/cosmosdb/cosmosdbManagedIdentity.js
+++ b/plugins/azure/cosmosdb/cosmosdbManagedIdentity.js
@@ -7,7 +7,7 @@ module.exports = {
domain: 'Databases',
severity: 'Medium',
description: 'Ensures that Azure Cosmos DB accounts have managed identity enabled.',
- more_info: 'Enabling managed identities eliminate the need for developers having to manage credentials by providing an identity for the Azure resource in Azure AD and using it to obtain Azure Active Directory (Azure AD) tokens.',
+ more_info: 'Enabling managed identities eliminate the need for developers having to manage credentials by providing an identity for the Azure resource in Azure AD and using it to obtain Azure Entra ID tokens.',
link: 'https://learn.microsoft.com/en-us/azure/cosmos-db/managed-identity-based-authentication',
recommended_action: 'Enable system or user-assigned identities for all Azure Cosmos DB accounts.',
apis: ['databaseAccounts:list'],
diff --git a/plugins/azure/activedirectory/appOrgnaizationalDirectoryAccess.js b/plugins/azure/entraid/appOrgnaizationalDirectoryAccess.js
similarity index 58%
rename from plugins/azure/activedirectory/appOrgnaizationalDirectoryAccess.js
rename to plugins/azure/entraid/appOrgnaizationalDirectoryAccess.js
index c0c9f6afa..614cea0ce 100644
--- a/plugins/azure/activedirectory/appOrgnaizationalDirectoryAccess.js
+++ b/plugins/azure/entraid/appOrgnaizationalDirectoryAccess.js
@@ -2,13 +2,13 @@ const async = require('async');
const helpers = require('../../../helpers/azure');
module.exports = {
- title: 'Azure AD App Organizational Directory Access',
- category: 'Active Directory',
+ title: 'Azure Entra ID App Organizational Directory Access',
+ category: 'Entra ID',
domain: 'Identity and Access Management',
severity: 'Medium',
- description: 'Ensures that Azure Active Directory applications are accessible to accounts in organisational directory only.',
- more_info: 'AAD provides different types of account access. By using single-tenant authentication, the impact gets limited to the application’s tenant i.e. all users from the same tenant could connect to the application and save app from unauthorised access.',
- link: 'https://learn.microsoft.com/en-us/azure/active-directory/develop/single-and-multi-tenant-apps',
+ description: 'Ensures that Azure Entra Id applications are accessible to accounts in organisational directory only.',
+ more_info: 'Entra ID provides different types of account access. By using single-tenant authentication, the impact gets limited to the application’s tenant i.e. all users from the same tenant could connect to the application and save app from unauthorised access.',
+ link: 'https://learn.microsoft.com/en-us/entra/identity-platform/single-and-multi-tenant-apps',
recommended_action: 'Modify the Azure app authentication setting and provide access to accounts in organisational directory only',
apis: ['applications:list'],
@@ -23,20 +23,20 @@ module.exports = {
if (!applications) return rcb();
if (applications.err || !applications.data) {
- helpers.addResult(results, 3, 'Unable to query for AAD applications: ' + helpers.addError(applications), location);
+ helpers.addResult(results, 3, 'Unable to query for Entra ID applications: ' + helpers.addError(applications), location);
return rcb();
}
if (!applications.data.length) {
- helpers.addResult(results, 0, 'No existing AAD applications found', location);
+ helpers.addResult(results, 0, 'No existing Entra ID applications found', location);
return rcb();
}
for (let app of applications.data) {
if (!app.appId) continue;
if (app.signInAudience && app.signInAudience === 'AzureADMultipleOrgs' || app.signInAudience === 'AzureADandPersonalMicrosoftAccount'){
- helpers.addResult(results, 2, 'AAD application has multi-tenant access enabled', location, app.appId);
+ helpers.addResult(results, 2, 'Entra ID application has multi-tenant access enabled', location, app.appId);
} else {
- helpers.addResult(results, 0, 'AAD application has single-tenant access enabled', location, app.appId);
+ helpers.addResult(results, 0, 'Entra ID application has single-tenant access enabled', location, app.appId);
}
}
rcb();
diff --git a/plugins/azure/activedirectory/appOrgnaizationalDirectoryAccess.spec.js b/plugins/azure/entraid/appOrgnaizationalDirectoryAccess.spec.js
similarity index 92%
rename from plugins/azure/activedirectory/appOrgnaizationalDirectoryAccess.spec.js
rename to plugins/azure/entraid/appOrgnaizationalDirectoryAccess.spec.js
index bc46c8b10..d00f401b2 100644
--- a/plugins/azure/activedirectory/appOrgnaizationalDirectoryAccess.spec.js
+++ b/plugins/azure/entraid/appOrgnaizationalDirectoryAccess.spec.js
@@ -48,7 +48,7 @@ describe('appOrgnaizationalDirectoryAccess', function() {
const callback = (err, results) => {
expect(results.length).to.equal(1);
expect(results[0].status).to.equal(0);
- expect(results[0].message).to.include('No existing AAD applications found');
+ expect(results[0].message).to.include('No existing Entra ID applications found');
expect(results[0].region).to.equal('global');
done()
};
@@ -64,7 +64,7 @@ describe('appOrgnaizationalDirectoryAccess', function() {
const callback = (err, results) => {
expect(results.length).to.equal(1);
expect(results[0].status).to.equal(3);
- expect(results[0].message).to.include('Unable to query for AAD applications:');
+ expect(results[0].message).to.include('Unable to query for Entra ID applications:');
expect(results[0].region).to.equal('global');
done()
};
@@ -81,7 +81,7 @@ describe('appOrgnaizationalDirectoryAccess', function() {
const callback = (err, results) => {
expect(results.length).to.equal(1);
expect(results[0].status).to.equal(2);
- expect(results[0].message).to.include('AAD application has multi-tenant access enabled');
+ expect(results[0].message).to.include('Entra ID application has multi-tenant access enabled');
expect(results[0].region).to.equal('global');
done()
};
@@ -95,7 +95,7 @@ describe('appOrgnaizationalDirectoryAccess', function() {
const callback = (err, results) => {
expect(results.length).to.equal(1);
expect(results[0].status).to.equal(0);
- expect(results[0].message).to.include('AAD application has single-tenant access enabled');
+ expect(results[0].message).to.include('Entra ID application has single-tenant access enabled');
expect(results[0].region).to.equal('global');
done()
};
diff --git a/plugins/azure/activedirectory/ensureNoGuestUser.js b/plugins/azure/entraid/ensureNoGuestUser.js
similarity index 91%
rename from plugins/azure/activedirectory/ensureNoGuestUser.js
rename to plugins/azure/entraid/ensureNoGuestUser.js
index 13afc564e..5116192d1 100644
--- a/plugins/azure/activedirectory/ensureNoGuestUser.js
+++ b/plugins/azure/entraid/ensureNoGuestUser.js
@@ -3,13 +3,13 @@ const helpers = require('../../../helpers/azure');
module.exports = {
title: 'Ensure No Guest User',
- category: 'Active Directory',
+ category: 'Entra ID',
domain: 'Identity and Access Management',
severity: 'Medium',
description: 'Ensures that there are no guest users in the subscription',
more_info: 'Guest users are usually users that are invited from outside the company structure, these users are not part of the onboarding/offboarding process and could be overlooked, causing security vulnerabilities.',
- link: 'https://learn.microsoft.com/en-us/azure/active-directory/b2b/add-users-administrator',
- recommended_action: 'Remove all guest users unless they are required to be members of the Active Directory account.',
+ link: 'https://learn.microsoft.com/en-us/entra/external-id/add-users-administrator',
+ recommended_action: 'Remove all guest users unless they are required to be members of the Entra ID tenant.',
apis: ['users:list'],
run: function(cache, settings, callback) {
diff --git a/plugins/azure/activedirectory/ensureNoGuestUser.spec.js b/plugins/azure/entraid/ensureNoGuestUser.spec.js
similarity index 100%
rename from plugins/azure/activedirectory/ensureNoGuestUser.spec.js
rename to plugins/azure/entraid/ensureNoGuestUser.spec.js
diff --git a/plugins/azure/activedirectory/minPasswordLength.js b/plugins/azure/entraid/minPasswordLength.js
similarity index 87%
rename from plugins/azure/activedirectory/minPasswordLength.js
rename to plugins/azure/entraid/minPasswordLength.js
index db9f98d39..b02b799ae 100644
--- a/plugins/azure/activedirectory/minPasswordLength.js
+++ b/plugins/azure/entraid/minPasswordLength.js
@@ -3,12 +3,12 @@ const helpers = require('../../../helpers/azure');
module.exports = {
title: 'Minimum Password Length',
- category: 'Active Directory',
+ category: 'Entra ID',
domain: 'Identity and Access Management',
severity: 'Low',
description: 'Ensures that all Azure passwords require a minimum length',
more_info: 'Azure handles most password policy settings, including the minimum password length, defaulted to 8 characters.',
- link: 'https://learn.microsoft.com/en-us/azure/active-directory/authentication/concept-sspr-policy#password-policies-that-only-apply-to-cloud-user-accounts',
+ link: 'https://learn.microsoft.com/en-us/entra/identity/authentication/concept-sspr-policy#password-policies-that-only-apply-to-cloud-user-accounts',
recommended_action: 'No action necessary. Azure handles password requirement settings.',
apis: ['resources:list'],
diff --git a/plugins/azure/activedirectory/noCustomOwnerRoles.js b/plugins/azure/entraid/noCustomOwnerRoles.js
similarity index 99%
rename from plugins/azure/activedirectory/noCustomOwnerRoles.js
rename to plugins/azure/entraid/noCustomOwnerRoles.js
index 5c7962b7e..a6c6a0cf6 100644
--- a/plugins/azure/activedirectory/noCustomOwnerRoles.js
+++ b/plugins/azure/entraid/noCustomOwnerRoles.js
@@ -3,7 +3,7 @@ const helpers = require('../../../helpers/azure');
module.exports = {
title: 'No Custom Owner Roles',
- category: 'Active Directory',
+ category: 'Entra ID',
domain: 'Identity and Access Management',
severity: 'Medium',
description: 'Ensures that no custom owner roles exist.',
diff --git a/plugins/azure/activedirectory/noCustomOwnerRoles.spec.js b/plugins/azure/entraid/noCustomOwnerRoles.spec.js
similarity index 100%
rename from plugins/azure/activedirectory/noCustomOwnerRoles.spec.js
rename to plugins/azure/entraid/noCustomOwnerRoles.spec.js
diff --git a/plugins/azure/activedirectory/passwordRequiresLowercase.js b/plugins/azure/entraid/passwordRequiresLowercase.js
similarity index 88%
rename from plugins/azure/activedirectory/passwordRequiresLowercase.js
rename to plugins/azure/entraid/passwordRequiresLowercase.js
index be1f298bf..bfa1183dd 100644
--- a/plugins/azure/activedirectory/passwordRequiresLowercase.js
+++ b/plugins/azure/entraid/passwordRequiresLowercase.js
@@ -3,12 +3,12 @@ const helpers = require('../../../helpers/azure');
module.exports = {
title: 'Password Requires Lowercase',
- category: 'Active Directory',
+ category: 'Entra ID',
domain: 'Identity and Access Management',
severity: 'Low',
description: 'Ensures that all Azure passwords require lowercase characters',
more_info: 'Azure handles most password policy settings, including which character types are required. Azure requires 3 out of 4 of the following character types: lowercase, uppercase, special characters, and numbers.',
- link: 'https://learn.microsoft.com/en-us/azure/active-directory/authentication/concept-sspr-policy#password-policies-that-only-apply-to-cloud-user-accounts',
+ link: 'https://learn.microsoft.com/en-us/entra/identity/authentication/concept-sspr-policy#password-policies-that-only-apply-to-cloud-user-accounts',
recommended_action: 'No action necessary. Azure handles password requirement settings.',
apis: ['resources:list'],
diff --git a/plugins/azure/activedirectory/passwordRequiresNumbers.js b/plugins/azure/entraid/passwordRequiresNumbers.js
similarity index 88%
rename from plugins/azure/activedirectory/passwordRequiresNumbers.js
rename to plugins/azure/entraid/passwordRequiresNumbers.js
index 3606f5d44..d15367385 100644
--- a/plugins/azure/activedirectory/passwordRequiresNumbers.js
+++ b/plugins/azure/entraid/passwordRequiresNumbers.js
@@ -3,12 +3,12 @@ const helpers = require('../../../helpers/azure');
module.exports = {
title: 'Password Requires Numbers',
- category: 'Active Directory',
+ category: 'Entra ID',
domain: 'Identity and Access Management',
severity: 'Low',
description: 'Ensures that all Azure passwords require numbers',
more_info: 'Azure handles most password policy settings, including which character types are required. Azure requires 3 out of 4 of the following character types: lowercase, uppercase, special characters, and numbers.',
- link: 'https://learn.microsoft.com/en-us/azure/active-directory/authentication/concept-sspr-policy#password-policies-that-only-apply-to-cloud-user-accounts',
+ link: 'https://learn.microsoft.com/en-us/entra/identity/authentication/concept-sspr-policy#password-policies-that-only-apply-to-cloud-user-accounts',
recommended_action: 'No action necessary. Azure handles password requirement settings.',
apis: ['resources:list'],
diff --git a/plugins/azure/activedirectory/passwordRequiresSymbols.js b/plugins/azure/entraid/passwordRequiresSymbols.js
similarity index 88%
rename from plugins/azure/activedirectory/passwordRequiresSymbols.js
rename to plugins/azure/entraid/passwordRequiresSymbols.js
index 91ea29453..a35a8e33c 100644
--- a/plugins/azure/activedirectory/passwordRequiresSymbols.js
+++ b/plugins/azure/entraid/passwordRequiresSymbols.js
@@ -3,12 +3,12 @@ const helpers = require('../../../helpers/azure');
module.exports = {
title: 'Password Requires Symbols',
- category: 'Active Directory',
+ category: 'Entra ID',
domain: 'Identity and Access Management',
severity: 'Low',
description: 'Ensures that all Azure passwords require symbol characters',
more_info: 'Azure handles most password policy settings, including which character types are required. Azure requires 3 out of 4 of the following character types: lowercase, uppercase, special characters, and numbers.',
- link: 'https://learn.microsoft.com/en-us/azure/active-directory/authentication/concept-sspr-policy#password-policies-that-only-apply-to-cloud-user-accounts',
+ link: 'https://learn.microsoft.com/en-us/entra/identity/authentication/concept-sspr-policy#password-policies-that-only-apply-to-cloud-user-accounts',
recommended_action: 'No action necessary. Azure handles password requirement settings.',
apis: ['resources:list'],
diff --git a/plugins/azure/activedirectory/passwordRequiresUppercase.js b/plugins/azure/entraid/passwordRequiresUppercase.js
similarity index 88%
rename from plugins/azure/activedirectory/passwordRequiresUppercase.js
rename to plugins/azure/entraid/passwordRequiresUppercase.js
index 5dd2b90e4..c5c4f71fd 100644
--- a/plugins/azure/activedirectory/passwordRequiresUppercase.js
+++ b/plugins/azure/entraid/passwordRequiresUppercase.js
@@ -3,12 +3,12 @@ const helpers = require('../../../helpers/azure');
module.exports = {
title: 'Password Requires Uppercase',
- category: 'Active Directory',
+ category: 'Entra ID',
domain: 'Identity and Access Management',
severity: 'Low',
description: 'Ensures that all Azure passwords require uppercase characters',
more_info: 'Azure handles most password policy settings, including which character types are required. Azure requires 3 out of 4 of the following character types: lowercase, uppercase, special characters, and numbers.',
- link: 'https://learn.microsoft.com/en-us/azure/active-directory/authentication/concept-sspr-policy#password-policies-that-only-apply-to-cloud-user-accounts',
+ link: 'https://learn.microsoft.com/en-us/entra/identity/authentication/concept-sspr-policy#password-policies-that-only-apply-to-cloud-user-accounts',
recommended_action: 'No action necessary. Azure handles password requirement settings.',
apis: ['resources:list'],
diff --git a/plugins/azure/eventGrid/domainLocalAuthDisabled.js b/plugins/azure/eventGrid/domainLocalAuthDisabled.js
index 739e1053f..e065a902d 100644
--- a/plugins/azure/eventGrid/domainLocalAuthDisabled.js
+++ b/plugins/azure/eventGrid/domainLocalAuthDisabled.js
@@ -7,7 +7,7 @@ module.exports = {
domain: 'Management and Governance',
severity: 'Low',
description: 'Ensures that local authentication is disabled for Event Grid domains.',
- more_info: 'For enhanced security, centralized identity management, and seamless integration with Azure\'s authentication and authorization services, it is recommended to rely on Azure Active Directory (Azure AD) and disable local authentication (shared access policies) for Azure Event Grid.',
+ more_info: 'For enhanced security, centralized identity management, and seamless integration with Azure\'s authentication and authorization services, it is recommended to rely on Azure Entra ID and disable local authentication (shared access policies) for Azure Event Grid.',
recommended_action: 'Ensure that Event Grid domains have local authentication disabled.',
link: 'https://learn.microsoft.com/en-us/azure/event-grid/authenticate-with-microsoft-entra-id#disable-key-and-shared-access-signature-authentication',
apis: ['eventGrid:listDomains'],
diff --git a/plugins/azure/eventGrid/domainManagedIdentity.js b/plugins/azure/eventGrid/domainManagedIdentity.js
index b50ca3910..29bbb2f34 100644
--- a/plugins/azure/eventGrid/domainManagedIdentity.js
+++ b/plugins/azure/eventGrid/domainManagedIdentity.js
@@ -7,7 +7,7 @@ module.exports = {
domain: 'Management and Governance',
severity: 'Medium',
description: 'Ensure that Event Grid domains have managed identity enabled.',
- more_info: 'Enabling managed identities eliminate the need for developers having to manage credentials by providing an identity for the Azure resource in Azure AD and using it to obtain Azure Active Directory (Azure AD) tokens.',
+ more_info: 'Enabling managed identities eliminate the need for developers having to manage credentials by providing an identity for the Azure resource in Azure AD and using it to obtain Azure Entra ID tokens.',
recommended_action: 'Modify Event Grid domains and enable managed identity.',
link: 'https://learn.microsoft.com/en-us/azure/event-grid/managed-service-identity',
apis: ['eventGrid:listDomains'],
diff --git a/plugins/azure/eventhub/eventHubLocalAuthDisabled.js b/plugins/azure/eventhub/eventHubLocalAuthDisabled.js
index b3c01abd0..cc3d8586d 100644
--- a/plugins/azure/eventhub/eventHubLocalAuthDisabled.js
+++ b/plugins/azure/eventhub/eventHubLocalAuthDisabled.js
@@ -7,7 +7,7 @@ module.exports = {
domain: 'Content Delivery',
severity: 'Low',
description: 'Ensures local authentication is disabled for Event Hub namespace.',
- more_info: 'For enhanced security, centralized identity management, and seamless integration with Azure\'s authentication and authorization services, it is recommended to rely on Azure Active Directory (Azure AD) and disable local authentication in Azure Event Hubs namespaces.',
+ more_info: 'For enhanced security, centralized identity management, and seamless integration with Azure\'s authentication and authorization services, it is recommended to rely on Azure Entra ID and disable local authentication in Azure Event Hubs namespaces.',
recommended_action: 'Ensure that Azure Event Hubs namespaces have local authentication disabled.',
link: 'https://learn.microsoft.com/en-us/azure/event-hubs/authenticate-shared-access-signature#disabling-localsas-key-authentication',
apis: ['eventHub:listEventHub'],
diff --git a/plugins/azure/eventhub/eventHubManagedIdentity.js b/plugins/azure/eventhub/eventHubManagedIdentity.js
index 0e6fb6b42..cde653721 100644
--- a/plugins/azure/eventhub/eventHubManagedIdentity.js
+++ b/plugins/azure/eventhub/eventHubManagedIdentity.js
@@ -7,7 +7,7 @@ module.exports = {
domain: 'Content Delivery',
severity: 'Medium',
description: 'Ensures Microsoft Azure Event Hubs namespaces have managed identity enabled.',
- more_info: 'Enabling managed identities eliminate the need for developers having to manage credentials by providing an identity for the Azure resource in Azure AD and using it to obtain Azure Active Directory (Azure AD) tokens.',
+ more_info: 'Enabling managed identities eliminate the need for developers having to manage credentials by providing an identity for the Azure resource in Azure AD and using it to obtain Azure Entra ID tokens.',
recommended_action: 'Modify Event Hubs namespace and enable managed identity.',
link: 'https://learn.microsoft.com/en-us/azure/event-hubs/authenticate-managed-identity',
apis: ['eventHub:listEventHub'],
diff --git a/plugins/azure/eventhub/eventHubPublicAccess.js b/plugins/azure/eventhub/eventHubPublicAccess.js
index 7f8f56e3e..7b0717b8f 100644
--- a/plugins/azure/eventhub/eventHubPublicAccess.js
+++ b/plugins/azure/eventhub/eventHubPublicAccess.js
@@ -10,13 +10,23 @@ module.exports = {
more_info: 'Configuring Azure Event Hubs namespace with public access poses a security risk. To mitigate this risk, it is advisable to limit access by allowing connections only from specific IP addresses or private networks.',
recommended_action: 'Ensure that public network access is disabled for each Event Hubs namespace.',
link: 'https://learn.microsoft.com/en-us/azure/event-hubs/event-hubs-ip-filtering#configure-public-access-when-creating-a-namespace',
- apis: ['eventHub:listEventHub'],
+ apis: ['eventHub:listEventHub', 'eventHub:listNetworkRuleSet'],
realtime_triggers: ['microsofteventhub:namespaces:write', 'microsofteventhub:namespaces:delete', 'microsofteventhub:namespaces:networkrulesets:write'],
-
+ settings: {
+ check_selected_networks: {
+ name: 'Evaluate Selected Networks',
+ description: 'Checks if specific IP addresses or virtual networks are set to restrict Event Hub access.',
+ regex: '^(true|false)$',
+ default: false,
+ }
+ },
run: function(cache, settings, callback) {
var results = [];
var source = {};
var locations = helpers.locations(settings.govcloud);
+ let config = {
+ check_selected_networks: settings.check_selected_networks || this.settings.check_selected_networks.default
+ };
async.each(locations.eventHub, function(location, rcb) {
var eventHubs = helpers.addSource(cache, source,
@@ -35,16 +45,33 @@ module.exports = {
return rcb();
}
- for (let eventHub of eventHubs.data){
+ for (let eventHub of eventHubs.data) {
if (!eventHub.id) continue;
- if (eventHub.sku && eventHub.sku.tier && eventHub.sku.tier.toLowerCase() === 'basic') {
+ if (eventHub.sku && eventHub.sku.tier && eventHub.sku.tier.toLowerCase() === 'basic') {
helpers.addResult(results, 0,
'Event Hubs namespace tier is basic', location, eventHub.id);
} else {
if (eventHub.publicNetworkAccess && eventHub.publicNetworkAccess.toLowerCase() === 'enabled') {
- helpers.addResult(results, 2,
- 'Event Hubs namespace is publicly accessible',location, eventHub.id);
+ if (config.check_selected_networks) {
+ const listNetworkRuleSet = helpers.addSource(cache, source,
+ ['eventHub', 'listNetworkRuleSet', location, eventHub.id]);
+ if (!listNetworkRuleSet || listNetworkRuleSet.err || !listNetworkRuleSet.data) {
+ helpers.addResult(results, 3,
+ 'Unable to query Event Hubs network rule set: ' + helpers.addError(listNetworkRuleSet), location, eventHub.id);
+ continue;
+ }
+ if ((listNetworkRuleSet.data.ipRules && listNetworkRuleSet.data.ipRules.length > 0) || (listNetworkRuleSet.data.virtualNetworkRules && listNetworkRuleSet.data.virtualNetworkRules.length > 0)) {
+ helpers.addResult(results, 0,
+ 'Event Hubs namespace is not publicly accessible', location, eventHub.id);
+ } else {
+ helpers.addResult(results, 2,
+ 'Event Hubs namespace is publicly accessible', location, eventHub.id);
+ }
+ } else {
+ helpers.addResult(results, 2,
+ 'Event Hubs namespace is publicly accessible', location, eventHub.id);
+ }
} else {
helpers.addResult(results, 0,
'Event Hubs namespace is not publicly accessible', location, eventHub.id);
diff --git a/plugins/azure/eventhub/eventHubPublicAccess.spec.js b/plugins/azure/eventhub/eventHubPublicAccess.spec.js
index d70e8c55b..0043cddfb 100644
--- a/plugins/azure/eventhub/eventHubPublicAccess.spec.js
+++ b/plugins/azure/eventhub/eventHubPublicAccess.spec.js
@@ -3,139 +3,350 @@ var eventHubPublicAccess = require('./eventHubPublicAccess');
const eventHubs = [
{
- "kind": "v12.0",
- "location": "eastus",
- "tags": {},
- "id": "/subscriptions/123/resourceGroups/test-rg/providers/Microsoft.EventHub/namespaces/testHub'",
- "name": "testHub",
- "type": 'Microsoft.EventHub/Namespaces',
- "location": 'East US',
- "tags": {},
- "minimumTlsVersion": '1.2',
- "publicNetworkAccess": 'Disabled',
- "disableLocalAuth": true,
- "zoneRedundant": true,
- "isAutoInflateEnabled": false,
- "maximumThroughputUnits": 0,
- "kafkaEnabled": false
+ kind: "v12.0",
+ location: "eastus",
+ tags: {},
+ id: "/subscriptions/123/resourceGroups/test-rg/providers/Microsoft.EventHub/namespaces/testHub",
+ name: "testHub",
+ type: "Microsoft.EventHub/Namespaces",
+ tags: {},
+ minimumTlsVersion: "1.2",
+ publicNetworkAccess: "Disabled",
+ disableLocalAuth: true,
+ zoneRedundant: true,
+ isAutoInflateEnabled: false,
+ maximumThroughputUnits: 0,
+ kafkaEnabled: false,
},
- {
- "kind": "v12.0",
- "location": "eastus",
- "tags": {},
- "id": "/subscriptions/123/resourceGroups/test-rg/providers/Microsoft.EventHub/namespaces/testHub'",
- "name": "testHub",
- "type": 'Microsoft.EventHub/Namespaces',
- "location": 'East US',
- "tags": {},
- "minimumTlsVersion": '1.1',
- "publicNetworkAccess": 'Enabled',
- "disableLocalAuth": true,
- "zoneRedundant": true,
- "isAutoInflateEnabled": false,
- "maximumThroughputUnits": 0,
- "kafkaEnabled": false,
+ {
+ kind: "v12.0",
+ location: "eastus",
+ tags: {},
+ id: "/subscriptions/123/resourceGroups/test-rg/providers/Microsoft.EventHub/namespaces/testHub",
+ name: "testHub",
+ type: "Microsoft.EventHub/Namespaces",
+ tags: {},
+ minimumTlsVersion: "1.1",
+ publicNetworkAccess: "Enabled",
+ disableLocalAuth: true,
+ zoneRedundant: true,
+ isAutoInflateEnabled: false,
+ maximumThroughputUnits: 0,
+ kafkaEnabled: false,
},
{
- "kind": "v12.0",
- "location": "eastus",
- "tags": {},
- "id": "/subscriptions/123/resourceGroups/test-rg/providers/Microsoft.EventHub/namespaces/testHub'",
- "name": "testHub2",
- "type": 'Microsoft.EventHub/Namespaces',
- "location": 'East US',
- "tags": {},
- "sku": {
- "name": "Basic",
- "tier": "Basic",
- "capacity": 1
+ kind: "v12.0",
+ location: "eastus",
+ tags: {},
+ id: "/subscriptions/123/resourceGroups/test-rg/providers/Microsoft.EventHub/namespaces/testHub",
+ name: "testHub2",
+ type: "Microsoft.EventHub/Namespaces",
+ tags: {},
+ sku: {
+ name: "Basic",
+ tier: "Basic",
+ capacity: 1,
},
- "minimumTlsVersion": '1.2',
- "publicNetworkAccess": 'Enabled',
- "disableLocalAuth": true,
- "isAutoInflateEnabled": false,
- "maximumThroughputUnits": 0,
- "kafkaEnabled": false
+ minimumTlsVersion: "1.2",
+ publicNetworkAccess: "Enabled",
+ disableLocalAuth: true,
+ isAutoInflateEnabled: false,
+ maximumThroughputUnits: 0,
+ kafkaEnabled: false,
+ },
+ {
+ kind: "v12.0",
+ location: "eastus",
+ tags: {},
+ id: "/subscriptions/123/resourceGroups/test-rg/providers/Microsoft.EventHub/namespaces/testHub",
+ name: "testHub",
+ type: "Microsoft.EventHub/Namespaces",
+ tags: {},
+ minimumTlsVersion: "1.2",
+ publicNetworkAccess: "Enabled",
+ disableLocalAuth: true,
+ zoneRedundant: true,
+ isAutoInflateEnabled: false,
+ maximumThroughputUnits: 0,
+ kafkaEnabled: false,
+ },
+ {
+ kind: "v12.0",
+ location: "eastus",
+ tags: {},
+ name: "testHub4",
+ type: "Microsoft.EventHub/Namespaces",
+ tags: {},
+ minimumTlsVersion: "1.2",
+ publicNetworkAccess: "Enabled",
+ disableLocalAuth: true,
+ zoneRedundant: true,
+ isAutoInflateEnabled: false,
+ maximumThroughputUnits: 0,
+ kafkaEnabled: false,
+ },
+ {
+ kind: "v12.0",
+ location: "eastus",
+ tags: {},
+ id: "/subscriptions/123/resourceGroups/test-rg/providers/Microsoft.EventHub/namespaces/testHub",
+ name: "testHub",
+ type: "Microsoft.EventHub/Namespaces",
+ tags: {},
+ minimumTlsVersion: "1.2",
+ publicNetworkAccess: "Enabled",
+ disableLocalAuth: true,
+ zoneRedundant: true,
+ isAutoInflateEnabled: false,
+ maximumThroughputUnits: 0,
+ kafkaEnabled: false,
+ },
+];
+
+const networkRuleSets = [
+ {
+ id: "/subscriptions/123/resourceGroups/test-rg/providers/Microsoft.EventHub/namespaces/testHub/networkrulesets/default",
+ name: "default",
+ type: "Microsoft.EventHub/Namespaces/NetworkRuleSets",
+ location: "eastus",
+ publicNetworkAccess: "Enabled",
+ defaultAction: "Allow",
+ virtualNetworkRules: [],
+ ipRules: [
+ {
+ ipMask: "192.168.1.0/24",
+ action: "Allow",
+ },
+ ],
+ },
+ {
+ id: "/subscriptions/123/resourceGroups/test-rg/providers/Microsoft.EventHub/namespaces/testHub/networkrulesets/default",
+ name: "default",
+ type: "Microsoft.EventHub/Namespaces/NetworkRuleSets",
+ location: "eastus",
+ publicNetworkAccess: "Enabled",
+ defaultAction: "Deny",
+ virtualNetworkRules: [],
+ ipRules: [
+ {
+ ipMask: "102.18.161.9",
+ action: "Allow"
+ }
+ ],
+ },
+ {
+ id: "/subscriptions/123/resourceGroups/test-rg/providers/Microsoft.EventHub/namespaces/testHub/networkrulesets/default",
+ name: "default",
+ type: "Microsoft.EventHub/Namespaces/NetworkRuleSets",
+ location: "eastus",
+ publicNetworkAccess: "Enabled",
+ defaultAction: "Allow",
+ virtualNetworkRules: [],
+ ipRules: [
+ ],
},
];
-const createCache = (hub) => {
+const createCache = (eventHub, networkRuleSet) => {
+ const id = eventHub && eventHub.length ? eventHub[0].id : null;
return {
eventHub: {
listEventHub: {
'eastus': {
- data: hub
+ data: eventHub
}
+ },
+ listNetworkRuleSet: {
+ 'eastus': {
+ [id]: {
+ data: networkRuleSet
+ }
+ }
+ }
+ }
+ };
+};
+
+const createErrorCache = () => {
+ return {
+ eventHub: {
+ listEventHub: {
+ eastus: {
+ err: "error",
+ },
+ },
+ },
+ };
+};
+
+const createNetworkRuleSetErrorCache = (hub) => {
+ let cache = {
+ eventHub: {
+ listEventHub: {
+ eastus: {
+ data: hub,
+ },
+ },
+ listNetworkRuleSet: {
+ eastus: {}
+ }
+ },
+ };
+
+ if (Array.isArray(hub) && hub.length > 0) {
+ for (let eventHub of hub) {
+ if (eventHub.id) {
+ cache.eventHub.listNetworkRuleSet.eastus[eventHub.id] = {
+ err: "Unable to query network rule sets",
+ };
}
}
}
+
+ return cache;
};
-describe('eventHubPublicAccess', function() {
- describe('run', function() {
- it('should give passing result if no event hub found', function(done) {
+describe("eventHubPublicAccess", function () {
+ describe("run", function () {
+ it("should give passing result if no event hub found", function (done) {
const callback = (err, results) => {
expect(results.length).to.equal(1);
expect(results[0].status).to.equal(0);
- expect(results[0].message).to.include('No Event Hubs namespaces found');
- expect(results[0].region).to.equal('eastus');
- done()
+ expect(results[0].message).to.include("No Event Hubs namespaces found");
+ expect(results[0].region).to.equal("eastus");
+ done();
};
const cache = createCache([]);
eventHubPublicAccess.run(cache, {}, callback);
});
- it('should give failing result if event hub is publicly accessible', function(done) {
+ it("should give failing result if event hub is publicly accessible", function (done) {
const callback = (err, results) => {
expect(results.length).to.equal(1);
expect(results[0].status).to.equal(2);
- expect(results[0].message).to.include('Event Hubs namespace is publicly accessible');
- expect(results[0].region).to.equal('eastus');
- done()
+ expect(results[0].message).to.include(
+ "Event Hubs namespace is publicly accessible"
+ );
+ expect(results[0].region).to.equal("eastus");
+ done();
};
const cache = createCache([eventHubs[1]]);
eventHubPublicAccess.run(cache, {}, callback);
});
- it('should give passing result if eventHub is not publicly accessible', function(done) {
+ it("should give passing result if eventHub is not publicly accessible", function (done) {
const callback = (err, results) => {
expect(results.length).to.equal(1);
expect(results[0].status).to.equal(0);
- expect(results[0].message).to.include('Event Hubs namespace is not publicly accessible');
- expect(results[0].region).to.equal('eastus');
- done()
+ expect(results[0].message).to.include(
+ "Event Hubs namespace is not publicly accessible"
+ );
+ expect(results[0].region).to.equal("eastus");
+ done();
};
const cache = createCache([eventHubs[0]]);
eventHubPublicAccess.run(cache, {}, callback);
});
- it('should give passing result if eventHub is of basic tier', function(done) {
+ it("should give passing result if eventHub is of basic tier", function (done) {
const callback = (err, results) => {
expect(results.length).to.equal(1);
expect(results[0].status).to.equal(0);
- expect(results[0].message).to.include('Event Hubs namespace tier is basic');
- expect(results[0].region).to.equal('eastus');
- done()
+ expect(results[0].message).to.include(
+ "Event Hubs namespace tier is basic"
+ );
+ expect(results[0].region).to.equal("eastus");
+ done();
};
const cache = createCache([eventHubs[2]]);
eventHubPublicAccess.run(cache, {}, callback);
});
- it('should give unknown result if unable to query for event hubs', function(done) {
+ it("should give unknown result if unable to query for event hubs", function (done) {
const callback = (err, results) => {
expect(results.length).to.equal(1);
expect(results[0].status).to.equal(3);
- expect(results[0].message).to.include('Unable to query for Event Hubs namespaces:');
- expect(results[0].region).to.equal('eastus');
- done()
+ expect(results[0].message).to.include(
+ "Unable to query for Event Hubs namespaces:"
+ );
+ expect(results[0].region).to.equal("eastus");
+ done();
};
- const cache = createCache(null);
+ const cache = createErrorCache();
eventHubPublicAccess.run(cache, {}, callback);
});
- })
-})
\ No newline at end of file
+
+ it("should give passing result when check_selected_networks is enabled and IP rules are configured", function (done) {
+ const callback = (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(0);
+ expect(results[0].message).to.include(
+ "Event Hubs namespace is not publicly accessible"
+ );
+ expect(results[0].region).to.equal("eastus");
+ done();
+ };
+
+ const cache = createCache([eventHubs[3]], networkRuleSets[1]);
+ eventHubPublicAccess.run(
+ cache,
+ { check_selected_networks: true },
+ callback
+ );
+ });
+
+ it("should give failing result when check_selected_networks is enabled and no IP rules are configured", function (done) {
+ const callback = (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(2);
+ expect(results[0].message).to.include(
+ "Event Hubs namespace is publicly accessible"
+ );
+ expect(results[0].region).to.equal("eastus");
+ done();
+ };
+
+ const cache = createCache([eventHubs[5]], networkRuleSets[2]);
+ eventHubPublicAccess.run(
+ cache,
+ { check_selected_networks: true },
+ callback
+ );
+ });
+
+ it("should give unknown result when check_selected_networks is enabled but unable to query network rule sets", function (done) {
+ const callback = (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(3);
+ expect(results[0].message).to.include(
+ "Unable to query Event Hubs network rule set:"
+ );
+ expect(results[0].region).to.equal("eastus");
+ done();
+ };
+
+ const cache = createNetworkRuleSetErrorCache([eventHubs[1]]);
+ eventHubPublicAccess.run(
+ cache,
+ { check_selected_networks: true },
+ callback
+ );
+ });
+
+ it("should skip event hub without ID", function (done) {
+ const callback = (err, results) => {
+ expect(results.length).to.equal(0);
+ done();
+ };
+
+ const cache = createCache([eventHubs[4]]);
+ eventHubPublicAccess.run(cache, {}, callback);
+ });
+
+ });
+});
diff --git a/plugins/azure/frontdoor/frontDoorManagedIdentity.js b/plugins/azure/frontdoor/frontDoorManagedIdentity.js
index a1a6ea01e..314eac71e 100644
--- a/plugins/azure/frontdoor/frontDoorManagedIdentity.js
+++ b/plugins/azure/frontdoor/frontDoorManagedIdentity.js
@@ -7,7 +7,7 @@ module.exports = {
domain: 'Content Delivery',
severity: 'Medium',
description: 'Ensures that Azure Front Door standard and premium profiles have managed identity enabled.',
- more_info: 'Enabling managed identities eliminate the need for developers having to manage credentials by providing an identity for the Azure resource in Azure AD and using it to obtain Azure Active Directory (Azure AD) tokens.',
+ more_info: 'Enabling managed identities eliminate the need for developers having to manage credentials by providing an identity for the Azure resource in Azure AD and using it to obtain Azure Entra ID tokens.',
recommended_action: 'Modify the Front Door standard and premium profile and add managed identity.',
link: 'https://learn.microsoft.com/en-us/azure/frontdoor/managed-identity',
apis: ['profiles:list'],
diff --git a/plugins/azure/keyvaults/keyVaultKeyExpiry.js b/plugins/azure/keyvaults/keyVaultKeyExpiry.js
index 4d1cfeda4..6ba32db01 100644
--- a/plugins/azure/keyvaults/keyVaultKeyExpiry.js
+++ b/plugins/azure/keyvaults/keyVaultKeyExpiry.js
@@ -30,7 +30,7 @@ module.exports = {
};
async.each(locations.vaults, function(location, rcb) {
- var vaults = helpers.addSource(cache, source,
+ var vaults = helpers.addSource(cache, source,
['vaults', 'list', location]);
if (!vaults) return rcb();
@@ -46,11 +46,7 @@ module.exports = {
}
vaults.data.forEach(function(vault) {
- if (!vault || !vault.properties) {
- helpers.addResult(results, 3, 'Unable to read vault properties', location, vault.id);
- return;
- }
- if (!vault.properties.enableRbacAuthorization) {
+ if (!vault.enableRbacAuthorization) {
return;
}
diff --git a/plugins/azure/keyvaults/keyVaultKeyExpiry.spec.js b/plugins/azure/keyvaults/keyVaultKeyExpiry.spec.js
index 2227db5dc..7aa4a804b 100644
--- a/plugins/azure/keyvaults/keyVaultKeyExpiry.spec.js
+++ b/plugins/azure/keyvaults/keyVaultKeyExpiry.spec.js
@@ -16,8 +16,8 @@ const listKeyVaults = [
name: 'test-vault',
type: 'Microsoft.KeyVault/vaults',
location: 'eastus',
+ enableRbacAuthorization: true,
properties: {
- enableRbacAuthorization: true,
vaultUri: 'https://test-vault.vault.azure.net/'
}
},
@@ -26,8 +26,8 @@ const listKeyVaults = [
name: 'test-vault-2',
type: 'Microsoft.KeyVault/vaults',
location: 'eastus',
+ enableRbacAuthorization: false,
properties: {
- enableRbacAuthorization: false,
vaultUri: 'https://test-vault-2.vault.azure.net/'
}
}
@@ -163,4 +163,4 @@ describe('keyVaultKeyExpiryRbac', function() {
auth.run(createCache(null, [listKeyVaults[0]], [getKeys[2]]), { key_vault_key_expiry_fail: '40' }, callback);
});
});
-});
+});
diff --git a/plugins/azure/keyvaults/keyVaultKeyExpiryNonRbac.js b/plugins/azure/keyvaults/keyVaultKeyExpiryNonRbac.js
index 4e9dae2d5..df9665a29 100644
--- a/plugins/azure/keyvaults/keyVaultKeyExpiryNonRbac.js
+++ b/plugins/azure/keyvaults/keyVaultKeyExpiryNonRbac.js
@@ -30,7 +30,7 @@ module.exports = {
};
async.each(locations.vaults, function(location, rcb) {
- var vaults = helpers.addSource(cache, source,
+ var vaults = helpers.addSource(cache, source,
['vaults', 'list', location]);
if (!vaults) return rcb();
@@ -46,12 +46,7 @@ module.exports = {
}
vaults.data.forEach(function(vault) {
- if (!vault || !vault.properties) {
- helpers.addResult(results, 3, 'Unable to read vault properties', location, vault.id);
- return;
- }
-
- if (vault.properties.enableRbacAuthorization) {
+ if (vault.enableRbacAuthorization) {
return;
}
@@ -59,10 +54,10 @@ module.exports = {
['vaults', 'getKeys', location, vault.id]);
if (!keys || keys.err || !keys.data) {
- helpers.addResult(results, 3,
+ helpers.addResult(results, 3,
'Unable to query for Key Vault keys: ' + helpers.addError(keys), location, vault.id);
} else if (!keys.data.length) {
- helpers.addResult(results, 0,
+ helpers.addResult(results, 0,
'No Key Vault keys found in non RBAC vault', location, vault.id);
} else {
keys.data.forEach(function(key) {
diff --git a/plugins/azure/keyvaults/keyVaultKeyExpiryNonRbac.spec.js b/plugins/azure/keyvaults/keyVaultKeyExpiryNonRbac.spec.js
index 1c5431cb1..43ed0ab13 100644
--- a/plugins/azure/keyvaults/keyVaultKeyExpiryNonRbac.spec.js
+++ b/plugins/azure/keyvaults/keyVaultKeyExpiryNonRbac.spec.js
@@ -5,7 +5,7 @@ var keyExpiryPass = new Date();
keyExpiryPass.setMonth(keyExpiryPass.getMonth() + 2);
var keyExpiryFail = new Date();
-keyExpiryFail.setDate(keyExpiryFail.getDate() + 25); // Set to 35 days in the future
+keyExpiryFail.setDate(keyExpiryFail.getDate() + 25);
var keyExpired = new Date();
keyExpired.setMonth(keyExpired.getMonth() - 1);
@@ -16,8 +16,8 @@ const listKeyVaults = [
name: 'test-vault',
type: 'Microsoft.KeyVault/vaults',
location: 'eastus',
+ enableRbacAuthorization: false,
properties: {
- enableRbacAuthorization: false,
vaultUri: 'https://test-vault.vault.azure.net/'
}
},
@@ -26,8 +26,8 @@ const listKeyVaults = [
name: 'test-vault-2',
type: 'Microsoft.KeyVault/vaults',
location: 'eastus',
+ enableRbacAuthorization: true,
properties: {
- enableRbacAuthorization: true,
vaultUri: 'https://test-vault-2.vault.azure.net/'
}
}
@@ -172,4 +172,4 @@ describe('keyVaultKeyExpiryNonRbac', function() {
auth.run(createCache(null, [listKeyVaults[0]], [getKeys[2]]), { key_vault_key_expiry_fail: '40' }, callback);
});
});
-});
\ No newline at end of file
+});
diff --git a/plugins/azure/keyvaults/keyVaultPublicAccess.js b/plugins/azure/keyvaults/keyVaultPublicAccess.js
new file mode 100644
index 000000000..1e0459769
--- /dev/null
+++ b/plugins/azure/keyvaults/keyVaultPublicAccess.js
@@ -0,0 +1,118 @@
+var async = require('async');
+const helpers = require('../../../helpers/azure');
+
+module.exports = {
+ title: 'Key Vault Public Access',
+ category: 'Key Vault',
+ domain: 'Security',
+ severity: 'High',
+ description: 'Ensures that Azure Key Vaults do not allow unrestricted public access',
+ more_info: 'Azure Key Vaults should be configured to restrict public access to protect sensitive data. This can be achieved by either disabling public network access or implementing strict network rules.',
+ recommended_action: 'Modify Key Vault network settings to disable public access or appropriate configure network rules.',
+ link: 'https://learn.microsoft.com/en-us/azure/key-vault/general/network-security',
+ apis: ['vaults:list'],
+ settings: {
+ keyvault_allowed_ips: {
+ name: 'Key Vault Allowed IPs',
+ description: 'Comma-separated list of IP addresses that are explicitly allowed to access Key Vaults',
+ regex: '^(?:\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}(?:/\\d{1,2})?(?:,\\s*)?)+$',
+ default: ''
+ }
+ },
+ realtime_triggers: ['microsoft.keyvault:vaults:write', 'microsoft.keyvault:vaults:delete'],
+
+ run: function(cache, settings, callback) {
+ var results = [];
+ var source = {};
+ var locations = helpers.locations(settings.govcloud);
+
+ var config = {
+ keyvault_allowed_ips: settings.keyvault_allowed_ips || this.settings.keyvault_allowed_ips.default
+ };
+
+ var allowedIps = [];
+ if (config.keyvault_allowed_ips && config.keyvault_allowed_ips.length) {
+ allowedIps = config.keyvault_allowed_ips.split(',').map(ip => ip.trim());
+ }
+ var checkAllowedIps = allowedIps.length > 0;
+
+ async.each(locations.vaults, function(location, rcb) {
+ var vaults = helpers.addSource(cache, source,
+ ['vaults', 'list', location]);
+
+ if (!vaults) return rcb();
+
+ if (vaults.err || !vaults.data) {
+ helpers.addResult(results, 3,
+ 'Unable to query for Key Vaults: ' + helpers.addError(vaults), location);
+ return rcb();
+ }
+
+ if (!vaults.data.length) {
+ helpers.addResult(results, 0, 'No Key Vaults found', location);
+ return rcb();
+ }
+
+ vaults.data.forEach(function(vault) {
+ if (!vault.id) return;
+
+ if (vault &&
+ vault.publicNetworkAccess &&
+ vault.publicNetworkAccess.toLowerCase() === 'disabled') {
+ helpers.addResult(results, 0,
+ 'Key Vault is protected from outside traffic',
+ location, vault.id);
+ return;
+ }
+
+
+ if (vault && vault.networkAcls) {
+ var networkAcls = vault.networkAcls;
+ var defaultAction = networkAcls.defaultAction ? networkAcls.defaultAction.toLowerCase() : null;
+
+ if (!defaultAction || defaultAction === 'allow') {
+ helpers.addResult(results, 2,
+ 'Key Vault is open to outside traffic',
+ location, vault.id);
+ return;
+ }
+
+ if (defaultAction === 'deny') {
+ var ipRules = networkAcls.ipRules || [];
+ var hasPublicAccess = false;
+ var publicAccessFound = [];
+
+ for (var rule of ipRules) {
+ if (checkAllowedIps) {
+ if (!allowedIps.includes(rule.value)) {
+ hasPublicAccess = true;
+ publicAccessFound.push(rule.value);
+ }
+ } else if (rule.value === '0.0.0.0/0' || rule.value === '0.0.0.0') {
+ hasPublicAccess = true;
+ publicAccessFound.push(rule.value);
+ }
+ }
+
+ if (hasPublicAccess) {
+ helpers.addResult(results, 2,
+ `Key Vault is open to outside traffic through IP rules: ${publicAccessFound.join(', ')}`,
+ location, vault.id);
+ } else {
+ var message = 'Key Vault is protected from outside traffic';
+ helpers.addResult(results, 0, message, location, vault.id);
+ }
+ }
+ } else {
+ helpers.addResult(results, 2,
+ 'Key Vault is open to outside traffic',
+ location, vault.id);
+ }
+ });
+
+ rcb();
+ }, function() {
+ callback(null, results, source);
+ });
+ }
+};
\ No newline at end of file
diff --git a/plugins/azure/keyvaults/keyVaultPublicAccess.spec.js b/plugins/azure/keyvaults/keyVaultPublicAccess.spec.js
new file mode 100644
index 000000000..e79bdbee4
--- /dev/null
+++ b/plugins/azure/keyvaults/keyVaultPublicAccess.spec.js
@@ -0,0 +1,192 @@
+var expect = require('chai').expect;
+var keyVaultPublicAccess = require('./keyVaultPublicAccess');
+
+const vaults = [
+ {
+ "id": "/subscriptions/123/resourceGroups/test/providers/Microsoft.KeyVault/vaults/test1",
+ "name": "test1",
+ "type": "Microsoft.KeyVault/vaults",
+ "publicNetworkAccess": "Disabled"
+
+ },
+ {
+ "id": "/subscriptions/123/resourceGroups/test/providers/Microsoft.KeyVault/vaults/test2",
+ "name": "test2",
+ "type": "Microsoft.KeyVault/vaults",
+ "publicNetworkAccess": "Enabled",
+ "networkAcls": {
+ "defaultAction": "Deny",
+ "ipRules": [
+ {
+ "value": "10.0.0.0/16"
+ }
+ ]
+ }
+
+ },
+ {
+ "id": "/subscriptions/123/resourceGroups/test/providers/Microsoft.KeyVault/vaults/test3",
+ "name": "test3",
+ "type": "Microsoft.KeyVault/vaults",
+ "publicNetworkAccess": "Enabled",
+ "networkAcls": {
+ "defaultAction": "Allow",
+ "ipRules": []
+ }
+
+ },
+ {
+ "id": "/subscriptions/123/resourceGroups/test/providers/Microsoft.KeyVault/vaults/test4",
+ "name": "test4",
+ "type": "Microsoft.KeyVault/vaults",
+ "publicNetworkAccess": "Enabled",
+ "networkAcls": {
+ "defaultAction": "Deny",
+ "ipRules": [
+ {
+ "value": "0.0.0.0/0"
+ }
+ ]
+ }
+ },
+ {
+ "id": "/subscriptions/123/resourceGroups/test/providers/Microsoft.KeyVault/vaults/test6",
+ "name": "test6",
+ "type": "Microsoft.KeyVault/vaults",
+ "publicNetworkAccess": "Enabled"
+
+ },
+ {
+ "id": "/subscriptions/123/resourceGroups/test/providers/Microsoft.KeyVault/vaults/test7",
+ "name": "test7",
+ "type": "Microsoft.KeyVault/vaults",
+ "publicNetworkAccess": "Enabled",
+ "networkAcls": {
+ "defaultAction": "Deny",
+ "ipRules": [
+ {
+ "value": "192.168.1.1"
+ }
+ ]
+
+ }
+ }
+];
+
+const createCache = (vaults) => {
+ return {
+ vaults: {
+ list: {
+ 'eastus': {
+ data: vaults
+ }
+ }
+ }
+ };
+};
+
+const createErrorCache = () => {
+ return {
+ vaults: {
+ list: {
+ 'eastus': {
+ err: {
+ message: 'error loading vaults'
+ }
+ }
+ }
+ }
+ };
+};
+
+describe('keyVaultPublicAccess', function () {
+ describe('run', function () {
+ it('should give passing result if no key vaults found', function (done) {
+ const cache = createCache([]);
+ keyVaultPublicAccess.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(0);
+ expect(results[0].message).to.include('No Key Vaults found');
+ expect(results[0].region).to.equal('eastus');
+ done();
+ });
+ });
+
+ it('should give unknown result if unable to query for key vaults', function (done) {
+ const cache = createErrorCache();
+ keyVaultPublicAccess.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(3);
+ expect(results[0].message).to.include('Unable to query for Key Vaults');
+ expect(results[0].region).to.equal('eastus');
+ done();
+ });
+ });
+
+ it('should give passing result if public network access is disabled', function (done) {
+ const cache = createCache([vaults[0]]);
+ keyVaultPublicAccess.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(0);
+ expect(results[0].message).to.include('Key Vault is protected from outside traffic');
+ expect(results[0].region).to.equal('eastus');
+ done();
+ });
+ });
+
+ it('should give passing result if default action is deny and no public IPs allowed', function (done) {
+ const cache = createCache([vaults[1]]);
+ keyVaultPublicAccess.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(0);
+ expect(results[0].message).to.include('Key Vault is protected from outside traffic');
+ expect(results[0].region).to.equal('eastus');
+ done();
+ });
+ });
+
+ it('should give failing result if default action is allow', function (done) {
+ const cache = createCache([vaults[2]]);
+ keyVaultPublicAccess.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(2);
+ expect(results[0].message).to.include('Key Vault is open to outside traffic');
+ expect(results[0].region).to.equal('eastus');
+ done();
+ });
+ });
+
+ it('should give failing result if IPv4 public access is allowed', function (done) {
+ const cache = createCache([vaults[3]]);
+ keyVaultPublicAccess.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(2);
+ expect(results[0].message).to.include('Key Vault is open to outside traffic');
+ expect(results[0].region).to.equal('eastus');
+ done();
+ });
+ });
+
+ it('should give failing result if no network ACLs configured', function (done) {
+ const cache = createCache([vaults[4]]);
+ keyVaultPublicAccess.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(2);
+ expect(results[0].message).to.include('Key Vault is open to outside traffic');
+ expect(results[0].region).to.equal('eastus');
+ done();
+ });
+ });
+
+ it('should give passing result if IP is in allowed list', function (done) {
+ const cache = createCache([vaults[5]]);
+ keyVaultPublicAccess.run(cache, { keyvault_allowed_ips: '192.168.1.1' }, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(0);
+ expect(results[0].message).to.include('Key Vault is protected from outside traffic');
+ expect(results[0].region).to.equal('eastus');
+ done();
+ });
+ });
+ });
+});
\ No newline at end of file
diff --git a/plugins/azure/keyvaults/keyVaultSecretExpiry.js b/plugins/azure/keyvaults/keyVaultSecretExpiry.js
index 063f3125d..2dd31788d 100644
--- a/plugins/azure/keyvaults/keyVaultSecretExpiry.js
+++ b/plugins/azure/keyvaults/keyVaultSecretExpiry.js
@@ -47,7 +47,7 @@ module.exports = {
vaults.data.forEach(function(vault) {
// Check if vault is RBAC-enabled
- if (!vault.properties || !vault.properties.enableRbacAuthorization) {
+ if (!vault.enableRbacAuthorization) {
return;
}
diff --git a/plugins/azure/keyvaults/keyVaultSecretExpiry.spec.js b/plugins/azure/keyvaults/keyVaultSecretExpiry.spec.js
index 72c5c5bae..4bf693b9e 100644
--- a/plugins/azure/keyvaults/keyVaultSecretExpiry.spec.js
+++ b/plugins/azure/keyvaults/keyVaultSecretExpiry.spec.js
@@ -21,9 +21,8 @@ const listKeyVaults = [
"family": "A",
"name": "Standard"
},
- "properties": {
- "enableRbacAuthorization": true
- }
+ "enableRbacAuthorization": true,
+
},
{
"id": "/subscriptions/abcdef123-ebf6-437f-a3b0-28fc0d22117e/resourceGroups/Default-ActivityLogAlerts/providers/Microsoft.KeyVault/vaults/testvault",
@@ -35,9 +34,7 @@ const listKeyVaults = [
"family": "A",
"name": "Standard"
},
- "properties": {
- "enableRbacAuthorization": false
- }
+ "enableRbacAuthorization": false,
}
];
diff --git a/plugins/azure/keyvaults/keyVaultSecretExpiryNonRbac.js b/plugins/azure/keyvaults/keyVaultSecretExpiryNonRbac.js
index 258fc67ed..e2f8a15fd 100644
--- a/plugins/azure/keyvaults/keyVaultSecretExpiryNonRbac.js
+++ b/plugins/azure/keyvaults/keyVaultSecretExpiryNonRbac.js
@@ -18,7 +18,7 @@ module.exports = {
regex: '^[1-9]{1}[0-9]{0,3}$',
default: '30'
}
- },
+ },
realtime_triggers: ['microsoftkeyvault:vaults:write', 'microsoftkeyvault:vaults:delete'],
run: function(cache, settings, callback) {
@@ -47,7 +47,7 @@ module.exports = {
vaults.data.forEach(function(vault) {
// Check if vault is non-RBAC
- if (vault.properties && vault.properties.enableRbacAuthorization) {
+ if (vault.enableRbacAuthorization) {
return;
}
diff --git a/plugins/azure/keyvaults/restrictDefaultNetworkAccess.js b/plugins/azure/keyvaults/restrictDefaultNetworkAccess.js
index 43ffc27ae..e9362dbcd 100644
--- a/plugins/azure/keyvaults/restrictDefaultNetworkAccess.js
+++ b/plugins/azure/keyvaults/restrictDefaultNetworkAccess.js
@@ -35,18 +35,14 @@ module.exports = {
}
vaults.data.forEach((vault) => {
- if (vault.networkAcls){
- if (vault.networkAcls && ((!vault.networkAcls.defaultAction) ||
- (vault.networkAcls.defaultAction && vault.networkAcls.defaultAction === 'Allow'))) {
- helpers.addResult(results, 2,
- 'Key Vault allows access to all networks', location, vault.id);
- } else {
- helpers.addResult(results, 0,
- 'Key Vault does not allow access to all networks', location, vault.id);
- }
- } else {
+ if (vault.networkAcls &&
+ vault.networkAcls.defaultAction &&
+ vault.networkAcls.defaultAction === 'Deny') {
helpers.addResult(results, 0,
- 'Network Acl is not configured for Key Vault', location, vault.id);
+ 'Key Vault does not allow access to all networks', location, vault.id);
+ } else {
+ helpers.addResult(results, 2,
+ 'Key Vault allows access to all networks', location, vault.id);
}
});
diff --git a/plugins/azure/keyvaults/restrictDefaultNetworkAccess.spec.js b/plugins/azure/keyvaults/restrictDefaultNetworkAccess.spec.js
index b7c0aa7a4..4b3d624b7 100644
--- a/plugins/azure/keyvaults/restrictDefaultNetworkAccess.spec.js
+++ b/plugins/azure/keyvaults/restrictDefaultNetworkAccess.spec.js
@@ -3,89 +3,89 @@ var auth = require('./restrictDefaultNetworkAccess');
const listVaults = [
{
- "id": "/subscriptions/1234/resourceGroups/testrg/providers/Microsoft.KeyVault/vaults/xZbb",
+ "id": "/subscriptions/1234/resourceGroups/testrg/providers/Microsoft.KeyVault/vaults/xZbb",
"name": "xZbb",
"type": "Microsoft.KeyVault/vaults",
"location": "eastus",
"tags": {},
"sku": {
- "family": "A",
- "name": "Standard"
+ "family": "A",
+ "name": "Standard"
},
"tenantId": "2d4f0836-5935-47f5-954c-14e713119ac2",
"networkAcls": {
- "bypass": "None",
- "defaultAction": "Deny",
- "ipRules": [],
- "virtualNetworkRules": [
- {
- "id": "/subscriptions/1234/resourcegroups/akhtar-rg/providers/microsoft.network/virtualnetworks/akhtar-rg-vnet/subnets/default",
- "ignoreMissingVnetServiceEndpoint": false
- }
- ]
+ "bypass": "None",
+ "defaultAction": "Deny",
+ "ipRules": [],
+ "virtualNetworkRules": [
+ {
+ "id": "/subscriptions/1234/resourcegroups/akhtar-rg/providers/microsoft.network/virtualnetworks/akhtar-rg-vnet/subnets/default",
+ "ignoreMissingVnetServiceEndpoint": false
+ }
+ ]
},
"privateEndpointConnections": [
- {
- "id": "/subscriptions/1234/resourceGroups/testrg/providers/Microsoft.KeyVault/vaults/xZbb/privateEndpointConnections/sadeed",
- "properties": {
- "provisioningState": "Succeeded",
- "privateEndpoint": {
- "id": "/subscriptions/1234/resourceGroups/testrg/providers/Microsoft.Network/privateEndpoints/sadeed"
- },
- "privateLinkServiceConnectionState": {
- "status": "Approved",
- "actionsRequired": "None"
- }
+ {
+ "id": "/subscriptions/1234/resourceGroups/testrg/providers/Microsoft.KeyVault/vaults/xZbb/privateEndpointConnections/sadeed",
+ "properties": {
+ "provisioningState": "Succeeded",
+ "privateEndpoint": {
+ "id": "/subscriptions/1234/resourceGroups/testrg/providers/Microsoft.Network/privateEndpoints/sadeed"
+ },
+ "privateLinkServiceConnectionState": {
+ "status": "Approved",
+ "actionsRequired": "None"
+ }
+ }
}
- }
],
"accessPolicies": [
- {
- "tenantId": "2d4f0836-5935-47f5-954c-14e713119ac2",
- "objectId": "d198cb4d-de06-40ff-8fc4-4f643fbeabc5",
- "permissions": {
- "keys": [
- "Get",
- "List",
- "Update",
- "Create",
- "Import",
- "Delete",
- "Recover",
- "Backup",
- "Restore",
- "GetRotationPolicy",
- "SetRotationPolicy",
- "Rotate"
- ],
- "secrets": [
- "Get",
- "List",
- "Set",
- "Delete",
- "Recover",
- "Backup",
- "Restore"
- ],
- "certificates": [
- "Get",
- "List",
- "Update",
- "Create",
- "Import",
- "Delete",
- "Recover",
- "Backup",
- "Restore",
- "ManageContacts",
- "ManageIssuers",
- "GetIssuers",
- "ListIssuers",
- "SetIssuers",
- "DeleteIssuers"
- ]
+ {
+ "tenantId": "2d4f0836-5935-47f5-954c-14e713119ac2",
+ "objectId": "d198cb4d-de06-40ff-8fc4-4f643fbeabc5",
+ "permissions": {
+ "keys": [
+ "Get",
+ "List",
+ "Update",
+ "Create",
+ "Import",
+ "Delete",
+ "Recover",
+ "Backup",
+ "Restore",
+ "GetRotationPolicy",
+ "SetRotationPolicy",
+ "Rotate"
+ ],
+ "secrets": [
+ "Get",
+ "List",
+ "Set",
+ "Delete",
+ "Recover",
+ "Backup",
+ "Restore"
+ ],
+ "certificates": [
+ "Get",
+ "List",
+ "Update",
+ "Create",
+ "Import",
+ "Delete",
+ "Recover",
+ "Backup",
+ "Restore",
+ "ManageContacts",
+ "ManageIssuers",
+ "GetIssuers",
+ "ListIssuers",
+ "SetIssuers",
+ "DeleteIssuers"
+ ]
+ }
}
- }
],
"enabledForDeployment": false,
"enabledForDiskEncryption": false,
@@ -97,89 +97,89 @@ const listVaults = [
"provisioningState": "Succeeded"
},
{
- "id": "/subscriptions/1234/resourceGroups/testrg/providers/Microsoft.KeyVault/vaults/xZbb",
+ "id": "/subscriptions/1234/resourceGroups/testrg/providers/Microsoft.KeyVault/vaults/xZbb",
"name": "xZbb",
"type": "Microsoft.KeyVault/vaults",
"location": "eastus",
"tags": {},
"sku": {
- "family": "A",
- "name": "Standard"
+ "family": "A",
+ "name": "Standard"
},
"tenantId": "2d4f0836-5935-47f5-954c-14e713119ac2",
"networkAcls": {
- "bypass": "None",
- "defaultAction": "Allow",
- "ipRules": [],
- "virtualNetworkRules": [
- {
- "id": "/subscriptions/1234/resourcegroups/akhtar-rg/providers/microsoft.network/virtualnetworks/akhtar-rg-vnet/subnets/default",
- "ignoreMissingVnetServiceEndpoint": false
- }
- ]
+ "bypass": "None",
+ "defaultAction": "Allow",
+ "ipRules": [],
+ "virtualNetworkRules": [
+ {
+ "id": "/subscriptions/1234/resourcegroups/akhtar-rg/providers/microsoft.network/virtualnetworks/akhtar-rg-vnet/subnets/default",
+ "ignoreMissingVnetServiceEndpoint": false
+ }
+ ]
},
"privateEndpointConnections": [
- {
- "id": "/subscriptions/1234/resourceGroups/testrg/providers/Microsoft.KeyVault/vaults/xZbb/privateEndpointConnections/sadeed",
- "properties": {
- "provisioningState": "Succeeded",
- "privateEndpoint": {
- "id": "/subscriptions/1234/resourceGroups/testrg/providers/Microsoft.Network/privateEndpoints/sadeed"
- },
- "privateLinkServiceConnectionState": {
- "status": "Approved",
- "actionsRequired": "None"
- }
+ {
+ "id": "/subscriptions/1234/resourceGroups/testrg/providers/Microsoft.KeyVault/vaults/xZbb/privateEndpointConnections/sadeed",
+ "properties": {
+ "provisioningState": "Succeeded",
+ "privateEndpoint": {
+ "id": "/subscriptions/1234/resourceGroups/testrg/providers/Microsoft.Network/privateEndpoints/sadeed"
+ },
+ "privateLinkServiceConnectionState": {
+ "status": "Approved",
+ "actionsRequired": "None"
+ }
+ }
}
- }
],
"accessPolicies": [
- {
- "tenantId": "2d4f0836-5935-47f5-954c-14e713119ac2",
- "objectId": "d198cb4d-de06-40ff-8fc4-4f643fbeabc5",
- "permissions": {
- "keys": [
- "Get",
- "List",
- "Update",
- "Create",
- "Import",
- "Delete",
- "Recover",
- "Backup",
- "Restore",
- "GetRotationPolicy",
- "SetRotationPolicy",
- "Rotate"
- ],
- "secrets": [
- "Get",
- "List",
- "Set",
- "Delete",
- "Recover",
- "Backup",
- "Restore"
- ],
- "certificates": [
- "Get",
- "List",
- "Update",
- "Create",
- "Import",
- "Delete",
- "Recover",
- "Backup",
- "Restore",
- "ManageContacts",
- "ManageIssuers",
- "GetIssuers",
- "ListIssuers",
- "SetIssuers",
- "DeleteIssuers"
- ]
+ {
+ "tenantId": "2d4f0836-5935-47f5-954c-14e713119ac2",
+ "objectId": "d198cb4d-de06-40ff-8fc4-4f643fbeabc5",
+ "permissions": {
+ "keys": [
+ "Get",
+ "List",
+ "Update",
+ "Create",
+ "Import",
+ "Delete",
+ "Recover",
+ "Backup",
+ "Restore",
+ "GetRotationPolicy",
+ "SetRotationPolicy",
+ "Rotate"
+ ],
+ "secrets": [
+ "Get",
+ "List",
+ "Set",
+ "Delete",
+ "Recover",
+ "Backup",
+ "Restore"
+ ],
+ "certificates": [
+ "Get",
+ "List",
+ "Update",
+ "Create",
+ "Import",
+ "Delete",
+ "Recover",
+ "Backup",
+ "Restore",
+ "ManageContacts",
+ "ManageIssuers",
+ "GetIssuers",
+ "ListIssuers",
+ "SetIssuers",
+ "DeleteIssuers"
+ ]
+ }
}
- }
],
"enabledForDeployment": false,
"enabledForDiskEncryption": false,
@@ -190,6 +190,55 @@ const listVaults = [
"vaultUri": "https://xzbb.vault.azure.net/",
"provisioningState": "Succeeded"
},
+ {
+ "id": "/subscriptions/1234/resourceGroups/testrg/providers/Microsoft.KeyVault/vaults/noNetworkAcls",
+ "name": "noNetworkAcls",
+ "type": "Microsoft.KeyVault/vaults",
+ "location": "eastus",
+ "tags": {},
+ "sku": {
+ "family": "A",
+ "name": "Standard"
+ },
+ "tenantId": "2d4f0836-5935-47f5-954c-14e713119ac2",
+ "privateEndpointConnections": [],
+ "accessPolicies": [],
+ "enabledForDeployment": false,
+ "enabledForDiskEncryption": false,
+ "enabledForTemplateDeployment": false,
+ "enableSoftDelete": true,
+ "softDeleteRetentionInDays": 90,
+ "enableRbacAuthorization": false,
+ "vaultUri": "https://nonetworkacls.vault.azure.net/",
+ "provisioningState": "Succeeded"
+ },
+ {
+ "id": "/subscriptions/1234/resourceGroups/testrg/providers/Microsoft.KeyVault/vaults/emptyDefaultAction",
+ "name": "emptyDefaultAction",
+ "type": "Microsoft.KeyVault/vaults",
+ "location": "eastus",
+ "tags": {},
+ "sku": {
+ "family": "A",
+ "name": "Standard"
+ },
+ "tenantId": "2d4f0836-5935-47f5-954c-14e713119ac2",
+ "networkAcls": {
+ "bypass": "None",
+ "ipRules": [],
+ "virtualNetworkRules": []
+ },
+ "privateEndpointConnections": [],
+ "accessPolicies": [],
+ "enabledForDeployment": false,
+ "enabledForDiskEncryption": false,
+ "enabledForTemplateDeployment": false,
+ "enableSoftDelete": true,
+ "softDeleteRetentionInDays": 90,
+ "enableRbacAuthorization": false,
+ "vaultUri": "https://emptydefaultaction.vault.azure.net/",
+ "provisioningState": "Succeeded"
+ }
];
const createCache = (err, list, get) => {
@@ -256,6 +305,30 @@ describe('restrictDefaultNetworkAccess', function() {
};
auth.run(createCache(null, [listVaults[1]]), {}, callback);
- })
+ });
+
+ it('should give failing result if Key Vault has no networkAcls configured', function(done) {
+ const callback = (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(2);
+ expect(results[0].message).to.include('Key Vault allows access to all networks');
+ expect(results[0].region).to.equal('eastus');
+ done()
+ };
+
+ auth.run(createCache(null, [listVaults[2]]), {}, callback);
+ });
+
+ it('should give failing result if Key Vault has networkAcls but no defaultAction', function(done) {
+ const callback = (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(2);
+ expect(results[0].message).to.include('Key Vault allows access to all networks');
+ expect(results[0].region).to.equal('eastus');
+ done()
+ };
+
+ auth.run(createCache(null, [listVaults[3]]), {}, callback);
+ });
})
});
diff --git a/plugins/azure/kubernetesservice/aksLatestVersion.js b/plugins/azure/kubernetesservice/aksLatestVersion.js
index 406d0a44e..e5900accd 100644
--- a/plugins/azure/kubernetesservice/aksLatestVersion.js
+++ b/plugins/azure/kubernetesservice/aksLatestVersion.js
@@ -9,7 +9,7 @@ module.exports = {
description: 'Ensures the latest version of Kubernetes is installed on AKS clusters',
more_info: 'AKS supports provisioning clusters from several versions of Kubernetes. Clusters should be kept up to date to ensure Kubernetes security patches are applied.',
recommended_action: 'Upgrade the version of Kubernetes on all AKS clusters to the latest available version.',
- link: 'https://learn.microsoft.com/en-us/azure/aks/aad-integration',
+ link: 'https://learn.microsoft.com/en-us/azure/aks/upgrade-aks-cluster?tabs=azure-portal',
apis: ['managedClusters:list', 'managedClusters:getUpgradeProfile'],
realtime_triggers: ['microsoftcontainerservice:managedclusters:write', 'microsoftcontainerservice:managedclusters:delete'],
@@ -47,7 +47,7 @@ module.exports = {
getUpgradeProfile.data.controlPlaneProfile.upgrades &&
getUpgradeProfile.data.controlPlaneProfile.upgrades.length) {
helpers.addResult(results, 2,
- `The managed cluster does not have the latest Kubernetes version: ${getUpgradeProfile.data.controlPlaneProfile.upgrades[0]}`, location, managedCluster.id);
+ `The managed cluster does not have the latest Kubernetes version: ${getUpgradeProfile.data.controlPlaneProfile.upgrades[0].kubernetesVersion}`, location, managedCluster.id);
} else {
helpers.addResult(results, 0,
'The managed cluster has the latest Kubernetes version', location, managedCluster.id);
diff --git a/plugins/azure/kubernetesservice/aksLatestVersion.spec.js b/plugins/azure/kubernetesservice/aksLatestVersion.spec.js
index 8043d8554..9d0db2b22 100644
--- a/plugins/azure/kubernetesservice/aksLatestVersion.spec.js
+++ b/plugins/azure/kubernetesservice/aksLatestVersion.spec.js
@@ -65,8 +65,8 @@ describe('aksLatestVersion', function() {
"kubernetesVersion": "1.11.10",
"osType": "Linux",
"upgrades": [
- "1.12.7",
- "1.12.8"
+ {"kubernetesVersion": "1.12.7"},
+ {"kubernetesVersion": "1.12.8"}
]
},
"agentPoolProfiles": [
diff --git a/plugins/azure/kubernetesservice/aksNetworkExposure.js b/plugins/azure/kubernetesservice/aksNetworkExposure.js
index 012fcdb67..0dd8326fe 100644
--- a/plugins/azure/kubernetesservice/aksNetworkExposure.js
+++ b/plugins/azure/kubernetesservice/aksNetworkExposure.js
@@ -2,7 +2,7 @@ var async = require('async');
var helpers = require('../../../helpers/azure/');
module.exports = {
- title: 'Network Exposure',
+ title: 'Internet Exposure',
category: 'Kubernetes Service',
domain: 'Containers',
severity: 'High',
@@ -83,7 +83,7 @@ module.exports = {
}
});
securityGroups = networkSecurityGroups.data.filter(nsg => securityGroupIDs.includes(nsg.id));
- internetExposed = helpers.checkNetworkExposure(cache, source, [], securityGroups, location, results, []);
+ internetExposed = helpers.checkNetworkExposure(cache, source, [], securityGroups, location, results, {}, cluster);
}
}
}
diff --git a/plugins/azure/kubernetesservice/aksPrivilegeAnalysis.js b/plugins/azure/kubernetesservice/aksPrivilegeAnalysis.js
new file mode 100644
index 000000000..266ffebf0
--- /dev/null
+++ b/plugins/azure/kubernetesservice/aksPrivilegeAnalysis.js
@@ -0,0 +1,24 @@
+module.exports = {
+ title: 'Privilege Analysis',
+ category: 'Kubernetes Service',
+ domain: 'Containers',
+ severity: 'Info',
+ description: 'Ensures that AKS clusters and workloads are not granted excessive permissions.',
+ more_info: 'AKS clusters often use managed identities to interact with Azure resources. Over-privileged identities can lead to privilege escalation or lateral movement within the cluster or the Azure environment. Following the principle of least privilege helps minimize potential attack surfaces.',
+ link: 'https://docs.microsoft.com/en-us/azure/aks/use-managed-identity',
+ recommended_action: 'Review and minimize Azure AD permissions granted to AKS managed identities and workload identities. Use Azure RBAC and Kubernetes RBAC best practices to ensure only required access is permitted.',
+ apis: [''],
+ realtime_triggers: [
+ 'Microsoft.ContainerService/managedClusters/write',
+ 'Microsoft.ContainerService/managedClusters/delete',
+ 'Microsoft.ContainerService/managedClusters/agentPools/write',
+ 'Microsoft.ManagedIdentity/userAssignedIdentities/assign/action',
+ ],
+
+ run: function(cache, settings, callback) {
+ var results = [];
+ var source = {};
+
+ callback(null, results, source);
+ },
+};
diff --git a/plugins/azure/mediaServices/amsManagedIdentityEnabled.js b/plugins/azure/mediaServices/amsManagedIdentityEnabled.js
index a35cb99a3..8babd67ed 100644
--- a/plugins/azure/mediaServices/amsManagedIdentityEnabled.js
+++ b/plugins/azure/mediaServices/amsManagedIdentityEnabled.js
@@ -7,7 +7,7 @@ module.exports = {
domain: 'Content Delivery',
severity: 'Medium',
description: 'Ensures that Azure Media Service accounts have managed identity enabled.',
- more_info: 'Enabling managed identities eliminate the need for developers having to manage credentials by providing an identity for the Azure resource in Azure AD and using it to obtain Azure Active Directory (Azure AD) tokens.',
+ more_info: 'Enabling managed identities eliminate the need for developers having to manage credentials by providing an identity for the Azure resource in Azure AD and using it to obtain Azure Entra ID tokens.',
link: 'https://learn.microsoft.com/en-us/azure/media-services/latest/concept-managed-identities',
recommended_action: 'Create a new Media service account with managed identity for storage account enabled.',
apis: ['mediaServices:listAll', 'mediaServices:get'],
diff --git a/plugins/azure/mysqlserver/mysqlFlexibleServerIdentity.js b/plugins/azure/mysqlserver/mysqlFlexibleServerIdentity.js
index a770cf047..731acafe6 100644
--- a/plugins/azure/mysqlserver/mysqlFlexibleServerIdentity.js
+++ b/plugins/azure/mysqlserver/mysqlFlexibleServerIdentity.js
@@ -7,7 +7,7 @@ module.exports = {
domain: 'Databases',
severity: 'Medium',
description: 'Ensures that MySQL flexible servers have managed identity enabled.',
- more_info: 'Enabling managed identities eliminate the need for developers having to manage credentials by providing an identity for the Azure resource in Azure AD and using it to obtain Azure Active Directory (Azure AD) tokens.',
+ more_info: 'Enabling managed identities eliminate the need for developers having to manage credentials by providing an identity for the Azure resource in Azure AD and using it to obtain Azure Entra ID tokens.',
recommended_action: 'Modify MySQL flexible server add managed identity.',
link: 'https://learn.microsoft.com/en-us/azure/mysql/flexible-server/how-to-azure-ad',
apis: ['servers:listMysqlFlexibleServer'],
diff --git a/plugins/azure/mysqlserver/mysqlFlexibleServerPublicAccess.js b/plugins/azure/mysqlserver/mysqlFlexibleServerPublicAccess.js
index c61bbcdb3..68d5f4d83 100644
--- a/plugins/azure/mysqlserver/mysqlFlexibleServerPublicAccess.js
+++ b/plugins/azure/mysqlserver/mysqlFlexibleServerPublicAccess.js
@@ -6,24 +6,42 @@ module.exports = {
category: 'MySQL Server',
domain: 'Databases',
severity: 'High',
- description: 'Ensures that MySQL flexible servers are not publicly accessible.',
- more_info: 'Configuring public access for MySQL flexible server instance allows the server to be accessible through public endpoint. This can expose the server to unauthorized access and various cyber threats. Disabling public access enhances security by limiting access to authorized connections only.',
- recommended_action: 'Modify MySQL flexible server and disable public network access.',
+ description: 'Ensures that MySQL flexible servers do not allow public access',
+ more_info: 'Configuring public access for MySQL flexible server instance allows the server to be accessible through public endpoint. MySQL flexible server instances should not have a public endpoint and should only be accessed from within a VNET.',
+ recommended_action: 'Ensure that the firewall of each MySQL flexible server is configured to prohibit traffic from the public.',
link: 'https://learn.microsoft.com/en-us/azure/mysql/flexible-server/concepts-networking-public',
- apis: ['servers:listMysqlFlexibleServer'],
- realtime_triggers: ['microsoftdbformysql:flexibleservers:write','microsoftdbformysql:flexibleservers:delete'],
+ apis: ['servers:listMysqlFlexibleServer', 'firewallRules:listByFlexibleServerMysql'],
+ settings: {
+ mysql_flexible_server_allowed_ips: {
+ name: 'MySQL Flexible Server Allowed IPs',
+ description: 'Comma-separated list of customer defined IP addresses/ranges that are allowed to access MySQL flexible servers.',
+ regex: '((25[0-5]|2[0-4]|[01]??)(25[0-5]|2[0-4]|[01]??)(25[0-5]|2[0-4]|[01]??)(25[0-5]|2[0-4]|[01]??)(,\n|,?$))',
+ default: ''
+ }
+ },
+ realtime_triggers: ['microsoftdbformysql:flexibleservers:write', 'microsoftdbformysql:flexibleservers:firewallrules:write', 'microsoftdbformysql:flexibleservers:firewallrules:delete', 'microsoftdbformysql:flexibleservers:delete'],
run: function(cache, settings, callback) {
const results = [];
const source = {};
const locations = helpers.locations(settings.govcloud);
+ var config = {
+ mysql_flexible_server_allowed_ips: settings.mysql_flexible_server_allowed_ips || this.settings.mysql_flexible_server_allowed_ips.default
+ };
+
+ var allowedIps = [];
+ if (config.mysql_flexible_server_allowed_ips && config.mysql_flexible_server_allowed_ips.length > 0) {
+ allowedIps = config.mysql_flexible_server_allowed_ips.split(',').map(ip => ip.trim());
+ }
+ var checkAllowedIps = allowedIps.length > 0;
+
async.each(locations.servers, (location, rcb) => {
const servers = helpers.addSource(cache, source,
['servers', 'listMysqlFlexibleServer', location]);
if (!servers) return rcb();
-
+
if (servers.err || !servers.data) {
helpers.addResult(results, 3,
'Unable to query for MySQL flexible servers: ' + helpers.addError(servers), location);
@@ -35,22 +53,61 @@ module.exports = {
return rcb();
}
- for (var flexibleServer of servers.data) {
- if (!flexibleServer.id) continue;
-
- if (flexibleServer.properties &&
- flexibleServer.properties.network &&
- flexibleServer.properties.network.publicNetworkAccess &&
- flexibleServer.properties.network.publicNetworkAccess.toLowerCase() == 'enabled') {
- helpers.addResult(results, 2, 'MySQL flexible server is publicly accessible', location, flexibleServer.id);
+ servers.data.forEach(function(server) {
+ if (!server.id) return;
+
+
+ if (server.network &&
+ server.network.publicNetworkAccess &&
+ server.network.publicNetworkAccess.toLowerCase() === 'disabled') {
+ helpers.addResult(results, 0, 'MySQL Flexible Server is protected from outside traffic', location, server.id);
} else {
- helpers.addResult(results, 0, 'MySQL flexible server is not publicly accessible', location, flexibleServer.id);
+ const firewallRules = helpers.addSource(cache, source,
+ ['firewallRules', 'listByFlexibleServerMysql', location, server.id]);
+
+ if (!firewallRules || firewallRules.err || !firewallRules.data) {
+ helpers.addResult(results, 3,
+ 'Unable to query MySQL Flexible Server Firewall Rules: ' + helpers.addError(firewallRules), location, server.id);
+ } else {
+ if (!firewallRules.data.length) {
+ helpers.addResult(results, 0, 'No existing MySQL Flexible Server Firewall Rules found', location, server.id);
+ } else {
+ var publicAccess = false;
+
+ firewallRules.data.forEach(firewallRule => {
+ const startIpAddr = firewallRule['startIpAddress'];
+ const endIpAddr = firewallRule['endIpAddress'];
+
+ if (startIpAddr && startIpAddr.toString().indexOf('0.0.0.0') > -1) {
+ if (checkAllowedIps) {
+ if (endIpAddr && allowedIps.includes(endIpAddr.toString())) {
+ publicAccess = true;
+ }
+ } else {
+ if (endIpAddr && endIpAddr.toString() === '255.255.255.255') {
+ publicAccess = true;
+ } else if (endIpAddr && endIpAddr.toString() === '0.0.0.0') {
+ publicAccess = true;
+ }
+ }
+ }
+ });
+
+
+ if (publicAccess) {
+ helpers.addResult(results, 2, 'The MySQL flexible server is open to outside traffic', location, server.id);
+ } else {
+ helpers.addResult(results, 0, 'The MySQL flexible server is protected from outside traffic', location, server.id);
+ }
+ }
+ }
}
- }
+ });
+
rcb();
}, function() {
// Global checking goes here
callback(null, results, source);
});
}
-};
+};
\ No newline at end of file
diff --git a/plugins/azure/mysqlserver/mysqlFlexibleServerPublicAccess.spec.js b/plugins/azure/mysqlserver/mysqlFlexibleServerPublicAccess.spec.js
index 4f6ab7706..1345564c3 100644
--- a/plugins/azure/mysqlserver/mysqlFlexibleServerPublicAccess.spec.js
+++ b/plugins/azure/mysqlserver/mysqlFlexibleServerPublicAccess.spec.js
@@ -1,23 +1,89 @@
-var assert = require('assert');
var expect = require('chai').expect;
-var auth = require('./mysqlFlexibleServerPublicAccess');
+var mysqlFlexibleServerPublicAccess = require('./mysqlFlexibleServerPublicAccess');
-const createCache = (err, list) => {
- return {
+const listMysqlFlexibleServer = [
+ {
+ "id": "/subscriptions/12345/resourceGroups/Default/providers/Microsoft.DBforMySQL/flexibleServers/test-server",
+ "type": "Microsoft.DBforMySQL/flexibleServers",
+ "network": {
+ "publicNetworkAccess": "Disabled"
+ }
+ },
+ {
+ "id": "/subscriptions/12345/resourceGroups/Default/providers/Microsoft.DBforMySQL/flexibleServers/test-server-2",
+ "type": "Microsoft.DBforMySQL/flexibleServers",
+ "network": {
+ "publicNetworkAccess": "Enabled"
+ }
+ },
+ {
+ "id": "/subscriptions/12345/resourceGroups/Default/providers/Microsoft.DBforMySQL/flexibleServers/test-server-3",
+ "type": "Microsoft.DBforMySQL/flexibleServers",
+ "network": {
+ "publicNetworkAccess": "Disabled"
+ }
+ },
+ {
+ "id": "/subscriptions/12345/resourceGroups/Default/providers/Microsoft.DBforMySQL/flexibleServers/test-server-4",
+ "type": "Microsoft.DBforMySQL/flexibleServers"
+ }
+];
+
+const firewallRules = [
+ {
+ "id": "/subscriptions/123/resourceGroups/test/providers/Microsoft.DBforMySQL/flexibleServers/test-server/firewallRules/AllowAll",
+ "name": "AllowAll",
+ "startIpAddress": "0.0.0.0",
+ "endIpAddress": "255.255.255.255"
+ },
+ {
+ "id": "/subscriptions/123/resourceGroups/test/providers/Microsoft.DBforMySQL/flexibleServers/test-server/firewallRules/AllowAllAlt",
+ "name": "AllowAllAlt",
+ "startIpAddress": "0.0.0.0",
+ "endIpAddress": "0.0.0.0"
+ },
+ {
+ "id": "/subscriptions/123/resourceGroups/test/providers/Microsoft.DBforMySQL/flexibleServers/test-server/firewallRules/AllowedIP",
+ "name": "AllowedIP",
+ "startIpAddress": "192.168.1.1",
+ "endIpAddress": "192.168.1.1"
+ }
+];
+
+const createCache = (servers, rules, serversErr, rulesErr) => {
+ const cache = {
servers: {
listMysqlFlexibleServer: {
'eastus': {
- err: err,
- data: list
+ data: servers || [],
+ err: serversErr || null
}
}
+ },
+ firewallRules: {
+ listByFlexibleServerMysql: {
+ 'eastus': {}
+ }
}
+ };
+
+ if (servers && servers.length > 0) {
+ servers.forEach(server => {
+ if (server && server.id) {
+ cache.firewallRules.listByFlexibleServerMysql.eastus[server.id] = {
+ data: rules || [],
+ err: rulesErr || null
+ };
+ }
+ });
}
+
+ return cache;
};
describe('mysqlFlexibleServerPublicAccess', function() {
describe('run', function() {
- it('should PASS if no existing servers found', function(done) {
+ it('should give passing result if no SQL servers found', function(done) {
const callback = (err, results) => {
expect(results.length).to.equal(1);
expect(results[0].status).to.equal(0);
@@ -27,112 +93,134 @@ describe('mysqlFlexibleServerPublicAccess', function() {
};
const cache = createCache(
- null,
- [],
- {}
+ []
);
- auth.run(cache, {}, callback);
+ mysqlFlexibleServerPublicAccess.run(cache, {}, callback);
});
- it('should FAIL if MySQL server is not publicly accessible', function(done) {
+ it('should give passing result if no existing SQL Flexible Server Firewall Rules found', function(done) {
const callback = (err, results) => {
expect(results.length).to.equal(1);
expect(results[0].status).to.equal(0);
- expect(results[0].message).to.include('MySQL flexible server is not publicly accessible');
+ expect(results[0].message).to.include('No existing MySQL Flexible Server Firewall Rules found');
expect(results[0].region).to.equal('eastus');
done()
};
const cache = createCache(
- null,
- [
- {
- "id": "/subscriptions/12345/resourceGroups/Default/providers/Microsoft.DBforMySQL/flexibleServers/test-server",
- "type": "Microsoft.DBforMySQL/flexibleServers",
- "properties": {
- "administratorLogin": "test",
- "storage": {
- "storageSizeGB": 20,
- "iops": 360,
- "autoGrow": "Enabled",
- "autoIoScaling": "Enabled",
- "storageSku": "Premium_LRS",
- "logOnDisk": "Disabled"
- },
- "version": "5.7",
- "state": "Ready",
- "fullyQualifiedDomainName": "test-flexibleserverr-mysql.mysql.database.azure.com",
- "availabilityZone": "3",
- "replicationRole": "None",
- "replicaCapacity": 10,
- "network": {
- "publicNetworkAccess": "Disabled"
- },
- }
- }
- ]
+ [listMysqlFlexibleServer[1]],
+ []
+
);
- auth.run(cache, {}, callback);
+ mysqlFlexibleServerPublicAccess.run(cache, {}, callback);
});
- it('should FAIL if MySQL server is publicly accessible', function(done) {
+ it('should give passing result if SQL Server has private network access disabled', function(done) {
+ const callback = (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(0);
+ expect(results[0].message).to.include('MySQL Flexible Server is protected from outside traffic');
+ expect(results[0].region).to.equal('eastus');
+ done()
+ };
+
+ const cache = createCache(
+ [listMysqlFlexibleServer[0]],
+ []
+ );
+
+ mysqlFlexibleServerPublicAccess.run(cache, {}, callback);
+ });
+
+ it('should give failing result if SQL Server is open to outside traffic', function(done) {
const callback = (err, results) => {
expect(results.length).to.equal(1);
expect(results[0].status).to.equal(2);
- expect(results[0].message).to.include('MySQL flexible server is publicly accessible');
+ expect(results[0].message).to.include('The MySQL flexible server is open to outside traffic');
+ expect(results[0].region).to.equal('eastus');
+ done()
+ };
+
+ const cache = createCache(
+ [listMysqlFlexibleServer[1]],
+ [firewallRules[0]]
+ );
+
+ mysqlFlexibleServerPublicAccess.run(cache, {}, callback);
+ });
+
+ it('should give passing result if SQL Server firewall does not allow public access', function(done) {
+ const callback = (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(0);
+ expect(results[0].message).to.include('The MySQL flexible server is protected from outside traffic');
+ expect(results[0].region).to.equal('eastus');
+ done()
+ };
+
+ const cache = createCache(
+ [listMysqlFlexibleServer[1]],
+ [firewallRules[2]]
+ );
+
+ mysqlFlexibleServerPublicAccess.run(cache, {}, callback);
+ });
+
+ it('should give passing result if The SQL server is protected from outside traffic', function(done) {
+ const callback = (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(0);
+ expect(results[0].message).to.include('The MySQL flexible server is protected from outside traffic');
+ expect(results[0].region).to.equal('eastus');
+ done()
+ };
+
+ const cache = createCache(
+ [listMysqlFlexibleServer[1]],
+ [firewallRules[2]]
+ );
+
+ mysqlFlexibleServerPublicAccess.run(cache, {}, callback);
+ });
+
+ it('should give unknown result if unable to query for SQL servers', function(done) {
+ const callback = (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(3);
+ expect(results[0].message).to.include('Unable to query for MySQL flexible servers');
expect(results[0].region).to.equal('eastus');
done()
};
const cache = createCache(
null,
- [
- {
- "id": "/subscriptions/12345/resourceGroups/Default/providers/Microsoft.DBforMySQL/flexibleServers/test-server",
- "type": "Microsoft.DBforMySQL/flexibleServers",
- "properties": {
- "administratorLogin": "test",
- "storage": {
- "storageSizeGB": 20,
- "iops": 360,
- "autoGrow": "Enabled",
- "autoIoScaling": "Enabled",
- "storageSku": "Premium_LRS",
- "logOnDisk": "Disabled"
- },
- "version": "5.7",
- "state": "Ready",
- "fullyQualifiedDomainName": "test-flexibleserverr-mysql.mysql.database.azure.com",
- "availabilityZone": "3",
- "replicationRole": "None",
- "replicaCapacity": 10,
- "network": {
- "publicNetworkAccess": "Enabled"
- },
- }
- }
- ],
+ [],
+ { message: 'unable to query servers'}
+
);
- auth.run(cache, {}, callback);
+ mysqlFlexibleServerPublicAccess.run(cache, {}, callback);
});
- it('should UNKNOWN if unable to query for server', function(done) {
+ it('should give unknown result if Unable to query for server firewall rules', function(done) {
const callback = (err, results) => {
expect(results.length).to.equal(1);
expect(results[0].status).to.equal(3);
- expect(results[0].message).to.include('Unable to query for MySQL flexible servers: ');
+ expect(results[0].message).to.include('Unable to query MySQL Flexible Server Firewall Rules');
expect(results[0].region).to.equal('eastus');
done()
};
const cache = createCache(
- null, null
+ [listMysqlFlexibleServer[1]],
+ [],
+ null,
+ { message: 'Unable to query for server firewall rules'}
);
- auth.run(cache, {}, callback);
- })
+ mysqlFlexibleServerPublicAccess.run(cache, {}, callback);
+ });
})
})
\ No newline at end of file
diff --git a/plugins/azure/postgresqlserver/activeDirectoryAdminEnabled.js b/plugins/azure/postgresqlserver/activeDirectoryAdminEnabled.js
index 7e3a0bd27..debdec24c 100644
--- a/plugins/azure/postgresqlserver/activeDirectoryAdminEnabled.js
+++ b/plugins/azure/postgresqlserver/activeDirectoryAdminEnabled.js
@@ -2,13 +2,13 @@ const async = require('async');
const helpers = require('../../../helpers/azure');
module.exports = {
- title: 'Azure Active Directory Admin Configured',
+ title: 'Azure Entra ID Admin Configured',
category: 'PostgreSQL Server',
domain: 'Databases',
severity: 'Medium',
- description: 'Ensures that Active Directory admin is set up on all PostgreSQL servers.',
- more_info: 'Using Azure Active Directory authentication allows key rotation and permission management to be managed in one location for all servers. This can be done are configuring an Active Directory administrator.',
- recommended_action: 'Set up an Active Directory admin for PostgreSQL database servers.',
+ description: 'Ensures that Entra ID admin is set up on all PostgreSQL servers.',
+ more_info: 'Using Azure Entra ID authentication allows key rotation and permission management to be managed in one location for all servers. This can be done are configuring an Entra ID administrator.',
+ recommended_action: 'Set up an Entra ID admin for PostgreSQL database servers.',
link: 'https://learn.microsoft.com/en-us/azure/postgresql/howto-configure-sign-in-aad-authentication',
apis: ['servers:listPostgres', 'serverAdministrators:list'],
realtime_triggers: ['microsoftdbforpostgresql:servers:write','microsoftdbforpostgresql:servers:delete'],
@@ -42,25 +42,25 @@ module.exports = {
if (!serverAdministrators || serverAdministrators.err || !serverAdministrators.data) {
helpers.addResult(results, 3,
- 'Unable to query for Active Directory admins: ' + helpers.addError(serverAdministrators), location, postgresServer.id);
+ 'Unable to query for Entra ID admins: ' + helpers.addError(serverAdministrators), location, postgresServer.id);
} else {
if (!serverAdministrators.data.length) {
- helpers.addResult(results, 2, 'No Active Directory admin found for the server', location, postgresServer.id);
+ helpers.addResult(results, 2, 'No Entra ID admin found for the server', location, postgresServer.id);
} else {
- var adAdminEnabled = false;
+ var entraIdAdminEnabled = false;
serverAdministrators.data.forEach(serverAdministrator => {
if (serverAdministrator.name &&
serverAdministrator.name.toLowerCase() === 'activedirectory') {
- adAdminEnabled = true;
+ entraIdAdminEnabled = true;
}
});
- if (adAdminEnabled) {
+ if (entraIdAdminEnabled) {
helpers.addResult(results, 0,
- 'Active Directory admin is enabled on the PostgreSQL server', location, postgresServer.id);
+ 'Entra ID admin is enabled on the PostgreSQL server', location, postgresServer.id);
} else {
helpers.addResult(results, 2,
- 'Active Directory admin is not enabled on the PostgreSQL server', location, postgresServer.id);
+ 'Entra ID admin is not enabled on the PostgreSQL server', location, postgresServer.id);
}
}
}
diff --git a/plugins/azure/postgresqlserver/activeDirectoryAdminEnabled.spec.js b/plugins/azure/postgresqlserver/activeDirectoryAdminEnabled.spec.js
index 58847a3e5..da241b6f1 100644
--- a/plugins/azure/postgresqlserver/activeDirectoryAdminEnabled.spec.js
+++ b/plugins/azure/postgresqlserver/activeDirectoryAdminEnabled.spec.js
@@ -66,11 +66,11 @@ describe('activeDirectoryAdminEnabled', function() {
activeDirectoryAdminEnabled.run(cache, {}, callback);
})
- it('should give failing result if Active Directory admin is not enabled on the PostgreSQL server', function(done) {
+ it('should give failing result if Entra ID admin is not enabled on the PostgreSQL server', function(done) {
const callback = (err, results) => {
expect(results.length).to.equal(1);
expect(results[0].status).to.equal(2);
- expect(results[0].message).to.include('Active Directory admin is not enabled on the PostgreSQL server');
+ expect(results[0].message).to.include('Entra ID admin is not enabled on the PostgreSQL server');
expect(results[0].region).to.equal('eastus');
done()
};
@@ -84,11 +84,11 @@ describe('activeDirectoryAdminEnabled', function() {
activeDirectoryAdminEnabled.run(cache, {}, callback);
});
- it('should give failing result if No Active Directory admin found for the server', function(done) {
+ it('should give failing result if No Entra ID admin found for the server', function(done) {
const callback = (err, results) => {
expect(results.length).to.equal(1);
expect(results[0].status).to.equal(2);
- expect(results[0].message).to.include('No Active Directory admin found for the server');
+ expect(results[0].message).to.include('No Entra ID admin found for the server');
expect(results[0].region).to.equal('eastus');
done()
};
@@ -102,11 +102,11 @@ describe('activeDirectoryAdminEnabled', function() {
activeDirectoryAdminEnabled.run(cache, {}, callback);
});
- it('should give passing result if Active Directory admin is enabled on the PostgreSQL server', function(done) {
+ it('should give passing result if Entra ID admin is enabled on the PostgreSQL server', function(done) {
const callback = (err, results) => {
expect(results.length).to.equal(1);
expect(results[0].status).to.equal(0);
- expect(results[0].message).to.include('Active Directory admin is enabled on the PostgreSQL server');
+ expect(results[0].message).to.include('Entra ID admin is enabled on the PostgreSQL server');
expect(results[0].region).to.equal('eastus');
done()
};
diff --git a/plugins/azure/postgresqlserver/postgresqlFlexibleServerPublicAccess.js b/plugins/azure/postgresqlserver/postgresqlFlexibleServerPublicAccess.js
new file mode 100644
index 000000000..52b39bd27
--- /dev/null
+++ b/plugins/azure/postgresqlserver/postgresqlFlexibleServerPublicAccess.js
@@ -0,0 +1,106 @@
+var async = require('async');
+const helpers = require('../../../helpers/azure');
+
+module.exports = {
+ title: 'PostgreSQL Flexible Server Public Access',
+ category: 'PostgreSQL Server',
+ domain: 'Databases',
+ severity: 'High',
+ description: 'Ensures that PostgreSQL flexible servers do not allow public access',
+ more_info: 'Configuring public access for PostgreSQL flexible server instance allows the server to be accessible through public endpoint. PostgreSQL flexible server instances should not have a public endpoint and should only be accessed from within a VNET.',
+ recommended_action: 'Ensure that the firewall of each PostgreSQL flexible server is configured to prohibit traffic from the public address.',
+ link: 'https://learn.microsoft.com/en-us/azure/postgresql/flexible-server/concepts-firewall-rules',
+ apis: ['servers:listPostgresFlexibleServer', 'firewallRules:listByFlexibleServerPostgres'],
+ settings: {
+ server_firewall_end_ip: {
+ name: 'PostgreSQL Server Firewall Rule End IP',
+ description: 'Comma separated list of IP addresses which cannot be end IPs for firewall rule',
+ regex: '((25[0-5]|2[0-4]|[01]??)(25[0-5]|2[0-4]|[01]??)(25[0-5]|2[0-4]|[01]??)(25[0-5]|2[0-4]|[01]??)(,\n|,?$))',
+ default: ''
+ }
+ },
+ realtime_triggers: ['microsoftdbforpostgresql:flexibleservers:write', 'microsoftdbforpostgresql:flexibleservers:firewallrules:write', 'microsoftdbforpostgresql:flexibleservers:firewallrules:delete', 'microsoftdbforpostgresql:flexibleservers:delete'],
+
+ run: function(cache, settings, callback) {
+
+ var results = [];
+ var source = {};
+ var locations = helpers.locations(settings.govcloud);
+
+ var config = {
+ server_firewall_end_ip: settings.server_firewall_end_ip || this.settings.server_firewall_end_ip.default
+ };
+
+ var checkEndIp = (config.server_firewall_end_ip.length > 0);
+
+ async.each(locations.servers, function(location, rcb) {
+
+ var servers = helpers.addSource(cache, source,
+ ['servers', 'listPostgresFlexibleServer', location]);
+
+ if (!servers) return rcb();
+
+ if (servers.err || !servers.data) {
+ helpers.addResult(results, 3,
+ 'Unable to query for PostgreSQL flexible servers: ' + helpers.addError(servers), location);
+ return rcb();
+ }
+
+ if (!servers.data.length) {
+ helpers.addResult(results, 0, 'No existing PostgreSQL flexible servers found', location);
+ return rcb();
+ }
+
+ servers.data.forEach(function(server) {
+
+ if (server.network && server.network.publicNetworkAccess && server.network.publicNetworkAccess.toLowerCase() === 'disabled') {
+ helpers.addResult(results, 0, 'The PostgreSQL flexible server has public network access disabled', location, server.id);
+
+ } else {
+ const firewallRules = helpers.addSource(cache, source,
+ ['firewallRules', 'listByFlexibleServerPostgres', location, server.id]);
+
+ if (!firewallRules || firewallRules.err || !firewallRules.data) {
+ helpers.addResult(results, 3,
+ 'Unable to query PostgreSQL Flexible Server Firewall Rules: ' + helpers.addError(firewallRules), location, server.id);
+ } else {
+ if (!firewallRules.data.length) {
+ helpers.addResult(results, 0, 'No existing PostgreSQL Flexible Server Firewall Rules found', location, server.id);
+ } else {
+ var publicAccess = false;
+
+ firewallRules.data.forEach(firewallRule => {
+ const startIpAddr = firewallRule['startIpAddress'];
+ const endIpAddr = firewallRule['endIpAddress'];
+
+ if (startIpAddr && endIpAddr) {
+ if (checkEndIp) {
+ if (startIpAddr.toString().indexOf('0.0.0.0') > -1 &&
+ config.server_firewall_end_ip.includes(endIpAddr.toString())) {
+ publicAccess = true;
+ }
+ } else if (startIpAddr.toString() === '0.0.0.0' &&
+ (endIpAddr.toString() === '255.255.255.255' || endIpAddr.toString() === '0.0.0.0')) {
+ publicAccess = true;
+ }
+ }
+ });
+
+ if (publicAccess) {
+ helpers.addResult(results, 2, 'The PostgreSQL flexible server is open to outside traffic', location, server.id);
+ } else {
+ helpers.addResult(results, 0, 'The PostgreSQL flexible server is protected from outside traffic', location, server.id);
+ }
+ }
+ }
+
+ }
+ });
+
+ rcb();
+ }, function() {
+ // Global checking goes here
+ callback(null, results, source);
+ });
+ }
+};
\ No newline at end of file
diff --git a/plugins/azure/postgresqlserver/postgresqlFlexibleServerPublicAccess.spec.js b/plugins/azure/postgresqlserver/postgresqlFlexibleServerPublicAccess.spec.js
new file mode 100644
index 000000000..72101d942
--- /dev/null
+++ b/plugins/azure/postgresqlserver/postgresqlFlexibleServerPublicAccess.spec.js
@@ -0,0 +1,218 @@
+var expect = require('chai').expect;
+var postgresqlFlexibleServerPublicAccess = require('./postgresqlFlexibleServerPublicAccess');
+
+const listPostgresFlexibleServer = [
+ {
+ "id": "/subscriptions/12345/resourceGroups/Default/providers/Microsoft.DBforPostgreSQL/flexibleServers/test-server",
+ "type": "Microsoft.DBforPostgreSQL/flexibleServers",
+ "network": {
+ "publicNetworkAccess": "Disabled"
+ }
+ },
+ {
+ "id": "/subscriptions/12345/resourceGroups/Default/providers/Microsoft.DBforPostgreSQL/flexibleServers/test-server-2",
+ "type": "Microsoft.DBforPostgreSQL/flexibleServers",
+ "network": {
+ "publicNetworkAccess": "Enabled"
+ }
+ },
+ {
+ "id": "/subscriptions/12345/resourceGroups/Default/providers/Microsoft.DBforPostgreSQL/flexibleServers/test-server-3",
+ "type": "Microsoft.DBforPostgreSQL/flexibleServers"
+ }
+];
+
+const firewallRules = [
+ {
+ "id": "/subscriptions/123/resourceGroups/test/providers/Microsoft.DBforPostgreSQL/flexibleServers/test-server/firewallRules/AllowAll",
+ "name": "AllowAll",
+ "startIpAddress": "0.0.0.0",
+ "endIpAddress": "255.255.255.255"
+ },
+ {
+ "id": "/subscriptions/123/resourceGroups/test/providers/Microsoft.DBforPostgreSQL/flexibleServers/test-server/firewallRules/AllowAllAlt",
+ "name": "AllowAllAlt",
+ "startIpAddress": "0.0.0.0",
+ "endIpAddress": "0.0.0.0"
+ },
+ {
+ "id": "/subscriptions/123/resourceGroups/test/providers/Microsoft.DBforPostgreSQL/flexibleServers/test-server/firewallRules/AllowIPv6",
+ "name": "AllowIPv6",
+ "startIpAddress": "::",
+ "endIpAddress": "::"
+ },
+ {
+ "id": "/subscriptions/123/resourceGroups/test/providers/Microsoft.DBforPostgreSQL/flexibleServers/test-server/firewallRules/AllowIPv6Alt",
+ "name": "AllowIPv6Alt",
+ "startIpAddress": "::/0",
+ "endIpAddress": "::/0"
+ },
+ {
+ "id": "/subscriptions/123/resourceGroups/test/providers/Microsoft.DBforPostgreSQL/flexibleServers/test-server/firewallRules/RestrictedIP",
+ "name": "RestrictedIP",
+ "startIpAddress": "0.0.0.0",
+ "endIpAddress": "192.168.1.1"
+ }
+];
+
+const createCache = (servers, rules1) => {
+ const serverId1 = (servers && servers.length > 0) ? servers[0].id : null;
+
+ const cache = {
+ servers: {
+ listPostgresFlexibleServer: {
+ 'eastus': {
+ data: servers
+ }
+ }
+ },
+ firewallRules: {
+ listByFlexibleServerPostgres: {
+ 'eastus': {}
+ }
+ }
+ };
+
+ if (serverId1) {
+ cache.firewallRules.listByFlexibleServerPostgres.eastus[serverId1] = {
+ data: rules1 || []
+ };
+ }
+
+ return cache;
+};
+
+describe('postgresqlFlexibleServerPublicAccess', function() {
+ describe('run', function() {
+ it('should give passing result if no servers', function(done) {
+ const callback = (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(0);
+ expect(results[0].message).to.include('No existing PostgreSQL flexible servers found');
+ expect(results[0].region).to.equal('eastus');
+ done();
+ };
+
+ const cache = createCache([]);
+ postgresqlFlexibleServerPublicAccess.run(cache, {}, callback);
+ });
+
+ it('should give passing result if server has public network access disabled', function(done) {
+ const callback = (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(0);
+ expect(results[0].message).to.include('The PostgreSQL flexible server has public network access disabled');
+ expect(results[0].region).to.equal('eastus');
+ done();
+ };
+
+ const cache = createCache([listPostgresFlexibleServer[0]]);
+ postgresqlFlexibleServerPublicAccess.run(cache, {}, callback);
+ });
+
+ it('should give passing result if server has public access enabled but no firewall rules', function(done) {
+ const callback = (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(0);
+ expect(results[0].message).to.include('No existing PostgreSQL Flexible Server Firewall Rules found');
+ expect(results[0].region).to.equal('eastus');
+ done();
+ };
+
+ const cache = createCache([listPostgresFlexibleServer[1]], []);
+ postgresqlFlexibleServerPublicAccess.run(cache, {}, callback);
+ });
+
+ it('should give failing result if server has firewall rule with restricted end IP', function(done) {
+ const callback = (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(2);
+ expect(results[0].message).to.include('The PostgreSQL flexible server is open to outside traffic');
+ expect(results[0].region).to.equal('eastus');
+ done();
+ };
+
+ const cache = createCache([listPostgresFlexibleServer[1]], [firewallRules[4]]);
+ postgresqlFlexibleServerPublicAccess.run(cache, {server_firewall_end_ip: '192.168.1.1'}, callback);
+ });
+
+ it('should give failing result if server has firewall rule allowing 0.0.0.0/0 access (full range)', function(done) {
+ const callback = (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(2);
+ expect(results[0].message).to.include('The PostgreSQL flexible server is open to outside traffic');
+ expect(results[0].region).to.equal('eastus');
+ done();
+ };
+
+ const cache = createCache([listPostgresFlexibleServer[1]], [firewallRules[0]]);
+ postgresqlFlexibleServerPublicAccess.run(cache, {}, callback);
+ });
+
+ it('should give failing result if server has firewall rule allowing 0.0.0.0/0 access (0.0.0.0-0.0.0.0)', function(done) {
+ const callback = (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(2);
+ expect(results[0].message).to.include('The PostgreSQL flexible server is open to outside traffic');
+ expect(results[0].region).to.equal('eastus');
+ done();
+ };
+
+ const cache = createCache([listPostgresFlexibleServer[1]], [firewallRules[1]]);
+ postgresqlFlexibleServerPublicAccess.run(cache, {}, callback);
+ });
+
+ it('should give unknown result if unable to query for PostgreSQL Servers', function(done) {
+ const callback = (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(3);
+ expect(results[0].message).to.include('Unable to query for PostgreSQL flexible servers');
+ expect(results[0].region).to.equal('eastus');
+ done();
+ };
+
+ const cache = {
+ servers: {
+ listPostgresFlexibleServer: {
+ 'eastus': {
+ err: 'Error querying servers'
+ }
+ }
+ }
+ };
+
+ postgresqlFlexibleServerPublicAccess.run(cache, {}, callback);
+ });
+
+ it('should give unknown result if unable to query firewall rules', function(done) {
+ const callback = (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(3);
+ expect(results[0].message).to.include('Unable to query PostgreSQL Flexible Server Firewall Rules');
+ expect(results[0].region).to.equal('eastus');
+ done();
+ };
+
+ const cache = {
+ servers: {
+ listPostgresFlexibleServer: {
+ 'eastus': {
+ data: [listPostgresFlexibleServer[1]]
+ }
+ }
+ },
+ firewallRules: {
+ listByFlexibleServerPostgres: {
+ 'eastus': {
+ [listPostgresFlexibleServer[1].id]: {
+ err: 'Error querying firewall rules'
+ }
+ }
+ }
+ }
+ };
+
+ postgresqlFlexibleServerPublicAccess.run(cache, {}, callback);
+ });
+ });
+});
\ No newline at end of file
diff --git a/plugins/azure/postgresqlserver/postgresqlServerPublicAccess.js b/plugins/azure/postgresqlserver/postgresqlServerPublicAccess.js
new file mode 100644
index 000000000..4e8f89633
--- /dev/null
+++ b/plugins/azure/postgresqlserver/postgresqlServerPublicAccess.js
@@ -0,0 +1,109 @@
+const async = require('async');
+const helpers = require('../../../helpers/azure');
+
+module.exports = {
+ title: 'PostgreSQL Server Public Access',
+ category: 'PostgreSQL Server',
+ domain: 'Databases',
+ severity: 'High',
+ description: 'Ensures that PostgreSQL servers do not allow public access',
+ more_info: 'Configuring public access for PostgreSQL server instance allows the server to be accessible through public endpoint. PostgreSQL server server instances should not have a public endpoint and should only be accessed from within a VNET.',
+ recommended_action: 'Ensure that the firewall of each PostgreSQL server is configured to prohibit traffic from the public address.',
+ link: 'https://learn.microsoft.com/en-us/azure/postgresql/concepts-firewall-rules',
+ apis: ['servers:listPostgres', 'firewallRules:listByServerPostgres'],
+ settings: {
+ postgresql_server_allowed_ips: {
+ name: 'PostgreSQL Server Allowed IPs',
+ description: 'Comma-separated list of customer defined IP addresses/ranges that are allowed to access PostgreSQL servers',
+ regex: '((25[0-5]|2[0-4]|[01]??)(25[0-5]|2[0-4]|[01]??)(25[0-5]|2[0-4]|[01]??)(25[0-5]|2[0-4]|[01]??)(,\n|,?$))',
+ default: ''
+ }
+ },
+ realtime_triggers: ['microsoftdbforpostgresql:servers:write', 'microsoftdbforpostgresql:servers:firewallrules:write', 'microsoftdbforpostgresql:servers:firewallrules:delete', 'microsoftdbforpostgresql:servers:delete'],
+
+ run: function(cache, settings, callback) {
+ const results = [];
+ const source = {};
+ const locations = helpers.locations(settings.govcloud);
+ var config = {
+ postgresql_server_allowed_ips: settings.postgresql_server_allowed_ips || this.settings.postgresql_server_allowed_ips.default
+ };
+
+ var allowedIps = [];
+ if (config.postgresql_server_allowed_ips && config.postgresql_server_allowed_ips.length > 0) {
+ allowedIps = config.postgresql_server_allowed_ips.split(',').map(ip => ip.trim());
+ }
+ var checkAllowedIps = allowedIps.length > 0;
+
+ async.each(locations.servers, (location, rcb) => {
+ const servers = helpers.addSource(cache, source,
+ ['servers', 'listPostgres', location]);
+
+ if (!servers) return rcb();
+
+ if (servers.err || !servers.data) {
+ helpers.addResult(results, 3,
+ 'Unable to query for PostgreSQL servers: ' + helpers.addError(servers), location);
+ return rcb();
+ }
+
+ if (!servers.data.length) {
+ helpers.addResult(results, 0, 'No existing PostgreSQL servers found', location);
+ return rcb();
+ }
+
+ servers.data.forEach(function(server) {
+ if (!server.id) return;
+
+ if (server.publicNetworkAccess && server.publicNetworkAccess.toLowerCase() === 'disabled') {
+ helpers.addResult(results, 0, 'The PostgreSQL server has public network access disabled', location, server.id);
+ } else {
+ const firewallRules = helpers.addSource(cache, source,
+ ['firewallRules', 'listByServerPostgres', location, server.id]);
+
+ if (!firewallRules || firewallRules.err || !firewallRules.data) {
+ helpers.addResult(results, 3,
+ 'Unable to query PostgreSQL Server Firewall Rules: ' + helpers.addError(firewallRules), location, server.id);
+ } else {
+ if (!firewallRules.data.length) {
+ helpers.addResult(results, 0, 'No existing PostgreSQL Server Firewall Rules found', location, server.id);
+ } else {
+ var publicAccess = false;
+
+ firewallRules.data.forEach(firewallRule => {
+ const startIpAddr = firewallRule['startIpAddress'];
+ const endIpAddr = firewallRule['endIpAddress'];
+
+ if (startIpAddr && startIpAddr.toString().indexOf('0.0.0.0') > -1) {
+ if (checkAllowedIps) {
+ if (endIpAddr && allowedIps.includes(endIpAddr.toString())) {
+ publicAccess = true;
+ }
+ } else {
+ if (endIpAddr && endIpAddr.toString() === '255.255.255.255') {
+ publicAccess = true;
+ } else if (endIpAddr && endIpAddr.toString() === '0.0.0.0') {
+ publicAccess = true;
+ }
+ }
+ }
+ });
+
+ if (publicAccess) {
+ helpers.addResult(results, 2, 'The PostgreSQL server is open to outside traffic', location, server.id);
+ } else {
+ helpers.addResult(results, 0, 'The PostgreSQL server is protected from outside traffic', location, server.id);
+ }
+ }
+ }
+ }
+
+ });
+
+ rcb();
+ }, function() {
+ // Global checking goes here
+ callback(null, results, source);
+ });
+ }
+};
\ No newline at end of file
diff --git a/plugins/azure/postgresqlserver/postgresqlServerPublicAccess.spec.js b/plugins/azure/postgresqlserver/postgresqlServerPublicAccess.spec.js
new file mode 100644
index 000000000..e8a9ab94b
--- /dev/null
+++ b/plugins/azure/postgresqlserver/postgresqlServerPublicAccess.spec.js
@@ -0,0 +1,298 @@
+var expect = require('chai').expect;
+var postgresqlServerPublicAccess = require('./postgresqlServerPublicAccess');
+
+const listPostgresServer = [
+ {
+ "id": "/subscriptions/12345/resourceGroups/Default/providers/Microsoft.DBforPostgreSQL/servers/test-server",
+ "type": "Microsoft.DBforPostgreSQL/servers",
+ "publicNetworkAccess": "Disabled"
+ },
+ {
+ "id": "/subscriptions/12345/resourceGroups/Default/providers/Microsoft.DBforPostgreSQL/servers/test-server-2",
+ "type": "Microsoft.DBforPostgreSQL/servers",
+ "publicNetworkAccess": "Enabled"
+ },
+ {
+ "id": "/subscriptions/12345/resourceGroups/Default/providers/Microsoft.DBforPostgreSQL/servers/test-server-3",
+ "type": "Microsoft.DBforPostgreSQL/servers"
+ }
+];
+
+const firewallRules = [
+ {
+ "id": "/subscriptions/12345/resourceGroups/Default/providers/Microsoft.DBforPostgreSQL/servers/test-server/firewallRules/TestRule",
+ "name": "TestRule",
+ "type": "Microsoft.DBforPostgreSQL/servers/firewallRules",
+ "startIpAddress": "192.168.1.1",
+ "endIpAddress": "192.168.1.10"
+ },
+ {
+ "id": "/subscriptions/12345/resourceGroups/Default/providers/Microsoft.DBforPostgreSQL/servers/test-server/firewallRules/AllowAll",
+ "name": "AllowAll",
+ "type": "Microsoft.DBforPostgreSQL/servers/firewallRules",
+ "startIpAddress": "0.0.0.0",
+ "endIpAddress": "255.255.255.255"
+ },
+ {
+ "id": "/subscriptions/12345/resourceGroups/Default/providers/Microsoft.DBforPostgreSQL/servers/test-server/firewallRules/AllowAllAlt",
+ "name": "AllowAllAlt",
+ "type": "Microsoft.DBforPostgreSQL/servers/firewallRules",
+ "startIpAddress": "0.0.0.0",
+ "endIpAddress": "0.0.0.0"
+ },
+ {
+ "id": "/subscriptions/12345/resourceGroups/Default/providers/Microsoft.DBforPostgreSQL/servers/test-server/firewallRules/CustomerIP",
+ "name": "CustomerIP",
+ "type": "Microsoft.DBforPostgreSQL/servers/firewallRules",
+ "startIpAddress": "10.0.0.1",
+ "endIpAddress": "10.0.0.1"
+ },
+ {
+ "id": "/subscriptions/12345/resourceGroups/Default/providers/Microsoft.DBforPostgreSQL/servers/test-server/firewallRules/AllowAllWindowsAzureIPs",
+ "name": "AllowAllWindowsAzureIPs",
+ "type": "Microsoft.DBforPostgreSQL/servers/firewallRules",
+ "startIpAddress": "0.0.0.0",
+ "endIpAddress": "0.0.0.0"
+ },
+ {
+ "id": "/subscriptions/12345/resourceGroups/Default/providers/Microsoft.DBforPostgreSQL/servers/test-server/firewallRules/CustomerDefinedRule",
+ "name": "CustomerDefinedRule",
+ "type": "Microsoft.DBforPostgreSQL/servers/firewallRules",
+ "startIpAddress": "0.0.0.0",
+ "endIpAddress": "10.0.0.1"
+ }
+];
+
+const createCache = (servers, rules1, rules2) => {
+ const serverId1 = (servers && servers.length > 0) ? servers[0].id : null;
+ const serverId2 = (servers && servers.length > 1) ? servers[1].id : null;
+
+ const cache = {
+ servers: {
+ listPostgres: {
+ 'eastus': {
+ data: servers
+ }
+ }
+ },
+ firewallRules: {
+ listByServerPostgres: {
+ 'eastus': {}
+ }
+ }
+ };
+
+ if (serverId1) {
+ cache.firewallRules.listByServerPostgres.eastus[serverId1] = {
+ data: rules1 || []
+ };
+ }
+
+ if (serverId2) {
+ cache.firewallRules.listByServerPostgres.eastus[serverId2] = {
+ data: rules2 || []
+ };
+ }
+
+ return cache;
+};
+
+describe('postgresqlServerPublicAccess', function() {
+ describe('run', function() {
+ it('should give passing result if no servers', function(done) {
+ const callback = (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(0);
+ expect(results[0].message).to.include('No existing PostgreSQL servers found');
+ expect(results[0].region).to.equal('eastus');
+ done();
+ };
+
+ const cache = createCache([]);
+ postgresqlServerPublicAccess.run(cache, {}, callback);
+ });
+
+ it('should give passing result if server has public network access disabled', function(done) {
+ const callback = (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(0);
+ expect(results[0].message).to.include('The PostgreSQL server has public network access disabled');
+ expect(results[0].region).to.equal('eastus');
+ done();
+ };
+
+ const cache = createCache([listPostgresServer[0]]);
+ postgresqlServerPublicAccess.run(cache, {}, callback);
+ });
+
+ it('should give passing result if server has public access enabled but no firewall rules', function(done) {
+ const callback = (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(0);
+ expect(results[0].message).to.include('No existing PostgreSQL Server Firewall Rules found');
+ expect(results[0].region).to.equal('eastus');
+ done();
+ };
+
+ const cache = createCache([listPostgresServer[1]], []);
+ postgresqlServerPublicAccess.run(cache, {}, callback);
+ });
+
+ it('should give passing result if server has public access enabled but restrictive firewall rules', function(done) {
+ const callback = (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(0);
+ expect(results[0].message).to.include('The PostgreSQL server is protected from outside traffic');
+ expect(results[0].region).to.equal('eastus');
+ done();
+ };
+
+ const cache = createCache([listPostgresServer[1]], [firewallRules[0]]);
+ postgresqlServerPublicAccess.run(cache, {}, callback);
+ });
+
+ it('should give failing result if server has firewall rule allowing 0.0.0.0/0 access (full range)', function(done) {
+ const callback = (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(2);
+ expect(results[0].message).to.include('The PostgreSQL server is open to outside traffic');
+ expect(results[0].region).to.equal('eastus');
+ done();
+ };
+
+ const cache = createCache([listPostgresServer[1]], [firewallRules[1]]);
+ postgresqlServerPublicAccess.run(cache, {}, callback);
+ });
+
+ it('should give failing result if server has firewall rule allowing 0.0.0.0/0 access (0.0.0.0-0.0.0.0)', function(done) {
+ const callback = (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(2);
+ expect(results[0].message).to.include('The PostgreSQL server is open to outside traffic');
+ expect(results[0].region).to.equal('eastus');
+ done();
+ };
+
+ const cache = createCache([listPostgresServer[1]], [firewallRules[2]]);
+ postgresqlServerPublicAccess.run(cache, {}, callback);
+ });
+
+ it('should give failing result if server has AllowAllWindowsAzureIPs firewall rule', function(done) {
+ const callback = (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(2);
+ expect(results[0].message).to.include('The PostgreSQL server is open to outside traffic');
+ expect(results[0].region).to.equal('eastus');
+ done();
+ };
+
+ const cache = createCache([listPostgresServer[1]], [firewallRules[4]]);
+ postgresqlServerPublicAccess.run(cache, {}, callback);
+ });
+
+ it('should give passing result if server has customer defined IP in firewall rules', function(done) {
+ const callback = (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(0);
+ expect(results[0].message).to.include('The PostgreSQL server is protected from outside traffic');
+ expect(results[0].region).to.equal('eastus');
+ done();
+ };
+
+ const cache = createCache([listPostgresServer[1]], [firewallRules[3]]);
+ postgresqlServerPublicAccess.run(cache, {}, callback);
+ });
+
+ it('should give unknown result if unable to query for PostgreSQL Servers', function(done) {
+ const callback = (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(3);
+ expect(results[0].message).to.include('Unable to query for PostgreSQL servers');
+ expect(results[0].region).to.equal('eastus');
+ done();
+ };
+
+ const cache = {
+ servers: {
+ listPostgres: {
+ 'eastus': {
+ err: 'Error querying servers'
+ }
+ }
+ }
+ };
+
+ postgresqlServerPublicAccess.run(cache, {}, callback);
+ });
+
+ it('should give unknown result if unable to query firewall rules', function(done) {
+ const callback = (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(3);
+ expect(results[0].message).to.include('Unable to query PostgreSQL Server Firewall Rules');
+ expect(results[0].region).to.equal('eastus');
+ done();
+ };
+
+ const cache = {
+ servers: {
+ listPostgres: {
+ 'eastus': {
+ data: [listPostgresServer[1]]
+ }
+ }
+ },
+ firewallRules: {
+ listByServerPostgres: {
+ 'eastus': {
+ [listPostgresServer[1].id]: {
+ err: 'Error querying firewall rules'
+ }
+ }
+ }
+ }
+ };
+
+ postgresqlServerPublicAccess.run(cache, {}, callback);
+ });
+
+ it('should check firewall rules if server has no publicNetworkAccess property', function(done) {
+ const callback = (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(0);
+ expect(results[0].message).to.include('The PostgreSQL server is protected from outside traffic');
+ expect(results[0].region).to.equal('eastus');
+ done();
+ };
+
+ const cache = createCache([listPostgresServer[2]], [firewallRules[0]]);
+ postgresqlServerPublicAccess.run(cache, {}, callback);
+ });
+
+ it('should give failing result if server has firewall rule with customer defined IP as end address', function(done) {
+ const callback = (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(2);
+ expect(results[0].message).to.include('The PostgreSQL server is open to outside traffic');
+ expect(results[0].region).to.equal('eastus');
+ done();
+ };
+
+ const cache = createCache([listPostgresServer[2]], [firewallRules[5]]);
+ postgresqlServerPublicAccess.run(cache, {postgresql_server_allowed_ips: '10.0.0.1'}, callback);
+ });
+
+ it('should give passing result if server has firewall rule with customer defined IP but not matching allowed IPs', function(done) {
+ const callback = (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(0);
+ expect(results[0].message).to.include('The PostgreSQL server is protected from outside traffic');
+ expect(results[0].region).to.equal('eastus');
+ done();
+ };
+
+ const cache = createCache([listPostgresServer[2]], [firewallRules[5]]);
+ postgresqlServerPublicAccess.run(cache, {postgresql_server_allowed_ips: '192.168.1.1'}, callback);
+ });
+ });
+});
\ No newline at end of file
diff --git a/plugins/azure/redisCache/redisCacheManagedIdentity.js b/plugins/azure/redisCache/redisCacheManagedIdentity.js
index 2f79fcbc2..f6a3947af 100644
--- a/plugins/azure/redisCache/redisCacheManagedIdentity.js
+++ b/plugins/azure/redisCache/redisCacheManagedIdentity.js
@@ -7,7 +7,7 @@ module.exports = {
domain: 'Databases',
severity: 'Medium',
description: 'Ensures that Azure Cache for Redis have managed identity enabled.',
- more_info: 'Enabling managed identities eliminate the need for developers having to manage credentials by providing an identity for the Azure resource in Azure AD and using it to obtain Azure Active Directory (Azure AD) tokens.',
+ more_info: 'Enabling managed identities eliminate the need for developers having to manage credentials by providing an identity for the Azure resource in Azure AD and using it to obtain Azure Entra ID tokens.',
recommended_action: 'Modify Azure Cache for Redis and add managed identity.',
link: 'https://learn.microsoft.com/en-us/azure/azure-cache-for-redis/cache-managed-identity#enable-managed-identity',
apis: ['redisCaches:listBySubscription'],
diff --git a/plugins/azure/servicebus/namespaceLocalAuth.js b/plugins/azure/servicebus/namespaceLocalAuth.js
index a37036392..c85df38ba 100644
--- a/plugins/azure/servicebus/namespaceLocalAuth.js
+++ b/plugins/azure/servicebus/namespaceLocalAuth.js
@@ -7,7 +7,7 @@ module.exports = {
domain: 'Application Integration',
severity: 'Low',
description: 'Ensures local authentication is disabled for Service Bus namespaces.',
- more_info: 'For enhanced security, centralized identity management, and seamless integration with Azure\'s authentication and authorization services, it is recommended to rely on Azure Active Directory (Azure AD) and disable local authentication (shared access policies) in Azure Service Bus namespaces.',
+ more_info: 'For enhanced security, centralized identity management, and seamless integration with Azure\'s authentication and authorization services, it is recommended to rely on Azure Entra ID and disable local authentication (shared access policies) in Azure Service Bus namespaces.',
recommended_action: 'Ensure that Azure Service Bus namespaces have local authentication disabled.',
link: 'https://learn.microsoft.com/en-us/azure/service-bus-messaging/disable-local-authentication',
apis: ['serviceBus:listNamespacesBySubscription'],
diff --git a/plugins/azure/servicebus/namespaceManagedIdentity.js b/plugins/azure/servicebus/namespaceManagedIdentity.js
index c4a3a8abd..a6018ac70 100644
--- a/plugins/azure/servicebus/namespaceManagedIdentity.js
+++ b/plugins/azure/servicebus/namespaceManagedIdentity.js
@@ -7,7 +7,7 @@ module.exports = {
domain: 'Application Integration',
severity: 'Medium',
description: 'Ensure that Azure Service Bus namespaces have managed identity enabled.',
- more_info: 'Enabling managed identities eliminate the need for developers having to manage credentials by providing an identity for the Azure resource in Azure AD and using it to obtain Azure Active Directory (Azure AD) tokens.',
+ more_info: 'Enabling managed identities eliminate the need for developers having to manage credentials by providing an identity for the Azure resource in Azure AD and using it to obtain Azure Entra ID tokens.',
recommended_action: 'Modify Service Bus namespace and enable managed identity.',
link: 'https://learn.microsoft.com/en-us/azure/service-bus-messaging/service-bus-managed-service-identity',
apis: ['serviceBus:listNamespacesBySubscription'],
diff --git a/plugins/azure/sqlserver/azureADAdminEnabled.js b/plugins/azure/sqlserver/azureADAdminEnabled.js
index 0d9282b95..9425656ab 100644
--- a/plugins/azure/sqlserver/azureADAdminEnabled.js
+++ b/plugins/azure/sqlserver/azureADAdminEnabled.js
@@ -2,13 +2,13 @@ var async = require('async');
var helpers = require('../../../helpers/azure');
module.exports = {
- title: 'Azure Active Directory Admin Enabled',
+ title: 'Azure Entra ID Admin Enabled',
category: 'SQL Server',
domain: 'Databases',
severity: 'Medium',
- description: 'Ensures that Active Directory admin is enabled on all SQL servers.',
- more_info: 'Enabling Active Directory admin allows users to manage account admins in a central location, allowing key rotation and permission management to be managed in one location for all servers and databases.',
- recommended_action: 'Ensure Azure Active Directory admin is enabled on all SQL servers.',
+ description: 'Ensures that Entra ID admin is enabled on all SQL servers.',
+ more_info: 'Enabling Entra ID admin allows users to manage account admins in a central location, allowing key rotation and permission management to be managed in one location for all servers and databases.',
+ recommended_action: 'Ensure Azure Entra ID admin is enabled on all SQL servers.',
link: 'https://learn.microsoft.com/en-us/azure/sql-database/sql-database-aad-authentication-configure',
apis: ['servers:listSql', 'serverAzureADAdministrators:listByServer'],
realtime_triggers: ['microsoftsql:servers:write', 'microsoftsql:servers:delete','microsoftsql:servers:administrators:write', 'microsoftsql:servers:administrators:delete'],
@@ -42,25 +42,25 @@ module.exports = {
if (!serverAzureADAdministrators || serverAzureADAdministrators.err || !serverAzureADAdministrators.data) {
helpers.addResult(results, 3,
- 'Unable to query for Active Directory admins: ' + helpers.addError(serverAzureADAdministrators), location, server.id);
+ 'Unable to query for Entra ID admins: ' + helpers.addError(serverAzureADAdministrators), location, server.id);
} else {
if (!serverAzureADAdministrators.data.length) {
- helpers.addResult(results, 2, 'Active Directory admin is not enabled on the server', location, server.id);
+ helpers.addResult(results, 2, 'Entra ID admin is not enabled on the server', location, server.id);
} else {
- var adAdminEnabled = false;
+ var entraIdAdminEnabled = false;
serverAzureADAdministrators.data.forEach(serverAzureADAdministrator => {
if (serverAzureADAdministrator.name &&
serverAzureADAdministrator.name.toLowerCase() === 'activedirectory') {
- adAdminEnabled = true;
+ entraIdAdminEnabled = true;
}
});
- if (adAdminEnabled) {
+ if (entraIdAdminEnabled) {
helpers.addResult(results, 0,
- 'Active Directory admin is enabled on the SQL server', location, server.id);
+ 'Entra ID admin is enabled on the SQL server', location, server.id);
} else {
helpers.addResult(results, 2,
- 'Active Directory admin is not enabled on the SQL server', location, server.id);
+ 'Entra ID admin is not enabled on the SQL server', location, server.id);
}
}
}
diff --git a/plugins/azure/sqlserver/azureADAdminEnabled.spec.js b/plugins/azure/sqlserver/azureADAdminEnabled.spec.js
index d6d69813c..026d4610b 100644
--- a/plugins/azure/sqlserver/azureADAdminEnabled.spec.js
+++ b/plugins/azure/sqlserver/azureADAdminEnabled.spec.js
@@ -44,7 +44,7 @@ describe('azureADAdminEnabled', function() {
const callback = (err, results) => {
expect(results.length).to.equal(1);
expect(results[0].status).to.equal(2);
- expect(results[0].message).to.include('Active Directory admin is not enabled on the server');
+ expect(results[0].message).to.include('Entra ID admin is not enabled on the server');
expect(results[0].region).to.equal('eastus');
done()
};
@@ -73,7 +73,7 @@ describe('azureADAdminEnabled', function() {
const callback = (err, results) => {
expect(results.length).to.equal(1);
expect(results[0].status).to.equal(0);
- expect(results[0].message).to.include('Active Directory admin is enabled on the SQL server');
+ expect(results[0].message).to.include('Entra ID admin is enabled on the SQL server');
expect(results[0].region).to.equal('eastus');
done()
};
diff --git a/plugins/azure/sqlserver/sqlServerManagedIdentity.js b/plugins/azure/sqlserver/sqlServerManagedIdentity.js
index 56683d330..0d5e6f55d 100644
--- a/plugins/azure/sqlserver/sqlServerManagedIdentity.js
+++ b/plugins/azure/sqlserver/sqlServerManagedIdentity.js
@@ -7,7 +7,7 @@ module.exports = {
domain: 'Databases',
severity: 'Medium',
description: 'Ensure that Azure SQL servers have managed identity enabled.',
- more_info: 'Enabling managed identities eliminate the need for developers having to manage credentials by providing an identity for the Azure resource in Azure AD and using it to obtain Azure Active Directory (Azure AD) tokens.',
+ more_info: 'Enabling managed identities eliminate the need for developers having to manage credentials by providing an identity for the Azure resource in Azure AD and using it to obtain Azure Entra ID tokens.',
recommended_action: 'Enable system or user-assigned managed identities for sql servers.',
link: 'https://learn.microsoft.com/en-us/azure/azure-sql/database/authentication-azure-ad-user-assigned-managed-identity?view=azuresql',
apis: ['servers:listSql'],
diff --git a/plugins/azure/storageaccounts/storageAccountPrivateEndpoint.js b/plugins/azure/storageaccounts/storageAccountPrivateEndpoint.js
index b5baa5df9..1150e02c5 100644
--- a/plugins/azure/storageaccounts/storageAccountPrivateEndpoint.js
+++ b/plugins/azure/storageaccounts/storageAccountPrivateEndpoint.js
@@ -6,17 +6,28 @@ module.exports = {
category: 'Storage Accounts',
domain: 'Storage',
severity: 'Medium',
- description: 'Ensure that Azure Storage accounts are accessible only through private endpoints.',
- more_info: 'Azure Private Endpoint is a network interface that connects you privately and securely to a service powered by Azure Private Link. Private Endpoint uses a private IP address from your VNet, effectively bringing the service such as Azure Storage Accounts into your VNet.',
- recommended_action: 'Modify storage accounts and configure private endpoints.',
+ description: 'Ensure that Azure Storage accounts are accessible only through private endpoints or have restricted public access.',
+ more_info: 'Azure Private Endpoint is a network interface that connects you privately and securely to a service powered by Azure Private Link. Private Endpoint uses a private IP address from your VNet, effectively bringing the service such as Azure Storage Accounts into your VNet. If private endpoints are not configured, ensure that public access is restricted to specific IP addresses or virtual networks.',
+ recommended_action: 'Modify storage accounts and configure private endpoints or restrict public access to specific networks.',
link: 'https://learn.microsoft.com/en-us/azure/storage/common/storage-private-endpoints',
apis: ['storageAccounts:list'],
realtime_triggers: ['microsoftstorage:storageaccounts:write', 'microsoftstorage:storageaccounts:delete', 'microsoftnetwork:privateendpoints:write', 'microsoftstorage:storageaccounts:privateendpointconnections:write'],
+ settings: {
+ check_selected_networks: {
+ name: 'Evaluate Selected Networks',
+ description: 'Checks if specific IP addresses or virtual networks are set to restrict Storage Account access when private endpoints are not configured.',
+ regex: '^(true|false)$',
+ default: false,
+ }
+ },
run: function(cache, settings, callback) {
var results = [];
var source = {};
var locations = helpers.locations(settings.govcloud);
+ let config = {
+ check_selected_networks: settings.check_selected_networks || this.settings.check_selected_networks.default
+ };
async.each(locations.storageAccounts, function(location, rcb) {
var storageAccount = helpers.addSource(cache, source,
@@ -41,7 +52,38 @@ module.exports = {
if (account.privateEndpointConnections && account.privateEndpointConnections.length){
helpers.addResult(results, 0, 'Private endpoints are configured for the storage account', location, account.id);
} else {
- helpers.addResult(results, 2, 'Private endpoints are not configured for the storage account', location, account.id);
+ // Check public network access when private endpoints are not configured
+ let isPublicAccessEnabled = (account.publicNetworkAccess && account.publicNetworkAccess.toLowerCase() === 'enabled') ||
+ (!account.publicNetworkAccess && account.networkAcls && account.networkAcls.defaultAction && account.networkAcls.defaultAction.toLowerCase() === 'allow');
+
+ if (isPublicAccessEnabled) {
+ if (config.check_selected_networks) {
+ let hasNetworkRestrictions = false;
+
+ if (account.networkAcls) {
+ // Check if default action is deny (meaning public access is restricted)
+ if (account.networkAcls.defaultAction && account.networkAcls.defaultAction.toLowerCase() === 'deny') {
+ hasNetworkRestrictions = true;
+ }
+
+ // Check if there are IP rules or virtual network rules configured
+ if ((account.networkAcls.ipRules && account.networkAcls.ipRules.length > 0) ||
+ (account.networkAcls.virtualNetworkRules && account.networkAcls.virtualNetworkRules.length > 0)) {
+ hasNetworkRestrictions = true;
+ }
+ }
+
+ if (hasNetworkRestrictions) {
+ helpers.addResult(results, 0, 'Storage account is not publicly accessible', location, account.id);
+ } else {
+ helpers.addResult(results, 2, 'Storage account is publicly accessible', location, account.id);
+ }
+ } else {
+ helpers.addResult(results, 2, 'Storage account is publicly accessible', location, account.id);
+ }
+ } else {
+ helpers.addResult(results, 0, 'Storage account is not publicly accessible', location, account.id);
+ }
}
}
diff --git a/plugins/azure/storageaccounts/storageAccountPrivateEndpoint.spec.js b/plugins/azure/storageaccounts/storageAccountPrivateEndpoint.spec.js
index b298d4807..ae72c29a3 100644
--- a/plugins/azure/storageaccounts/storageAccountPrivateEndpoint.spec.js
+++ b/plugins/azure/storageaccounts/storageAccountPrivateEndpoint.spec.js
@@ -3,25 +3,78 @@ var storageAccountPrivateEndpoint = require('./storageAccountPrivateEndpoint');
const storageAccounts = [
{
- 'id': '/subscriptions/123/resourceGroups/khulnasoft-resource-group/providers/Microsoft.Storage/storageAccounts/acc',
+ 'id': '/subscriptions/123/resourceGroups/khulnasoft-resource-group/providers/Microsoft.Storage/storageAccounts/acc1',
'location': 'eastus',
- 'name': 'acc',
+ 'name': 'acc1',
'tags': { 'key': 'value' },
"privateEndpointConnections": [
-
{
- "id": "/subscriptions/123/resourceGroups/khulnasoft-resource-group/providers/Microsoft.Storage/storageAccounts/acc/privateEndpointConnections/test.3d321801-7cb1-4586-afa7-deee7ab88744",
+ "id": "/subscriptions/123/resourceGroups/khulnasoft-resource-group/providers/Microsoft.Storage/storageAccounts/acc1/privateEndpointConnections/test.3d321801-7cb1-4586-afa7-deee7ab88744",
"name": "test.3d321801-7cb1-4586-afa7-deee7ab88744",
"type": "Microsoft.Storage/storageAccounts/privateEndpointConnections",
}
],
+ "publicNetworkAccess": "Enabled"
+ },
+ {
+ 'id': '/subscriptions/123/resourceGroups/khulnasoft-resource-group/providers/Microsoft.Storage/storageAccounts/acc2',
+ 'location': 'eastus',
+ 'name': 'acc2',
+ 'tags': {},
+ "privateEndpointConnections": [],
+ "publicNetworkAccess": "Disabled"
+ },
+ {
+ 'id': '/subscriptions/123/resourceGroups/khulnasoft-resource-group/providers/Microsoft.Storage/storageAccounts/acc3',
+ 'location': 'eastus',
+ 'name': 'acc3',
+ 'tags': {},
+ "privateEndpointConnections": [],
+ "publicNetworkAccess": "Enabled"
+ },
+ {
+ 'id': '/subscriptions/123/resourceGroups/khulnasoft-resource-group/providers/Microsoft.Storage/storageAccounts/acc4',
+ 'location': 'eastus',
+ 'name': 'acc4',
+ 'tags': {},
+ "privateEndpointConnections": [],
+ "publicNetworkAccess": "Enabled",
+ "networkAcls": {
+ "defaultAction": "Deny",
+ "ipRules": [
+ {
+ "value": "192.168.1.0/24",
+ "action": "Allow"
+ }
+ ],
+ "virtualNetworkRules": []
+ }
},
{
- 'id': '/subscriptions/123/resourceGroups/khulnasoft-resource-group/providers/Microsoft.Storage/storageAccounts/acc',
+ 'id': '/subscriptions/123/resourceGroups/khulnasoft-resource-group/providers/Microsoft.Storage/storageAccounts/acc5',
'location': 'eastus',
- 'name': 'acc',
+ 'name': 'acc5',
'tags': {},
- "privateEndpointConnections": []
+ "privateEndpointConnections": [],
+ "publicNetworkAccess": "Enabled",
+ "networkAcls": {
+ "defaultAction": "Allow",
+ "ipRules": [],
+ "virtualNetworkRules": []
+ }
+ },
+ {
+ 'id': '/subscriptions/123/resourceGroups/khulnasoft-resource-group/providers/Microsoft.Storage/storageAccounts/acc6',
+ 'location': 'eastus',
+ 'name': 'acc6',
+ 'tags': {},
+ "privateEndpointConnections": [],
+ // publicNetworkAccess property missing
+ "networkAcls": {
+ "defaultAction": "Allow",
+ "ipRules": [],
+ "virtualNetworkRules": []
+ }
}
];
@@ -82,12 +135,70 @@ describe('storageAccountPrivateEndpoint', function() {
});
});
- it('should give failing result if no private endpoint', function(done) {
+ it('should give passing result if public network access is disabled', function(done) {
const cache = createCache([storageAccounts[1]]);
+ storageAccountPrivateEndpoint.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(0);
+ expect(results[0].message).to.include('Storage account is not publicly accessible');
+ expect(results[0].region).to.equal('eastus');
+ done();
+ });
+ });
+
+ it('should give failing result if public network access is enabled without network restrictions', function(done) {
+ const cache = createCache([storageAccounts[2]]);
+ storageAccountPrivateEndpoint.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(2);
+ expect(results[0].message).to.include('Storage account is publicly accessible');
+ expect(results[0].region).to.equal('eastus');
+ done();
+ });
+ });
+
+ it('should give passing result if public network access is enabled with network restrictions when check_selected_networks is true', function(done) {
+ const cache = createCache([storageAccounts[3]]);
+ const settings = { check_selected_networks: true };
+ storageAccountPrivateEndpoint.run(cache, settings, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(0);
+ expect(results[0].message).to.include('Storage account is not publicly accessible');
+ expect(results[0].region).to.equal('eastus');
+ done();
+ });
+ });
+
+ it('should give failing result if public network access is enabled without sufficient network restrictions when check_selected_networks is true', function(done) {
+ const cache = createCache([storageAccounts[4]]);
+ const settings = { check_selected_networks: true };
+ storageAccountPrivateEndpoint.run(cache, settings, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(2);
+ expect(results[0].message).to.include('Storage account is publicly accessible');
+ expect(results[0].region).to.equal('eastus');
+ done();
+ });
+ });
+
+ it('should give failing result if public network access is enabled regardless of network restrictions when check_selected_networks is false', function(done) {
+ const cache = createCache([storageAccounts[3]]);
+ const settings = { check_selected_networks: false };
+ storageAccountPrivateEndpoint.run(cache, settings, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(2);
+ expect(results[0].message).to.include('Storage account is publicly accessible');
+ expect(results[0].region).to.equal('eastus');
+ done();
+ });
+ });
+
+ it('should give failing result if publicNetworkAccess is missing but networkAcls defaultAction is Allow', function(done) {
+ const cache = createCache([storageAccounts[5]]);
storageAccountPrivateEndpoint.run(cache, {}, (err, results) => {
expect(results.length).to.equal(1);
expect(results[0].status).to.equal(2);
- expect(results[0].message).to.include('Private endpoints are not configured for the storage account');
+ expect(results[0].message).to.include('Storage account is publicly accessible');
expect(results[0].region).to.equal('eastus');
done();
});
diff --git a/plugins/azure/storageaccounts/storageAccountsAADEnabled.js b/plugins/azure/storageaccounts/storageAccountsAADEnabled.js
index 1684e8905..37bf4beca 100644
--- a/plugins/azure/storageaccounts/storageAccountsAADEnabled.js
+++ b/plugins/azure/storageaccounts/storageAccountsAADEnabled.js
@@ -2,12 +2,12 @@ var async = require('async');
var helpers = require('../../../helpers/azure/');
module.exports = {
- title: 'Storage Accounts AAD Enabled',
+ title: 'Storage Accounts Entra ID Enabled',
category: 'Storage Accounts',
domain: 'Storage',
severity: 'Medium',
description: 'Ensures that identity-based Directory Service for Azure File Authentication is enabled for all Azure Files',
- more_info: 'Enabling identity-based Authentication ensures that only the authorized Active Directory members can access or connect to the file shares, enforcing granular access control.',
+ more_info: 'Enabling identity-based Authentication ensures that only the authorized Entra ID members can access or connect to the file shares, enforcing granular access control.',
recommended_action: 'Ensure that identity-based Directory Service for Azure File Authentication is enabled for all Azure File Shares.',
link: 'https://learn.microsoft.com/en-us/azure/storage/files/storage-files-active-directory-overview',
apis: ['storageAccounts:list', 'fileShares:list'],
@@ -66,7 +66,7 @@ module.exports = {
location, storageAccount.id, custom);
} else {
if (storageAccount.enableAzureFilesAadIntegration) {
- helpers.addResult(results, 0, 'Storage Account is configured with AAD Authentication', location, storageAccount.id);
+ helpers.addResult(results, 0, 'Storage Account is configured with Entra ID Authentication', location, storageAccount.id);
} else if (config.storage_account_check_file_share) {
var fileShares = helpers.addSource(cache, source,
['fileShares', 'list', location, storageAccount.id]);
@@ -76,13 +76,13 @@ module.exports = {
'Unable to query for file shares: ' + helpers.addError(fileShares), location, storageAccount.id);
} else {
if (!fileShares.data.length) {
- helpers.addResult(results, 0, 'Storage Account is not configured with AAD Authentication but no file shares are present', location, storageAccount.id);
+ helpers.addResult(results, 0, 'Storage Account is not configured with Entra ID Authentication but no file shares are present', location, storageAccount.id);
} else {
- helpers.addResult(results, 2, 'Storage Account is not configured with AAD Authentication', location, storageAccount.id);
+ helpers.addResult(results, 2, 'Storage Account is not configured with Entra ID Authentication', location, storageAccount.id);
}
}
} else {
- helpers.addResult(results, 2, 'Storage Account is not configured with AAD Authentication', location, storageAccount.id);
+ helpers.addResult(results, 2, 'Storage Account is not configured with Entra ID Authentication', location, storageAccount.id);
}
}
diff --git a/plugins/azure/storageaccounts/storageAccountsAADEnabled.spec.js b/plugins/azure/storageaccounts/storageAccountsAADEnabled.spec.js
index 14a9ec935..3b7447e9e 100644
--- a/plugins/azure/storageaccounts/storageAccountsAADEnabled.spec.js
+++ b/plugins/azure/storageaccounts/storageAccountsAADEnabled.spec.js
@@ -41,11 +41,11 @@ describe('storageAccountsAADEnabled', function() {
auth.run(cache, {}, callback);
})
- it('should give failing result if storage account is not configured with aad authentication', function(done) {
+ it('should give failing result if storage account is not configured with Entra ID authentication', function(done) {
const callback = (err, results) => {
expect(results.length).to.equal(1);
expect(results[0].status).to.equal(2);
- expect(results[0].message).to.include('Storage Account is not configured with AAD Authentication');
+ expect(results[0].message).to.include('Storage Account is not configured with Entra ID Authentication');
expect(results[0].region).to.equal('eastus');
done()
};
@@ -114,11 +114,11 @@ describe('storageAccountsAADEnabled', function() {
auth.run(cache, {}, callback);
})
- it('should give passing result if storage account is not configured with aad authentication but no file shares', function(done) {
+ it('should give passing result if storage account is not configured with Entra ID authentication but no file shares', function(done) {
const callback = (err, results) => {
expect(results.length).to.equal(1);
expect(results[0].status).to.equal(0);
- expect(results[0].message).to.include('Storage Account is not configured with AAD Authentication but no file shares are present');
+ expect(results[0].message).to.include('Storage Account is not configured with Entra ID Authentication but no file shares are present');
expect(results[0].region).to.equal('eastus');
done()
};
@@ -183,7 +183,7 @@ describe('storageAccountsAADEnabled', function() {
const callback = (err, results) => {
expect(results.length).to.equal(1)
expect(results[0].status).to.equal(0)
- expect(results[0].message).to.include('Storage Account is configured with AAD Authentication');
+ expect(results[0].message).to.include('Storage Account is configured with Entra ID Authentication');
expect(results[0].region).to.equal('eastus')
done()
};
diff --git a/plugins/azure/synapse/synapseWorkspaceAdAuthEnabled.js b/plugins/azure/synapse/synapseWorkspaceAdAuthEnabled.js
index 80606b792..42a942f9d 100644
--- a/plugins/azure/synapse/synapseWorkspaceAdAuthEnabled.js
+++ b/plugins/azure/synapse/synapseWorkspaceAdAuthEnabled.js
@@ -2,13 +2,13 @@ var async = require('async');
var helpers = require('../../../helpers/azure');
module.exports = {
- title: 'Synapse Workspace AD Auth Enabled',
+ title: 'Synapse Workspace Entra ID Auth Enabled',
category: 'AI & ML',
domain: 'Machine Learning',
severity: 'Medium',
- description: 'Ensures that Azure Synapse workspace has Active Directory (AD) authentication enabled.',
- more_info: 'Enabling Azure Active Directory authentication for Synapse workspace enhances security by ensuring that only authenticated and authorized users can access resources and eliminating the need for password storage. This integration simplifies permission management and secure access.',
- recommended_action: 'Enable Active Directory (AD) authentication mode for all Synapse workspace.',
+ description: 'Ensures that Azure Synapse workspace has Entra ID authentication enabled.',
+ more_info: 'Enabling Azure Entra ID authentication for Synapse workspace enhances security by ensuring that only authenticated and authorized users can access resources and eliminating the need for password storage. This integration simplifies permission management and secure access.',
+ recommended_action: 'Enable Entra ID authentication mode for all Synapse workspace.',
link: 'https://learn.microsoft.com/en-us/azure/synapse-analytics/sql/active-directory-authentication',
apis: ['synapse:listWorkspaces'],
realtime_triggers: ['microsoftsynapse:workspaces:write','microsoftsynapse:workspaces:delete'],
@@ -39,12 +39,11 @@ module.exports = {
if (!workspace.id) continue;
if (workspace.azureADOnlyAuthentication) {
- helpers.addResult(results, 0, 'Synapse workspace has Active Directory authentication enabled', location, workspace.id);
+ helpers.addResult(results, 0, 'Synapse workspace has Entra ID authentication enabled', location, workspace.id);
} else {
- helpers.addResult(results, 2, 'Synapse workspace does not have Active Directory authentication enabled', location, workspace.id);
+ helpers.addResult(results, 2, 'Synapse workspace does not have Entra ID authentication enabled', location, workspace.id);
}
}
-
rcb();
}, function() {
// Global checking goes here
diff --git a/plugins/azure/synapse/synapseWorkspaceAdAuthEnabled.spec.js b/plugins/azure/synapse/synapseWorkspaceAdAuthEnabled.spec.js
index 0db95cd9e..674a19296 100644
--- a/plugins/azure/synapse/synapseWorkspaceAdAuthEnabled.spec.js
+++ b/plugins/azure/synapse/synapseWorkspaceAdAuthEnabled.spec.js
@@ -57,23 +57,23 @@ describe('synapseWorkspaceAdAuthEnabled', function () {
});
});
- it('should give passing result if workspace has AAD auth enabled', function (done) {
+ it('should give passing result if workspace has Entra ID auth enabled', function (done) {
const cache = createCache([workspaces[0]], null);
synapseWorkspaceAdAuthEnabled.run(cache, {}, (err, results) => {
expect(results.length).to.equal(1);
expect(results[0].status).to.equal(0);
- expect(results[0].message).to.include('Synapse workspace has Active Directory authentication enabled');
+ expect(results[0].message).to.include('Synapse workspace has Entra ID authentication enabled');
expect(results[0].region).to.equal('eastus');
done();
});
});
- it('should give failing result if workspace does not have AAD auth', function (done) {
+ it('should give failing result if workspace does not have Entra ID auth', function (done) {
const cache = createCache([workspaces[1]], null);
synapseWorkspaceAdAuthEnabled.run(cache, {}, (err, results) => {
expect(results.length).to.equal(1);
expect(results[0].status).to.equal(2);
- expect(results[0].message).to.include('Synapse workspace does not have Active Directory authentication enabled');
+ expect(results[0].message).to.include('Synapse workspace does not have Entra ID authentication enabled');
expect(results[0].region).to.equal('eastus');
done();
});
diff --git a/plugins/azure/synapse/workspaceManagedIdentity.js b/plugins/azure/synapse/workspaceManagedIdentity.js
index 51e45b6c0..4f46ce60e 100644
--- a/plugins/azure/synapse/workspaceManagedIdentity.js
+++ b/plugins/azure/synapse/workspaceManagedIdentity.js
@@ -7,7 +7,7 @@ module.exports = {
domain: 'Machine Learning',
severity: 'Medium',
description: 'Ensure that Azure Synapse workspace has managed identity enabled.',
- more_info: 'Enabling managed identities eliminate the need for developers having to manage credentials by providing an identity for the Azure resource in Azure AD and using it to obtain Azure Active Directory (Azure AD) tokens.',
+ more_info: 'Enabling managed identities eliminate the need for developers having to manage credentials by providing an identity for the Azure resource in Azure AD and using it to obtain Azure Entra ID tokens.',
recommended_action: 'Modify Synapse workspace and enable managed identity.',
link: 'https://learn.microsoft.com/en-us/azure/synapse-analytics/synapse-service-identity',
apis: ['synapse:listWorkspaces'],
diff --git a/plugins/azure/virtualmachines/vmAdAuthenticationEnabled.js b/plugins/azure/virtualmachines/vmAdAuthenticationEnabled.js
index 734562f99..c72e70bc7 100644
--- a/plugins/azure/virtualmachines/vmAdAuthenticationEnabled.js
+++ b/plugins/azure/virtualmachines/vmAdAuthenticationEnabled.js
@@ -2,14 +2,14 @@ var async = require('async');
var helpers = require('../../../helpers/azure');
module.exports = {
- title: 'VM Active Directory (AD) Authentication Enabled',
+ title: 'VM Entra ID Authentication Enabled',
category: 'Virtual Machines',
domain: 'Compute',
severity: 'Medium',
- description: 'Ensures that Azure Active Directory (AD) authentication is enabled for virtual machines.',
- more_info: 'Organizations can now improve the security of virtual machines (VMs) in Azure by integrating with Azure Active Directory (AD) authentication. Enabling Azure Active Directory (AD) authentication for Azure virtual machines (VMs) ensures access to VMs from one central point and simplifies access permission management.',
- recommended_action: 'Enable Azure Active Directory (AD) authentication for Azure virtual machines',
- link: 'https://learn.microsoft.com/en-us/azure/active-directory/devices/howto-vm-sign-in-azure-ad-windows',
+ description: 'Ensures that Azure Entra ID authentication is enabled for virtual machines.',
+ more_info: 'Organizations can now improve the security of virtual machines (VMs) in Azure by integrating with Azure Entra ID authentication. Enabling Azure Entra ID authentication for Azure virtual machines (VMs) ensures access to VMs from one central point and simplifies access permission management.',
+ recommended_action: 'Enable Azure Entra ID authentication for Azure virtual machines',
+ link: 'https://learn.microsoft.com/en-us/entra/identity/devices/howto-vm-sign-in-azure-ad-windows',
apis: ['virtualMachines:listAll', 'virtualMachineExtensions:list'],
realtime_triggers: ['microsoftcompute:virtualmachines:write', 'microsoftcompute:virtualmachines:delete', 'microsoftcompute:virtualmachines:extensions:write', 'microsoftcompute:virtualmachines:extensions:delete'],
@@ -44,7 +44,7 @@ module.exports = {
}
if (!virtualMachineExtensions.data.length) {
- helpers.addResult(results, 2, 'Azure Active Directory (AD) authentication is disabled for the virtual machine', location, virtualMachine.id);
+ helpers.addResult(results, 2, 'Azure Entra ID authentication is disabled for the virtual machine', location, virtualMachine.id);
return scb();
}
@@ -65,9 +65,9 @@ module.exports = {
(!windowsImg && virtualMachineExtension.name && virtualMachineExtension.name === 'AADSSHLoginForLinux')));
if (adEnabled) {
- helpers.addResult(results, 0, 'Azure Active Directory (AD) authentication is enabled for the virtual machine', location, virtualMachine.id);
+ helpers.addResult(results, 0, 'Azure Entra ID authentication is enabled for the virtual machine', location, virtualMachine.id);
} else {
- helpers.addResult(results, 2, 'Azure Active Directory (AD) authentication is disabled for the virtual machine', location, virtualMachine.id);
+ helpers.addResult(results, 2, 'Azure Entra ID authentication is disabled for the virtual machine', location, virtualMachine.id);
}
scb();
diff --git a/plugins/azure/virtualmachines/vmAdAuthenticationEnabled.spec.js b/plugins/azure/virtualmachines/vmAdAuthenticationEnabled.spec.js
index 7e57a92fe..3360c8861 100644
--- a/plugins/azure/virtualmachines/vmAdAuthenticationEnabled.spec.js
+++ b/plugins/azure/virtualmachines/vmAdAuthenticationEnabled.spec.js
@@ -63,7 +63,7 @@ const createCache = (virtualMachines, virtualMachineExtension) => {
};
};
-describe('adAuthenticationEnabled', function() {
+describe('vmAdAuthenticationEnabled', function() {
describe('run', function() {
it('should give passing result if no virtual machines', function(done) {
const cache = createCache([]);
@@ -92,7 +92,7 @@ describe('adAuthenticationEnabled', function() {
adAuthenticationEnabled.run(cache, {}, (err, results) => {
expect(results.length).to.equal(1);
expect(results[0].status).to.equal(2);
- expect(results[0].message).to.include('Azure Active Directory (AD) authentication is disabled for the virtual machine');
+ expect(results[0].message).to.include('Azure Entra ID authentication is disabled for the virtual machine');
expect(results[0].region).to.equal('eastus');
done();
});
@@ -109,34 +109,34 @@ describe('adAuthenticationEnabled', function() {
});
});
- it('should give passing result if Azure Active Directory (AD) authentication is enabled for the virtual machine for windows machine', function(done) {
+ it('should give passing result if Azure Entra ID authentication is enabled for the virtual machine for windows machine', function(done) {
const cache = createCache([virtualMachines[0]], [virtualMachineExtension[0]]);
adAuthenticationEnabled.run(cache, {}, (err, results) => {
expect(results.length).to.equal(1);
expect(results[0].status).to.equal(0);
- expect(results[0].message).to.include('Azure Active Directory (AD) authentication is enabled for the virtual machine');
+ expect(results[0].message).to.include('Azure Entra ID authentication is enabled for the virtual machine');
expect(results[0].region).to.equal('eastus');
done();
});
});
- it('should give passing result if Azure Active Directory (AD) authentication is enabled for the virtual machine for linux machine', function(done) {
+ it('should give passing result if Azure Entra ID authentication is enabled for the virtual machine for linux machine', function(done) {
const cache = createCache([virtualMachines[1]], [virtualMachineExtension[1]]);
adAuthenticationEnabled.run(cache, {}, (err, results) => {
expect(results.length).to.equal(1);
expect(results[0].status).to.equal(0);
- expect(results[0].message).to.include('Azure Active Directory (AD) authentication is enabled for the virtual machine');
+ expect(results[0].message).to.include('Azure Entra ID authentication is enabled for the virtual machine');
expect(results[0].region).to.equal('eastus');
done();
});
});
- it('should give failing result if Azure Active Directory (AD) authentication is disabled for the virtual machine', function(done) {
+ it('should give failing result if Azure Entra ID authentication is disabled for the virtual machine', function(done) {
const cache = createCache([virtualMachines[0]], [virtualMachineExtension[1]]);
adAuthenticationEnabled.run(cache, { vm_approved_extensions: 'Extension' }, (err, results) => {
expect(results.length).to.equal(1);
expect(results[0].status).to.equal(2);
- expect(results[0].message).to.include('Azure Active Directory (AD) authentication is disabled for the virtual machine');
+ expect(results[0].message).to.include('Azure Entra ID authentication is disabled for the virtual machine');
expect(results[0].region).to.equal('eastus');
done();
});
diff --git a/plugins/azure/virtualmachines/vmAgentEnabled.js b/plugins/azure/virtualmachines/vmAgentEnabled.js
index 7c0733938..6d96c80a4 100644
--- a/plugins/azure/virtualmachines/vmAgentEnabled.js
+++ b/plugins/azure/virtualmachines/vmAgentEnabled.js
@@ -8,9 +8,9 @@ module.exports = {
domain: 'Compute',
severity: 'Medium',
description: 'Ensures that the VM Agent is enabled for virtual machines',
- more_info: 'The VM agent must be enabled on Azure virtual machines in order to enable Azure Security Center for data collection.',
+ more_info: 'The VM agent must be enabled on Azure virtual machines in order to enable Azure Defender for data collection.',
recommended_action: 'Enable the VM agent for all virtual machines.',
- link: 'https://learn.microsoft.com/en-us/azure/security-center/security-center-enable-vm-agent',
+ link: 'https://learn.microsoft.com/en-us/azure/defender-for-cloud/enable-agentless-scanning-vms',
apis: ['virtualMachines:listAll'],
compliance: {
hipaa: 'HIPAA requires the logging of all activity ' +
diff --git a/plugins/azure/virtualmachines/vmEncryptionAtHost.js b/plugins/azure/virtualmachines/vmEncryptionAtHost.js
index 0c15c204a..a0e4e0bb9 100644
--- a/plugins/azure/virtualmachines/vmEncryptionAtHost.js
+++ b/plugins/azure/virtualmachines/vmEncryptionAtHost.js
@@ -6,7 +6,7 @@ module.exports = {
category: 'Virtual Machines',
domain: 'Compute',
severity: 'High',
- description: 'Ensures that encryption at host is enabled for Azure Virtual Machine disks.',
+ description: 'Encryption at host ensures that data on Azure Virtual Machine disks- including temporary and cached data- is encrypted at the physical host level before being persisted. This provides end-to-end encryption independent of the guest OS, and does not require Azure Disk Encryption (ADE). Enabling this setting can help meet certain compliance and data residency requirements.',
more_info: 'The data for temporary disk and OS/data disk caches is stored on the VM host. Enabling encryption at host for Azure Virtual Machine disks allows the data to be end-to-end encrypted, ensuring compliance and bolstering overall security with Azure Disk Encryption.',
recommended_action: 'Ensure that all Azure Virtual Machines have encryption at host enabled for disks.',
link: 'https://learn.microsoft.com/en-us/azure/virtual-machines/disk-encryption#encryption-at-host---end-to-end-encryption-for-your-vm-data',
diff --git a/plugins/azure/virtualmachines/vmNetworkExposure.js b/plugins/azure/virtualmachines/vmNetworkExposure.js
index ccb42e09e..fc237daa3 100644
--- a/plugins/azure/virtualmachines/vmNetworkExposure.js
+++ b/plugins/azure/virtualmachines/vmNetworkExposure.js
@@ -2,7 +2,7 @@ var async = require('async');
var helpers = require('../../../helpers/azure');
module.exports = {
- title: 'Network Exposure',
+ title: 'Internet Exposure',
category: 'Virtual Machines',
domain: 'Compute',
severity: 'Info',
@@ -118,7 +118,7 @@ module.exports = {
}
}
}
- let internetExposed = helpers.checkNetworkExposure(cache, source, vm_interfaces, securityGroups, location, results, loadBalancers);
+ let internetExposed = helpers.checkNetworkExposure(cache, source, vm_interfaces, securityGroups, location, results, {lbNames: loadBalancers}, virtualMachine);
if (internetExposed && internetExposed.length) {
helpers.addResult(results, 2, `VM is exposed to the internet through ${internetExposed}`, location, virtualMachine.id);
} else {
diff --git a/plugins/azure/virtualmachines/vmPrivilegeAnalysis.js b/plugins/azure/virtualmachines/vmPrivilegeAnalysis.js
new file mode 100644
index 000000000..e3818bc8a
--- /dev/null
+++ b/plugins/azure/virtualmachines/vmPrivilegeAnalysis.js
@@ -0,0 +1,23 @@
+module.exports = {
+ title: 'Privilege Analysis',
+ category: 'Virtual Machines',
+ domain: 'Compute',
+ severity: 'Info',
+ description: 'Ensures that no virtual machines in your Azure environment have excessive permissions.',
+ more_info: 'Virtual machines that use managed identities with excessive Azure AD permissions may pose security risks. It is a best practice to assign only the necessary permissions to the managed identities attached to virtual machines.',
+ link: 'https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/how-to-use-vm-token',
+ recommended_action: 'Review and restrict the Azure AD roles associated with managed identities used by virtual machines to follow the principle of least privilege.',
+ apis: [''],
+ realtime_triggers: [
+ 'Microsoft.Compute/virtualMachines/write',
+ 'Microsoft.Compute/virtualMachines/delete',
+ 'Microsoft.ManagedIdentity/userAssignedIdentities/assign/action',
+ ],
+
+ run: function(cache, settings, callback) {
+ var results = [];
+ var source = {};
+
+ callback(null, results, source);
+ },
+};
diff --git a/plugins/azure/virtualmachines/vmWindowsAntiMalwareExtension.js b/plugins/azure/virtualmachines/vmWindowsAntiMalwareExtension.js
index 9087dae11..64f4dadbb 100644
--- a/plugins/azure/virtualmachines/vmWindowsAntiMalwareExtension.js
+++ b/plugins/azure/virtualmachines/vmWindowsAntiMalwareExtension.js
@@ -1,6 +1,6 @@
var async = require('async');
-var helpers = require('../../../helpers/azure/');
+var helpers = require('../../../helpers/azure');
module.exports = {
title: 'VM Windows AntiMalware Extension',
@@ -54,7 +54,13 @@ module.exports = {
continue;
}
- let found = virtualMachineExtensions.data.find(vmExt => vmExt.name && vmExt.name.toLowerCase() === 'iaasantimalware');
+ let found = virtualMachineExtensions.data.find(vmExt =>
+ vmExt.name && (
+ vmExt.name.toLowerCase() === 'iaasantimalware' ||
+ vmExt.name.toLowerCase() === 'iaasantimalwareext' ||
+ vmExt.name.toLowerCase().includes('antimalware')
+ ) && vmExt.provisioningState === 'Succeeded'
+ );
if (found) {
helpers.addResult(results, 0, 'Windows Virtual Machine has IaaS Antimalware extension installed', location, virtualMachine.id);
diff --git a/plugins/azure/virtualmachines/vmWindowsAntiMalwareExtension.spec.js b/plugins/azure/virtualmachines/vmWindowsAntiMalwareExtension.spec.js
index 310685720..1f067a2e5 100644
--- a/plugins/azure/virtualmachines/vmWindowsAntiMalwareExtension.spec.js
+++ b/plugins/azure/virtualmachines/vmWindowsAntiMalwareExtension.spec.js
@@ -13,12 +13,26 @@ const virtualMachineExtension = [
{
'name': 'TestExtension',
'id': '/subscriptions/123/resourceGroups/KHULNASOFT-RESOURCE_GROUP/providers/Microsoft.Compute/virtualMachines/test-vm/extensions/TestExtension',
- 'type': 'Microsoft.Compute/virtualMachines/extensions'
+ 'type': 'Microsoft.Compute/virtualMachines/extensions',
+ 'provisioningState': 'Succeeded'
},
{
'name': 'IaaSAntimalware',
- 'id': '/subscriptions/123/resourceGroups/KHULNASOFT-RESOURCE_GROUP/providers/Microsoft.Compute/virtualMachines/test-vm/extensions/TestExtension',
- 'type': 'Microsoft.Compute/virtualMachines/extensions'
+ 'id': '/subscriptions/123/resourceGroups/KHULNASOFT-RESOURCE_GROUP/providers/Microsoft.Compute/virtualMachines/test-vm/extensions/IaaSAntimalware',
+ 'type': 'Microsoft.Compute/virtualMachines/extensions',
+ 'provisioningState': 'Succeeded'
+ },
+ {
+ 'name': 'IaaSAntiMalwareExt',
+ 'id': '/subscriptions/123/resourceGroups/KHULNASOFT-RESOURCE_GROUP/providers/Microsoft.Compute/virtualMachines/test-vm/extensions/IaaSAntiMalwareExt',
+ 'type': 'Microsoft.Compute/virtualMachines/extensions',
+ 'provisioningState': 'Succeeded'
+ },
+ {
+ 'name': 'IaaSAntimalware',
+ 'id': '/subscriptions/123/resourceGroups/KHULNASOFT-RESOURCE_GROUP/providers/Microsoft.Compute/virtualMachines/test-vm/extensions/IaaSAntimalware',
+ 'type': 'Microsoft.Compute/virtualMachines/extensions',
+ 'provisioningState': 'Failed'
}
];
@@ -104,6 +118,28 @@ describe('vmWindowsAntiMalwareExtension', function() {
});
});
+ it('should give passing result if windows vm has IaaSAntiMalwareExt extension installed', function(done) {
+ const cache = createCache([virtualMachines[0]], [virtualMachineExtension[2]]);
+ vmWindowsAntiMalwareExtension.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(0);
+ expect(results[0].message).to.include('Windows Virtual Machine has IaaS Antimalware extension installed');
+ expect(results[0].region).to.equal('eastus');
+ done();
+ });
+ });
+
+ it('should give failing result if antimalware extension has failed provisioning state', function(done) {
+ const cache = createCache([virtualMachines[0]], [virtualMachineExtension[3]]);
+ vmWindowsAntiMalwareExtension.run(cache, {}, (err, results) => {
+ expect(results.length).to.equal(1);
+ expect(results[0].status).to.equal(2);
+ expect(results[0].message).to.include('Windows Virtual Machine does not have IaaS Antimalware extension installed');
+ expect(results[0].region).to.equal('eastus');
+ done();
+ });
+ });
+
it('should give failing result if windows vm not have antimalware extension installed', function(done) {
const cache = createCache([virtualMachines[0]], [virtualMachineExtension[0]]);
vmWindowsAntiMalwareExtension.run(cache, {}, (err, results) => {
diff --git a/plugins/azure/virtualmachinescaleset/scaleSetAdAuthEnabled.js b/plugins/azure/virtualmachinescaleset/scaleSetAdAuthEnabled.js
index fb5bbdc84..70451d48e 100644
--- a/plugins/azure/virtualmachinescaleset/scaleSetAdAuthEnabled.js
+++ b/plugins/azure/virtualmachinescaleset/scaleSetAdAuthEnabled.js
@@ -2,13 +2,13 @@ const async = require('async');
const helpers = require('../../../helpers/azure');
module.exports = {
- title: 'Scale Sets AD Authentication Enabled',
+ title: 'Scale Sets Entra ID Authentication Enabled',
category: 'Virtual Machine Scale Set',
domain: 'Compute',
severity: 'Medium',
- description: 'Ensures that Azure Active Directory (AD) authentication is enabled for Virtual Machine Scale Sets.',
- more_info: 'Enabling Azure Active Directory (AD) authentication for VM Scale Sets ensures access from one central point and simplifies access permission management. It allows conditional access by using Role-Based Access Control (RBAC) policies, and enable MFA.',
- recommended_action: 'Enable Active Directory authentication for all Virtual Machines scale sets.',
+ description: 'Ensures that Azure Entra ID authentication is enabled for Virtual Machine Scale Sets.',
+ more_info: 'Enabling Azure Entra ID authentication for VM Scale Sets ensures access from one central point and simplifies access permission management. It allows conditional access by using Role-Based Access Control (RBAC) policies, and enable MFA.',
+ recommended_action: 'Enable Entra ID authentication for all Virtual Machines scale sets.',
link: 'https://learn.microsoft.com/en-us/entra/identity/devices/howto-vm-sign-in-azure-ad-linux',
apis: ['virtualMachineScaleSets:listAll'],
realtime_triggers: ['microsoftcompute:virtualmachinescalesets:write', 'microsoftcompute:virtualmachinescalesets:delete', 'microsoftcompute:virtualmachinescalesets:extensions:write', 'microsoftcompute:virtualmachinescalesets:extensions:delete'],
@@ -51,10 +51,10 @@ module.exports = {
if (adAuthentication) {
helpers.addResult(results, 0,
- 'Virtual Machine Scale Set has Active Directory authentication enabled', location, virtualMachineScaleSet.id);
+ 'Virtual Machine Scale Set has Entra ID authentication enabled', location, virtualMachineScaleSet.id);
} else {
helpers.addResult(results, 2,
- 'Virtual Machine Scale Set has Active Directory authentication disabled', location, virtualMachineScaleSet.id);
+ 'Virtual Machine Scale Set has Entra ID authentication disabled', location, virtualMachineScaleSet.id);
}
}
rcb();
diff --git a/plugins/azure/virtualmachinescaleset/scaleSetAdAuthEnabled.spec.js b/plugins/azure/virtualmachinescaleset/scaleSetAdAuthEnabled.spec.js
index 425c35166..7fdf14e2a 100644
--- a/plugins/azure/virtualmachinescaleset/scaleSetAdAuthEnabled.spec.js
+++ b/plugins/azure/virtualmachinescaleset/scaleSetAdAuthEnabled.spec.js
@@ -92,33 +92,33 @@ describe('scaleSetAdAuthEnabled', function() {
});
});
- it('should give passing result if linux Virtual Machine Scale Set has AD authentication enabled', function(done) {
+ it('should give passing result if linux Virtual Machine Scale Set has Entra ID authentication enabled', function(done) {
const cache = createCache([virtualMachineScaleSets[0]]);
scaleSetAdAuthEnabled.run(cache, {}, (err, results) => {
expect(results.length).to.equal(1);
expect(results[0].status).to.equal(0);
- expect(results[0].message).to.include('Virtual Machine Scale Set has Active Directory authentication enabled');
+ expect(results[0].message).to.include('Virtual Machine Scale Set has Entra ID authentication enabled');
expect(results[0].region).to.equal('eastus');
done();
});
});
- it('should give passing result if windows Virtual Machine Scale Set has AD authentication enabled', function(done) {
+ it('should give passing result if windows Virtual Machine Scale Set has Entra ID authentication enabled', function(done) {
const cache = createCache([virtualMachineScaleSets[1]]);
scaleSetAdAuthEnabled.run(cache, {}, (err, results) => {
expect(results.length).to.equal(1);
expect(results[0].status).to.equal(0);
- expect(results[0].message).to.include('Virtual Machine Scale Set has Active Directory authentication enabled');
+ expect(results[0].message).to.include('Virtual Machine Scale Set has Entra ID authentication enabled');
expect(results[0].region).to.equal('eastus');
done();
});
});
- it('should give failing result if Virtual Machine Scale Set has AD authentication disabled', function(done) {
+ it('should give failing result if Virtual Machine Scale Set has Entra ID authentication disabled', function(done) {
const cache = createCache([virtualMachineScaleSets[2]]);
scaleSetAdAuthEnabled.run(cache, {}, (err, results) => {
expect(results.length).to.equal(1);
expect(results[0].status).to.equal(2);
- expect(results[0].message).to.include('Virtual Machine Scale Set has Active Directory authentication disabled');
+ expect(results[0].message).to.include('Virtual Machine Scale Set has Entra ID authentication disabled');
expect(results[0].region).to.equal('eastus');
done();
});
diff --git a/plugins/azure/virtualmachinescaleset/vmssManagedIdentityEnabled.js b/plugins/azure/virtualmachinescaleset/vmssManagedIdentityEnabled.js
index cc109e3bf..4ddb27d33 100644
--- a/plugins/azure/virtualmachinescaleset/vmssManagedIdentityEnabled.js
+++ b/plugins/azure/virtualmachinescaleset/vmssManagedIdentityEnabled.js
@@ -7,7 +7,7 @@ module.exports = {
domain: 'Compute',
severity: 'Medium',
description: 'Ensures that Azure Virtual Machine Scale Sets have managed identity enabled.',
- more_info: 'Enabling managed identities eliminate the need for developers having to manage credentials by providing an identity for the Azure resource in Azure AD and using it to obtain Azure Active Directory (Azure AD) tokens.',
+ more_info: 'Enabling managed identities eliminate the need for developers having to manage credentials by providing an identity for the Azure resource in Azure AD and using it to obtain Azure Entra ID tokens.',
link: 'https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/qs-configure-portal-windows-vmss',
recommended_action: 'Modify VM Scale Set and enable managed identity.',
apis: ['virtualMachineScaleSets:listAll'],
diff --git a/plugins/google/cloudfunctions/cloudFunctionNetworkExposure.js b/plugins/google/cloudfunctions/cloudFunctionNetworkExposure.js
new file mode 100644
index 000000000..f05a4b4d4
--- /dev/null
+++ b/plugins/google/cloudfunctions/cloudFunctionNetworkExposure.js
@@ -0,0 +1,159 @@
+var async = require('async');
+var helpers = require('../../../helpers/google');
+
+module.exports = {
+ title: 'Internet Exposure',
+ category: 'Cloud Functions',
+ domain: 'Serverless',
+ severity: 'Info',
+ description: 'Ensures Cloud Functions are not publicly exposed to all inbound traffic.',
+ more_info: 'Cloud Functions should be properly secured using ingress settings and load balancer configurations to control which sources can invoke the function.',
+ link: 'https://cloud.google.com/functions/docs/networking/network-settings',
+ recommended_action: 'Modify the Cloud Function to restrict ingress settings and ensure load balancer and api gateway configurations are properly secured.',
+ apis: ['functions:list', 'urlMaps:list', 'targetHttpProxies:list', 'targetHttpsProxies:list',
+ 'forwardingRules:list', 'backendServices:list', 'apiGateways:list', 'api:list', 'apiConfigs:list', 'apiGateways:getIamPolicy'],
+ realtime_triggers: ['functions.CloudFunctionsService.UpdateFunction', 'functions.CloudFunctionsService.CreateFunction', 'functions.CloudFunctionsService.DeleteFunction',
+ 'compute.backendServices.insert', 'compute.backendServices.delete', 'compute.backendServices.patch', 'compute.instanceGroups.removeInstances', 'compute.urlMaps.insert', 'compute.urlMaps.delete', 'compute.urlMaps.update', 'compute.urlMaps.patch',
+ 'compute.targetHttpProxies.insert', 'compute.targetHttpProxies.delete', 'compute.targetHttpProxies.patch', 'compute.targetHttpsProxies.insert', 'compute.targetHttpsProxies.delete', 'compute.targetHttpsProxies.patch',
+ 'compute.forwardingRules.insert', 'compute.forwardingRules.delete', 'compute.forwardingRules.patch', 'apigateway.gateways.create', 'apigateway.gateways.update', 'apigateway.gateways.delete'
+ ],
+
+ run: function(cache, settings, callback) {
+ var results = [];
+ var source = {};
+ var regions = helpers.regions();
+
+ let projects = helpers.addSource(cache, source,
+ ['projects', 'get', 'global']);
+
+ if (!projects || projects.err || !projects.data || !projects.data.length) {
+ helpers.addResult(results, 3,
+ 'Unable to query for projects: ' + helpers.addError(projects), 'global', null, null, (projects) ? projects.err : null);
+ return callback(null, results, source);
+ }
+
+ let apiGateways = [], apis = [], apiConfigs = [];
+ for (let region of regions.apiGateways) {
+ var gateways = helpers.addSource(cache, source,
+ ['apiGateways', 'list', region]);
+
+ if (gateways && !gateways.err && gateways.data && gateways.data.length) {
+ apiGateways = apiGateways.concat(gateways.data);
+ }
+
+
+ var apiList = helpers.addSource(cache, source,
+ ['api', 'list', region]);
+
+ if (apiList && !apiList.err && apiList.data && apiList.data.length) {
+ apis = apis.concat(apiList.data);
+ }
+
+ var configs = helpers.addSource(cache, source,
+ ['apiConfigs', 'list', region]);
+
+ if (configs && !configs.err && configs.data && configs.data.length) {
+ apiConfigs = apiConfigs.concat(configs.data);
+ }
+ }
+
+ async.each(regions.functions, (region, rcb) => {
+ var functions = helpers.addSource(cache, source,
+ ['functions', 'list', region]);
+
+ if (!functions) return rcb();
+
+ if (functions.err || !functions.data) {
+ helpers.addResult(results, 3,
+ 'Unable to query for Google Cloud Functions: ' + helpers.addError(functions), region, null, null, functions.err);
+ return rcb();
+ }
+
+ if (!functions.data.length) {
+ helpers.addResult(results, 0, 'No Google Cloud functions found', region);
+ return rcb();
+ }
+
+ functions.data.forEach(func => {
+ if (!func.name) return;
+ let internetExposed = '';
+ if (func.ingressSettings && func.ingressSettings.toUpperCase() == 'ALLOW_ALL') {
+ internetExposed = 'public access';
+ } else if (func.ingressSettings && func.ingressSettings.toUpperCase() == 'ALLOW_INTERNAL_AND_GCLB') {
+ // only check load balancer flow if it allows traffic from LBs
+ let forwardingRules = [];
+ let networks = [];
+ let firewallRules = [];
+ forwardingRules = helpers.getForwardingRules(cache, source, region, func);
+ internetExposed = helpers.checkNetworkExposure(cache, source, networks, firewallRules, region, results, forwardingRules);
+
+ if (!internetExposed || !internetExposed.length) {
+ const gatewayPolicies = helpers.addSource(cache, source,
+ ['apiGateways', 'getIamPolicy', region]);
+
+ if (apiGateways && apiGateways.length && apiConfigs && apiConfigs.length) {
+ apiGateways.forEach(gateway => {
+ let isGatewayExposed = false;
+ if (!gateway.apiConfig || !gateway.defaultHostname) return;
+
+ const apiConfig = apiConfigs.find(config =>
+ gateway.apiConfig.includes(config.name));
+
+ if (!apiConfig) return;
+
+ if (apiConfig.openapiDocuments) {
+ const specs = apiConfig.openapiDocuments.map(doc =>
+ typeof doc === 'string' ? JSON.parse(doc) : doc);
+
+ const hasFunctionReference = specs.some(spec =>
+ JSON.stringify(spec).includes(func.httpsTrigger.url) ||
+ JSON.stringify(spec).includes(func.name)
+ );
+
+ if (!hasFunctionReference) return;
+
+ const gatewayPolicy = gatewayPolicies.data.find(policy =>
+ policy.parent && policy.parent.name === gateway.name);
+
+ if (gatewayPolicy && gatewayPolicy.bindings) {
+ const publicAccess = gatewayPolicy.bindings.some(binding =>
+ binding.members.includes('allUsers') ||
+ binding.members.includes('allAuthenticatedUsers'));
+ if (publicAccess) {
+ isGatewayExposed = true;
+ }
+ }
+
+ if (!apiConfig.securityDefinitions || !Object.keys(apiConfig.securityDefinitions).length ||
+ !apiConfig.security || !apiConfig.security.length) {
+ isGatewayExposed = true;
+ }
+
+
+ if (isGatewayExposed) {
+ internetExposed += internetExposed.length ? `, ag ${gateway.displayName}` : `ag ${gateway.displayName}`;
+ }
+ }
+ });
+ }
+ }
+
+ }
+
+ if (internetExposed && internetExposed.length) {
+ helpers.addResult(results, 2, `Cloud function is exposed to the internet through ${internetExposed}`, region, func.name);
+ } else {
+ helpers.addResult(results, 0, 'Cloud function is not exposed to the internet', region, func.name);
+ }
+
+
+ });
+
+ rcb();
+ }, function() {
+ callback(null, results, source);
+ });
+
+ }
+};
+
diff --git a/plugins/google/cloudfunctions/cloudFunctionsPrivilegeAnalysis.js b/plugins/google/cloudfunctions/cloudFunctionsPrivilegeAnalysis.js
new file mode 100644
index 000000000..0913ea859
--- /dev/null
+++ b/plugins/google/cloudfunctions/cloudFunctionsPrivilegeAnalysis.js
@@ -0,0 +1,23 @@
+module.exports = {
+ title: 'Privilege Analysis',
+ category: 'Cloud Functions',
+ domain: 'Cloud Functions',
+ severity: 'Info',
+ description: 'Ensures that no Cloud Functions in your cloud environment have excessive permissions.',
+ more_info: 'Cloud Functions that use service accounts with excessive IAM permissions may pose security risks. It is a best practice to assign only the necessary permissions to the service accounts attached to functions.',
+ link: 'https://cloud.google.com/functions/docs/securing/authenticating',
+ recommended_action: 'Review and restrict the IAM roles associated with service accounts used by Cloud Functions to follow the principle of least privilege.',
+ apis: [''],
+ realtime_triggers: [
+ 'functions.CloudFunctionsService.UpdateFunction',
+ 'functions.CloudFunctionsService.CreateFunction',
+ 'functions.CloudFunctionsService.DeleteFunction'
+ ],
+
+ run: function(cache, settings, callback) {
+ var results = [];
+ var source = {};
+
+ callback(null, results, source);
+ }
+};
diff --git a/plugins/google/cloudfunctionsv2/cloudFunctionV2HttpsOnly.js b/plugins/google/cloudfunctionsv2/cloudFunctionV2HttpsOnly.js
new file mode 100644
index 000000000..00cecfef9
--- /dev/null
+++ b/plugins/google/cloudfunctionsv2/cloudFunctionV2HttpsOnly.js
@@ -0,0 +1,66 @@
+var async = require('async');
+var helpers = require('../../../helpers/google');
+
+module.exports = {
+ title: 'HTTP Trigger Require HTTPS V2',
+ category: 'Cloud Functions',
+ domain: 'Serverless',
+ severity: 'Medium',
+ description: 'Ensure that Cloud Functions V2 are configured to require HTTPS for HTTP invocations.',
+ more_info: 'You can make your Google Cloud Functions V2 calls secure by making sure that they require HTTPS.',
+ link: 'https://cloud.google.com/functions/docs/writing/http',
+ recommended_action: 'Ensure that your Google Cloud Functions V2 always require HTTPS.',
+ apis: ['functionsv2:list'],
+ realtime_triggers: ['functions.CloudFunctionsService.UpdateFunction','functions.CloudFunctionsService.DeleteFunction', 'functions.CloudFunctionsService.CreateFunction'],
+
+ run: function(cache, settings, callback) {
+ var results = [];
+ var source = {};
+ var regions = helpers.regions();
+
+ async.each(regions.functions, (region, rcb) => {
+ var functions = helpers.addSource(cache, source,
+ ['functionsv2', 'list', region]);
+
+ if (!functions) return rcb();
+
+ if (functions.err || !functions.data) {
+ helpers.addResult(results, 3,
+ 'Unable to query for Google Cloud functions: ' + helpers.addError(functions), region, null, null, functions.err);
+ return rcb();
+ }
+
+ if (!functions.data.length) {
+ helpers.addResult(results, 0, 'No Google Cloud functions found', region);
+ return rcb();
+ }
+
+ functions.data.forEach(funct => {
+ if (!funct.name) return;
+
+ if (!funct.environment || funct.environment !== 'GEN_2') return;
+
+ let serviceConfig = funct.serviceConfig || {};
+
+ if (serviceConfig.uri) {
+ if (serviceConfig.securityLevel && serviceConfig.securityLevel == 'SECURE_ALWAYS') {
+ helpers.addResult(results, 0,
+ 'Cloud Function is configured to require HTTPS for HTTP invocations', region, funct.name);
+ } else {
+ helpers.addResult(results, 2,
+ 'Cloud Function is not configured to require HTTPS for HTTP invocations', region, funct.name);
+ }
+ } else {
+ helpers.addResult(results, 0,
+ 'Cloud Function trigger type is not HTTP', region, funct.name);
+ }
+ });
+
+ rcb();
+ }, function() {
+ callback(null, results, source);
+ });
+ }
+
+};
+
diff --git a/plugins/google/cloudfunctionsv2/cloudFunctionV2HttpsOnly.spec.js b/plugins/google/cloudfunctionsv2/cloudFunctionV2HttpsOnly.spec.js
new file mode 100644
index 000000000..0d7d2b42f
--- /dev/null
+++ b/plugins/google/cloudfunctionsv2/cloudFunctionV2HttpsOnly.spec.js
@@ -0,0 +1,177 @@
+var expect = require('chai').expect;
+var plugin = require('./cloudFunctionV2HttpsOnly');
+
+
+const functions = [
+ {
+ "name": "projects/my-test-project/locations/us-central1/functions/function-1",
+ "environment": "GEN_2",
+ "state": "ACTIVE",
+ "updateTime": "2021-09-24T06:18:15.265Z",
+ "buildConfig": {
+ "runtime": "nodejs20",
+ "entryPoint": "helloWorld"
+ },
+ "serviceConfig": {
+ "serviceAccountEmail": "test@test-project.iam.gserviceaccount.com",
+ "uri": "https://us-central1-my-test-project.cloudfunctions.net/function-1",
+ "securityLevel": "SECURE_OPTIONAL"
+ }
+ },
+ {
+ "name": "projects/my-test-project/locations/us-central1/functions/function-2",
+ "environment": "GEN_2",
+ "state": "ACTIVE",
+ "updateTime": "2021-09-24T06:18:15.265Z",
+ "buildConfig": {
+ "runtime": "nodejs20",
+ "entryPoint": "helloWorld"
+ },
+ "serviceConfig": {
+ "serviceAccountEmail": "test@test-project.iam.gserviceaccount.com",
+ "uri": "https://us-central1-my-test-project.cloudfunctions.net/function-2",
+ "securityLevel": "SECURE_ALWAYS"
+ }
+ },
+ {
+ "name": "projects/my-test-project/locations/us-central1/functions/function-3",
+ "environment": "GEN_2",
+ "state": "ACTIVE",
+ "updateTime": "2021-09-24T06:18:15.265Z",
+ "buildConfig": {
+ "runtime": "nodejs20",
+ "entryPoint": "handleEvent"
+ },
+ "serviceConfig": {
+ "serviceAccountEmail": "test@test-project.iam.gserviceaccount.com"
+ }
+ },
+ {
+ "name": "projects/my-test-project/locations/us-central1/functions/function-4",
+ "environment": "GEN_1",
+ "state": "ACTIVE",
+ "runtime": "nodejs14",
+ "httpsTrigger": {
+ "url": "https://us-central1-my-test-project.cloudfunctions.net/function-4",
+ "securityLevel": "SECURE_OPTIONAL"
+ }
+ }
+];
+
+const createCache = (list, err) => {
+ return {
+ functionsv2: {
+ list: {
+ 'us-central1': {
+ err: err,
+ data: list
+ }
+ }
+ }
+ }
+};
+
+describe('httpTriggerRequireHttps', function () {
+ describe('run', function () {
+ it('should give passing result if no Cloud Functions V2 found', function (done) {
+ const callback = (err, results) => {
+ expect(results.length).to.be.above(0);
+ expect(results[0].status).to.equal(0);
+ expect(results[0].message).to.include('No Google Cloud functions found');
+ expect(results[0].region).to.equal('us-central1');
+ done()
+ };
+
+ const cache = createCache(
+ [],
+ null
+ );
+
+ plugin.run(cache, {}, callback);
+ });
+
+ it('should give unknown result if unable to query for Google Cloud functions', function (done) {
+ const callback = (err, results) => {
+ expect(results.length).to.be.above(0);
+ expect(results[0].status).to.equal(3);
+ expect(results[0].message).to.include('Unable to query for Google Cloud functions');
+ expect(results[0].region).to.equal('us-central1');
+ done()
+ };
+
+ const cache = createCache(
+ [],
+ {message: 'error'},
+ );
+
+ plugin.run(cache, {}, callback);
+ });
+
+ it('should give passing result if Cloud Function is configured to require HTTPS for HTTP invocations', function (done) {
+ const callback = (err, results) => {
+ expect(results.length).to.be.above(0);
+ expect(results[0].status).to.equal(0);
+ expect(results[0].message).to.include('Cloud Function is configured to require HTTPS for HTTP invocations');
+ expect(results[0].region).to.equal('us-central1');
+ done()
+ };
+
+ const cache = createCache(
+ [functions[1]],
+ null
+ );
+
+ plugin.run(cache, {}, callback);
+ });
+
+ it('should give failing result if Cloud Function is not configured to require HTTPS for HTTP invocations', function (done) {
+ const callback = (err, results) => {
+ expect(results.length).to.be.above(0);
+ expect(results[0].status).to.equal(2);
+ expect(results[0].message).to.include('Cloud Function is not configured to require HTTPS for HTTP invocations');
+ expect(results[0].region).to.equal('us-central1');
+ done();
+ };
+
+ const cache = createCache(
+ [functions[0]],
+ null
+ );
+
+ plugin.run(cache, {}, callback);
+ });
+
+ it('should give passing result if Cloud Function trigger type is not HTTP', function (done) {
+ const callback = (err, results) => {
+ expect(results.length).to.be.above(0);
+ expect(results[0].status).to.equal(0);
+ expect(results[0].message).to.include('Cloud Function trigger type is not HTTP');
+ expect(results[0].region).to.equal('us-central1');
+ done();
+ };
+
+ const cache = createCache(
+ [functions[2]],
+ null
+ );
+
+ plugin.run(cache, {}, callback);
+ });
+
+ it('should not check Gen 1 functions in v2 API response', function (done) {
+ const callback = (err, results) => {
+ expect(results.length).to.equal(0);
+ done();
+ };
+
+ const cache = createCache(
+ [functions[3]],
+ null
+ );
+
+ plugin.run(cache, {}, callback);
+ });
+
+ })
+});
+
diff --git a/plugins/google/cloudfunctionsv2/cloudFunctionV2IngressSettings.js b/plugins/google/cloudfunctionsv2/cloudFunctionV2IngressSettings.js
new file mode 100644
index 000000000..78219be8b
--- /dev/null
+++ b/plugins/google/cloudfunctionsv2/cloudFunctionV2IngressSettings.js
@@ -0,0 +1,65 @@
+var async = require('async');
+var helpers = require('../../../helpers/google');
+
+module.exports = {
+ title: 'Ingress All Traffic Disabled V2',
+ category: 'Cloud Functions',
+ domain: 'Serverless',
+ severity: 'Medium',
+ description: 'Ensure that Cloud Functions V2 are configured to allow only internal traffic or traffic from Cloud Load Balancer.',
+ more_info: 'You can secure your Google Cloud Functions V2 by implementing network-based access control.',
+ link: 'https://cloud.google.com/functions/docs/securing/authenticating',
+ recommended_action: 'Ensure that your Google Cloud Functions V2 do not allow external traffic from the internet.',
+ apis: ['functionsv2:list'],
+ realtime_triggers: ['functions.CloudFunctionsService.UpdateFunction', 'functions.CloudFunctionsService.CreateFunction', 'functions.CloudFunctionsService.DeleteFunction'],
+
+ run: function(cache, settings, callback) {
+ var results = [];
+ var source = {};
+ var regions = helpers.regions();
+
+ async.each(regions.functions, (region, rcb) => {
+ var functions = helpers.addSource(cache, source,
+ ['functionsv2', 'list', region]);
+
+ if (!functions) return rcb();
+
+ if (functions.err || !functions.data) {
+ helpers.addResult(results, 3,
+ 'Unable to query for Google Cloud functions: ' + helpers.addError(functions), region, null, null, functions.err);
+ return rcb();
+ }
+
+ if (!functions.data.length) {
+ helpers.addResult(results, 0, 'No Google Cloud functions found', region);
+ return rcb();
+ }
+
+ functions.data.forEach(func => {
+ if (!func.name) return;
+
+ if (!func.environment || func.environment !== 'GEN_2') return;
+
+ let ingressSettings = func.serviceConfig && func.serviceConfig.ingressSettings
+ ? func.serviceConfig.ingressSettings
+ : null;
+
+ if (ingressSettings && ingressSettings.toUpperCase() == 'ALLOW_ALL') {
+ helpers.addResult(results, 2,
+ 'Cloud Function is configured to allow all traffic', region, func.name);
+ } else if (ingressSettings) {
+ helpers.addResult(results, 0,
+ 'Cloud Function is configured to allow only internal and CLB traffic', region, func.name);
+ } else {
+ helpers.addResult(results, 2,
+ 'Cloud Function does not have ingress settings configured', region, func.name);
+ }
+ });
+
+ rcb();
+ }, function() {
+ callback(null, results, source);
+ });
+ }
+};
+
diff --git a/plugins/google/cloudfunctionsv2/cloudFunctionV2IngressSettings.spec.js b/plugins/google/cloudfunctionsv2/cloudFunctionV2IngressSettings.spec.js
new file mode 100644
index 000000000..13168f81e
--- /dev/null
+++ b/plugins/google/cloudfunctionsv2/cloudFunctionV2IngressSettings.spec.js
@@ -0,0 +1,172 @@
+var expect = require('chai').expect;
+var plugin = require('./cloudFunctionV2IngressSettings');
+
+
+const functions = [
+ {
+ "name": "projects/my-test-project/locations/us-central1/functions/function-1",
+ "environment": "GEN_2",
+ "state": "ACTIVE",
+ "updateTime": "2021-09-24T06:18:15.265Z",
+ "buildConfig": {
+ "runtime": "nodejs20",
+ "entryPoint": "helloWorld"
+ },
+ "serviceConfig": {
+ "serviceAccountEmail": "test@test-project.iam.gserviceaccount.com",
+ "ingressSettings": "ALLOW_ALL"
+ }
+ },
+ {
+ "name": "projects/my-test-project/locations/us-central1/functions/function-2",
+ "environment": "GEN_2",
+ "state": "ACTIVE",
+ "updateTime": "2021-09-24T06:18:15.265Z",
+ "buildConfig": {
+ "runtime": "nodejs20",
+ "entryPoint": "helloWorld"
+ },
+ "serviceConfig": {
+ "serviceAccountEmail": "test@test-project.iam.gserviceaccount.com",
+ "ingressSettings": "ALLOW_INTERNAL_AND_GCLB"
+ }
+ },
+ {
+ "name": "projects/my-test-project/locations/us-central1/functions/function-3",
+ "environment": "GEN_2",
+ "state": "ACTIVE",
+ "updateTime": "2021-09-24T06:18:15.265Z",
+ "buildConfig": {
+ "runtime": "nodejs20",
+ "entryPoint": "helloWorld"
+ },
+ "serviceConfig": {
+ "serviceAccountEmail": "test@test-project.iam.gserviceaccount.com"
+ }
+ },
+ {
+ "name": "projects/my-test-project/locations/us-central1/functions/function-4",
+ "environment": "GEN_1",
+ "state": "ACTIVE",
+ "runtime": "nodejs14",
+ "ingressSettings": "ALLOW_ALL"
+ }
+];
+
+const createCache = (list, err) => {
+ return {
+ functionsv2: {
+ list: {
+ 'us-central1': {
+ err: err,
+ data: list
+ }
+ }
+ }
+ }
+};
+
+describe('ingressAllTrafficDisabled', function () {
+ describe('run', function () {
+ it('should give passing result if no Cloud Functions V2 found', function (done) {
+ const callback = (err, results) => {
+ expect(results.length).to.be.above(0);
+ expect(results[0].status).to.equal(0);
+ expect(results[0].message).to.include('No Google Cloud functions found');
+ expect(results[0].region).to.equal('us-central1');
+ done()
+ };
+
+ const cache = createCache(
+ [],
+ null
+ );
+
+ plugin.run(cache, {}, callback);
+ });
+
+ it('should give unknown result if unable to query for Google Cloud functions', function (done) {
+ const callback = (err, results) => {
+ expect(results.length).to.be.above(0);
+ expect(results[0].status).to.equal(3);
+ expect(results[0].message).to.include('Unable to query for Google Cloud functions');
+ expect(results[0].region).to.equal('us-central1');
+ done()
+ };
+
+ const cache = createCache(
+ [],
+ {message: 'error'},
+ );
+
+ plugin.run(cache, {}, callback);
+ });
+
+ it('should give passing result if Cloud Function is configured to allow only internal and CLB traffic', function (done) {
+ const callback = (err, results) => {
+ expect(results.length).to.be.above(0);
+ expect(results[0].status).to.equal(0);
+ expect(results[0].message).to.include('Cloud Function is configured to allow only internal and CLB traffic');
+ expect(results[0].region).to.equal('us-central1');
+ done()
+ };
+
+ const cache = createCache(
+ [functions[1]],
+ null
+ );
+
+ plugin.run(cache, {}, callback);
+ });
+
+ it('should give failing result if Cloud Function is configured to allow all traffic', function (done) {
+ const callback = (err, results) => {
+ expect(results.length).to.be.above(0);
+ expect(results[0].status).to.equal(2);
+ expect(results[0].message).to.include('Cloud Function is configured to allow all traffic');
+ expect(results[0].region).to.equal('us-central1');
+ done();
+ };
+
+ const cache = createCache(
+ [functions[0]],
+ null
+ );
+
+ plugin.run(cache, {}, callback);
+ });
+
+ it('should give failing result if Cloud Function does not have ingress settings configured', function (done) {
+ const callback = (err, results) => {
+ expect(results.length).to.be.above(0);
+ expect(results[0].status).to.equal(2);
+ expect(results[0].message).to.include('Cloud Function does not have ingress settings configured');
+ expect(results[0].region).to.equal('us-central1');
+ done();
+ };
+
+ const cache = createCache(
+ [functions[2]],
+ null
+ );
+
+ plugin.run(cache, {}, callback);
+ });
+
+ it('should not check Gen 1 functions in v2 API response', function (done) {
+ const callback = (err, results) => {
+ expect(results.length).to.equal(0);
+ done();
+ };
+
+ const cache = createCache(
+ [functions[3]],
+ null
+ );
+
+ plugin.run(cache, {}, callback);
+ });
+
+ })
+});
+
diff --git a/plugins/google/cloudfunctionsv2/cloudFunctionV2LabelsAdded.js b/plugins/google/cloudfunctionsv2/cloudFunctionV2LabelsAdded.js
new file mode 100644
index 000000000..3d7bdd644
--- /dev/null
+++ b/plugins/google/cloudfunctionsv2/cloudFunctionV2LabelsAdded.js
@@ -0,0 +1,58 @@
+var async = require('async');
+var helpers = require('../../../helpers/google');
+
+module.exports = {
+ title: 'Cloud Function V2 Labels Added',
+ category: 'Cloud Functions',
+ domain: 'Serverless',
+ severity: 'Low',
+ description: 'Ensure that all Cloud Functions V2 have labels added.',
+ more_info: 'Labels are a lightweight way to group resources together that are related to or associated with each other. It is a best practice to label cloud resources to better organize and gain visibility into their usage.',
+ link: 'https://cloud.google.com/functions/docs/configuring',
+ recommended_action: 'Ensure labels are added to all Cloud Functions V2.',
+ apis: ['functionsv2:list'],
+ realtime_triggers: ['functions.CloudFunctionsService.UpdateFunction', 'functions.CloudFunctionsService.CreateFunction', 'functions.CloudFunctionsService.DeleteFunction'],
+
+ run: function(cache, settings, callback) {
+ var results = [];
+ var source = {};
+ var regions = helpers.regions();
+
+ async.each(regions.functions, (region, rcb) => {
+ var functions = helpers.addSource(cache, source,
+ ['functionsv2', 'list', region]);
+
+ if (!functions) return rcb();
+
+ if (functions.err || !functions.data) {
+ helpers.addResult(results, 3,
+ 'Unable to query for Google Cloud functions: ' + helpers.addError(functions), region, null, null, functions.err);
+ return rcb();
+ }
+
+ if (!functions.data.length) {
+ helpers.addResult(results, 0, 'No Google Cloud functions found', region);
+ return rcb();
+ }
+
+ functions.data.forEach(func => {
+ if (!func.name) return;
+
+ if (!func.environment || func.environment !== 'GEN_2') return;
+
+ if (func.labels && Object.keys(func.labels).length) {
+ helpers.addResult(results, 0,
+ `${Object.keys(func.labels).length} labels found for Cloud Function`, region, func.name);
+ } else {
+ helpers.addResult(results, 2,
+ 'Cloud Function does not have any labels', region, func.name);
+ }
+ });
+
+ rcb();
+ }, function() {
+ callback(null, results, source);
+ });
+ }
+};
+
diff --git a/plugins/google/cloudfunctionsv2/cloudFunctionV2LabelsAdded.spec.js b/plugins/google/cloudfunctionsv2/cloudFunctionV2LabelsAdded.spec.js
new file mode 100644
index 000000000..2bf55b1c3
--- /dev/null
+++ b/plugins/google/cloudfunctionsv2/cloudFunctionV2LabelsAdded.spec.js
@@ -0,0 +1,143 @@
+var expect = require('chai').expect;
+var plugin = require('./cloudFunctionV2LabelsAdded');
+
+
+const functions = [
+ {
+ "name": "projects/my-test-project/locations/us-central1/functions/function-1",
+ "environment": "GEN_2",
+ "state": "ACTIVE",
+ "updateTime": "2021-09-24T06:18:15.265Z",
+ "buildConfig": {
+ "runtime": "nodejs20",
+ "entryPoint": "helloWorld"
+ },
+ "serviceConfig": {
+ "serviceAccountEmail": "test@test-project.iam.gserviceaccount.com",
+ "ingressSettings": "ALLOW_ALL"
+ }
+ },
+ {
+ "name": "projects/my-test-project/locations/us-central1/functions/function-2",
+ "environment": "GEN_2",
+ "state": "ACTIVE",
+ "updateTime": "2021-09-24T06:18:15.265Z",
+ "buildConfig": {
+ "runtime": "nodejs20",
+ "entryPoint": "helloWorld"
+ },
+ "serviceConfig": {
+ "serviceAccountEmail": "test@test-project.iam.gserviceaccount.com",
+ "ingressSettings": "ALLOW_INTERNAL_AND_GCLB"
+ },
+ "labels": { 'deployment-tool': 'console-cloud', 'env': 'production' }
+ },
+ {
+ "name": "projects/my-test-project/locations/us-central1/functions/function-3",
+ "environment": "GEN_1",
+ "state": "ACTIVE",
+ "runtime": "nodejs14",
+ "ingressSettings": "ALLOW_ALL"
+ }
+];
+
+const createCache = (list, err) => {
+ return {
+ functionsv2: {
+ list: {
+ 'us-central1': {
+ err: err,
+ data: list
+ }
+ }
+ }
+ }
+};
+
+describe('cloudFunctionLabelsAdded', function () {
+ describe('run', function () {
+ it('should give passing result if no Google Cloud functions found', function (done) {
+ const callback = (err, results) => {
+ expect(results.length).to.be.above(0);
+ expect(results[0].status).to.equal(0);
+ expect(results[0].message).to.include('No Google Cloud functions found');
+ expect(results[0].region).to.equal('us-central1');
+ done()
+ };
+
+ const cache = createCache(
+ [],
+ null
+ );
+
+ plugin.run(cache, {}, callback);
+ });
+
+ it('should give unknown result if unable to query for Google Cloud functions', function (done) {
+ const callback = (err, results) => {
+ expect(results.length).to.be.above(0);
+ expect(results[0].status).to.equal(3);
+ expect(results[0].message).to.include('Unable to query for Google Cloud functions');
+ expect(results[0].region).to.equal('us-central1');
+ done()
+ };
+
+ const cache = createCache(
+ [],
+ {message: 'error'},
+ );
+
+ plugin.run(cache, {}, callback);
+ });
+
+ it('should give passing result if Cloud Function has labels added', function (done) {
+ const callback = (err, results) => {
+ expect(results.length).to.be.above(0);
+ expect(results[0].status).to.equal(0);
+ expect(results[0].message).to.include('labels found for Cloud Function');
+ expect(results[0].region).to.equal('us-central1');
+ done()
+ };
+
+ const cache = createCache(
+ [functions[1]],
+ null
+ );
+
+ plugin.run(cache, {}, callback);
+ });
+
+ it('should give failing result if Cloud Function does not have labels added', function (done) {
+ const callback = (err, results) => {
+ expect(results.length).to.be.above(0);
+ expect(results[0].status).to.equal(2);
+ expect(results[0].message).to.include('does not have any labels');
+ expect(results[0].region).to.equal('us-central1');
+ done();
+ };
+
+ const cache = createCache(
+ [functions[0]],
+ null
+ );
+
+ plugin.run(cache, {}, callback);
+ });
+
+ it('should not check Gen 1 functions in v2 API response', function (done) {
+ const callback = (err, results) => {
+ expect(results.length).to.equal(0);
+ done();
+ };
+
+ const cache = createCache(
+ [functions[2]],
+ null
+ );
+
+ plugin.run(cache, {}, callback);
+ });
+
+ })
+});
+
diff --git a/plugins/google/cloudfunctionsv2/cloudFunctionV2OldRuntime.js b/plugins/google/cloudfunctionsv2/cloudFunctionV2OldRuntime.js
new file mode 100644
index 000000000..424fd12b6
--- /dev/null
+++ b/plugins/google/cloudfunctionsv2/cloudFunctionV2OldRuntime.js
@@ -0,0 +1,129 @@
+var async = require('async');
+var helpers = require('../../../helpers/google');
+
+module.exports = {
+ title: 'Cloud Function V2 Old Runtimes',
+ category: 'Cloud Functions',
+ domain: 'Compute',
+ severity: 'Medium',
+ description: 'Ensure that Cloud Functions V2 are not using deprecated runtime versions.',
+ more_info: 'Cloud Functions V2 runtimes should be kept current with recent versions of the underlying codebase. It is recommended to update to the latest supported versions to avoid potential security risks and ensure compatibility.',
+ link: 'https://cloud.google.com/functions/docs/concepts/execution-environment',
+ recommended_action: 'Modify Cloud Functions V2 to use latest versions.',
+ apis: ['functionsv2:list'],
+ settings: {
+ function_runtime_fail: {
+ name: 'Cloud Function V2 Runtime Fail',
+ description: 'Return a failing result for Cloud Function V2 runtime before this number of days for their end of life date.',
+ regex: '^[1-9]{1}[0-9]{0,3}$',
+ default: 0
+ }
+ },
+ realtime_triggers: ['functions.CloudFunctionsService.UpdateFunction', 'functions.CloudFunctionsService.CreateFunction', 'functions.CloudFunctionsService.DeleteFunction'],
+
+ run: function(cache, settings, callback) {
+ var results = [];
+ var source = {};
+ var regions = helpers.regions();
+
+ var config = {
+ function_runtime_fail: parseInt(settings.function_runtime_fail || this.settings.function_runtime_fail.default)
+ };
+
+ var deprecatedRuntimes = [
+ { 'id':'nodejs10', 'name': 'Node.js 10.x', 'endOfLifeDate': '2021-07-30' },
+ { 'id':'nodejs12', 'name': 'Node.js 12', 'endOfLifeDate': '2024-01-30' },
+ { 'id':'nodejs14', 'name': 'Node.js 14', 'endOfLifeDate': '2024-01-30' },
+ { 'id':'nodejs16', 'name': 'Node.js 16', 'endOfLifeDate': '2024-01-30' },
+ { 'id':'nodejs18', 'name': 'Node.js 18', 'endOfLifeDate': '2025-04-30' },
+ { 'id':'nodejs20', 'name': 'Node.js 20', 'endOfLifeDate': '2026-04-30' },
+ { 'id':'dotnet6', 'name': '.Net 6', 'endOfLifeDate': '2024-11-12' },
+ { 'id':'dotnet7', 'name': '.Net 7', 'endOfLifeDate': '2024-05-14' },
+ { 'id':'dotnet3', 'name': '.Net Core 3', 'endOfLifeDate': '2024-01-30' },
+ { 'id':'python27', 'name': 'Python 2.7', 'endOfLifeDate': '2021-07-15' },
+ { 'id':'python36', 'name': 'Python 3.6', 'endOfLifeDate': '2022-07-18' },
+ { 'id':'python37', 'name': 'Python 3.7', 'endOfLifeDate': '2024-01-30' },
+ { 'id':'python38', 'name': 'Python 3.8', 'endOfLifeDate': '2024-10-14' },
+ { 'id':'python39', 'name': 'Python 3.9', 'endOfLifeDate': '2025-10-05' },
+ { 'id':'python310', 'name': 'Python 3.10', 'endOfLifeDate': '2026-10-04' },
+ { 'id':'python311', 'name': 'Python 3.11', 'endOfLifeDate': '2027-10-24' },
+ { 'id':'python312', 'name': 'Python 3.12', 'endOfLifeDate': '2028-10-02' },
+ { 'id':'ruby25', 'name': 'Ruby 2.5', 'endOfLifeDate': '2021-07-30' },
+ { 'id':'ruby27', 'name': 'Ruby 2.7', 'endOfLifeDate': '2024-01-30' },
+ { 'id':'ruby30', 'name': 'Ruby 3.0', 'endOfLifeDate': '2024-03-31' },
+ { 'id':'ruby32', 'name': 'Ruby 3.2', 'endOfLifeDate': '2026-03-31' },
+ { 'id':'go121', 'name': 'Go 1.21', 'endOfLifeDate': '2024-05-01' },
+ { 'id':'go119', 'name': 'Go 1.19', 'endOfLifeDate': '2024-04-30' },
+ { 'id':'go118', 'name': 'Go 1.18', 'endOfLifeDate': '2024-01-30' },
+ { 'id':'go116', 'name': 'Go 1.16', 'endOfLifeDate': '2024-01-30' },
+ { 'id':'go113', 'name': 'Go 1.13', 'endOfLifeDate': '2024-01-30' },
+ { 'id':'java8', 'name': 'Java 8', 'endOfLifeDate': '2024-01-08' },
+ { 'id':'java11', 'name': 'Java 11', 'endOfLifeDate': '2024-10-01' },
+ { 'id':'java17', 'name': 'Java 17', 'endOfLifeDate': '2027-10-01' },
+ { 'id':'php74', 'name': 'PHP 7.4', 'endOfLifeDate': '2024-01-30' },
+ { 'id':'php81', 'name': 'PHP 8.1', 'endOfLifeDate': '2024-11-25' },
+ { 'id':'php82', 'name': 'PHP 8.2', 'endOfLifeDate': '2025-12-08' },
+ ];
+
+ async.each(regions.functions, (region, rcb) => {
+ var functions = helpers.addSource(cache, source,
+ ['functionsv2', 'list', region]);
+
+ if (!functions) return rcb();
+
+ if (functions.err || !functions.data) {
+ helpers.addResult(results, 3,
+ 'Unable to query for Google Cloud functions: ' + helpers.addError(functions), region, null, null, functions.err);
+ return rcb();
+ }
+
+ if (!functions.data.length) {
+ helpers.addResult(results, 0, 'No Google Cloud functions found', region);
+ return rcb();
+ }
+
+ functions.data.forEach(func => {
+ if (!func.name) return;
+
+ if (!func.environment || func.environment !== 'GEN_2') return;
+
+ let buildConfig = func.buildConfig || {};
+ let runtime = buildConfig.runtime;
+
+ if (!runtime) {
+ helpers.addResult(results, 2,
+ 'Cloud Function does not have a runtime configured', region, func.name);
+ return;
+ }
+
+ var deprecatedRuntime = deprecatedRuntimes.filter((d) => {
+ return d.id == runtime;
+ });
+
+ var version = runtime;
+ var runtimeDeprecationDate = (deprecatedRuntime && deprecatedRuntime.length && deprecatedRuntime[0].endOfLifeDate) ? Date.parse(deprecatedRuntime[0].endOfLifeDate) : null;
+ let today = new Date();
+ today = Date.parse(`${today.getFullYear()}-${today.getMonth()+1}-${today.getDate()}`);
+ var difference = runtimeDeprecationDate? Math.round((runtimeDeprecationDate - today)/(1000 * 3600 * 24)): null;
+ if (runtimeDeprecationDate && today > runtimeDeprecationDate) {
+ helpers.addResult(results, 2,
+ 'Cloud Function is using runtime: ' + deprecatedRuntime[0].name + ' which was deprecated on: ' + deprecatedRuntime[0].endOfLifeDate,
+ region, func.name);
+ } else if (difference && config.function_runtime_fail >= difference) {
+ helpers.addResult(results, 2,
+ 'Cloud Function is using runtime: ' + version + ' which is deprecating in ' + Math.abs(difference) + ' days',
+ region, func.name);
+ } else {
+ helpers.addResult(results, 0,
+ 'Cloud Function is running the current version: ' + version,
+ region, func.name);
+ }
+ });
+
+ rcb();
+ }, function() {
+ callback(null, results, source);
+ });
+ }
+};
+
diff --git a/plugins/google/cloudfunctionsv2/cloudFunctionV2OldRuntime.spec.js b/plugins/google/cloudfunctionsv2/cloudFunctionV2OldRuntime.spec.js
new file mode 100644
index 000000000..8aa995313
--- /dev/null
+++ b/plugins/google/cloudfunctionsv2/cloudFunctionV2OldRuntime.spec.js
@@ -0,0 +1,168 @@
+var expect = require('chai').expect;
+var plugin = require('./cloudFunctionV2OldRuntime');
+
+
+const functions = [
+ {
+ "name": "projects/my-test-project/locations/us-central1/functions/function-1",
+ "environment": "GEN_2",
+ "state": "ACTIVE",
+ "updateTime": "2021-09-24T06:18:15.265Z",
+ "buildConfig": {
+ "runtime": "nodejs14",
+ "entryPoint": "helloWorld"
+ },
+ "serviceConfig": {
+ "serviceAccountEmail": "test@test-project.iam.gserviceaccount.com",
+ "ingressSettings": "ALLOW_ALL"
+ }
+ },
+ {
+ "name": "projects/my-test-project/locations/us-central1/functions/function-2",
+ "environment": "GEN_2",
+ "state": "ACTIVE",
+ "updateTime": "2021-09-24T06:18:15.265Z",
+ "buildConfig": {
+ "runtime": "python312",
+ "entryPoint": "main"
+ },
+ "serviceConfig": {
+ "serviceAccountEmail": "test@test-project.iam.gserviceaccount.com",
+ "ingressSettings": "ALLOW_INTERNAL_AND_GCLB"
+ },
+ "labels": { 'deployment-tool': 'console-cloud' }
+ },
+ {
+ "name": "projects/my-test-project/locations/us-central1/functions/function-3",
+ "environment": "GEN_2",
+ "state": "ACTIVE",
+ "updateTime": "2021-09-24T06:18:15.265Z",
+ "serviceConfig": {
+ "serviceAccountEmail": "test@test-project.iam.gserviceaccount.com"
+ }
+ },
+ {
+ "name": "projects/my-test-project/locations/us-central1/functions/function-4",
+ "environment": "GEN_1",
+ "state": "ACTIVE",
+ "runtime": "nodejs14"
+ }
+];
+
+const createCache = (list, err) => {
+ return {
+ functionsv2: {
+ list: {
+ 'us-central1': {
+ err: err,
+ data: list
+ }
+ }
+ }
+ }
+};
+
+describe('cloudFunctionOldRuntime', function () {
+ describe('run', function () {
+ it('should give passing result if no Cloud Functions V2 found', function (done) {
+ const callback = (err, results) => {
+ expect(results.length).to.be.above(0);
+ expect(results[0].status).to.equal(0);
+ expect(results[0].message).to.include('No Google Cloud functions found');
+ expect(results[0].region).to.equal('us-central1');
+ done()
+ };
+
+ const cache = createCache(
+ [],
+ null
+ );
+
+ plugin.run(cache, {}, callback);
+ });
+
+ it('should give unknown result if unable to query for Google Cloud functions', function (done) {
+ const callback = (err, results) => {
+ expect(results.length).to.be.above(0);
+ expect(results[0].status).to.equal(3);
+ expect(results[0].message).to.include('Unable to query for Google Cloud functions');
+ expect(results[0].region).to.equal('us-central1');
+ done()
+ };
+
+ const cache = createCache(
+ [],
+ {message: 'error'},
+ );
+
+ plugin.run(cache, {}, callback);
+ });
+
+ it('should give passing result if Cloud Function is using latest runtime version', function (done) {
+ const callback = (err, results) => {
+ expect(results.length).to.be.above(0);
+ expect(results[0].status).to.equal(0);
+ expect(results[0].message).to.include('Cloud Function is running the current version: ');
+ expect(results[0].region).to.equal('us-central1');
+ done()
+ };
+
+ const cache = createCache(
+ [functions[1]],
+ null
+ );
+
+ plugin.run(cache, {}, callback);
+ });
+
+ it('should give failing result if Cloud Function is using deprecated runtime version', function (done) {
+ const callback = (err, results) => {
+ expect(results.length).to.be.above(0);
+ expect(results[0].status).to.equal(2);
+ expect(results[0].message).to.include('which was deprecated on');
+ expect(results[0].region).to.equal('us-central1');
+ done();
+ };
+
+ const cache = createCache(
+ [functions[0]],
+ null
+ );
+
+ plugin.run(cache, {}, callback);
+ });
+
+ it('should give failing result if Cloud Function does not have a runtime configured', function (done) {
+ const callback = (err, results) => {
+ expect(results.length).to.be.above(0);
+ expect(results[0].status).to.equal(2);
+ expect(results[0].message).to.include('Cloud Function does not have a runtime configured');
+ expect(results[0].region).to.equal('us-central1');
+ done();
+ };
+
+ const cache = createCache(
+ [functions[2]],
+ null
+ );
+
+ plugin.run(cache, {}, callback);
+ });
+
+ it('should not check Gen 1 functions in v2 API response', function (done) {
+ const callback = (err, results) => {
+ expect(results.length).to.equal(0);
+ done();
+ };
+
+ const cache = createCache(
+ [functions[3]],
+ null
+ );
+
+ plugin.run(cache, {}, callback);
+ });
+
+ })
+});
+
diff --git a/plugins/google/cloudfunctionsv2/cloudFunctionV2VPCConnector.js b/plugins/google/cloudfunctionsv2/cloudFunctionV2VPCConnector.js
new file mode 100644
index 000000000..075a94fb4
--- /dev/null
+++ b/plugins/google/cloudfunctionsv2/cloudFunctionV2VPCConnector.js
@@ -0,0 +1,67 @@
+var async = require('async');
+var helpers = require('../../../helpers/google');
+
+module.exports = {
+ title: 'Cloud Function V2 Serverless VPC Access',
+ category: 'Cloud Functions',
+ domain: 'Serverless',
+ severity: 'High',
+ description: 'Ensure that Cloud Functions V2 are allowed to access only VPC resources.',
+ more_info: 'Cloud Functions V2 may require to connect directly to Compute Engine VM instances, Memorystore instances, Cloud SQL instances, and any other resources. It is a best practice to send requests to these resources using an internal IP address by connecting to VPC network using "Serverless VPC Access" configuration.',
+ link: 'https://cloud.google.com/functions/docs/networking/connecting-vpc#create-connector',
+ recommended_action: 'Ensure all Cloud Functions V2 are using serverless VPC connectors.',
+ apis: ['functionsv2:list'],
+ realtime_triggers: ['functions.CloudFunctionsService.UpdateFunction', 'functions.CloudFunctionsService.CreateFunction', 'functions.CloudFunctionsService.DeleteFunction'],
+
+ run: function(cache, settings, callback) {
+ var results = [];
+ var source = {};
+ var regions = helpers.regions();
+
+ async.each(regions.functions, (region, rcb) => {
+ var functions = helpers.addSource(cache, source,
+ ['functionsv2', 'list', region]);
+
+ if (!functions) return rcb();
+
+ if (functions.err || !functions.data) {
+ helpers.addResult(results, 3,
+ 'Unable to query for Google Cloud functions: ' + helpers.addError(functions), region, null, null, functions.err);
+ return rcb();
+ }
+
+ if (!functions.data.length) {
+ helpers.addResult(results, 0, 'No Google Cloud functions found', region);
+ return rcb();
+ }
+
+ functions.data.forEach(func => {
+ if (!func.name) return;
+
+ if (!func.environment || func.environment !== 'GEN_2') return;
+
+ let serviceConfig = func.serviceConfig || {};
+ let vpcConnector = serviceConfig.vpcConnector;
+ let vpcConnectorEgressSettings = serviceConfig.vpcConnectorEgressSettings;
+
+ if (vpcConnector) {
+ if (vpcConnectorEgressSettings && vpcConnectorEgressSettings.toUpperCase() === 'ALL_TRAFFIC') {
+ helpers.addResult(results, 0,
+ 'Cloud Function is using a VPC Connector to route all traffic', region, func.name);
+ } else {
+ helpers.addResult(results, 2,
+ 'Cloud Function is using a VPC Connector for requests to private IPs only', region, func.name);
+ }
+ } else {
+ helpers.addResult(results, 2,
+ 'Cloud Function is not configured with Serverless VPC Access', region, func.name);
+ }
+ });
+
+ rcb();
+ }, function() {
+ callback(null, results, source);
+ });
+ }
+};
+
diff --git a/plugins/google/cloudfunctionsv2/cloudFunctionV2VPCConnector.spec.js b/plugins/google/cloudfunctionsv2/cloudFunctionV2VPCConnector.spec.js
new file mode 100644
index 000000000..13ed92c6a
--- /dev/null
+++ b/plugins/google/cloudfunctionsv2/cloudFunctionV2VPCConnector.spec.js
@@ -0,0 +1,176 @@
+var expect = require('chai').expect;
+var plugin = require('./cloudFunctionV2VPCConnector');
+
+
+const functions = [
+ {
+ "name": "projects/my-test-project/locations/us-central1/functions/function-1",
+ "environment": "GEN_2",
+ "state": "ACTIVE",
+ "updateTime": "2021-09-24T06:18:15.265Z",
+ "buildConfig": {
+ "runtime": "nodejs20",
+ "entryPoint": "helloWorld"
+ },
+ "serviceConfig": {
+ "serviceAccountEmail": "test@test-project.iam.gserviceaccount.com",
+ "ingressSettings": "ALLOW_ALL"
+ }
+ },
+ {
+ "name": "projects/my-test-project/locations/us-central1/functions/function-2",
+ "environment": "GEN_2",
+ "state": "ACTIVE",
+ "updateTime": "2021-09-24T06:18:15.265Z",
+ "buildConfig": {
+ "runtime": "nodejs20",
+ "entryPoint": "helloWorld"
+ },
+ "serviceConfig": {
+ "serviceAccountEmail": "test@test-project.iam.gserviceaccount.com",
+ "ingressSettings": "ALLOW_INTERNAL_AND_GCLB",
+ "vpcConnector": "projects/my-test-project/locations/us-central1/connectors/cloud-func-connector",
+ "vpcConnectorEgressSettings": "ALL_TRAFFIC"
+ }
+ },
+ {
+ "name": "projects/my-test-project/locations/us-central1/functions/function-3",
+ "environment": "GEN_2",
+ "state": "ACTIVE",
+ "updateTime": "2021-09-24T06:18:15.265Z",
+ "buildConfig": {
+ "runtime": "nodejs20",
+ "entryPoint": "helloWorld"
+ },
+ "serviceConfig": {
+ "serviceAccountEmail": "test@test-project.iam.gserviceaccount.com",
+ "vpcConnector": "projects/my-test-project/locations/us-central1/connectors/cloud-func-connector",
+ "vpcConnectorEgressSettings": "PRIVATE_RANGES_ONLY"
+ }
+ },
+ {
+ "name": "projects/my-test-project/locations/us-central1/functions/function-4",
+ "environment": "GEN_1",
+ "state": "ACTIVE",
+ "runtime": "nodejs14",
+ "vpcConnector": "projects/my-test-project/locations/us-central1/connectors/cloud-func-connector"
+ }
+];
+
+const createCache = (list, err) => {
+ return {
+ functionsv2: {
+ list: {
+ 'us-central1': {
+ err: err,
+ data: list
+ }
+ }
+ }
+ }
+};
+
+describe('serverlessVPCAccess', function () {
+ describe('run', function () {
+ it('should give passing result if no Cloud Functions V2 found', function (done) {
+ const callback = (err, results) => {
+ expect(results.length).to.be.above(0);
+ expect(results[0].status).to.equal(0);
+ expect(results[0].message).to.include('No Google Cloud functions found');
+ expect(results[0].region).to.equal('us-central1');
+ done()
+ };
+
+ const cache = createCache(
+ [],
+ null
+ );
+
+ plugin.run(cache, {}, callback);
+ });
+
+ it('should give unknown result if unable to query for Google Cloud functions', function (done) {
+ const callback = (err, results) => {
+ expect(results.length).to.be.above(0);
+ expect(results[0].status).to.equal(3);
+ expect(results[0].message).to.include('Unable to query for Google Cloud functions');
+ expect(results[0].region).to.equal('us-central1');
+ done()
+ };
+
+ const cache = createCache(
+ [],
+ {message: 'error'},
+ );
+
+ plugin.run(cache, {}, callback);
+ });
+
+ it('should give passing result if Cloud Function is using a VPC Connector to route all traffic', function (done) {
+ const callback = (err, results) => {
+ expect(results.length).to.be.above(0);
+ expect(results[0].status).to.equal(0);
+ expect(results[0].message).to.include('Cloud Function is using a VPC Connector to route all traffic');
+ expect(results[0].region).to.equal('us-central1');
+ done()
+ };
+
+ const cache = createCache(
+ [functions[1]],
+ null
+ );
+
+ plugin.run(cache, {}, callback);
+ });
+
+ it('should give failing result if Cloud Function is using a VPC Connector for requests to private IPs only', function (done) {
+ const callback = (err, results) => {
+ expect(results.length).to.be.above(0);
+ expect(results[0].status).to.equal(2);
+ expect(results[0].message).to.include('Cloud Function is using a VPC Connector for requests to private IPs only');
+ expect(results[0].region).to.equal('us-central1');
+ done();
+ };
+
+ const cache = createCache(
+ [functions[2]],
+ null
+ );
+
+ plugin.run(cache, {}, callback);
+ });
+
+ it('should give failing result if Cloud Function is not configured with Serverless VPC Access', function (done) {
+ const callback = (err, results) => {
+ expect(results.length).to.be.above(0);
+ expect(results[0].status).to.equal(2);
+ expect(results[0].message).to.include('Cloud Function is not configured with Serverless VPC Access');
+ expect(results[0].region).to.equal('us-central1');
+ done();
+ };
+
+ const cache = createCache(
+ [functions[0]],
+ null
+ );
+
+ plugin.run(cache, {}, callback);
+ });
+
+ it('should not check Gen 1 functions in v2 API response', function (done) {
+ const callback = (err, results) => {
+ expect(results.length).to.equal(0);
+ done();
+ };
+
+ const cache = createCache(
+ [functions[3]],
+ null
+ );
+
+ plugin.run(cache, {}, callback);
+ });
+
+ })
+});
+
diff --git a/plugins/google/cloudfunctionsv2/functionV2DefaultServiceAccount.js b/plugins/google/cloudfunctionsv2/functionV2DefaultServiceAccount.js
new file mode 100644
index 000000000..cceeadedb
--- /dev/null
+++ b/plugins/google/cloudfunctionsv2/functionV2DefaultServiceAccount.js
@@ -0,0 +1,65 @@
+var async = require('async');
+var helpers = require('../../../helpers/google');
+
+module.exports = {
+ title: 'Cloud Function V2 Default Service Account',
+ category: 'Cloud Functions',
+ domain: 'Serverless',
+ severity: 'Medium',
+ description: 'Ensure that Cloud Functions V2 are not using the default service account.',
+ more_info: 'Using the default service account for Cloud Functions V2 can lead to privilege escalation and overly permissive access. It is recommended to use a user-managed service account for each function in a project instead of the default service account. A managed service account allows more precise access control by granting only the necessary permissions through Identity and Access Management (IAM).',
+ link: 'https://cloud.google.com/functions/docs/securing/function-identity',
+ recommended_action: 'Ensure that no Cloud Functions V2 are using the default service account.',
+ apis: ['functionsv2:list'],
+ realtime_triggers: ['functions.CloudFunctionsService.UpdateFunction', 'functions.CloudFunctionsService.CreateFunction', 'functions.CloudFunctionsService.DeleteFunction'],
+
+ run: function(cache, settings, callback) {
+ var results = [];
+ var source = {};
+ var regions = helpers.regions();
+
+ async.each(regions.functions, (region, rcb) => {
+ var functions = helpers.addSource(cache, source,
+ ['functionsv2', 'list', region]);
+
+ if (!functions) return rcb();
+
+ if (functions.err || !functions.data) {
+ helpers.addResult(results, 3,
+ 'Unable to query for Google Cloud functions: ' + helpers.addError(functions), region, null, null, functions.err);
+ return rcb();
+ }
+
+ if (!functions.data.length) {
+ helpers.addResult(results, 0, 'No Google Cloud functions found', region);
+ return rcb();
+ }
+
+ functions.data.forEach(func => {
+ if (!func.name) return;
+
+ if (!func.environment || func.environment !== 'GEN_2') return;
+
+ let serviceAccountEmail = func.serviceConfig && func.serviceConfig.serviceAccountEmail
+ ? func.serviceConfig.serviceAccountEmail
+ : null;
+
+ if (serviceAccountEmail && serviceAccountEmail.endsWith('@appspot.gserviceaccount.com')) {
+ helpers.addResult(results, 2,
+ 'Cloud Function is using default service account', region, func.name);
+ } else if (serviceAccountEmail) {
+ helpers.addResult(results, 0,
+ 'Cloud Function is not using default service account', region, func.name);
+ } else {
+ helpers.addResult(results, 2,
+ 'Cloud Function does not have a service account configured', region, func.name);
+ }
+ });
+
+ rcb();
+ }, function() {
+ callback(null, results, source);
+ });
+ }
+};
+
diff --git a/plugins/google/cloudfunctionsv2/functionV2DefaultServiceAccount.spec.js b/plugins/google/cloudfunctionsv2/functionV2DefaultServiceAccount.spec.js
new file mode 100644
index 000000000..d27f40ab9
--- /dev/null
+++ b/plugins/google/cloudfunctionsv2/functionV2DefaultServiceAccount.spec.js
@@ -0,0 +1,173 @@
+var expect = require('chai').expect;
+var plugin = require('./functionV2DefaultServiceAccount');
+
+
+const functions = [
+ {
+ "name": "projects/my-test-project/locations/us-central1/functions/function-1",
+ "environment": "GEN_2",
+ "state": "ACTIVE",
+ "updateTime": "2021-09-24T06:18:15.265Z",
+ "buildConfig": {
+ "runtime": "nodejs20",
+ "entryPoint": "helloWorld"
+ },
+ "serviceConfig": {
+ "serviceAccountEmail": "khulnasoft@appspot.gserviceaccount.com",
+ "ingressSettings": "ALLOW_ALL"
+ }
+ },
+ {
+ "name": "projects/my-test-project/locations/us-central1/functions/function-2",
+ "environment": "GEN_2",
+ "state": "ACTIVE",
+ "updateTime": "2021-09-24T06:18:15.265Z",
+ "buildConfig": {
+ "runtime": "nodejs20",
+ "entryPoint": "helloWorld"
+ },
+ "serviceConfig": {
+ "serviceAccountEmail": "custom-sa@my-test-project.iam.gserviceaccount.com",
+ "ingressSettings": "ALLOW_INTERNAL_AND_GCLB"
+ },
+ "labels": { 'deployment-tool': 'console-cloud' }
+ },
+ {
+ "name": "projects/my-test-project/locations/us-central1/functions/function-3",
+ "environment": "GEN_2",
+ "state": "ACTIVE",
+ "updateTime": "2021-09-24T06:18:15.265Z",
+ "buildConfig": {
+ "runtime": "nodejs20",
+ "entryPoint": "helloWorld"
+ },
+ "serviceConfig": {
+ "ingressSettings": "ALLOW_INTERNAL_ONLY"
+ }
+ },
+ {
+ "name": "projects/my-test-project/locations/us-central1/functions/function-4",
+ "environment": "GEN_1",
+ "state": "ACTIVE",
+ "runtime": "nodejs14",
+ "serviceAccountEmail": "khulnasoft@appspot.gserviceaccount.com"
+ }
+];
+
+const createCache = (list, err) => {
+ return {
+ functionsv2: {
+ list: {
+ 'us-central1': {
+ err: err,
+ data: list
+ }
+ }
+ }
+ }
+};
+
+describe('functionDefaultServiceAccount', function () {
+ describe('run', function () {
+ it('should give passing result if no Cloud Functions V2 found', function (done) {
+ const callback = (err, results) => {
+ expect(results.length).to.be.above(0);
+ expect(results[0].status).to.equal(0);
+ expect(results[0].message).to.include('No Google Cloud functions found');
+ expect(results[0].region).to.equal('us-central1');
+ done()
+ };
+
+ const cache = createCache(
+ [],
+ null
+ );
+
+ plugin.run(cache, {}, callback);
+ });
+
+ it('should give unknown result if unable to query for Google Cloud functions', function (done) {
+ const callback = (err, results) => {
+ expect(results.length).to.be.above(0);
+ expect(results[0].status).to.equal(3);
+ expect(results[0].message).to.include('Unable to query for Google Cloud functions');
+ expect(results[0].region).to.equal('us-central1');
+ done()
+ };
+
+ const cache = createCache(
+ [],
+ {message: 'error'},
+ );
+
+ plugin.run(cache, {}, callback);
+ });
+
+ it('should give passing result if Cloud Function is not using default service account', function (done) {
+ const callback = (err, results) => {
+ expect(results.length).to.be.above(0);
+ expect(results[0].status).to.equal(0);
+ expect(results[0].message).to.include('Cloud Function is not using default service account');
+ expect(results[0].region).to.equal('us-central1');
+ done()
+ };
+
+ const cache = createCache(
+ [functions[1]],
+ null
+ );
+
+ plugin.run(cache, {}, callback);
+ });
+
+ it('should give failing result if Cloud Function is using default service account', function (done) {
+ const callback = (err, results) => {
+ expect(results.length).to.be.above(0);
+ expect(results[0].status).to.equal(2);
+ expect(results[0].message).to.include('Cloud Function is using default service account');
+ expect(results[0].region).to.equal('us-central1');
+ done();
+ };
+
+ const cache = createCache(
+ [functions[0]],
+ null
+ );
+
+ plugin.run(cache, {}, callback);
+ });
+
+ it('should give failing result if Cloud Function does not have a service account configured', function (done) {
+ const callback = (err, results) => {
+ expect(results.length).to.be.above(0);
+ expect(results[0].status).to.equal(2);
+ expect(results[0].message).to.include('Cloud Function does not have a service account configured');
+ expect(results[0].region).to.equal('us-central1');
+ done();
+ };
+
+ const cache = createCache(
+ [functions[2]],
+ null
+ );
+
+ plugin.run(cache, {}, callback);
+ });
+
+ it('should not check Gen 1 functions in v2 API response', function (done) {
+ const callback = (err, results) => {
+ expect(results.length).to.equal(0);
+ done();
+ };
+
+ const cache = createCache(
+ [functions[3]],
+ null
+ );
+
+ plugin.run(cache, {}, callback);
+ });
+
+ })
+});
+
diff --git a/plugins/google/compute/computePrivilegeAnalysis.js b/plugins/google/compute/computePrivilegeAnalysis.js
new file mode 100644
index 000000000..49c0b3b1b
--- /dev/null
+++ b/plugins/google/compute/computePrivilegeAnalysis.js
@@ -0,0 +1,20 @@
+module.exports = {
+ title: 'Privilege Analysis',
+ category: 'Compute',
+ domain: 'Compute',
+ severity: 'Info',
+ description: 'Ensures that no compute instances in your cloud has excessive permissions.',
+ more_info: 'Compute instances having service account attached with excessive permissions can lead to security risks. Compute instances should have restrictive permissions assigned through service accounts for security best practices.',
+ link: 'https://cloud.google.com/compute/docs/access/iam',
+ recommended_action: 'Make sure that compute instances are using service account with only required permissions.',
+ apis: [''],
+ realtime_triggers: ['compute.instances.insert', 'compute.instances.delete'],
+
+ run: function(cache, settings, callback) {
+ var results = [];
+ var source = {};
+
+ callback(null, results, source);
+
+ }
+};
diff --git a/plugins/google/compute/instanceLeastPrivilege.js b/plugins/google/compute/instanceLeastPrivilege.js
index 7974b2a8d..654b34186 100644
--- a/plugins/google/compute/instanceLeastPrivilege.js
+++ b/plugins/google/compute/instanceLeastPrivilege.js
@@ -1,4 +1,4 @@
-var async = require('async');
+var async = require('async');
var helpers = require('../../../helpers/google');
module.exports = {
@@ -10,7 +10,7 @@ module.exports = {
more_info: 'To support the principle of least privilege and prevent potential privilege escalation, it is recommended that instances are not assigned to the default service account, Compute Engine default service account with a scope allowing full access to all cloud APIs.',
link: 'https://cloud.google.com/compute/docs/access/create-enable-service-accounts-for-instances',
recommended_action: 'For all instances, if the default service account is used, ensure full access to all cloud APIs is not configured.',
- apis: ['compute:list'],
+ apis: ['compute:list', 'projects:getIamPolicy'],
compliance: {
pci: 'PCI has explicit requirements around default accounts and ' +
'resources. PCI recommends removing all default accounts, ' +
@@ -35,52 +35,104 @@ module.exports = {
var project = projects.data[0].name;
- async.each(regions.compute, (region, rcb) => {
- var zones = regions.zones;
- var noInstances = [];
+ var serviceAccountRoles = {};
- async.each(zones[region], function(zone, zcb) {
- var instances = helpers.addSource(cache, source,
- ['compute','list', zone]);
+ async.each(regions.projects, function(region, rcb) {
+ let iamPolicy = helpers.addSource(cache, source,
+ ['projects', 'getIamPolicy', region]);
- if (!instances) return zcb();
+ if (!iamPolicy) return rcb();
- if (instances.err || !instances.data) {
- helpers.addResult(results, 3, 'Unable to query compute instances', region, null, null, instances.err);
- return zcb();
- }
+ if (iamPolicy.err || !iamPolicy.data || !iamPolicy.data.length) {
+ helpers.addResult(results, 3,
+ 'Unable to query for IAM policies: ' + helpers.addError(iamPolicy), region);
+ return rcb();
+ }
- if (!instances.data.length) {
- noInstances.push(zone);
- return zcb();
- }
+ var iamPolicyData = iamPolicy.data[0];
+
+ if (iamPolicyData && iamPolicyData.bindings && iamPolicyData.bindings.length) {
+ iamPolicyData.bindings.forEach(roleBinding => {
+ if (!roleBinding.role || !roleBinding.members) return;
+
+ var role = roleBinding.role;
+
+ roleBinding.members.forEach(member => {
+ if (member.startsWith('serviceAccount:')) {
+ var serviceAccountEmail = member.split(':')[1];
+
+ if (!serviceAccountRoles[serviceAccountEmail]) {
+ serviceAccountRoles[serviceAccountEmail] = [];
+ }
+ serviceAccountRoles[serviceAccountEmail].push(role);
+ }
+ });
+ });
+ }
+
+ rcb();
+ }, function() {
+ async.each(regions.compute, (computeRegion, computeRcb) => {
+ var zones = regions.zones;
+ var noInstances = [];
- instances.data.forEach(instance => {
- let found = false;
- if (instance.serviceAccounts && instance.serviceAccounts.length) {
- found = instance.serviceAccounts.find(serviceAccount => serviceAccount.scopes &&
- serviceAccount.scopes.indexOf('https://www.googleapis.com/auth/cloud-platform') > -1);
+ async.each(zones[computeRegion], function(zone, zcb) {
+ var instances = helpers.addSource(cache, source,
+ ['compute', 'list', zone]);
+
+ if (!instances) return zcb();
+
+ if (instances.err || !instances.data) {
+ helpers.addResult(results, 3, 'Unable to query compute instances', computeRegion, null, null, instances.err);
+ return zcb();
+ }
+
+ if (!instances.data.length) {
+ noInstances.push(zone);
+ return zcb();
}
- let resource = helpers.createResourceName('instances', instance.name, project, 'zone', zone);
+ instances.data.forEach(instance => {
+ let resource = helpers.createResourceName('instances', instance.name, project, 'zone', zone);
- if (found) {
- helpers.addResult(results, 2,
- 'Instance Service account has full access' , region, resource);
- } else {
- helpers.addResult(results, 0,
- 'Instance Service account follows least privilege' , region, resource);
+ let instanceServiceAccountEmail = null;
+ let hasBroadRole = false;
+
+ if (instance.serviceAccounts && instance.serviceAccounts.length) {
+ instance.serviceAccounts.forEach(serviceAccount => {
+ if (serviceAccount.email) {
+ instanceServiceAccountEmail = serviceAccount.email;
+ var roles = serviceAccountRoles[serviceAccount.email] || [];
+ var broadRoles = roles.filter(role =>
+ role === 'roles/owner' ||
+ role === 'roles/editor' ||
+ role.endsWith('.admin')
+ );
+ if (broadRoles.length > 0) {
+ hasBroadRole = true;
+ }
+ }
+ });
+ }
+
+ if (hasBroadRole && instanceServiceAccountEmail) {
+ helpers.addResult(results, 2,
+ 'Instance Service account has full access', computeRegion, resource);
+ } else {
+ helpers.addResult(results, 0,
+ 'Instance service account follows least privilege', computeRegion, resource);
+ }
+ });
+ return zcb();
+ }, function() {
+ if (noInstances.length) {
+ helpers.addResult(results, 0, `No instances found in following zones: ${noInstances.join(', ')}`, computeRegion);
}
+ computeRcb();
});
- return zcb();
- }, function(){
- if (noInstances.length) {
- helpers.addResult(results, 0, `No instances found in following zones: ${noInstances.join(', ')}`, region);
- }
- rcb();
+ }, function() {
+ callback(null, results, source);
});
- }, function() {
- callback(null, results, source);
});
}
};
diff --git a/plugins/google/compute/instanceLeastPrivilege.spec.js b/plugins/google/compute/instanceLeastPrivilege.spec.js
index 7d12b4218..a63428618 100644
--- a/plugins/google/compute/instanceLeastPrivilege.spec.js
+++ b/plugins/google/compute/instanceLeastPrivilege.spec.js
@@ -2,20 +2,28 @@ var assert = require('assert');
var expect = require('chai').expect;
var plugin = require('./instanceLeastPrivilege');
-const createCache = (instanceData, error) => {
+const createCache = (instanceData, error, iamPolicyData, defaultServiceAccount) => {
return {
- compute: {
- list: {
- 'us-central1-a': {
- data: instanceData,
- err: error
- }
+ compute: {
+ list: {
+ 'us-central1-a': {
+ data: instanceData,
+ err: error
}
+ }
},
projects: {
get: {
'global': {
- data: 'test-proj'
+ data: [{
+ name: 'test-proj',
+ defaultServiceAccount: defaultServiceAccount || '123456789-compute@developer.gserviceaccount.com'
+ }]
+ }
+ },
+ getIamPolicy: {
+ 'global': {
+ data: iamPolicyData || []
}
}
}
@@ -33,9 +41,16 @@ describe('instanceLeastPrivilege', function () {
done()
};
+ const defaultSA = '123456789-compute@developer.gserviceaccount.com';
+ const iamPolicy = [{
+ bindings: []
+ }];
+
const cache = createCache(
[],
- ['error']
+ ['error'],
+ iamPolicy,
+ defaultSA
);
plugin.run(cache, {}, callback);
@@ -50,15 +65,22 @@ describe('instanceLeastPrivilege', function () {
done()
};
+ const defaultSA = '123456789-compute@developer.gserviceaccount.com';
+ const iamPolicy = [{
+ bindings: []
+ }];
+
const cache = createCache(
[],
- null
+ null,
+ iamPolicy,
+ defaultSA
);
plugin.run(cache, {}, callback);
});
- it('should fail with full access service account', function (done) {
+ it('should fail when default service account has broad IAM role (editor)', function (done) {
const callback = (err, results) => {
expect(results.length).to.be.above(0);
expect(results[0].status).to.equal(2);
@@ -67,6 +89,18 @@ describe('instanceLeastPrivilege', function () {
done()
};
+ const defaultSA = '123456789-compute@developer.gserviceaccount.com';
+ const iamPolicy = [{
+ bindings: [
+ {
+ role: 'roles/editor',
+ members: [
+ 'serviceAccount:' + defaultSA
+ ]
+ }
+ ]
+ }];
+
const cache = createCache(
[
{
@@ -76,7 +110,7 @@ describe('instanceLeastPrivilege', function () {
'https://www.googleapis.com/compute/v1/projects/lofty-advantage-242315/zones/us-central1-a',
serviceAccounts: [
{
- email: '413092707322-compute@developer.gserviceaccount.com',
+ email: defaultSA,
scopes: [
'https://www.googleapis.com/auth/cloud-platform'
]
@@ -84,21 +118,35 @@ describe('instanceLeastPrivilege', function () {
]
}
],
- null
+ null,
+ iamPolicy,
+ defaultSA
);
plugin.run(cache, {}, callback);
});
- it('should pass with no full access service account', function (done) {
+ it('should pass when default service account has restricted IAM roles', function (done) {
const callback = (err, results) => {
expect(results.length).to.be.above(0);
expect(results[0].status).to.equal(0);
- expect(results[0].message).to.include('Instance Service account follows least privilege');
+ expect(results[0].message).to.include('follows least privilege');
expect(results[0].region).to.equal('us-central1');
done()
};
+ const defaultSA = '123456789-compute@developer.gserviceaccount.com';
+ const iamPolicy = [{
+ bindings: [
+ {
+ role: 'roles/storage.objectViewer',
+ members: [
+ 'serviceAccount:' + defaultSA
+ ]
+ }
+ ]
+ }];
+
const cache = createCache(
[
{
@@ -108,19 +156,160 @@ describe('instanceLeastPrivilege', function () {
'https://www.googleapis.com/compute/v1/projects/lofty-advantage-242315/zones/us-central1-a',
serviceAccounts: [
{
- email: '413092707322-compute@developer.gserviceaccount.com',
+ email: defaultSA,
scopes: [
- 'https://www.googleapis.com/auth/devstorage.read_only',
- 'https://www.googleapis.com/auth/logging.write'
+ 'https://www.googleapis.com/auth/cloud-platform'
]
}
]
}
+ ],
+ null,
+ iamPolicy,
+ defaultSA
+ );
+
+ plugin.run(cache, {}, callback);
+ });
+
+ it('should fail when custom service account has broad IAM role', function (done) {
+ const callback = (err, results) => {
+ expect(results.length).to.be.above(0);
+ expect(results[0].status).to.equal(2);
+ expect(results[0].message).to.include('Instance Service account has full access');
+ expect(results[0].region).to.equal('us-central1');
+ done()
+ };
+
+ const defaultSA = '123456789-compute@developer.gserviceaccount.com';
+ const customSA = 'custom-sa@test-proj.iam.gserviceaccount.com';
+ const iamPolicy = [{
+ bindings: [
+ {
+ role: 'roles/editor',
+ members: [
+ 'serviceAccount:' + customSA
+ ]
+ }
]
+ }];
+
+ const cache = createCache(
+ [
+ {
+ name: 'instance-1',
+ description: '',
+ zone:
+ 'https://www.googleapis.com/compute/v1/projects/lofty-advantage-242315/zones/us-central1-a',
+ serviceAccounts: [
+ {
+ email: customSA,
+ scopes: [
+ 'https://www.googleapis.com/auth/cloud-platform'
+ ]
+ }
+ ]
+ }
+ ],
+ null,
+ iamPolicy,
+ defaultSA
+ );
+
+ plugin.run(cache, {}, callback);
+ });
+
+ it('should fail when default service account has owner role', function (done) {
+ const callback = (err, results) => {
+ expect(results.length).to.be.above(0);
+ expect(results[0].status).to.equal(2);
+ expect(results[0].message).to.include('Instance Service account has full access');
+ expect(results[0].region).to.equal('us-central1');
+ done()
+ };
+
+ const defaultSA = '123456789-compute@developer.gserviceaccount.com';
+ const iamPolicy = [{
+ bindings: [
+ {
+ role: 'roles/owner',
+ members: [
+ 'serviceAccount:' + defaultSA
+ ]
+ }
+ ]
+ }];
+
+ const cache = createCache(
+ [
+ {
+ name: 'instance-1',
+ description: '',
+ zone:
+ 'https://www.googleapis.com/compute/v1/projects/lofty-advantage-242315/zones/us-central1-a',
+ serviceAccounts: [
+ {
+ email: defaultSA,
+ scopes: [
+ 'https://www.googleapis.com/auth/cloud-platform'
+ ]
+ }
+ ]
+ }
+ ],
+ null,
+ iamPolicy,
+ defaultSA
+ );
+
+ plugin.run(cache, {}, callback);
+ });
+
+ it('should fail when default service account has admin role', function (done) {
+ const callback = (err, results) => {
+ expect(results.length).to.be.above(0);
+ expect(results[0].status).to.equal(2);
+ expect(results[0].message).to.include('Instance Service account has full access');
+ expect(results[0].region).to.equal('us-central1');
+ done()
+ };
+
+ const defaultSA = '123456789-compute@developer.gserviceaccount.com';
+ const iamPolicy = [{
+ bindings: [
+ {
+ role: 'roles/compute.admin',
+ members: [
+ 'serviceAccount:' + defaultSA
+ ]
+ }
+ ]
+ }];
+
+ const cache = createCache(
+ [
+ {
+ name: 'instance-1',
+ description: '',
+ zone:
+ 'https://www.googleapis.com/compute/v1/projects/lofty-advantage-242315/zones/us-central1-a',
+ serviceAccounts: [
+ {
+ email: defaultSA,
+ scopes: [
+ 'https://www.googleapis.com/auth/cloud-platform'
+ ]
+ }
+ ]
+ }
+ ],
+ null,
+ iamPolicy,
+ defaultSA
);
plugin.run(cache, {}, callback);
})
})
-})
\ No newline at end of file
+})
diff --git a/plugins/google/compute/instanceNetworkExposure.js b/plugins/google/compute/instanceNetworkExposure.js
index d00d6299c..62b7336c9 100644
--- a/plugins/google/compute/instanceNetworkExposure.js
+++ b/plugins/google/compute/instanceNetworkExposure.js
@@ -2,7 +2,7 @@ var async = require('async');
var helpers = require('../../../helpers/google');
module.exports = {
- title: 'Network Exposure',
+ title: 'Internet Exposure',
category: 'Compute',
domain: 'Compute',
severity: 'Info',
@@ -78,6 +78,7 @@ module.exports = {
let serviceAccount = instance.serviceAccounts && instance.serviceAccounts[0] && instance.serviceAccounts[0].email ? instance.serviceAccounts[0].email : '';
let firewallRules = firewalls.data.filter(rule => {
+ if (!rule.network) return false;
let isNetworkMatch = networks.some(network => rule.network.endsWith(network));
let isTagMatch = rule.targetTags ? rule.targetTags.some(tag => tags.includes(tag)) : true;
diff --git a/plugins/google/kubernetes/clusterNetworkExposure.js b/plugins/google/kubernetes/clusterNetworkExposure.js
index 44540c889..f7572be4f 100644
--- a/plugins/google/kubernetes/clusterNetworkExposure.js
+++ b/plugins/google/kubernetes/clusterNetworkExposure.js
@@ -2,7 +2,7 @@ var async = require('async');
var helpers = require('../../../helpers/google');
module.exports = {
- title: 'Network Exposure',
+ title: 'Internet Exposure',
category: 'Kubernetes',
domain: 'Containers',
severity: 'Info',
@@ -30,19 +30,18 @@ module.exports = {
var project = projects.data[0].name;
- async.each(regions.kubernetes, function(region, rcb){
-
- let firewalls = helpers.addSource(
- cache, source, ['firewalls', 'list', 'global']);
-
- if (!firewalls || firewalls.err || !firewalls.data) {
- helpers.addResult(results, 3, 'Unable to query firewall rules', region, null, null, firewalls.err);
- }
-
- if (!firewalls.data.length) {
- helpers.addResult(results, 0, 'No firewall rules found', region);
- }
+ let firewalls = helpers.addSource(
+ cache, source, ['firewalls', 'list', 'global']);
+
+ if (!firewalls || firewalls.err || !firewalls.data) {
+ helpers.addResult(results, 3, 'Unable to query firewall rules', 'global', null, null, firewalls.err);
+ }
+ if (!firewalls.data.length) {
+ helpers.addResult(results, 0, 'No firewall rules found', 'global');
+ }
+
+ async.each(regions.kubernetes, function(region, rcb){
let clusters = helpers.addSource(cache, source,
['kubernetes', 'list', region]);
@@ -71,16 +70,19 @@ module.exports = {
if (helpers.checkClusterExposure(cluster)) {
internetExposed = 'public endpoint access';
} else {
- let clusterNetwork = cluster.networkConfig ? cluster.networkConfig.network : cluster.network;
- if (!clusterNetwork.includes('/')) clusterNetwork = `${clusterNetwork}`;
- let firewallRules = firewalls.data.filter(rule => rule.network.endsWith(clusterNetwork));
+ let clusterNetwork = cluster.networkConfig && cluster.networkConfig.network ? cluster.networkConfig.network : cluster.network;
+ if (clusterNetwork && !clusterNetwork.includes('/')) clusterNetwork = `${clusterNetwork}`;
+ let firewallRules = firewalls.data.filter(rule => {
+ return rule.network && rule.network.endsWith(clusterNetwork);
+ });
+
let isExposed = helpers.checkFirewallRules(firewallRules);
if (isExposed && isExposed.exposed && isExposed.networkName) {
internetExposed = isExposed.networkName;
} else {
// check node pools
- let exposedNodePools = cluster.nodePools.filter(nodepool => nodepool.networkConfig && !nodepool.networkConfig.enablePrivateNodes).map(nodepool => nodepool.name);
+ let exposedNodePools = Array.isArray(cluster.nodePools) ? cluster.nodePools.filter(nodepool => nodepool.networkConfig && !nodepool.networkConfig.enablePrivateNodes).map(nodepool => nodepool.name) : [] ;
if (exposedNodePools.length) {
internetExposed = `node pools ${exposedNodePools.join(',')}`;
}
diff --git a/plugins/google/kubernetes/kubernetesPrivilegeAnalysis.js b/plugins/google/kubernetes/kubernetesPrivilegeAnalysis.js
new file mode 100644
index 000000000..8d61c30a3
--- /dev/null
+++ b/plugins/google/kubernetes/kubernetesPrivilegeAnalysis.js
@@ -0,0 +1,25 @@
+module.exports = {
+ title: 'Privilege Analysis',
+ category: 'Kubernetes',
+ domain: 'Containers',
+ severity: 'Info',
+ description: 'Ensures that Kubernetes workloads and service accounts are not granted excessive permissions.',
+ more_info: 'Kubernetes workloads often use service accounts to interact with the Kubernetes API and other GCP resources. Over-privileged service accounts can lead to privilege escalation or lateral movement within the cluster or the cloud environment. Following the principle of least privilege helps minimize potential attack surfaces.',
+ link: 'https://cloud.google.com/kubernetes-engine/docs/how-to/iam',
+ recommended_action: 'Review and minimize IAM permissions granted to Kubernetes service accounts and workload identities. Use role-based access control (RBAC) and GCP IAM best practices to ensure only required access is permitted.',
+ apis: [''],
+ realtime_triggers: [
+ 'container.projects.updateCluster',
+ 'container.projects.createCluster',
+ 'container.projects.deleteCluster',
+ 'iam.serviceAccounts.setIamPolicy',
+ 'iam.serviceAccounts.getIamPolicy'
+ ],
+
+ run: function(cache, settings, callback) {
+ var results = [];
+ var source = {};
+
+ callback(null, results, source);
+ }
+};