chore(merge): update v4 with latest changes of v3 (#3653)

This commit is contained in:
Sergio Garcia
2024-04-03 18:31:36 +02:00
committed by GitHub
parent 191cf276c3
commit 63412e3645
58 changed files with 7332 additions and 213 deletions
@@ -3,6 +3,8 @@ name: build-lint-push-containers
on:
push:
branches:
# TODO: update it for v3 and v4
# - "v3"
- "master"
paths-ignore:
- ".github/**"
@@ -13,15 +15,27 @@ on:
types: [published]
env:
# AWS Configuration
AWS_REGION_STG: eu-west-1
AWS_REGION_PLATFORM: eu-west-1
AWS_REGION: us-east-1
# Container's configuration
IMAGE_NAME: prowler
DOCKERFILE_PATH: ./Dockerfile
# Tags
LATEST_TAG: latest
STABLE_TAG: stable
TEMPORARY_TAG: temporary
DOCKERFILE_PATH: ./Dockerfile
PYTHON_VERSION: 3.9
# The RELEASE_TAG is set during runtime in releases
RELEASE_TAG: ""
# The PROWLER_VERSION and PROWLER_VERSION_MAJOR are set during runtime in releases
PROWLER_VERSION: ""
PROWLER_VERSION_MAJOR: ""
# TEMPORARY_TAG: temporary
# Python configuration
PYTHON_VERSION: 3.11
jobs:
# Build Prowler OSS container
@@ -30,26 +44,57 @@ jobs:
runs-on: ubuntu-latest
env:
POETRY_VIRTUALENVS_CREATE: "false"
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup python (release)
if: github.event_name == 'release'
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install dependencies (release)
if: github.event_name == 'release'
- name: Install Poetry
run: |
pipx install poetry
pipx inject poetry poetry-bumpversion
- name: Get Prowler version
run: |
PROWLER_VERSION="$(poetry version -s 2>/dev/null)"
# Store prowler version major just for the release
PROWLER_VERSION_MAJOR="${PROWLER_VERSION%%.*}"
echo "PROWLER_VERSION_MAJOR=${PROWLER_VERSION_MAJOR}" >> "${GITHUB_ENV}"
case ${PROWLER_VERSION_MAJOR} in
3)
# TODO: update it for v3 and v4
# echo "LATEST=v3-latest" >> "${GITHUB_ENV}"
# echo "STABLE_TAG=v3-stable" >> "${GITHUB_ENV}"
echo "LATEST=latest" >> "${GITHUB_ENV}"
echo "STABLE_TAG=stable" >> "${GITHUB_ENV}"
;;
# TODO: uncomment for v3 and v4
# 4)
# echo "LATEST=latest" >> "${GITHUB_ENV}"
# echo "STABLE_TAG=stable" >> "${GITHUB_ENV}"
# ;;
*)
# Fallback if any other version is present
echo "Releasing another Prowler major version, aborting..."
exit 1
;;
esac
- name: Update Prowler version (release)
if: github.event_name == 'release'
run: |
poetry version ${{ github.event.release.tag_name }}
PROWLER_VERSION="${{ github.event.release.tag_name }}"
poetry version "${PROWLER_VERSION}"
echo "PROWLER_VERSION="${PROWLER_VERSION}" >> "${GITHUB_ENV}"
- name: Login to DockerHub
uses: docker/login-action@v3
@@ -90,9 +135,9 @@ jobs:
context: .
push: true
tags: |
${{ secrets.DOCKER_HUB_REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ github.event.release.tag_name }}
${{ secrets.DOCKER_HUB_REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ env.PROWLER_VERSION }}
${{ secrets.DOCKER_HUB_REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ env.STABLE_TAG }}
${{ secrets.PUBLIC_ECR_REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ github.event.release.tag_name }}
${{ secrets.PUBLIC_ECR_REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ env.PROWLER_VERSION }}
${{ secrets.PUBLIC_ECR_REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ env.STABLE_TAG }}
file: ${{ env.DOCKERFILE_PATH }}
cache-from: type=gha
@@ -102,16 +147,26 @@ jobs:
needs: container-build-push
runs-on: ubuntu-latest
steps:
- name: Get latest commit info
- name: Get latest commit info (latest)
if: github.event_name == 'push'
run: |
LATEST_COMMIT_HASH=$(echo ${{ github.event.after }} | cut -b -7)
echo "LATEST_COMMIT_HASH=${LATEST_COMMIT_HASH}" >> $GITHUB_ENV
- name: Dispatch event for latest
if: github.event_name == 'push'
- name: Dispatch event (latest)
if: github.event_name == 'push' && ${{ env. PROWLER_VERSION_MAJOR }} == '3'
run: |
curl https://api.github.com/repos/${{ secrets.DISPATCH_OWNER }}/${{ secrets.DISPATCH_REPO }}/dispatches -H "Accept: application/vnd.github+json" -H "Authorization: Bearer ${{ secrets.ACCESS_TOKEN }}" -H "X-GitHub-Api-Version: 2022-11-28" --data '{"event_type":"dispatch","client_payload":{"version":"latest", "tag": "${{ env.LATEST_COMMIT_HASH }}"}}'
- name: Dispatch event for release
if: github.event_name == 'release'
curl https://api.github.com/repos/${{ secrets.DISPATCH_OWNER }}/${{ secrets.DISPATCH_REPO }}/dispatches \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${{ secrets.ACCESS_TOKEN }}" \
-H "X-GitHub-Api-Version: 2022-11-28" \
--data '{"event_type":"dispatch","client_payload":{"version":"latest", "tag": "${{ env.LATEST_COMMIT_HASH }}"}}'
- name: Dispatch event (release)
if: github.event_name == 'release' && ${{ env. PROWLER_VERSION_MAJOR }} == '3'
run: |
curl https://api.github.com/repos/${{ secrets.DISPATCH_OWNER }}/${{ secrets.DISPATCH_REPO }}/dispatches -H "Accept: application/vnd.github+json" -H "Authorization: Bearer ${{ secrets.ACCESS_TOKEN }}" -H "X-GitHub-Api-Version: 2022-11-28" --data '{"event_type":"dispatch","client_payload":{"version":"release", "tag":"${{ github.event.release.tag_name }}"}}'
curl https://api.github.com/repos/${{ secrets.DISPATCH_OWNER }}/${{ secrets.DISPATCH_REPO }}/dispatches \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${{ secrets.ACCESS_TOKEN }}" \
-H "X-GitHub-Api-Version: 2022-11-28" \
--data '{"event_type":"dispatch","client_payload":{"version":"release", "tag":"${{ env.PROWLER_VERSION }}"}}'
+1 -1
View File
@@ -11,7 +11,7 @@ jobs:
with:
fetch-depth: 0
- name: TruffleHog OSS
uses: trufflesecurity/trufflehog@v3.71.0
uses: trufflesecurity/trufflehog@v3.71.2
with:
path: ./
base: ${{ github.event.repository.default_branch }}
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
- uses: actions/checkout@v4
- name: Test if changes are in not ignored paths
id: are-non-ignored-files-changed
uses: tj-actions/changed-files@v43
uses: tj-actions/changed-files@v44
with:
files: ./**
files_ignore: |
+2 -35
View File
@@ -8,10 +8,7 @@ env:
RELEASE_TAG: ${{ github.event.release.tag_name }}
PYTHON_VERSION: 3.11
CACHE: "poetry"
# This base branch is used to create a PR with the updated version
# We'd need to handle the base branch for v4 and v3, since they will be
# `master` and `3.0-dev`, respectively
GITHUB_BASE_BRANCH: "master"
# TODO: create a bot user for this kind of tasks, like prowler-bot
GIT_COMMITTER_EMAIL: "sergio@prowler.com"
jobs:
@@ -21,24 +18,16 @@ jobs:
POETRY_VIRTUALENVS_CREATE: "false"
name: Release Prowler to PyPI
steps:
- name: Get base branch regarding Prowler version
- name: Get Prowler version
run: |
PROWLER_VERSION="${{ env.RELEASE_TAG }}"
BASE_BRANCH=""
case ${PROWLER_VERSION%%.*} in
3)
echo "Releasing Prowler v3 with tag ${PROWLER_VERSION}"
# TODO: modify it once v4 is ready
# echo "BASE_BRANCH=3.0-dev" >> "${GITHUB_ENV}"
echo "BASE_BRANCH=master" >> "${GITHUB_ENV}"
;;
4)
echo "Releasing Prowler v4 with tag ${PROWLER_VERSION}"
# TODO: modify it once v4 is ready
# echo "BASE_BRANCH=master" >> "${GITHUB_ENV}"
echo "Not available, aborting..."
exit 1
;;
*)
echo "Releasing another Prowler major version, aborting..."
@@ -87,11 +76,6 @@ jobs:
# Push the tag
git push -f origin ${{ env.RELEASE_TAG }}
- name: Create new branch for the version update
run: |
git switch -c release-${{ env.RELEASE_TAG }}
git push --set-upstream origin release-${{ env.RELEASE_TAG }}
- name: Build Prowler package
run: |
poetry build
@@ -101,23 +85,6 @@ jobs:
poetry config pypi-token.pypi ${{ secrets.PYPI_API_TOKEN }}
poetry publish
- name: Create PR to update version in the branch
run: |
echo "### Description
This PR updates Prowler Version to ${{ env.RELEASE_TAG }}.
### License
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license." |\
gh pr create \
--base ${{ env.BASE_BRANCH }} \
--head release-${{ env.RELEASE_TAG }} \
--title "chore(release): update Prowler Version to ${{ env.RELEASE_TAG }}." \
--body-file -
env:
GH_TOKEN: ${{ secrets.PROWLER_ACCESS_TOKEN }}
- name: Replicate PyPI package
run: |
rm -rf ./dist && rm -rf ./build && rm -rf prowler.egg-info
+1 -1
View File
@@ -49,7 +49,7 @@ It contains hundreds of controls covering CIS, NIST 800, NIST CSF, CISA, RBI, Fe
|---|---|---|---|---|
| AWS | 304 | 61 -> `prowler aws --list-services` | 28 -> `prowler aws --list-compliance` | 6 -> `prowler aws --list-categories` |
| GCP | 75 | 11 -> `prowler gcp --list-services` | 1 -> `prowler gcp --list-compliance` | 2 -> `prowler gcp --list-categories`|
| Azure | 109 | 16 -> `prowler azure --list-services` | CIS soon | 2 -> `prowler azure --list-categories` |
| Azure | 126 | 16 -> `prowler azure --list-services` | 2 -> `prowler azure --list-compliance` | 2 -> `prowler azure --list-categories` |
| Kubernetes | 83 | 7 -> `prowler kubernetes --list-services` | 1 -> `prowler kubernetes --list-compliance` | 7 -> `prowler kubernetes --list-categories` |
# 📖 Documentation
+5 -3
View File
@@ -64,16 +64,17 @@ The other three cases does not need additional configuration, `--az-cli-auth` an
To use each one you need to pass the proper flag to the execution. Prowler for Azure handles two types of permission scopes, which are:
- **Azure Active Directory permissions**: Used to retrieve metadata from the identity assumed by Prowler and future AAD checks (not mandatory to have access to execute the tool)
- **Microsoft Entra ID permissions**: Used to retrieve metadata from the identity assumed by Prowler (not mandatory to have access to execute the tool).
- **Subscription scope permissions**: Required to launch the checks against your resources, mandatory to launch the tool.
#### Azure Active Directory scope
#### Microsoft Entra ID scope
Microsoft Entra ID (AAD earlier) permissions required by the tool are the following:
- `Directory.Read.All`
- `Policy.Read.All`
- `UserAuthenticationMethod.Read.All`
The best way to assign it is through the Azure web console:
@@ -86,9 +87,10 @@ The best way to assign it is through the Azure web console:
5. In the left menu bar, select "API permissions"
6. Then click on "+ Add a permission" and select "Microsoft Graph"
7. Once in the "Microsoft Graph" view, select "Application permissions"
8. Finally, search for "Directory" and "Policy" and select the following permissions:
8. Finally, search for "Directory", "Policy" and "UserAuthenticationMethod" select the following permissions:
- `Directory.Read.All`
- `Policy.Read.All`
- `UserAuthenticationMethod.Read.All`
![EntraID Permissions](../img/AAD-permissions.png)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 358 KiB

After

Width:  |  Height:  |  Size: 376 KiB

+2
View File
@@ -30,6 +30,8 @@ Currently, the available frameworks are:
- `cis_1.5_aws`
- `cis_2.0_aws`
- `cis_2.0_gcp`
- `cis_2.0_azure`
- `cis_2.1_azure`
- `cis_3.0_aws`
- `cis_1.8_kubernetes`
- `cisa_aws`
Generated
+70 -31
View File
@@ -153,6 +153,17 @@ files = [
about-time = "4.2.1"
grapheme = "0.6.0"
[[package]]
name = "antlr4-python3-runtime"
version = "4.13.1"
description = "ANTLR 4.13.1 runtime for Python 3"
optional = false
python-versions = "*"
files = [
{file = "antlr4-python3-runtime-4.13.1.tar.gz", hash = "sha256:3cd282f5ea7cfb841537fe01f143350fdb1c0b1ce7981443a2fa8513fddb6d1a"},
{file = "antlr4_python3_runtime-4.13.1-py3-none-any.whl", hash = "sha256:78ec57aad12c97ac039ca27403ad61cb98aaec8a3f9bb8144f889aa0fa28b943"},
]
[[package]]
name = "anyio"
version = "4.3.0"
@@ -1598,20 +1609,20 @@ grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"]
[[package]]
name = "google-api-python-client"
version = "2.123.0"
version = "2.124.0"
description = "Google API Client Library for Python"
optional = false
python-versions = ">=3.7"
files = [
{file = "google-api-python-client-2.123.0.tar.gz", hash = "sha256:a17226b02f71de581afe045437b441844110a9cd91580b73549d41108cf1b9f0"},
{file = "google_api_python_client-2.123.0-py2.py3-none-any.whl", hash = "sha256:1c2bcaa846acf5bac4d6f244d8373d4de9de73d64eb6e77b56767ab4cf681419"},
{file = "google-api-python-client-2.124.0.tar.gz", hash = "sha256:f6d3258420f7c76b0f5266b5e402e6f804e30351b018a10083f4a46c3ec33773"},
{file = "google_api_python_client-2.124.0-py2.py3-none-any.whl", hash = "sha256:07dc674449ed353704b1169fdee792f74438d024261dad71b6ce7bb9c683d51f"},
]
[package.dependencies]
google-api-core = ">=1.31.5,<2.0.dev0 || >2.3.0,<3.0.0.dev0"
google-auth = ">=1.19.0,<3.0.0.dev0"
google-auth-httplib2 = ">=0.1.0"
httplib2 = ">=0.15.0,<1.dev0"
google-auth = ">=1.32.0,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0.dev0"
google-auth-httplib2 = ">=0.2.0,<1.0.0"
httplib2 = ">=0.19.0,<1.dev0"
uritemplate = ">=3.0.1,<5"
[[package]]
@@ -1967,6 +1978,20 @@ files = [
[package.dependencies]
jsonpointer = ">=1.9"
[[package]]
name = "jsonpath-ng"
version = "1.6.1"
description = "A final implementation of JSONPath for Python that aims to be standard compliant, including arithmetic and binary comparison operators and providing clear AST for metaprogramming."
optional = false
python-versions = "*"
files = [
{file = "jsonpath-ng-1.6.1.tar.gz", hash = "sha256:086c37ba4917304850bd837aeab806670224d3f038fe2833ff593a672ef0a5fa"},
{file = "jsonpath_ng-1.6.1-py3-none-any.whl", hash = "sha256:8f22cd8273d7772eea9aaa84d922e0841aa36fdb8a2c6b7f6c3791a16a9bc0be"},
]
[package.dependencies]
ply = "*"
[[package]]
name = "jsonpickle"
version = "3.0.3"
@@ -2425,13 +2450,13 @@ pytz = "*"
[[package]]
name = "mkdocs-material"
version = "9.5.15"
version = "9.5.17"
description = "Documentation that simply works"
optional = false
python-versions = ">=3.8"
files = [
{file = "mkdocs_material-9.5.15-py3-none-any.whl", hash = "sha256:e5c96dec3d19491de49ca643fc1dbb92b278e43cdb816c775bc47db77d9b62fb"},
{file = "mkdocs_material-9.5.15.tar.gz", hash = "sha256:39f03cca45e82bf54eb7456b5a18bd252eabfdd67f237a229471484a0a4d4635"},
{file = "mkdocs_material-9.5.17-py3-none-any.whl", hash = "sha256:14a2a60119a785e70e765dd033e6211367aca9fc70230e577c1cf6a326949571"},
{file = "mkdocs_material-9.5.17.tar.gz", hash = "sha256:06ae1275a72db1989cf6209de9e9ecdfbcfdbc24c58353877b2bb927dbe413e4"},
]
[package.dependencies]
@@ -2481,16 +2506,17 @@ test = ["pytest", "pytest-cov"]
[[package]]
name = "moto"
version = "5.0.3"
version = "5.0.4"
description = ""
optional = false
python-versions = ">=3.8"
files = [
{file = "moto-5.0.3-py2.py3-none-any.whl", hash = "sha256:261d312d1d69c2afccb450a0566666d7b75d76ed6a7d00aac278a9633b073ff0"},
{file = "moto-5.0.3.tar.gz", hash = "sha256:070ac2edf89ad7aee28534481ce68e2f344c8a6a8fefec5427eea0d599bfdbdb"},
{file = "moto-5.0.4-py2.py3-none-any.whl", hash = "sha256:4054360b882b6e7bab25d52d057e196b978b8d15f1921333f534c4d8f6510bbb"},
{file = "moto-5.0.4.tar.gz", hash = "sha256:8d19125d40c919cb40df62f4576904c2647c4e9a0e1ebc42491dd7787d09e107"},
]
[package.dependencies]
antlr4-python3-runtime = {version = "*", optional = true, markers = "extra == \"all\""}
aws-xray-sdk = {version = ">=0.93,<0.96 || >0.96", optional = true, markers = "extra == \"all\""}
boto3 = ">=1.9.201"
botocore = ">=1.14.0"
@@ -2501,9 +2527,10 @@ graphql-core = {version = "*", optional = true, markers = "extra == \"all\""}
Jinja2 = ">=2.10.1"
joserfc = {version = ">=0.9.0", optional = true, markers = "extra == \"all\""}
jsondiff = {version = ">=1.1.2", optional = true, markers = "extra == \"all\""}
jsonpath-ng = {version = "*", optional = true, markers = "extra == \"all\""}
multipart = {version = "*", optional = true, markers = "extra == \"all\""}
openapi-spec-validator = {version = ">=0.5.0", optional = true, markers = "extra == \"all\""}
py-partiql-parser = {version = "0.5.1", optional = true, markers = "extra == \"all\""}
py-partiql-parser = {version = "0.5.2", optional = true, markers = "extra == \"all\""}
pyparsing = {version = ">=3.0.7", optional = true, markers = "extra == \"all\""}
python-dateutil = ">=2.1,<3.0.0"
PyYAML = {version = ">=5.1", optional = true, markers = "extra == \"all\""}
@@ -2514,24 +2541,25 @@ werkzeug = ">=0.5,<2.2.0 || >2.2.0,<2.2.1 || >2.2.1"
xmltodict = "*"
[package.extras]
all = ["PyYAML (>=5.1)", "aws-xray-sdk (>=0.93,!=0.96)", "cfn-lint (>=0.40.0)", "docker (>=3.0.0)", "graphql-core", "joserfc (>=0.9.0)", "jsondiff (>=1.1.2)", "multipart", "openapi-spec-validator (>=0.5.0)", "py-partiql-parser (==0.5.1)", "pyparsing (>=3.0.7)", "setuptools"]
all = ["PyYAML (>=5.1)", "antlr4-python3-runtime", "aws-xray-sdk (>=0.93,!=0.96)", "cfn-lint (>=0.40.0)", "docker (>=3.0.0)", "graphql-core", "joserfc (>=0.9.0)", "jsondiff (>=1.1.2)", "jsonpath-ng", "multipart", "openapi-spec-validator (>=0.5.0)", "py-partiql-parser (==0.5.2)", "pyparsing (>=3.0.7)", "setuptools"]
apigateway = ["PyYAML (>=5.1)", "joserfc (>=0.9.0)", "openapi-spec-validator (>=0.5.0)"]
apigatewayv2 = ["PyYAML (>=5.1)", "openapi-spec-validator (>=0.5.0)"]
appsync = ["graphql-core"]
awslambda = ["docker (>=3.0.0)"]
batch = ["docker (>=3.0.0)"]
cloudformation = ["PyYAML (>=5.1)", "aws-xray-sdk (>=0.93,!=0.96)", "cfn-lint (>=0.40.0)", "docker (>=3.0.0)", "graphql-core", "joserfc (>=0.9.0)", "jsondiff (>=1.1.2)", "openapi-spec-validator (>=0.5.0)", "py-partiql-parser (==0.5.1)", "pyparsing (>=3.0.7)", "setuptools"]
cloudformation = ["PyYAML (>=5.1)", "aws-xray-sdk (>=0.93,!=0.96)", "cfn-lint (>=0.40.0)", "docker (>=3.0.0)", "graphql-core", "joserfc (>=0.9.0)", "jsondiff (>=1.1.2)", "openapi-spec-validator (>=0.5.0)", "py-partiql-parser (==0.5.2)", "pyparsing (>=3.0.7)", "setuptools"]
cognitoidp = ["joserfc (>=0.9.0)"]
dynamodb = ["docker (>=3.0.0)", "py-partiql-parser (==0.5.1)"]
dynamodbstreams = ["docker (>=3.0.0)", "py-partiql-parser (==0.5.1)"]
dynamodb = ["docker (>=3.0.0)", "py-partiql-parser (==0.5.2)"]
dynamodbstreams = ["docker (>=3.0.0)", "py-partiql-parser (==0.5.2)"]
glue = ["pyparsing (>=3.0.7)"]
iotdata = ["jsondiff (>=1.1.2)"]
proxy = ["PyYAML (>=5.1)", "aws-xray-sdk (>=0.93,!=0.96)", "cfn-lint (>=0.40.0)", "docker (>=2.5.1)", "graphql-core", "joserfc (>=0.9.0)", "jsondiff (>=1.1.2)", "multipart", "openapi-spec-validator (>=0.5.0)", "py-partiql-parser (==0.5.1)", "pyparsing (>=3.0.7)", "setuptools"]
resourcegroupstaggingapi = ["PyYAML (>=5.1)", "cfn-lint (>=0.40.0)", "docker (>=3.0.0)", "graphql-core", "joserfc (>=0.9.0)", "jsondiff (>=1.1.2)", "openapi-spec-validator (>=0.5.0)", "py-partiql-parser (==0.5.1)", "pyparsing (>=3.0.7)"]
s3 = ["PyYAML (>=5.1)", "py-partiql-parser (==0.5.1)"]
s3crc32c = ["PyYAML (>=5.1)", "crc32c", "py-partiql-parser (==0.5.1)"]
server = ["PyYAML (>=5.1)", "aws-xray-sdk (>=0.93,!=0.96)", "cfn-lint (>=0.40.0)", "docker (>=3.0.0)", "flask (!=2.2.0,!=2.2.1)", "flask-cors", "graphql-core", "joserfc (>=0.9.0)", "jsondiff (>=1.1.2)", "openapi-spec-validator (>=0.5.0)", "py-partiql-parser (==0.5.1)", "pyparsing (>=3.0.7)", "setuptools"]
proxy = ["PyYAML (>=5.1)", "antlr4-python3-runtime", "aws-xray-sdk (>=0.93,!=0.96)", "cfn-lint (>=0.40.0)", "docker (>=2.5.1)", "graphql-core", "joserfc (>=0.9.0)", "jsondiff (>=1.1.2)", "jsonpath-ng", "multipart", "openapi-spec-validator (>=0.5.0)", "py-partiql-parser (==0.5.2)", "pyparsing (>=3.0.7)", "setuptools"]
resourcegroupstaggingapi = ["PyYAML (>=5.1)", "cfn-lint (>=0.40.0)", "docker (>=3.0.0)", "graphql-core", "joserfc (>=0.9.0)", "jsondiff (>=1.1.2)", "openapi-spec-validator (>=0.5.0)", "py-partiql-parser (==0.5.2)", "pyparsing (>=3.0.7)"]
s3 = ["PyYAML (>=5.1)", "py-partiql-parser (==0.5.2)"]
s3crc32c = ["PyYAML (>=5.1)", "crc32c", "py-partiql-parser (==0.5.2)"]
server = ["PyYAML (>=5.1)", "antlr4-python3-runtime", "aws-xray-sdk (>=0.93,!=0.96)", "cfn-lint (>=0.40.0)", "docker (>=3.0.0)", "flask (!=2.2.0,!=2.2.1)", "flask-cors", "graphql-core", "joserfc (>=0.9.0)", "jsondiff (>=1.1.2)", "jsonpath-ng", "openapi-spec-validator (>=0.5.0)", "py-partiql-parser (==0.5.2)", "pyparsing (>=3.0.7)", "setuptools"]
ssm = ["PyYAML (>=5.1)"]
stepfunctions = ["antlr4-python3-runtime", "jsonpath-ng"]
xray = ["aws-xray-sdk (>=0.93,!=0.96)", "setuptools"]
[[package]]
@@ -3226,6 +3254,17 @@ files = [
dev = ["pre-commit", "tox"]
testing = ["pytest", "pytest-benchmark"]
[[package]]
name = "ply"
version = "3.11"
description = "Python Lex & Yacc"
optional = false
python-versions = "*"
files = [
{file = "ply-3.11-py2.py3-none-any.whl", hash = "sha256:096f9b8350b65ebd2fd1346b12452efe5b9607f7482813ffca50c22722a807ce"},
{file = "ply-3.11.tar.gz", hash = "sha256:00c7c1aaa88358b9c765b6d3000c6eec0ba42abca5351b095321aef446081da3"},
]
[[package]]
name = "portalocker"
version = "2.8.2"
@@ -3282,13 +3321,13 @@ pydantic = "1.10.14"
[[package]]
name = "py-partiql-parser"
version = "0.5.1"
version = "0.5.2"
description = "Pure Python PartiQL Parser"
optional = false
python-versions = "*"
files = [
{file = "py-partiql-parser-0.5.1.tar.gz", hash = "sha256:aeac8f46529d8651bbae88a1a6c14dc3aa38ebc4bc6bd1eb975044c0564246c6"},
{file = "py_partiql_parser-0.5.1-py3-none-any.whl", hash = "sha256:53053e70987dea2983e1990ad85f87a7d8cec13dd4a4b065a740bcfd661f5a6b"},
{file = "py-partiql-parser-0.5.2.tar.gz", hash = "sha256:bdec65fe17d6093c05e9bc1742a99a041ef810b50a71cc0d9e74a88218d938cf"},
{file = "py_partiql_parser-0.5.2-py3-none-any.whl", hash = "sha256:9c79b59bbe0cb50daa8090020f2e7f3e5a0e33f7846b48924f19a8f7704f4877"},
]
[package.extras]
@@ -4158,13 +4197,13 @@ crt = ["botocore[crt] (>=1.20.29,<2.0a.0)"]
[[package]]
name = "safety"
version = "3.0.1"
version = "3.1.0"
description = "Checks installed dependencies for known vulnerabilities and licenses."
optional = false
python-versions = ">=3.7"
files = [
{file = "safety-3.0.1-py3-none-any.whl", hash = "sha256:1ed058bc4bef132b974e58d7fcad020fb897cd255328016f8a5a194b94ca91d2"},
{file = "safety-3.0.1.tar.gz", hash = "sha256:1f2000f03652f3a0bfc67f8fd1e98bc5723ccb76e15cb1bdd68545c3d803df01"},
{file = "safety-3.1.0-py3-none-any.whl", hash = "sha256:f2ba2d36f15ac1e24751547a73b854509a7d6db31efd30b57f64ffdf9d021934"},
{file = "safety-3.1.0.tar.gz", hash = "sha256:71f47b82ece153ec2f240e277f7cbfa70d5da2e0d143162c67f63b2f7459a1aa"},
]
[package.dependencies]
@@ -4174,11 +4213,11 @@ dparse = ">=0.6.4b0"
jinja2 = ">=3.1.0"
marshmallow = ">=3.15.0"
packaging = ">=21.0"
pydantic = ">=1.10.12,<2.0"
pydantic = ">=1.10.12"
requests = "*"
rich = "*"
"ruamel.yaml" = ">=0.17.21"
safety-schemas = ">=0.0.1"
safety-schemas = ">=0.0.2"
setuptools = ">=65.5.1"
typer = "*"
typing-extensions = ">=4.7.1"
@@ -4806,4 +4845,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p
[metadata]
lock-version = "2.0"
python-versions = ">=3.9,<3.13"
content-hash = "e414b353bd5fb303fc5b109fe908c3ac556e507c01dcc789b85aa7f8f9463fa0"
content-hash = "cd517148e98cb2d0e98f86a3aa335e5f2a718ec7a32e6ad907bbcf00efe7957a"
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+1
View File
@@ -87,6 +87,7 @@ class CIS_Requirement_Attribute(BaseModel):
RemediationProcedure: str
AuditProcedure: str
AdditionalInformation: str
DefaultValue: Optional[str]
References: str
DefaultValue: Optional[str]
+44 -25
View File
@@ -1,4 +1,6 @@
from prowler.lib.logger import logger
from prowler.lib.outputs.compliance.cis_aws import generate_compliance_row_cis_aws
from prowler.lib.outputs.compliance.cis_azure import generate_compliance_row_cis_azure
from prowler.lib.outputs.compliance.cis_gcp import generate_compliance_row_cis_gcp
from prowler.lib.outputs.compliance.cis_kubernetes import (
generate_compliance_row_cis_kubernetes,
@@ -14,28 +16,17 @@ def write_compliance_row_cis(
provider,
input_compliance_frameworks,
):
compliance_output = "cis_" + compliance.Version + "_" + compliance.Provider.lower()
try:
compliance_output = (
"cis_" + compliance.Version + "_" + compliance.Provider.lower()
)
# Only with the version of CIS that was selected
if compliance_output in str(input_compliance_frameworks):
for requirement in compliance.Requirements:
for attribute in requirement.Attributes:
if compliance.Provider == "AWS":
(compliance_row, csv_header) = generate_compliance_row_cis_aws(
finding,
compliance,
requirement,
attribute,
output_options,
provider,
)
elif compliance.Provider == "GCP":
(compliance_row, csv_header) = generate_compliance_row_cis_gcp(
finding, compliance, requirement, attribute, output_options
)
elif compliance.Provider == "Kubernetes":
(compliance_row, csv_header) = (
generate_compliance_row_cis_kubernetes(
# Only with the version of CIS that was selected
if compliance_output in str(input_compliance_frameworks):
for requirement in compliance.Requirements:
for attribute in requirement.Attributes:
if compliance.Provider == "AWS":
(compliance_row, csv_header) = generate_compliance_row_cis_aws(
finding,
compliance,
requirement,
@@ -43,8 +34,36 @@ def write_compliance_row_cis(
output_options,
provider,
)
)
elif compliance.Provider == "Azure":
(compliance_row, csv_header) = (
generate_compliance_row_cis_azure(
finding,
compliance,
requirement,
attribute,
output_options,
)
)
elif compliance.Provider == "GCP":
(compliance_row, csv_header) = generate_compliance_row_cis_gcp(
finding, compliance, requirement, attribute, output_options
)
elif compliance.Provider == "Kubernetes":
(compliance_row, csv_header) = (
generate_compliance_row_cis_kubernetes(
finding,
compliance,
requirement,
attribute,
output_options,
provider,
)
)
write_csv(
file_descriptors[compliance_output], csv_header, compliance_row
)
write_csv(
file_descriptors[compliance_output], csv_header, compliance_row
)
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
@@ -0,0 +1,36 @@
from prowler.config.config import timestamp
from prowler.lib.outputs.compliance.models import Check_Output_CSV_AZURE_CIS
from prowler.lib.outputs.csv.csv import generate_csv_fields
from prowler.lib.utils.utils import outputs_unix_timestamp
def generate_compliance_row_cis_azure(
finding, compliance, requirement, attribute, output_options
):
compliance_row = Check_Output_CSV_AZURE_CIS(
Provider=finding.check_metadata.Provider,
Description=compliance.Description,
Subscription=finding.subscription,
AssessmentDate=outputs_unix_timestamp(output_options.unix_timestamp, timestamp),
Requirements_Id=requirement.Id,
Requirements_Description=requirement.Description,
Requirements_Attributes_Section=attribute.Section,
Requirements_Attributes_Profile=attribute.Profile,
Requirements_Attributes_AssessmentStatus=attribute.AssessmentStatus,
Requirements_Attributes_Description=attribute.Description,
Requirements_Attributes_RationaleStatement=attribute.RationaleStatement,
Requirements_Attributes_ImpactStatement=attribute.ImpactStatement,
Requirements_Attributes_RemediationProcedure=attribute.RemediationProcedure,
Requirements_Attributes_AuditProcedure=attribute.AuditProcedure,
Requirements_Attributes_AdditionalInformation=attribute.AdditionalInformation,
Requirements_Attributes_DefaultValue=attribute.DefaultValue,
Requirements_Attributes_References=attribute.References,
Status=finding.status,
StatusExtended=finding.status_extended,
ResourceId=finding.resource_id,
ResourceName=finding.resource_name,
CheckId=finding.check_metadata.CheckID,
)
csv_header = generate_csv_fields(Check_Output_CSV_AZURE_CIS)
return compliance_row, csv_header
@@ -38,6 +38,7 @@ def add_manual_controls(
manual_finding.region = ""
manual_finding.location = ""
manual_finding.project_id = ""
manual_finding.subscription = ""
manual_finding.namespace = ""
fill_compliance(
output_options,
+29
View File
@@ -84,6 +84,35 @@ class Check_Output_CSV_AWS_CIS(BaseModel):
CheckId: str
class Check_Output_CSV_AZURE_CIS(BaseModel):
"""
Check_Output_CSV_CIS generates a finding's output in CSV CIS format.
"""
Provider: str
Description: str
Subscription: str
AssessmentDate: str
Requirements_Id: str
Requirements_Description: str
Requirements_Attributes_Section: str
Requirements_Attributes_Profile: str
Requirements_Attributes_AssessmentStatus: str
Requirements_Attributes_Description: str
Requirements_Attributes_RationaleStatement: str
Requirements_Attributes_ImpactStatement: str
Requirements_Attributes_RemediationProcedure: str
Requirements_Attributes_AuditProcedure: str
Requirements_Attributes_AdditionalInformation: str
Requirements_Attributes_DefaultValue: str
Requirements_Attributes_References: str
Status: str
StatusExtended: str
ResourceId: str
ResourceName: str
CheckId: str
class Check_Output_CSV_GCP_CIS(BaseModel):
"""
Check_Output_CSV_CIS generates a finding's output in CSV CIS format.
+18
View File
@@ -13,6 +13,7 @@ from prowler.lib.outputs.compliance.models import (
Check_Output_CSV_AWS_CIS,
Check_Output_CSV_AWS_ISO27001_2013,
Check_Output_CSV_AWS_Well_Architected,
Check_Output_CSV_AZURE_CIS,
Check_Output_CSV_ENS_RD2022,
Check_Output_CSV_GCP_CIS,
Check_Output_CSV_Generic_Compliance,
@@ -107,6 +108,23 @@ def fill_file_descriptors(output_modes, output_directory, output_filename, provi
)
file_descriptors.update({output_mode: file_descriptor})
elif provider.type == "azure":
filename = f"{output_directory}/compliance/{output_filename}_{output_mode}{csv_file_suffix}"
if "cis_" in output_mode:
file_descriptor = initialize_file_descriptor(
filename,
output_mode,
Check_Output_CSV_AZURE_CIS,
)
file_descriptors.update({output_mode: file_descriptor})
else:
file_descriptor = initialize_file_descriptor(
filename,
output_mode,
Check_Output_CSV_Generic_Compliance,
)
file_descriptors.update({output_mode: file_descriptor})
elif provider.type == "aws":
if output_mode == "json-asff":
filename = f"{output_directory}/{output_filename}{json_asff_file_suffix}"
@@ -2306,6 +2306,7 @@
"ap-southeast-1",
"ap-southeast-2",
"ap-southeast-3",
"ap-southeast-4",
"ca-central-1",
"eu-central-1",
"eu-central-2",
@@ -2836,6 +2837,22 @@
"aws-us-gov": []
}
},
"deadline-cloud": {
"regions": {
"aws": [
"ap-northeast-1",
"ap-southeast-1",
"ap-southeast-2",
"eu-central-1",
"eu-west-1",
"us-east-1",
"us-east-2",
"us-west-2"
],
"aws-cn": [],
"aws-us-gov": []
}
},
"deepcomposer": {
"regions": {
"aws": [
+21
View File
@@ -0,0 +1,21 @@
from uuid import UUID
# Service management API
WINDOWS_AZURE_SERVICE_MANAGEMENT_API = "797f4846-ba00-4fd7-ba43-dac1f8f63013"
# Authorization policy roles
GUEST_USER_ACCESS_NO_RESTRICTICTED = UUID("a0b1b346-4d3e-4e8b-98f8-753987be4970")
GUEST_USER_ACCESS_RESTRICTICTED = UUID("2af84b1e-32c8-42b7-82bc-daa82404023b")
# General built-in roles
CONTRIBUTOR_ROLE_ID = "b24988ac-6180-42a0-ab88-20f7382dd24c"
OWNER_ROLE_ID = "8e3af657-a8ff-443c-a75c-2fe8c4bcb635"
# Compute roles
VIRTUAL_MACHINE_CONTRIBUTOR_ROLE_ID = "9980e02c-c2be-4d73-94e8-173b1dc7cf3c"
VIRTUAL_MACHINE_ADMINISTRATOR_LOGIN_ROLE_ID = "1c0163c0-47e6-4577-8991-ea5c82e286e4"
VIRTUAL_MACHINE_USER_LOGIN_ROLE_ID = "fb879df8-f326-4884-b1cf-06f3ad86be52"
VIRTUAL_MACHINE_LOCAL_USER_LOGIN_ROLE_ID = "602da2ba-a5c2-41da-b01d-5360126ab525"
WINDOWS_ADMIN_CENTER_ADMINISTRATOR_LOGIN_ROLE_ID = (
"a6333a3e-0164-44c3-b281-7a577aff287f"
)
@@ -1,11 +1,11 @@
from prowler.lib.check.models import Check, Check_Report_Azure
from prowler.providers.azure.config import WINDOWS_AZURE_SERVICE_MANAGEMENT_API
from prowler.providers.azure.services.entra.entra_client import entra_client
class entra_conditional_access_policy_require_mfa_for_management_api(Check):
def execute(self) -> Check_Report_Azure:
findings = []
WINDOWS_AZURE_SERVICE_MANAGEMENT_API = "797f4846-ba00-4fd7-ba43-dac1f8f63013"
for (
tenant_name,
@@ -1,6 +1,6 @@
{
"Provider": "azure",
"CheckID": "entra_users_less_than_five_global_admins",
"CheckID": "entra_global_admin_in_less_than_five_users",
"CheckTitle": "Ensure fewer than 5 users have global administrator assignment",
"CheckType": [],
"ServiceName": "entra",
@@ -2,14 +2,14 @@ from prowler.lib.check.models import Check, Check_Report_Azure
from prowler.providers.azure.services.entra.entra_client import entra_client
class entra_users_less_than_five_global_admins(Check):
class entra_global_admin_in_less_than_five_users(Check):
def execute(self) -> Check_Report_Azure:
findings = []
for tenant_domain, directory_roles in entra_client.directory_roles.items():
report = Check_Report_Azure(self.metadata())
report.status = "FAIL"
report.subscription = f"Tenant: '{tenant_domain}'"
report.subscription = f"Tenant: {tenant_domain}"
report.resource_name = "Global Administrator"
if "Global Administrator" in directory_roles:
@@ -20,12 +20,14 @@ class entra_non_privileged_user_has_mfa(Check):
report.resource_name = user_domain_name
report.resource_id = user.id
report.status_extended = (
f"User '{user.name}' does not have MFA enabled."
f"Non-privileged user {user.name} does not have MFA."
)
if len(user.authentication_methods) > 1:
report.status = "PASS"
report.status_extended = f"User '{user.name}' has MFA enabled."
report.status_extended = (
f"Non-privileged user {user.name} has MFA."
)
findings.append(report)
@@ -10,7 +10,7 @@ class entra_policy_default_users_cannot_create_security_groups(Check):
report = Check_Report_Azure(self.metadata())
report.status = "FAIL"
report.subscription = f"Tenant: '{tenant_domain}'"
report.subscription = f"Tenant: {tenant_domain}"
report.resource_name = getattr(auth_policy, "name", "Authorization Policy")
report.resource_id = getattr(auth_policy, "id", "authorizationPolicy")
report.status_extended = "Non-privileged users are able to create security groups via the Access Panel and the Azure administration portal."
@@ -10,7 +10,7 @@ class entra_policy_ensure_default_user_cannot_create_apps(Check):
report = Check_Report_Azure(self.metadata())
report.status = "FAIL"
report.subscription = f"Tenant: '{tenant_domain}'"
report.subscription = f"Tenant: {tenant_domain}"
report.resource_name = getattr(auth_policy, "name", "Authorization Policy")
report.resource_id = getattr(auth_policy, "id", "authorizationPolicy")
report.status_extended = "App creation is not disabled for non-admin users."
@@ -9,7 +9,7 @@ class entra_policy_ensure_default_user_cannot_create_tenants(Check):
for tenant_domain, auth_policy in entra_client.authorization_policy.items():
report = Check_Report_Azure(self.metadata())
report.status = "FAIL"
report.subscription = f"Tenant: '{tenant_domain}'"
report.subscription = f"Tenant: {tenant_domain}"
report.resource_name = getattr(auth_policy, "name", "Authorization Policy")
report.resource_id = getattr(auth_policy, "id", "authorizationPolicy")
report.status_extended = (
@@ -9,7 +9,7 @@ class entra_policy_guest_invite_only_for_admin_roles(Check):
for tenant_domain, auth_policy in entra_client.authorization_policy.items():
report = Check_Report_Azure(self.metadata())
report.status = "FAIL"
report.subscription = f"Tenant: '{tenant_domain}'"
report.subscription = f"Tenant: {tenant_domain}"
report.resource_name = getattr(auth_policy, "name", "Authorization Policy")
report.resource_id = getattr(auth_policy, "id", "authorizationPolicy")
report.status_extended = "Guest invitations are not restricted to users with specific administrative roles only."
@@ -1,18 +1,16 @@
from uuid import UUID
from prowler.lib.check.models import Check, Check_Report_Azure
from prowler.providers.azure.config import GUEST_USER_ACCESS_RESTRICTICTED
from prowler.providers.azure.services.entra.entra_client import entra_client
class entra_policy_guest_users_access_restrictions(Check):
def execute(self) -> Check_Report_Azure:
GUEST_USER_ACCESS_RESTRICTICTED = UUID("2af84b1e-32c8-42b7-82bc-daa82404023b")
findings = []
for tenant_domain, auth_policy in entra_client.authorization_policy.items():
report = Check_Report_Azure(self.metadata())
report.status = "FAIL"
report.subscription = f"Tenant: '{tenant_domain}'"
report.subscription = f"Tenant: {tenant_domain}"
report.resource_name = getattr(auth_policy, "name", "Authorization Policy")
report.resource_id = getattr(auth_policy, "id", "authorizationPolicy")
report.status_extended = "Guest user access is not restricted to properties and memberships of their own directory objects"
@@ -9,7 +9,7 @@ class entra_policy_restricts_user_consent_for_apps(Check):
for tenant_domain, auth_policy in entra_client.authorization_policy.items():
report = Check_Report_Azure(self.metadata())
report.status = "FAIL"
report.subscription = f"Tenant: '{tenant_domain}'"
report.subscription = f"Tenant: {tenant_domain}"
report.resource_name = getattr(auth_policy, "name", "Authorization Policy")
report.resource_id = getattr(auth_policy, "id", "authorizationPolicy")
report.status_extended = "Entra allows users to consent apps accessing company data on their behalf"
@@ -9,7 +9,7 @@ class entra_policy_user_consent_for_verified_apps(Check):
for tenant_domain, auth_policy in entra_client.authorization_policy.items():
report = Check_Report_Azure(self.metadata())
report.status = "PASS"
report.subscription = f"Tenant: '{tenant_domain}'"
report.subscription = f"Tenant: {tenant_domain}"
report.resource_name = getattr(auth_policy, "name", "Authorization Policy")
report.resource_id = getattr(auth_policy, "id", "authorizationPolicy")
report.status_extended = "Entra does not allow users to consent non-verified apps accessing company data on their behalf."
@@ -20,12 +20,12 @@ class entra_privileged_user_has_mfa(Check):
report.resource_name = user_domain_name
report.resource_id = user.id
report.status_extended = (
f"User '{user.name}' does not have MFA enabled."
f"Privileged user {user.name} does not have MFA."
)
if len(user.authentication_methods) > 1:
report.status = "PASS"
report.status_extended = f"User '{user.name}' has MFA enabled."
report.status_extended = f"Privileged user {user.name} has MFA."
findings.append(report)
@@ -12,7 +12,7 @@ class entra_security_defaults_enabled(Check):
) in entra_client.security_default.items():
report = Check_Report_Azure(self.metadata())
report.status = "FAIL"
report.subscription = f"Tenant: '{tenant}'"
report.subscription = f"Tenant: {tenant}"
report.resource_name = getattr(security_default, "name", "Security Default")
report.resource_id = getattr(security_default, "id", "Security Default")
report.status_extended = "Entra security defaults is diabled."
@@ -12,6 +12,7 @@ from pydantic import BaseModel
from prowler.lib.logger import logger
from prowler.providers.azure.azure_provider import AzureProvider
from prowler.providers.azure.config import GUEST_USER_ACCESS_NO_RESTRICTICTED
from prowler.providers.azure.lib.service.service import AzureService
@@ -76,9 +77,7 @@ class Entra(AzureService):
async def __get_authorization_policy__(self):
logger.info("Entra - Getting authorization policy...")
GUEST_USER_ACCESS_NO_RESTRICTICTED = UUID(
"a0b1b346-4d3e-4e8b-98f8-753987be4970"
)
authorization_policy = {}
try:
for tenant, client in self.clients.items():
@@ -9,7 +9,7 @@ class entra_trusted_named_locations_exists(Check):
for tenant, named_locations in entra_client.named_locations.items():
report = Check_Report_Azure(self.metadata())
report.status = "FAIL"
report.subscription = f"Tenant: '{tenant}'"
report.subscription = f"Tenant: {tenant}"
report.resource_name = "Named Locations"
report.resource_id = "Named Locations"
report.status_extended = (
@@ -0,0 +1,30 @@
{
"Provider": "azure",
"CheckID": "entra_user_with_vm_access_has_mfa",
"CheckTitle": "Ensure only MFA enabled identities can access privileged Virtual Machine",
"CheckType": [],
"ServiceName": "iam",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "#microsoft.graph.users",
"Description": "Verify identities without MFA that can log in to a privileged virtual machine using separate login credentials. An adversary can leverage the access to move laterally and perform actions with the virtual machine's managed identity. Make sure the virtual machine only has necessary permissions, and revoke the admin-level permissions according to the least privileges principal",
"Risk": "Managed disks are by default encrypted on the underlying hardware, so no additional encryption is required for basic protection. It is available if additional encryption is required. Managed disks are by design more resilient that storage accounts. For ARM-deployed Virtual Machines, Azure Adviser will at some point recommend moving VHDs to managed disks both from a security and cost management perspective.",
"RelatedUrl": "",
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "",
"Terraform": ""
},
"Recommendation": {
"Text": "1. Log in to the Azure portal. Reducing access of managed identities attached to virtual machines. 2. This can be remediated by enabling MFA for user, Removing user access or • Case I : Enable MFA for users having access on virtual machines. 1. Navigate to Azure AD from the left pane and select Users from the Manage section. 2. Click on Per-User MFA from the top menu options and select each user with MULTI-FACTOR AUTH STATUS as Disabled and can login to virtual machines:  From quick steps on the right side select enable.  Click on enable multi-factor auth and share the link with the user to setup MFA as required. • Case II : Removing user access on a virtual machine. 1. Select the Subscription, then click on Access control (IAM). 2. Select Role assignments and search for Virtual Machine Administrator Login or Virtual Machine User Login or any role that provides access to log into virtual machines. 3. Click on Role Name, Select Assignments, and remove identities with no MFA configured. • Case III : Reducing access of managed identities attached to virtual machines. 1. Select the Subscription, then click on Access control (IAM). 2. Select Role Assignments from the top menu and apply filters on Assignment type as Privileged administrator roles and Type as Virtual Machines. 3. Click on Role Name, Select Assignments, and remove identities access make sure this follows the least privileges principal.",
"Url": ""
}
},
"Categories": [],
"DependsOn": [],
"RelatedTo": [],
"Notes": "This recommendation requires an Azure AD P2 License to implement. Ensure that identities that are provisioned to a virtual machine utilizes an RBAC/ABAC group and is allocated a role using Azure PIM, and the Role settings require MFA or use another PAM solution (like CyberArk) for accessing Virtual Machines."
}
@@ -0,0 +1,53 @@
from prowler.lib.check.models import Check, Check_Report_Azure
from prowler.providers.azure.config import (
CONTRIBUTOR_ROLE_ID,
OWNER_ROLE_ID,
VIRTUAL_MACHINE_ADMINISTRATOR_LOGIN_ROLE_ID,
VIRTUAL_MACHINE_CONTRIBUTOR_ROLE_ID,
VIRTUAL_MACHINE_LOCAL_USER_LOGIN_ROLE_ID,
VIRTUAL_MACHINE_USER_LOGIN_ROLE_ID,
WINDOWS_ADMIN_CENTER_ADMINISTRATOR_LOGIN_ROLE_ID,
)
from prowler.providers.azure.services.entra.entra_client import entra_client
from prowler.providers.azure.services.iam.iam_client import iam_client
class entra_user_with_vm_access_has_mfa(Check):
def execute(self) -> Check_Report_Azure:
findings = []
for users in entra_client.users.values():
for user_domain_name, user in users.items():
for (
subscription_name,
role_assigns,
) in iam_client.role_assignments.items():
for assignment in role_assigns.values():
if (
assignment.agent_type == "User"
and assignment.role_id
in [
CONTRIBUTOR_ROLE_ID,
OWNER_ROLE_ID,
VIRTUAL_MACHINE_CONTRIBUTOR_ROLE_ID,
VIRTUAL_MACHINE_ADMINISTRATOR_LOGIN_ROLE_ID,
VIRTUAL_MACHINE_USER_LOGIN_ROLE_ID,
VIRTUAL_MACHINE_LOCAL_USER_LOGIN_ROLE_ID,
WINDOWS_ADMIN_CENTER_ADMINISTRATOR_LOGIN_ROLE_ID,
]
and assignment.agent_id == user.id
):
report = Check_Report_Azure(self.metadata())
report.status = "FAIL"
report.status_extended = f"User {user.name} without MFA can access VMs in subscription {subscription_name}"
report.subscription = subscription_name
report.resource_name = user_domain_name
report.resource_id = user.id
if len(user.authentication_methods) > 1:
report.status = "PASS"
report.status_extended = f"User {user.name} can access VMs in subscription {subscription_name} but it has MFA."
findings.append(report)
return findings
@@ -9,7 +9,7 @@ class entra_users_cannot_create_microsoft_365_groups(Check):
for tenant_domain, group_settings in entra_client.group_settings.items():
report = Check_Report_Azure(self.metadata())
report.status = "FAIL"
report.subscription = f"Tenant: '{tenant_domain}'"
report.subscription = f"Tenant: {tenant_domain}"
report.resource_name = "Microsoft365 Groups"
report.resource_id = "Microsoft365 Groups"
report.status_extended = "Users can create Microsoft 365 groups."
@@ -13,6 +13,7 @@ class IAM(AzureService):
def __init__(self, provider: AzureProvider):
super().__init__(AuthorizationManagementClient, provider)
self.roles, self.custom_roles = self.__get_roles__()
self.role_assignments = self.__get_role_assignments__()
def __get_roles__(self):
logger.info("IAM - Getting roles...")
@@ -53,6 +54,34 @@ class IAM(AzureService):
)
return builtin_roles, custom_roles
def __get_role_assignments__(self):
logger.info("IAM - Getting role assignments...")
role_assignments = {}
for subscription, client in self.clients.items():
try:
role_assignments.update({subscription: {}})
all_role_assignments = client.role_assignments.list_for_subscription(
filter="atScope()"
)
for role_assignment in all_role_assignments:
role_assignments[subscription].update(
{
role_assignment.id: RoleAssignment(
agent_id=role_assignment.principal_id,
agent_type=role_assignment.principal_type,
role_id=role_assignment.role_definition_id.split("/")[
-1
],
)
}
)
except Exception as error:
logger.error(f"Subscription name: {subscription}")
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
return role_assignments
@dataclass
class Role:
@@ -61,3 +90,10 @@ class Role:
type: str
assignable_scopes: list[str]
permissions: list[Permission]
@dataclass
class RoleAssignment:
agent_id: str
agent_type: str
role_id: str
@@ -29,5 +29,4 @@ class monitor_alert_create_update_public_ip_address_rule(Check):
break
findings.append(report)
return findings
+6 -4
View File
@@ -50,10 +50,12 @@ boto3 = "1.26.165"
botocore = "1.29.165"
colorama = "0.4.6"
detect-secrets = "1.4.0"
google-api-python-client = "2.123.0"
google-api-python-client = "2.124.0"
google-auth-httplib2 = ">=0.1,<0.3"
jsonschema = "4.21.1"
kubernetes = "28.1.0"
mkdocs-git-revision-date-localized-plugin = "1.2.4"
mkdocs-material = "9.5.17"
msgraph-sdk = "1.0.0"
msrestazure = "0.6.4"
py-ocsf-models = "0.0.3"
@@ -81,7 +83,7 @@ docker = "7.0.0"
flake8 = "7.0.0"
freezegun = "1.4.0"
mock = "5.1.0"
moto = {extras = ["all"], version = "5.0.3"}
moto = {extras = ["all"], version = "5.0.4"}
openapi-schema-validator = "0.6.2"
openapi-spec-validator = "0.7.1"
pylint = "3.1.0"
@@ -90,7 +92,7 @@ pytest-cov = "5.0.0"
pytest-env = "1.1.3"
pytest-randomly = "3.15.0"
pytest-xdist = "3.5.0"
safety = "3.0.1"
safety = "3.1.0"
vulture = "2.11"
[tool.poetry.group.docs]
@@ -99,7 +101,7 @@ optional = true
[tool.poetry.group.docs.dependencies]
mkdocs = "1.5.3"
mkdocs-git-revision-date-localized-plugin = "1.2.4"
mkdocs-material = "9.5.15"
mkdocs-material = "9.5.17"
mkdocs-material-extensions = "1.3.1"
[tool.poetry.scripts]
@@ -232,11 +232,7 @@ class Test_Lambda_Service:
}
assert awslambda.functions[lambda_arn_2].region == AWS_REGION_US_EAST_1
# Emtpy policy
assert awslambda.functions[lambda_arn_2].policy == {
"Id": "default",
"Statement": [],
"Version": "2012-10-17",
}
assert awslambda.functions[lambda_arn_2].policy == {}
# Lambda Code
with tempfile.TemporaryDirectory() as tmp_dir_name:
@@ -4,7 +4,7 @@ from uuid import uuid4
from tests.providers.azure.azure_fixtures import DOMAIN, set_mocked_azure_provider
class Test_entra_users_less_than_five_global_admins:
class Test_entra_global_admin_in_less_than_five_users:
def test_entra_no_tenants(self):
entra_client = mock.MagicMock
@@ -12,16 +12,16 @@ class Test_entra_users_less_than_five_global_admins:
"prowler.providers.common.common.get_global_provider",
return_value=set_mocked_azure_provider(),
), mock.patch(
"prowler.providers.azure.services.entra.entra_users_less_than_five_global_admins.entra_users_less_than_five_global_admins.entra_client",
"prowler.providers.azure.services.entra.entra_global_admin_in_less_than_five_users.entra_global_admin_in_less_than_five_users.entra_client",
new=entra_client,
):
from prowler.providers.azure.services.entra.entra_users_less_than_five_global_admins.entra_users_less_than_five_global_admins import (
entra_users_less_than_five_global_admins,
from prowler.providers.azure.services.entra.entra_global_admin_in_less_than_five_users.entra_global_admin_in_less_than_five_users import (
entra_global_admin_in_less_than_five_users,
)
entra_client.directory_roles = {}
check = entra_users_less_than_five_global_admins()
check = entra_global_admin_in_less_than_five_users()
result = check.execute()
assert len(result) == 0
@@ -32,16 +32,16 @@ class Test_entra_users_less_than_five_global_admins:
"prowler.providers.common.common.get_global_provider",
return_value=set_mocked_azure_provider(),
), mock.patch(
"prowler.providers.azure.services.entra.entra_users_less_than_five_global_admins.entra_users_less_than_five_global_admins.entra_client",
"prowler.providers.azure.services.entra.entra_global_admin_in_less_than_five_users.entra_global_admin_in_less_than_five_users.entra_client",
new=entra_client,
):
from prowler.providers.azure.services.entra.entra_users_less_than_five_global_admins.entra_users_less_than_five_global_admins import (
entra_users_less_than_five_global_admins,
from prowler.providers.azure.services.entra.entra_global_admin_in_less_than_five_users.entra_global_admin_in_less_than_five_users import (
entra_global_admin_in_less_than_five_users,
)
entra_client.directory_roles = {DOMAIN: {}}
check = entra_users_less_than_five_global_admins()
check = entra_global_admin_in_less_than_five_users()
result = check.execute()
assert len(result) == 0
@@ -52,16 +52,16 @@ class Test_entra_users_less_than_five_global_admins:
"prowler.providers.common.common.get_global_provider",
return_value=set_mocked_azure_provider(),
), mock.patch(
"prowler.providers.azure.services.entra.entra_users_less_than_five_global_admins.entra_users_less_than_five_global_admins.entra_client",
"prowler.providers.azure.services.entra.entra_global_admin_in_less_than_five_users.entra_global_admin_in_less_than_five_users.entra_client",
new=entra_client,
):
from prowler.providers.azure.services.entra.entra_global_admin_in_less_than_five_users.entra_global_admin_in_less_than_five_users import (
entra_global_admin_in_less_than_five_users,
)
from prowler.providers.azure.services.entra.entra_service import (
DirectoryRole,
User,
)
from prowler.providers.azure.services.entra.entra_users_less_than_five_global_admins.entra_users_less_than_five_global_admins import (
entra_users_less_than_five_global_admins,
)
id = str(uuid4())
id_user1 = str(uuid4())
@@ -79,12 +79,12 @@ class Test_entra_users_less_than_five_global_admins:
}
}
check = entra_users_less_than_five_global_admins()
check = entra_global_admin_in_less_than_five_users()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert result[0].status_extended == "There are 2 global administrators."
assert result[0].subscription == f"Tenant: '{DOMAIN}'"
assert result[0].subscription == f"Tenant: {DOMAIN}"
assert result[0].resource_name == "Global Administrator"
assert result[0].resource_id == id
@@ -95,16 +95,16 @@ class Test_entra_users_less_than_five_global_admins:
"prowler.providers.common.common.get_global_provider",
return_value=set_mocked_azure_provider(),
), mock.patch(
"prowler.providers.azure.services.entra.entra_users_less_than_five_global_admins.entra_users_less_than_five_global_admins.entra_client",
"prowler.providers.azure.services.entra.entra_global_admin_in_less_than_five_users.entra_global_admin_in_less_than_five_users.entra_client",
new=entra_client,
):
from prowler.providers.azure.services.entra.entra_global_admin_in_less_than_five_users.entra_global_admin_in_less_than_five_users import (
entra_global_admin_in_less_than_five_users,
)
from prowler.providers.azure.services.entra.entra_service import (
DirectoryRole,
User,
)
from prowler.providers.azure.services.entra.entra_users_less_than_five_global_admins.entra_users_less_than_five_global_admins import (
entra_users_less_than_five_global_admins,
)
id = str(uuid4())
id_user1 = str(uuid4())
@@ -130,7 +130,7 @@ class Test_entra_users_less_than_five_global_admins:
}
}
check = entra_users_less_than_five_global_admins()
check = entra_global_admin_in_less_than_five_users()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
@@ -138,7 +138,7 @@ class Test_entra_users_less_than_five_global_admins:
result[0].status_extended
== "There are 6 global administrators. It should be less than five."
)
assert result[0].subscription == f"Tenant: '{DOMAIN}'"
assert result[0].subscription == f"Tenant: {DOMAIN}"
assert result[0].resource_name == "Global Administrator"
assert result[0].resource_id == id
@@ -149,16 +149,16 @@ class Test_entra_users_less_than_five_global_admins:
"prowler.providers.common.common.get_global_provider",
return_value=set_mocked_azure_provider(),
), mock.patch(
"prowler.providers.azure.services.entra.entra_users_less_than_five_global_admins.entra_users_less_than_five_global_admins.entra_client",
"prowler.providers.azure.services.entra.entra_global_admin_in_less_than_five_users.entra_global_admin_in_less_than_five_users.entra_client",
new=entra_client,
):
from prowler.providers.azure.services.entra.entra_global_admin_in_less_than_five_users.entra_global_admin_in_less_than_five_users import (
entra_global_admin_in_less_than_five_users,
)
from prowler.providers.azure.services.entra.entra_service import (
DirectoryRole,
User,
)
from prowler.providers.azure.services.entra.entra_users_less_than_five_global_admins.entra_users_less_than_five_global_admins import (
entra_users_less_than_five_global_admins,
)
id = str(uuid4())
id_user1 = str(uuid4())
@@ -182,7 +182,7 @@ class Test_entra_users_less_than_five_global_admins:
}
}
check = entra_users_less_than_five_global_admins()
check = entra_global_admin_in_less_than_five_users()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
@@ -190,6 +190,6 @@ class Test_entra_users_less_than_five_global_admins:
result[0].status_extended
== "There are 5 global administrators. It should be less than five."
)
assert result[0].subscription == f"Tenant: '{DOMAIN}'"
assert result[0].subscription == f"Tenant: {DOMAIN}"
assert result[0].resource_name == "Global Administrator"
assert result[0].resource_id == id
@@ -77,7 +77,10 @@ class Test_entra_non_privileged_user_has_mfa:
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert result[0].status_extended == "User 'foo' does not have MFA enabled."
assert (
result[0].status_extended
== "Non-privileged user foo does not have MFA."
)
assert result[0].resource_name == f"foo@{DOMAIN}"
assert result[0].resource_id == user_id
assert result[0].subscription == f"Tenant: {DOMAIN}"
@@ -114,7 +117,7 @@ class Test_entra_non_privileged_user_has_mfa:
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert result[0].status_extended == "User 'foo' has MFA enabled."
assert result[0].status_extended == "Non-privileged user foo has MFA."
assert result[0].resource_name == f"foo@{DOMAIN}"
assert result[0].resource_id == user_id
assert result[0].subscription == f"Tenant: {DOMAIN}"
@@ -43,7 +43,7 @@ class Test_entra_policy_default_users_cannot_create_security_groups:
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert result[0].subscription == f"Tenant: '{DOMAIN}'"
assert result[0].subscription == f"Tenant: {DOMAIN}"
assert result[0].resource_name == "Authorization Policy"
assert result[0].resource_id == "authorizationPolicy"
assert (
@@ -94,7 +94,7 @@ class Test_entra_policy_default_users_cannot_create_security_groups:
)
assert result[0].resource_name == "Test"
assert result[0].resource_id == id
assert result[0].subscription == f"Tenant: '{DOMAIN}'"
assert result[0].subscription == f"Tenant: {DOMAIN}"
def test_entra_default_user_role_permissions_not_allowed_to_create_security_groups(
self,
@@ -139,4 +139,4 @@ class Test_entra_policy_default_users_cannot_create_security_groups:
)
assert result[0].resource_name == "Test"
assert result[0].resource_id == id
assert result[0].subscription == f"Tenant: '{DOMAIN}'"
assert result[0].subscription == f"Tenant: {DOMAIN}"
@@ -45,7 +45,7 @@ class Test_entra_policy_ensure_default_user_cannot_create_apps:
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert result[0].subscription == f"Tenant: '{DOMAIN}'"
assert result[0].subscription == f"Tenant: {DOMAIN}"
assert result[0].resource_name == "Authorization Policy"
assert result[0].resource_id == "authorizationPolicy"
assert (
@@ -94,7 +94,7 @@ class Test_entra_policy_ensure_default_user_cannot_create_apps:
)
assert result[0].resource_name == "Test"
assert result[0].resource_id == id
assert result[0].subscription == f"Tenant: '{DOMAIN}'"
assert result[0].subscription == f"Tenant: {DOMAIN}"
def test_entra_default_user_role_permissions_allowed_to_create_apps(self):
id = str(uuid4())
@@ -137,4 +137,4 @@ class Test_entra_policy_ensure_default_user_cannot_create_apps:
)
assert result[0].resource_name == "Test"
assert result[0].resource_id == id
assert result[0].subscription == f"Tenant: '{DOMAIN}'"
assert result[0].subscription == f"Tenant: {DOMAIN}"
@@ -43,7 +43,7 @@ class Test_entra_policy_ensure_default_user_cannot_create_tenants:
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert result[0].subscription == f"Tenant: '{DOMAIN}'"
assert result[0].subscription == f"Tenant: {DOMAIN}"
assert result[0].resource_name == "Authorization Policy"
assert result[0].resource_id == "authorizationPolicy"
assert (
@@ -92,7 +92,7 @@ class Test_entra_policy_ensure_default_user_cannot_create_tenants:
)
assert result[0].resource_name == "Test"
assert result[0].resource_id == id
assert result[0].subscription == f"Tenant: '{DOMAIN}'"
assert result[0].subscription == f"Tenant: {DOMAIN}"
def test_entra_default_user_role_permissions_allowed_to_create_tenants(self):
id = str(uuid4())
@@ -135,4 +135,4 @@ class Test_entra_policy_ensure_default_user_cannot_create_tenants:
)
assert result[0].resource_name == "Test"
assert result[0].resource_id == id
assert result[0].subscription == f"Tenant: '{DOMAIN}'"
assert result[0].subscription == f"Tenant: {DOMAIN}"
@@ -45,7 +45,7 @@ class Test_entra_policy_guest_invite_only_for_admin_roles:
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert result[0].subscription == f"Tenant: '{DOMAIN}'"
assert result[0].subscription == f"Tenant: {DOMAIN}"
assert result[0].resource_name == "Authorization Policy"
assert result[0].resource_id == "authorizationPolicy"
assert (
@@ -91,7 +91,7 @@ class Test_entra_policy_guest_invite_only_for_admin_roles:
)
assert result[0].resource_name == "TestPolicy"
assert result[0].resource_id == id
assert result[0].subscription == f"Tenant: '{DOMAIN}'"
assert result[0].subscription == f"Tenant: {DOMAIN}"
def test_entra_tenant_policy_allow_invites_from_admins(self):
entra_client = mock.MagicMock
@@ -131,7 +131,7 @@ class Test_entra_policy_guest_invite_only_for_admin_roles:
)
assert result[0].resource_name == "TestPolicy"
assert result[0].resource_id == id
assert result[0].subscription == f"Tenant: '{DOMAIN}'"
assert result[0].subscription == f"Tenant: {DOMAIN}"
def test_entra_tenant_policy_allow_invites_from_none(self):
entra_client = mock.MagicMock
@@ -171,4 +171,4 @@ class Test_entra_policy_guest_invite_only_for_admin_roles:
)
assert result[0].resource_name == "TestPolicy"
assert result[0].resource_id == id
assert result[0].subscription == f"Tenant: '{DOMAIN}'"
assert result[0].subscription == f"Tenant: {DOMAIN}"
@@ -45,7 +45,7 @@ class Test_entra_policy_guest_users_access_restrictions:
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert result[0].subscription == f"Tenant: '{DOMAIN}'"
assert result[0].subscription == f"Tenant: {DOMAIN}"
assert result[0].resource_name == "Authorization Policy"
assert result[0].resource_id == "authorizationPolicy"
assert (
@@ -86,7 +86,7 @@ class Test_entra_policy_guest_users_access_restrictions:
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert result[0].subscription == f"Tenant: '{DOMAIN}'"
assert result[0].subscription == f"Tenant: {DOMAIN}"
assert result[0].resource_name == "Authorization Policy"
assert result[0].resource_id == id
assert (
@@ -127,7 +127,7 @@ class Test_entra_policy_guest_users_access_restrictions:
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert result[0].subscription == f"Tenant: '{DOMAIN}'"
assert result[0].subscription == f"Tenant: {DOMAIN}"
assert result[0].resource_name == "Authorization Policy"
assert result[0].resource_id == id
assert (
@@ -168,7 +168,7 @@ class Test_entra_policy_guest_users_access_restrictions:
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert result[0].subscription == f"Tenant: '{DOMAIN}'"
assert result[0].subscription == f"Tenant: {DOMAIN}"
assert result[0].resource_name == "Authorization Policy"
assert result[0].resource_id == id
assert (
@@ -45,7 +45,7 @@ class Test_entra_policy_restricts_user_consent_for_apps:
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert result[0].subscription == f"Tenant: '{DOMAIN}'"
assert result[0].subscription == f"Tenant: {DOMAIN}"
assert result[0].resource_name == "Authorization Policy"
assert result[0].resource_id == "authorizationPolicy"
assert (
@@ -85,7 +85,7 @@ class Test_entra_policy_restricts_user_consent_for_apps:
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert result[0].subscription == f"Tenant: '{DOMAIN}'"
assert result[0].subscription == f"Tenant: {DOMAIN}"
assert result[0].resource_name == "Authorization Policy"
assert result[0].resource_id == auth_policy.id
assert (
@@ -128,7 +128,7 @@ class Test_entra_policy_restricts_user_consent_for_apps:
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert result[0].subscription == f"Tenant: '{DOMAIN}'"
assert result[0].subscription == f"Tenant: {DOMAIN}"
assert result[0].resource_name == "Authorization Policy"
assert result[0].resource_id == auth_policy.id
assert (
@@ -173,7 +173,7 @@ class Test_entra_policy_restricts_user_consent_for_apps:
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert result[0].subscription == f"Tenant: '{DOMAIN}'"
assert result[0].subscription == f"Tenant: {DOMAIN}"
assert result[0].resource_name == "Authorization Policy"
assert result[0].resource_id == auth_policy.id
assert (
@@ -60,7 +60,7 @@ class Test_entra_policy_user_consent_for_verified_apps:
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert result[0].subscription == f"Tenant: '{DOMAIN}'"
assert result[0].subscription == f"Tenant: {DOMAIN}"
assert result[0].resource_name == "Authorization Policy"
assert result[0].resource_id == auth_policy.id
assert (
@@ -105,7 +105,7 @@ class Test_entra_policy_user_consent_for_verified_apps:
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert result[0].subscription == f"Tenant: '{DOMAIN}'"
assert result[0].subscription == f"Tenant: {DOMAIN}"
assert result[0].resource_name == "Authorization Policy"
assert result[0].resource_id == auth_policy.id
assert (
@@ -143,7 +143,7 @@ class Test_entra_privileged_user_has_mfa:
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert result[0].status_extended == "User 'foo' does not have MFA enabled."
assert result[0].status_extended == "Privileged user foo does not have MFA."
assert result[0].resource_name == f"foo@{DOMAIN}"
assert result[0].resource_id == user_id
assert result[0].subscription == f"Tenant: {DOMAIN}"
@@ -182,7 +182,7 @@ class Test_entra_privileged_user_has_mfa:
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert result[0].status_extended == "User 'foo' has MFA enabled."
assert result[0].status_extended == "Privileged user foo has MFA."
assert result[0].resource_name == f"foo@{DOMAIN}"
assert result[0].resource_id == user_id
assert result[0].subscription == f"Tenant: {DOMAIN}"
@@ -46,7 +46,7 @@ class Test_entra_security_defaults_enabled:
assert len(result) == 1
assert result[0].status == "FAIL"
assert result[0].status_extended == "Entra security defaults is diabled."
assert result[0].subscription == f"Tenant: '{DOMAIN}'"
assert result[0].subscription == f"Tenant: {DOMAIN}"
assert result[0].resource_name == "Security Default"
assert result[0].resource_id == "Security Default"
@@ -78,7 +78,7 @@ class Test_entra_security_defaults_enabled:
assert len(result) == 1
assert result[0].status == "PASS"
assert result[0].status_extended == "Entra security defaults is enabled."
assert result[0].subscription == f"Tenant: '{DOMAIN}'"
assert result[0].subscription == f"Tenant: {DOMAIN}"
assert result[0].resource_name == "Sec Default"
assert result[0].resource_id == id
@@ -110,6 +110,6 @@ class Test_entra_security_defaults_enabled:
assert len(result) == 1
assert result[0].status == "FAIL"
assert result[0].status_extended == "Entra security defaults is diabled."
assert result[0].subscription == f"Tenant: '{DOMAIN}'"
assert result[0].subscription == f"Tenant: {DOMAIN}"
assert result[0].resource_name == "Sec Default"
assert result[0].resource_id == id
@@ -48,7 +48,7 @@ class Test_entra_trusted_named_locations_exists:
result[0].status_extended
== "There is no trusted location with IP ranges defined."
)
assert result[0].subscription == f"Tenant: '{DOMAIN}'"
assert result[0].subscription == f"Tenant: {DOMAIN}"
assert result[0].resource_name == "Named Locations"
assert result[0].resource_id == "Named Locations"
@@ -87,7 +87,7 @@ class Test_entra_trusted_named_locations_exists:
result[0].status_extended
== "Exits trusted location with trusted IP ranges, this IPs ranges are: ['192.168.0.1/24']"
)
assert result[0].subscription == f"Tenant: '{DOMAIN}'"
assert result[0].subscription == f"Tenant: {DOMAIN}"
assert result[0].resource_name == "Test Location"
assert result[0].resource_id == "location_id"
@@ -124,7 +124,7 @@ class Test_entra_trusted_named_locations_exists:
result[0].status_extended
== "There is no trusted location with IP ranges defined."
)
assert result[0].subscription == f"Tenant: '{DOMAIN}'"
assert result[0].subscription == f"Tenant: {DOMAIN}"
assert result[0].resource_name == "Test Location"
assert result[0].resource_id == "location_id"
@@ -163,6 +163,6 @@ class Test_entra_trusted_named_locations_exists:
result[0].status_extended
== "There is no trusted location with IP ranges defined."
)
assert result[0].subscription == f"Tenant: '{DOMAIN}'"
assert result[0].subscription == f"Tenant: {DOMAIN}"
assert result[0].resource_name == "Test Location"
assert result[0].resource_id == "location_id"
@@ -0,0 +1,252 @@
from unittest import mock
from uuid import uuid4
from prowler.providers.azure.config import VIRTUAL_MACHINE_ADMINISTRATOR_LOGIN_ROLE_ID
from tests.providers.azure.azure_fixtures import (
AZURE_SUBSCRIPTION_ID,
DOMAIN,
set_mocked_azure_provider,
)
class Test_iam_assignment_priviledge_access_vm_has_mfa:
def test_iam_no_roles(self):
iam_client = mock.MagicMock
entra_client = mock.MagicMock
with mock.patch(
"prowler.providers.common.common.get_global_provider",
return_value=set_mocked_azure_provider(),
), mock.patch(
"prowler.providers.azure.services.entra.entra_user_with_vm_access_has_mfa.entra_user_with_vm_access_has_mfa.iam_client",
new=iam_client,
):
from prowler.providers.azure.services.entra.entra_user_with_vm_access_has_mfa.entra_user_with_vm_access_has_mfa import (
entra_user_with_vm_access_has_mfa,
)
iam_client.role_assignments = {}
entra_client.users = {}
check = entra_user_with_vm_access_has_mfa()
result = check.execute()
assert len(result) == 0
def test_entra_user_with_vm_access_has_mfa(self):
iam_client = mock.MagicMock
role_assigment_id = str(uuid4())
entra_client = mock.MagicMock
user_id = str(uuid4())
with mock.patch(
"prowler.providers.common.common.get_global_provider",
return_value=set_mocked_azure_provider(),
), mock.patch(
"prowler.providers.azure.services.entra.entra_user_with_vm_access_has_mfa.entra_user_with_vm_access_has_mfa.iam_client",
new=iam_client,
):
with mock.patch(
"prowler.providers.common.common.get_global_provider",
return_value=set_mocked_azure_provider(),
), mock.patch(
"prowler.providers.azure.services.entra.entra_user_with_vm_access_has_mfa.entra_user_with_vm_access_has_mfa.entra_client",
new=entra_client,
):
from prowler.providers.azure.services.entra.entra_service import User
from prowler.providers.azure.services.entra.entra_user_with_vm_access_has_mfa.entra_user_with_vm_access_has_mfa import (
entra_user_with_vm_access_has_mfa,
)
from prowler.providers.azure.services.iam.iam_service import (
RoleAssignment,
)
iam_client.role_assignments = {
AZURE_SUBSCRIPTION_ID: {
role_assigment_id: RoleAssignment(
role_id=VIRTUAL_MACHINE_ADMINISTRATOR_LOGIN_ROLE_ID,
agent_type="User",
agent_id=user_id,
)
}
}
entra_client.users = {
DOMAIN: {
f"test@{DOMAIN}": User(
id=user_id,
name="test",
authentication_methods=[
"Password",
"MicrosoftAuthenticator",
],
)
}
}
check = entra_user_with_vm_access_has_mfa()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"User test can access VMs in subscription {AZURE_SUBSCRIPTION_ID} but it has MFA."
)
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
assert result[0].resource_name == f"test@{DOMAIN}"
assert result[0].resource_id == user_id
def test_entra_user_with_vm_access_has_mfa_no_mfa(self):
iam_client = mock.MagicMock
role_assigment_id = str(uuid4())
entra_client = mock.MagicMock
user_id = str(uuid4())
with mock.patch(
"prowler.providers.common.common.get_global_provider",
return_value=set_mocked_azure_provider(),
), mock.patch(
"prowler.providers.azure.services.entra.entra_user_with_vm_access_has_mfa.entra_user_with_vm_access_has_mfa.iam_client",
new=iam_client,
):
with mock.patch(
"prowler.providers.common.common.get_global_provider",
return_value=set_mocked_azure_provider(),
), mock.patch(
"prowler.providers.azure.services.entra.entra_user_with_vm_access_has_mfa.entra_user_with_vm_access_has_mfa.entra_client",
new=entra_client,
):
from prowler.providers.azure.services.entra.entra_service import User
from prowler.providers.azure.services.entra.entra_user_with_vm_access_has_mfa.entra_user_with_vm_access_has_mfa import (
entra_user_with_vm_access_has_mfa,
)
from prowler.providers.azure.services.iam.iam_service import (
RoleAssignment,
)
iam_client.role_assignments = {
AZURE_SUBSCRIPTION_ID: {
role_assigment_id: RoleAssignment(
role_id=VIRTUAL_MACHINE_ADMINISTRATOR_LOGIN_ROLE_ID,
agent_type="User",
agent_id=user_id,
)
}
}
entra_client.users = {
DOMAIN: {
f"test@{DOMAIN}": User(
id=user_id, name="test", authentication_methods=["Password"]
)
}
}
check = entra_user_with_vm_access_has_mfa()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"User test without MFA can access VMs in subscription {AZURE_SUBSCRIPTION_ID}"
)
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
assert result[0].resource_name == f"test@{DOMAIN}"
assert result[0].resource_id == user_id
def test_entra_user_with_vm_access_has_mfa_no_user(self):
iam_client = mock.MagicMock
role_assigment_id = str(uuid4())
entra_client = mock.MagicMock
user_id = str(uuid4())
with mock.patch(
"prowler.providers.common.common.get_global_provider",
return_value=set_mocked_azure_provider(),
), mock.patch(
"prowler.providers.azure.services.entra.entra_user_with_vm_access_has_mfa.entra_user_with_vm_access_has_mfa.iam_client",
new=iam_client,
):
with mock.patch(
"prowler.providers.common.common.get_global_provider",
return_value=set_mocked_azure_provider(),
), mock.patch(
"prowler.providers.azure.services.entra.entra_user_with_vm_access_has_mfa.entra_user_with_vm_access_has_mfa.entra_client",
new=entra_client,
):
from prowler.providers.azure.services.entra.entra_user_with_vm_access_has_mfa.entra_user_with_vm_access_has_mfa import (
entra_user_with_vm_access_has_mfa,
)
from prowler.providers.azure.services.iam.iam_service import (
RoleAssignment,
)
iam_client.role_assignments = {
AZURE_SUBSCRIPTION_ID: {
role_assigment_id: RoleAssignment(
role_id=VIRTUAL_MACHINE_ADMINISTRATOR_LOGIN_ROLE_ID,
agent_type="User",
agent_id=user_id,
)
}
}
entra_client.users = {DOMAIN: {}}
check = entra_user_with_vm_access_has_mfa()
result = check.execute()
assert len(result) == 0
def test_entra_user_with_vm_access_has_mfa_no_role(self):
iam_client = mock.MagicMock
role_assigment_id = str(uuid4())
entra_client = mock.MagicMock
user_id = str(uuid4())
with mock.patch(
"prowler.providers.common.common.get_global_provider",
return_value=set_mocked_azure_provider(),
), mock.patch(
"prowler.providers.azure.services.entra.entra_user_with_vm_access_has_mfa.entra_user_with_vm_access_has_mfa.iam_client",
new=iam_client,
):
with mock.patch(
"prowler.providers.common.common.get_global_provider",
return_value=set_mocked_azure_provider(),
), mock.patch(
"prowler.providers.azure.services.entra.entra_user_with_vm_access_has_mfa.entra_user_with_vm_access_has_mfa.entra_client",
new=entra_client,
):
from prowler.providers.azure.services.entra.entra_service import User
from prowler.providers.azure.services.entra.entra_user_with_vm_access_has_mfa.entra_user_with_vm_access_has_mfa import (
entra_user_with_vm_access_has_mfa,
)
from prowler.providers.azure.services.iam.iam_service import (
RoleAssignment,
)
iam_client.role_assignments = {
AZURE_SUBSCRIPTION_ID: {
role_assigment_id: RoleAssignment(
role_id=str(uuid4()),
agent_type="User",
agent_id=user_id,
)
}
}
entra_client.users = {
DOMAIN: {
f"test@{DOMAIN}": User(
id=user_id,
name="test",
authentication_methods=[
"Password",
"MicrosoftAuthenticator",
],
)
}
}
check = entra_user_with_vm_access_has_mfa()
result = check.execute()
assert len(result) == 0
@@ -46,7 +46,7 @@ class Test_entra_users_cannot_create_microsoft_365_groups:
assert len(result) == 1
assert result[0].status == "FAIL"
assert result[0].status_extended == "Users can create Microsoft 365 groups."
assert result[0].subscription == f"Tenant: '{DOMAIN}'"
assert result[0].subscription == f"Tenant: {DOMAIN}"
assert result[0].resource_name == "Microsoft365 Groups"
assert result[0].resource_id == "Microsoft365 Groups"
@@ -91,7 +91,7 @@ class Test_entra_users_cannot_create_microsoft_365_groups:
assert (
result[0].status_extended == "Users cannot create Microsoft 365 groups."
)
assert result[0].subscription == f"Tenant: '{DOMAIN}'"
assert result[0].subscription == f"Tenant: {DOMAIN}"
assert result[0].resource_name == "Microsoft365 Groups"
assert result[0].resource_id == "Microsoft365 Groups"
@@ -134,7 +134,7 @@ class Test_entra_users_cannot_create_microsoft_365_groups:
assert len(result) == 1
assert result[0].status == "FAIL"
assert result[0].status_extended == "Users can create Microsoft 365 groups."
assert result[0].subscription == f"Tenant: '{DOMAIN}'"
assert result[0].subscription == f"Tenant: {DOMAIN}"
assert result[0].resource_name == "Microsoft365 Groups"
assert result[0].resource_id == "Microsoft365 Groups"
@@ -173,6 +173,6 @@ class Test_entra_users_cannot_create_microsoft_365_groups:
assert len(result) == 1
assert result[0].status == "FAIL"
assert result[0].status_extended == "Users can create Microsoft 365 groups."
assert result[0].subscription == f"Tenant: '{DOMAIN}'"
assert result[0].subscription == f"Tenant: {DOMAIN}"
assert result[0].resource_name == "Microsoft365 Groups"
assert result[0].resource_id == "Microsoft365 Groups"