Merge branch 'master' into feat/PROWLER-1091-create-new-stepper-implement-aws-organizations-endpoints

This commit is contained in:
alejandrobailo
2026-02-18 15:37:18 +01:00
136 changed files with 7661 additions and 2237 deletions
+50
View File
@@ -44,6 +44,35 @@ jobs:
ui/README.md
ui/AGENTS.md
- name: Get changed source files for targeted tests
id: changed-source
if: steps.check-changes.outputs.any_changed == 'true'
uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
with:
files: |
ui/**/*.ts
ui/**/*.tsx
files_ignore: |
ui/**/*.test.ts
ui/**/*.test.tsx
ui/**/*.spec.ts
ui/**/*.spec.tsx
ui/vitest.config.ts
ui/vitest.setup.ts
- name: Check for critical path changes (run all tests)
id: critical-changes
if: steps.check-changes.outputs.any_changed == 'true'
uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
with:
files: |
ui/lib/**
ui/types/**
ui/config/**
ui/middleware.ts
ui/vitest.config.ts
ui/vitest.setup.ts
- name: Setup Node.js ${{ env.NODE_VERSION }}
if: steps.check-changes.outputs.any_changed == 'true'
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
@@ -83,6 +112,27 @@ jobs:
if: steps.check-changes.outputs.any_changed == 'true'
run: pnpm run healthcheck
- name: Run unit tests (all - critical paths changed)
if: steps.check-changes.outputs.any_changed == 'true' && steps.critical-changes.outputs.any_changed == 'true'
run: |
echo "Critical paths changed - running ALL unit tests"
pnpm run test:run
- name: Run unit tests (related to changes only)
if: steps.check-changes.outputs.any_changed == 'true' && steps.critical-changes.outputs.any_changed != 'true' && steps.changed-source.outputs.all_changed_files != ''
run: |
echo "Running tests related to changed files:"
echo "${{ steps.changed-source.outputs.all_changed_files }}"
# Convert space-separated to vitest related format (remove ui/ prefix for relative paths)
CHANGED_FILES=$(echo "${{ steps.changed-source.outputs.all_changed_files }}" | tr ' ' '\n' | sed 's|^ui/||' | tr '\n' ' ')
pnpm exec vitest related $CHANGED_FILES --run
- name: Run unit tests (test files only changed)
if: steps.check-changes.outputs.any_changed == 'true' && steps.critical-changes.outputs.any_changed != 'true' && steps.changed-source.outputs.all_changed_files == ''
run: |
echo "Only test files changed - running ALL unit tests"
pnpm run test:run
- name: Build application
if: steps.check-changes.outputs.any_changed == 'true'
run: pnpm run build
+14 -1
View File
@@ -24,6 +24,8 @@ Use these skills for detailed patterns on-demand:
| `zod-4` | New API (z.email(), z.uuid()) | [SKILL.md](skills/zod-4/SKILL.md) |
| `zustand-5` | Persist, selectors, slices | [SKILL.md](skills/zustand-5/SKILL.md) |
| `ai-sdk-5` | UIMessage, streaming, LangChain | [SKILL.md](skills/ai-sdk-5/SKILL.md) |
| `vitest` | Unit testing, React Testing Library | [SKILL.md](skills/vitest/SKILL.md) |
| `tdd` | Test-Driven Development workflow | [SKILL.md](skills/tdd/SKILL.md) |
### Prowler-Specific Skills
| Skill | Description | URL |
@@ -56,8 +58,8 @@ When performing these actions, ALWAYS invoke the corresponding skill FIRST:
| Add changelog entry for a PR or feature | `prowler-changelog` |
| Adding DRF pagination or permissions | `django-drf` |
| Adding new providers | `prowler-provider` |
| Adding services to existing providers | `prowler-provider` |
| Adding privilege escalation detection queries | `prowler-attack-paths-query` |
| Adding services to existing providers | `prowler-provider` |
| After creating/modifying a skill | `skill-sync` |
| App Router / Server Actions | `nextjs-15` |
| Building AI chat features | `ai-sdk-5` |
@@ -76,30 +78,38 @@ When performing these actions, ALWAYS invoke the corresponding skill FIRST:
| Creating/updating compliance frameworks | `prowler-compliance` |
| Debug why a GitHub Actions job is failing | `prowler-ci` |
| Fill .github/pull_request_template.md (Context/Description/Steps to review/Checklist) | `prowler-pr` |
| Fixing bug | `tdd` |
| General Prowler development questions | `prowler` |
| Implementing JSON:API endpoints | `django-drf` |
| Implementing feature | `tdd` |
| Inspect PR CI checks and gates (.github/workflows/*) | `prowler-ci` |
| Inspect PR CI workflows (.github/workflows/*): conventional-commit, pr-check-changelog, pr-conflict-checker, labeler | `prowler-pr` |
| Mapping checks to compliance controls | `prowler-compliance` |
| Mocking AWS with moto in tests | `prowler-test-sdk` |
| Modifying API responses | `jsonapi` |
| Modifying component | `tdd` |
| Refactoring code | `tdd` |
| Regenerate AGENTS.md Auto-invoke tables (sync.sh) | `skill-sync` |
| Review PR requirements: template, title conventions, changelog gate | `prowler-pr` |
| Review changelog format and conventions | `prowler-changelog` |
| Reviewing JSON:API compliance | `jsonapi` |
| Reviewing compliance framework PRs | `prowler-compliance-review` |
| Testing RLS tenant isolation | `prowler-test-api` |
| Testing hooks or utilities | `vitest` |
| Troubleshoot why a skill is missing from AGENTS.md auto-invoke | `skill-sync` |
| Understand CODEOWNERS/labeler-based automation | `prowler-ci` |
| Understand PR title conventional-commit validation | `prowler-ci` |
| Understand changelog gate and no-changelog label behavior | `prowler-ci` |
| Understand review ownership with CODEOWNERS | `prowler-pr` |
| Update CHANGELOG.md in any component | `prowler-changelog` |
| Updating README.md provider statistics table | `prowler-readme-table` |
| Updating checks, services, compliance, or categories count in README.md | `prowler-readme-table` |
| Updating existing Attack Paths queries | `prowler-attack-paths-query` |
| Updating existing checks and metadata | `prowler-sdk-check` |
| Using Zustand stores | `zustand-5` |
| Working on MCP server tools | `prowler-mcp` |
| Working on Prowler UI structure (actions/adapters/types/hooks) | `prowler-ui` |
| Working on task | `tdd` |
| Working with Prowler UI test helpers/pages | `prowler-test-ui` |
| Working with Tailwind classes | `tailwind-4` |
| Writing Playwright E2E tests | `playwright` |
@@ -107,9 +117,12 @@ When performing these actions, ALWAYS invoke the corresponding skill FIRST:
| Writing Prowler SDK tests | `prowler-test-sdk` |
| Writing Prowler UI E2E tests | `prowler-test-ui` |
| Writing Python tests with pytest | `pytest` |
| Writing React component tests | `vitest` |
| Writing React components | `react-19` |
| Writing TypeScript types/interfaces | `typescript` |
| Writing Vitest tests | `vitest` |
| Writing documentation | `prowler-docs` |
| Writing unit tests for UI | `vitest` |
---
+5
View File
@@ -24,13 +24,18 @@ When performing these actions, ALWAYS invoke the corresponding skill FIRST:
| Creating ViewSets, serializers, or filters in api/ | `django-drf` |
| Creating a git commit | `prowler-commit` |
| Creating/modifying models, views, serializers | `prowler-api` |
| Fixing bug | `tdd` |
| Implementing JSON:API endpoints | `django-drf` |
| Implementing feature | `tdd` |
| Modifying API responses | `jsonapi` |
| Modifying component | `tdd` |
| Refactoring code | `tdd` |
| Review changelog format and conventions | `prowler-changelog` |
| Reviewing JSON:API compliance | `jsonapi` |
| Testing RLS tenant isolation | `prowler-test-api` |
| Update CHANGELOG.md in any component | `prowler-changelog` |
| Updating existing Attack Paths queries | `prowler-attack-paths-query` |
| Working on task | `tdd` |
| Writing Prowler API tests | `prowler-test-api` |
| Writing Python tests with pytest | `pytest` |
+15
View File
@@ -7,6 +7,7 @@ All notable changes to the **Prowler API** are documented in this file.
### 🚀 Added
- OpenStack provider support [(#10003)](https://github.com/prowler-cloud/prowler/pull/10003)
- PDF report for the CSA CCM compliance framework [(#10088)](https://github.com/prowler-cloud/prowler/pull/10088)
### 🔄 Changed
@@ -20,6 +21,12 @@ All notable changes to the **Prowler API** are documented in this file.
- Support CSA CCM 4.0 for the Alibaba Cloud provider [(#10061)](https://github.com/prowler-cloud/prowler/pull/10061)
- Attack Paths: Mark attack Paths scan as failed when Celery task fails outside job error handling [(#10065)](https://github.com/prowler-cloud/prowler/pull/10065)
- Attack Paths: Remove legacy per-scan `graph_database` and `is_graph_database_deleted` fields from AttackPathsScan model [(#10077)](https://github.com/prowler-cloud/prowler/pull/10077)
- Attack Paths: Add `graph_data_ready` field to decouple query availability from scan state [(#10089)](https://github.com/prowler-cloud/prowler/pull/10089)
- AI agent guidelines with TDD and testing skills references [(#9925)](https://github.com/prowler-cloud/prowler/pull/9925)
### 🐞 Fixed
- Attack Paths: Orphaned temporary Neo4j databases are now cleaned up on scan failure and provider deletion [(#10101)](https://github.com/prowler-cloud/prowler/pull/10101)
### 🔐 Security
@@ -27,6 +34,14 @@ All notable changes to the **Prowler API** are documented in this file.
---
## [1.19.3] (Prowler UNRELEASED)
### 🐞 Fixed
- GCP provider UID validation regex to allow domain prefixes [(#10078)](https://github.com/prowler-cloud/prowler/pull/10078)
---
## [1.19.2] (Prowler v5.18.2)
### 🐞 Fixed
@@ -7,6 +7,7 @@
"provider": "b85601a8-4b45-4194-8135-03fb980ef428",
"scan": "01920573-aa9c-73c9-bcda-f2e35c9b19d2",
"state": "completed",
"graph_data_ready": true,
"progress": 100,
"update_tag": 1693586667,
"task": null,
@@ -0,0 +1,17 @@
# Generated by Django 5.1.15 on 2026-02-16 13:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("api", "0078_remove_attackpathsscan_graph_database_fields"),
]
operations = [
migrations.AddField(
model_name="attackpathsscan",
name="graph_data_ready",
field=models.BooleanField(default=False),
),
]
@@ -0,0 +1,26 @@
# Separate from 0079 because psqlextra's schema editor runs AddField DDL and DML
# on different database connections, causing a deadlock when combined with RunPython
# in the same migration.
from django.db import migrations
from api.db_router import MainRouter
def backfill_graph_data_ready(apps, schema_editor):
"""Set graph_data_ready=True for all completed AttackPathsScan rows."""
AttackPathsScan = apps.get_model("api", "AttackPathsScan")
AttackPathsScan.objects.using(MainRouter.admin_db).filter(
state="completed",
graph_data_ready=False,
).update(graph_data_ready=True)
class Migration(migrations.Migration):
dependencies = [
("api", "0079_attackpathsscan_graph_data_ready"),
]
operations = [
migrations.RunPython(backfill_graph_data_ready, migrations.RunPython.noop),
]
+7 -3
View File
@@ -327,10 +327,13 @@ class Provider(RowLevelSecurityProtectedModel):
@staticmethod
def validate_gcp_uid(value):
if not re.match(r"^[a-z][a-z0-9-]{5,29}$", value):
# Standard format: 6-30 chars, starts with letter, lowercase + digits + hyphens
# Legacy App Engine format: domain.com:project-id
if not re.match(r"^([a-z][a-z0-9.-]*:)?[a-z][a-z0-9-]{5,29}$", value):
raise ModelValidationError(
detail="GCP provider ID must be 6 to 30 characters, start with a letter, and contain only lowercase "
"letters, numbers, and hyphens.",
detail="GCP provider ID must be a valid project ID: 6 to 30 characters, start with a letter, "
"and contain only lowercase letters, numbers, and hyphens. "
"Legacy App Engine project IDs with a domain prefix (e.g., example.com:my-project) are also accepted.",
code="gcp-uid",
pointer="/data/attributes/uid",
)
@@ -655,6 +658,7 @@ class AttackPathsScan(RowLevelSecurityProtectedModel):
state = StateEnumField(choices=StateChoices.choices, default=StateChoices.AVAILABLE)
progress = models.IntegerField(default=0)
graph_data_ready = models.BooleanField(default=False)
# Timing
started_at = models.DateTimeField(null=True, blank=True)
+118 -165
View File
@@ -296,6 +296,7 @@ paths:
enum:
- state
- progress
- graph_data_ready
- provider
- provider_alias
- provider_type
@@ -355,7 +356,7 @@ paths:
name: filter[provider_type]
schema:
type: string
x-spec-enum-id: 2d8d323e9cc0044b
x-spec-enum-id: 4b8815b179aa7216
enum:
- alibabacloud
- aws
@@ -388,7 +389,7 @@ paths:
type: array
items:
type: string
x-spec-enum-id: 2d8d323e9cc0044b
x-spec-enum-id: 4b8815b179aa7216
enum:
- alibabacloud
- aws
@@ -571,6 +572,7 @@ paths:
enum:
- state
- progress
- graph_data_ready
- provider
- provider_alias
- provider_type
@@ -631,6 +633,7 @@ paths:
enum:
- state
- progress
- graph_data_ready
- provider
- provider_alias
- provider_type
@@ -1340,7 +1343,7 @@ paths:
name: filter[provider_type]
schema:
type: string
x-spec-enum-id: 2d8d323e9cc0044b
x-spec-enum-id: 4b8815b179aa7216
enum:
- alibabacloud
- aws
@@ -1373,7 +1376,7 @@ paths:
type: array
items:
type: string
x-spec-enum-id: 2d8d323e9cc0044b
x-spec-enum-id: 4b8815b179aa7216
enum:
- alibabacloud
- aws
@@ -1934,7 +1937,7 @@ paths:
name: filter[provider_type]
schema:
type: string
x-spec-enum-id: 2d8d323e9cc0044b
x-spec-enum-id: 4b8815b179aa7216
enum:
- alibabacloud
- aws
@@ -1967,7 +1970,7 @@ paths:
type: array
items:
type: string
x-spec-enum-id: 2d8d323e9cc0044b
x-spec-enum-id: 4b8815b179aa7216
enum:
- alibabacloud
- aws
@@ -2436,7 +2439,7 @@ paths:
name: filter[provider_type]
schema:
type: string
x-spec-enum-id: 2d8d323e9cc0044b
x-spec-enum-id: 4b8815b179aa7216
enum:
- alibabacloud
- aws
@@ -2469,7 +2472,7 @@ paths:
type: array
items:
type: string
x-spec-enum-id: 2d8d323e9cc0044b
x-spec-enum-id: 4b8815b179aa7216
enum:
- alibabacloud
- aws
@@ -2936,7 +2939,7 @@ paths:
name: filter[provider_type]
schema:
type: string
x-spec-enum-id: 2d8d323e9cc0044b
x-spec-enum-id: 4b8815b179aa7216
enum:
- alibabacloud
- aws
@@ -2969,7 +2972,7 @@ paths:
type: array
items:
type: string
x-spec-enum-id: 2d8d323e9cc0044b
x-spec-enum-id: 4b8815b179aa7216
enum:
- alibabacloud
- aws
@@ -3424,7 +3427,7 @@ paths:
name: filter[provider_type]
schema:
type: string
x-spec-enum-id: 2d8d323e9cc0044b
x-spec-enum-id: 4b8815b179aa7216
enum:
- alibabacloud
- aws
@@ -3457,7 +3460,7 @@ paths:
type: array
items:
type: string
x-spec-enum-id: 2d8d323e9cc0044b
x-spec-enum-id: 4b8815b179aa7216
enum:
- alibabacloud
- aws
@@ -5253,7 +5256,7 @@ paths:
name: filter[provider_type]
schema:
type: string
x-spec-enum-id: 2d8d323e9cc0044b
x-spec-enum-id: 4b8815b179aa7216
enum:
- alibabacloud
- aws
@@ -5286,7 +5289,7 @@ paths:
type: array
items:
type: string
x-spec-enum-id: 2d8d323e9cc0044b
x-spec-enum-id: 4b8815b179aa7216
enum:
- alibabacloud
- aws
@@ -5420,7 +5423,7 @@ paths:
name: filter[provider_type]
schema:
type: string
x-spec-enum-id: 2d8d323e9cc0044b
x-spec-enum-id: 4b8815b179aa7216
enum:
- alibabacloud
- aws
@@ -5453,7 +5456,7 @@ paths:
type: array
items:
type: string
x-spec-enum-id: 2d8d323e9cc0044b
x-spec-enum-id: 4b8815b179aa7216
enum:
- alibabacloud
- aws
@@ -5763,7 +5766,7 @@ paths:
name: filter[provider_type]
schema:
type: string
x-spec-enum-id: 2d8d323e9cc0044b
x-spec-enum-id: 4b8815b179aa7216
enum:
- alibabacloud
- aws
@@ -5796,7 +5799,7 @@ paths:
type: array
items:
type: string
x-spec-enum-id: 2d8d323e9cc0044b
x-spec-enum-id: 4b8815b179aa7216
enum:
- alibabacloud
- aws
@@ -5964,7 +5967,7 @@ paths:
name: filter[provider_type]
schema:
type: string
x-spec-enum-id: 2d8d323e9cc0044b
x-spec-enum-id: 4b8815b179aa7216
enum:
- alibabacloud
- aws
@@ -5997,7 +6000,7 @@ paths:
type: array
items:
type: string
x-spec-enum-id: 2d8d323e9cc0044b
x-spec-enum-id: 4b8815b179aa7216
enum:
- alibabacloud
- aws
@@ -6395,7 +6398,7 @@ paths:
name: filter[provider_type]
schema:
type: string
x-spec-enum-id: 2d8d323e9cc0044b
x-spec-enum-id: 4b8815b179aa7216
enum:
- alibabacloud
- aws
@@ -6428,7 +6431,7 @@ paths:
type: array
items:
type: string
x-spec-enum-id: 2d8d323e9cc0044b
x-spec-enum-id: 4b8815b179aa7216
enum:
- alibabacloud
- aws
@@ -6561,7 +6564,7 @@ paths:
name: filter[provider_type]
schema:
type: string
x-spec-enum-id: 2d8d323e9cc0044b
x-spec-enum-id: 4b8815b179aa7216
enum:
- alibabacloud
- aws
@@ -6594,7 +6597,7 @@ paths:
type: array
items:
type: string
x-spec-enum-id: 2d8d323e9cc0044b
x-spec-enum-id: 4b8815b179aa7216
enum:
- alibabacloud
- aws
@@ -6751,7 +6754,7 @@ paths:
name: filter[provider_type]
schema:
type: string
x-spec-enum-id: 2d8d323e9cc0044b
x-spec-enum-id: 4b8815b179aa7216
enum:
- alibabacloud
- aws
@@ -6784,7 +6787,7 @@ paths:
type: array
items:
type: string
x-spec-enum-id: 2d8d323e9cc0044b
x-spec-enum-id: 4b8815b179aa7216
enum:
- alibabacloud
- aws
@@ -7582,7 +7585,7 @@ paths:
name: filter[provider]
schema:
type: string
x-spec-enum-id: 2d8d323e9cc0044b
x-spec-enum-id: 4b8815b179aa7216
enum:
- alibabacloud
- aws
@@ -7615,7 +7618,7 @@ paths:
type: array
items:
type: string
x-spec-enum-id: 2d8d323e9cc0044b
x-spec-enum-id: 4b8815b179aa7216
enum:
- alibabacloud
- aws
@@ -7650,7 +7653,7 @@ paths:
name: filter[provider_type]
schema:
type: string
x-spec-enum-id: 2d8d323e9cc0044b
x-spec-enum-id: 4b8815b179aa7216
enum:
- alibabacloud
- aws
@@ -7683,7 +7686,7 @@ paths:
type: array
items:
type: string
x-spec-enum-id: 2d8d323e9cc0044b
x-spec-enum-id: 4b8815b179aa7216
enum:
- alibabacloud
- aws
@@ -8341,7 +8344,7 @@ paths:
name: filter[provider_type]
schema:
type: string
x-spec-enum-id: 2d8d323e9cc0044b
x-spec-enum-id: 4b8815b179aa7216
enum:
- alibabacloud
- aws
@@ -8374,7 +8377,7 @@ paths:
type: array
items:
type: string
x-spec-enum-id: 2d8d323e9cc0044b
x-spec-enum-id: 4b8815b179aa7216
enum:
- alibabacloud
- aws
@@ -8847,7 +8850,7 @@ paths:
name: filter[provider_type]
schema:
type: string
x-spec-enum-id: 2d8d323e9cc0044b
x-spec-enum-id: 4b8815b179aa7216
enum:
- alibabacloud
- aws
@@ -8880,7 +8883,7 @@ paths:
type: array
items:
type: string
x-spec-enum-id: 2d8d323e9cc0044b
x-spec-enum-id: 4b8815b179aa7216
enum:
- alibabacloud
- aws
@@ -9166,7 +9169,7 @@ paths:
name: filter[provider_type]
schema:
type: string
x-spec-enum-id: 2d8d323e9cc0044b
x-spec-enum-id: 4b8815b179aa7216
enum:
- alibabacloud
- aws
@@ -9199,7 +9202,7 @@ paths:
type: array
items:
type: string
x-spec-enum-id: 2d8d323e9cc0044b
x-spec-enum-id: 4b8815b179aa7216
enum:
- alibabacloud
- aws
@@ -9491,7 +9494,7 @@ paths:
name: filter[provider_type]
schema:
type: string
x-spec-enum-id: 2d8d323e9cc0044b
x-spec-enum-id: 4b8815b179aa7216
enum:
- alibabacloud
- aws
@@ -9524,7 +9527,7 @@ paths:
type: array
items:
type: string
x-spec-enum-id: 2d8d323e9cc0044b
x-spec-enum-id: 4b8815b179aa7216
enum:
- alibabacloud
- aws
@@ -10350,7 +10353,7 @@ paths:
name: filter[provider_type]
schema:
type: string
x-spec-enum-id: 2d8d323e9cc0044b
x-spec-enum-id: 4b8815b179aa7216
enum:
- alibabacloud
- aws
@@ -10383,7 +10386,7 @@ paths:
type: array
items:
type: string
x-spec-enum-id: 2d8d323e9cc0044b
x-spec-enum-id: 4b8815b179aa7216
enum:
- alibabacloud
- aws
@@ -10747,6 +10750,72 @@ paths:
description: CSV file containing the compliance report
'404':
description: Compliance report not found
/api/v1/scans/{id}/csa:
get:
operationId: scans_csa_retrieve
description: Download CSA Cloud Controls Matrix (CCM) v4.0 compliance report
as a PDF file.
summary: Retrieve CSA CCM compliance report
parameters:
- in: query
name: fields[scans]
schema:
type: array
items:
type: string
enum:
- name
- trigger
- state
- unique_resource_count
- progress
- duration
- provider
- task
- inserted_at
- started_at
- completed_at
- scheduled_at
- next_scan_at
- processor
- url
description: endpoint return only specific fields in the response on a per-type
basis by including a fields[TYPE] query parameter.
explode: false
- in: path
name: id
schema:
type: string
format: uuid
description: A UUID string identifying this scan.
required: true
- in: query
name: include
schema:
type: array
items:
type: string
enum:
- provider
description: include query parameter to allow the client to customize which
related resources should be returned.
explode: false
tags:
- Scan
security:
- JWT or API Key: []
responses:
'200':
description: PDF file containing the CSA CCM compliance report
'202':
description: The task is in progress
'401':
description: API key missing or user not Authenticated
'403':
description: There is a problem with credentials
'404':
description: The scan has no CSA CCM reports, or the CSA CCM report generation
task has not started yet
/api/v1/scans/{id}/ens:
get:
operationId: scans_ens_retrieve
@@ -12530,16 +12599,16 @@ components:
type: string
description:
type: string
attribution:
allOf:
- $ref: '#/components/schemas/AttackPathsQueryAttribution'
nullable: true
provider:
type: string
parameters:
type: array
items:
$ref: '#/components/schemas/AttackPathsQueryParameter'
attribution:
allOf:
- $ref: '#/components/schemas/AttackPathsQueryAttribution'
nullable: true
required:
- id
- name
@@ -12737,6 +12806,8 @@ components:
type: integer
maximum: 2147483647
minimum: -2147483648
graph_data_ready:
type: boolean
provider_alias:
type: string
readOnly: true
@@ -17450,36 +17521,6 @@ components:
required:
- clouds_yaml_content
- clouds_yaml_cloud
- type: object
title: OpenStack Explicit Credentials
properties:
auth_url:
type: string
description: OpenStack Keystone authentication URL (e.g.,
https://openstack.example.com:5000/v3).
username:
type: string
description: OpenStack username for authentication.
password:
type: string
description: OpenStack password for authentication.
region_name:
type: string
description: OpenStack region name (e.g., RegionOne).
identity_api_version:
type: string
description: Keystone API version (default: 3).
user_domain_name:
type: string
description: User domain name (default: Default).
project_domain_name:
type: string
description: Project domain name (default: Default).
required:
- auth_url
- username
- password
- region_name
writeOnly: true
required:
- secret
@@ -18494,7 +18535,7 @@ components:
* `alibabacloud` - Alibaba Cloud
* `cloudflare` - Cloudflare
* `openstack` - OpenStack
x-spec-enum-id: 2d8d323e9cc0044b
x-spec-enum-id: 4b8815b179aa7216
uid:
type: string
title: Unique identifier for the provider, set by the provider
@@ -18613,7 +18654,7 @@ components:
- cloudflare
- openstack
type: string
x-spec-enum-id: 2d8d323e9cc0044b
x-spec-enum-id: 4b8815b179aa7216
description: |-
Type of provider to create.
@@ -18679,7 +18720,7 @@ components:
- cloudflare
- openstack
type: string
x-spec-enum-id: 2d8d323e9cc0044b
x-spec-enum-id: 4b8815b179aa7216
description: |-
Type of provider to create.
@@ -19529,35 +19570,6 @@ components:
required:
- clouds_yaml_content
- clouds_yaml_cloud
- type: object
title: OpenStack Explicit Credentials
properties:
auth_url:
type: string
description: OpenStack Keystone authentication URL (e.g., https://openstack.example.com:5000/v3).
username:
type: string
description: OpenStack username for authentication.
password:
type: string
description: OpenStack password for authentication.
region_name:
type: string
description: OpenStack region name (e.g., RegionOne).
identity_api_version:
type: string
description: Keystone API version (default: 3).
user_domain_name:
type: string
description: User domain name (default: Default).
project_domain_name:
type: string
description: Project domain name (default: Default).
required:
- auth_url
- username
- password
- region_name
writeOnly: true
required:
- secret_type
@@ -19958,36 +19970,6 @@ components:
required:
- clouds_yaml_content
- clouds_yaml_cloud
- type: object
title: OpenStack Explicit Credentials
properties:
auth_url:
type: string
description: OpenStack Keystone authentication URL (e.g.,
https://openstack.example.com:5000/v3).
username:
type: string
description: OpenStack username for authentication.
password:
type: string
description: OpenStack password for authentication.
region_name:
type: string
description: OpenStack region name (e.g., RegionOne).
identity_api_version:
type: string
description: Keystone API version (default: 3).
user_domain_name:
type: string
description: User domain name (default: Default).
project_domain_name:
type: string
description: Project domain name (default: Default).
required:
- auth_url
- username
- password
- region_name
writeOnly: true
required:
- secret_type
@@ -20399,35 +20381,6 @@ components:
required:
- clouds_yaml_content
- clouds_yaml_cloud
- type: object
title: OpenStack Explicit Credentials
properties:
auth_url:
type: string
description: OpenStack Keystone authentication URL (e.g., https://openstack.example.com:5000/v3).
username:
type: string
description: OpenStack username for authentication.
password:
type: string
description: OpenStack password for authentication.
region_name:
type: string
description: OpenStack region name (e.g., RegionOne).
identity_api_version:
type: string
description: Keystone API version (default: 3).
user_domain_name:
type: string
description: User domain name (default: Default).
project_domain_name:
type: string
description: Project domain name (default: Default).
required:
- auth_url
- username
- password
- region_name
writeOnly: true
required:
- secret
+122 -2
View File
@@ -1079,6 +1079,11 @@ class TestProviderViewSet:
[
{"provider": "aws", "uid": "111111111111", "alias": "test"},
{"provider": "gcp", "uid": "a12322-test54321", "alias": "test"},
{
"provider": "gcp",
"uid": "example.com:my-project-123456",
"alias": "legacy-gcp",
},
{
"provider": "kubernetes",
"uid": "kubernetes-test-123456789",
@@ -1203,6 +1208,11 @@ class TestProviderViewSet:
[
{"provider": "aws", "uid": "111111111111", "alias": "test"},
{"provider": "gcp", "uid": "a12322-test54321", "alias": "test"},
{
"provider": "gcp",
"uid": "example.com:my-project-123456",
"alias": "legacy-gcp",
},
{
"provider": "kubernetes",
"uid": "kubernetes-test-123456789",
@@ -3922,6 +3932,7 @@ class TestAttackPathsScanViewSet:
attack_paths_scan = create_attack_paths_scan(
provider,
scan=scans_fixture[0],
graph_data_ready=True,
)
query_definition = AttackPathsQueryDefinition(
id="aws-rds",
@@ -4000,7 +4011,7 @@ class TestAttackPathsScanViewSet:
assert attributes["nodes"] == graph_payload["nodes"]
assert attributes["relationships"] == graph_payload["relationships"]
def test_run_attack_paths_query_requires_completed_scan(
def test_run_attack_paths_query_blocks_when_graph_data_not_ready(
self,
authenticated_client,
providers_fixture,
@@ -4012,6 +4023,7 @@ class TestAttackPathsScanViewSet:
provider,
scan=scans_fixture[0],
state=StateChoices.EXECUTING,
graph_data_ready=False,
)
response = authenticated_client.post(
@@ -4023,7 +4035,113 @@ class TestAttackPathsScanViewSet:
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert "must be completed" in response.json()["errors"][0]["detail"]
assert "not available" in response.json()["errors"][0]["detail"]
def test_run_attack_paths_query_allows_executing_scan_when_graph_data_ready(
self,
authenticated_client,
providers_fixture,
scans_fixture,
create_attack_paths_scan,
):
provider = providers_fixture[0]
attack_paths_scan = create_attack_paths_scan(
provider,
scan=scans_fixture[0],
state=StateChoices.EXECUTING,
graph_data_ready=True,
)
query_definition = AttackPathsQueryDefinition(
id="aws-test",
name="Test",
short_description="Test query.",
description="Test query",
provider=provider.provider,
cypher="MATCH (n) RETURN n",
parameters=[],
)
with (
patch("api.v1.views.get_query_by_id", return_value=query_definition),
patch(
"api.v1.views.attack_paths_views_helpers.prepare_query_parameters",
return_value={"provider_uid": provider.uid},
),
patch(
"api.v1.views.attack_paths_views_helpers.execute_attack_paths_query",
return_value={
"nodes": [{"id": "n1", "labels": ["AWSAccount"], "properties": {}}],
"relationships": [],
},
),
patch("api.v1.views.graph_database.clear_cache"),
patch(
"api.v1.views.graph_database.get_database_name", return_value="db-test"
),
):
response = authenticated_client.post(
reverse(
"attack-paths-scans-queries-run",
kwargs={"pk": attack_paths_scan.id},
),
data=self._run_payload("aws-test"),
content_type=API_JSON_CONTENT_TYPE,
)
assert response.status_code == status.HTTP_200_OK
def test_run_attack_paths_query_allows_failed_scan_when_graph_data_ready(
self,
authenticated_client,
providers_fixture,
scans_fixture,
create_attack_paths_scan,
):
provider = providers_fixture[0]
attack_paths_scan = create_attack_paths_scan(
provider,
scan=scans_fixture[0],
state=StateChoices.FAILED,
graph_data_ready=True,
)
query_definition = AttackPathsQueryDefinition(
id="aws-test",
name="Test",
short_description="Test query.",
description="Test query",
provider=provider.provider,
cypher="MATCH (n) RETURN n",
parameters=[],
)
with (
patch("api.v1.views.get_query_by_id", return_value=query_definition),
patch(
"api.v1.views.attack_paths_views_helpers.prepare_query_parameters",
return_value={"provider_uid": provider.uid},
),
patch(
"api.v1.views.attack_paths_views_helpers.execute_attack_paths_query",
return_value={
"nodes": [{"id": "n1", "labels": ["AWSAccount"], "properties": {}}],
"relationships": [],
},
),
patch("api.v1.views.graph_database.clear_cache"),
patch(
"api.v1.views.graph_database.get_database_name", return_value="db-test"
),
):
response = authenticated_client.post(
reverse(
"attack-paths-scans-queries-run",
kwargs={"pk": attack_paths_scan.id},
),
data=self._run_payload("aws-test"),
content_type=API_JSON_CONTENT_TYPE,
)
assert response.status_code == status.HTTP_200_OK
def test_run_attack_paths_query_unknown_query(
self,
@@ -4036,6 +4154,7 @@ class TestAttackPathsScanViewSet:
attack_paths_scan = create_attack_paths_scan(
provider,
scan=scans_fixture[0],
graph_data_ready=True,
)
with patch("api.v1.views.get_query_by_id", return_value=None):
@@ -4062,6 +4181,7 @@ class TestAttackPathsScanViewSet:
attack_paths_scan = create_attack_paths_scan(
provider,
scan=scans_fixture[0],
graph_data_ready=True,
)
query_definition = AttackPathsQueryDefinition(
id="aws-empty",
+1
View File
@@ -1145,6 +1145,7 @@ class AttackPathsScanSerializer(RLSSerializer):
"id",
"state",
"progress",
"graph_data_ready",
"provider",
"provider_alias",
"provider_type",
+63 -2
View File
@@ -1759,6 +1759,25 @@ class ProviderViewSet(DisablePaginationMixin, BaseRLSViewSet):
),
},
),
csa=extend_schema(
tags=["Scan"],
summary="Retrieve CSA CCM compliance report",
description="Download CSA Cloud Controls Matrix (CCM) v4.0 compliance report as a PDF file.",
request=None,
responses={
200: OpenApiResponse(
description="PDF file containing the CSA CCM compliance report"
),
202: OpenApiResponse(description="The task is in progress"),
401: OpenApiResponse(
description="API key missing or user not Authenticated"
),
403: OpenApiResponse(description="There is a problem with credentials"),
404: OpenApiResponse(
description="The scan has no CSA CCM reports, or the CSA CCM report generation task has not started yet"
),
},
),
)
@method_decorator(CACHE_DECORATOR, name="list")
@method_decorator(CACHE_DECORATOR, name="retrieve")
@@ -1824,6 +1843,9 @@ class ScanViewSet(BaseRLSViewSet):
elif self.action == "nis2":
if hasattr(self, "response_serializer_class"):
return self.response_serializer_class
elif self.action == "csa":
if hasattr(self, "response_serializer_class"):
return self.response_serializer_class
return super().get_serializer_class()
def partial_update(self, request, *args, **kwargs):
@@ -2185,6 +2207,45 @@ class ScanViewSet(BaseRLSViewSet):
content, filename = loader
return self._serve_file(content, filename, "application/pdf")
@action(
detail=True,
methods=["get"],
url_name="csa",
)
def csa(self, request, pk=None):
scan = self.get_object()
running_resp = self._get_task_status(scan)
if running_resp:
return running_resp
if not scan.output_location:
return Response(
{
"detail": "The scan has no reports, or the CSA CCM report generation task has not started yet."
},
status=status.HTTP_404_NOT_FOUND,
)
if scan.output_location.startswith("s3://"):
bucket = env.str("DJANGO_OUTPUT_S3_AWS_OUTPUT_BUCKET", "")
key_prefix = scan.output_location.removeprefix(f"s3://{bucket}/")
prefix = os.path.join(
os.path.dirname(key_prefix),
"csa",
"*_csa_report.pdf",
)
loader = self._load_file(prefix, s3=True, bucket=bucket, list_objects=True)
else:
base = os.path.dirname(scan.output_location)
pattern = os.path.join(base, "csa", "*_csa_report.pdf")
loader = self._load_file(pattern, s3=False)
if isinstance(loader, Response):
return loader
content, filename = loader
return self._serve_file(content, filename, "application/pdf")
def create(self, request, *args, **kwargs):
input_serializer = self.get_serializer(data=request.data)
input_serializer.is_valid(raise_exception=True)
@@ -2421,10 +2482,10 @@ class AttackPathsScanViewSet(BaseRLSViewSet):
def run_attack_paths_query(self, request, pk=None):
attack_paths_scan = self.get_object()
if attack_paths_scan.state != StateChoices.COMPLETED:
if not attack_paths_scan.graph_data_ready:
raise ValidationError(
{
"detail": "The Attack Paths scan must be completed before running Attack Paths queries"
"detail": "Attack Paths data is not available for querying - a scan must complete at least once before queries can be run"
}
)
@@ -2,7 +2,9 @@ from datetime import datetime, timezone
from typing import Any
from cartography.config import Config as CartographyConfig
from celery.utils.log import get_task_logger
from api.attack_paths import database as graph_database
from api.db_utils import rls_transaction
from api.models import (
AttackPathsScan as ProwlerAPIAttackPathsScan,
@@ -11,6 +13,8 @@ from api.models import (
)
from tasks.jobs.attack_paths.config import is_provider_available
logger = get_task_logger(__name__)
def can_provider_run_attack_paths_scan(tenant_id: str, provider_id: int) -> bool:
with rls_transaction(tenant_id):
@@ -28,12 +32,21 @@ def create_attack_paths_scan(
return None
with rls_transaction(tenant_id):
# Inherit graph_data_ready from the previous scan for this provider,
# so queries remain available while the new scan runs.
previous_data_ready = ProwlerAPIAttackPathsScan.objects.filter(
tenant_id=tenant_id,
provider_id=provider_id,
graph_data_ready=True,
).exists()
attack_paths_scan = ProwlerAPIAttackPathsScan.objects.create(
tenant_id=tenant_id,
provider_id=provider_id,
scan_id=scan_id,
state=StateChoices.SCHEDULED,
started_at=datetime.now(tz=timezone.utc),
graph_data_ready=previous_data_ready,
)
attack_paths_scan.save()
@@ -116,6 +129,32 @@ def update_attack_paths_scan_progress(
attack_paths_scan.save(update_fields=["progress"])
def set_graph_data_ready(
attack_paths_scan: ProwlerAPIAttackPathsScan,
ready: bool,
) -> None:
with rls_transaction(attack_paths_scan.tenant_id):
attack_paths_scan.graph_data_ready = ready
attack_paths_scan.save(update_fields=["graph_data_ready"])
def set_provider_graph_data_ready(
attack_paths_scan: ProwlerAPIAttackPathsScan,
ready: bool,
) -> None:
"""
Set `graph_data_ready` for ALL scans of the same provider.
Used before drop/sync so that older scan IDs cannot bypass the query gate while the graph is being replaced.
"""
with rls_transaction(attack_paths_scan.tenant_id):
ProwlerAPIAttackPathsScan.objects.filter(
tenant_id=attack_paths_scan.tenant_id,
provider_id=attack_paths_scan.provider_id,
).update(graph_data_ready=ready)
attack_paths_scan.refresh_from_db(fields=["graph_data_ready"])
def fail_attack_paths_scan(
tenant_id: str,
scan_id: str,
@@ -130,6 +169,17 @@ def fail_attack_paths_scan(
StateChoices.COMPLETED,
StateChoices.FAILED,
):
tmp_db_name = graph_database.get_database_name(
attack_paths_scan.id, temporary=True
)
try:
graph_database.drop_database(tmp_db_name)
except Exception:
logger.exception(
f"Failed to drop temp database {tmp_db_name} during failure handling"
)
finish_attack_paths_scan(
attack_paths_scan,
StateChoices.FAILED,
@@ -169,6 +169,7 @@ def run(tenant_id: str, scan_id: str, task_id: str) -> dict[str, Any]:
sync.create_sync_indexes(tenant_neo4j_session)
logger.info(f"Deleting existing provider graph in {tenant_database_name}")
db_utils.set_provider_graph_data_ready(attack_paths_scan, False)
graph_database.drop_subgraph(
database=tenant_database_name,
provider_id=str(prowler_api_provider.id),
@@ -183,6 +184,7 @@ def run(tenant_id: str, scan_id: str, task_id: str) -> dict[str, Any]:
target_database=tenant_database_name,
provider_id=str(prowler_api_provider.id),
)
db_utils.set_graph_data_ready(attack_paths_scan, True)
db_utils.update_attack_paths_scan_progress(attack_paths_scan, 99)
logger.info(f"Clearing Neo4j cache for database {tenant_database_name}")
@@ -209,6 +211,7 @@ def run(tenant_id: str, scan_id: str, task_id: str) -> dict[str, Any]:
# Handling databases changes
try:
graph_database.drop_database(tmp_cartography_config.neo4j_database)
except Exception:
logger.exception(
f"Failed to drop temporary Neo4j database {tmp_cartography_config.neo4j_database} during cleanup"
+38 -15
View File
@@ -27,23 +27,24 @@ def delete_provider(tenant_id: str, pk: str):
Returns:
dict: A dictionary with the count of deleted objects per model,
including related models.
Raises:
Provider.DoesNotExist: If no instance with the provided primary key exists.
including related models. Returns an empty dict if the provider
was already deleted.
"""
# Delete the Attack Paths' graph data related to the provider
tenant_database_name = graph_database.get_database_name(tenant_id)
try:
graph_database.drop_subgraph(tenant_database_name, str(pk))
except graph_database.GraphDatabaseQueryException as gdb_error:
logger.error(f"Error deleting Provider graph data: {gdb_error}")
raise
# Get all provider related data and delete them in batches
# Get all provider related data to delete them in batches
with rls_transaction(tenant_id):
instance = Provider.all_objects.get(pk=pk)
try:
instance = Provider.all_objects.get(pk=pk)
except Provider.DoesNotExist:
logger.info(f"Provider `{pk}` already deleted, skipping")
return {}
attack_paths_scan_ids = list(
AttackPathsScan.all_objects.filter(provider=instance).values_list(
"id", flat=True
)
)
deletion_steps = [
("Scan Summaries", ScanSummary.all_objects.filter(scan__provider=instance)),
("Findings", Finding.all_objects.filter(scan__provider=instance)),
@@ -52,6 +53,25 @@ def delete_provider(tenant_id: str, pk: str):
("AttackPathsScans", AttackPathsScan.all_objects.filter(provider=instance)),
]
# Drop orphaned temporary Neo4j databases
for aps_id in attack_paths_scan_ids:
tmp_db_name = graph_database.get_database_name(aps_id, temporary=True)
try:
graph_database.drop_database(tmp_db_name)
except graph_database.GraphDatabaseQueryException:
logger.warning(f"Failed to drop temp database {tmp_db_name}, continuing")
# Delete the Attack Paths' graph data related to the provider from the tenant database
tenant_database_name = graph_database.get_database_name(tenant_id)
try:
graph_database.drop_subgraph(tenant_database_name, str(pk))
except graph_database.GraphDatabaseQueryException as gdb_error:
logger.error(f"Error deleting Provider graph data: {gdb_error}")
raise
# Delete related data in batches
deletion_summary = {}
for step_name, queryset in deletion_steps:
try:
@@ -61,6 +81,7 @@ def delete_provider(tenant_id: str, pk: str):
logger.error(f"Error deleting {step_name}: {db_error}")
raise
# Delete the provider instance itself
try:
with rls_transaction(tenant_id):
_, provider_summary = instance.delete()
@@ -85,7 +106,9 @@ def delete_tenant(pk: str):
"""
deletion_summary = {}
for provider in Provider.objects.using(MainRouter.admin_db).filter(tenant_id=pk):
for provider in Provider.all_objects.using(MainRouter.admin_db).filter(
tenant_id=pk
):
summary = delete_provider(pk, provider.id)
deletion_summary.update(summary)
+118 -2
View File
@@ -6,6 +6,7 @@ from config.django.base import DJANGO_TMP_OUTPUT_DIRECTORY
from tasks.jobs.export import _generate_compliance_output_directory, _upload_to_s3
from tasks.jobs.reports import (
FRAMEWORK_REGISTRY,
CSAReportGenerator,
ENSReportGenerator,
NIS2ReportGenerator,
ThreatScoreReportGenerator,
@@ -147,6 +148,49 @@ def generate_nis2_report(
)
def generate_csa_report(
tenant_id: str,
scan_id: str,
compliance_id: str,
output_path: str,
provider_id: str,
only_failed: bool = True,
include_manual: bool = False,
provider_obj: Provider | None = None,
requirement_statistics: dict[str, dict[str, int]] | None = None,
findings_cache: dict[str, list[FindingOutput]] | None = None,
) -> None:
"""
Generate a PDF compliance report for CSA Cloud Controls Matrix (CCM) v4.0.
Args:
tenant_id: The tenant ID for Row-Level Security context.
scan_id: ID of the scan executed by Prowler.
compliance_id: ID of the compliance framework (e.g., "csa_ccm_4.0_aws").
output_path: Output PDF file path.
provider_id: Provider ID for the scan.
only_failed: If True, only include failed requirements in detailed section.
include_manual: If True, include manual requirements in detailed section.
provider_obj: Pre-fetched Provider object to avoid duplicate queries.
requirement_statistics: Pre-aggregated requirement statistics.
findings_cache: Cache of already loaded findings to avoid duplicate queries.
"""
generator = CSAReportGenerator(FRAMEWORK_REGISTRY["csa_ccm"])
generator.generate(
tenant_id=tenant_id,
scan_id=scan_id,
compliance_id=compliance_id,
output_path=output_path,
provider_id=provider_id,
provider_obj=provider_obj,
requirement_statistics=requirement_statistics,
findings_cache=findings_cache,
only_failed=only_failed,
include_manual=include_manual,
)
def generate_compliance_reports(
tenant_id: str,
scan_id: str,
@@ -154,11 +198,14 @@ def generate_compliance_reports(
generate_threatscore: bool = True,
generate_ens: bool = True,
generate_nis2: bool = True,
generate_csa: bool = True,
only_failed_threatscore: bool = True,
min_risk_level_threatscore: int = 4,
include_manual_ens: bool = True,
include_manual_nis2: bool = False,
only_failed_nis2: bool = True,
only_failed_csa: bool = True,
include_manual_csa: bool = False,
) -> dict[str, dict[str, bool | str]]:
"""
Generate multiple compliance reports with shared database queries.
@@ -175,23 +222,27 @@ def generate_compliance_reports(
generate_threatscore: Whether to generate ThreatScore report.
generate_ens: Whether to generate ENS report.
generate_nis2: Whether to generate NIS2 report.
generate_csa: Whether to generate CSA CCM report.
only_failed_threatscore: For ThreatScore, only include failed requirements.
min_risk_level_threatscore: Minimum risk level for ThreatScore critical requirements.
include_manual_ens: For ENS, include manual requirements.
include_manual_nis2: For NIS2, include manual requirements.
only_failed_nis2: For NIS2, only include failed requirements.
only_failed_csa: For CSA CCM, only include failed requirements.
include_manual_csa: For CSA CCM, include manual requirements.
Returns:
Dictionary with results for each report type.
"""
logger.info(
"Generating compliance reports for scan %s with provider %s"
" (ThreatScore: %s, ENS: %s, NIS2: %s)",
" (ThreatScore: %s, ENS: %s, NIS2: %s, CSA: %s)",
scan_id,
provider_id,
generate_threatscore,
generate_ens,
generate_nis2,
generate_csa,
)
results = {}
@@ -206,6 +257,8 @@ def generate_compliance_reports(
results["ens"] = {"upload": False, "path": ""}
if generate_nis2:
results["nis2"] = {"upload": False, "path": ""}
if generate_csa:
results["csa"] = {"upload": False, "path": ""}
return results
provider_obj = Provider.objects.get(id=provider_id)
@@ -235,7 +288,23 @@ def generate_compliance_reports(
results["nis2"] = {"upload": False, "path": ""}
generate_nis2 = False
if not generate_threatscore and not generate_ens and not generate_nis2:
if generate_csa and provider_type not in [
"aws",
"azure",
"gcp",
"oraclecloud",
"alibabacloud",
]:
logger.info("Provider %s not supported for CSA CCM report", provider_type)
results["csa"] = {"upload": False, "path": ""}
generate_csa = False
if (
not generate_threatscore
and not generate_ens
and not generate_nis2
and not generate_csa
):
return results
# Aggregate requirement statistics once
@@ -274,6 +343,13 @@ def generate_compliance_reports(
scan_id,
compliance_framework="nis2",
)
csa_path = _generate_compliance_output_directory(
DJANGO_TMP_OUTPUT_DIRECTORY,
provider_uid,
tenant_id,
scan_id,
compliance_framework="csa",
)
out_dir = str(Path(threatscore_path).parent.parent)
except Exception as e:
logger.error("Error generating output directory: %s", e)
@@ -284,6 +360,8 @@ def generate_compliance_reports(
results["ens"] = error_dict.copy()
if generate_nis2:
results["nis2"] = error_dict.copy()
if generate_csa:
results["csa"] = error_dict.copy()
return results
# Generate ThreatScore report
@@ -456,6 +534,41 @@ def generate_compliance_reports(
logger.error("Error generating NIS2 report: %s", e)
results["nis2"] = {"upload": False, "path": "", "error": str(e)}
# Generate CSA CCM report
if generate_csa:
compliance_id_csa = f"csa_ccm_4.0_{provider_type}"
pdf_path_csa = f"{csa_path}_csa_report.pdf"
logger.info("Generating CSA CCM report with compliance %s", compliance_id_csa)
try:
generate_csa_report(
tenant_id=tenant_id,
scan_id=scan_id,
compliance_id=compliance_id_csa,
output_path=pdf_path_csa,
provider_id=provider_id,
only_failed=only_failed_csa,
include_manual=include_manual_csa,
provider_obj=provider_obj,
requirement_statistics=requirement_statistics,
findings_cache=findings_cache,
)
upload_uri_csa = _upload_to_s3(
tenant_id, scan_id, pdf_path_csa, f"csa/{Path(pdf_path_csa).name}"
)
if upload_uri_csa:
results["csa"] = {"upload": True, "path": upload_uri_csa}
logger.info("CSA CCM report uploaded to %s", upload_uri_csa)
else:
results["csa"] = {"upload": False, "path": out_dir}
logger.warning("CSA CCM report saved locally at %s", out_dir)
except Exception as e:
logger.error("Error generating CSA CCM report: %s", e)
results["csa"] = {"upload": False, "path": "", "error": str(e)}
# Clean up temporary files if all reports were uploaded successfully
all_uploaded = all(
result.get("upload", False)
@@ -481,6 +594,7 @@ def generate_compliance_reports_job(
generate_threatscore: bool = True,
generate_ens: bool = True,
generate_nis2: bool = True,
generate_csa: bool = True,
) -> dict[str, dict[str, bool | str]]:
"""
Celery task wrapper for generate_compliance_reports.
@@ -492,6 +606,7 @@ def generate_compliance_reports_job(
generate_threatscore: Whether to generate ThreatScore report.
generate_ens: Whether to generate ENS report.
generate_nis2: Whether to generate NIS2 report.
generate_csa: Whether to generate CSA CCM report.
Returns:
Dictionary with results for each report type.
@@ -503,4 +618,5 @@ def generate_compliance_reports_job(
generate_threatscore=generate_threatscore,
generate_ens=generate_ens,
generate_nis2=generate_nis2,
generate_csa=generate_csa,
)
@@ -71,6 +71,8 @@ from .config import (
COLOR_PROWLER_DARK_GREEN,
COLOR_SAFE,
COLOR_WHITE,
CSA_CCM_SECTION_SHORT_NAMES,
CSA_CCM_SECTIONS,
DIMENSION_KEYS,
DIMENSION_MAPPING,
DIMENSION_NAMES,
@@ -90,6 +92,7 @@ from .config import (
)
# Framework-specific generators
from .csa import CSAReportGenerator
from .ens import ENSReportGenerator
from .nis2 import NIS2ReportGenerator
from .threatscore import ThreatScoreReportGenerator
@@ -105,6 +108,7 @@ __all__ = [
"ThreatScoreReportGenerator",
"ENSReportGenerator",
"NIS2ReportGenerator",
"CSAReportGenerator",
# Configuration
"FrameworkConfig",
"FRAMEWORK_REGISTRY",
@@ -147,6 +151,8 @@ __all__ = [
"THREATSCORE_SECTIONS",
"NIS2_SECTIONS",
"NIS2_SECTION_TITLES",
"CSA_CCM_SECTIONS",
"CSA_CCM_SECTION_SHORT_NAMES",
# Layout constants
"COL_WIDTH_SMALL",
"COL_WIDTH_MEDIUM",
@@ -662,6 +662,9 @@ class BaseComplianceReportGenerator(ABC):
elements.append(create_status_badge(req.status))
elements.append(Spacer(1, 0.1 * inch))
# Hook for subclasses to add extra detail (e.g., CSA attributes)
elements.extend(self._render_requirement_detail_extras(req, data))
# Findings for this requirement
for check_id in req.checks:
elements.append(Paragraph(f"Check: {check_id}", self.styles["h2"]))
@@ -701,6 +704,24 @@ class BaseComplianceReportGenerator(ABC):
return page_text, "Powered by Prowler"
def _render_requirement_detail_extras(
self, req: RequirementData, data: ComplianceData
) -> list:
"""Hook for subclasses to render extra content in detailed findings.
Called after the status badge for each requirement in the detailed
findings section. Override in subclasses to add framework-specific
metadata (e.g., CSA CCM attributes).
Args:
req: The requirement being rendered.
data: Aggregated compliance data.
Returns:
List of ReportLab elements (empty by default).
"""
return []
# =========================================================================
# Private Helper Methods
# =========================================================================
@@ -143,6 +143,36 @@ NIS2_SECTION_TITLES = {
"12": "12. Asset Management",
}
# CSA CCM sections (Cloud Controls Matrix v4.0 domains)
CSA_CCM_SECTIONS = [
"Application & Interface Security",
"Audit & Assurance",
"Business Continuity Management and Operational Resilience",
"Change Control and Configuration Management",
"Cryptography, Encryption & Key Management",
"Data Security and Privacy Lifecycle Management",
"Datacenter Security",
"Governance, Risk and Compliance",
"Identity & Access Management",
"Infrastructure & Virtualization Security",
"Interoperability & Portability",
"Logging and Monitoring",
"Security Incident Management, E-Discovery, & Cloud Forensics",
"Threat & Vulnerability Management",
"Universal Endpoint Management",
]
# Short names for CSA CCM sections (used in chart labels)
CSA_CCM_SECTION_SHORT_NAMES = {
"Application & Interface Security": "App & Interface Security",
"Business Continuity Management and Operational Resilience": "Business Continuity",
"Change Control and Configuration Management": "Change Control & Config",
"Cryptography, Encryption & Key Management": "Cryptography & Encryption",
"Data Security and Privacy Lifecycle Management": "Data Security & Privacy",
"Security Incident Management, E-Discovery, & Cloud Forensics": "Incident Mgmt & Forensics",
"Infrastructure & Virtualization Security": "Infrastructure & Virtualization",
}
# Table column widths
COL_WIDTH_SMALL = 0.4 * inch
COL_WIDTH_MEDIUM = 0.9 * inch
@@ -261,6 +291,28 @@ FRAMEWORK_REGISTRY: dict[str, FrameworkConfig] = {
has_niveles=False,
has_weight=False,
),
"csa_ccm": FrameworkConfig(
name="csa_ccm",
display_name="CSA Cloud Controls Matrix (CCM)",
logo_filename=None,
primary_color=COLOR_BLUE,
secondary_color=COLOR_LIGHT_BLUE,
bg_color=COLOR_BG_BLUE,
attribute_fields=[
"Section",
"CCMLite",
"IaaS",
"PaaS",
"SaaS",
"ScopeApplicability",
],
sections=CSA_CCM_SECTIONS,
language="en",
has_risk_levels=False,
has_dimensions=False,
has_niveles=False,
has_weight=False,
),
}
@@ -282,5 +334,7 @@ def get_framework_config(compliance_id: str) -> FrameworkConfig | None:
return FRAMEWORK_REGISTRY["ens"]
if "nis2" in compliance_lower:
return FRAMEWORK_REGISTRY["nis2"]
if "csa" in compliance_lower or "ccm" in compliance_lower:
return FRAMEWORK_REGISTRY["csa_ccm"]
return None
+474
View File
@@ -0,0 +1,474 @@
from collections import defaultdict
from celery.utils.log import get_task_logger
from reportlab.lib.units import inch
from reportlab.platypus import Image, PageBreak, Paragraph, Spacer, Table, TableStyle
from api.models import StatusChoices
from .base import (
BaseComplianceReportGenerator,
ComplianceData,
get_requirement_metadata,
)
from .charts import create_horizontal_bar_chart, get_chart_color_for_percentage
from .config import (
COLOR_BG_BLUE,
COLOR_BLUE,
COLOR_BORDER_GRAY,
COLOR_DARK_GRAY,
COLOR_GRID_GRAY,
COLOR_HIGH_RISK,
COLOR_SAFE,
COLOR_WHITE,
CSA_CCM_SECTION_SHORT_NAMES,
CSA_CCM_SECTIONS,
)
logger = get_task_logger(__name__)
class CSAReportGenerator(BaseComplianceReportGenerator):
"""
PDF report generator for CSA Cloud Controls Matrix (CCM) v4.0.
This generator creates comprehensive PDF reports containing:
- Cover page with Prowler logo
- Executive summary with overall compliance score
- Section analysis with horizontal bar chart
- Section breakdown table
- Requirements index organized by section
- Detailed findings for failed requirements
"""
def create_executive_summary(self, data: ComplianceData) -> list:
"""
Create the executive summary with compliance metrics.
Args:
data: Aggregated compliance data.
Returns:
List of ReportLab elements.
"""
elements = []
elements.append(Paragraph("Executive Summary", self.styles["h1"]))
elements.append(Spacer(1, 0.1 * inch))
# Calculate statistics
total = len(data.requirements)
passed = sum(1 for r in data.requirements if r.status == StatusChoices.PASS)
failed = sum(1 for r in data.requirements if r.status == StatusChoices.FAIL)
manual = sum(1 for r in data.requirements if r.status == StatusChoices.MANUAL)
logger.info(
"CSA CCM Executive Summary: total=%d, passed=%d, failed=%d, manual=%d",
total,
passed,
failed,
manual,
)
# Log sample of requirements for debugging
for req in data.requirements[:5]:
logger.info(
" Requirement %s: status=%s, passed_findings=%d, total_findings=%d",
req.id,
req.status,
req.passed_findings,
req.total_findings,
)
# Calculate compliance excluding manual
evaluated = passed + failed
overall_compliance = (passed / evaluated * 100) if evaluated > 0 else 100
# Summary statistics table
summary_data = [
["Metric", "Value"],
["Total Requirements", str(total)],
["Passed \u2713", str(passed)],
["Failed \u2717", str(failed)],
["Manual \u2299", str(manual)],
["Overall Compliance", f"{overall_compliance:.1f}%"],
]
summary_table = Table(summary_data, colWidths=[3 * inch, 2 * inch])
summary_table.setStyle(
TableStyle(
[
("BACKGROUND", (0, 0), (-1, 0), COLOR_BLUE),
("TEXTCOLOR", (0, 0), (-1, 0), COLOR_WHITE),
("BACKGROUND", (0, 2), (0, 2), COLOR_SAFE),
("TEXTCOLOR", (0, 2), (0, 2), COLOR_WHITE),
("BACKGROUND", (0, 3), (0, 3), COLOR_HIGH_RISK),
("TEXTCOLOR", (0, 3), (0, 3), COLOR_WHITE),
("BACKGROUND", (0, 4), (0, 4), COLOR_DARK_GRAY),
("TEXTCOLOR", (0, 4), (0, 4), COLOR_WHITE),
("ALIGN", (0, 0), (-1, -1), "CENTER"),
("FONTNAME", (0, 0), (-1, 0), "PlusJakartaSans"),
("FONTSIZE", (0, 0), (-1, 0), 12),
("FONTSIZE", (0, 1), (-1, -1), 10),
("BOTTOMPADDING", (0, 0), (-1, 0), 10),
("GRID", (0, 0), (-1, -1), 0.5, COLOR_BORDER_GRAY),
(
"ROWBACKGROUNDS",
(1, 1),
(1, -1),
[COLOR_WHITE, COLOR_BG_BLUE],
),
]
)
)
elements.append(summary_table)
return elements
def create_charts_section(self, data: ComplianceData) -> list:
"""
Create the charts section with section analysis.
Args:
data: Aggregated compliance data.
Returns:
List of ReportLab elements.
"""
elements = []
# Section chart
elements.append(Paragraph("Compliance by Section", self.styles["h1"]))
elements.append(Spacer(1, 0.1 * inch))
elements.append(
Paragraph(
"The following chart shows compliance percentage for each domain "
"of the CSA Cloud Controls Matrix:",
self.styles["normal_center"],
)
)
elements.append(Spacer(1, 0.1 * inch))
chart_buffer = self._create_section_chart(data)
chart_buffer.seek(0)
chart_image = Image(chart_buffer, width=6.5 * inch, height=5 * inch)
elements.append(chart_image)
elements.append(PageBreak())
# Section breakdown table
elements.append(Paragraph("Section Breakdown", self.styles["h1"]))
elements.append(Spacer(1, 0.1 * inch))
section_table = self._create_section_table(data)
elements.append(section_table)
return elements
def create_requirements_index(self, data: ComplianceData) -> list:
"""
Create the requirements index organized by section.
Args:
data: Aggregated compliance data.
Returns:
List of ReportLab elements.
"""
elements = []
elements.append(Paragraph("Requirements Index", self.styles["h1"]))
elements.append(Spacer(1, 0.1 * inch))
# Organize by section
sections = {}
for req in data.requirements:
m = get_requirement_metadata(req.id, data.attributes_by_requirement_id)
if m:
section = getattr(m, "Section", "Other")
if section not in sections:
sections[section] = []
sections[section].append(
{
"id": req.id,
"description": req.description,
"status": req.status,
}
)
# Sort by CSA CCM section order
for section in CSA_CCM_SECTIONS:
if section not in sections:
continue
elements.append(Paragraph(section, self.styles["h2"]))
for req in sections[section]:
status_indicator = (
"\u2713" if req["status"] == StatusChoices.PASS else "\u2717"
)
if req["status"] == StatusChoices.MANUAL:
status_indicator = "\u2299"
desc = (
req["description"][:80] + "..."
if len(req["description"]) > 80
else req["description"]
)
elements.append(
Paragraph(
f"{status_indicator} <b>{req['id']}</b>: {desc}",
self.styles["normal"],
)
)
elements.append(Spacer(1, 0.1 * inch))
return elements
def _render_requirement_detail_extras(self, req, data: ComplianceData) -> list:
"""
Render CSA CCM attributes in the detailed findings view.
Shows CCMLite flag, IaaS/PaaS/SaaS applicability, and
cross-framework references after the status badge for each requirement.
Args:
req: The requirement being rendered.
data: Aggregated compliance data.
Returns:
List of ReportLab elements.
"""
m = get_requirement_metadata(req.id, data.attributes_by_requirement_id)
if not m:
return []
return self._format_requirement_attributes(m)
def _format_requirement_attributes(self, m) -> list:
"""
Format CSA CCM requirement attributes as compact PDF elements.
Displays CCMLite flag, IaaS/PaaS/SaaS applicability, and
cross-framework references from ScopeApplicability.
Args:
m: Requirement metadata (CSA_CCM_Requirement_Attribute).
Returns:
List of ReportLab elements.
"""
elements = []
# Applicability line: CCMLite | IaaS | PaaS | SaaS
ccm_lite = getattr(m, "CCMLite", "")
iaas = getattr(m, "IaaS", "")
paas = getattr(m, "PaaS", "")
saas = getattr(m, "SaaS", "")
applicability_parts = []
if ccm_lite:
applicability_parts.append(f"CCMLite: {ccm_lite}")
if iaas:
applicability_parts.append(f"IaaS: {iaas}")
if paas:
applicability_parts.append(f"PaaS: {paas}")
if saas:
applicability_parts.append(f"SaaS: {saas}")
if applicability_parts:
elements.append(
Paragraph(
f"<font color='#4A5568' size='10'>"
f"{'&nbsp;&nbsp;|&nbsp;&nbsp;'.join(applicability_parts)}"
f"</font>",
self._attr_style(),
)
)
# ScopeApplicability references (compact)
scope_list = getattr(m, "ScopeApplicability", [])
if scope_list:
refs = []
for scope in scope_list:
ref_id = scope.get("ReferenceId", "") if isinstance(scope, dict) else ""
identifiers = (
scope.get("Identifiers", []) if isinstance(scope, dict) else []
)
if ref_id and identifiers:
ids_str = ", ".join(str(i) for i in identifiers[:4])
if len(identifiers) > 4:
ids_str += "..."
refs.append(f"{ref_id}: {ids_str}")
if refs:
refs_text = "&nbsp;&nbsp;|&nbsp;&nbsp;".join(refs)
elements.append(
Paragraph(
f"<font color='#718096' size='9'>{refs_text}</font>",
self._attr_style(),
)
)
return elements
def _attr_style(self):
"""
Return a compact style for attribute text lines.
Returns:
ParagraphStyle for attribute display.
"""
from reportlab.lib.styles import ParagraphStyle
return ParagraphStyle(
"AttrLine",
parent=self.styles["normal"],
fontSize=10,
spaceBefore=2,
spaceAfter=2,
leftIndent=30,
leading=13,
)
def _create_section_chart(self, data: ComplianceData):
"""
Create the section compliance chart.
Args:
data: Aggregated compliance data.
Returns:
BytesIO buffer containing the chart image.
"""
section_scores = defaultdict(lambda: {"passed": 0, "total": 0})
no_metadata_count = 0
for req in data.requirements:
if req.status == StatusChoices.MANUAL:
continue
m = get_requirement_metadata(req.id, data.attributes_by_requirement_id)
if m:
section = getattr(m, "Section", "Other")
section_scores[section]["total"] += 1
if req.status == StatusChoices.PASS:
section_scores[section]["passed"] += 1
else:
no_metadata_count += 1
if no_metadata_count > 0:
logger.warning(
"CSA CCM chart: %d requirements had no metadata", no_metadata_count
)
logger.info("CSA CCM section scores:")
for section in CSA_CCM_SECTIONS:
if section in section_scores:
scores = section_scores[section]
pct = (
(scores["passed"] / scores["total"] * 100)
if scores["total"] > 0
else 0
)
logger.info(
" %s: %d/%d (%.1f%%)",
section,
scores["passed"],
scores["total"],
pct,
)
# Build labels and values in CSA CCM section order
labels = []
values = []
for section in CSA_CCM_SECTIONS:
if section in section_scores and section_scores[section]["total"] > 0:
scores = section_scores[section]
pct = (scores["passed"] / scores["total"]) * 100
# Use short name if available
label = CSA_CCM_SECTION_SHORT_NAMES.get(section, section)
labels.append(label)
values.append(pct)
return create_horizontal_bar_chart(
labels=labels,
values=values,
xlabel="Compliance (%)",
color_func=get_chart_color_for_percentage,
)
def _create_section_table(self, data: ComplianceData) -> Table:
"""
Create the section breakdown table.
Args:
data: Aggregated compliance data.
Returns:
ReportLab Table element.
"""
section_scores = defaultdict(lambda: {"passed": 0, "failed": 0, "manual": 0})
for req in data.requirements:
m = get_requirement_metadata(req.id, data.attributes_by_requirement_id)
if m:
section = getattr(m, "Section", "Other")
if req.status == StatusChoices.PASS:
section_scores[section]["passed"] += 1
elif req.status == StatusChoices.FAIL:
section_scores[section]["failed"] += 1
else:
section_scores[section]["manual"] += 1
table_data = [["Section", "Passed", "Failed", "Manual", "Compliance"]]
for section in CSA_CCM_SECTIONS:
if section not in section_scores:
continue
scores = section_scores[section]
total = scores["passed"] + scores["failed"]
pct = (scores["passed"] / total * 100) if total > 0 else 100
# Use short name if available
label = CSA_CCM_SECTION_SHORT_NAMES.get(section, section)
table_data.append(
[
label,
str(scores["passed"]),
str(scores["failed"]),
str(scores["manual"]),
f"{pct:.1f}%",
]
)
table = Table(
table_data,
colWidths=[2.4 * inch, 0.9 * inch, 0.9 * inch, 0.9 * inch, 1.2 * inch],
)
table.setStyle(
TableStyle(
[
("BACKGROUND", (0, 0), (-1, 0), COLOR_BLUE),
("TEXTCOLOR", (0, 0), (-1, 0), COLOR_WHITE),
("FONTNAME", (0, 0), (-1, 0), "FiraCode"),
("FONTSIZE", (0, 0), (-1, 0), 10),
("ALIGN", (0, 0), (-1, -1), "CENTER"),
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
("FONTSIZE", (0, 1), (-1, -1), 9),
("GRID", (0, 0), (-1, -1), 0.5, COLOR_GRID_GRAY),
("LEFTPADDING", (0, 0), (-1, -1), 6),
("RIGHTPADDING", (0, 0), (-1, -1), 6),
("TOPPADDING", (0, 0), (-1, -1), 4),
("BOTTOMPADDING", (0, 0), (-1, -1), 4),
(
"ROWBACKGROUNDS",
(0, 1),
(-1, -1),
[COLOR_WHITE, COLOR_BG_BLUE],
),
]
)
)
return table
@@ -114,6 +114,11 @@ def _calculate_requirements_data_from_statistics(
requirement_status = StatusChoices.PASS
else:
requirement_status = StatusChoices.FAIL
elif requirement_checks:
# Requirement has checks but none produced findings — consistent
# with the dashboard's scan processing which treats this as PASS
# (no failed checks means the requirement is considered compliant).
requirement_status = StatusChoices.PASS
else:
requirement_status = StatusChoices.MANUAL
+5 -4
View File
@@ -10,8 +10,8 @@ from config.django.base import DJANGO_FINDINGS_BATCH_SIZE, DJANGO_TMP_OUTPUT_DIR
from django_celery_beat.models import PeriodicTask
from tasks.jobs.attack_paths import (
attack_paths_scan,
db_utils as attack_paths_db_utils,
can_provider_run_attack_paths_scan,
db_utils as attack_paths_db_utils,
)
from tasks.jobs.backfill import (
backfill_compliance_summaries,
@@ -906,11 +906,11 @@ def jira_integration_task(
@handle_provider_deletion
def generate_compliance_reports_task(tenant_id: str, scan_id: str, provider_id: str):
"""
Optimized task to generate ThreatScore, ENS, and NIS2 reports with shared queries.
Optimized task to generate ThreatScore, ENS, NIS2, and CSA CCM reports with shared queries.
This task is more efficient than running separate report tasks because it reuses database queries:
- Provider object fetched once (instead of three times)
- Requirement statistics aggregated once (instead of three times)
- Provider object fetched once (instead of multiple times)
- Requirement statistics aggregated once (instead of multiple times)
- Can reduce database load by up to 50-70%
Args:
@@ -928,6 +928,7 @@ def generate_compliance_reports_task(tenant_id: str, scan_id: str, provider_id:
generate_threatscore=True,
generate_ens=True,
generate_nis2=True,
generate_csa=True,
)
@@ -28,6 +28,8 @@ class TestAttackPathsRun:
"tasks.jobs.attack_paths.scan.utils.call_within_event_loop",
side_effect=lambda fn, *a, **kw: fn(*a, **kw),
)
@patch("tasks.jobs.attack_paths.scan.db_utils.set_graph_data_ready")
@patch("tasks.jobs.attack_paths.scan.db_utils.set_provider_graph_data_ready")
@patch("tasks.jobs.attack_paths.scan.db_utils.finish_attack_paths_scan")
@patch("tasks.jobs.attack_paths.scan.db_utils.update_attack_paths_scan_progress")
@patch("tasks.jobs.attack_paths.scan.db_utils.starting_attack_paths_scan")
@@ -72,6 +74,8 @@ class TestAttackPathsRun:
mock_starting,
mock_update_progress,
mock_finish,
mock_set_provider_graph_data_ready,
mock_set_graph_data_ready,
mock_event_loop,
mock_drop_db,
tenants_fixture,
@@ -159,9 +163,66 @@ class TestAttackPathsRun:
mock_finish.assert_called_once_with(
attack_paths_scan, StateChoices.COMPLETED, ingestion_result
)
mock_set_provider_graph_data_ready.assert_called_once_with(
attack_paths_scan, False
)
mock_set_graph_data_ready.assert_called_once_with(attack_paths_scan, True)
@patch(
"tasks.jobs.attack_paths.scan.utils.stringify_exception",
return_value="Cartography failed: ingestion boom",
)
@patch(
"tasks.jobs.attack_paths.scan.utils.call_within_event_loop",
side_effect=lambda fn, *a, **kw: fn(*a, **kw),
)
@patch("tasks.jobs.attack_paths.scan.graph_database.drop_database")
@patch("tasks.jobs.attack_paths.scan.db_utils.finish_attack_paths_scan")
@patch("tasks.jobs.attack_paths.scan.db_utils.set_graph_data_ready")
@patch("tasks.jobs.attack_paths.scan.db_utils.set_provider_graph_data_ready")
@patch("tasks.jobs.attack_paths.scan.db_utils.update_attack_paths_scan_progress")
@patch("tasks.jobs.attack_paths.scan.db_utils.starting_attack_paths_scan")
@patch("tasks.jobs.attack_paths.scan.findings.analysis")
@patch("tasks.jobs.attack_paths.scan.internet.analysis")
@patch("tasks.jobs.attack_paths.scan.findings.create_findings_indexes")
@patch("tasks.jobs.attack_paths.scan.cartography_analysis.run")
@patch("tasks.jobs.attack_paths.scan.cartography_create_indexes.run")
@patch("tasks.jobs.attack_paths.scan.graph_database.create_database")
@patch(
"tasks.jobs.attack_paths.scan.graph_database.get_database_name",
return_value="db-scan-id",
)
@patch("tasks.jobs.attack_paths.scan.graph_database.get_uri")
@patch(
"tasks.jobs.attack_paths.scan.initialize_prowler_provider",
return_value=MagicMock(_enabled_regions=["us-east-1"]),
)
@patch(
"tasks.jobs.attack_paths.scan.rls_transaction",
new=lambda *args, **kwargs: nullcontext(),
)
def test_run_failure_marks_scan_failed(
self, tenants_fixture, providers_fixture, scans_fixture
self,
mock_init_provider,
mock_get_uri,
mock_get_db_name,
mock_create_db,
mock_cartography_indexes,
mock_cartography_analysis,
mock_findings_indexes,
mock_internet_analysis,
mock_findings_analysis,
mock_starting,
mock_update_progress,
mock_set_provider_graph_data_ready,
mock_set_graph_data_ready,
mock_finish,
mock_drop_db,
mock_event_loop,
mock_stringify,
tenants_fixture,
providers_fixture,
scans_fixture,
):
tenant = tenants_fixture[0]
provider = providers_fixture[0]
@@ -185,53 +246,18 @@ class TestAttackPathsRun:
ingestion_fn = MagicMock(side_effect=RuntimeError("ingestion boom"))
with (
patch(
"tasks.jobs.attack_paths.scan.rls_transaction",
new=lambda *args, **kwargs: nullcontext(),
),
patch(
"tasks.jobs.attack_paths.scan.initialize_prowler_provider",
return_value=MagicMock(_enabled_regions=["us-east-1"]),
),
patch("tasks.jobs.attack_paths.scan.graph_database.get_uri"),
patch(
"tasks.jobs.attack_paths.scan.graph_database.get_database_name",
return_value="db-scan-id",
),
patch("tasks.jobs.attack_paths.scan.graph_database.create_database"),
patch(
"tasks.jobs.attack_paths.scan.graph_database.get_session",
return_value=session_ctx,
),
patch("tasks.jobs.attack_paths.scan.cartography_create_indexes.run"),
patch("tasks.jobs.attack_paths.scan.cartography_analysis.run"),
patch("tasks.jobs.attack_paths.scan.findings.create_findings_indexes"),
patch("tasks.jobs.attack_paths.scan.internet.analysis"),
patch("tasks.jobs.attack_paths.scan.findings.analysis"),
patch(
"tasks.jobs.attack_paths.scan.db_utils.retrieve_attack_paths_scan",
return_value=attack_paths_scan,
),
patch("tasks.jobs.attack_paths.scan.db_utils.starting_attack_paths_scan"),
patch(
"tasks.jobs.attack_paths.scan.db_utils.update_attack_paths_scan_progress"
),
patch(
"tasks.jobs.attack_paths.scan.db_utils.finish_attack_paths_scan"
) as mock_finish,
patch("tasks.jobs.attack_paths.scan.graph_database.drop_database"),
patch(
"tasks.jobs.attack_paths.scan.get_cartography_ingestion_function",
return_value=ingestion_fn,
),
patch(
"tasks.jobs.attack_paths.scan.utils.call_within_event_loop",
side_effect=lambda fn, *a, **kw: fn(*a, **kw),
),
patch(
"tasks.jobs.attack_paths.scan.utils.stringify_exception",
return_value="Cartography failed: ingestion boom",
),
):
with pytest.raises(RuntimeError, match="ingestion boom"):
attack_paths_run(str(tenant.id), str(scan.id), "task-456")
@@ -241,8 +267,64 @@ class TestAttackPathsRun:
assert failure_args[1] == StateChoices.FAILED
assert failure_args[2] == {"global_error": "Cartography failed: ingestion boom"}
@patch(
"tasks.jobs.attack_paths.scan.utils.stringify_exception",
return_value="Cartography failed: ingestion boom",
)
@patch(
"tasks.jobs.attack_paths.scan.utils.call_within_event_loop",
side_effect=lambda fn, *a, **kw: fn(*a, **kw),
)
@patch(
"tasks.jobs.attack_paths.scan.graph_database.drop_database",
side_effect=ConnectionError("neo4j down"),
)
@patch("tasks.jobs.attack_paths.scan.db_utils.finish_attack_paths_scan")
@patch("tasks.jobs.attack_paths.scan.db_utils.set_graph_data_ready")
@patch("tasks.jobs.attack_paths.scan.db_utils.set_provider_graph_data_ready")
@patch("tasks.jobs.attack_paths.scan.db_utils.update_attack_paths_scan_progress")
@patch("tasks.jobs.attack_paths.scan.db_utils.starting_attack_paths_scan")
@patch("tasks.jobs.attack_paths.scan.findings.analysis")
@patch("tasks.jobs.attack_paths.scan.internet.analysis")
@patch("tasks.jobs.attack_paths.scan.findings.create_findings_indexes")
@patch("tasks.jobs.attack_paths.scan.cartography_analysis.run")
@patch("tasks.jobs.attack_paths.scan.cartography_create_indexes.run")
@patch("tasks.jobs.attack_paths.scan.graph_database.create_database")
@patch(
"tasks.jobs.attack_paths.scan.graph_database.get_database_name",
return_value="db-scan-id",
)
@patch("tasks.jobs.attack_paths.scan.graph_database.get_uri")
@patch(
"tasks.jobs.attack_paths.scan.initialize_prowler_provider",
return_value=MagicMock(_enabled_regions=["us-east-1"]),
)
@patch(
"tasks.jobs.attack_paths.scan.rls_transaction",
new=lambda *args, **kwargs: nullcontext(),
)
def test_run_failure_marks_scan_failed_even_when_drop_database_fails(
self, tenants_fixture, providers_fixture, scans_fixture
self,
mock_init_provider,
mock_get_uri,
mock_get_db_name,
mock_create_db,
mock_cartography_indexes,
mock_cartography_analysis,
mock_findings_indexes,
mock_internet_analysis,
mock_findings_analysis,
mock_starting,
mock_update_progress,
mock_set_provider_graph_data_ready,
mock_set_graph_data_ready,
mock_finish,
mock_drop_db,
mock_event_loop,
mock_stringify,
tenants_fixture,
providers_fixture,
scans_fixture,
):
tenant = tenants_fixture[0]
provider = providers_fixture[0]
@@ -266,56 +348,18 @@ class TestAttackPathsRun:
ingestion_fn = MagicMock(side_effect=RuntimeError("ingestion boom"))
with (
patch(
"tasks.jobs.attack_paths.scan.rls_transaction",
new=lambda *args, **kwargs: nullcontext(),
),
patch(
"tasks.jobs.attack_paths.scan.initialize_prowler_provider",
return_value=MagicMock(_enabled_regions=["us-east-1"]),
),
patch("tasks.jobs.attack_paths.scan.graph_database.get_uri"),
patch(
"tasks.jobs.attack_paths.scan.graph_database.get_database_name",
return_value="db-scan-id",
),
patch("tasks.jobs.attack_paths.scan.graph_database.create_database"),
patch(
"tasks.jobs.attack_paths.scan.graph_database.get_session",
return_value=session_ctx,
),
patch("tasks.jobs.attack_paths.scan.cartography_create_indexes.run"),
patch("tasks.jobs.attack_paths.scan.cartography_analysis.run"),
patch("tasks.jobs.attack_paths.scan.findings.create_findings_indexes"),
patch("tasks.jobs.attack_paths.scan.internet.analysis"),
patch("tasks.jobs.attack_paths.scan.findings.analysis"),
patch(
"tasks.jobs.attack_paths.scan.db_utils.retrieve_attack_paths_scan",
return_value=attack_paths_scan,
),
patch("tasks.jobs.attack_paths.scan.db_utils.starting_attack_paths_scan"),
patch(
"tasks.jobs.attack_paths.scan.db_utils.update_attack_paths_scan_progress"
),
patch(
"tasks.jobs.attack_paths.scan.db_utils.finish_attack_paths_scan"
) as mock_finish,
patch(
"tasks.jobs.attack_paths.scan.graph_database.drop_database",
side_effect=ConnectionError("neo4j down"),
),
patch(
"tasks.jobs.attack_paths.scan.get_cartography_ingestion_function",
return_value=ingestion_fn,
),
patch(
"tasks.jobs.attack_paths.scan.utils.call_within_event_loop",
side_effect=lambda fn, *a, **kw: fn(*a, **kw),
),
patch(
"tasks.jobs.attack_paths.scan.utils.stringify_exception",
return_value="Cartography failed: ingestion boom",
),
):
with pytest.raises(RuntimeError, match="ingestion boom"):
attack_paths_run(str(tenant.id), str(scan.id), "task-789")
@@ -397,6 +441,9 @@ class TestFailAttackPathsScan:
"tasks.jobs.attack_paths.db_utils.retrieve_attack_paths_scan",
return_value=attack_paths_scan,
) as mock_retrieve,
patch(
"tasks.jobs.attack_paths.db_utils.graph_database.drop_database"
) as mock_drop_db,
patch(
"tasks.jobs.attack_paths.db_utils.finish_attack_paths_scan"
) as mock_finish,
@@ -404,6 +451,51 @@ class TestFailAttackPathsScan:
fail_attack_paths_scan(str(tenant.id), str(scan.id), "setup exploded")
mock_retrieve.assert_called_once_with(str(tenant.id), str(scan.id))
expected_tmp_db = f"db-tmp-scan-{str(attack_paths_scan.id).lower()}"
mock_drop_db.assert_called_once_with(expected_tmp_db)
mock_finish.assert_called_once_with(
attack_paths_scan,
StateChoices.FAILED,
{"global_error": "setup exploded"},
)
def test_drops_temp_database_even_when_drop_fails(
self, tenants_fixture, providers_fixture, scans_fixture
):
from tasks.jobs.attack_paths.db_utils import (
fail_attack_paths_scan,
)
tenant = tenants_fixture[0]
provider = providers_fixture[0]
provider.provider = Provider.ProviderChoices.AWS
provider.save()
scan = scans_fixture[0]
scan.provider = provider
scan.save()
attack_paths_scan = AttackPathsScan.objects.create(
tenant_id=tenant.id,
provider=provider,
scan=scan,
state=StateChoices.EXECUTING,
)
with (
patch(
"tasks.jobs.attack_paths.db_utils.retrieve_attack_paths_scan",
return_value=attack_paths_scan,
),
patch(
"tasks.jobs.attack_paths.db_utils.graph_database.drop_database",
side_effect=Exception("Neo4j unreachable"),
),
patch(
"tasks.jobs.attack_paths.db_utils.finish_attack_paths_scan"
) as mock_finish,
):
fail_attack_paths_scan(str(tenant.id), str(scan.id), "setup exploded")
mock_finish.assert_called_once_with(
attack_paths_scan,
StateChoices.FAILED,
@@ -437,12 +529,16 @@ class TestFailAttackPathsScan:
"tasks.jobs.attack_paths.db_utils.retrieve_attack_paths_scan",
return_value=attack_paths_scan,
),
patch(
"tasks.jobs.attack_paths.db_utils.graph_database.drop_database"
) as mock_drop_db,
patch(
"tasks.jobs.attack_paths.db_utils.finish_attack_paths_scan"
) as mock_finish,
):
fail_attack_paths_scan(str(tenant.id), str(scan.id), "setup exploded")
mock_drop_db.assert_not_called()
mock_finish.assert_not_called()
def test_skips_when_no_scan_found(self, tenants_fixture):
@@ -1017,3 +1113,317 @@ class TestInternetAnalysis:
result = internet_module.analysis(mock_session, provider, config)
assert result == 0
@pytest.mark.django_db
class TestAttackPathsDbUtilsGraphDataReady:
"""Tests for db_utils functions related to graph_data_ready lifecycle."""
def test_create_attack_paths_scan_first_scan_defaults_to_false(
self, tenants_fixture, providers_fixture, scans_fixture
):
from tasks.jobs.attack_paths.db_utils import create_attack_paths_scan
tenant = tenants_fixture[0]
provider = providers_fixture[0]
provider.provider = Provider.ProviderChoices.AWS
provider.save()
scan = scans_fixture[0]
scan.provider = provider
scan.save()
with patch(
"tasks.jobs.attack_paths.db_utils.rls_transaction",
new=lambda *args, **kwargs: nullcontext(),
):
attack_paths_scan = create_attack_paths_scan(
str(tenant.id), str(scan.id), provider.id
)
assert attack_paths_scan is not None
assert attack_paths_scan.graph_data_ready is False
def test_create_attack_paths_scan_inherits_true_from_previous(
self, tenants_fixture, providers_fixture, scans_fixture
):
from tasks.jobs.attack_paths.db_utils import create_attack_paths_scan
tenant = tenants_fixture[0]
provider = providers_fixture[0]
provider.provider = Provider.ProviderChoices.AWS
provider.save()
scan = scans_fixture[0]
scan.provider = provider
scan.save()
AttackPathsScan.objects.create(
tenant_id=tenant.id,
provider=provider,
scan=scan,
state=StateChoices.COMPLETED,
graph_data_ready=True,
)
new_scan = Scan.objects.create(
name="New Scan",
provider=provider,
trigger=Scan.TriggerChoices.MANUAL,
state=StateChoices.AVAILABLE,
tenant_id=tenant.id,
)
with patch(
"tasks.jobs.attack_paths.db_utils.rls_transaction",
new=lambda *args, **kwargs: nullcontext(),
):
attack_paths_scan = create_attack_paths_scan(
str(tenant.id), str(new_scan.id), provider.id
)
assert attack_paths_scan is not None
assert attack_paths_scan.graph_data_ready is True
def test_create_attack_paths_scan_inherits_false_when_no_previous_ready(
self, tenants_fixture, providers_fixture, scans_fixture
):
from tasks.jobs.attack_paths.db_utils import create_attack_paths_scan
tenant = tenants_fixture[0]
provider = providers_fixture[0]
provider.provider = Provider.ProviderChoices.AWS
provider.save()
scan = scans_fixture[0]
scan.provider = provider
scan.save()
AttackPathsScan.objects.create(
tenant_id=tenant.id,
provider=provider,
scan=scan,
state=StateChoices.FAILED,
graph_data_ready=False,
)
new_scan = Scan.objects.create(
name="New Scan",
provider=provider,
trigger=Scan.TriggerChoices.MANUAL,
state=StateChoices.AVAILABLE,
tenant_id=tenant.id,
)
with patch(
"tasks.jobs.attack_paths.db_utils.rls_transaction",
new=lambda *args, **kwargs: nullcontext(),
):
attack_paths_scan = create_attack_paths_scan(
str(tenant.id), str(new_scan.id), provider.id
)
assert attack_paths_scan is not None
assert attack_paths_scan.graph_data_ready is False
def test_set_graph_data_ready_updates_field(
self, tenants_fixture, providers_fixture, scans_fixture
):
from tasks.jobs.attack_paths.db_utils import set_graph_data_ready
tenant = tenants_fixture[0]
provider = providers_fixture[0]
provider.provider = Provider.ProviderChoices.AWS
provider.save()
scan = scans_fixture[0]
scan.provider = provider
scan.save()
attack_paths_scan = AttackPathsScan.objects.create(
tenant_id=tenant.id,
provider=provider,
scan=scan,
state=StateChoices.EXECUTING,
graph_data_ready=True,
)
with patch(
"tasks.jobs.attack_paths.db_utils.rls_transaction",
new=lambda *args, **kwargs: nullcontext(),
):
set_graph_data_ready(attack_paths_scan, False)
attack_paths_scan.refresh_from_db()
assert attack_paths_scan.graph_data_ready is False
with patch(
"tasks.jobs.attack_paths.db_utils.rls_transaction",
new=lambda *args, **kwargs: nullcontext(),
):
set_graph_data_ready(attack_paths_scan, True)
attack_paths_scan.refresh_from_db()
assert attack_paths_scan.graph_data_ready is True
def test_finish_attack_paths_scan_does_not_modify_graph_data_ready(
self, tenants_fixture, providers_fixture, scans_fixture
):
from tasks.jobs.attack_paths.db_utils import finish_attack_paths_scan
tenant = tenants_fixture[0]
provider = providers_fixture[0]
provider.provider = Provider.ProviderChoices.AWS
provider.save()
scan = scans_fixture[0]
scan.provider = provider
scan.save()
attack_paths_scan = AttackPathsScan.objects.create(
tenant_id=tenant.id,
provider=provider,
scan=scan,
state=StateChoices.EXECUTING,
graph_data_ready=True,
)
with patch(
"tasks.jobs.attack_paths.db_utils.rls_transaction",
new=lambda *args, **kwargs: nullcontext(),
):
finish_attack_paths_scan(attack_paths_scan, StateChoices.COMPLETED, {})
attack_paths_scan.refresh_from_db()
assert attack_paths_scan.state == StateChoices.COMPLETED
assert attack_paths_scan.graph_data_ready is True
def test_finish_attack_paths_scan_preserves_graph_data_ready_on_failure(
self, tenants_fixture, providers_fixture, scans_fixture
):
from tasks.jobs.attack_paths.db_utils import finish_attack_paths_scan
tenant = tenants_fixture[0]
provider = providers_fixture[0]
provider.provider = Provider.ProviderChoices.AWS
provider.save()
scan = scans_fixture[0]
scan.provider = provider
scan.save()
attack_paths_scan = AttackPathsScan.objects.create(
tenant_id=tenant.id,
provider=provider,
scan=scan,
state=StateChoices.EXECUTING,
graph_data_ready=True,
)
with patch(
"tasks.jobs.attack_paths.db_utils.rls_transaction",
new=lambda *args, **kwargs: nullcontext(),
):
finish_attack_paths_scan(
attack_paths_scan,
StateChoices.FAILED,
{"global_error": "boom"},
)
attack_paths_scan.refresh_from_db()
assert attack_paths_scan.state == StateChoices.FAILED
assert attack_paths_scan.graph_data_ready is True
def test_set_provider_graph_data_ready_updates_all_scans_for_provider(
self, tenants_fixture, providers_fixture, scans_fixture
):
from tasks.jobs.attack_paths.db_utils import set_provider_graph_data_ready
tenant = tenants_fixture[0]
provider = providers_fixture[0]
provider.provider = Provider.ProviderChoices.AWS
provider.save()
scan_a = scans_fixture[0]
scan_a.provider = provider
scan_a.save()
scan_b = Scan.objects.create(
name="Second Scan",
provider=provider,
trigger=Scan.TriggerChoices.MANUAL,
state=StateChoices.AVAILABLE,
tenant_id=tenant.id,
)
old_ap_scan = AttackPathsScan.objects.create(
tenant_id=tenant.id,
provider=provider,
scan=scan_a,
state=StateChoices.COMPLETED,
graph_data_ready=True,
)
new_ap_scan = AttackPathsScan.objects.create(
tenant_id=tenant.id,
provider=provider,
scan=scan_b,
state=StateChoices.EXECUTING,
graph_data_ready=True,
)
with patch(
"tasks.jobs.attack_paths.db_utils.rls_transaction",
new=lambda *args, **kwargs: nullcontext(),
):
set_provider_graph_data_ready(new_ap_scan, False)
old_ap_scan.refresh_from_db()
new_ap_scan.refresh_from_db()
assert old_ap_scan.graph_data_ready is False
assert new_ap_scan.graph_data_ready is False
def test_set_provider_graph_data_ready_does_not_affect_other_providers(
self, tenants_fixture, providers_fixture, scans_fixture
):
from tasks.jobs.attack_paths.db_utils import set_provider_graph_data_ready
tenant = tenants_fixture[0]
provider_a = providers_fixture[0]
provider_a.provider = Provider.ProviderChoices.AWS
provider_a.save()
provider_b = providers_fixture[1]
provider_b.provider = Provider.ProviderChoices.AWS
provider_b.save()
scan_a = scans_fixture[0]
scan_a.provider = provider_a
scan_a.save()
scan_b = Scan.objects.create(
name="Scan for provider B",
provider=provider_b,
trigger=Scan.TriggerChoices.MANUAL,
state=StateChoices.COMPLETED,
tenant_id=tenant.id,
)
ap_scan_a = AttackPathsScan.objects.create(
tenant_id=tenant.id,
provider=provider_a,
scan=scan_a,
state=StateChoices.EXECUTING,
graph_data_ready=True,
)
ap_scan_b = AttackPathsScan.objects.create(
tenant_id=tenant.id,
provider=provider_b,
scan=scan_b,
state=StateChoices.COMPLETED,
graph_data_ready=True,
)
with patch(
"tasks.jobs.attack_paths.db_utils.rls_transaction",
new=lambda *args, **kwargs: nullcontext(),
):
set_provider_graph_data_ready(ap_scan_a, False)
ap_scan_a.refresh_from_db()
ap_scan_b.refresh_from_db()
assert ap_scan_a.graph_data_ready is False
assert ap_scan_b.graph_data_ready is True
+108 -7
View File
@@ -4,6 +4,7 @@ import pytest
from django.core.exceptions import ObjectDoesNotExist
from api.attack_paths import database as graph_database
from api.models import Provider, Tenant
from tasks.jobs.deletion import delete_provider, delete_tenant
@@ -47,14 +48,61 @@ class TestDeleteProvider:
tenant_id = str(tenants_fixture[0].id)
non_existent_pk = "babf6796-cfcc-4fd3-9dcf-88d012247645"
with pytest.raises(ObjectDoesNotExist):
delete_provider(tenant_id, non_existent_pk)
result = delete_provider(tenant_id, non_existent_pk)
mock_get_database_name.assert_called_once_with(tenant_id)
mock_drop_subgraph.assert_called_once_with(
"tenant-db",
non_existent_pk,
)
assert result == {}
mock_get_database_name.assert_not_called()
mock_drop_subgraph.assert_not_called()
def test_delete_provider_drops_temp_attack_paths_databases(
self, providers_fixture, create_attack_paths_scan
):
instance = providers_fixture[0]
tenant_id = str(instance.tenant_id)
aps1 = create_attack_paths_scan(instance)
aps2 = create_attack_paths_scan(instance)
with (
patch(
"tasks.jobs.deletion.graph_database.drop_subgraph",
),
patch(
"tasks.jobs.deletion.graph_database.drop_database",
) as mock_drop_database,
):
result = delete_provider(tenant_id, instance.id)
assert result
expected_tmp_calls = [
call(f"db-tmp-scan-{str(aps1.id).lower()}"),
call(f"db-tmp-scan-{str(aps2.id).lower()}"),
]
mock_drop_database.assert_has_calls(expected_tmp_calls, any_order=True)
def test_delete_provider_continues_when_temp_db_drop_fails(
self, providers_fixture, create_attack_paths_scan
):
instance = providers_fixture[0]
tenant_id = str(instance.tenant_id)
create_attack_paths_scan(instance)
with (
patch(
"tasks.jobs.deletion.graph_database.drop_subgraph",
),
patch(
"tasks.jobs.deletion.graph_database.drop_database",
side_effect=graph_database.GraphDatabaseQueryException(
"Neo4j unreachable"
),
),
):
result = delete_provider(tenant_id, instance.id)
assert result
assert not Provider.all_objects.filter(pk=instance.id).exists()
@pytest.mark.django_db
@@ -142,3 +190,56 @@ class TestDeleteTenant:
mock_get_database_name.assert_called_once_with(tenant.id)
mock_drop_subgraph.assert_not_called()
mock_drop_database.assert_called_once_with("tenant-db")
def test_delete_tenant_includes_soft_deleted_providers(self, tenants_fixture):
tenant = tenants_fixture[0]
provider = Provider.objects.create(
provider="aws",
uid="999999999999",
alias="soft_deleted_provider",
tenant_id=tenant.id,
)
# Soft-delete the provider so ActiveProviderManager would skip it
Provider.all_objects.filter(pk=provider.id).update(is_deleted=True)
with (
patch(
"tasks.jobs.deletion.graph_database.get_database_name",
return_value="tenant-db",
),
patch(
"tasks.jobs.deletion.graph_database.drop_subgraph"
) as mock_drop_subgraph,
patch("tasks.jobs.deletion.graph_database.drop_database"),
):
delete_tenant(tenant.id)
mock_drop_subgraph.assert_any_call("tenant-db", str(provider.id))
def test_delete_tenant_handles_concurrently_deleted_provider(self, tenants_fixture):
tenant = tenants_fixture[0]
Provider.objects.create(
provider="aws",
uid="111111111111",
alias="vanishing_provider",
tenant_id=tenant.id,
)
def drop_subgraph_side_effect(_db_name, provider_id):
# Simulate concurrent deletion by another process
Provider.all_objects.filter(pk=provider_id).delete()
with (
patch(
"tasks.jobs.deletion.graph_database.get_database_name",
return_value="tenant-db",
),
patch(
"tasks.jobs.deletion.graph_database.drop_subgraph",
side_effect=drop_subgraph_side_effect,
),
patch("tasks.jobs.deletion.graph_database.drop_database"),
):
deletion_summary = delete_tenant(tenant.id)
assert deletion_summary is not None
File diff suppressed because it is too large Load Diff
+8
View File
@@ -144,6 +144,10 @@ services:
condition: service_healthy
neo4j:
condition: service_healthy
ulimits:
nofile:
soft: 65536
hard: 65536
entrypoint:
- "/home/prowler/docker-entrypoint.sh"
- "worker"
@@ -166,6 +170,10 @@ services:
condition: service_healthy
neo4j:
condition: service_healthy
ulimits:
nofile:
soft: 65536
hard: 65536
entrypoint:
- "../docker-entrypoint.sh"
- "beat"
+8
View File
@@ -117,6 +117,10 @@ services:
condition: service_healthy
postgres:
condition: service_healthy
ulimits:
nofile:
soft: 65536
hard: 65536
entrypoint:
- "/home/prowler/docker-entrypoint.sh"
- "worker"
@@ -131,6 +135,10 @@ services:
condition: service_healthy
postgres:
condition: service_healthy
ulimits:
nofile:
soft: 65536
hard: 65536
entrypoint:
- "../docker-entrypoint.sh"
- "beat"
+5 -11
View File
@@ -49,15 +49,13 @@ AWS_PROFILE=prowler-profile
- If you are scanning multiple AWS accounts, you may need to add multiple profiles to your AWS config. Note that this workaround is mainly for local testing; for production or multi-account setups, follow the [CloudFormation Template guide](https://github.com/prowler-cloud/prowler/issues/7745) and ensure the correct IAM roles and permissions are set up in each account.
### Scans complete but reports are missing or compliance data is empty (`Too many open files` error)
### Scans Complete but Reports Are Missing or Compliance Data Is Empty (`Too many open files` Error)
When running Prowler App via Docker Compose, you may encounter situations where scans complete successfully but reports are not available for download, compliance data shows as empty, or you see 404 errors when trying to access scan reports. Checking the `worker` container logs may reveal errors like `[Errno 24] Too many open files`.
When running Prowler App via Docker Compose, scans may complete successfully but reports are not available for download, compliance data shows as empty, or 404 errors appear when trying to access scan reports. Checking the `worker` container logs may reveal errors like `[Errno 24] Too many open files`.
This issue occurs because the default file descriptor limits in Docker containers are too low for Prowler's operations.
This issue occurs because the default file descriptor limits in Docker containers are too low for Prowler's operations. The default `docker-compose.yml` already includes `ulimits` configuration with `nofile` set to `65536` for the `worker` and `worker-beat` services to prevent this issue.
**Solution:**
Add `ulimits` configuration to the `worker` and `worker-beat` services in your `docker-compose.yaml`:
If a custom `docker-compose.yml` is being used or the default configuration has been modified, ensure the `ulimits` configuration is present in both the `worker` and `worker-beat` services:
```yaml
services:
@@ -76,17 +74,13 @@ services:
# ... rest of service configuration
```
After making these changes, restart your Docker Compose stack:
After making these changes, restart the Docker Compose stack:
```bash
docker compose down
docker compose up -d
```
<Note>
We are evaluating adding these values to the default `docker-compose.yml` to avoid this issue in future releases.
</Note>
### API Container Fails to Start with JWT Key Permission Error
See [GitHub Issue #8897](https://github.com/prowler-cloud/prowler/issues/8897) for more details.
@@ -1,231 +1,447 @@
---
title: 'GitHub Authentication in Prowler'
title: "GitHub Authentication in Prowler"
---
Prowler supports multiple methods to [authenticate with GitHub](https://docs.github.com/en/rest/authentication/authenticating-to-the-rest-api). These include:
Prowler for GitHub offers multiple authentication types across Prowler Cloud and Prowler CLI.
- [Personal Access Token (PAT)](/user-guide/providers/github/authentication#personal-access-token-pat)
- [OAuth App Token](/user-guide/providers/github/authentication#oauth-app-token)
- [GitHub App Credentials](/user-guide/providers/github/authentication#github-app-credentials)
## Common Setup
This flexibility enables scanning and analysis of GitHub accounts, including repositories, organizations, and applications, using the method that best suits the use case.
### Authentication Methods Overview
## Personal Access Token (PAT)
Prowler offers three authentication methods. Fine-Grained Personal Access Tokens are recommended for most use cases.
| Method | Best For | Key Benefit |
|--------|----------|-------------|
| [**Fine-Grained Personal Access Token**](#fine-grained-personal-access-token-recommended) | Individual users, quick setup | Simple, user-scoped access |
| [**GitHub App**](#github-app-credentials) | Organizations, automation, CI/CD | Organization-scoped, no personal account dependency |
| [**OAuth App Token**](#oauth-app-token) | Delegated user authorization | User-consented access flows |
<Note>
**Which should I choose?**
- **Personal scanning or quick setup**: Use Fine-Grained PAT
- **Organization-wide scanning or CI/CD pipelines**: Use GitHub App (recommended for production)
- **Building apps with user authorization**: Use OAuth App
</Note>
Personal Access Tokens provide the simplest GitHub authentication method, but it can only access resources owned by a single user or organization.
<Warning>
**Classic Tokens Deprecated**
**Classic Personal Access Tokens**
GitHub has deprecated Personal Access Tokens (classic) in favor of fine-grained Personal Access Tokens. We recommend using fine-grained tokens as they provide better security through more granular permissions and resource-specific access control.
GitHub has deprecated classic Personal Access Tokens. Use Fine-Grained Tokens instead - they provide granular permission control and better security.
</Warning>
#### **Option 1: Create a Fine-Grained Personal Access Token (Recommended)**
1. **Navigate to GitHub Settings**
- Open [GitHub](https://github.com) and sign in
- Click the profile picture in the top right corner
- Select "Settings" from the dropdown menu
### Required Permissions
2. **Access Developer Settings**
- Scroll down the left sidebar
- Click "Developer settings"
Required permissions depend on the scan scope: user repositories, organization repositories, or both.
3. **Generate Fine-Grained Token**
- Click "Personal access tokens"
- Select "Fine-grained tokens"
- Click "Generate new token"
#### Repository Permissions
4. **Configure Token Settings**
- **Token name**: Give your token a descriptive name (e.g., "Prowler Security Scanner")
- **Resource owner**: Select the account that owns the resources to scan — either a personal account or a specific organization
- **Expiration**: Set an appropriate expiration date (recommended: 90 days or less)
- **Repository access**: Choose "All repositories" or "Only select repositories" based on your needs
Required for scanning repository security settings:
<Note>
**Public repositories**
| Permission | Access Level | Purpose | Checks Enabled |
|------------|-------------|---------|----------------|
| **Administration** | Read | Branch protection, security settings | All branch protection checks, secret scanning status |
| **Contents** | Read | File existence checks | `repository_public_has_securitymd_file`, `repository_has_codeowners_file` |
| **Metadata** | Read | Basic repository information | All checks (automatically granted) |
| **Dependabot alerts** | Read | Dependency vulnerability scanning | `repository_dependency_scanning_enabled` |
Even if you select 'Only select repositories', the token will have access to the public repositories that you own or are a member of.
<Note>
**Pull requests permission is optional.** It's only needed if you want to audit PR-specific settings beyond what branch protection provides.
</Note>
</Note>
5. **Configure Token Permissions**
To enable Prowler functionality, configure the following permissions:
#### Organization Permissions
- **Repository permissions:**
- **Administration**: Read-only access
- **Contents**: Read-only access
- **Metadata**: Read-only access
- **Pull requests**: Read-only access
Required for scanning organization-level security settings:
- **Organization permissions** (available when an organization is selected as Resource Owner):
- **Administration**: Read-only access
- **Members**: Read-only access
<Note>
**For Fine-Grained PATs:** Organization permissions only appear when the **Resource Owner** is set to an organization (not your personal account).
- **Account permissions** (available when a personal account is selected as Resource Owner):
- **Email addresses**: Read-only access
**For GitHub Apps:** Organization permissions are configured during app creation and apply to all organizations where the app is installed.
</Note>
6. **Copy and Store the Token**
- Copy the generated token immediately (GitHub displays tokens only once)
- Store tokens securely using environment variables
| Permission | Access Level | Purpose | Checks Enabled |
|------------|-------------|---------|----------------|
| **Administration** | Read | Organization security policies | `organization_members_mfa_required`, `organization_repository_creation_limited`, `organization_default_repository_permission_strict` |
| **Members** | Read | Member access reviews | Organization membership auditing |
![GitHub Personal Access Token Permissions](/images/providers/github-pat-permissions.png)
#### Account Permissions (Fine-Grained PAT only)
#### **Option 2: Create a Classic Personal Access Token (Not Recommended)**
| Permission | Access Level | Purpose |
|------------|-------------|---------|
| **Email addresses** | Read | User email verification |
<Note>
GitHub Apps don't have account-level permissions - they operate at the organization/repository level.
</Note>
### Permissions and Check Coverage
With the **Read-only permissions** listed above, Prowler can run:
| Check Category | Coverage | Notes |
|----------------|----------|-------|
| Branch protection checks (12 checks) | ✅ Full | Signed commits, status checks, PR reviews, etc. |
| Repository security checks | ✅ Full | Secret scanning, Dependabot, SECURITY.md, CODEOWNERS |
| Organization checks (3 checks) | ✅ Full | MFA, repo creation policies, default permissions |
| Compliance frameworks | ✅ Full | CIS GitHub Benchmark and others |
| Merge settings (`delete_branch_on_merge`) | ⚠️ MANUAL | Requires write permission (see below) |
**Check that returns `MANUAL` status with Read-only permissions:**
- `repository_branch_delete_on_merge_enabled`
<Warning>
**Security Risk**
**About Write Permissions**
Classic tokens provide broad permissions that may exceed what Prowler actually needs. Use fine-grained tokens instead for better security.
The `delete_branch_on_merge` setting is only returned by the GitHub API when the token has **Administration: Read and write** permission.
**Granting Write permissions is not recommended under any circumstances:**
- Token can modify repository settings
- Token can change branch protection rules
- Violates the principle of least privilege
**Recommendation:** Accept `MANUAL` status for this single check rather than granting write access. This limitation applies equally to Fine-Grained PATs and GitHub Apps.
</Warning>
### Step-by-Step Permission Assignment
#### Fine-Grained Personal Access Token (Recommended for Individual Use)
**Benefits of Fine-Grained Tokens**
Fine-Grained Personal Access Tokens are ideal for:
- **Individual users** scanning their own repositories
- **Quick setup** without app registration overhead
- **Temporary access** with mandatory expiration
- **Repository-specific access** when you only need to scan certain repos
**Create a Fine-Grained Token:**
1. Navigate to **GitHub Settings** > **Developer settings**.
2. Click **Personal access tokens** > **Fine-grained tokens** > **Generate new token**.
3. Configure basic settings:
- **Token name**: Descriptive name (e.g., "Prowler Security Scanner")
- **Expiration**: 90 days or less (recommended)
- **Resource owner**:
- Personal account (for user repositories)
- Organization name (for organization scanning - requires admin approval)
- **Repository access**: "All repositories" (recommended)
4. Configure **Repository permissions**:
- Administration: Read
- Contents: Read
- Metadata: Read (auto-selected)
- Dependabot alerts: Read
5. Configure **Organization permissions** (only appears when Resource owner is an organization):
- Administration: Read
- Members: Read
6. Configure **Account permissions**:
- Email addresses: Read (optional)
7. Click **Generate token** and copy the token immediately.
<Warning>
GitHub shows the token only once. Store it securely.
</Warning>
1. **Navigate to GitHub Settings**
- Open [GitHub](https://github.com) and sign in
- Click the profile picture in the top right corner
- Select "Settings" from the dropdown menu
2. **Access Developer Settings**
- Scroll down the left sidebar
- Click "Developer settings"
![GitHub Fine-Grained Token Permissions](/images/providers/github-pat-permissions.png)
3. **Generate Classic Token**
- Click "Personal access tokens"
- Select "Tokens (classic)"
- Click "Generate new token"
#### OAuth App Token
4. **Configure Token Permissions**
To enable Prowler functionality, configure the following scopes:
- `repo`: Full control of private repositories (includes `repo:status` and `repo:contents`)
- `read:org`: Read organization and team membership
- `read:user`: Read user profile data
- `security_events`: Access security events (secret scanning and Dependabot alerts)
- `read:enterprise`: Read enterprise data (if using GitHub Enterprise)
**Recommended OAuth App Use Cases:**
5. **Copy and Store the Token**
- Copy the generated token immediately (GitHub displays tokens only once)
- Store tokens securely using environment variables
Use OAuth App Tokens when building applications that need delegated user permissions and explicit user authorization.
## OAuth App Token
**OAuth Scopes:**
OAuth Apps enable applications to act on behalf of users with explicit consent.
- `repo`: Full control of repositories
- `read:org`: Read organization and team membership
- `read:user`: Read user profile data
### Create an OAuth App Token
**Create an OAuth App:**
1. **Navigate to Developer Settings**
- Open GitHub Settings → Developer settings
- Click "OAuth Apps"
1. Navigate to **GitHub Settings** > **Developer settings** > **OAuth Apps**.
2. **Register New Application**
- Click "New OAuth App"
- Complete the required fields:
- **Application name**: Descriptive application name
- **Homepage URL**: Application homepage
- **Authorization callback URL**: User redirection URL after authorization
2. Click **New OAuth App** and complete:
- Application name
- Homepage URL
- Authorization callback URL
3. **Obtain Authorization Code**
- Request authorization code (replace `{app_id}` with the application ID):
3. Obtain authorization code:
```
https://github.com/login/oauth/authorize?client_id={app_id}
```
4. **Exchange Code for Token**
- Exchange authorization code for access token (replace `{app_id}`, `{secret}`, and `{code}`):
4. Exchange authorization code for access token:
```
https://github.com/login/oauth/access_token?code={code}&client_id={app_id}&client_secret={secret}
```
## GitHub App Credentials
GitHub Apps provide the recommended integration method for accessing multiple repositories or organizations.
#### GitHub App Credentials
### Create a GitHub App
<Note>
**When to Use GitHub Apps**
1. **Navigate to Developer Settings**
- Open GitHub Settings → Developer settings
- Click "GitHub Apps"
GitHub Apps are ideal for:
- **Organization-wide scanning** without tying access to a personal account
- **CI/CD pipelines** where you need machine identity (not user-based)
- **Multi-organization setups** with centralized app management
- **Audit compliance** where you need to track app-level access separately from users
2. **Create New GitHub App**
- Click "New GitHub App"
- Complete the required fields:
- **GitHub App name**: Choose a unique, descriptive name (e.g., "Prowler Security Scanner")
- **Homepage URL**: Enter your organization's website or the Prowler documentation URL (e.g., `https://prowler.com` or `https://docs.prowler.com`). This is just for reference and doesn't affect functionality.
- **Webhook URL**: Leave blank or uncheck "Active" under Webhook. Prowler doesn't require webhooks since it performs on-demand scans rather than responding to GitHub events.
- **Webhook secret**: Leave blank (not needed for Prowler)
- **Permissions**: Configure in the next step (see below)
GitHub Apps use the same permission model as Fine-Grained PATs - both provide full access to all Prowler checks.
</Note>
<Note>
**About Homepage URL and Webhooks**
**GitHub App Permissions:**
The Homepage URL is purely informational and can be any valid URL - it's just displayed to users who view the app. Use your company website, your GitHub organization URL, or even `https://docs.prowler.com`.
If a GitHub App is required:
Webhooks are **not required** for Prowler. Since Prowler performs on-demand security scans when you run it (rather than automatically responding to GitHub events), you can safely disable webhooks or leave the URL blank.
</Note>
**Repository permissions:**
3. **Configure Permissions**
To enable Prowler functionality, configure these permissions:
- **Repository permissions**:
- Contents (Read)
- Metadata (Read)
- Pull requests (Read)
- **Organization permissions**:
- Members (Read)
- Administration (Read)
- **Account permissions**:
- Email addresses (Read)
| Permission | Access Level | Purpose | Checks Enabled |
|------------|-------------|---------|----------------|
| **Administration** | Read | Branch protection, security settings | All branch protection checks, `repository_secret_scanning_enabled` |
| **Contents** | Read | File existence checks | `repository_public_has_securitymd_file`, `repository_has_codeowners_file` |
| **Metadata** | Read | Basic repository information | All checks (automatically granted) |
| **Dependabot alerts** | Read | Dependency vulnerability scanning | `repository_dependency_scanning_enabled` |
4. **Where can this GitHub App be installed?**
- Select "Any account" to be able to install the GitHub App in any organization.
**Organization permissions:**
5. **Generate Private Key**
- Scroll to the "Private keys" section after app creation
- Click "Generate a private key"
- Download the `.pem` file and store securely
| Permission | Access Level | Purpose | Checks Enabled |
|------------|-------------|---------|----------------|
| **Administration** | Read | Organization security policies | `organization_members_mfa_required`, `organization_repository_creation_limited`, `organization_default_repository_permission_strict` |
| **Members** | Read | Member access reviews | Organization membership auditing |
5. **Record App ID**
- Locate the App ID at the top of the GitHub App settings page
**Create a GitHub App:**
### Install the GitHub App
1. Navigate to **GitHub Settings** > **Developer settings** > **GitHub Apps**.
1. **Install Application**
- Navigate to GitHub App settings
- Click "Install App" in the left sidebar
- Select the target account/organization
- Choose specific repositories or select "All repositories"
2. Click **New GitHub App** and complete:
- **GitHub App name**: Descriptive name (e.g., "Prowler Security Scanner")
- **Homepage URL**: Your organization's URL or Prowler documentation
- **Webhook**: Uncheck "Active" (Prowler doesn't need webhooks)
## Best Practices
3. Configure **Repository permissions** (see table above):
- Administration: Read
- Contents: Read
- Metadata: Read (auto-selected)
- Dependabot alerts: Read
### Security Considerations
4. Configure **Organization permissions** (see table above):
- Administration: Read
- Members: Read
Implement the following security measures:
5. Under **Where can this GitHub App be installed?**, select:
- "Only on this account" for single-organization use
- "Any account" if you need to install across multiple organizations
- **Secure Credential Storage**: Store credentials using environment variables instead of hardcoding tokens
- **Secrets Management**: Use dedicated secrets management systems in production environments
- **Regular Token Rotation**: Rotate tokens and keys regularly
- **Least Privilege Principle**: Grant only minimum required permissions
- **Permission Auditing**: Review and audit permissions regularly
- **Token Expiration**: Set appropriate expiration times for tokens
- **Usage Monitoring**: Monitor token usage and revoke unused tokens
6. Click **Create GitHub App**.
### Authentication Method Selection
7. On the app settings page:
- Record the **App ID** (displayed at the top)
- Click **Generate a private key** and download the `.pem` file
Choose the appropriate method based on use case:
8. Install the GitHub App:
- Click **Install App** in the left sidebar
- Select target account/organization
- Choose "All repositories" or select specific repositories
- Click **Install**
- **Personal Access Token**: Individual use, testing, or simple automation
- **OAuth App Token**: Applications requiring user consent and delegation
- **GitHub App**: Production integrations, especially for organizations
<Warning>
**Private Key Security**
## Troubleshooting Common Issues
Store the `.pem` private key securely. Anyone with this key can authenticate as your GitHub App. Never commit it to version control.
</Warning>
### Insufficient Permissions
- Verify token/app has necessary scopes/permissions
- Check organization restrictions on third-party applications
---
### Token Expiration
- Confirm token has not expired
- Verify fine-grained tokens have correct resource access
## Prowler Cloud Authentication
For step-by-step setup instructions for Prowler Cloud, see the [Getting Started Guide](/user-guide/providers/github/getting-started-github#prowler-cloudapp).
### Using Personal Access Token
1. In Prowler Cloud, navigate to **Configuration** > **Cloud Providers** > **Add Cloud Provider** > **GitHub**.
2. Enter your GitHub Account ID (username or organization name).
3. Select **Personal Access Token** as the authentication method.
4. Enter your Fine-Grained Personal Access Token.
5. Click **Verify** to test the connection, then **Save**.
### Using OAuth App Token
1. Follow the same steps as Personal Access Token.
2. Select **OAuth App Token** as the authentication method.
3. Enter your OAuth App Token.
### Using GitHub App
1. Follow the same steps as Personal Access Token.
2. Select **GitHub App** as the authentication method.
3. Enter your GitHub App ID and upload the private key (`.pem` file).
For complete step-by-step instructions, see the [Getting Started Guide](/user-guide/providers/github/getting-started-github#prowler-cloudapp).
---
## Prowler CLI Authentication
### Authentication Methods
Prowler CLI automatically detects credentials using environment variables in this order:
1. `GITHUB_PERSONAL_ACCESS_TOKEN`
2. `GITHUB_OAUTH_APP_TOKEN`
3. `GITHUB_APP_ID` and `GITHUB_APP_KEY`
### Using Environment Variables (Recommended)
```bash
# Personal Access Token (Recommended)
export GITHUB_PERSONAL_ACCESS_TOKEN="ghp_xxxxxxxxxxxx"
prowler github
# OAuth App Token
export GITHUB_OAUTH_APP_TOKEN="oauth_token_here"
prowler github
# GitHub App
export GITHUB_APP_ID="123456"
export GITHUB_APP_KEY="$(cat /path/to/private-key.pem)"
prowler github
```
### Using CLI Flags
```bash
# Personal Access Token
prowler github --personal-access-token ghp_xxxxxxxxxxxx
# OAuth App Token
prowler github --oauth-app-token oauth_token_here
# GitHub App
prowler github --github-app-id 123456 --github-app-key-path /path/to/private-key.pem
```
### Scan Scope
<Warning>
**Understanding Scan Scope**
What Prowler scans depends on the invocation method:
| Command | What Gets Scanned | Organization Checks? |
|---------|------------------|---------------------|
| `prowler github` | All accessible repositories | No |
| `prowler github --repository owner/repo` | Single repository | No |
| `prowler github --organization org-name` | Organization repos + settings | Yes |
**Key Point:** Scanning user repositories does NOT include organization-level checks. To audit organization MFA, security policies, etc., you must use `--organization`.
</Warning>
**Scan user repositories:**
```bash
prowler github
prowler github --repository username/my-repo
```
**Scan organizations:**
```bash
prowler github --organization org-name
prowler github --organization org1 --organization org2
```
**Filter scans:**
```bash
prowler github --severity critical
prowler github --checks repository_default_branch_protection_enabled
prowler github --compliance cis_1.0_github
```
For complete step-by-step instructions, see the [Getting Started Guide](/user-guide/providers/github/getting-started-github#prowler-cli).
---
## Troubleshooting
### "Insufficient Permissions" Errors
**Symptom:** Checks fail or return `MANUAL` status.
**Solutions:**
1. Verify token has all required permissions
2. For organization scans, ensure organization approved the Fine-Grained Token
3. For merge settings checks, accept `MANUAL` status (Write permission not recommended)
### "No Organizations Found"
**Symptom:** Prowler doesn't find organizations even though you're a member.
**Cause:** Fine-Grained Token's Resource Owner is set to personal account.
**Solution:** Create a new token with Resource Owner set to the organization and get it approved by an admin.
### Organization Checks Return `MANUAL`
**Symptom:** Checks like `organization_members_mfa_required` return `MANUAL`.
**Cause:** Token lacks `Organization → Administration: Read` permission.
**Solutions:**
1. Edit token and grant `Organization → Administration: Read`
2. Ensure token's **Resource owner** is the organization (not personal account)
3. Get organization admin approval
### Token Not Showing Organization Permissions
**Symptom:** Can't find Organization permissions section when creating token.
**Cause:** **Resource owner** is set to personal account.
**Solution:** Change **Resource owner** dropdown to the organization name. Organization permissions section will appear.
### Rate Limiting
- GitHub implements API call rate limits
- Consider GitHub Apps for higher rate limits
### Organization Settings
- Some organizations restrict third-party applications
- Contact organization administrator if access is denied
**Symptom:** "API rate limit exceeded" errors.
**Solutions:**
- Scan during off-peak hours
- Use `--repository` to scan specific repos instead of all
- Implement delays between scans
### Token Expired or Revoked
**Symptom:** Authentication fails with valid-looking token.
**Solutions:**
1. Check token expiration date in GitHub settings
2. Verify token wasn't revoked
3. For Fine-Grained Tokens, check if organization approval was revoked
4. Generate a new token
---
## Additional Resources
- [GitHub REST API Authentication](https://docs.github.com/en/rest/authentication)
- [Fine-Grained Personal Access Tokens](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-personal-access-token)
- [GitHub Apps Documentation](https://docs.github.com/en/apps)
- [GitHub API Rate Limits](https://docs.github.com/en/rest/overview/rate-limits-for-the-rest-api)
- [Getting Started Guide](/user-guide/providers/github/getting-started-github)
@@ -2,96 +2,276 @@
title: 'Getting Started with GitHub'
---
## Prowler App
This guide covers setting up GitHub security scanning with Prowler. Choose a preferred interface below:
<Note>
**Understanding GitHub Scan Scope**
Prowler can scan either:
- **User Repositories**: All repositories owned by or accessible to a specific GitHub user
- **Organizations**: Repositories and organization-level settings
**Important**: Scanning user repositories does NOT include organization-level checks (MFA requirements, security policies, etc.). To scan organizations, you must explicitly configure them.
</Note>
<CardGroup cols={2}>
<Card title="Prowler Cloud/App" icon="cloud" href="#prowler-cloudapp">
Web-based interface with centralized management
</Card>
<Card title="Prowler CLI" icon="terminal" href="#prowler-cli">
Command-line interface for local or automated scans
</Card>
</CardGroup>
---
## Prowler Cloud/App
<iframe width="560" height="380" src="https://www.youtube-nocookie.com/embed/9ETI84Xpu2g" title="Prowler Cloud Onboarding Github" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen="1"></iframe>
> Walkthrough video onboarding a GitHub Account using GitHub App.
### Prerequisites
Before adding GitHub to Prowler Cloud/App, ensure you have:
1. **GitHub Account Access**
- Personal GitHub account, OR
- Admin access to a GitHub organization
2. **Authentication Credentials**
- Choose one method (see [Authentication Guide](/user-guide/providers/github/authentication)):
- **Fine-Grained Personal Access Token** (Recommended)
- OAuth App Token
- GitHub App Credentials (Not Recommended - limited data access)
### Step 1: Access Prowler Cloud/App
1. Navigate to [Prowler Cloud](https://cloud.prowler.com/) or launch [Prowler App](/user-guide/tutorials/prowler-app)
2. Go to "Configuration" > "Cloud Providers"
2. Go to **Configuration** → **Cloud Providers**
![Cloud Providers Page](/images/prowler-app/cloud-providers-page.png)
3. Click "Add Cloud Provider"
3. Click **Add Cloud Provider**
![Add a Cloud Provider](/images/prowler-app/add-cloud-provider.png)
4. Select "GitHub"
4. Select **GitHub**
![Select GitHub](/images/providers/select-github.png)
5. Add the GitHub Account ID (username or organization name) and an optional alias, then click "Next"
### Step 2: Configure GitHub Account
5. Add the **GitHub Account ID** and an optional alias:
- **Account ID**: Your GitHub username (e.g., `username`) or organization name (e.g., `org-name`)
- **Alias** (optional): Friendly name for this connection (e.g., `My Personal Repos` or `Prowler Org`)
![Add GitHub Account ID](/images/providers/add-github-account-id.png)
### Step 2: Choose the preferred authentication method
6. Click **Next**
6. Choose the preferred authentication method:
### Step 3: Choose Authentication Method
<Note>
**Recommended: Fine-Grained Personal Access Token**
**Fine-Grained Personal Access Tokens** are strongly recommended because they provide:
- Best data access for comprehensive security scanning
- Granular permission control
- Resource-specific access
**GitHub Apps are not recommended** — they provide the most limited access to GitHub data for security scanning purposes.
</Note>
7. Select your preferred authentication method:
![Select auth method](/images/providers/select-auth-method.png)
7. Configure the authentication method:
<Tabs>
<Tab title="Personal Access Token">
<Tab title="Personal Access Token (Recommended)">
![Configure Personal Access Token](/images/providers/auth-pat.png)
For more details on how to create a Personal Access Token, see [Authentication > Personal Access Token](/user-guide/providers/github/authentication#personal-access-token-pat).
**Recommended method** - provides the best data access for security scanning.
1. Enter your Fine-Grained Personal Access Token
2. Click **Verify** to test the connection
3. Click **Save**
**Don't have a token yet?** See [How to create a Personal Access Token](/user-guide/providers/github/authentication#create-a-fine-grained-personal-access-token)
</Tab>
<Tab title="OAuth App Token">
![Configure OAuth App Token](/images/providers/auth-oauth.png)
For more details on how to create an OAuth App Token, see [Authentication > OAuth App Token](/user-guide/providers/github/authentication#oauth-app-token).
For applications requiring user consent and delegated permissions.
1. Enter your OAuth App Token
2. Click **Verify** to test the connection
3. Click **Save**
**Don't have an OAuth token?** See [How to create an OAuth App Token](/user-guide/providers/github/authentication#oauth-app-token)
</Tab>
<Tab title="GitHub App">
<Tab title="GitHub App (Not Recommended)">
![Configure GitHub App](/images/providers/auth-github-app.png)
For more details on how to create a GitHub App, see [Authentication > GitHub App](/user-guide/providers/github/authentication#github-app-credentials).
<Warning>
**Not recommended** - most limited data access. Use only if required by organization policy.
</Warning>
1. Enter your GitHub App ID
2. Upload or paste your Private Key (`.pem` file)
3. Click **Verify** to test the connection
4. Click **Save**
**Don't have a GitHub App?** See [How to create a GitHub App](/user-guide/providers/github/authentication#github-app-credentials)
</Tab>
</Tabs>
8. Click **Start Scan** to begin your first security assessment
### Step 5: View Results
Once the scan completes, you can:
- View security findings in the dashboard
- Export results in multiple formats (JSON, CSV, HTML)
- Set up continuous scanning schedules
- Configure alerts for critical findings
---
## Prowler CLI
### Authentication
### Prerequisites
If no login method is explicitly provided, Prowler will automatically attempt to authenticate using environment variables in the following order of precedence:
Before running Prowler CLI for GitHub, ensure you have:
1. **Prowler Installed**
```bash
# Install via pip
pip install prowler
# Or via poetry
poetry install
```
2. **Authentication Credentials**
- Choose one method (see [Authentication Guide](/user-guide/providers/github/authentication)):
- **Fine-Grained Personal Access Token** (Recommended)
- OAuth App Token
- GitHub App Credentials (Not Recommended)
### Authentication Setup
Prowler CLI automatically detects authentication credentials using environment variables in this order:
1. `GITHUB_PERSONAL_ACCESS_TOKEN`
2. `GITHUB_OAUTH_APP_TOKEN`
3. `GITHUB_APP_ID` and `GITHUB_APP_KEY` (where the key is the content of the private key file)
3. `GITHUB_APP_ID` and `GITHUB_APP_KEY`
<Note>
Ensure the corresponding environment variables are set up before running Prowler for automatic detection when not specifying the login method.
<Tabs>
<Tab title="Environment Variables (Recommended)">
```bash
# Personal Access Token (Recommended)
export GITHUB_PERSONAL_ACCESS_TOKEN="ghp_xxxxxxxxxxxx"
</Note>
For more details on how to set up authentication with GitHub, see [Authentication > GitHub](/user-guide/providers/github/authentication).
# OAuth App Token
export GITHUB_OAUTH_APP_TOKEN="oauth_token_here"
#### Personal Access Token (PAT)
Use this method by providing a personal access token directly.
```console
prowler github --personal-access-token pat
# GitHub App
export GITHUB_APP_ID="123456"
export GITHUB_APP_KEY="$(cat /path/to/private-key.pem)"
```
#### OAuth App Token
Then run Prowler without additional flags:
```bash
prowler github
```
</Tab>
Authenticate using an OAuth app token.
<Tab title="CLI Flags">
```bash
# Personal Access Token
prowler github --personal-access-token ghp_xxxxxxxxxxxx
```console
prowler github --oauth-app-token oauth_token
# OAuth App Token
prowler github --oauth-app-token oauth_token_here
# GitHub App
prowler github --github-app-id 123456 --github-app-key-path /path/to/private-key.pem
```
</Tab>
</Tabs>
**Don't have credentials yet?** See the [Authentication Guide](/user-guide/providers/github/authentication) for step-by-step instructions.
### Scan Scope: Understanding What Gets Scanned
<Warning>
**Distinguishing User Scans from Organization Scans**
The scan scope depends entirely on the Prowler CLI invocation method:
| Command | What Gets Scanned | Organization Checks Included? |
|---------|------------------|-------------------------------|
| `prowler github` | All repositories the token has access to | No |
| `prowler github --repository owner/repo` | Single specified repository | No |
| `prowler github --organization org-name` | Organization repos + org settings | Yes |
| `prowler github --organization org-name --repository owner/repo` | Organization + single repository | Yes |
**Key Points:**
- Scanning **user repositories** does NOT run organization-level checks
- To audit organization MFA, security policies, etc., the `--organization` flag is required
- Members of multiple organizations should specify each one explicitly
</Warning>
### Scanning User Repositories
Scan repositories owned by your user account:
```bash
# Scan all repositories accessible to your token
prowler github
# Scan a specific repository
prowler github --repository username/my-repo
# Scan multiple specific repositories
prowler github --repository username/repo1 --repository username/repo2
```
#### GitHub App Credentials
**What gets scanned:**
- Repository security settings
- Branch protection rules
- Secret scanning configuration
- Dependabot settings
- Organization-level policies (not included)
Use GitHub App credentials by specifying the App ID and the private key path.
### Scanning Organizations
```console
prowler github --github-app-id app_id --github-app-key-path app_key_path
Scan organization repositories and organization-level security settings:
```bash
# Scan a single organization
prowler github --organization prowler-cloud
# Scan multiple organizations
prowler github --organization org1 --organization org2
# Scan organization and specific repositories within it
prowler github --organization my-org --repository my-org/critical-repo
```
**What gets scanned:**
- All organization repositories
- Repository security settings
- Organization MFA requirements
- Organization security policies
- Member access and permissions
### Scan Scoping
Scan scoping controls which repositories and organizations Prowler includes in a security assessment. By default, Prowler scans all repositories accessible to the authenticated user or organization. To limit the scan to specific repositories or organizations, use the following flags.
@@ -143,3 +323,120 @@ In this case, `my-repo` is qualified as `my-org/my-repo`, while `other-owner/oth
<Note>
The `--repository` and `--organization` flags can be combined with any authentication method.
</Note>
### Filtering Scans
Customize your scan scope with these options:
```bash
# Run only critical severity checks
prowler github --severity critical
# Run specific checks
prowler github --checks repository_default_branch_protection_enabled,organization_members_mfa_required
# Exclude specific checks
prowler github --excluded-checks repository_archived
# Scan with specific compliance framework
prowler github --compliance cis_1.0_github
# Output results in specific format
prowler github --output-formats json,csv,html
```
### Example Workflows
<Tabs>
<Tab title="Quick Security Assessment">
```bash
# Scan your personal repositories for critical issues
export GITHUB_PERSONAL_ACCESS_TOKEN="ghp_xxxx"
prowler github --severity critical high
```
</Tab>
<Tab title="Organization Compliance Audit">
```bash
# Full organization scan with CIS compliance
export GITHUB_PERSONAL_ACCESS_TOKEN="ghp_xxxx"
prowler github \
--organization prowler-cloud \
--compliance cis_1.0_github \
--output-formats json,html
```
</Tab>
<Tab title="CI/CD Integration">
```bash
# Scan specific repository in CI pipeline
prowler github \
--personal-access-token "$GITHUB_TOKEN" \
--repository "$GITHUB_REPOSITORY" \
--severity critical \
--output-formats json
# Exit with non-zero if critical findings
if grep -q '"Status": "FAIL".*"Severity": "critical"' prowler-output*.json; then
echo "Critical security issues found!"
exit 1
fi
```
</Tab>
<Tab title="Multi-Organization Scan">
```bash
# Scan multiple organizations you're part of
export GITHUB_PERSONAL_ACCESS_TOKEN="ghp_xxxx"
prowler github \
--organization org1 \
--organization org2 \
--organization org3 \
--output-formats csv
```
</Tab>
</Tabs>
### Viewing Prowler CLI Scan Results
Prowler CLI generates results in multiple formats:
```bash
# Results are saved in ./output/ directory by default
ls output/
# View HTML report in browser
open output/prowler-output-*.html
# Parse JSON results with jq
cat output/prowler-output-*.json | jq '.findings[] | select(.Status=="FAIL")'
# Import CSV into spreadsheet
open output/prowler-output-*.csv
```
---
## Next Steps
<CardGroup cols={2}>
<Card title="Authentication Guide" icon="key" href="/user-guide/providers/github/authentication">
Detailed permissions and token creation
</Card>
<Card title="Available Checks" icon="list-check" href="https://hub.prowler.com/github">
Browse all GitHub security checks
</Card>
<Card title="Compliance Frameworks" icon="shield-check" href="https://hub.prowler.com/compliance">
CIS, NIST, and other frameworks
</Card>
<Card title="Troubleshooting" icon="circle-question" href="/user-guide/providers/github/authentication#troubleshooting">
Common issues and solutions
</Card>
</CardGroup>
## Additional Resources
- [GitHub REST API Documentation](https://docs.github.com/en/rest)
- [Fine-Grained Personal Access Tokens](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-personal-access-token)
- [GitHub Security Best Practices](https://docs.github.com/en/code-security)
- [Prowler CLI Reference](/getting-started/basic-usage/prowler-cli)
Generated
+14 -2
View File
@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand.
# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand.
[[package]]
name = "about-time"
@@ -2139,6 +2139,18 @@ files = [
{file = "decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360"},
]
[[package]]
name = "defusedxml"
version = "0.7.1"
description = "XML bomb protection for Python stdlib modules"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
groups = ["main"]
files = [
{file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"},
{file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"},
]
[[package]]
name = "deprecated"
version = "1.2.18"
@@ -6881,4 +6893,4 @@ files = [
[metadata]
lock-version = "2.1"
python-versions = ">3.9.1,<3.13"
content-hash = "f1ac30f34fd838017ad24702043564e2c37afd1fdbf674cf5e5def79082463d5"
content-hash = "509440ff7a10d735686d330ac032f824fc92cf2dbacc66371e688ae1dd25dc2f"
+10
View File
@@ -20,10 +20,12 @@ All notable changes to the **Prowler SDK** are documented in this file.
- OpenStack compute 7 new checks [(#9944)](https://github.com/prowler-cloud/prowler/pull/9944)
- CSA CCM 4.0 for the Alibaba Cloud provider [(#10061)](https://github.com/prowler-cloud/prowler/pull/10061)
- ECS Exec (ECS-006) privilege escalation detection via `ecs:ExecuteCommand` + `ecs:DescribeTasks` [(#10066)](https://github.com/prowler-cloud/prowler/pull/10066)
- Add file descriptor limits (`ulimits`) to Docker Compose worker services to prevent `Too many open files` errors [(#10107)](https://github.com/prowler-cloud/prowler/pull/10107)
### 🔄 Changed
- Update Azure Monitor service metadata to new format [(#9622)](https://github.com/prowler-cloud/prowler/pull/9622)
- GitHub provider enhanced documentation and `repository_branch_delete_on_merge_enabled` logic [(#9830)](https://github.com/prowler-cloud/prowler/pull/9830)
- Parallelize Cloudflare zone API calls with threading to improve scan performance [(#9982)](https://github.com/prowler-cloud/prowler/pull/9982)
- Update GCP API Keys service metadata to new format [(#9637)](https://github.com/prowler-cloud/prowler/pull/9637)
- Update GCP BigQuery service metadata to new format [(#9638)](https://github.com/prowler-cloud/prowler/pull/9638)
@@ -37,6 +39,9 @@ All notable changes to the **Prowler SDK** are documented in this file.
- Update GCP IAM service metadata to new format [(#9646)](https://github.com/prowler-cloud/prowler/pull/9646)
- Update GCP KMS service metadata to new format [(#9647)](https://github.com/prowler-cloud/prowler/pull/9647)
- Update GCP Logging service metadata to new format [(#9648)](https://github.com/prowler-cloud/prowler/pull/9648)
- Update Azure Key Vault service metadata to new format [(#9621)](https://github.com/prowler-cloud/prowler/pull/9621)
- Update Azure Entra ID service metadata to new format [(#9619)](https://github.com/prowler-cloud/prowler/pull/9619)
- Update Azure Virtual Machines service metadata to new format [(#9629)](https://github.com/prowler-cloud/prowler/pull/9629)
### 🔐 Security
@@ -50,6 +55,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
- `pip install prowler` failing on systems without C compiler due to `netifaces` transitive dependency from `openstacksdk` [(#10055)](https://github.com/prowler-cloud/prowler/pull/10055)
- `kms_key_not_publicly_accessible` false negative for specific KMS actions (e.g., `kms:DescribeKey`, `kms:Decrypt`) with unrestricted principals [(#10071)](https://github.com/prowler-cloud/prowler/pull/10071)
- Remove account_id and location for manual requirements in M365CIS [(#10105)](https://github.com/prowler-cloud/prowler/pull/10105)
---
@@ -60,6 +66,10 @@ All notable changes to the **Prowler SDK** are documented in this file.
- `--repository` and `--organization` flags combined interaction in GitHub provider, qualifying unqualified repository names with organization [(#10001)](https://github.com/prowler-cloud/prowler/pull/10001)
- HPACK library logging tokens in debug mode for Azure, M365, and Cloudflare providers [(#10010)](https://github.com/prowler-cloud/prowler/pull/10010)
### 🐞 Fixed
- Use `defusedxml` in the Alibaba Cloud OSS service to prevent XXE vulnerabilities when parsing XML responses [(#9999)](https://github.com/prowler-cloud/prowler/pull/9999)
---
## [5.18.0] (Prowler v5.18.0)
@@ -77,8 +77,8 @@ class M365CIS(ComplianceOutput):
compliance_row = M365CISModel(
Provider=compliance.Provider.lower(),
Description=compliance.Description,
TenantId=finding.account_uid,
Location=finding.region,
TenantId="",
Location="",
AssessmentDate=str(timestamp),
Requirements_Id=requirement.Id,
Requirements_Description=requirement.Description,
@@ -6,9 +6,9 @@ from datetime import datetime
from email.utils import formatdate
from threading import Lock
from typing import Optional
from xml.etree import ElementTree
import requests
from defusedxml import ElementTree
from pydantic.v1 import BaseModel
from prowler.lib.logger import logger
@@ -1,30 +1,37 @@
{
"Provider": "azure",
"CheckID": "entra_conditional_access_policy_require_mfa_for_management_api",
"CheckTitle": "Ensure Multifactor Authentication is Required for Windows Azure Service Management API",
"CheckTitle": "Tenant requires MFA for all users to access Windows Azure Service Management API",
"CheckType": [],
"ServiceName": "entra",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "#microsoft.graph.conditionalAccess",
"Severity": "critical",
"ResourceType": "microsoft.aadiam/tenants",
"ResourceGroup": "IAM",
"Description": "This recommendation ensures that users accessing the Windows Azure Service Management API (i.e. Azure Powershell, Azure CLI, Azure Resource Manager API, etc.) are required to use multifactor authentication (MFA) credentials when accessing resources through the Windows Azure Service Management API.",
"Risk": "Administrative access to the Windows Azure Service Management API should be secured with a higher level of scrutiny to authenticating mechanisms. Enabling multifactor authentication is recommended to reduce the potential for abuse of Administrative actions, and to prevent intruders or compromised admin credentials from changing administrative settings.",
"RelatedUrl": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/howto-conditional-access-policy-azure-management",
"Description": "**Microsoft Entra Conditional Access** requires **MFA** for the **Windows Azure Service Management API** when an `enabled` policy targets `All users` and grants `Require multifactor authentication` to tokens for this management endpoint.",
"Risk": "Without MFA on Azure management endpoints, stolen or phished passwords can enable control-plane access.\n\nAttackers can change configs, create/delete resources, extract secrets, pivot laterally, and disrupt services-compromising confidentiality, integrity, and availability, with added cost exposure.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-sg/entra/identity/conditional-access/policy-old-require-mfa-azure-mgmt",
"https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-conditional-access-cloud-apps",
"https://support.icompaas.com/support/solutions/articles/62000233942-ensure-that-multi-factor-authentication-is-required-for-windows-azure-service-management-api-manual-"
],
"Remediation": {
"Code": {
"CLI": "",
"CLI": "New-MgIdentityConditionalAccessPolicy -DisplayName \"Require MFA for Azure management\" -State \"enabled\" -Conditions @{Users=@{IncludeUsers=@(\"All\")}; Applications=@{IncludeApplications=@(\"797f4846-ba00-4fd7-ba43-dac1f8f63013\")}} -GrantControls @{BuiltInControls=@(\"mfa\")}",
"NativeIaC": "",
"Other": "",
"Terraform": ""
"Other": "1. In the Microsoft Entra admin center, go to Protection > Conditional Access > Policies\n2. Click New policy\n3. Users: Include > All users\n4. Target resources: Resources > Include > Select resources > choose \"Windows Azure Service Management API\"\n5. Grant: Grant access > check Require multifactor authentication > Select\n6. Enable policy: On > Create",
"Terraform": "```hcl\nresource \"azuread_conditional_access_policy\" \"<example_resource_name>\" {\n display_name = \"Require MFA for Azure management\"\n state = \"enabled\" # Critical: policy must be enabled to pass\n\n conditions {\n client_app_types = [\"all\"]\n applications {\n included_applications = [\n \"797f4846-ba00-4fd7-ba43-dac1f8f63013\" # Critical: Windows Azure Service Management API (Azure management)\n ]\n }\n users {\n included_users = [\"All\"] # Critical: apply to all users\n }\n }\n\n grant_controls {\n operator = \"OR\"\n built_in_controls = [\"mfa\"] # Critical: require multifactor authentication\n }\n}\n```"
},
"Recommendation": {
"Text": "1. From the Azure Admin Portal dashboard, open Microsoft Entra ID. 2. Click Security in the Entra ID blade. 3. Click Conditional Access in the Security blade. 4. Click Policies in the Conditional Access blade. 5. Click + New policy. 6. Enter a name for the policy. 7. Click the blue text under Users. 8. Under Include, select All users. 9. Under Exclude, check Users and groups. 10. Select users or groups to be exempted from this policy (e.g. break-glass emergency accounts, and non-interactive service accounts) then click the Select button. 11. Click the blue text under Target Resources. 12. Under Include, click the Select apps radio button. 13. Click the blue text under Select. 14. Check the box next to Windows Azure Service Management APIs then click the Select button. 15. Click the blue text under Grant. 16. Under Grant access check the box for Require multifactor authentication then click the Select button. 17. Before creating, set Enable policy to Report-only. 18. Click Create. After testing the policy in report-only mode, update the Enable policy setting from Report-only to On.",
"Url": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-conditional-access-cloud-apps"
"Text": "Enforce **MFA** via Conditional Access for `Windows Azure Service Management API` scoped to `All users`, with only break-glass exclusions. Prefer **phishing-resistant** methods, apply **least privilege** and **separation of duties**, and monitor sign-ins. Also secure related admin apps and explicitly protect Azure DevOps as a distinct target.",
"Url": "https://hub.prowler.com/check/entra_conditional_access_policy_require_mfa_for_management_api"
}
},
"Categories": [],
"Categories": [
"identity-access"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "Conditional Access policies require Microsoft Entra ID P1 or P2 licenses. Similarly, they may require additional overhead to maintain if users lose access to their MFA. Any users or groups which are granted an exception to this policy should be carefully tracked, be granted only minimal necessary privileges, and conditional access exceptions should be regularly reviewed or investigated."
@@ -1,30 +1,36 @@
{
"Provider": "azure",
"CheckID": "entra_global_admin_in_less_than_five_users",
"CheckTitle": "Ensure fewer than 5 users have global administrator assignment",
"CheckTitle": "Global Administrator role has fewer than 5 members",
"CheckType": [],
"ServiceName": "entra",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "#microsoft.graph.directoryRole",
"ResourceType": "microsoft.aadiam/tenants",
"ResourceGroup": "IAM",
"Description": "This recommendation aims to maintain a balance between security and operational efficiency by ensuring that a minimum of 2 and a maximum of 4 users are assigned the Global Administrator role in Microsoft Entra ID. Having at least two Global Administrators ensures redundancy, while limiting the number to four reduces the risk of excessive privileged access.",
"Risk": "The Global Administrator role has extensive privileges across all services in Microsoft Entra ID. The Global Administrator role should never be used in regular daily activities, administrators should have a regular user account for daily activities, and a separate account for administrative responsibilities. Limiting the number of Global Administrators helps mitigate the risk of unauthorized access, reduces the potential impact of human error, and aligns with the principle of least privilege to reduce the attack surface of an Azure tenant. Conversely, having at least two Global Administrators ensures that administrative functions can be performed without interruption in case of unavailability of a single admin.",
"RelatedUrl": "https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/best-practices#5-limit-the-number-of-global-administrators-to-less-than-5",
"Description": "**Microsoft Entra Global Administrator** assignments are evaluated by counting current role members per tenant and identifying when the number of assignees is `5` or more.",
"Risk": "Having **5+ Global Administrators** expands the privileged attack surface. Compromised credentials or tokens can enable tenant-wide changes, disable security controls, exfiltrate data, and create persistence, impacting **confidentiality**, **integrity**, and **availability** across Entra, Microsoft 365, and Azure.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/best-practices#5-limit-the-number-of-global-administrators-to-less-than-5",
"https://learn.microsoft.com/en-us/microsoft-365/admin/add-users/about-admin-roles?view=o365-worldwide#security-guidelines-for-assigning-roles"
],
"Remediation": {
"Code": {
"CLI": "",
"CLI": "Remove-MgDirectoryRoleMember -DirectoryRoleId (Get-MgDirectoryRole -Filter \"displayName eq 'Global Administrator'\").Id -DirectoryObjectId '<example_user_id>'",
"NativeIaC": "",
"Other": "",
"Terraform": ""
"Other": "1. Sign in to the Microsoft Entra admin center\n2. Go to Identity > Roles & admins > Global Administrator\n3. Select View assignments (or Assignments)\n4. Remove members until the total Global Administrator assignments are fewer than 5\n5. Save changes",
"Terraform": "```hcl\n# Keep Global Administrator assignments below 5 by defining only required principals\ndata \"azuread_directory_role\" \"global_admin\" {\n display_name = \"Global Administrator\"\n}\n\n# Critical: This assignment grants GA to a specific principal; keep total GA assignments < 5\nresource \"azuread_directory_role_assignment\" \"ga_primary\" {\n role_id = data.azuread_directory_role.global_admin.id # Assigns the Global Administrator role\n principal_object_id = \"<example_resource_id>\" # Required account (e.g., break-glass)\n}\n\n# Critical: Add only necessary GA assignments; remove extras to ensure count < 5\nresource \"azuread_directory_role_assignment\" \"ga_secondary\" {\n role_id = data.azuread_directory_role.global_admin.id # Assigns the Global Administrator role\n principal_object_id = \"<example_resource_id>\" # Second required account\n}\n```"
},
"Recommendation": {
"Text": "1. From Azure Home select the Portal Menu 2. Select Microsoft Entra ID 3. Select Roles and Administrators 4. Select Global Administrator 5. Ensure less than 5 users are actively assigned the role. 6. Ensure that at least 2 users are actively assigned the role.",
"Url": "https://learn.microsoft.com/en-us/microsoft-365/admin/add-users/about-admin-roles?view=o365-worldwide#security-guidelines-for-assigning-roles"
"Text": "Limit the **Global Administrator** role to **fewer than 5** users.\n- Apply **least privilege**; use narrower roles where possible\n- Use **PIM** for just-in-time, no standing access\n- Enforce **MFA** and dedicated admin accounts\n- Run **access reviews** regularly and keep cloud-only `break-glass` accounts for emergencies",
"Url": "https://hub.prowler.com/check/entra_global_admin_in_less_than_five_users"
}
},
"Categories": [],
"Categories": [
"identity-access"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "Implementing this recommendation may require changes in administrative workflows or the redistribution of roles and responsibilities. Adequate training and awareness should be provided to all Global Administrators."
@@ -1,30 +1,37 @@
{
"Provider": "azure",
"CheckID": "entra_non_privileged_user_has_mfa",
"CheckTitle": "Ensure that 'Multi-Factor Auth Status' is 'Enabled' for all Non-Privileged Users",
"CheckTitle": "Non-privileged user has multi-factor authentication enabled",
"CheckType": [],
"ServiceName": "entra",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "#microsoft.graph.users",
"Severity": "medium",
"ResourceType": "microsoft.aadiam/tenants",
"ResourceGroup": "IAM",
"Description": "Enable multi-factor authentication for all non-privileged users.",
"Risk": "Multi-factor authentication requires an individual to present a minimum of two separate forms of authentication before access is granted. Multi-factor authentication provides additional assurance that the individual attempting to gain access is who they claim to be. With multi-factor authentication, an attacker would need to compromise at least two different authentication mechanisms, increasing the difficulty of compromise and thus reducing the risk.",
"RelatedUrl": "https://learn.microsoft.com/en-us/entra/identity/authentication/concept-mfa-howitworks",
"Description": "**Microsoft Entra** non-privileged users are assessed for **multifactor authentication** by verifying they have **two or more registered authentication methods** (*MFA enrollment*).",
"Risk": "Absent **MFA** on standard accounts enables password-only logins after phishing, reuse, or spraying, leading to **account takeover**. Attackers can access email, files, and apps, send internal phishing, and escalate, undermining **confidentiality** and **integrity**, and risking **availability** via malicious changes.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/entra/identity/authentication/tutorial-enable-azure-mfa",
"https://support.icompaas.com/support/solutions/articles/62000219680-ensure-that-multi-factor-auth-status-is-enabled-for-all-non-privileged-users",
"https://learn.microsoft.com/en-us/entra/identity/authentication/concept-mfa-howitworks"
],
"Remediation": {
"Code": {
"CLI": "",
"CLI": "az rest --method POST --url https://graph.microsoft.com/v1.0/users/<example_user_id>/authentication/temporaryAccessPassMethods --body '{\"lifetimeInMinutes\":60,\"isUsableOnce\":true}'",
"NativeIaC": "",
"Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/ActiveDirectory/multi-factor-authentication-for-all-non-privileged-users.html#",
"Other": "1. Sign in to the Microsoft Entra admin center\n2. Go to Entra ID > Users and select the non-privileged user\n3. Select Security > Authentication methods\n4. Click Add authentication method > Temporary Access Pass\n5. Click Create (accept defaults)\n6. Confirm the method appears under the user's authentication methods",
"Terraform": ""
},
"Recommendation": {
"Text": "Activate one of the available multi-factor authentication methods for users in Microsoft Entra ID.",
"Url": "https://learn.microsoft.com/en-us/entra/identity/authentication/tutorial-enable-azure-mfa"
"Text": "Enforce **MFA** for all users, including non-privileged. Prefer **phishing-resistant** methods (FIDO2/passkeys or Authenticator with number matching); avoid SMS/voice when possible. Use **Conditional Access** to require MFA by risk and context. Pair with **least privilege**, device trust, and sign-in monitoring.",
"Url": "https://hub.prowler.com/check/entra_non_privileged_user_has_mfa"
}
},
"Categories": [],
"Categories": [
"identity-access"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "Users would require two forms of authentication before any access is granted. Also, this requires an overhead for managing dual forms of authentication."
@@ -1,30 +1,36 @@
{
"Provider": "azure",
"CheckID": "entra_policy_default_users_cannot_create_security_groups",
"CheckTitle": "Ensure that 'Users can create security groups in Azure portals, API or PowerShell' is set to 'No'",
"CheckTitle": "Authorization policy disallows non-privileged users from creating security groups",
"CheckType": [],
"ServiceName": "entra",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "#microsoft.graph.authorizationPolicy",
"Severity": "medium",
"ResourceType": "microsoft.aadiam/tenants",
"ResourceGroup": "IAM",
"Description": "Restrict security group creation to administrators only.",
"Risk": "When creating security groups is enabled, all users in the directory are allowed to create new security groups and add members to those groups. Unless a business requires this day-to-day delegation, security group creation should be restricted to administrators only.",
"RelatedUrl": "https://learn.microsoft.com/en-us/entra/identity/users/groups-self-service-management",
"Description": "**Microsoft Entra authorization policy** setting for default user role permissions governing creation of **security groups** by non-privileged users.\n\nThe value of `allowed_to_create_security_groups` is examined to ensure group creation is limited to administrators across portals, API, and PowerShell.",
"Risk": "Allowing standard users to create security groups drives **entitlement sprawl** and can grant **unauthorized access** when those groups are tied to apps, sites, or roles. This weakens **least privilege**, complicates audits, and enables **lateral movement** or data exfiltration via misassigned group-based permissions.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/azure/ActiveDirectory/users-can-create-security-groups.html",
"https://learn.microsoft.com/en-us/entra/identity/users/groups-self-service-management"
],
"Remediation": {
"Code": {
"CLI": "",
"CLI": "az rest --method PATCH --url https://graph.microsoft.com/v1.0/policies/authorizationPolicy/authorizationPolicy --body '{\"defaultUserRolePermissions\":{\"allowedToCreateSecurityGroups\":false}}'",
"NativeIaC": "",
"Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/ActiveDirectory/users-can-create-security-groups.html",
"Terraform": ""
"Other": "1. Sign in to the Microsoft Entra admin center\n2. Go to Identity > Users > User settings\n3. Find \"Users can create security groups in Azure portals, API, or PowerShell\"\n4. Set it to \"No\"\n5. Click Save",
"Terraform": "```hcl\nresource \"azuread_authorization_policy\" \"<example_resource_name>\" {\n default_user_role_permissions {\n allowed_to_create_security_groups = false # Critical: disables security group creation for non-privileged users\n }\n}\n```"
},
"Recommendation": {
"Text": "1. From Azure Home select the Portal Menu 2. Select Microsoft Entra ID 3. Select Groups 4. Select General under Settings 5. Set Users can create security groups in Azure portals, API or PowerShell to No",
"Url": ""
"Text": "Restrict creation to **administrators** or a narrowly delegated role per **least privilege**. Set `allowed_to_create_security_groups` to `false` and use request/approval for new groups. Apply **governance**: naming standards, owner accountability, periodic **access reviews**, and monitor group lifecycle in audit logs.",
"Url": "https://hub.prowler.com/check/entra_policy_default_users_cannot_create_security_groups"
}
},
"Categories": [],
"Categories": [
"identity-access"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "Enabling this setting could create a number of requests that would need to be managed by an administrator."
@@ -1,30 +1,37 @@
{
"Provider": "azure",
"CheckID": "entra_policy_ensure_default_user_cannot_create_apps",
"CheckTitle": "Ensure That 'Users Can Register Applications' Is Set to 'No'",
"CheckTitle": "Tenant does not allow non-admin users to register applications",
"CheckType": [],
"ServiceName": "entra",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "#microsoft.graph.authorizationPolicy",
"ResourceType": "microsoft.aadiam/tenants",
"ResourceGroup": "IAM",
"Description": "Require administrators or appropriately delegated users to register third-party applications.",
"Risk": "It is recommended to only allow an administrator to register custom-developed applications. This ensures that the application undergoes a formal security review and approval process prior to exposing Azure Active Directory data. Certain users like developers or other high-request users may also be delegated permissions to prevent them from waiting on an administrative user. Your organization should review your policies and decide your needs.",
"RelatedUrl": "https://learn.microsoft.com/en-us/entra/identity-platform/how-applications-are-added#who-has-permission-to-add-applications-to-my-azure-ad-instance",
"Description": "**Microsoft Entra authorization policy** controls whether default users can create application registrations via `allowed_to_create_apps`. App creation is expected to be limited to administrators or explicitly delegated roles.",
"Risk": "Permitting default users to register apps enables **unvetted service principals**, **consent phishing**, and **over-privileged API access**, threatening data **confidentiality** and **integrity**. Adversaries can persist with app credentials, exfiltrate mail/files, and perform **lateral movement** using rogue permissions.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/azure/ActiveDirectory/users-can-register-applications.html",
"https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/delegate-app-roles#restrict-who-can-create-applications",
"https://learn.microsoft.com/en-us/entra/identity-platform/how-applications-are-added#who-has-permission-to-add-applications-to-my-azure-ad-instance"
],
"Remediation": {
"Code": {
"CLI": "",
"CLI": "az rest --method PATCH --url https://graph.microsoft.com/v1.0/policies/authorizationPolicy/authorizationPolicy --body '{\"defaultUserRolePermissions\":{\"allowedToCreateApps\":false}}'",
"NativeIaC": "",
"Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/ActiveDirectory/users-can-register-applications.html",
"Terraform": ""
"Other": "1. Sign in to the Microsoft Entra admin center\n2. Go to Microsoft Entra ID > Users > User settings\n3. Set \"Users can register applications\" to \"No\"\n4. Click Save",
"Terraform": "```hcl\nresource \"azuread_authorization_policy\" \"<example_resource_name>\" {\n default_user_role_permissions {\n allowed_to_create_apps = false # Critical: disables application registration for non-privileged users\n }\n}\n```"
},
"Recommendation": {
"Text": "1. From Azure Home select the Portal Menu 2. Select Azure Active Directory 3. Select Users 4. Select User settings 5. Ensure that Users can register applications is set to No",
"Url": "https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/delegate-app-roles#restrict-who-can-create-applications"
"Text": "Apply **least privilege**: restrict app registration to admins or delegated roles; set `Users can register applications` to `No`. Use the **Application Developer** role for exceptions, require **admin consent** workflows, routinely review app/service principal permissions, and audit changes for **defense in depth**.",
"Url": "https://hub.prowler.com/check/entra_policy_ensure_default_user_cannot_create_apps"
}
},
"Categories": [],
"Categories": [
"identity-access"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "Enforcing this setting will create additional requests for approval that will need to be addressed by an administrator. If permissions are delegated, a user may approve a malevolent third party application, potentially giving it access to your data."
@@ -1,30 +1,37 @@
{
"Provider": "azure",
"CheckID": "entra_policy_ensure_default_user_cannot_create_tenants",
"CheckTitle": "Ensure that 'Restrict non-admin users from creating tenants' is set to 'Yes'",
"CheckTitle": "Authorization policy restricts non-admin users from creating tenants",
"CheckType": [],
"ServiceName": "entra",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "#microsoft.graph.authorizationPolicy",
"Severity": "medium",
"ResourceType": "microsoft.aadiam/tenants",
"ResourceGroup": "IAM",
"Description": "Require administrators or appropriately delegated users to create new tenants.",
"Risk": "It is recommended to only allow an administrator to create new tenants. This prevent users from creating new Azure AD or Azure AD B2C tenants and ensures that only authorized users are able to do so.",
"RelatedUrl": "https://learn.microsoft.com/en-us/entra/fundamentals/users-default-permissions",
"Description": "**Microsoft Entra authorization policy** governs whether default users can create new tenants. This evaluates if tenant creation is disabled for non-admin users via `allowed_to_create_tenants=false`.",
"Risk": "Permitting default users to create tenants fuels **shadow IT** and identity sprawl. Creators become **Global Administrators** of unmanaged tenants, eroding **confidentiality** and **integrity** through unsanctioned apps and unmonitored data flows, and degrading **availability** of centralized governance.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/permissions-reference#tenant-creator",
"https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/azure/ActiveDirectory/disable-user-tenant-creation.html",
"https://learn.microsoft.com/en-us/entra/fundamentals/users-default-permissions"
],
"Remediation": {
"Code": {
"CLI": "",
"CLI": "Update-MgPolicyAuthorizationPolicy -AuthorizationPolicyId authorizationPolicy -BodyParameter @{ defaultUserRolePermissions = @{ allowedToCreateTenants = $false } }",
"NativeIaC": "",
"Other": "",
"Terraform": ""
"Other": "1. Go to Microsoft Entra admin center (https://entra.microsoft.com)\n2. Navigate: Microsoft Entra ID > Users > User settings\n3. Set \"Restrict non-admin users from creating tenants\" to Yes\n4. Click Save",
"Terraform": "```hcl\nresource \"azuread_authorization_policy\" \"<example_resource_name>\" {\n default_user_role_permissions {\n allowed_to_create_tenants = false # Critical: disables tenant creation for non-privileged users\n }\n}\n```"
},
"Recommendation": {
"Text": "1. From Azure Home select the Portal Menu 2. Select Azure Active Directory 3. Select Users 4. Select User settings 5. Set 'Restrict non-admin users from creating' tenants to 'Yes'",
"Url": "https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/permissions-reference#tenant-creator"
"Text": "Apply **least privilege**: set `allowed_to_create_tenants=false` so only vetted admins or the **Tenant Creator** role (managed with **PIM**) can create tenants. Enforce **separation of duties**, require approvals, and monitor audits. Review this setting regularly to prevent tenant sprawl and maintain **defense in depth**.",
"Url": "https://hub.prowler.com/check/entra_policy_ensure_default_user_cannot_create_tenants"
}
},
"Categories": [],
"Categories": [
"identity-access"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "Enforcing this setting will ensure that only authorized users are able to create new tenants."
@@ -1,30 +1,36 @@
{
"Provider": "azure",
"CheckID": "entra_policy_guest_invite_only_for_admin_roles",
"CheckTitle": "Ensure that 'Guest invite restrictions' is set to 'Only users assigned to specific admin roles can invite guest users'",
"CheckTitle": "Tenant authorization policy restricts guest invitations to users with specific admin roles or disables guest invitations",
"CheckType": [],
"ServiceName": "entra",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "#microsoft.graph.authorizationPolicy",
"Severity": "high",
"ResourceType": "microsoft.aadiam/tenants",
"ResourceGroup": "IAM",
"Description": "Restrict invitations to users with specific administrative roles only.",
"Risk": "Restricting invitations to users with specific administrator roles ensures that only authorized accounts have access to cloud resources. This helps to maintain 'Need to Know' permissions and prevents inadvertent access to data. By default the setting Guest invite restrictions is set to Anyone in the organization can invite guest users including guests and non-admins. This would allow anyone within the organization to invite guests and non-admins to the tenant, posing a security risk.",
"RelatedUrl": "https://learn.microsoft.com/en-us/entra/external-id/external-collaboration-settings-configure",
"Description": "**Microsoft Entra authorization policy** controls who can send **B2B guest invitations**.\n\nSecure posture is when invitations are restricted to specific admin roles (`adminsAndGuestInviters`) or completely disabled (`none`).",
"Risk": "**Open guest invitation** rights let members or guests add external users without oversight, expanding the attack surface.\n\nImpacts:\n- **Confidentiality**: data leakage via overshared resources\n- **Integrity**: privilege escalation through group/team access\n- **Availability**: difficult containment due to account sprawl",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/answers/questions/685101/how-to-allow-only-admins-to-add-guests",
"https://learn.microsoft.com/en-us/entra/external-id/external-collaboration-settings-configure"
],
"Remediation": {
"Code": {
"CLI": "",
"CLI": "az rest --method PATCH --url https://graph.microsoft.com/v1.0/policies/authorizationPolicy/authorizationPolicy --headers 'Content-Type=application/json' --body '{\"allowInvitesFrom\":\"adminsAndGuestInviters\"}'",
"NativeIaC": "",
"Other": "",
"Terraform": ""
"Other": "1. Sign in to the Microsoft Entra admin center\n2. Go to Entra ID > External Identities > External collaboration settings\n3. Under Guest invite settings, select \"Only users assigned to specific admin roles can invite guest users\" (or select \"No one in the organization can invite guest users\")\n4. Click Save",
"Terraform": "```hcl\nresource \"azuread_authorization_policy\" \"<example_resource_name>\" {\n allow_invites_from = \"adminsAndGuestInviters\" # Restricts guest invitations to specific admin roles, making the check PASS\n}\n```"
},
"Recommendation": {
"Text": "1. From Azure Home select the Portal Menu 2. Select Microsoft Entra ID 3. Then External Identities 4. Select External collaboration settings 5. Under Guest invite settings, for Guest invite restrictions, ensure that Only users assigned to specific admin roles can invite guest users is selected",
"Url": "https://learn.microsoft.com/en-us/answers/questions/685101/how-to-allow-only-admins-to-add-guests"
"Text": "Restrict invitations to `Only users assigned to specific admin roles can invite guest users`, or disable them where not needed. Apply **least privilege** (use dedicated Guest Inviter role), enforce approvals, allowlist trusted domains, and run periodic access reviews with audit monitoring to remove stale or risky guests.",
"Url": "https://hub.prowler.com/check/entra_policy_guest_invite_only_for_admin_roles"
}
},
"Categories": [],
"Categories": [
"identity-access"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "With the option of Only users assigned to specific admin roles can invite guest users selected, users with specific admin roles will be in charge of sending invitations to the external users, requiring additional overhead by them to manage user accounts. This will mean coordinating with other departments as they are onboarding new users."
@@ -1,30 +1,36 @@
{
"Provider": "azure",
"CheckID": "entra_policy_guest_users_access_restrictions",
"CheckTitle": "Ensure That 'Guest users access restrictions' is set to 'Guest user access is restricted to properties and memberships of their own directory objects'",
"CheckTitle": "Authorization policy restricts guest user access to properties and memberships of their own directory objects",
"CheckType": [],
"ServiceName": "entra",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "#microsoft.graph.authorizationPolicy",
"ResourceType": "microsoft.aadiam/tenants",
"ResourceGroup": "IAM",
"Description": "Limit guest user permissions.",
"Risk": "Limiting guest access ensures that guest accounts do not have permission for certain directory tasks, such as enumerating users, groups or other directory resources, and cannot be assigned to administrative roles in your directory. Guest access has three levels of restriction. 1. Guest users have the same access as members (most inclusive), 2. Guest users have limited access to properties and memberships of directory objects (default value), 3. Guest user access is restricted to properties and memberships of their own directory objects (most restrictive). The recommended option is the 3rd, most restrictive: 'Guest user access is restricted to their own directory object'.",
"RelatedUrl": "https://learn.microsoft.com/en-us/entra/identity/users/users-restrict-guest-permissions",
"Description": "**Microsoft Entra authorization policy** guest settings are assessed to determine whether guest user access is limited to the properties and memberships of their own directory objects (`Restricted access`) instead of broader visibility into users and groups",
"Risk": "Excess guest visibility enables **directory reconnaissance**, exposing user and group details for **phishing**, **password spraying**, and targeted attacks. This weakens **confidentiality** and can facilitate **privilege escalation** and lateral movement through informed abuse of group memberships and access paths.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/entra/identity/users/users-restrict-guest-permissions",
"https://learn.microsoft.com/en-us/entra/fundamentals/users-default-permissions#member-and-guest-users"
],
"Remediation": {
"Code": {
"CLI": "",
"CLI": "az rest --method patch --url https://graph.microsoft.com/v1.0/policies/authorizationPolicy/authorizationPolicy --body '{\"guestUserRoleId\":\"2af84b1e-32c8-42b7-82bc-daa82404023b\"}'",
"NativeIaC": "",
"Other": "",
"Terraform": ""
"Other": "1. Go to Microsoft Entra admin center > External Identities > External collaboration settings\n2. Select \"Guest user access is restricted to properties and memberships of their own directory objects\"\n3. Click Save\n4. Allow up to 15 minutes for the change to take effect",
"Terraform": "```hcl\nresource \"azuread_authorization_policy\" \"<example_resource_name>\" {\n # Critical: sets guests to 'Restricted access' so they can only access their own directory object\n guest_user_role_id = \"2af84b1e-32c8-42b7-82bc-daa82404023b\"\n}\n```"
},
"Recommendation": {
"Text": "1. From Azure Home select the Portal Menu 2. Select Microsoft Entra ID 3. Then External Identities 4. Select External collaboration settings 5. Under Guest user access, change Guest user access restrictions to be Guest user access is restricted to properties and memberships of their own directory objects",
"Url": "https://learn.microsoft.com/en-us/entra/fundamentals/users-default-permissions#member-and-guest-users"
"Text": "Apply **least privilege** to external users:\n- Set guest access to `Restricted access` so guests can only view their own directory objects\n- Avoid assigning admin roles to guests; use **PIM** for rare exceptions\n- Constrain external collaboration and group visibility, and run periodic **access reviews** to remove stale guest access",
"Url": "https://hub.prowler.com/check/entra_policy_guest_users_access_restrictions"
}
},
"Categories": [],
"Categories": [
"identity-access"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "This may create additional requests for permissions to access resources that administrators will need to approve. According to https://learn.microsoft.com/en-us/azure/active-directory/enterprise- users/users-restrict-guest-permissions#services-currently-not-supported Service without current support might have compatibility issues with the new guest restriction setting."
@@ -1,30 +1,38 @@
{
"Provider": "azure",
"CheckID": "entra_policy_restricts_user_consent_for_apps",
"CheckTitle": "Ensure 'User consent for applications' is set to 'Do not allow user consent'",
"CheckTitle": "Entra authorization policy disallows user consent for applications",
"CheckType": [],
"ServiceName": "entra",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "#microsoft.graph.authorizationPolicy",
"ResourceType": "microsoft.aadiam/tenants",
"ResourceGroup": "IAM",
"Description": "Require administrators to provide consent for applications before use.",
"Risk": "If Microsoft Entra ID is running as an identity provider for third-party applications, permissions and consent should be limited to administrators or pre-approved. Malicious applications may attempt to exfiltrate data or abuse privileged user accounts.",
"RelatedUrl": "https://learn.microsoft.com/en-gb/entra/identity/enterprise-apps/configure-user-consent?pivots=portal",
"Description": "Microsoft Entra authorization settings are evaluated to determine if the default user role permits **user consent to applications**. The check looks at permission grant policies to see whether end users can authorize apps to access organization data on their behalf, or if consent is restricted (e.g., `Do not allow user consent`).",
"Risk": "Permitting end-user consent enables **consent phishing** and over-privileged OAuth grants. Attackers can obtain tokens to read/send mail, access files, or act as the user, causing **data exfiltration**, persistence beyond password resets/MFA changes, and abuse of connected apps, impacting confidentiality and integrity.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-privileged-access#pa-1-separate-and-limit-highly-privilegedadministrative-users",
"https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/azure/ActiveDirectory/users-can-consent-to-apps-accessing-company-data-on-their-behalf.html#",
"https://learn.microsoft.com/en-gb/entra/identity/enterprise-apps/configure-user-consent?pivots=portal",
"https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/configure-user-consent"
],
"Remediation": {
"Code": {
"CLI": "",
"CLI": "Update-MgPolicyAuthorizationPolicy -BodyParameter @{ permissionGrantPolicyIdsAssignedToDefaultUserRole = @() }",
"NativeIaC": "",
"Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/ActiveDirectory/users-can-consent-to-apps-accessing-company-data-on-their-behalf.html#",
"Terraform": ""
"Other": "1. Sign in to the Microsoft Entra admin center (entra.microsoft.com) with a Global Administrator\n2. Go to Identity > Applications > Enterprise applications\n3. Select Consent and permissions > User consent settings\n4. Choose Do not allow user consent\n5. Click Save",
"Terraform": "```hcl\nresource \"azuread_authorization_policy\" \"<example_resource_name>\" {\n # Critical: remove all self-consent policies so users cannot consent to apps\n permission_grant_policy_ids_assigned_to_default_user_role = []\n}\n```"
},
"Recommendation": {
"Text": "1. From Azure Home select the Portal Menu 2. Select Microsoft Entra ID 3. Select Enterprise Applications 4. Select Consent and permissions 5. Select User consent settings 6. Set User consent for applications to Do not allow user consent 7. Click save",
"Url": "https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-privileged-access#pa-1-separate-and-limit-highly-privilegedadministrative-users"
"Text": "Enforce **least privilege** by setting user consent to `Do not allow user consent`. Use the **admin consent workflow** to review requests and pre-approve only vetted apps. *If needed*, allow consent only for verified publishers with low-impact scopes. Regularly review existing grants and monitor audit/sign-in logs.",
"Url": "https://hub.prowler.com/check/entra_policy_restricts_user_consent_for_apps"
}
},
"Categories": [],
"Categories": [
"identity-access"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "Enforcing this setting may create additional requests that administrators need to review."
@@ -1,30 +1,36 @@
{
"Provider": "azure",
"CheckID": "entra_policy_user_consent_for_verified_apps",
"CheckTitle": "Ensure 'User consent for applications' Is Set To 'Allow for Verified Publishers'",
"CheckTitle": "Entra tenant does not allow users to consent to non-verified applications",
"CheckType": [],
"ServiceName": "entra",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "#microsoft.graph.authorizationPolicy",
"ResourceType": "microsoft.aadiam/tenants",
"ResourceGroup": "IAM",
"Description": "Allow users to provide consent for selected permissions when a request is coming from a verified publisher.",
"Risk": "If Microsoft Entra ID is running as an identity provider for third-party applications, permissions and consent should be limited to administrators or pre-approved. Malicious applications may attempt to exfiltrate data or abuse privileged user accounts.",
"RelatedUrl": "https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/configure-user-consent?pivots=portal#configure-user-consent-to-applications",
"Description": "**Microsoft Entra** authorization policy for the default user role is assessed for assignment of the user-consent policy `microsoft-user-default-legacy`. Its presence means users can self-consent to app permissions; its absence indicates consent is restricted (e.g., only verified publishers or low-impact scopes).",
"Risk": "Broad self-consent enables **OAuth consent phishing** and rogue apps to gain tokens to tenant data (**confidentiality**), request write scopes to change resources (**integrity**), and persist via refresh tokens after password changes. Mis-scoped grants can drive lateral movement and privilege escalation.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-privileged-access#pa-1-separate-and-limit-highly-privilegedadministrative-users",
"https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/configure-user-consent?pivots=portal#configure-user-consent-to-applications"
],
"Remediation": {
"Code": {
"CLI": "",
"CLI": "Update-MgPolicyAuthorizationPolicy -BodyParameter @{permissionGrantPolicyIdsAssignedToDefaultUserRole=@('ManagePermissionGrantsForSelf.microsoft-user-default-low')}",
"NativeIaC": "",
"Other": "",
"Terraform": ""
"Other": "1. Sign in to Microsoft Entra admin center as Global Administrator or Privileged Role Administrator\n2. Go to Identity > Applications > Enterprise applications\n3. Select Consent and permissions > User consent settings\n4. Under User consent for applications, select \"Allow user consent for apps from verified publishers, for selected permissions\"\n5. Click Save",
"Terraform": "```hcl\nresource \"azuread_authorization_policy\" \"<example_resource_name>\" {\n # Critical: restricts user consent to verified publishers with low-impact permissions only\n permission_grant_policy_ids_assigned_to_default_user_role = [\"ManagePermissionGrantsForSelf.microsoft-user-default-low\"]\n}\n```"
},
"Recommendation": {
"Text": "1. From Azure Home select the Portal Menu 2. Select Microsoft Entra ID 3. Select Enterprise Applications 4. Select Consent and permissions 5. Select User consent settings 6. Under User consent for applications, select Allow user consent for apps from verified publishers, for selected permissions 7. Select Save",
"Url": "https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-privileged-access#pa-1-separate-and-limit-highly-privilegedadministrative-users"
"Text": "Enforce **least privilege** for app consent:\n- Remove `microsoft-user-default-legacy`\n- Allow consent only for verified publishers and low-impact permissions (e.g., `microsoft-user-default-low`)\n- Require admin approval for higher-risk scopes via the admin consent workflow\n- Periodically review and revoke unused consent grants",
"Url": "https://hub.prowler.com/check/entra_policy_user_consent_for_verified_apps"
}
},
"Categories": [],
"Categories": [
"identity-access"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "Enforcing this setting may create additional requests that administrators need to review."
@@ -1,30 +1,37 @@
{
"Provider": "azure",
"CheckID": "entra_privileged_user_has_mfa",
"CheckTitle": "Ensure that 'Multi-Factor Auth Status' is 'Enabled' for all Privileged Users",
"CheckTitle": "Privileged user has multi-factor authentication enabled",
"CheckType": [],
"ServiceName": "entra",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "#microsoft.graph.users",
"ResourceType": "microsoft.aadiam/tenants",
"ResourceGroup": "IAM",
"Description": "Enable multi-factor authentication for all roles, groups, and users that have write access or permissions to Azure resources. These include custom created objects or built-in roles such as, - Service Co-Administrators - Subscription Owners - Contributors",
"Risk": "Multi-factor authentication requires an individual to present a minimum of two separate forms of authentication before access is granted. Multi-factor authentication provides additional assurance that the individual attempting to gain access is who they claim to be. With multi-factor authentication, an attacker would need to compromise at least two different authentication mechanisms, increasing the difficulty of compromise and thus reducing the risk.",
"RelatedUrl": "https://learn.microsoft.com/en-us/entra/identity/authentication/concept-mfa-howitworks",
"Description": "**Microsoft Entra** privileged accounts are expected to use **multifactor authentication**. This evaluates users assigned to elevated directory roles and confirms they have **multiple authentication methods** registered for sign-in.",
"Risk": "Without **MFA**, privileged accounts face **phishing**, **password spraying**, and **credential reuse** risks. Compromise can grant tenant-wide admin control to alter roles, create backdoors, exfiltrate data, and weaken defenses, impacting **confidentiality**, **integrity**, and **availability**.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/entra/identity/authentication/tutorial-enable-azure-mfa",
"https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/azure/ActiveDirectory/multi-factor-authentication-for-all-privileged-users.html#",
"https://learn.microsoft.com/en-us/entra/identity/authentication/concept-mfa-howitworks"
],
"Remediation": {
"Code": {
"CLI": "",
"CLI": "az rest --method post --url https://graph.microsoft.com/v1.0/users/<example_resource_id>/authentication/phoneMethods --body '{\"phoneNumber\":\"+10000000000\",\"phoneType\":\"mobile\"}'",
"NativeIaC": "",
"Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/ActiveDirectory/multi-factor-authentication-for-all-privileged-users.html#",
"Terraform": ""
"Other": "1. Sign in to Microsoft Entra admin center\n2. Go to Identity > Protection > Conditional Access > + New policy\n3. Name the policy\n4. Under Users > Select users and groups > Directory roles, select the privileged roles to protect\n5. Under Target resources (or Cloud apps), select All cloud apps\n6. Under Grant, select Grant access and check Require multifactor authentication\n7. Set Enable policy to On and click Create\n8. Have each privileged user go to https://myaccount.microsoft.com/security-info and add at least one MFA method (e.g., Microsoft Authenticator or phone) to complete registration",
"Terraform": "```hcl\nresource \"azuread_conditional_access_policy\" \"<example_resource_name>\" {\n display_name = \"<example_resource_name>\"\n state = \"enabled\"\n\n conditions {\n users {\n included_roles = [\"<example_role_template_id>\"] # Critical: targets privileged role(s)\n }\n applications {\n included_applications = [\"All\"]\n }\n }\n\n grant_controls {\n operator = \"OR\"\n built_in_controls = [\"mfa\"] # Critical: requires MFA to access\n }\n}\n```"
},
"Recommendation": {
"Text": "Activate one of the available multi-factor authentication methods for users in Microsoft Entra ID.",
"Url": "https://learn.microsoft.com/en-us/entra/identity/authentication/tutorial-enable-azure-mfa"
"Text": "Enforce **MFA** for all privileged roles via **Conditional Access** or security defaults. Prefer **phishing-resistant** methods (FIDO2, passkeys, Authenticator push) over SMS/voice. Require registration before granting privileges, block legacy/basic auth, and apply **least privilege** with protected break-glass accounts.",
"Url": "https://hub.prowler.com/check/entra_privileged_user_has_mfa"
}
},
"Categories": [],
"Categories": [
"identity-access"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "Users would require two forms of authentication before any access is granted. Additional administrative time will be required for managing dual forms of authentication when enabling multi-factor authentication."
@@ -1,30 +1,36 @@
{
"Provider": "azure",
"CheckID": "entra_security_defaults_enabled",
"CheckTitle": "Ensure Security Defaults is enabled on Microsoft Entra ID",
"CheckTitle": "Microsoft Entra ID tenant has Security Defaults enabled",
"CheckType": [],
"ServiceName": "entra",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "#microsoft.graph.identitySecurityDefaultsEnforcementPolicy",
"Severity": "critical",
"ResourceType": "microsoft.aadiam/tenants",
"ResourceGroup": "security",
"Description": "Security defaults in Microsoft Entra ID make it easier to be secure and help protect your organization. Security defaults contain preconfigured security settings for common attacks. Security defaults is available to everyone. The goal is to ensure that all organizations have a basic level of security enabled at no extra cost. You may turn on security defaults in the Azure portal.",
"Risk": "Security defaults provide secure default settings that we manage on behalf of organizations to keep customers safe until they are ready to manage their own identity security settings. For example, doing the following: - Requiring all users and admins to register for MFA. - Challenging users with MFA - when necessary, based on factors such as location, device, role, and task. - Disabling authentication from legacy authentication clients, which cant do MFA.",
"RelatedUrl": "https://learn.microsoft.com/en-us/entra/fundamentals/security-defaults",
"Description": "Microsoft Entra **Security defaults** provide tenant-wide baseline identity protections:\n- MFA registration and challenges\n- Legacy auth (`IMAP/POP/SMTP`) blocked\n- Extra checks for privileged access\n\nThis evaluation identifies whether that baseline is enabled at the tenant level.",
"Risk": "Absent these defaults, users can sign in with **password-only** or via **legacy protocols** that bypass MFA, enabling **password spray**, replay, and phishing-based takeovers. Compromise risks data exposure (confidentiality), unauthorized changes (integrity), and service disruption (availability).",
"RelatedUrl": "",
"AdditionalURLs": [
"https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/azure/ActiveDirectory/security-defaults-enabled.html",
"https://learn.microsoft.com/en-us/entra/fundamentals/security-defaults"
],
"Remediation": {
"Code": {
"CLI": "",
"CLI": "az rest --method PATCH --url https://graph.microsoft.com/v1.0/policies/identitySecurityDefaultsEnforcementPolicy --body '{\"isEnabled\":true}'",
"NativeIaC": "",
"Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/ActiveDirectory/security-defaults-enabled.html#",
"Other": "1. Sign in to the Microsoft Entra admin center with a Conditional Access Administrator or Global Administrator account\n2. Go to Identity > Overview > Properties\n3. Click Manage security defaults\n4. Select Enabled and click Save",
"Terraform": ""
},
"Recommendation": {
"Text": "1. From Azure Home select the Portal Menu. 2. Browse to Microsoft Entra ID > Properties 3. Select Manage security defaults 4. Set the Enable security defaults to Enabled 5. Select Save",
"Url": "https://techcommunity.microsoft.com/t5/microsoft-entra-blog/introducing-security-defaults/ba-p/1061414"
"Text": "Activate **Security defaults** or implement equivalent **Conditional Access** as defense in depth:\n- Require MFA for all identities\n- Block legacy authentication\n- Safeguard admin portals and APIs\nApply **least privilege** and **zero trust**, and regularly review access patterns and break-glass exceptions to keep coverage complete.",
"Url": "https://hub.prowler.com/check/entra_security_defaults_enabled"
}
},
"Categories": [],
"Categories": [
"identity-access"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "This recommendation should be implemented initially and then may be overridden by other service/product specific CIS Benchmarks. Administrators should also be aware that certain configurations in Microsoft Entra ID may impact other Microsoft services such as Microsoft 365."
@@ -1,30 +1,37 @@
{
"Provider": "azure",
"CheckID": "entra_trusted_named_locations_exists",
"CheckTitle": "Ensure Trusted Locations Are Defined",
"CheckTitle": "Entra tenant has a trusted named location with IP ranges defined",
"CheckType": [],
"ServiceName": "entra",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "#microsoft.graph.ipNamedLocation",
"Severity": "low",
"ResourceType": "microsoft.aadiam/tenants",
"ResourceGroup": "network",
"Description": "Microsoft Entra ID Conditional Access allows an organization to configure Named locations and configure whether those locations are trusted or untrusted. These settings provide organizations the means to specify Geographical locations for use in conditional access policies, or define actual IP addresses and IP ranges and whether or not those IP addresses and/or ranges are trusted by the organization.",
"Risk": "Defining trusted source IP addresses or ranges helps organizations create and enforce Conditional Access policies around those trusted or untrusted IP addresses and ranges. Users authenticating from trusted IP addresses and/or ranges may have less access restrictions or access requirements when compared to users that try to authenticate to Microsoft Entra ID from untrusted locations or untrusted source IP addresses/ranges.",
"RelatedUrl": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/location-condition",
"Description": "**Microsoft Entra ID Conditional Access** supports **trusted named locations** defined by **public IP ranges**. Presence of at least one location marked `trusted` with IP CIDR ranges available for use in policy conditions.",
"Risk": "Without trusted IP-based locations, policies can't reliably distinguish corporate networks from unknown sources. This weakens **confidentiality and integrity**, enabling risky sign-ins to avoid stricter controls and forcing coarse rules that over-prompt users or leave **account takeover** and **data exfiltration** paths open.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-identity-management#im-7-restrict-resource-access-based-on--conditions",
"https://learn.microsoft.com/en-us/entra/identity/conditional-access/location-condition"
],
"Remediation": {
"Code": {
"CLI": "",
"CLI": "az rest --method post --url https://graph.microsoft.com/v1.0/identity/conditionalAccess/namedLocations --headers Content-Type=application/json --body '{\"@odata.type\":\"#microsoft.graph.ipNamedLocation\",\"displayName\":\"<example_resource_name>\",\"isTrusted\":true,\"ipRanges\":[{\"@odata.type\":\"#microsoft.graph.iPv4CidrRange\",\"cidrAddress\":\"203.0.113.0/24\"}]}'",
"NativeIaC": "",
"Other": "",
"Terraform": ""
"Other": "1. Sign in to the Microsoft Entra admin center (entra.microsoft.com)\n2. Go to Microsoft Entra ID > Protection > Conditional Access > Named locations\n3. Click New location\n4. Enter Name: <example_resource_name>\n5. Choose IP ranges location and add an IP range (e.g., 203.0.113.0/24)\n6. Check Mark as trusted location\n7. Click Create",
"Terraform": "```hcl\nresource \"azuread_named_location\" \"<example_resource_name>\" {\n display_name = \"<example_resource_name>\"\n\n ip {\n ip_ranges = [\"203.0.113.0/24\"]\n trusted = true # Critical: marks the location as trusted for Conditional Access policies\n }\n}\n```"
},
"Recommendation": {
"Text": "1. Navigate to the Microsoft Entra ID Conditional Access Blade 2. Click on the Named locations blade 3. Within the Named locations blade, click on IP ranges location 4. Enter a name for this location setting in the Name text box 5. Click on the + sign 6. Add an IP Address Range in CIDR notation inside the text box that appears 7. Click on the Add button 8. Repeat steps 5 through 7 for each IP Range that needs to be added 9. If the information entered are trusted ranges, select the Mark as trusted location check box 10. Once finished, click on Create",
"Url": "https://learn.microsoft.com/en-us/security/benchmark/azure/mcsb-identity-management#im-7-restrict-resource-access-based-on--conditions"
"Text": "Define **named locations** for your organization's egress IP ranges and mark them as `trusted`. Keep ranges accurate and narrow; review regularly. Use them in **Conditional Access** to enforce stronger controls off trusted networks. Apply **zero trust** and **least privilege**, and require MFA or device compliance when outside trusted locations.",
"Url": "https://hub.prowler.com/check/entra_trusted_named_locations_exists"
}
},
"Categories": [],
"Categories": [
"identity-access",
"trust-boundaries"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "When configuring Named locations, the organization can create locations using Geographical location data or by defining source IP addresses or ranges. Configuring Named locations using a Country location does not provide the organization the ability to mark those locations as trusted, and any Conditional Access policy relying on those Countries location setting will not be able to use the All trusted locations setting within the Conditional Access policy. They instead will have to rely on the Select locations setting. This may add additional resource requirements when configuring, and will require thorough organizational testing. In general, Conditional Access policies may completely prevent users from authenticating to Microsoft Entra ID, and thorough testing is recommended. To avoid complete lockout, a 'Break Glass' account with full Global Administrator rights is recommended in the event all other administrators are locked out of authenticating to Microsoft Entra ID. This 'Break Glass' account should be excluded from Conditional Access Policies and should be configured with the longest pass phrase feasible. This account should only be used in the event of an emergency and complete administrator lockout."
@@ -1,30 +1,37 @@
{
"Provider": "azure",
"CheckID": "entra_user_with_vm_access_has_mfa",
"CheckTitle": "Ensure only MFA enabled identities can access privileged Virtual Machine",
"CheckTitle": "Entra ID user with VM access has multi-factor authentication enabled",
"CheckType": [],
"ServiceName": "entra",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "#microsoft.graph.users",
"Severity": "high",
"ResourceType": "microsoft.aadiam/tenants",
"ResourceGroup": "IAM",
"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.",
"Description": "**Microsoft Entra** users with Azure roles that grant VM sign-in or management access-such as `Owner`, `Contributor`, `Virtual Machine * Login`, and `Virtual Machine Contributor`-are evaluated for **multi-factor authentication** enrollment. The finding highlights accounts with VM access that lack more than one authentication factor.",
"Risk": "Without **MFA**, accounts with VM access are vulnerable to phishing, password spraying, and credential stuffing. Compromise can enable remote VM login, abuse of the VM's managed identity, privilege escalation, and lateral movement-impacting confidentiality, integrity, and availability of workloads.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://www.rebeladmin.com/step-step-guide-enable-mfa-azure-admins-preview/",
"https://learn.microsoft.com/en-us/entra/identity/authentication/howto-mfa-userdevicesettings",
"https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/azure/VirtualMachines/vm-access-with-mfa-enabled-identities.html"
],
"Remediation": {
"Code": {
"CLI": "",
"CLI": "az rest --method post --url https://graph.microsoft.com/v1.0/users/<example_user_id>/authentication/phoneMethods --body '{\"phoneNumber\":\"+10000000000\",\"phoneType\":\"mobile\"}'",
"NativeIaC": "",
"Other": "",
"Other": "1. Sign in to Microsoft Entra admin center (entra.microsoft.com) with an admin account\n2. Go to Identity > Users > select the user with VM access\n3. Select Authentication methods > Add authentication method > choose Phone\n4. Enter the user's E.164 phone number (e.g., +15551234567) and click Add",
"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": ""
"Text": "Enforce **MFA** for all identities that can sign in to or manage VMs via **Conditional Access**, preferring strong, phishing-resistant methods. Apply **least privilege** by removing broad roles (`Owner`, `Contributor`) when not required. Use **PIM/JIT** for admin access and monitor sign-in risk for continuous assurance.",
"Url": "https://hub.prowler.com/check/entra_user_with_vm_access_has_mfa"
}
},
"Categories": [],
"Categories": [
"identity-access"
],
"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."
@@ -1,30 +1,37 @@
{
"Provider": "azure",
"CheckID": "entra_users_cannot_create_microsoft_365_groups",
"CheckTitle": "Ensure that 'Users can create Microsoft 365 groups in Azure portals, API or PowerShell' is set to 'No'",
"CheckTitle": "Microsoft 365 group creation by users is disabled",
"CheckType": [],
"ServiceName": "entra",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "Microsoft.Users/Settings",
"Severity": "medium",
"ResourceType": "microsoft.aadiam/tenants",
"ResourceGroup": "IAM",
"Description": "Restrict Microsoft 365 group creation to administrators only.",
"Risk": "Restricting Microsoft 365 group creation to administrators only ensures that creation of Microsoft 365 groups is controlled by the administrator. Appropriate groups should be created and managed by the administrator and group creation rights should not be delegated to any other user.",
"RelatedUrl": "https://learn.microsoft.com/en-us/microsoft-365/community/all-about-groups#microsoft-365-groups",
"Description": "**Microsoft Entra** directory setting **Group.Unified** governs who can create **Microsoft 365 Groups**. The evaluation inspects `EnableGroupCreation` and, when present, `GroupCreationAllowedGroupId` to determine if group creation is broadly allowed or restricted to a designated group.",
"Risk": "Unrestricted group creation drives sprawl of Teams, SharePoint sites, and mailboxes, undermining **confidentiality** via public spaces and guest invites. Compromised accounts can create groups to stage exfiltration or impersonation. It also heightens **integrity** risks from unsanctioned owners and **operational** burden for lifecycle and governance.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/azure/ActiveDirectory/users-can-create-office-365-groups.html#",
"https://learn.microsoft.com/en-us/microsoft-365/community/all-about-groups#microsoft-365-groups",
"https://learn.microsoft.com/en-us/microsoft-365/solutions/manage-creation-of-groups?view=o365-worldwide&redirectSourcePath=%252fen-us%252farticle%252fControl-who-can-create-Office-365-Groups-4c46c8cb-17d0-44b5-9776-005fced8e618"
],
"Remediation": {
"Code": {
"CLI": "",
"CLI": "Update-MgDirectorySetting -DirectorySettingId (Get-MgDirectorySetting | Where-Object {$_.DisplayName -eq 'Group.Unified'}).Id -BodyParameter @{Values = @(@{Name = 'EnableGroupCreation'; Value = 'false'})}",
"NativeIaC": "",
"Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/ActiveDirectory/users-can-create-office-365-groups.html#",
"Terraform": ""
"Other": "1. Sign in to the Microsoft Entra admin center\n2. Go to Groups > General\n3. Set \"Users can create Microsoft 365 groups in Azure portals, API, or PowerShell\" to \"No\"\n4. Click Save",
"Terraform": "```hcl\ndata \"azuread_directory_setting_template\" \"unified\" {\n display_name = \"Group.Unified\"\n}\n\nresource \"azuread_directory_setting\" \"example_resource_name\" {\n template_id = data.azuread_directory_setting_template.unified.id\n\n values = {\n EnableGroupCreation = \"false\"\n # Critical: sets EnableGroupCreation to false so users cannot create Microsoft 365 groups\n }\n}\n```"
},
"Recommendation": {
"Text": "1. From Azure Home select the Portal Menu 2. Select Microsoft Entra ID 3. Then Groups 4. Select General in settings 5. Set Users can create Microsoft 365 groups in Azure portals, API or PowerShell to No",
"Url": "https://learn.microsoft.com/en-us/microsoft-365/solutions/manage-creation-of-groups?view=o365-worldwide&redirectSourcePath=%252fen-us%252farticle%252fControl-who-can-create-Office-365-Groups-4c46c8cb-17d0-44b5-9776-005fced8e618"
"Text": "Apply **least privilege**: set `EnableGroupCreation=false` and allow only a controlled group via `GroupCreationAllowedGroupId`. Use **governed provisioning** with naming policies, sensitivity labels, and expiration/owner reviews. Monitor creation events and enforce **separation of duties** with approvals and lifecycle management.",
"Url": "https://hub.prowler.com/check/entra_users_cannot_create_microsoft_365_groups"
}
},
"Categories": [],
"Categories": [
"identity-access"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "Enabling this setting could create a number of requests that would need to be managed by an administrator."
@@ -1,30 +1,38 @@
{
"Provider": "azure",
"CheckID": "keyvault_access_only_through_private_endpoints",
"CheckTitle": "Ensure that public network access when using private endpoint is disabled.",
"CheckTitle": "Key Vault using private endpoints has public network access disabled",
"CheckType": [],
"ServiceName": "keyvault",
"SubServiceName": "",
"ResourceIdTemplate": "/subscriptions/{subscription_id}/resourceGroups/{resource_group}/providers/Microsoft.KeyVault/vaults/{vault_name}",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "KeyVault",
"ResourceType": "microsoft.keyvault/vaults",
"ResourceGroup": "security",
"Description": "Checks if Key Vaults with private endpoints have public network access disabled.",
"Risk": "Allowing public network access to Key Vault when using private endpoint can expose sensitive data to unauthorized access.",
"RelatedUrl": "https://learn.microsoft.com/en-us/azure/key-vault/general/network-security",
"Description": "**Azure Key Vaults** configured with **private endpoints** have **public network access** set to `Disabled`, so connectivity occurs only over the private link.",
"Risk": "Internet exposure alongside a **private endpoint** breaks isolation and expands attack surface:\n- Brute-force or token replay on the data plane\n- Abuse of misconfigured allowlists or trusted bypass\n- DDoS on the public endpoint\nThis can enable secret exfiltration or unauthorized key use, impacting **confidentiality** and **integrity**.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://support.icompaas.com/support/solutions/articles/62000234050-ensure-that-public-network-access-when-using-private-endpoint-is-disabled-automated-",
"https://learn.microsoft.com/en-us/azure/private-link/private-endpoint-overview",
"https://learn.microsoft.com/en-us/azure/key-vault/general/network-security"
],
"Remediation": {
"Code": {
"CLI": "az keyvault update --resource-group <resource_group> --name <vault_name> --public-network-access disabled",
"NativeIaC": "{\n \"type\": \"Microsoft.KeyVault/vaults\",\n \"apiVersion\": \"2022-07-01\",\n \"properties\": {\n \"publicNetworkAccess\": \"disabled\"\n }\n}",
"Terraform": "resource \"azurerm_key_vault\" \"example\" {\n # ... other configuration ...\n\n public_network_access_enabled = false\n}",
"Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/KeyVault/use-private-endpoints.html"
"CLI": "az keyvault update --resource-group <resource_group> --name <vault_name> --public-network-access Disabled",
"NativeIaC": "```bicep\n// Disable public network access for the Key Vault\nresource kv 'Microsoft.KeyVault/vaults@2023-07-01' = {\n name: '<example_resource_name>'\n location: '<location>'\n properties: {\n tenantId: '<tenant_id>'\n sku: {\n name: 'standard'\n family: 'A'\n }\n publicNetworkAccess: 'Disabled' // Critical: disables public access so only private endpoints are used\n }\n}\n```",
"Other": "1. In the Azure portal, go to Key vaults and select your vault\n2. Open Networking\n3. Under Public access, set Public network access to Disabled\n4. Click Save",
"Terraform": "```hcl\nresource \"azurerm_key_vault\" \"kv\" {\n name = \"<example_resource_name>\"\n location = \"<location>\"\n resource_group_name = \"<example_resource_name>\"\n tenant_id = \"<tenant_id>\"\n sku_name = \"standard\"\n\n public_network_access_enabled = false # Critical: disables public access when using private endpoints\n}\n```"
},
"Recommendation": {
"Text": "Disable public network access for Key Vaults that use private endpoint to ensure network traffic only flows through the private endpoint.",
"Url": "https://learn.microsoft.com/en-us/azure/private-link/private-endpoint-overview"
"Text": "Restrict access to **private endpoints** only:\n- Set `publicNetworkAccess` to `Disabled`\n- Avoid broad allowlists; limit `Trusted services`\n- Use private DNS with controlled egress\n- Enforce **least privilege** and monitor access logs\nThis sustains **defense in depth** and prevents Internet exposure.",
"Url": "https://hub.prowler.com/check/keyvault_access_only_through_private_endpoints"
}
},
"Categories": [],
"Categories": [
"internet-exposed",
"trust-boundaries"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
@@ -1,30 +1,37 @@
{
"Provider": "azure",
"CheckID": "keyvault_key_expiration_set_in_non_rbac",
"CheckTitle": "Ensure that the Expiration Date is set for all Keys in Non-RBAC Key Vaults.",
"CheckTitle": "Key Vault without RBAC authorization has expiration date set for all enabled keys",
"CheckType": [],
"ServiceName": "keyvault",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "KeyVault",
"ResourceType": "microsoft.keyvault/vaults",
"ResourceGroup": "security",
"Description": "Ensure that all Keys in Non Role Based Access Control (RBAC) Azure Key Vaults have an expiration date set.",
"Risk": "Azure Key Vault enables users to store and use cryptographic keys within the Microsoft Azure environment. The exp (expiration date) attribute identifies the expiration date on or after which the key MUST NOT be used for a cryptographic operation. By default, keys never expire. It is thus recommended that keys be rotated in the key vault and set an explicit expiration date for all keys. This ensures that the keys cannot be used beyond their assigned lifetimes.",
"RelatedUrl": "https://docs.microsoft.com/en-us/azure/key-vault/key-vault-whatis",
"Description": "**Azure Key Vaults** using access **policies (non-RBAC)** are assessed to confirm all **enabled keys** have an `expiration` (`exp`) defined. The finding highlights keys in these vaults that lack a set lifetime.",
"Risk": "Non-expiring keys enable indefinite use, degrading **confidentiality** and **integrity**. Stale or compromised keys can decrypt data, forge signatures, and maintain persistence. Absent lifetimes weaken rotation discipline and impede timely revocation, increasing exposure to cryptographic and operational drift.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/azure/key-vault/general/basic-concepts",
"https://learn.microsoft.com/en-us/azure/key-vault/general/about-keys-secrets-certificates#key-vault-keys",
"https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/azure/KeyVault/key-expiration-check.html#"
],
"Remediation": {
"Code": {
"CLI": "az keyvault key set-attributes --name <keyName> --vault-name <vaultName> --expires Y-m-d'T'H:M:S'Z'",
"NativeIaC": "",
"Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/KeyVault/key-expiration-check.html#",
"Terraform": "https://docs.prowler.com/checks/azure/azure-general-policies/set-an-expiration-date-on-all-keys#terraform"
"CLI": "az keyvault key set-attributes --vault-name <example_resource_name> --name <example_resource_name> --expires 2030-01-01T00:00:00Z",
"NativeIaC": "```bicep\n// Set expiration on a Key Vault key\nresource kv 'Microsoft.KeyVault/vaults@2023-07-01' existing = {\n name: '<example_resource_name>'\n}\n\nresource key 'Microsoft.KeyVault/vaults/keys@2023-07-01' = {\n name: '<example_resource_name>'\n parent: kv\n properties: {\n kty: 'RSA'\n attributes: {\n exp: 1893456000 // CRITICAL: sets the key expiration (Unix epoch seconds) to pass the check\n }\n }\n}\n```",
"Other": "1. In the Azure portal, go to Key vaults and open the vault\n2. Select Keys and choose the enabled key that failed\n3. Open the current version and click Edit (or Update)\n4. Set Expiration date (UTC) to a future date\n5. Click Save",
"Terraform": "```hcl\nresource \"azurerm_key_vault_key\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n key_vault_id = \"<example_resource_id>\"\n key_type = \"RSA\"\n\n expires_on = \"2030-01-01T00:00:00Z\" # CRITICAL: sets key expiration to pass the check\n}\n```"
},
"Recommendation": {
"Text": "From Azure Portal: 1. Go to Key vaults. 2. For each Key vault, click on Keys. 3. In the main pane, ensure that an appropriate Expiration date is set for any keys that are Enabled. From Azure CLI: Update the Expiration date for the key using the below command: az keyvault key set-attributes --name <keyName> --vault-name <vaultName> -- expires Y-m-d'T'H:M:S'Z' Note: To view the expiration date on all keys in a Key Vault using Microsoft API, the 'List' Key permission is required. To update the expiration date for the keys: 1. Go to the Key vault, click on Access Control (IAM). 2. Click on Add role assignment and assign the role of Key Vault Crypto Officer to the appropriate user. From PowerShell: Set-AzKeyVaultKeyAttribute -VaultName <VaultName> -Name <KeyName> -Expires <DateTime>",
"Url": "https://docs.microsoft.com/en-us/rest/api/keyvault/about-keys--secrets-and-certificates#key-vault-keys"
"Text": "Set an `expiration` on all keys and enforce **automated rotation** with advance alerts. Retire or disable old versions promptly and rotate after any suspected exposure. Apply **least privilege** and **separation of duties** for key administration. Prefer standardized lifecycle policies (e.g., RBAC-based governance) to enforce consistent control.",
"Url": "https://hub.prowler.com/check/keyvault_key_expiration_set_in_non_rbac"
}
},
"Categories": [],
"Categories": [
"encryption"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "Keys cannot be used beyond their assigned expiration dates respectively. Keys need to be rotated periodically wherever they are used."
@@ -1,30 +1,37 @@
{
"Provider": "azure",
"CheckID": "keyvault_key_rotation_enabled",
"CheckTitle": "Ensure Automatic Key Rotation is Enabled Within Azure Key Vault for the Supported Services",
"CheckTitle": "Key Vault key has automatic rotation enabled",
"CheckType": [],
"ServiceName": "keyvault",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "KeyVault",
"ResourceType": "microsoft.keyvault/vaults",
"ResourceGroup": "security",
"Description": "Automatic Key Rotation is available in Public Preview. The currently supported applications are Key Vault, Managed Disks, and Storage accounts accessing keys within Key Vault. The number of supported applications will incrementally increased.",
"Risk": "Once set up, Automatic Private Key Rotation removes the need for manual administration when keys expire at intervals determined by your organization's policy. The recommended key lifetime is 2 years. Your organization should determine its own key expiration policy.",
"RelatedUrl": "https://docs.microsoft.com/en-us/azure/key-vault/keys/how-to-configure-key-rotation",
"Description": "**Azure Key Vault** keys configured with a **rotation policy** that includes a `Rotate` lifetime action.\n\nThe evaluation looks for lifetime actions that schedule automatic key version creation; keys without this policy are not configured for auto-rotation.",
"Risk": "Without **auto-rotation**, keys may outlive policy, increasing exposure if material is leaked and weakening **confidentiality**.\n\nExpired keys without planned rollover can break decrypt/unwrap operations, impacting **availability**. Long-lived keys hinder incident response and enable prolonged misuse of stale versions.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/azure/storage/common/customer-managed-keys-overview#update-the-key-version",
"https://learn.microsoft.com/en-us/azure/key-vault/keys/how-to-configure-key-rotation",
"https://www.techtarget.com/searchcloudcomputing/tutorial/How-to-perform-and-automate-key-rotation-in-Azure-Key-Vault"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "",
"Terraform": ""
"CLI": "az keyvault key rotation-policy update --vault-name <VAULT_NAME> --name <KEY_NAME> --value '{\"lifetimeActions\":[{\"trigger\":{\"timeAfterCreate\":\"P18M\"},\"action\":{\"type\":\"Rotate\"}}]}'",
"NativeIaC": "```bicep\nresource key 'Microsoft.KeyVault/vaults/keys@2023-02-01' = {\n name: '<example_resource_name>/<example_resource_name>'\n location: resourceGroup().location\n properties: {\n kty: 'RSA'\n rotationPolicy: {\n lifetimeActions: [\n {\n trigger: { timeAfterCreate: 'P18M' }\n action: { type: 'Rotate' } // Critical: enables automatic rotation, satisfying the check\n }\n ]\n }\n }\n}\n```",
"Other": "1. In Azure Portal, go to Key Vaults > <your key vault> > Keys\n2. Select the key <KEY_NAME>\n3. Click Rotation policy\n4. Enable auto-rotation and set a rotation interval (e.g., After creation: P18M)\n5. Click Save",
"Terraform": "```hcl\nresource \"azurerm_key_vault_key\" \"key\" {\n name = \"<example_resource_name>\"\n key_vault_id = \"<example_resource_id>\"\n key_type = \"RSA\"\n\n rotation_policy {\n automatic {\n time_after_creation = \"P18M\" # Critical: creates a Rotate lifetime action to enable auto-rotation\n }\n }\n}\n```"
},
"Recommendation": {
"Text": "Note: Azure CLI and Powershell use ISO8601 flags to input timespans. Every timespan input will be in the format P<timespanInISO8601Format>(Y,M,D). The leading P is required with it denoting period. The (Y,M,D) are for the duration of Year, Month,and Day respectively. A time frame of 2 years, 2 months, 2 days would be (P2Y2M2D). From Azure Portal 1. From Azure Portal select the Portal Menu in the top left. 2. Select Key Vaults. 3. Select a Key Vault to audit. 4. Under Objects select Keys. 5. Select a key to audit. 6. In the top row select Rotation policy. 7. Select an Expiry time. 8. Set Enable auto rotation to Enabled. 9. Set an appropriate Rotation option and Rotation time. 10. Optionally set the Notification time. 11. Select Save. 12. Repeat steps 3-11 for each Key Vault and Key. From PowerShell Run the following command for each key to update its policy: Set-AzKeyVaultKeyRotationPolicy -VaultName test-kv -Name test-key -PolicyPath rotation_policy.json",
"Url": "https://docs.microsoft.com/en-us/azure/storage/common/customer-managed-keys-overview#update-the-key-version"
"Text": "Define a per-key **rotation policy** to automatically `Rotate` on a fixed cadence (e.g., `P2Y`) and set an **expiry** to enforce lifecycle.\n\nUse versionless key URIs in dependent services, apply **least privilege** to rotation roles, enable near-expiry notifications, and monitor events for **defense in depth**.",
"Url": "https://hub.prowler.com/check/keyvault_key_rotation_enabled"
}
},
"Categories": [],
"Categories": [
"encryption"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "There are an additional costs per operation in running the needed applications."
@@ -1,30 +1,38 @@
{
"Provider": "azure",
"CheckID": "keyvault_logging_enabled",
"CheckTitle": "Ensure that logging for Azure Key Vault is 'Enabled'",
"CheckTitle": "Key Vault has a diagnostic setting capturing audit logs",
"CheckType": [],
"ServiceName": "keyvault",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "KeyVault",
"Severity": "high",
"ResourceType": "microsoft.keyvault/vaults",
"ResourceGroup": "security",
"Description": "Enable AuditEvent logging for key vault instances to ensure interactions with key vaults are logged and available.",
"Risk": "Monitoring how and when key vaults are accessed, and by whom, enables an audit trail of interactions with confidential information, keys, and certificates managed by Azure Keyvault. Enabling logging for Key Vault saves information in an Azure storage account which the user provides. This creates a new container named insights-logs-auditevent automatically for the specified storage account. This same storage account can be used for collecting logs for multiple key vaults.",
"RelatedUrl": "https://docs.microsoft.com/en-us/azure/key-vault/key-vault-logging",
"Description": "**Azure Key Vault** diagnostic settings capture **audit logs** (`AuditEvent`) when category groups `audit` and `allLogs` are enabled and routed to a supported destination. Logged events include management and data-plane operations on vaults, keys, secrets, and certificates.",
"Risk": "Without **Key Vault audit logging**, access and changes to keys, secrets, and certificates are untracked.\n\nAttackers can misuse keys to decrypt data, alter or delete crypto material, and evade detection-eroding **confidentiality** and **integrity** and delaying **incident response**.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/azure/key-vault/general/logging?tabs=Vault",
"https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/azure/KeyVault/enable-audit-event-logging-for-azure-key-vaults.html",
"https://learn.microsoft.com/en-us/security/benchmark/azure/security-controls-v3-data-protection#dp-8-ensure-security-of-key-and-certificate-repository",
"https://learn.microsoft.com/en-us/azure/key-vault/general/howto-logging?tabs=azure-cli"
],
"Remediation": {
"Code": {
"CLI": "az monitor diagnostic-settings create --name <diagnostic settings name> --resource <key vault resource ID> --logs'[{category:AuditEvents,enabled:true,retention-policy:{enabled:true,days:180}}]' --metrics'[{category:AllMetrics,enabled:true,retention-policy:{enabled:true,days:180}}]' <[--event-hub <event hub ID> --event-hub-rule <event hub auth rule ID> | --storage-account <storage account ID> |--workspace <log analytics workspace ID> | --marketplace-partner-id <full resource ID of third-party solution>]>",
"NativeIaC": "",
"Other": "https://www.trendmicro.com/cloudoneconformity-staging/knowledge-base/azure/KeyVault/enable-audit-event-logging-for-azure-key-vaults.html",
"Terraform": ""
"CLI": "az monitor diagnostic-settings create --name <example_resource_name> --resource <example_resource_id> --workspace <example_resource_id> --logs '[{\"categoryGroup\":\"audit\",\"enabled\":true},{\"categoryGroup\":\"allLogs\",\"enabled\":true}]'",
"NativeIaC": "```bicep\n// Enable Key Vault diagnostic settings with audit + allLogs\nparam keyVaultName string\nparam workspaceId string\n\nresource kv 'Microsoft.KeyVault/vaults@2023-07-01' existing = {\n name: keyVaultName\n}\n\nresource diag 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = {\n name: '<example_resource_name>'\n scope: kv\n properties: {\n workspaceId: workspaceId\n logs: [\n {\n categoryGroup: 'audit' // critical: enables audit logs\n enabled: true // required to pass the check\n }\n {\n categoryGroup: 'allLogs' // critical: enables allLogs group\n enabled: true // required to pass the check\n }\n ]\n }\n}\n```",
"Other": "1. In Azure Portal, go to your Key Vault > Monitoring > Diagnostic settings\n2. Click Add diagnostic setting\n3. Under Category groups, select audit and allLogs\n4. Choose a destination (e.g., Send to Log Analytics workspace) and select the workspace\n5. Click Save",
"Terraform": "```hcl\n# Enable diagnostic settings on Key Vault with audit + allLogs\nresource \"azurerm_monitor_diagnostic_setting\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n target_resource_id = \"<example_resource_id>\" # Key Vault resource ID\n log_analytics_workspace_id = \"<example_resource_id>\" # Destination workspace ID\n\n enabled_log { # critical: audit category group\n category_group = \"audit\" # enables audit logs\n }\n enabled_log { # critical: allLogs category group\n category_group = \"allLogs\" # enables all logs\n }\n}\n```"
},
"Recommendation": {
"Text": "1. Go to Key vaults 2. For each Key vault 3. Go to Diagnostic settings 4. Click on Edit Settings 5. Ensure that Archive to a storage account is Enabled 6. Ensure that AuditEvent is checked, and the retention days is set to 180 days or as appropriate",
"Url": "https://docs.microsoft.com/en-us/security/benchmark/azure/security-controls-v3-data-protection#dp-8-ensure-security-of-key-and-certificate-repository"
"Text": "Enable **diagnostic settings** to collect `AuditEvent` logs-covering category groups `audit` and `allLogs`-and send them to a central sink. Apply **least privilege** to log access, enforce secure **retention/immutability**, monitor with alerts for anomalous operations, and use **separation of duties** to prevent logging bypass.",
"Url": "https://hub.prowler.com/check/keyvault_logging_enabled"
}
},
"Categories": [],
"Categories": [
"logging"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "By default, Diagnostic AuditEvent logging is not enabled for Key Vault instances."
@@ -1,30 +1,36 @@
{
"Provider": "azure",
"CheckID": "keyvault_non_rbac_secret_expiration_set",
"CheckTitle": "Ensure that the Expiration Date is set for all Secrets in Non-RBAC Key Vaults",
"CheckTitle": "Non-RBAC Key Vault has expiration date set for all secrets",
"CheckType": [],
"ServiceName": "keyvault",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "KeyVault",
"Severity": "medium",
"ResourceType": "microsoft.keyvault/vaults",
"ResourceGroup": "security",
"Description": "Ensure that all Secrets in Non Role Based Access Control (RBAC) Azure Key Vaults have an expiration date set.",
"Risk": "The Azure Key Vault enables users to store and keep secrets within the Microsoft Azure environment. Secrets in the Azure Key Vault are octet sequences with a maximum size of 25k bytes each. The exp (expiration date) attribute identifies the expiration date on or after which the secret MUST NOT be used. By default, secrets never expire. It is thus recommended to rotate secrets in the key vault and set an explicit expiration date for all secrets. This ensures that the secrets cannot be used beyond their assigned lifetimes.",
"RelatedUrl": "https://docs.microsoft.com/en-us/azure/key-vault/key-vault-whatis",
"Description": "**Azure Key Vault (non-RBAC)** secrets are expected to have an **explicit expiration date**.\n\nThis examines each **enabled secret** to confirm the `expires` attribute is defined.",
"Risk": "Secrets without expiration persist indefinitely, widening the window for misuse.\n\nIf leaked or forgotten, they allow long-term, covert access to services and data, undermining **confidentiality** and **integrity**, and complicating incident response and revocation.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/azure/key-vault/general/basic-concepts",
"https://learn.microsoft.com/en-us/azure/key-vault/general/about-keys-secrets-certificates#key-vault-secrets"
],
"Remediation": {
"Code": {
"CLI": "az keyvault secret set-attributes --name <secretName> --vault-name <vaultName> --expires Y-m-d'T'H:M:S'Z'",
"NativeIaC": "",
"Other": "",
"Terraform": "https://docs.prowler.com/checks/azure/azure-secrets-policies/set-an-expiration-date-on-all-secrets#terraform"
"CLI": "az keyvault secret set-attributes --vault-name <vaultName> --name <secretName> --expires <YYYY-MM-DDTHH:MM:SSZ>",
"NativeIaC": "```bicep\n// Set an expiration date on a Key Vault secret\nresource secret 'Microsoft.KeyVault/vaults/secrets@2023-07-01' = {\n name: '<example_vault_name>/<example_resource_name>'\n properties: {\n value: '<example_value>'\n attributes: {\n exp: 1767225599 // CRITICAL: sets the secret expiration (Unix time in seconds) so the check passes\n }\n }\n}\n```",
"Other": "1. In the Azure portal, go to Key vaults and open your vault\n2. Select Secrets, then click the secret that failed\n3. Click + New version\n4. Set Expiration date and click Create\n5. Repeat for any other secret without an expiration",
"Terraform": "```hcl\nresource \"azurerm_key_vault_secret\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n value = \"<example_value>\"\n key_vault_id = \"<example_resource_id>\"\n\n expiration_date = \"2025-12-31T23:59:59Z\" # CRITICAL: sets the secret expiration so the check passes\n}\n```"
},
"Recommendation": {
"Text": "From Azure Portal: 1. Go to Key vaults. 2. For each Key vault, click on Secrets. 3. In the main pane, ensure that the status of the secret is Enabled. 4. Set an appropriate Expiration date on all secrets. From Azure CLI: Update the Expiration date for the secret using the below command: az keyvault secret set-attributes --name <secretName> --vault-name <vaultName> --expires Y-m-d'T'H:M:S'Z' Note: To view the expiration date on all secrets in a Key Vault using Microsoft API, the List Key permission is required. To update the expiration date for the secrets: 1. Go to Key vault, click on Access policies. 2. Click on Create and add an access policy with the Update permission (in the Secret Permissions - Secret Management Operations section). From PowerShell: For each Key vault with the EnableRbacAuthorization setting set to False or empty, run the following command. Set-AzKeyVaultSecret -VaultName <Vault Name> -Name <Secret Name> -Expires <DateTime>",
"Url": "https://docs.microsoft.com/en-us/rest/api/keyvault/about-keys--secrets-and-certificates#key-vault-secrets"
"Text": "Set an **expiration** on every secret and enforce a **rotation policy** aligned with risk and compliance.\n\nAutomate rotation and alerts, disable or purge stale versions, and apply **least privilege**. *Where possible*, use **managed identities** to reduce secret sprawl.",
"Url": "https://hub.prowler.com/check/keyvault_non_rbac_secret_expiration_set"
}
},
"Categories": [],
"Categories": [
"secrets"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "Secrets cannot be used beyond their assigned expiry date respectively. Secrets need to be rotated periodically wherever they are used."
@@ -1,30 +1,37 @@
{
"Provider": "azure",
"CheckID": "keyvault_private_endpoints",
"CheckTitle": "Ensure that Private Endpoints are Used for Azure Key Vault",
"CheckTitle": "Key Vault uses private endpoints",
"CheckType": [],
"ServiceName": "keyvault",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "KeyVault",
"ResourceType": "microsoft.keyvault/vaults",
"ResourceGroup": "security",
"Description": "Private endpoints will secure network traffic from Azure Key Vault to the resources requesting secrets and keys.",
"Risk": "Private endpoints will keep network requests to Azure Key Vault limited to the endpoints attached to the resources that are whitelisted to communicate with each other. Assigning the Key Vault to a network without an endpoint will allow other resources on that network to view all traffic from the Key Vault to its destination. In spite of the complexity in configuration, this is recommended for high security secrets.",
"RelatedUrl": "https://docs.microsoft.com/en-us/azure/private-link/private-endpoint-overview",
"Description": "**Azure Key Vault** has **private endpoint connections** to serve secret and key operations over a private IP within your virtual network via Azure Private Link.",
"Risk": "Without **private endpoints**, the vault relies on a public endpoint, expanding exposure to scanning and misconfigured allowlists. Egress controls are harder to enforce, enabling unauthorized secret retrieval for **data exfiltration** and potential key misuse, impacting confidentiality and integrity.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/azure/private-link/private-endpoint-overview",
"https://learn.microsoft.com/en-us/azure/storage/common/storage-private-endpoints"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "",
"Terraform": ""
"CLI": "az network private-endpoint create --name <example_resource_name> --resource-group <example_resource_name> --location <LOCATION> --vnet-name <example_resource_name> --subnet <example_resource_name> --private-connection-resource-id <example_resource_id> --group-ids vault --connection-name <example_resource_name>",
"NativeIaC": "```bicep\n// Create a Private Endpoint for an existing Key Vault\nresource pe 'Microsoft.Network/privateEndpoints@2021-08-01' = {\n name: '<example_resource_name>'\n location: '<LOCATION>'\n properties: {\n subnet: {\n id: '<example_resource_id>' // Critical: subnet resource ID where the private endpoint NIC will be placed\n }\n privateLinkServiceConnections: [\n {\n name: '<example_resource_name>'\n properties: {\n privateLinkServiceId: '<example_resource_id>' // Critical: Key Vault resource ID to connect to\n groupIds: [ 'vault' ] // Critical: targets the Key Vault subresource to create the private endpoint connection\n }\n }\n ]\n }\n}\n```",
"Other": "1. In Azure portal, open your Key Vault\n2. Go to Networking > Private endpoint connections > + Create\n3. Basics: select Subscription and Resource group, then Next\n4. Resource: Service = Microsoft.KeyVault/vaults (your vault is preselected), Subresource = vault, Next\n5. Configuration: choose Virtual network and Subnet, then Next and Create\n6. Wait for the connection state to show Approved (auto-approves if you have permission)",
"Terraform": "```hcl\n# Create a Private Endpoint for an existing Key Vault\nresource \"azurerm_private_endpoint\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n location = \"<LOCATION>\"\n resource_group_name = \"<example_resource_name>\"\n subnet_id = \"<example_resource_id>\" # Critical: subnet resource ID for the private endpoint NIC\n\n private_service_connection {\n name = \"<example_resource_name>\"\n private_connection_resource_id = \"<example_resource_id>\" # Critical: Key Vault resource ID to connect\n subresource_names = [\"vault\"] # Critical: targets Key Vault subresource to create the connection\n }\n}\n```"
},
"Recommendation": {
"Text": "Please see the additional information about the requirements needed before starting this remediation procedure. From Azure Portal 1. From Azure Home open the Portal Menu in the top left. 2. Select Key Vaults. 3. Select a Key Vault to audit. 4. Select Networking in the left column. 5. Select Private endpoint connections from the top row. 6. Select + Create. 7. Select the subscription the Key Vault is within, and other desired configuration. 8. Select Next. 9. For resource type select Microsoft.KeyVault/vaults. 10. Select the Key Vault to associate the Private Endpoint with. 11. Select Next. 12. In the Virtual Networking field, select the network to assign the Endpoint. 13. Select other configuration options as desired, including an existing or new application security group. 14. Select Next. 15. Select the private DNS the Private Endpoints will use. 16. Select Next. 17. Optionally add Tags. 18. Select Next : Review + Create. 19. Review the information and select Create. Follow the Audit Procedure to determine if it has successfully applied. 20. Repeat steps 3-19 for each Key Vault. From Azure CLI 1. To create an endpoint, run the following command: az network private-endpoint create --resource-group <resourceGroup --vnet- name <vnetName> --subnet <subnetName> --name <PrivateEndpointName> -- private-connection-resource-id '/subscriptions/<AZURE SUBSCRIPTION ID>/resourceGroups/<resourceGroup>/providers/Microsoft.KeyVault/vaults/<keyVa ultName>' --group-ids vault --connection-name <privateLinkConnectionName> -- location <azureRegion> --manual-request 2. To manually approve the endpoint request, run the following command: az keyvault private-endpoint-connection approve --resource-group <resourceGroup> --vault-name <keyVaultName> name <privateLinkName> 4. Determine the Private Endpoint's IP address to connect the Key Vault to the Private DNS you have previously created: 5. Look for the property networkInterfaces then id, the value must be placed in the variable <privateEndpointNIC> within step 7. az network private-endpoint show -g <resourceGroupName> -n <privateEndpointName> 6. Look for the property networkInterfaces then id, the value must be placed on <privateEndpointNIC> in step 7. az network nic show --ids <privateEndpointName> 7. Create a Private DNS record within the DNS Zone you created for the Private Endpoint: az network private-dns record-set a add-record -g <resourcecGroupName> -z 'privatelink.vaultcore.azure.net' -n <keyVaultName> -a <privateEndpointNIC> 8. nslookup the private endpoint to determine if the DNS record is correct: nslookup <keyVaultName>.vault.azure.net nslookup <keyVaultName>.privatelink.vaultcore.azure.n",
"Url": "https://docs.microsoft.com/en-us/azure/storage/common/storage-private-endpoints"
"Text": "Enable **Private Endpoints** for each Key Vault and disable public network access. Use private DNS so the vault FQDN resolves to the private IP. Apply **least privilege** with RBAC and managed identities, restrict traffic with NSGs and routing, and monitor access logs as part of **defense in depth**.",
"Url": "https://hub.prowler.com/check/keyvault_private_endpoints"
}
},
"Categories": [],
"Categories": [
"internet-exposed",
"trust-boundaries"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "Incorrect or poorly-timed changing of network configuration could result in service interruption. There are also additional costs tiers for running a private endpoint perpetabyte or more of networking traffic."
@@ -1,30 +1,36 @@
{
"Provider": "azure",
"CheckID": "keyvault_rbac_enabled",
"CheckTitle": "Enable Role Based Access Control for Azure Key Vault",
"CheckTitle": "Key Vault uses Azure RBAC for access control",
"CheckType": [],
"ServiceName": "keyvault",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "KeyVault",
"ResourceType": "microsoft.keyvault/vaults",
"ResourceGroup": "security",
"Description": "WARNING: Role assignments disappear when a Key Vault has been deleted (soft-delete) and recovered. Afterwards it will be required to recreate all role assignments. This is a limitation of the soft-delete feature across all Azure services.",
"Risk": "The new RBAC permissions model for Key Vaults enables a much finer grained access control for key vault secrets, keys, certificates, etc., than the vault access policy. This in turn will permit the use of privileged identity management over these roles, thus securing the key vaults with JIT Access management.",
"RelatedUrl": "https://docs.microsoft.com/en-gb/azure/key-vault/general/rbac-migration#vault-access-policy-to-azure-rbac-migration-steps",
"Description": "**Azure Key Vault** uses the **Azure RBAC permission model** for data-plane access to keys, secrets, and certificates, rather than legacy access policies.\n\nEvaluates whether data access is managed through role assignments at the vault.",
"Risk": "Without **Azure RBAC**, data access relies on coarse access policies. **Control-plane Contributors** can grant themselves data-plane rights, enabling secret or key exfiltration and unauthorized crypto operations.\n\nLack of JIT and least-privilege weakens **confidentiality** and **integrity** and hinders auditing.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/azure/key-vault/general/rbac-guide?tabs=azure-cli",
"https://learn.microsoft.com/en-gb/azure/role-based-access-control/role-assignments-portal?tabs=current"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "",
"Terraform": ""
"CLI": "az keyvault update --name <KEY_VAULT_NAME> --resource-group <RESOURCE_GROUP_NAME> --enable-rbac-authorization true",
"NativeIaC": "```bicep\n// Enable Azure RBAC on a Key Vault\nresource kv 'Microsoft.KeyVault/vaults@2023-07-01' = {\n name: '<example_resource_name>'\n location: '<location>'\n properties: {\n tenantId: '<tenant_id>'\n enableRbacAuthorization: true // Critical: switches permission model to Azure RBAC to pass the check\n sku: {\n family: 'A'\n name: 'standard'\n }\n }\n}\n```",
"Other": "1. In Azure Portal, go to Key Vaults and open <KEY_VAULT_NAME>\n2. Under Settings, select Properties\n3. Set Permission model to Azure role-based access control\n4. Click Save",
"Terraform": "```hcl\nresource \"azurerm_key_vault\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n location = \"<location>\"\n resource_group_name = \"<example_resource_name>\"\n tenant_id = \"<tenant_id>\"\n sku_name = \"standard\"\n enable_rbac_authorization = true // Critical: enables Azure RBAC to satisfy the control\n}\n```"
},
"Recommendation": {
"Text": "From Azure Portal Key Vaults can be configured to use Azure role-based access control on creation. For existing Key Vaults: 1. From Azure Home open the Portal Menu in the top left corner 2. Select Key Vaults 3. Select a Key Vault to audit 4. Select Access configuration 5. Set the Permission model radio button to Azure role-based access control, taking note of the warning message 6. Click Save 7. Select Access Control (IAM) 8. Select the Role Assignments tab 9. Reapply permissions as needed to groups or users",
"Url": "https://docs.microsoft.com/en-gb/azure/role-based-access-control/role-assignments-portal?tabs=current"
"Text": "Adopt **Azure RBAC** for Key Vault data access and design roles with **least privilege** at appropriate scopes (prefer vault-level per app/env). Use **Privileged Identity Management** for JIT, restrict control-plane Contributor rights, and monitor role assignments. *Role assignments aren't preserved after soft-delete recovery*.",
"Url": "https://hub.prowler.com/check/keyvault_rbac_enabled"
}
},
"Categories": [],
"Categories": [
"identity-access"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "Implementation needs to be properly designed from the ground up, as this is a fundamental change to the way key vaults are accessed/managed. Changing permissions to key vaults will result in loss of service as permissions are re-applied. For the least amount of downtime, map your current groups and users to their corresponding permission needs."
@@ -1,30 +1,39 @@
{
"Provider": "azure",
"CheckID": "keyvault_rbac_key_expiration_set",
"CheckTitle": "Ensure that the Expiration Date is set for all Keys in RBAC Key Vaults",
"CheckTitle": "RBAC-enabled Key Vault has expiration date set for all keys",
"CheckType": [],
"ServiceName": "keyvault",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "KeyVault",
"Severity": "medium",
"ResourceType": "microsoft.keyvault/vaults",
"ResourceGroup": "security",
"Description": "Ensure that all Keys in Role Based Access Control (RBAC) Azure Key Vaults have an expiration date set",
"Risk": "Azure Key Vault enables users to store and use cryptographic keys within the Microsoft Azure environment. The exp (expiration date) attribute identifies the expiration date on or after which the key MUST NOT be used for encryption of new data, wrapping of new keys, and signing. By default, keys never expire. It is thus recommended that keys be rotated in the key vault and set an explicit expiration date for all keys to help enforce the key rotation. This ensures that the keys cannot be used beyond their assigned lifetimes.",
"RelatedUrl": "https://docs.microsoft.com/en-us/azure/key-vault/key-vault-whatis",
"Description": "**Azure Key Vaults** with **RBAC-enabled access control** are evaluated to confirm every **enabled key** defines an **expiration** (`exp`). Any key lacking this attribute is identified.",
"Risk": "**Keys without expiration** can remain active indefinitely.\nIf exposed, attackers can decrypt data, forge signatures (code/tokens), and maintain persistence, undermining **confidentiality** and **integrity**. Absent end-of-life also weakens rotation discipline and crypto agility.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/azure/key-vault/general/basic-concepts",
"https://learn.microsoft.com/en-us/azure/key-vault/general/about-keys-secrets-certificates#key-vault-keys",
"https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/azure/KeyVault/key-expiration-check.html#",
"https://learn.microsoft.com/en-us/azure/key-vault/general/azure-policy",
"https://learn.microsoft.com/en-us/azure/defender-for-cloud/recommendations-reference-keyvault"
],
"Remediation": {
"Code": {
"CLI": "az keyvault key set-attributes --name <keyName> --vault-name <vaultName> --expires Y-m-d'T'H:M:S'Z'",
"NativeIaC": "",
"Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/KeyVault/key-expiration-check.html#",
"Terraform": "https://docs.prowler.com/checks/azure/azure-general-policies/set-an-expiration-date-on-all-keys#terraform"
"CLI": "az keyvault key set-attributes --vault-name <vaultName> --name <keyName> --expires <YYYY-MM-DDThh:mm:ssZ>",
"NativeIaC": "```bicep\n// Set an expiration date on a Key Vault key\nresource key 'Microsoft.KeyVault/vaults/keys@2023-07-01' = {\n name: '<example_resource_name>/<example_resource_name>'\n properties: {\n kty: 'RSA'\n attributes: {\n exp: 1767225599 // Critical: expiration timestamp (UTC epoch seconds). Ensures the key has an expiration date to pass the check.\n }\n }\n}\n```",
"Other": "1. In Azure Portal, go to Key vaults and open <vaultName>\n2. Select Keys and choose the key missing an expiration\n3. Open the current version and click Update/Edit\n4. Set Expiration date (UTC) and click Save",
"Terraform": "```hcl\n# Set an expiration date on a Key Vault key\nresource \"azurerm_key_vault_key\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n key_vault_id = \"<example_resource_id>\"\n key_type = \"RSA\"\n\n expiration_date = \"2025-12-31T23:59:59Z\" # Critical: ensures the key has an expiration date to pass the check\n}\n```"
},
"Recommendation": {
"Text": "From Azure Portal: 1. Go to Key vaults. 2. For each Key vault, click on Keys. 3. In the main pane, ensure that an appropriate Expiration date is set for any keys that are Enabled. From Azure CLI: Update the Expiration date for the key using the below command: az keyvault key set-attributes --name <keyName> --vault-name <vaultName> -- expires Y-m-d'T'H:M:S'Z' Note: To view the expiration date on all keys in a Key Vault using Microsoft API, the 'List' Key permission is required. To update the expiration date for the keys: 1. Go to the Key vault, click on Access Control (IAM). 2. Click on Add role assignment and assign the role of Key Vault Crypto Officer to the appropriate user. From PowerShell: Set-AzKeyVaultKeyAttribute -VaultName <VaultName> -Name <KeyName> -Expires <DateTime>",
"Url": "https://docs.microsoft.com/en-us/rest/api/keyvault/about-keys--secrets-and-certificates#key-vault-keys"
"Text": "- Set `exp` on all enabled keys and enforce a **rotation policy** with short lifetimes and automated renewal.\n- Use **governance policies** to require expiration and alert before expiry.\n- Apply **least privilege** and **separation of duties** for key admins vs consumers.",
"Url": "https://hub.prowler.com/check/keyvault_rbac_key_expiration_set"
}
},
"Categories": [],
"Categories": [
"encryption"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "Keys cannot be used beyond their assigned expiration dates respectively. Keys need to be rotated periodically wherever they are used."
@@ -1,30 +1,36 @@
{
"Provider": "azure",
"CheckID": "keyvault_rbac_secret_expiration_set",
"CheckTitle": "Ensure that the Expiration Date is set for all Secrets in RBAC Key Vaults",
"CheckTitle": "RBAC-enabled Key Vault has expiration date set for all enabled secrets",
"CheckType": [],
"ServiceName": "keyvault",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "KeyVault",
"Severity": "medium",
"ResourceType": "microsoft.keyvault/vaults",
"ResourceGroup": "security",
"Description": "Ensure that all Secrets in Role Based Access Control (RBAC) Azure Key Vaults have an expiration date set.",
"Risk": "The Azure Key Vault enables users to store and keep secrets within the Microsoft Azure environment. Secrets in the Azure Key Vault are octet sequences with a maximum size of 25k bytes each. The exp (expiration date) attribute identifies the expiration date on or after which the secret MUST NOT be used. By default, secrets never expire. It is thus recommended to rotate secrets in the key vault and set an explicit expiration date for all secrets. This ensures that the secrets cannot be used beyond their assigned lifetimes.",
"RelatedUrl": "https://docs.microsoft.com/en-us/azure/key-vault/key-vault-whatis",
"Description": "**Azure Key Vault (RBAC)** secrets are assessed to confirm every **enabled secret** has an `exp` (expiration) date configured",
"Risk": "Without an **expiration**, secrets become perpetual credentials. Leaked or abandoned values can grant persistent access, undermining **confidentiality** and **integrity**. Attackers can reuse old secrets to maintain footholds, perform unauthorized API calls, and exfiltrate data.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/azure/key-vault/general/basic-concepts",
"https://learn.microsoft.com/en-us/azure/key-vault/general/about-keys-secrets-certificates#key-vault-secrets"
],
"Remediation": {
"Code": {
"CLI": "az keyvault secret set-attributes --name <secretName> --vault-name <vaultName> --expires Y-m-d'T'H:M:S'Z'",
"NativeIaC": "",
"Other": "",
"Terraform": "https://docs.prowler.com/checks/azure/azure-secrets-policies/set-an-expiration-date-on-all-secrets#terraform"
"CLI": "az keyvault secret set-attributes --vault-name <vaultName> --name <secretName> --expires <YYYY-MM-DDTHH:MM:SSZ>",
"NativeIaC": "```bicep\n// Set expiration on a Key Vault secret\nresource secret 'Microsoft.KeyVault/vaults/secrets@2019-09-01' = {\n name: '<example_resource_name>/<example_resource_name>'\n properties: {\n // CRITICAL: sets the secret expiration timestamp (Unix epoch seconds)\n attributes: {\n exp: 1735689600 // 2025-01-01T00:00:00Z\n }\n value: '<secret_value>'\n }\n}\n```",
"Other": "1. In Azure Portal, go to Key vaults and open your vault\n2. Select Secrets, choose the secret, then open its Current version\n3. Set Expiration date (UTC) and click Save",
"Terraform": "```hcl\nresource \"azurerm_key_vault_secret\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n value = \"<secret_value>\"\n key_vault_id = \"<example_resource_id>\"\n\n # CRITICAL: sets the secret expiration timestamp\n expiration_date = \"2025-01-01T00:00:00Z\"\n}\n```"
},
"Recommendation": {
"Text": "From Azure Portal: 1. Go to Key vaults. 2. For each Key vault, click on Secrets. 3. In the main pane, ensure that the status of the secret is Enabled. 4. For each enabled secret, ensure that an appropriate Expiration date is set. From Azure CLI: Update the Expiration date for the secret using the below command: az keyvault secret set-attributes --name <secretName> --vault-name <vaultName> --expires Y-m-d'T'H:M:S'Z' Note: To view the expiration date on all secrets in a Key Vault using Microsoft API, the List Key permission is required. To update the expiration date for the secrets: 1. Go to the Key vault, click on Access Control (IAM). 2. Click on Add role assignment and assign the role of Key Vault Secrets Officer to the appropriate user. From PowerShell: Set-AzKeyVaultSecretAttribute -VaultName <Vault Name> -Name <Secret Name> - Expires <DateTime>",
"Url": "https://docs.microsoft.com/en-us/rest/api/keyvault/about-keys--secrets-and-certificates#key-vault-secrets"
"Text": "Set an **expiration** on all enabled secrets and enforce a **regular rotation policy**.\n\nPrefer **short-lived, identity-based access** to reduce secret usage. Apply **least privilege** for secret access, alert on upcoming expirations, and automate rotation and version cleanup to minimize exposure.",
"Url": "https://hub.prowler.com/check/keyvault_rbac_secret_expiration_set"
}
},
"Categories": [],
"Categories": [
"secrets"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "Secrets cannot be used beyond their assigned expiry date respectively. Secrets need to be rotated periodically wherever they are used."
@@ -1,30 +1,36 @@
{
"Provider": "azure",
"CheckID": "keyvault_recoverable",
"CheckTitle": "Ensure the Key Vault is Recoverable",
"CheckTitle": "Key Vault has soft delete and purge protection enabled",
"CheckType": [],
"ServiceName": "keyvault",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "KeyVault",
"ResourceType": "microsoft.keyvault/vaults",
"ResourceGroup": "security",
"Description": "The Key Vault contains object keys, secrets, and certificates. Accidental unavailability of a Key Vault can cause immediate data loss or loss of security functions (authentication, validation, verification, non-repudiation, etc.) supported by the Key Vault objects. It is recommended the Key Vault be made recoverable by enabling the 'Do Not Purge' and 'Soft Delete' functions. This is in order to prevent loss of encrypted data, including storage accounts, SQL databases, and/or dependent services provided by Key Vault objects (Keys, Secrets, Certificates) etc. This may happen in the case of accidental deletion by a user or from disruptive activity by a malicious user. WARNING: A current limitation of the soft-delete feature across all Azure services is role assignments disappearing when Key Vault is deleted. All role assignments will need to be recreated after recovery.",
"Risk": "There could be scenarios where users accidentally run delete/purge commands on Key Vault or an attacker/malicious user deliberately does so in order to cause disruption. Deleting or purging a Key Vault leads to immediate data loss, as keys encrypting data and secrets/certificates allowing access/services will become non-accessible. There are 2 Key Vault properties that play a role in permanent unavailability of a Key Vault: 1. enableSoftDelete: Setting this parameter to 'true' for a Key Vault ensures that even if Key Vault is deleted, Key Vault itself or its objects remain recoverable for the next 90 days. Key Vault/objects can either be recovered or purged (permanent deletion) during those 90 days. If no action is taken, key vault and its objects will subsequently be purged. 2. enablePurgeProtection: enableSoftDelete only ensures that Key Vault is not deleted permanently and will be recoverable for 90 days from date of deletion. However, there are scenarios in which the Key Vault and/or its objects are accidentally purged and hence will not be recoverable. Setting enablePurgeProtection to 'true' ensures that the Key Vault and its objects cannot be purged. Enabling both the parameters on Key Vaults ensures that Key Vaults and their objects cannot be deleted/purged permanently.",
"RelatedUrl": "https://docs.microsoft.com/en-us/azure/key-vault/key-vault-soft-delete-cli",
"Description": "**Azure Key Vault** recoverability requires both `enable_soft_delete` and `enable_purge_protection`. With these enabled, vault objects remain recoverable after deletion and cannot be permanently purged during the retention period.",
"Risk": "Absent these protections, deleted vaults or objects can be permanently removed, cutting access to keys, secrets, and certificates. This can render data unreadable, break app authentication, and halt signing/verification, degrading **availability** and **integrity**. Malicious insiders can purge to block recovery.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/azure/key-vault/general/key-vault-recovery?tabs=azure-cli",
"https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/azure/KeyVault/enable-key-vault-recoverability.html#"
],
"Remediation": {
"Code": {
"CLI": "az resource update --id /subscriptions/xxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/<resourceGroupName>/providers/Microsoft.KeyVault/vaults/<keyVaultName> --set properties.enablePurgeProtection=trueproperties.enableSoftDelete=true",
"NativeIaC": "",
"Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/KeyVault/enable-key-vault-recoverability.html#",
"Terraform": "https://docs.prowler.com/checks/azure/azure-general-policies/ensure-the-key-vault-is-recoverable#terraform"
"CLI": "az keyvault update -g <resourceGroupName> -n <keyVaultName> --enable-soft-delete true --enable-purge-protection true",
"NativeIaC": "```bicep\n// Enable soft delete and purge protection on an existing/new Key Vault\nresource kv 'Microsoft.KeyVault/vaults@2023-07-01' = {\n name: '<example_resource_name>'\n location: '<location>'\n properties: {\n tenantId: '<tenant_id>'\n sku: { name: 'standard' }\n enableSoftDelete: true // Critical: ensures soft delete is enabled\n enablePurgeProtection: true // Critical: prevents permanent purge during retention\n }\n}\n```",
"Other": "1. In Azure Portal, go to Key vaults and open <keyVaultName>\n2. Select Properties > Recovery\n3. Turn on Soft delete and Purge protection\n4. Click Save",
"Terraform": "```hcl\nresource \"azurerm_key_vault\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n location = \"<location>\"\n resource_group_name = \"<resource_group_name>\"\n tenant_id = \"<tenant_id>\"\n sku_name = \"standard\"\n\n soft_delete_enabled = true # Critical: enables soft delete\n purge_protection_enabled = true # Critical: enables purge protection\n}\n```"
},
"Recommendation": {
"Text": "To enable 'Do Not Purge' and 'Soft Delete' for a Key Vault: From Azure Portal 1. Go to Key Vaults 2. For each Key Vault 3. Click Properties 4. Ensure the status of soft-delete reads Soft delete has been enabled on this key vault. 5. At the bottom of the page, click 'Enable Purge Protection' Note, once enabled you cannot disable it. From Azure CLI az resource update --id /subscriptions/xxxxxx-xxxx-xxxx-xxxx- xxxxxxxxxxxx/resourceGroups/<resourceGroupName>/providers/Microsoft.KeyVault /vaults/<keyVaultName> --set properties.enablePurgeProtection=true properties.enableSoftDelete=true From PowerShell Update-AzKeyVault -VaultName <vaultName -ResourceGroupName <resourceGroupName -EnablePurgeProtection",
"Url": "https://blogs.technet.microsoft.com/kv/2017/05/10/azure-key-vault-recovery-options/"
"Text": "Enable both `enable_soft_delete` and `enable_purge_protection` on all vaults. Enforce with policy, restrict purge/recover to **least privilege** and apply **separation of duties**. Keep backups and test recovery. Monitor delete/purge with alerts. *Adjust retention to business needs* to strengthen defense in depth.",
"Url": "https://hub.prowler.com/check/keyvault_recoverable"
}
},
"Categories": [],
"Categories": [
"resilience"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "Once purge-protection and soft-delete are enabled for a Key Vault, the action is irreversible."
@@ -1,30 +1,37 @@
{
"Provider": "azure",
"CheckID": "vm_backup_enabled",
"CheckTitle": "Ensure Backups are enabled for Azure Virtual Machines",
"CheckTitle": "Virtual Machine is protected by Azure Backup",
"CheckType": [],
"ServiceName": "vm",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "Microsoft.Compute/virtualMachines",
"ResourceType": "microsoft.compute/virtualmachines",
"ResourceGroup": "compute",
"Description": "Ensure that Microsoft Azure Backup service is in use for your Azure virtual machines (VMs) to protect against accidental deletion or corruption.",
"Risk": "Without Azure Backup enabled, VMs are at risk of data loss due to accidental deletion, corruption, or other failures, and recovery options are limited.",
"RelatedUrl": "https://docs.microsoft.com/en-us/azure/backup/backup-overview",
"Description": "**Azure VMs** are evaluated for protection by **Azure Backup** by confirming they exist as VM backup items in a **Recovery Services vault**.\n\nVMs absent from any vault item indicate no configured backup coverage.",
"Risk": "Unprotected VMs jeopardize **availability** and **integrity**. Deletion, corruption, or ransomware can wipe data, and without recovery points recovery is slow or impossible, causing extended outages, missed `RPO/RTO`, and cascading impact on dependent services.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/azure/backup/backup-azure-arm-vms-prepare",
"https://learn.microsoft.com/en-us/azure/backup/quick-backup-vm-portal",
"https://learn.microsoft.com/en-us/azure/backup/backup-overview"
],
"Remediation": {
"Code": {
"CLI": "az backup protection enable-for-vm --resource-group <resource-group> --vm <vm-name> --vault-name <vault-name> --policy-name DefaultPolicy",
"NativeIaC": "",
"Other": "https://learn.microsoft.com/en-us/azure/backup/quick-backup-vm-portal",
"Terraform": ""
"CLI": "az backup protection enable-for-vm --resource-group <vault-resource-group> --vault-name <vault-name> --vm <vm-name> --vm-resource-group <vm-resource-group> --policy-name DefaultPolicy",
"NativeIaC": "```bicep\n// Enable Azure Backup protection for an existing VM in an existing Recovery Services vault\nparam vmId string // e.g., /subscriptions/<sub>/resourceGroups/<rg>/providers/Microsoft.Compute/virtualMachines/<vm>\nparam vaultName string\nparam vaultRg string\nparam policyId string // e.g., /subscriptions/<sub>/resourceGroups/<rg>/providers/Microsoft.RecoveryServices/vaults/<vault>/backupPolicies/<policy>\n\nresource vault 'Microsoft.RecoveryServices/vaults@2023-04-01' existing = {\n name: vaultName\n scope: resourceGroup(vaultRg)\n}\n\nvar vmRg = split(split(vmId, '/resourceGroups/')[1], '/')[0]\nvar vmName = split(vmId, '/virtualMachines/')[1]\n\nresource protect 'Microsoft.RecoveryServices/vaults/backupFabrics/protectionContainers/protectedItems@2023-02-01' = {\n // critical: this resource creates the protected item, enabling backup for the VM\n name: '${vault.name}/Azure/protectionContainers/iaasvmcontainer;iaasvmcontainerv2;${vmRg};${vmName}/protectedItems/VM;iaasvmcontainerv2;${vmRg};${vmName}'\n properties: {\n protectedItemType: 'Microsoft.Compute/virtualMachines' // critical: VM backup item type\n sourceResourceId: vmId // critical: target VM to protect\n policyId: policyId // critical: associates the VM with a backup policy\n }\n}\n```",
"Other": "1. In Azure Portal, go to Virtual machines > <VM> > Backup\n2. Select a Recovery Services vault in the same region (or create one if prompted)\n3. Choose the DefaultPolicy (or an existing VM backup policy)\n4. Click Enable backup",
"Terraform": "```hcl\n# Protect an existing VM with Azure Backup\nresource \"azurerm_backup_protected_vm\" \"<example_resource_name>\" {\n resource_group_name = \"<example_resource_name>\" # vault's resource group\n recovery_vault_name = \"<example_resource_name>\"\n source_vm_id = \"<example_resource_id>\" # critical: VM to protect\n backup_policy_id = \"<example_resource_id>\" # critical: policy that enables backup\n}\n```"
},
"Recommendation": {
"Text": "Enable Azure Backup for each VM by associating it with a Recovery Services vault and a backup policy.",
"Url": "https://docs.microsoft.com/en-us/azure/backup/quick-backup-vm-portal"
"Text": "Protect all VMs with **Azure Backup** in a **Recovery Services vault** under a standardized policy. Align schedules and retention to `RPO/RTO`, use `GRS`/`ZRS` and `immutable` vault features, enforce least privilege on backup operations, automate enrollment at provisioning, and regularly test restores.",
"Url": "https://hub.prowler.com/check/vm_backup_enabled"
}
},
"Categories": [],
"Categories": [
"resilience"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
@@ -1,27 +1,31 @@
{
"Provider": "azure",
"CheckID": "vm_desired_sku_size",
"CheckTitle": "Ensure that your virtual machine instances are using SKU sizes that are approved by your organization",
"CheckTitle": "Virtual Machine uses an organization-approved SKU size",
"CheckType": [],
"ServiceName": "vm",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "Microsoft.Compute/virtualMachines",
"Severity": "medium",
"ResourceType": "microsoft.compute/virtualmachines",
"ResourceGroup": "compute",
"Description": "Ensure that your virtual machine instances are using SKU sizes that are approved by your organization. This check requires configuration of the desired VM SKU sizes in the Prowler configuration file.",
"Risk": "Setting limits for the SKU size(s) of the virtual machine instances provisioned in your Microsoft Azure account can help you to manage better your cloud compute power, address internal compliance requirements and prevent unexpected charges on your Azure monthly bill. Without proper SKU size controls, organizations may face cost overruns and compliance violations.",
"RelatedUrl": "https://learn.microsoft.com/en-us/azure/virtual-machines/sizes/overview",
"Description": "**Azure virtual machines** are compared against an organization **allowlist of VM size SKUs** defined in `desired_vm_sku_sizes`.\n\nInstances using sizes outside this list are flagged as non-standard.",
"Risk": "Unrestricted VM sizes enable over-provisioned or exotic SKUs, leading to:\n- Sudden cost spikes and quota exhaustion (availability)\n- Use of hardware lacking required security features (confidentiality/integrity)\n- Abuse by compromised accounts for cryptomining at scale",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/azure/virtual-machines/sizes/resize-vm",
"https://learn.microsoft.com/en-us/azure/virtual-machines/sizes/overview"
],
"Remediation": {
"Code": {
"CLI": "az policy assignment create --display-name 'Allowed VM SKU Sizes' --policy cccc23c7-8427-4f53-ad12-b6a63eb452b3 -p '{\"listOfAllowedSKUs\": {\"value\": [\"<desired-sku-1>\", \"<desired-sku-2>\"]}}' --scope /subscriptions/<subscription-id>",
"NativeIaC": "",
"Other": "",
"Terraform": ""
"CLI": "az vm resize --resource-group <RESOURCE_GROUP> --name <VM_NAME> --size <desired-sku-1>",
"NativeIaC": "```bicep\n// Resize VM to an approved SKU\nresource vm 'Microsoft.Compute/virtualMachines@2023-09-01' = {\n name: '<example_resource_name>'\n location: '<example_location>'\n properties: {\n hardwareProfile: {\n vmSize: '<desired-sku-1>' // CRITICAL: sets VM to an approved size so the check passes\n }\n }\n}\n```",
"Other": "1. In Azure Portal, go to Virtual machines and select the VM\n2. Under Availability + scale, select Size\n3. Choose an approved size (e.g., <desired-sku-1>) and click Resize\n4. If the size isn't listed, Stop (deallocate) the VM, then retry Resize",
"Terraform": "```hcl\n# Enforce allowed VM SKUs via Azure Policy\nresource \"azurerm_policy_assignment\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n scope = \"/subscriptions/<example_resource_id>\"\n policy_definition_id = \"/providers/Microsoft.Authorization/policyDefinitions/<example_resource_id>\"\n\n parameters = jsonencode({\n listOfAllowedSKUs = { value = [\"<desired-sku-1>\", \"<desired-sku-2>\"] } # CRITICAL: only these SKUs are allowed\n })\n}\n```"
},
"Recommendation": {
"Text": "1. Define and document your organization's approved VM SKU sizes based on workload requirements, cost constraints, and compliance needs. 2. Implement Azure Policy to enforce VM size restrictions across your subscriptions. 3. Use the 'Allowed virtual machine size SKUs' built-in policy to restrict VM creation to approved sizes. 4. Regularly review and update your approved SKU list based on changing business requirements and cost optimization goals. 5. Monitor VM usage and costs to ensure compliance with your SKU size policies.",
"Url": "https://learn.microsoft.com/en-us/azure/virtual-machines/sizes/resize-vm"
"Text": "Adopt a **least privilege, policy-enforced allowlist** of VM size SKUs per workload tier and region.\n- Deny creation of non-approved sizes across scopes\n- Require exception reviews with time-bound overrides\n- Reassess the list regularly for cost, performance, and compliance\n- Monitor provisioning and cost anomalies for drift",
"Url": "https://hub.prowler.com/check/vm_desired_sku_size"
}
},
"Categories": [],
@@ -1,30 +1,38 @@
{
"Provider": "azure",
"CheckID": "vm_ensure_attached_disks_encrypted_with_cmk",
"CheckTitle": "Ensure that 'OS and Data' disks are encrypted with Customer Managed Key (CMK)",
"CheckTitle": "Virtual Machine OS or data disk is encrypted with a customer-managed key (CMK)",
"CheckType": [],
"ServiceName": "vm",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "Microsoft.Compute/virtualMachines",
"Severity": "medium",
"ResourceType": "microsoft.compute/disks",
"ResourceGroup": "compute",
"Description": "Ensure that OS disks (boot volumes) and data disks (non-boot volumes) are encrypted with CMK (Customer Managed Keys). Customer Managed keys can be either ADE or Server Side Encryption (SSE).",
"Risk": "Encrypting the IaaS VM's OS disk (boot volume) and Data disks (non-boot volume) ensures that the entire content is fully unrecoverable without a key, thus protecting the volume from unwanted reads. PMK (Platform Managed Keys) are enabled by default in Azure-managed disks and allow encryption at rest. CMK is recommended because it gives the customer the option to control which specific keys are used for the encryption and decryption of the disk. The customer can then change keys and increase security by disabling them instead of relying on the PMK key that remains unchanging. There is also the option to increase security further by using automatically rotating keys so that access to disk is ensured to be limited. Organizations should evaluate what their security requirements are, however, for the data stored on the disk. For high-risk data using CMK is a must, as it provides extra steps of security. If the data is low risk, PMK is enabled by default and provides sufficient data security.",
"RelatedUrl": "https://learn.microsoft.com/en-us/azure/virtual-machines/disk-encryption-overview",
"Description": "**Attached Azure managed disks** (OS and data) are assessed to confirm encryption uses **customer-managed keys** (`CMK`) via disk encryption sets rather than platform-managed keys. Scope includes disks currently attached to VMs, evaluating their encryption type to verify CMK is applied.",
"Risk": "Without **CMK**, you lose control over key lifecycle and access. This weakens confidentiality and compliance: you can't enforce independent rotation, promptly revoke keys to crypto-lock stolen copies/snapshots, or separate duties. Misuse or compromise may keep data readable beyond your trust boundary.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/azure/virtual-machines/disk-encryption-overview",
"https://support.icompaas.com/support/solutions/articles/62000229895-ensure-that-os-and-data-disks-are-encrypted-with-customer-managed-key-cmk-",
"https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/azure/VirtualMachines/sse-boot-disk-cmk.html#",
"https://learn.microsoft.com/en-us/azure/security/fundamentals/data-encryption-best-practices#protect-data-at-rest"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/VirtualMachines/sse-boot-disk-cmk.html#",
"Terraform": "https://docs.prowler.com/checks/azure/azure-general-policies/bc_azr_general_1#terraform"
"CLI": "az disk update -g <RESOURCE_GROUP> -n <DISK_NAME> --encryption-type EncryptionAtRestWithCustomerKey --disk-encryption-set <DES_ID>",
"NativeIaC": "```bicep\n// Encrypt a managed disk with a customer-managed key via Disk Encryption Set (DES)\nresource disk 'Microsoft.Compute/disks@2023-10-02' = {\n name: '<example_resource_name>'\n location: '<LOCATION>'\n sku: {\n name: 'Standard_LRS'\n }\n properties: {\n creationData: {\n createOption: 'Empty'\n }\n diskSizeGB: 32\n\n // CRITICAL: Use CMK by attaching a Disk Encryption Set (DES)\n // This switches encryption from platform-managed to customer-managed\n encryption: {\n type: 'EncryptionAtRestWithCustomerKey' // critical: CMK\n diskEncryptionSetId: '<example_resource_id>' // critical: DES resource ID\n }\n }\n}\n```",
"Other": "1. In Azure Portal, open the target VM and click Stop to deallocate it.\n2. For each data disk: Go to VM > Disks > select the data disk > Detach > Save.\n3. In the portal search box, open Disks, select the detached disk > Encryption.\n4. Set Encryption type to Customer-managed key (Disk encryption set), select your Disk Encryption Set, then Save.\n5. Reattach the disk: VM > Disks > Add data disk (select the same disk) > Save.\n6. For OS disk, use Swap OS disk: create a new managed disk from a snapshot/image with Encryption type = Customer-managed key (select the same DES), then VM > Disks > Swap OS disk and choose that disk.\n7. Start the VM.",
"Terraform": "```hcl\n# Encrypt a managed disk with a customer-managed key via Disk Encryption Set (DES)\nresource \"azurerm_managed_disk\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n location = \"<LOCATION>\"\n resource_group_name = \"<example_resource_name>\"\n storage_account_type = \"Standard_LRS\"\n create_option = \"Empty\"\n disk_size_gb = 32\n\n # CRITICAL: Attach DES to enable SSE with CMK and pass the check\n disk_encryption_set_id = \"<example_resource_id>\" # critical: DES ID\n}\n```"
},
"Recommendation": {
"Text": "Note: Disks must be detached from VMs to have encryption changed. 1. Go to Virtual machines 2. For each virtual machine, go to Settings 3. Click on Disks 4. Click the ellipsis (...), then click Detach to detach the disk from the VM 5. Now search for Disks and locate the unattached disk 6. Click the disk then select Encryption 7. Change your encryption type, then select your encryption set 8. Click Save 9. Go back to the VM and re-attach the disk",
"Url": "https://learn.microsoft.com/en-us/azure/security/fundamentals/data-encryption-best-practices#protect-data-at-rest"
"Text": "Use **CMK** with disk encryption sets for all attached OS and data disks.\n- Enforce **least privilege** on key usage and scope\n- Enable periodic key rotation and auditing\n- Store keys in HSM-backed vaults; separate key and data admins\n- Combine with **encryption at host** to cover temp disks and caches",
"Url": "https://hub.prowler.com/check/vm_ensure_attached_disks_encrypted_with_cmk"
}
},
"Categories": [],
"Categories": [
"encryption"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "Using CMK/BYOK will entail additional management of keys."
@@ -1,30 +1,38 @@
{
"Provider": "azure",
"CheckID": "vm_ensure_unattached_disks_encrypted_with_cmk",
"CheckTitle": "Ensure that 'Unattached disks' are encrypted with 'Customer Managed Key' (CMK)",
"CheckTitle": "Unattached disk is encrypted with a customer-managed key (CMK)",
"CheckType": [],
"ServiceName": "vm",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "Microsoft.Compute/virtualMachines",
"Severity": "medium",
"ResourceType": "microsoft.compute/disks",
"ResourceGroup": "compute",
"Description": "Ensure that unattached disks in a subscription are encrypted with a Customer Managed Key (CMK).",
"Risk": "Managed disks are encrypted by default with Platform-managed keys. Using Customer-managed keys may provide an additional level of security or meet an organization's regulatory requirements. Encrypting managed disks ensures that its entire content is fully unrecoverable without a key and thus protects the volume from unwarranted reads. Even if the disk is not attached to any of the VMs, there is always a risk where a compromised user account with administrative access to VM service can mount/attach these data disks, which may lead to sensitive information disclosure and tampering.",
"RelatedUrl": "https://docs.microsoft.com/en-us/azure/security/fundamentals/azure-disk-encryption-vms-vmss",
"Description": "Unattached **Azure managed disks** use **Customer-Managed Keys** (`CMK`) for server-side encryption rather than platform-managed keys. Only disks not currently attached to a VM are in scope.",
"Risk": "Without **CMK**, you lack independent key control on unattached disks. A compromised admin could attach a disk to read or alter data before you can revoke access. Missing customer-controlled rotation and revocation weakens **confidentiality** and **integrity**, and can hinder data-sovereignty compliance.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/azure/VirtualMachines/sse-unattached-disk-cmk.html#",
"https://learn.microsoft.com/en-us/azure/security/fundamentals/data-encryption-best-practices#protect-data-at-rest",
"https://learn.microsoft.com/en-us/rest/api/compute/disks/delete?view=rest-compute-2023-10-02&tabs=HTTP",
"https://learn.microsoft.com/en-us/azure/virtual-machines/disk-encryption-overview"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/VirtualMachines/sse-unattached-disk-cmk.html#",
"Terraform": ""
"CLI": "az disk update -g <RESOURCE_GROUP> -n <DISK_NAME> --encryption-type EncryptionAtRestWithCustomerKey --disk-encryption-set <DES_ID>",
"NativeIaC": "```bicep\n// Update an existing managed disk to use a customer-managed key via Disk Encryption Set\nresource example_disk 'Microsoft.Compute/disks@2023-08-01' = {\n name: '<example_resource_name>'\n location: '<location>'\n properties: {\n encryption: {\n type: 'EncryptionAtRestWithCustomerKey' // CRITICAL: switch to CMK-based encryption\n diskEncryptionSetId: '<example_resource_id>' // CRITICAL: DES resource ID that holds the CMK\n }\n }\n}\n```",
"Other": "1. In Azure portal, go to Disks and open the unattached disk\n2. Select Encryption\n3. Set Encryption type to Customer-managed key\n4. Select the Disk encryption set to use\n5. Click Save",
"Terraform": "```hcl\n# Associate an unattached managed disk with a Disk Encryption Set (CMK)\nresource \"azurerm_managed_disk\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n location = \"<location>\"\n resource_group_name = \"<example_resource_name>\"\n create_option = \"Empty\"\n disk_size_gb = 1\n\n disk_encryption_set_id = \"<example_resource_id>\" # CRITICAL: enables CMK by linking the Disk Encryption Set\n}\n```"
},
"Recommendation": {
"Text": "If data stored in the disk is no longer useful, refer to Azure documentation to delete unattached data disks at: https://learn.microsoft.com/en-us/rest/api/compute/disks/delete?view=rest-compute-2023-10-02&tabs=HTTP",
"Url": "https://learn.microsoft.com/en-us/azure/security/fundamentals/data-encryption-best-practices#protect-data-at-rest"
"Text": "Encrypt unattached disks with **CMK** backed by a hardened key service. Enforce **least privilege** for disk attachment and key usage, enable key rotation and auditing, and restrict access to keys. Apply lifecycle governance-*remove stale disks*-to reduce exposure and support **defense in depth**.",
"Url": "https://hub.prowler.com/check/vm_ensure_unattached_disks_encrypted_with_cmk"
}
},
"Categories": [],
"Categories": [
"encryption"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "You must have your key vault set up to utilize this. Encryption is available only on Standard tier VMs. This might cost you more. Utilizing and maintaining Customer-managed keys will require additional work to create, protect, and rotate keys."
@@ -1,30 +1,36 @@
{
"Provider": "azure",
"CheckID": "vm_ensure_using_approved_images",
"CheckTitle": "Ensure that Azure VMs are using an approved machine image.",
"CheckTitle": "Virtual Machine uses an approved custom machine image",
"CheckType": [],
"ServiceName": "vm",
"SubServiceName": "image",
"ResourceIdTemplate": "/subscriptions/<subscription-id>/resourceGroups/<resource-group-name>/providers/Microsoft.Compute/images/<virtual-machine-image-id>",
"Severity": "medium",
"ResourceType": "Microsoft.Compute/images",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "microsoft.compute/virtualmachines",
"ResourceGroup": "compute",
"Description": "Ensure that all your Azure virtual machine instances are launched from approved machine images only.",
"Risk": "An approved machine image is a custom virtual machine (VM) image that contains a pre-configured OS and a well-defined stack of server software approved by Azure, fully configured to run your application. Using approved (golden) machine images to launch new VM instances within your Azure cloud environment brings major benefits such as fast and stable application deployment and scaling, secure application stack upgrades, and versioning.",
"RelatedUrl": "https://learn.microsoft.com/en-us/azure/virtual-machines/windows/create-vm-generalized-managed",
"Description": "**Azure VMs** are evaluated for use of an **approved custom image** by inspecting the VM image reference. The expected format is a subscription-scoped ID like `/subscriptions/.../providers/Microsoft.Compute/images/<image>`, not marketplace, gallery, or community sources.",
"Risk": "Using **unapproved images** undermines **integrity** and **confidentiality** by introducing unknown packages, misconfigurations, or malware. Attackers can implant backdoors, weaken hardening, and bypass baselines, enabling data exfiltration and lateral movement, and harming **availability** with unpatched software.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/azure/VirtualMachines/approved-machine-image.html",
"https://learn.microsoft.com/en-us/azure/virtual-machines/windows/create-vm-generalized-managed"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/VirtualMachines/approved-machine-image.html",
"Terraform": ""
"CLI": "az vm create --resource-group <RESOURCE_GROUP> --name <VM_NAME> --image /subscriptions/<SUBSCRIPTION_ID>/resourceGroups/<RESOURCE_GROUP>/providers/Microsoft.Compute/images/<IMAGE_NAME> --admin-username azureuser --generate-ssh-keys",
"NativeIaC": "```bicep\n// Create a VM using an approved custom managed image\nparam location string = resourceGroup().location\nparam nicId string\nparam adminUsername string\nparam adminPassword string\n\nresource vm 'Microsoft.Compute/virtualMachines@2023-09-01' = {\n name: '<example_resource_name>'\n location: location\n properties: {\n hardwareProfile: { vmSize: 'Standard_DS1_v2' }\n storageProfile: {\n imageReference: {\n id: '/subscriptions/<subscription_id>/resourceGroups/<example_resource_id>/providers/Microsoft.Compute/images/<example_resource_name>' // CRITICAL: use managed image ID to pass check\n }\n osDisk: { createOption: 'FromImage' }\n }\n osProfile: {\n computerName: '<example_resource_name>'\n adminUsername: adminUsername\n adminPassword: adminPassword\n }\n networkProfile: {\n networkInterfaces: [{ id: nicId }]\n }\n }\n}\n```",
"Other": "1. In Azure Portal, go to Virtual machines > Create > Azure virtual machine\n2. Under Image, click See all images, then select the My Images tab\n3. Choose the approved managed image (type: Microsoft.Compute/images)\n4. Complete required basics and create the VM\n5. If replacing a non-compliant VM, migrate workload and delete the old VM",
"Terraform": "```hcl\n# VM created from an approved custom managed image\nresource \"azurerm_windows_virtual_machine\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n resource_group_name = \"<example_resource_id>\"\n location = \"<example_resource_location>\"\n size = \"Standard_DS1_v2\"\n admin_username = \"<example_resource_name>\"\n admin_password = \"<example_resource_id>\"\n network_interface_ids = [\"<example_resource_id>\"]\n\n source_image_id = \"/subscriptions/<subscription_id>/resourceGroups/<example_resource_id>/providers/Microsoft.Compute/images/<example_resource_name>\" # CRITICAL: managed image ID ensures PASS\n\n os_disk {\n caching = \"ReadWrite\"\n storage_account_type = \"Standard_LRS\"\n }\n}\n```"
},
"Recommendation": {
"Text": "Re-create the required VM instances using the approved (golden) machine image.",
"Url": "https://docs.microsoft.com/en-us/azure/virtual-machines/windows/create-vm-generalized-managed"
"Text": "Standardize on **golden images** maintained in an Azure Compute Gallery or managed images.\n- Harden and patch each release; scan for vulnerabilities\n- Restrict who can create/publish images (**least privilege**)\n- Enforce deployments only from approved images via policy\n- Version, sign, and retire images regularly",
"Url": "https://hub.prowler.com/check/vm_ensure_using_approved_images"
}
},
"Categories": [],
"Categories": [
"software-supply-chain"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "This check only validates if the VM was launched from a custom image. It does not validate the image content or security baseline."
@@ -1,30 +1,38 @@
{
"Provider": "azure",
"CheckID": "vm_ensure_using_managed_disks",
"CheckTitle": "Ensure Virtual Machines are utilizing Managed Disks",
"CheckTitle": "Virtual Machine uses managed disks for OS and data disks",
"CheckType": [],
"ServiceName": "vm",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "Microsoft.Compute/virtualMachines",
"Severity": "high",
"ResourceType": "microsoft.compute/virtualmachines",
"ResourceGroup": "compute",
"Description": "Migrate blob-based VHDs to Managed Disks on Virtual Machines to exploit the default features of this configuration. The features include: 1. Default Disk Encryption 2. Resilience, as Microsoft will managed the disk storage and move around if underlying hardware goes faulty 3. Reduction of costs over storage accounts",
"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": "https://learn.microsoft.com/en-us/azure/virtual-machines/unmanaged-disks-deprecation",
"Description": "**Azure virtual machines** use **managed disks** for the OS disk and every data disk, rather than page blob VHDs in storage accounts.\n\nThe evaluation confirms that all attached VM disks are managed.",
"Risk": "Using **unmanaged disks** ties VM data to storage account keys and SAS, increasing exposure of disk contents if those secrets leak (**confidentiality/integrity**). Limited platform resiliency and quotas increase **availability** risk. With Azure retiring unmanaged disks on `2026-03-31`, such VMs may be stopped and unable to start.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/azure/virtual-machines/unmanaged-disks-deprecation",
"https://learn.microsoft.com/en-us/azure/virtual-machines/windows/convert-unmanaged-to-managed-disks",
"https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/azure/VirtualMachines/managed-disks-in-use.html",
"https://learn.microsoft.com/en-us/security/benchmark/azure/security-controls-v3-data-protection#dp-4-enable-data-at-rest-encryption-by-default"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/VirtualMachines/managed-disks-in-use.html",
"Terraform": "https://docs.prowler.com/checks/azure/azure-general-policies/ensure-virtual-machines-are-utilizing-managed-disks#terraform"
"CLI": "az vm convert --resource-group <RESOURCE_GROUP> --name <VM_NAME>",
"NativeIaC": "```bicep\n// VM configured to use a managed OS disk\nresource vm 'Microsoft.Compute/virtualMachines@2023-09-01' = {\n name: '<example_resource_name>'\n location: resourceGroup().location\n properties: {\n hardwareProfile: { vmSize: 'Standard_DS1_v2' }\n storageProfile: {\n osDisk: {\n name: 'osdisk'\n osType: 'Linux'\n createOption: 'Attach'\n managedDisk: {\n id: '<example_resource_id>' // CRITICAL: attaching a managed disk makes the VM use managed disks\n }\n }\n }\n networkProfile: { networkInterfaces: [ { id: '<example_resource_id>' } ] }\n }\n}\n```",
"Other": "1. In Azure Portal, go to Virtual machines > select your VM\n2. Click Stop to deallocate the VM\n3. In the VM menu, select Disks\n4. Click Migrate to managed disks, then click Migrate to start\n5. After migration completes, click Start to power on the VM\n6. If the VM is in an availability set: first go to Availability sets > select the set > Convert to managed (SKU: Aligned), then repeat steps 1-5",
"Terraform": "```hcl\n# VM attached to a managed OS disk\nresource \"azurerm_virtual_machine\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n location = \"<LOCATION>\"\n resource_group_name = \"<RESOURCE_GROUP>\"\n network_interface_ids = [\"<example_resource_id>\"]\n vm_size = \"Standard_DS1_v2\"\n\n storage_os_disk {\n name = \"osdisk\"\n create_option = \"Attach\"\n managed_disk_id = \"<example_resource_id>\" # CRITICAL: use a managed disk for the OS disk\n }\n}\n```"
},
"Recommendation": {
"Text": "1. Using the search feature, go to Virtual Machines 2. Select the virtual machine you would like to convert 3. Select Disks in the menu for the VM 4. At the top select Migrate to managed disks 5. You may follow the prompts to convert the disk and finish by selecting Migrate to start the process",
"Url": "https://docs.microsoft.com/en-us/security/benchmark/azure/security-controls-v3-data-protection#dp-4-enable-data-at-rest-encryption-by-default"
"Text": "Adopt **managed disks** for all VM OS and data volumes.\n- Enforce **least privilege** via RBAC at disk scope\n- Use encryption at rest; prefer `CMEK` when mandated\n- Apply backups/snapshots and zone-aware designs for **defense in depth**\n- After migration, delete orphaned VHD blobs to avoid data exposure and cost.",
"Url": "https://hub.prowler.com/check/vm_ensure_using_managed_disks"
}
},
"Categories": [],
"Categories": [
"resilience"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "There are additional costs for managed disks based off of disk space allocated. When converting to managed disks, VMs will be powered off and back on."
@@ -1,30 +1,35 @@
{
"Provider": "azure",
"CheckID": "vm_jit_access_enabled",
"CheckTitle": "Enable Just-In-Time Access for Virtual Machines",
"CheckTitle": "Virtual Machine has Just-in-Time (JIT) access enabled",
"CheckType": [],
"ServiceName": "vm",
"SubServiceName": "",
"ResourceIdTemplate": "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Compute/virtualMachines/{vmName}",
"Severity": "high",
"ResourceType": "Microsoft.Compute/virtualMachines",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "microsoft.compute/virtualmachines",
"ResourceGroup": "compute",
"Description": "Ensure that Microsoft Azure virtual machines are configured to use Just-in-Time (JIT) access.",
"Risk": "Without JIT access, management ports such as 22 (SSH) and 3389 (RDP) may be exposed, increasing the risk of brute-force and DDoS attacks.",
"RelatedUrl": "https://docs.microsoft.com/en-us/azure/security-center/security-center-just-in-time?tabs=jit-config-asc%2Cjit-request-asc",
"Description": "**Azure virtual machines** are associated with a **Just-in-Time (JIT) network access policy** that opens management ports only for approved, time-bound requests from specified source IPs.",
"Risk": "Without **JIT**, management ports like `22`/`3389` may stay reachable, enabling:\n- brute-force and password-spray attempts\n- exploitation of remote access flaws or stolen keys\nThis threatens **confidentiality** (data exfiltration), **integrity** (unauthorized changes), and **availability** (service disruption).",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/azure/defender-for-cloud/enable-just-in-time-access?tabs=jit-config-asc%2Cjit-request-asc"
],
"Remediation": {
"Code": {
"CLI": "az security jit-policy list --query '[*].virtualMachines[*].id | []'",
"NativeIaC": "",
"Other": "",
"Terraform": ""
"CLI": "az rest --method PUT --url \"https://management.azure.com/subscriptions/<SUBSCRIPTION_ID>/resourceGroups/<RESOURCE_GROUP>/providers/Microsoft.Security/locations/<LOCATION>/jitNetworkAccessPolicies/default?api-version=2020-01-01\" --body '{\"kind\":\"Basic\",\"properties\":{\"virtualMachines\":[{\"id\":\"/subscriptions/<SUBSCRIPTION_ID>/resourceGroups/<RESOURCE_GROUP>/providers/Microsoft.Compute/virtualMachines/<VM_NAME>\",\"ports\":[{\"number\":22,\"protocol\":\"*\",\"allowedSourceAddressPrefix\":[\"*\"],\"maxRequestAccessDuration\":\"PT3H\"}]}]}}'",
"NativeIaC": "```bicep\n// Bicep: Enable JIT for a VM by creating a JIT policy that references the VM\nparam location string = \"<LOCATION>\"\nparam vmId string = \"/subscriptions/<SUBSCRIPTION_ID>/resourceGroups/<example_resource_name>/providers/Microsoft.Compute/virtualMachines/<example_resource_name>\"\n\nresource jit 'Microsoft.Security/locations/jitNetworkAccessPolicies@2020-01-01' = {\n name: '${location}/default'\n properties: {\n virtualMachines: [\n {\n id: vmId // Critical: Adding the VM ID enables JIT for this VM\n ports: [\n {\n number: 22\n protocol: '*'\n allowedSourceAddressPrefix: ['*']\n maxRequestAccessDuration: 'PT3H' // Critical: Minimal required port configuration for JIT\n }\n ]\n }\n ]\n }\n}\n```",
"Other": "1. In the Azure portal, go to Microsoft Defender for Cloud\n2. Select Workload protections > Just-in-time VM access\n3. Open the Not configured tab, select the VM, and click Enable JIT on VMs\n4. Keep the default port (22 for Linux or 3389 for Windows) and click Save",
"Terraform": "```hcl\n# Enable JIT by creating a Security Center JIT policy for the VM\nresource \"azurerm_security_center_jit_network_access_policy\" \"<example_resource_name>\" {\n name = \"default\"\n location = \"<LOCATION>\"\n resource_group_name = \"<example_resource_name>\"\n\n virtual_machines {\n id = \"<example_resource_id>\" # Critical: VM ID included in the JIT policy enables JIT for this VM\n\n ports {\n number = 22\n protocol = \"*\"\n allowed_source_address_prefixes = [\"*\"]\n max_request_access_duration = \"PT3H\" # Critical: Minimal port config required by JIT\n }\n }\n}\n```"
},
"Recommendation": {
"Text": "Enable Just-in-Time (JIT) network access for your Microsoft Azure virtual machines using the Azure Portal under Security Center > Just-in-time VM access.",
"Url": "https://docs.microsoft.com/en-us/azure/security-center/security-center-just-in-time?tabs=jit-config-asc%2Cjit-request-asc"
"Text": "Enable **JIT network access** and apply **least privilege** and **zero trust**:\n- keep admin ports closed by default\n- approve only specific IPs, minimal ports (e.g., `22`, `3389`), and short windows\n- favor **private access** (VPN, Bastion)\n- layer controls (**defense in depth**) and audit access requests",
"Url": "https://hub.prowler.com/check/vm_jit_access_enabled"
}
},
"Categories": [],
"Categories": [
"internet-exposed"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "JIT access can only be enabled via the Azure Portal. Ensure Security Center standard pricing tier for servers is enabled."
@@ -1,30 +1,36 @@
{
"Provider": "azure",
"CheckID": "vm_linux_enforce_ssh_authentication",
"CheckTitle": "Ensure SSH key authentication is enforced on Linux-based Virtual Machines",
"CheckTitle": "Linux Virtual Machine has password authentication disabled (SSH key authentication enforced)",
"CheckType": [],
"ServiceName": "vm",
"SubServiceName": "",
"ResourceIdTemplate": "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Compute/virtualMachines/{vmName}",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "Microsoft.Compute/virtualMachines",
"ResourceType": "microsoft.compute/virtualmachines",
"ResourceGroup": "compute",
"Description": "Ensure that Azure Linux-based virtual machines are configured to use SSH keys by disabling password authentication.",
"Risk": "Allowing password-based SSH authentication increases the risk of brute-force attacks and unauthorized access. Enforcing SSH key authentication ensures only users with the private key can access the VM.",
"RelatedUrl": "https://docs.microsoft.com/en-us/azure/virtual-machines/linux/create-ssh-keys-detailed",
"Description": "**Azure Linux virtual machines** are assessed for SSH configuration where `disablePasswordAuthentication` is set to `true`, allowing only **public key authentication** and disallowing interactive passwords.",
"Risk": "With **password-based SSH**, attackers can run brute-force or password-spray campaigns. A successful login grants remote shell, enabling command execution, data exfiltration, and lateral movement, degrading **confidentiality** and **integrity**.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/azure/VirtualMachines/ssh-authentication-type.html",
"https://learn.microsoft.com/en-us/azure/virtual-machines/linux/create-ssh-keys-detailed"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/VirtualMachines/ssh-authentication-type.html",
"Terraform": ""
"NativeIaC": "```bicep\n// Bicep snippet to enforce SSH key authentication on a Linux VM\nresource linuxVm 'Microsoft.Compute/virtualMachines@2024-03-01' = {\n name: '<example_resource_name>'\n location: '<example_location>'\n properties: {\n osProfile: {\n linuxConfiguration: {\n disablePasswordAuthentication: true // CRITICAL: Disables password-based SSH; enforces SSH key authentication\n }\n }\n }\n}\n```",
"Other": "1. In Azure Portal, go to your Linux VM > Settings > Export template\n2. Click Edit template\n3. Find properties.osProfile.linuxConfiguration and set disablePasswordAuthentication to true\n4. Ensure an SSH public key is provided (adminPassword must be absent)\n5. Click Save, then Review + create to deploy the update",
"Terraform": "```hcl\n# Minimal Terraform to enforce SSH key authentication on a Linux VM\nresource \"azurerm_linux_virtual_machine\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n resource_group_name = \"<example_resource_name>\"\n location = \"<example_location>\"\n size = \"Standard_B1s\"\n network_interface_ids = [\"<example_resource_id>\"]\n admin_username = \"azureuser\"\n\n disable_password_authentication = true # CRITICAL: Disables password-based SSH; enforces SSH key authentication\n\n admin_ssh_key { # Required when password auth is disabled\n username = \"azureuser\"\n public_key = \"<example_public_key>\"\n }\n\n os_disk {\n caching = \"ReadWrite\"\n storage_account_type = \"Standard_LRS\"\n }\n\n source_image_reference {\n publisher = \"Canonical\"\n offer = \"0001-com-ubuntu-server-focal\"\n sku = \"20_04-lts\"\n version = \"latest\"\n }\n}\n```"
},
"Recommendation": {
"Text": "Recreate Linux VMs with SSH key authentication enabled and password authentication disabled.",
"Url": "https://docs.microsoft.com/en-us/azure/virtual-machines/linux/create-ssh-keys-detailed"
"Text": "Enforce **key-only SSH** by setting `disablePasswordAuthentication` to `true` and disallowing OS password logins. Use strong, passphrase-protected keys with rotation; restrict SSH via NSGs and private access or **Azure Bastion**; enable JIT; and apply **least privilege**.",
"Url": "https://hub.prowler.com/check/vm_linux_enforce_ssh_authentication"
}
},
"Categories": [],
"Categories": [
"identity-access"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
@@ -1,30 +1,38 @@
{
"Provider": "azure",
"CheckID": "vm_scaleset_associated_with_load_balancer",
"CheckTitle": "VM Scale Set Is Associated With Load Balancer",
"CheckTitle": "Virtual Machine Scale Set is associated with a load balancer backend pool",
"CheckType": [],
"ServiceName": "vm",
"SubServiceName": "scaleset",
"ResourceIdTemplate": "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Compute/virtualMachineScaleSets/{vmScaleSetName}",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "Microsoft.Compute/virtualMachineScaleSets",
"ResourceType": "microsoft.compute/virtualmachinescalesets",
"ResourceGroup": "compute",
"Description": "Ensure that your Azure virtual machine scale sets are using load balancers for traffic distribution.",
"Risk": "Without load balancer integration, Azure virtual machine scale sets may experience reduced availability and potential service disruptions during traffic spikes or instance failures, leading to degraded user experience and potential business impact.",
"RelatedUrl": "https://learn.microsoft.com/en-us/azure/virtual-network/network-overview",
"Description": "**Azure VM scale sets** are associated with at least one **load balancer backend pool**.\n\nThe evaluation looks for a backend pool link on each scale set's network configuration.",
"Risk": "Without a load balancer, traffic targets instances directly, bypassing health-based distribution. This degrades **availability** (overloads, no failover) and **reliability** (dropped sessions, uneven scaling) and can amplify **DoS** impact by concentrating flows on fewer nodes, increasing outage risk.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/azure/virtual-network/network-overview",
"https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/azure/VirtualMachines/associated-load-balancers.html",
"https://learn.microsoft.com/ms-my/azure/load-balancer/load-balancer-multiple-virtual-machine-scale-set?tabs=azureportal",
"https://learn.microsoft.com/en-us/azure/load-balancer/load-balancer-overview"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/VirtualMachines/associated-load-balancers.html",
"Terraform": ""
"CLI": "az vmss update --resource-group <RESOURCE_GROUP> --name <VMSS_NAME> --add virtualMachineProfile.networkProfile.networkInterfaceConfigurations[0].ipConfigurations[0].loadBalancerBackendAddressPools '{\"id\":\"/subscriptions/<SUBSCRIPTION_ID>/resourceGroups/<RESOURCE_GROUP>/providers/Microsoft.Network/loadBalancers/<LB_NAME>/backendAddressPools/<BACKEND_POOL_NAME>\"}'",
"NativeIaC": "```bicep\n// Associate VMSS with a Load Balancer backend pool\nresource vmss 'Microsoft.Compute/virtualMachineScaleSets@2023-09-01' = {\n name: '<example_resource_name>'\n location: resourceGroup().location\n properties: {\n virtualMachineProfile: {\n networkProfile: {\n networkInterfaceConfigurations: [\n {\n name: 'nic'\n properties: {\n ipConfigurations: [\n {\n name: 'ipcfg'\n properties: {\n loadBalancerBackendAddressPools: [\n { id: '<example_resource_id>' } // CRITICAL: attach VMSS NIC IP config to LB backend pool\n ]\n }\n }\n ]\n }\n }\n ]\n }\n }\n }\n}\n```",
"Other": "1. In Azure Portal, go to Load balancers > select <LB_NAME>\n2. Under Settings, open Backend pools and select the target backend pool\n3. Click Add > IP configurations\n4. Choose Virtual machine scale set, select <VMSS_NAME> and its IP configuration\n5. Click Add, then Save to attach the scale set to the backend pool",
"Terraform": "```hcl\n# Attach VMSS to an existing Load Balancer backend pool\nresource \"azurerm_linux_virtual_machine_scale_set\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n resource_group_name = \"<example_resource_name>\"\n location = \"<example_location>\"\n sku = \"Standard_DS1_v2\"\n instances = 1\n\n admin_username = \"azureuser\"\n disable_password_authentication = true\n admin_ssh_key {\n username = \"azureuser\"\n public_key = \"<SSH_PUBLIC_KEY>\"\n }\n\n source_image_reference {\n publisher = \"Canonical\"\n offer = \"0001-com-ubuntu-server-jammy\"\n sku = \"22_04-lts\"\n version = \"latest\"\n }\n\n network_interface {\n name = \"nic\"\n primary = true\n ip_configuration {\n name = \"ipcfg\"\n primary = true\n subnet_id = \"<example_resource_id>\"\n load_balancer_backend_address_pool_ids = [\"<example_resource_id>\"] # CRITICAL: associates VMSS with LB backend pool\n }\n }\n}\n```"
},
"Recommendation": {
"Text": "Attach a load balancer to your Azure virtual machine scale set to ensure high availability and optimal traffic distribution.",
"Url": "https://docs.microsoft.com/en-us/azure/load-balancer/load-balancer-overview"
"Text": "Associate VM scale sets with a **Standard Load Balancer** backend pool to enforce health-probed, even distribution and seamless failover.\n\nDesign for **high availability**: run multiple instances across zones, enable autoscale, and apply **defense in depth** with limited exposure and NSG controls. *Prefer SKU `Standard` over legacy Basic.*",
"Url": "https://hub.prowler.com/check/vm_scaleset_associated_with_load_balancer"
}
},
"Categories": [],
"Categories": [
"resilience"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
@@ -1,30 +1,36 @@
{
"Provider": "azure",
"CheckID": "vm_scaleset_not_empty",
"CheckTitle": "Check for Empty Virtual Machine Scale Sets",
"CheckTitle": "Virtual Machine Scale Set has at least one VM instance",
"CheckType": [],
"ServiceName": "vm",
"SubServiceName": "scaleset",
"ResourceIdTemplate": "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Compute/virtualMachineScaleSets/{vmScaleSetName}",
"Severity": "low",
"ResourceType": "Microsoft.Compute/virtualMachineScaleSets",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "microsoft.compute/virtualmachinescalesets",
"ResourceGroup": "compute",
"Description": "Identify and remove empty virtual machine scale sets from your Azure cloud account.",
"Risk": "Empty virtual machine scale sets may incur unnecessary costs and complicate cloud resource management, impacting cost optimization and compliance.",
"RelatedUrl": "https://learn.microsoft.com/en-us/azure/virtual-machine-scale-sets/overview",
"Description": "**Azure VM scale sets** with `0` VM instances are identified as **empty**.",
"Risk": "Orphaned scale sets degrade governance and can hide **stale autoscale** or permissive identities. If reactivated, they may launch VMs from **outdated images**, exposing vulnerable services and enabling lateral movement, impacting **confidentiality** and **availability**. They also create inventory sprawl and unnecessary spend.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/azure/virtual-machine-scale-sets/overview",
"https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/azure/VirtualMachines/empty-vm-scale-sets.html"
],
"Remediation": {
"Code": {
"CLI": "az vmss delete --name <scale-set-name> --resource-group <resource-group>",
"CLI": "az vmss scale --resource-group <resource-group> --name <scale-set-name> --new-capacity 1",
"NativeIaC": "",
"Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/VirtualMachines/empty-vm-scale-sets.html",
"Terraform": ""
"Other": "1. In Azure Portal, go to Virtual machine scale sets and select <scale-set-name>\n2. Under Settings, click Scaling\n3. Set Instance count to 1\n4. Click Save",
"Terraform": "```hcl\n# Patch the scale set to have at least one instance\nresource \"azapi_update_resource\" \"<example_resource_name>\" {\n type = \"Microsoft.Compute/virtualMachineScaleSets@2024-03-01\"\n resource_id = \"<example_resource_id>\"\n\n body = jsonencode({\n sku = {\n capacity = 1 # Critical: ensures the scale set has at least one VM instance\n }\n })\n}\n```"
},
"Recommendation": {
"Text": "Remove empty Azure virtual machine scale sets to optimize costs and simplify management.",
"Url": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/VirtualMachines/empty-vm-scale-sets.html"
"Text": "Decommission empty scale sets or consolidate capacity. Apply **resource lifecycle** and **governance policies** to prevent drift; require tagging and owners, review regularly. Manage capacity through **IaC**, enforce **least privilege** on scale set identities, and use change control to avoid unintended autoscale activations. *If retained temporarily*, document purpose and review date.",
"Url": "https://hub.prowler.com/check/vm_scaleset_not_empty"
}
},
"Categories": [],
"Categories": [
"resilience"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
@@ -1,30 +1,36 @@
{
"Provider": "azure",
"CheckID": "vm_sufficient_daily_backup_retention_period",
"CheckTitle": "Ensure there is a sufficient daily backup retention period configured for Azure virtual machines.",
"CheckTitle": "Virtual Machine has a backup policy with a daily retention period meeting the configured minimum",
"CheckType": [],
"ServiceName": "vm",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "Microsoft.Compute/virtualMachines",
"ResourceType": "microsoft.compute/virtualmachines",
"ResourceGroup": "compute",
"Description": "Ensure there is a sufficient daily backup retention period configured for Azure virtual machines.",
"Risk": "Having an optimal daily backup retention period for your Azure virtual machines will enforce your backup strategy to follow the best practices as specified in the compliance regulations promoted by your organization. Retaining VM backups for a longer period of time will allow you to handle more efficiently your data restoration process in the event of a failure.",
"RelatedUrl": "https://docs.microsoft.com/en-us/azure/backup/backup-azure-vms-introduction",
"Description": "**Azure virtual machines** are evaluated for a backup policy in a Recovery Services vault with a **daily retention** period that meets the configured minimum. VMs lacking protection or using a shorter retention window are identified for review.",
"Risk": "**Insufficient or missing VM backups** weaken **availability** and **integrity**. Short retention reduces recovery points, limiting rollback after **ransomware**, accidental deletion, or faulty changes. This increases RPO, extends RTO, and can force rebuilds, causing data loss and downtime.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://www.trendmicro.com/trendaivisiononecloudriskmanagement/knowledge-base/azure/VirtualMachines/sufficient-backup-retention-period.html",
"https://learn.microsoft.com/en-us/azure/backup/backup-azure-vms-introduction"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/VirtualMachines/sufficient-backup-retention-period.html",
"Terraform": ""
"NativeIaC": "```bicep\n// Create/ensure a VM backup policy with daily retention >= minimum\nresource rv 'Microsoft.RecoveryServices/vaults@2023-01-01' existing = {\n name: '<example_resource_name>'\n}\n\nresource vmPolicy 'Microsoft.RecoveryServices/vaults/backupPolicies@2023-02-01' = {\n name: '${rv.name}/<example_resource_name>'\n properties: {\n backupManagementType: 'AzureIaasVM'\n schedulePolicy: {\n schedulePolicyType: 'SimpleSchedulePolicyV2'\n scheduleRunFrequency: 'Daily'\n scheduleRunTimes: ['2020-01-01T23:00:00Z']\n }\n retentionPolicy: {\n retentionPolicyType: 'LongTermRetentionPolicy'\n dailySchedule: {\n retentionTimes: ['2020-01-01T23:00:00Z']\n retentionDuration: {\n count: 7 // CRITICAL: sets daily retention to at least the minimum required days\n durationType: 'Days' // explains the unit\n }\n }\n }\n }\n}\n```",
"Other": "1. In Azure portal, go to Recovery Services vault <example_resource_name>\n2. Select Backup policies > Azure Virtual Machine > Edit (or Create new)\n3. Set Daily retention to at least 7 days and Save\n4. Go to Backup items > Azure Virtual Machine\n5. If the VM is unprotected: click Backup, select the policy from step 3, and Enable\n6. If the VM is already protected with an insufficient policy: select the VM > Change Policy > choose the policy from step 3 > Save",
"Terraform": "```hcl\n# Backup policy with sufficient daily retention\nresource \"azurerm_backup_policy_vm\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n resource_group_name = \"<example_resource_name>\"\n recovery_vault_name = \"<example_resource_name>\"\n\n backup {\n frequency = \"Daily\"\n time = \"23:00\"\n }\n\n retention_daily {\n count = 7 # CRITICAL: ensures daily retention meets minimum required days\n }\n}\n\n# Protect the VM with the compliant policy\nresource \"azurerm_backup_protected_vm\" \"<example_resource_name>\" {\n resource_group_name = \"<example_resource_name>\"\n recovery_vault_name = \"<example_resource_name>\"\n source_vm_id = \"<example_resource_id>\"\n backup_policy_id = azurerm_backup_policy_vm.<example_resource_name>.id # CRITICAL: applies the policy to the VM\n}\n```"
},
"Recommendation": {
"Text": "Set the daily backup retention period for each VM's backup policy to meet or exceed your organization's minimum requirement.",
"Url": "https://docs.microsoft.com/en-us/azure/backup/backup-azure-vms-introduction"
"Text": "Enforce backup policies that provide **daily retention** aligned to business RPO/RTO for every VM. Apply **defense in depth**: isolate backups in a vault, enable immutability/soft delete, limit changes with **least privilege** and MFA, and regularly test restores. Use tiered retention (daily/weekly/monthly/yearly) based on data criticality.",
"Url": "https://hub.prowler.com/check/vm_sufficient_daily_backup_retention_period"
}
},
"Categories": [],
"Categories": [
"resilience"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
@@ -1,30 +1,35 @@
{
"Provider": "azure",
"CheckID": "vm_trusted_launch_enabled",
"CheckTitle": "Ensure Trusted Launch is enabled on Virtual Machines",
"CheckTitle": "Virtual Machine has Trusted Launch with Secure Boot and vTPM enabled",
"CheckType": [],
"ServiceName": "vm",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "Microsoft.Compute/virtualMachines",
"Severity": "medium",
"ResourceType": "microsoft.compute/virtualmachines",
"ResourceGroup": "compute",
"Description": "When Secure Boot and vTPM are enabled together, they provide a strong foundation for protecting your VM from boot attacks. For example, if an attacker attempts to replace the bootloader with a malicious version, Secure Boot will prevent the VM from booting. If the attacker is able to bypass Secure Boot and install a malicious bootloader, vTPM can be used to detect the intrusion and alert you.",
"Risk": "Secure Boot and vTPM work together to protect your VM from a variety of boot attacks, including bootkits, rootkits, and firmware rootkits. Not enabling Trusted Launch in Azure VM can lead to increased vulnerability to rootkits and boot-level malware, reduced ability to detect and prevent unauthorized changes to the boot process, and a potential compromise of system integrity and data security.",
"RelatedUrl": "https://learn.microsoft.com/en-us/azure/virtual-machines/trusted-launch-existing-vm?tabs=portal",
"Description": "**Azure VMs** are evaluated for **Trusted Launch** with both **Secure Boot** and **vTPM** enabled.\n\nIdentifies VMs not set to `TrustedLaunch` or missing `secureBootEnabled` and `vTpmEnabled` together.",
"Risk": "Missing **Trusted Launch** weakens boot-chain integrity. Attackers can persist via bootkits/rootkits, bypass OS controls, steal secrets, and tamper with drivers. Loss of attestation reduces detection, risking **integrity**, **confidentiality**, and **availability** through stealthy, hard-to-remediate compromises.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/azure/virtual-machines/trusted-launch"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "",
"Terraform": ""
"CLI": "az vm update --resource-group <RESOURCE_GROUP> --name <VM_NAME> --security-type TrustedLaunch --enable-secure-boot true --enable-vtpm true",
"NativeIaC": "```bicep\n// Enables Trusted Launch with Secure Boot and vTPM on the VM\nresource vm 'Microsoft.Compute/virtualMachines@2022-11-01' = {\n name: '<example_resource_name>'\n location: '<region>'\n properties: {\n securityProfile: {\n securityType: 'TrustedLaunch' // Critical: sets VM security type to Trusted Launch\n uefiSettings: {\n secureBootEnabled: true // Critical: enables Secure Boot\n vTpmEnabled: true // Critical: enables vTPM\n }\n }\n }\n}\n```",
"Other": "1. In Azure Portal, open the VM and click Stop to deallocate it\n2. Go to Settings > Configuration\n3. Under Security type, select Trusted launch\n4. Check Secure Boot and vTPM\n5. Click Save\n6. Start the VM from the Overview page",
"Terraform": "```hcl\n# Patch the existing VM to enable Trusted Launch with Secure Boot and vTPM\nresource \"azapi_update_resource\" \"<example_resource_name>\" {\n type = \"Microsoft.Compute/virtualMachines@2022-11-01\"\n resource_id = \"<example_resource_id>\"\n\n body = jsonencode({\n properties = {\n securityProfile = {\n securityType = \"TrustedLaunch\" # Critical: sets VM security type to Trusted Launch\n uefiSettings = {\n secureBootEnabled = true # Critical: enables Secure Boot\n vTpmEnabled = true # Critical: enables vTPM\n }\n }\n }\n })\n}\n```"
},
"Recommendation": {
"Text": "1. Go to Virtual Machines 2. For each VM, under Settings, click on Configuration on the left blade 3. Under Security Type, select 'Trusted Launch Virtual Machines' 4. Make sure Enable Secure Boot & Enable vTPM are checked 5. Click on Apply.",
"Url": "https://learn.microsoft.com/en-us/azure/virtual-machines/trusted-launch-existing-vm?tabs=portal#enable-trusted-launch-on-existing-vm"
"Text": "Adopt **defense in depth**: enable **Trusted Launch** with **Secure Boot** and **vTPM** on Gen2 VMs. Standardize on images with signed boot components and use supported sizes/OS. Enforce **least privilege** for administrators and enable attestation monitoring to prevent and detect boot-level tampering.",
"Url": "https://hub.prowler.com/check/vm_trusted_launch_enabled"
}
},
"Categories": [],
"Categories": [
"node-security"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "Secure Boot and vTPM are not currently supported for Azure Generation 1 VMs. IMPORTANT: Before enabling Secure Boot and vTPM on a Generation 2 VM which does not already have both enabled, it is highly recommended to create a restore point of the VM prior to remediation."
@@ -285,6 +285,18 @@ class GithubProvider(Provider):
else:
app_key = format_rsa_key(github_app_key_content)
# Check for incomplete GitHub App credentials (user provided only part of them)
elif (github_app_key or github_app_key_content) and not github_app_id:
raise GithubEnvironmentVariableError(
file=os.path.basename(__file__),
message="GitHub App authentication requires both --github-app-id and --github-app-key-path (or --github-app-key). Missing: --github-app-id",
)
elif github_app_id and not (github_app_key or github_app_key_content):
raise GithubEnvironmentVariableError(
file=os.path.basename(__file__),
message="GitHub App authentication requires both --github-app-id and --github-app-key-path (or --github-app-key). Missing: --github-app-key-path or --github-app-key",
)
else:
# PAT
logger.info(
@@ -26,7 +26,10 @@ class repository_branch_delete_on_merge_enabled(Check):
report.status = "FAIL"
report.status_extended = f"Repository {repo.name} does not delete branches on merge in default branch ({repo.default_branch.name})."
if repo.delete_branch_on_merge:
if repo.delete_branch_on_merge is None:
report.status = "MANUAL"
report.status_extended = f"Repository {repo.name} branch deletion setting could not be checked in default branch ({repo.default_branch.name}) due to insufficient permissions. Requires Administration: Read and Write permission."
elif repo.delete_branch_on_merge:
report.status = "PASS"
report.status_extended = f"Repository {repo.name} does delete branches on merge in default branch ({repo.default_branch.name})."
@@ -241,11 +241,9 @@ class Repository(GithubService):
codeowners_exists = None
else:
codeowners_exists = False
delete_branch_on_merge = (
repo.delete_branch_on_merge
if repo.delete_branch_on_merge is not None
else False
)
# GitHub API only returns delete_branch_on_merge with Administration: Read and Write
# With Read-only permission, it returns None - set to None for MANUAL status
delete_branch_on_merge = repo.delete_branch_on_merge
require_pr = False
approval_cnt = 0
+1
View File
@@ -47,6 +47,7 @@ dependencies = [
"cryptography==44.0.3",
"dash==3.1.1",
"dash-bootstrap-components==2.0.3",
"defusedxml>=0.7.1",
"detect-secrets==1.5.0",
"dulwich==0.23.0",
"google-api-python-client==2.163.0",
+2
View File
@@ -53,6 +53,8 @@ Reusable patterns for common technologies:
| `nextjs-15` | App Router, Server Actions, streaming |
| `tailwind-4` | cn() utility, Tailwind 4 patterns |
| `playwright` | Page Object Model, selectors |
| `vitest` | Unit testing, React Testing Library |
| `tdd` | Test-Driven Development workflow |
| `pytest` | Fixtures, mocking, markers |
| `django-drf` | ViewSets, Serializers, Filters |
| `zod-4` | Zod 4 API patterns |
+371
View File
@@ -0,0 +1,371 @@
---
name: tdd
description: >
Test-Driven Development workflow for ALL Prowler components (UI, SDK, API).
Trigger: ALWAYS when implementing features, fixing bugs, or refactoring - regardless of component.
This is a MANDATORY workflow, not optional.
license: Apache-2.0
metadata:
author: prowler-cloud
version: "2.0"
scope: [root, ui, api, prowler]
auto_invoke:
- "Implementing feature"
- "Fixing bug"
- "Refactoring code"
- "Working on task"
- "Modifying component"
allowed-tools: Read, Edit, Write, Glob, Grep, Bash, Task
---
## TDD Cycle (MANDATORY)
```
+-----------------------------------------+
| RED -> GREEN -> REFACTOR |
| ^ | |
| +------------------------+ |
+-----------------------------------------+
```
**The question is NOT "should I write tests?" but "what tests do I need?"**
---
## The Three Laws of TDD
1. **No production code** until you have a failing test
2. **No more test** than necessary to fail
3. **No more code** than necessary to pass
---
## Detect Your Stack
Before starting, identify which component you're working on:
| Working in | Stack | Runner | Test pattern | Details |
|------------|-------|--------|-------------|---------|
| `ui/` | TypeScript / React | Vitest + RTL | `*.test.{ts,tsx}` (co-located) | See `vitest` skill |
| `prowler/` | Python | pytest + moto | `*_test.py` (suffix) in `tests/` | See `prowler-test-sdk` skill |
| `api/` | Python / Django | pytest + django | `test_*.py` (prefix) in `api/src/backend/**/tests/` | See `prowler-test-api` skill |
---
## Phase 0: Assessment (ALWAYS FIRST)
Before writing ANY code:
### UI (`ui/`)
```bash
# 1. Find existing tests
fd "*.test.tsx" ui/components/feature/
# 2. Check coverage
pnpm test:coverage -- components/feature/
# 3. Read existing tests
```
### SDK (`prowler/`)
```bash
# 1. Find existing tests
fd "*_test.py" tests/providers/aws/services/ec2/
# 2. Run specific test
poetry run pytest tests/providers/aws/services/ec2/ec2_ami_public/ -v
# 3. Read existing tests
```
### API (`api/`)
```bash
# 1. Find existing tests
fd "test_*.py" api/src/backend/api/tests/
# 2. Run specific test
poetry run pytest api/src/backend/api/tests/test_models.py -v
# 3. Read existing tests
```
### Decision Tree (All Stacks)
```
+------------------------------------------+
| Does test file exist for this code? |
+----------+-----------------------+-------+
| NO | YES
v v
+------------------+ +------------------+
| CREATE test file | | Check coverage |
| -> Phase 1: RED | | for your change |
+------------------+ +--------+---------+
|
+--------+--------+
| Missing cases? |
+---+---------+---+
| YES | NO
v v
+-----------+ +-----------+
| ADD tests | | Proceed |
| Phase 1 | | Phase 2 |
+-----------+ +-----------+
```
---
## Phase 1: RED - Write Failing Tests
### For NEW Functionality
**UI (Vitest)**
```typescript
describe("PriceCalculator", () => {
it("should return 0 for quantities below threshold", () => {
// Given
const quantity = 3;
// When
const result = calculateDiscount(quantity);
// Then
expect(result).toBe(0);
});
});
```
**SDK (pytest)**
```python
class Test_ec2_ami_public:
@mock_aws
def test_no_public_amis(self):
# Given - No AMIs exist
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
with mock.patch("prowler...ec2_service", new=EC2(aws_provider)):
from prowler...ec2_ami_public import ec2_ami_public
# When
check = ec2_ami_public()
result = check.execute()
# Then
assert len(result) == 0
```
**API (pytest-django)**
```python
@pytest.mark.django_db
class TestResourceModel:
def test_create_resource_with_tags(self, providers_fixture):
# Given
provider, *_ = providers_fixture
tenant_id = provider.tenant_id
# When
resource = Resource.objects.create(
tenant_id=tenant_id, provider=provider,
uid="arn:aws:ec2:us-east-1:123456789:instance/i-1234",
name="test", region="us-east-1", service="ec2", type="instance",
)
# Then
assert resource.uid == "arn:aws:ec2:us-east-1:123456789:instance/i-1234"
```
**Run -> MUST fail:** Test references code that doesn't exist yet.
### For BUG FIXES
Write a test that **reproduces the bug** first:
**UI:** `expect(() => render(<DatePicker value={null} />)).not.toThrow();`
**SDK:** `assert result[0].status == "FAIL" # Currently returns PASS incorrectly`
**API:** `assert response.status_code == 403 # Currently returns 200`
**Run -> Should FAIL (reproducing the bug)**
### For REFACTORING
Capture ALL current behavior BEFORE refactoring:
```
# Any stack: run ALL existing tests, they should PASS
# This is your safety net - if any fail after refactoring, you broke something
```
**Run -> All should PASS (baseline)**
---
## Phase 2: GREEN - Minimum Code
Write the MINIMUM code to make the test pass. Hardcoding is valid for the first test.
**UI:**
```typescript
// Test expects calculateDiscount(100, 10) === 10
function calculateDiscount() {
return 10; // FAKE IT - hardcoded is valid for first test
}
```
**Python (SDK/API):**
```python
# Test expects check.execute() returns 0 results
def execute(self):
return [] # FAKE IT - hardcoded is valid for first test
```
**This passes. But we're not done...**
---
## Phase 3: Triangulation (CRITICAL)
**One test allows faking. Multiple tests FORCE real logic.**
Add tests with different inputs that break the hardcoded value:
| Scenario | Required? |
|----------|-----------|
| Happy path | YES |
| Zero/empty values | YES |
| Boundary values | YES |
| Different valid inputs | YES (breaks fake) |
| Error conditions | YES |
**UI:**
```typescript
it("should calculate 10% discount", () => {
expect(calculateDiscount(100, 10)).toBe(10);
});
// ADD - breaks the fake:
it("should calculate 15% on 200", () => {
expect(calculateDiscount(200, 15)).toBe(30);
});
it("should return 0 for 0% rate", () => {
expect(calculateDiscount(100, 0)).toBe(0);
});
```
**Python:**
```python
def test_single_public_ami(self):
# Different input -> breaks hardcoded empty list
assert len(result) == 1
assert result[0].status == "FAIL"
def test_private_ami(self):
assert result[0].status == "PASS"
```
**Now fake BREAKS -> Real implementation required.**
---
## Phase 4: REFACTOR
Tests GREEN -> Improve code quality WITHOUT changing behavior.
- Extract functions/methods
- Improve naming
- Add types/validation
- Reduce duplication
**Run tests after EACH change -> Must stay GREEN**
---
## Quick Reference
```
+------------------------------------------------+
| TDD WORKFLOW |
+------------------------------------------------+
| 0. ASSESS: What tests exist? What's missing? |
| |
| 1. RED: Write ONE failing test |
| +-- Run -> Must fail with clear error |
| |
| 2. GREEN: Write MINIMUM code to pass |
| +-- Fake It is valid for first test |
| |
| 3. TRIANGULATE: Add tests that break the fake |
| +-- Different inputs, edge cases |
| |
| 4. REFACTOR: Improve with confidence |
| +-- Tests stay green throughout |
| |
| 5. REPEAT: Next behavior/requirement |
+------------------------------------------------+
```
---
## Anti-Patterns (NEVER DO)
```
# ANY language:
# 1. Code first, tests after
def new_feature(): ... # Then writing tests = USELESS
# 2. Skip triangulation
# Single test allows faking forever
# 3. Test implementation details
assert component.state.is_loading == True # BAD - test behavior, not internals
assert mock_service.call_count == 3 # BAD - brittle coupling
# 4. All tests at once before any code
# Write ONE test, make it pass, THEN write the next
# 5. Giant test methods
# Each test should verify ONE behavior
```
---
## Commands by Stack
### UI (`ui/`)
```bash
pnpm test # Watch mode
pnpm test:run # Single run (CI)
pnpm test:coverage # Coverage report
pnpm test ComponentName # Filter by name
```
### SDK (`prowler/`)
```bash
poetry run pytest tests/path/ -v # Run specific tests
poetry run pytest tests/path/ -v -k "test_name" # Filter by name
poetry run pytest -n auto tests/ # Parallel run
poetry run pytest --cov=./prowler tests/ # Coverage
```
### API (`api/`)
```bash
poetry run pytest -x --tb=short # Run all (stop on first fail)
poetry run pytest api/src/backend/api/tests/test_file.py # Specific file
poetry run pytest -k "test_name" -v # Filter by name
```
+201
View File
@@ -0,0 +1,201 @@
---
name: vitest
description: >
Vitest unit testing patterns with React Testing Library.
Trigger: When writing unit tests for React components, hooks, or utilities.
license: Apache-2.0
metadata:
author: prowler-cloud
version: "1.0"
scope: [root, ui]
auto_invoke:
- "Writing Vitest tests"
- "Writing React component tests"
- "Writing unit tests for UI"
- "Testing hooks or utilities"
allowed-tools: Read, Edit, Write, Glob, Grep, Bash, Task
---
> **For E2E tests**: Use `prowler-test-ui` skill (Playwright).
> This skill covers **unit/integration tests** with Vitest + React Testing Library.
## Test Structure (REQUIRED)
Use **Given/When/Then** (AAA) pattern with comments:
```typescript
it("should update user name when form is submitted", async () => {
// Given - Arrange
const user = userEvent.setup();
const onSubmit = vi.fn();
render(<UserForm onSubmit={onSubmit} />);
// When - Act
await user.type(screen.getByLabelText(/name/i), "John");
await user.click(screen.getByRole("button", { name: /submit/i }));
// Then - Assert
expect(onSubmit).toHaveBeenCalledWith({ name: "John" });
});
```
---
## Describe Block Organization
```typescript
describe("ComponentName", () => {
describe("when [condition]", () => {
it("should [expected behavior]", () => {});
});
});
```
**Group by behavior, NOT by method.**
---
## Query Priority (REQUIRED)
| Priority | Query | Use Case |
|----------|-------|----------|
| 1 | `getByRole` | Buttons, inputs, headings |
| 2 | `getByLabelText` | Form fields |
| 3 | `getByPlaceholderText` | Inputs without label |
| 4 | `getByText` | Static text |
| 5 | `getByTestId` | Last resort only |
```typescript
// ✅ GOOD
screen.getByRole("button", { name: /submit/i });
screen.getByLabelText(/email/i);
// ❌ BAD
container.querySelector(".btn-primary");
```
---
## userEvent over fireEvent (REQUIRED)
```typescript
// ✅ ALWAYS use userEvent
const user = userEvent.setup();
await user.click(button);
await user.type(input, "hello");
// ❌ NEVER use fireEvent for interactions
fireEvent.click(button);
```
---
## Async Testing Patterns
```typescript
// ✅ findBy for elements that appear async
const element = await screen.findByText(/loaded/i);
// ✅ waitFor for assertions
await waitFor(() => {
expect(screen.getByText(/success/i)).toBeInTheDocument();
});
// ✅ ONE assertion per waitFor
await waitFor(() => expect(mockFn).toHaveBeenCalled());
await waitFor(() => expect(screen.getByText(/done/i)).toBeVisible());
// ❌ NEVER multiple assertions in waitFor
await waitFor(() => {
expect(mockFn).toHaveBeenCalled();
expect(screen.getByText(/done/i)).toBeVisible(); // Slower failures
});
```
---
## Mocking
```typescript
// Basic mock
const handleClick = vi.fn();
// Mock with return value
const fetchUser = vi.fn().mockResolvedValue({ name: "John" });
// Always clean up
afterEach(() => {
vi.restoreAllMocks();
});
```
### vi.spyOn vs vi.mock
| Method | When to Use |
|--------|-------------|
| `vi.spyOn` | Observe without replacing (PREFERRED) |
| `vi.mock` | Replace entire module (use sparingly) |
---
## Common Matchers
```typescript
// Presence
expect(element).toBeInTheDocument();
expect(element).toBeVisible();
// State
expect(button).toBeDisabled();
expect(input).toHaveValue("text");
expect(checkbox).toBeChecked();
// Content
expect(element).toHaveTextContent(/hello/i);
expect(element).toHaveAttribute("href", "/home");
// Functions
expect(fn).toHaveBeenCalledWith(arg1, arg2);
expect(fn).toHaveBeenCalledTimes(2);
```
---
## What NOT to Test
```typescript
// ❌ Internal state
expect(component.state.isLoading).toBe(true);
// ❌ Third-party libraries
expect(axios.get).toHaveBeenCalled();
// ❌ Static content (unless conditional)
expect(screen.getByText("Welcome")).toBeInTheDocument();
// ✅ User-visible behavior
expect(screen.getByRole("button")).toBeDisabled();
```
---
## File Organization
```
components/
├── Button/
│ ├── Button.tsx
│ ├── Button.test.tsx # Co-located
│ └── index.ts
```
---
## Commands
```bash
pnpm test # Watch mode
pnpm test:run # Single run
pnpm test:coverage # With coverage
pnpm test Button # Filter by name
```
@@ -97,8 +97,8 @@ class TestM365CIS:
assert output_data_manual.Provider == "m365"
assert output_data_manual.Framework == CIS_4_0_M365.Framework
assert output_data_manual.Name == CIS_4_0_M365.Name
assert output_data_manual.TenantId == TENANT_ID
assert output_data_manual.Location == LOCATION
assert output_data_manual.TenantId == ""
assert output_data_manual.Location == ""
assert output_data_manual.Description == CIS_4_0_M365.Description
assert output_data_manual.Requirements_Id == CIS_4_0_M365.Requirements[1].Id
assert (
@@ -184,6 +184,6 @@ class TestM365CIS:
mock_file.seek(0)
content = mock_file.read()
expected_csv = f"PROVIDER;DESCRIPTION;TENANTID;LOCATION;ASSESSMENTDATE;REQUIREMENTS_ID;REQUIREMENTS_DESCRIPTION;REQUIREMENTS_ATTRIBUTES_SECTION;REQUIREMENTS_ATTRIBUTES_SUBSECTION;REQUIREMENTS_ATTRIBUTES_PROFILE;REQUIREMENTS_ATTRIBUTES_ASSESSMENTSTATUS;REQUIREMENTS_ATTRIBUTES_DESCRIPTION;REQUIREMENTS_ATTRIBUTES_RATIONALESTATEMENT;REQUIREMENTS_ATTRIBUTES_IMPACTSTATEMENT;REQUIREMENTS_ATTRIBUTES_REMEDIATIONPROCEDURE;REQUIREMENTS_ATTRIBUTES_AUDITPROCEDURE;REQUIREMENTS_ATTRIBUTES_ADDITIONALINFORMATION;REQUIREMENTS_ATTRIBUTES_DEFAULTVALUE;REQUIREMENTS_ATTRIBUTES_REFERENCES;STATUS;STATUSEXTENDED;RESOURCEID;RESOURCENAME;CHECKID;MUTED;FRAMEWORK;NAME\r\nm365;The CIS Microsoft 365 Foundations Benchmark provides prescriptive guidance for configuring security options for Microsoft 365 with an emphasis on foundational, testable, and architecture agnostic settings.;00000000-0000-0000-0000-000000000000;global;{datetime.now()};2.1.3;Ensure MFA Delete is enabled on S3 buckets;2.1. Simple Storage Service (S3);;Level 1;Automated;Once MFA Delete is enabled on your sensitive and classified S3 bucket it requires the user to have two forms of authentication.;Adding MFA delete to an S3 bucket, requires additional authentication when you change the version state of your bucket or you delete and object version adding another layer of security in the event your security credentials are compromised or unauthorized access is granted.;;Perform the steps below to enable MFA delete on an S3 bucket.Note:-You cannot enable MFA Delete using the AWS Management Console. You must use the AWS CLI or API.-You must use your 'root' account to enable MFA Delete on S3 buckets.**From Command line:**1. Run the s3api put-bucket-versioning command aws s3api put-bucket-versioning --profile my-root-profile --bucket Bucket_Name --versioning-configuration Status=Enabled,MFADelete=Enabled --mfa arn:aws:iam::aws_account_id:mfa/root-account-mfa-device passcode;Perform the steps below to confirm MFA delete is configured on an S3 Bucket**From Console:**1. Login to the S3 console at `https://console.aws.amazon.com/s3/`2. Click the `Check` box next to the Bucket name you want to confirm3. In the window under `Properties`4. Confirm that Versioning is `Enabled`5. Confirm that MFA Delete is `Enabled`**From Command Line:**1. Run the `get-bucket-versioning aws s3api get-bucket-versioning --bucket my-bucket Output example: <VersioningConfiguration xmlns=`http://s3.amazonaws.com/doc/2006-03-01/`> <Status>Enabled</Status> <MfaDelete>Enabled</MfaDelete></VersioningConfiguration>\ If the Console or the CLI output does not show Versioning and MFA Delete `enabled` refer to the remediation below.;;By default, MFA Delete is not enabled on S3 buckets.;https://docs.aws.amazon.com/AmazonS3/latest/dev/Versioning.html#MultiFactorAuthenticationDelete:https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingMFADelete.html:https://aws.amazon.com/blogs/security/securing-access-to-aws-using-mfa-part-3/:https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_mfa_lost-or-broken.html;PASS;;;;service_test_check_id;False;CIS;CIS Microsoft 365 Foundations Benchmark v4.0.0\r\nm365;The CIS Microsoft 365 Foundations Benchmark provides prescriptive guidance for configuring security options for Microsoft 365 with an emphasis on foundational, testable, and architecture agnostic settings.;00000000-0000-0000-0000-000000000000;global;{datetime.now()};2.1.4;Ensure that the controller manager pod specification file permissions are set to 600 or more restrictive;1.1 Control Plane Node Configuration Files;;Level 1;Automated;Ensure that the controller manager pod specification file has permissions of `600` or more restrictive.;The controller manager pod specification file controls various parameters that set the behavior of the Controller Manager on the master node. You should restrict its file permissions to maintain the integrity of the file. The file should be writable by only the administrators on the system.;;Run the below command (based on the file location on your system) on the Control Plane node. For example, ``` chmod 600 /etc/kubernetes/manifests/kube-controller-manager.yaml ```;Run the below command (based on the file location on your system) on the Control Plane node. For example, ``` stat -c %a /etc/kubernetes/manifests/kube-controller-manager.yaml ``` Verify that the permissions are `600` or more restrictive.;;By default, the `kube-controller-manager.yaml` file has permissions of `640`.;https://kubernetes.io/docs/admin/kube-apiserver/;MANUAL;Manual check;manual_check;Manual check;manual;False;CIS;CIS Microsoft 365 Foundations Benchmark v4.0.0\r\n"
expected_csv = f"PROVIDER;DESCRIPTION;TENANTID;LOCATION;ASSESSMENTDATE;REQUIREMENTS_ID;REQUIREMENTS_DESCRIPTION;REQUIREMENTS_ATTRIBUTES_SECTION;REQUIREMENTS_ATTRIBUTES_SUBSECTION;REQUIREMENTS_ATTRIBUTES_PROFILE;REQUIREMENTS_ATTRIBUTES_ASSESSMENTSTATUS;REQUIREMENTS_ATTRIBUTES_DESCRIPTION;REQUIREMENTS_ATTRIBUTES_RATIONALESTATEMENT;REQUIREMENTS_ATTRIBUTES_IMPACTSTATEMENT;REQUIREMENTS_ATTRIBUTES_REMEDIATIONPROCEDURE;REQUIREMENTS_ATTRIBUTES_AUDITPROCEDURE;REQUIREMENTS_ATTRIBUTES_ADDITIONALINFORMATION;REQUIREMENTS_ATTRIBUTES_DEFAULTVALUE;REQUIREMENTS_ATTRIBUTES_REFERENCES;STATUS;STATUSEXTENDED;RESOURCEID;RESOURCENAME;CHECKID;MUTED;FRAMEWORK;NAME\r\nm365;The CIS Microsoft 365 Foundations Benchmark provides prescriptive guidance for configuring security options for Microsoft 365 with an emphasis on foundational, testable, and architecture agnostic settings.;00000000-0000-0000-0000-000000000000;global;{datetime.now()};2.1.3;Ensure MFA Delete is enabled on S3 buckets;2.1. Simple Storage Service (S3);;Level 1;Automated;Once MFA Delete is enabled on your sensitive and classified S3 bucket it requires the user to have two forms of authentication.;Adding MFA delete to an S3 bucket, requires additional authentication when you change the version state of your bucket or you delete and object version adding another layer of security in the event your security credentials are compromised or unauthorized access is granted.;;Perform the steps below to enable MFA delete on an S3 bucket.Note:-You cannot enable MFA Delete using the AWS Management Console. You must use the AWS CLI or API.-You must use your 'root' account to enable MFA Delete on S3 buckets.**From Command line:**1. Run the s3api put-bucket-versioning command aws s3api put-bucket-versioning --profile my-root-profile --bucket Bucket_Name --versioning-configuration Status=Enabled,MFADelete=Enabled --mfa arn:aws:iam::aws_account_id:mfa/root-account-mfa-device passcode;Perform the steps below to confirm MFA delete is configured on an S3 Bucket**From Console:**1. Login to the S3 console at `https://console.aws.amazon.com/s3/`2. Click the `Check` box next to the Bucket name you want to confirm3. In the window under `Properties`4. Confirm that Versioning is `Enabled`5. Confirm that MFA Delete is `Enabled`**From Command Line:**1. Run the `get-bucket-versioning aws s3api get-bucket-versioning --bucket my-bucket Output example: <VersioningConfiguration xmlns=`http://s3.amazonaws.com/doc/2006-03-01/`> <Status>Enabled</Status> <MfaDelete>Enabled</MfaDelete></VersioningConfiguration>\ If the Console or the CLI output does not show Versioning and MFA Delete `enabled` refer to the remediation below.;;By default, MFA Delete is not enabled on S3 buckets.;https://docs.aws.amazon.com/AmazonS3/latest/dev/Versioning.html#MultiFactorAuthenticationDelete:https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingMFADelete.html:https://aws.amazon.com/blogs/security/securing-access-to-aws-using-mfa-part-3/:https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_mfa_lost-or-broken.html;PASS;;;;service_test_check_id;False;CIS;CIS Microsoft 365 Foundations Benchmark v4.0.0\r\nm365;The CIS Microsoft 365 Foundations Benchmark provides prescriptive guidance for configuring security options for Microsoft 365 with an emphasis on foundational, testable, and architecture agnostic settings.;;;{datetime.now()};2.1.4;Ensure that the controller manager pod specification file permissions are set to 600 or more restrictive;1.1 Control Plane Node Configuration Files;;Level 1;Automated;Ensure that the controller manager pod specification file has permissions of `600` or more restrictive.;The controller manager pod specification file controls various parameters that set the behavior of the Controller Manager on the master node. You should restrict its file permissions to maintain the integrity of the file. The file should be writable by only the administrators on the system.;;Run the below command (based on the file location on your system) on the Control Plane node. For example, ``` chmod 600 /etc/kubernetes/manifests/kube-controller-manager.yaml ```;Run the below command (based on the file location on your system) on the Control Plane node. For example, ``` stat -c %a /etc/kubernetes/manifests/kube-controller-manager.yaml ``` Verify that the permissions are `600` or more restrictive.;;By default, the `kube-controller-manager.yaml` file has permissions of `640`.;https://kubernetes.io/docs/admin/kube-apiserver/;MANUAL;Manual check;manual_check;Manual check;manual;False;CIS;CIS Microsoft 365 Foundations Benchmark v4.0.0\r\n"
assert content == expected_csv
@@ -74,3 +74,26 @@ def test_list_buckets_respects_audit_filters():
oss._list_buckets()
assert list(oss.buckets.keys()) == []
def test_list_buckets_rejects_xxe_payload():
oss = _build_oss_service()
xxe_payload = """<?xml version="1.0"?>
<!DOCTYPE data [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<ListAllMyBucketsResult>
<Buckets>
<Bucket>
<Name>&xxe;</Name>
<CreationDate>2025-01-01T00:00:00.000Z</CreationDate>
<Location>oss-cn-hangzhou</Location>
</Bucket>
</Buckets>
</ListAllMyBucketsResult>"""
with patch("requests.get") as get_mock:
get_mock.return_value = MagicMock(status_code=200, text=xxe_payload)
oss._list_buckets()
assert oss.buckets == {}
@@ -149,3 +149,64 @@ class Test_repository_branch_delete_on_merge_enabled_test:
result[0].status_extended
== f"Repository {repo_name} does delete branches on merge in default branch ({default_branch})."
)
def test_branch_deletion_insufficient_permissions(self):
repository_client = mock.MagicMock
repo_name = "repo3"
default_branch = "main"
repository_client.repositories = {
3: Repo(
id=3,
name=repo_name,
owner="account-name",
full_name="account-name/repo3",
default_branch=Branch(
name=default_branch,
protected=False,
default_branch=True,
require_pull_request=False,
approval_count=0,
required_linear_history=False,
allow_force_pushes=True,
branch_deletion=True,
status_checks=False,
enforce_admins=False,
require_code_owner_reviews=False,
require_signed_commits=False,
conversation_resolution=False,
),
private=False,
archived=False,
pushed_at=datetime.now(timezone.utc),
securitymd=False,
codeowners_exists=False,
secret_scanning_enabled=False,
dependabot_alerts_enabled=False,
delete_branch_on_merge=None, # Insufficient permissions
),
}
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_github_provider(),
),
mock.patch(
"prowler.providers.github.services.repository.repository_branch_delete_on_merge_enabled.repository_branch_delete_on_merge_enabled.repository_client",
new=repository_client,
),
):
from prowler.providers.github.services.repository.repository_branch_delete_on_merge_enabled.repository_branch_delete_on_merge_enabled import (
repository_branch_delete_on_merge_enabled,
)
check = repository_branch_delete_on_merge_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].resource_id == 3
assert result[0].resource_name == repo_name
assert result[0].status == "MANUAL"
assert (
result[0].status_extended
== f"Repository {repo_name} branch deletion setting could not be checked in default branch ({default_branch}) due to insufficient permissions. Requires Administration: Read and Write permission."
)
+55
View File
@@ -159,6 +159,61 @@ else
exit 1
fi
# Run unit tests (targeted based on staged files)
echo -e "${BLUE}🧪 Running unit tests...${NC}"
echo ""
# Get staged source files (exclude test files)
# Note: we already cd'd into ui/, so pathspecs are relative (no ui/ prefix)
STAGED_SOURCE_FILES=$(git diff --cached --name-only --diff-filter=ACM -- '*.ts' '*.tsx' | grep -v '\.test\.\|\.spec\.\|vitest\.config\|vitest\.setup' || true)
# Check if critical paths changed (lib/, types/, config/)
CRITICAL_PATHS_CHANGED=$(git diff --cached --name-only -- 'lib/**' 'types/**' 'config/**' 'middleware.ts' 'vitest.config.ts' 'vitest.setup.ts' || true)
if [ -n "$CRITICAL_PATHS_CHANGED" ]; then
echo -e "${YELLOW}Critical paths changed - running ALL unit tests${NC}"
if pnpm run test:run; then
echo ""
echo -e "${GREEN}✅ Unit tests passed${NC}"
echo ""
else
echo ""
echo -e "${RED}❌ Unit tests failed${NC}"
echo -e "${RED}Fix failing tests before committing${NC}"
echo ""
exit 1
fi
elif [ -n "$STAGED_SOURCE_FILES" ]; then
echo -e "${YELLOW}Running tests related to changed files:${NC}"
echo "$STAGED_SOURCE_FILES" | while IFS= read -r file; do [ -n "$file" ] && echo " - $file"; done
echo ""
# shellcheck disable=SC2086 # Word splitting is intentional - vitest needs each file as separate arg
if pnpm exec vitest related $STAGED_SOURCE_FILES --run; then
echo ""
echo -e "${GREEN}✅ Unit tests passed${NC}"
echo ""
else
echo ""
echo -e "${RED}❌ Unit tests failed${NC}"
echo -e "${RED}Fix failing tests before committing${NC}"
echo ""
exit 1
fi
else
echo -e "${YELLOW}No source files changed - running ALL unit tests${NC}"
if pnpm run test:run; then
echo ""
echo -e "${GREEN}✅ Unit tests passed${NC}"
echo ""
else
echo ""
echo -e "${RED}❌ Unit tests failed${NC}"
echo -e "${RED}Fix failing tests before committing${NC}"
echo ""
exit 1
fi
fi
# Run build
echo -e "${BLUE}🔨 Running build...${NC}"
echo ""
+11
View File
@@ -11,6 +11,8 @@
> - [`zustand-5`](../skills/zustand-5/SKILL.md) - Selectors, persist middleware
> - [`ai-sdk-5`](../skills/ai-sdk-5/SKILL.md) - UIMessage, sendMessage
> - [`playwright`](../skills/playwright/SKILL.md) - Page Object Model, selectors
> - [`vitest`](../skills/vitest/SKILL.md) - Unit testing with React Testing Library
> - [`tdd`](../skills/tdd/SKILL.md) - TDD workflow (MANDATORY for UI tasks)
### Auto-invoke Skills
@@ -26,16 +28,25 @@ When performing these actions, ALWAYS invoke the corresponding skill FIRST:
| Creating Zod schemas | `zod-4` |
| Creating a git commit | `prowler-commit` |
| Creating/modifying Prowler UI components | `prowler-ui` |
| Fixing bug | `tdd` |
| Implementing feature | `tdd` |
| Modifying component | `tdd` |
| Refactoring code | `tdd` |
| Review changelog format and conventions | `prowler-changelog` |
| Testing hooks or utilities | `vitest` |
| Update CHANGELOG.md in any component | `prowler-changelog` |
| Using Zustand stores | `zustand-5` |
| Working on Prowler UI structure (actions/adapters/types/hooks) | `prowler-ui` |
| Working on task | `tdd` |
| Working with Prowler UI test helpers/pages | `prowler-test-ui` |
| Working with Tailwind classes | `tailwind-4` |
| Writing Playwright E2E tests | `playwright` |
| Writing Prowler UI E2E tests | `prowler-test-ui` |
| Writing React component tests | `vitest` |
| Writing React components | `react-19` |
| Writing TypeScript types/interfaces | `typescript` |
| Writing Vitest tests | `vitest` |
| Writing unit tests for UI | `vitest` |
---
+21 -1
View File
@@ -4,10 +4,17 @@ All notable changes to the **Prowler UI** are documented in this file.
## [1.19.0] (Prowler UNRELEASED)
### 🚀 Added
- PDF report available for the CSA CCM compliance framework [(#10088)](https://github.com/prowler-cloud/prowler/pull/10088)
- CSV and PDF download buttons in compliance views [(#10093)](https://github.com/prowler-cloud/prowler/pull/10093)
### 🔄 Changed
- Attack Paths: Query list now shows their name and short description, when one is selected it also shows a longer description and an attribution if it has it [(#9983)](https://github.com/prowler-cloud/prowler/pull/9983)
- Updated GitHub provider form placeholder to clarify both username and organization names are valid inputs [(#9830)](https://github.com/prowler-cloud/prowler/pull/9830)
- CSA CCM detailed view and small fix related with `Top Failed Sections` width [(#10018)](https://github.com/prowler-cloud/prowler/pull/10018)
- Attack Paths: Show scan data availability status with badges and tooltips, allow selecting scans for querying while a new scan is in progress [(#10089)](https://github.com/prowler-cloud/prowler/pull/10089)
### 🔐 Security
@@ -15,7 +22,16 @@ All notable changes to the **Prowler UI** are documented in this file.
---
## [1.18.2] (Prowler UNRELEASED)
## [1.18.3] (Prowler UNRELEASED)
### 🐞 Fixed
- Dropdown selects in the "Send to Jira" modal and other dialogs not responding to clicks [(#10097)](https://github.com/prowler-cloud/prowler/pull/10097)
- Update credentials for the Alibaba Cloud provider [(#10098)](https://github.com/prowler-cloud/prowler/pull/10098)
---
## [1.18.2] (Prowler v5.18.2)
### 🐞 Fixed
@@ -38,6 +54,10 @@ All notable changes to the **Prowler UI** are documented in this file.
## [1.18.0] (Prowler v5.18.0)
### 🚀 Added
- Setup Vitest with React Testing Library for unit testing with targeted test execution [(#9925)](https://github.com/prowler-cloud/prowler/pull/9925)
### 🔄 Changed
- Restyle resources view with improved resource detail drawer [(#9864)](https://github.com/prowler-cloud/prowler/pull/9864)
@@ -30,7 +30,7 @@ import {
} from "@/components/ui/table";
import { cn } from "@/lib/utils";
import type { ProviderType } from "@/types";
import type { AttackPathScan } from "@/types/attack-paths";
import type { AttackPathScan, ScanState } from "@/types/attack-paths";
import { SCAN_STATES } from "@/types/attack-paths";
import { ScanStatusBadge } from "./scan-status-badge";
@@ -42,6 +42,11 @@ interface ScanListTableProps {
const TABLE_COLUMN_COUNT = 6;
const DEFAULT_PAGE_SIZE = 5;
const PAGE_SIZE_OPTIONS = [2, 5, 10, 15];
const WAITING_STATES: readonly ScanState[] = [
SCAN_STATES.SCHEDULED,
SCAN_STATES.AVAILABLE,
SCAN_STATES.EXECUTING,
];
const baseLinkClass =
"relative block rounded border-0 bg-transparent px-3 py-1.5 text-button-primary outline-none transition-all duration-300 hover:bg-bg-neutral-tertiary hover:text-text-neutral-primary focus:shadow-none dark:hover:bg-bg-neutral-secondary dark:hover:text-text-neutral-primary";
@@ -77,20 +82,17 @@ export const ScanListTable = ({ scans }: ScanListTableProps) => {
};
const isSelectDisabled = (scan: AttackPathScan) => {
return (
scan.attributes.state !== SCAN_STATES.COMPLETED ||
selectedScanId === scan.id
);
return !scan.attributes.graph_data_ready || selectedScanId === scan.id;
};
const getSelectButtonLabel = (scan: AttackPathScan) => {
if (selectedScanId === scan.id) {
return "Selected";
}
if (scan.attributes.state === SCAN_STATES.SCHEDULED) {
return "Scheduled";
if (scan.attributes.graph_data_ready) {
return "Select";
}
if (scan.attributes.state === SCAN_STATES.EXECUTING) {
if (WAITING_STATES.includes(scan.attributes.state)) {
return "Waiting...";
}
if (scan.attributes.state === SCAN_STATES.FAILED) {
@@ -184,6 +186,7 @@ export const ScanListTable = ({ scans }: ScanListTableProps) => {
<ScanStatusBadge
status={scan.attributes.state}
progress={scan.attributes.progress}
graphDataReady={scan.attributes.graph_data_ready}
/>
</TableCell>
<TableCell>
@@ -342,8 +345,8 @@ export const ScanListTable = ({ scans }: ScanListTableProps) => {
)}
</div>
<p className="text-text-neutral-secondary dark:text-text-neutral-secondary mt-6 text-xs">
Only Attack Paths scans with &quot;Completed&quot; status can be
selected. Scans in progress will update automatically.
Scans can be selected when data is available. A new scan does not
interrupt access to existing data.
</p>
</>
);
@@ -3,57 +3,94 @@
import { Loader2 } from "lucide-react";
import { Badge } from "@/components/shadcn/badge/badge";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/shadcn/tooltip";
import type { ScanState } from "@/types/attack-paths";
import { SCAN_STATES } from "@/types/attack-paths";
const BADGE_CONFIG: Record<
ScanState,
{ className: string; label: string; showGraphDot: boolean }
> = {
[SCAN_STATES.SCHEDULED]: {
className: "bg-bg-neutral-tertiary text-text-neutral-primary",
label: "Scheduled",
showGraphDot: true,
},
[SCAN_STATES.AVAILABLE]: {
className: "bg-bg-neutral-tertiary text-text-neutral-primary",
label: "Queued",
showGraphDot: true,
},
[SCAN_STATES.EXECUTING]: {
className: "bg-bg-warning-secondary text-text-neutral-primary",
label: "In Progress",
showGraphDot: false,
},
[SCAN_STATES.COMPLETED]: {
className: "bg-bg-pass-secondary text-text-success-primary",
label: "Completed",
showGraphDot: false,
},
[SCAN_STATES.FAILED]: {
className: "bg-bg-fail-secondary text-text-error-primary",
label: "Failed",
showGraphDot: true,
},
};
interface ScanStatusBadgeProps {
status: ScanState;
progress?: number;
graphDataReady?: boolean;
}
/**
* Status badge for attack path scan status
* Shows visual indicator and text for scan progress
*/
export const ScanStatusBadge = ({
status,
progress = 0,
graphDataReady = false,
}: ScanStatusBadgeProps) => {
if (status === "scheduled") {
return (
<Badge className="bg-bg-neutral-tertiary text-text-neutral-primary gap-2">
<span>Scheduled</span>
</Badge>
);
}
const config = BADGE_CONFIG[status];
if (status === "available") {
return (
<Badge className="bg-bg-neutral-tertiary text-text-neutral-primary gap-2">
<span>Queued</span>
</Badge>
);
}
const graphDot = graphDataReady && config.showGraphDot && (
<span className="inline-block size-2 rounded-full bg-green-500" />
);
if (status === "executing") {
return (
<Badge className="bg-bg-warning-secondary text-text-neutral-primary gap-2">
<Loader2 size={14} className="animate-spin" />
<span>In Progress ({progress}%)</span>
</Badge>
);
}
const tooltipText = graphDataReady
? "Graph available"
: status === SCAN_STATES.FAILED || status === SCAN_STATES.COMPLETED
? "Graph not available"
: "Graph not available yet";
if (status === "completed") {
return (
<Badge className="bg-bg-pass-secondary text-text-success-primary gap-2">
<span>Completed</span>
</Badge>
const icon =
status === SCAN_STATES.EXECUTING ? (
<Loader2
size={14}
className={
graphDataReady ? "animate-spin text-green-500" : "animate-spin"
}
/>
) : (
graphDot
);
}
const label =
status === SCAN_STATES.EXECUTING
? `${config.label} (${progress}%)`
: config.label;
return (
<Badge className="bg-bg-fail-secondary text-text-error-primary gap-2">
<span>Failed</span>
</Badge>
<Tooltip>
<TooltipTrigger asChild>
<Badge className={`${config.className} gap-2`}>
{icon}
<span>{label}</span>
</Badge>
</TooltipTrigger>
<TooltipContent>{tooltipText}</TooltipContent>
</Tooltip>
);
};
@@ -9,7 +9,7 @@ import {
import { getThreatScore } from "@/actions/overview";
import {
ClientAccordionWrapper,
ComplianceDownloadButton,
ComplianceDownloadContainer,
ComplianceHeader,
RequirementsStatusCard,
RequirementsStatusCardSkeleton,
@@ -128,20 +128,17 @@ export default async function ComplianceDetail({
selectedScan={selectedScan}
/>
</div>
{(() => {
const framework = attributesData?.data?.[0]?.attributes?.framework;
const reportType = getReportTypeForFramework(framework);
return selectedScanId && reportType ? (
<div className="mb-4 flex-shrink-0 self-end sm:mb-0 sm:self-start sm:pt-1">
<ComplianceDownloadButton
scanId={selectedScanId}
reportType={reportType}
label="Download report"
/>
</div>
) : null;
})()}
{selectedScanId && (
<div className="mb-4 flex-shrink-0 self-end sm:mb-0 sm:self-start sm:pt-1">
<ComplianceDownloadContainer
scanId={selectedScanId}
complianceId={complianceId}
reportType={getReportTypeForFramework(
attributesData?.data?.[0]?.attributes?.framework,
)}
/>
</div>
)}
</div>
<Suspense
+9 -19
View File
@@ -3,14 +3,13 @@
import { Progress } from "@heroui/progress";
import Image from "next/image";
import { useRouter, useSearchParams } from "next/navigation";
import { useState } from "react";
import { Card, CardContent } from "@/components/shadcn/card/card";
import { DownloadIconButton, toast } from "@/components/ui";
import { downloadComplianceCsv } from "@/lib/helper";
import { getReportTypeForFramework } from "@/lib/compliance/compliance-report-types";
import { ScanEntity } from "@/types/scans";
import { getComplianceIcon } from "../icons";
import { ComplianceDownloadContainer } from "./compliance-download-container";
interface ComplianceCardProps {
title: string;
@@ -38,7 +37,6 @@ export const ComplianceCard: React.FC<ComplianceCardProps> = ({
const searchParams = useSearchParams();
const router = useRouter();
const hasRegionFilter = searchParams.has("filter[region__in]");
const [isDownloading, setIsDownloading] = useState<boolean>(false);
const formatTitle = (title: string) => {
return title.split("-").join(" ");
@@ -85,14 +83,6 @@ export const ComplianceCard: React.FC<ComplianceCardProps> = ({
router.push(`${path}?${params.toString()}`);
};
const handleDownload = async () => {
setIsDownloading(true);
try {
await downloadComplianceCsv(scanId, complianceId, toast);
} finally {
setIsDownloading(false);
}
};
return (
<Card
@@ -143,15 +133,15 @@ export const ComplianceCard: React.FC<ComplianceCardProps> = ({
e.stopPropagation();
}
}}
role="button"
role="group"
tabIndex={0}
>
<DownloadIconButton
paramId={complianceId}
onDownload={handleDownload}
textTooltip="Download compliance CSV report"
isDisabled={hasRegionFilter}
isDownloading={isDownloading}
<ComplianceDownloadContainer
compact
scanId={scanId}
complianceId={complianceId}
reportType={getReportTypeForFramework(title)}
disabled={hasRegionFilter}
/>
</div>
</div>
@@ -1,101 +0,0 @@
"use client";
import { DownloadIcon } from "lucide-react";
import { useState } from "react";
import { Button } from "@/components/shadcn/button/button";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/shadcn/tooltip";
import { toast } from "@/components/ui";
import {
COMPLIANCE_REPORT_BUTTON_LABELS,
type ComplianceReportType,
} from "@/lib/compliance/compliance-report-types";
import { downloadComplianceReportPdf } from "@/lib/helper";
interface ComplianceDownloadButtonProps {
scanId: string;
reportType: ComplianceReportType;
label?: string;
/** Show only icon with tooltip on mobile (sm and below) */
iconOnlyOnMobile?: boolean;
}
export const ComplianceDownloadButton = ({
scanId,
reportType,
label,
iconOnlyOnMobile = false,
}: ComplianceDownloadButtonProps) => {
const [isDownloading, setIsDownloading] = useState<boolean>(false);
const handleDownload = async () => {
setIsDownloading(true);
try {
await downloadComplianceReportPdf(scanId, reportType, toast);
} finally {
setIsDownloading(false);
}
};
const defaultLabel = COMPLIANCE_REPORT_BUTTON_LABELS[reportType];
const buttonLabel = label || defaultLabel;
if (iconOnlyOnMobile) {
return (
<>
{/* Mobile and Tablet: Icon only with tooltip */}
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="default"
size="icon"
onClick={handleDownload}
disabled={isDownloading}
className="md:hidden"
aria-label={buttonLabel}
>
<DownloadIcon
className={isDownloading ? "animate-download-icon" : ""}
size={16}
/>
</Button>
</TooltipTrigger>
<TooltipContent>{buttonLabel}</TooltipContent>
</Tooltip>
{/* Desktop: Full button with label */}
<Button
variant="default"
size="sm"
onClick={handleDownload}
disabled={isDownloading}
className="hidden md:inline-flex"
>
<DownloadIcon
className={isDownloading ? "animate-download-icon" : ""}
size={16}
/>
{buttonLabel}
</Button>
</>
);
}
return (
<Button
variant="default"
size="sm"
onClick={handleDownload}
disabled={isDownloading}
>
<DownloadIcon
className={isDownloading ? "animate-download-icon" : ""}
size={16}
/>
{buttonLabel}
</Button>
);
};
@@ -0,0 +1,92 @@
"use client";
import { DownloadIcon, FileTextIcon } from "lucide-react";
import { useState } from "react";
import { Button } from "@/components/shadcn/button/button";
import { toast } from "@/components/ui";
import type { ComplianceReportType } from "@/lib/compliance/compliance-report-types";
import {
downloadComplianceCsv,
downloadComplianceReportPdf,
} from "@/lib/helper";
import { cn } from "@/lib/utils";
interface ComplianceDownloadContainerProps {
scanId: string;
complianceId: string;
reportType?: ComplianceReportType;
compact?: boolean;
disabled?: boolean;
}
export const ComplianceDownloadContainer = ({
scanId,
complianceId,
reportType,
compact = false,
disabled = false,
}: ComplianceDownloadContainerProps) => {
const [isDownloadingCsv, setIsDownloadingCsv] = useState(false);
const [isDownloadingPdf, setIsDownloadingPdf] = useState(false);
const handleDownloadCsv = async () => {
if (isDownloadingCsv) return;
setIsDownloadingCsv(true);
try {
await downloadComplianceCsv(scanId, complianceId, toast);
} finally {
setIsDownloadingCsv(false);
}
};
const handleDownloadPdf = async () => {
if (!reportType || isDownloadingPdf) return;
setIsDownloadingPdf(true);
try {
await downloadComplianceReportPdf(scanId, reportType, toast);
} finally {
setIsDownloadingPdf(false);
}
};
const buttonClassName = cn(
"border-button-primary text-button-primary hover:bg-button-primary/10",
compact && "h-7 px-2 text-xs",
);
return (
<div className={cn("flex gap-2", compact ? "items-center" : "flex-col")}>
<Button
size="sm"
variant="outline"
className={buttonClassName}
onClick={handleDownloadCsv}
disabled={disabled || isDownloadingCsv}
aria-label="Download compliance CSV report"
>
<FileTextIcon
size={14}
className={isDownloadingCsv ? "animate-download-icon" : ""}
/>
CSV
</Button>
{reportType && (
<Button
size="sm"
variant="outline"
className={buttonClassName}
onClick={handleDownloadPdf}
disabled={disabled || isDownloadingPdf}
aria-label="Download compliance PDF report"
>
<DownloadIcon
size={14}
className={isDownloadingPdf ? "animate-download-icon" : ""}
/>
PDF
</Button>
)}
</div>
);
};
+1 -1
View File
@@ -12,7 +12,7 @@ export * from "./compliance-charts/top-failed-sections-card";
export * from "./compliance-custom-details/cis-details";
export * from "./compliance-custom-details/ens-details";
export * from "./compliance-custom-details/iso-details";
export * from "./compliance-download-button";
export * from "./compliance-download-container";
export * from "./compliance-header/compliance-header";
export * from "./compliance-header/compliance-scan-info";
export * from "./compliance-header/data-compliance";
@@ -1,35 +0,0 @@
"use client";
import { Select, SelectItem } from "@heroui/select";
const accounts = [
{ key: "audit-test-1", label: "740350143844" },
{ key: "audit-test-2", label: "890837126756" },
{ key: "audit-test-3", label: "563829104923" },
{ key: "audit-test-4", label: "678943217543" },
{ key: "audit-test-5", label: "932187465320" },
{ key: "audit-test-6", label: "492837106587" },
{ key: "audit-test-7", label: "812736459201" },
{ key: "audit-test-8", label: "374829106524" },
{ key: "audit-test-9", label: "926481053298" },
{ key: "audit-test-10", label: "748192364579" },
{ key: "audit-test-11", label: "501374829106" },
];
export const CustomAccountSelection = () => {
return (
<Select
label="Account"
aria-label="Select an Account"
placeholder="Select an account"
classNames={{
selectorIcon: "right-2",
}}
selectionMode="multiple"
className="w-full"
size="sm"
>
{accounts.map((acc) => (
<SelectItem key={acc.key}>{acc.label}</SelectItem>
))}
</Select>
);
};
@@ -1,48 +0,0 @@
"use client";
import { Select, SelectItem } from "@heroui/select";
import { useRouter, useSearchParams } from "next/navigation";
import React, { useCallback, useMemo } from "react";
export const CustomRegionSelection: React.FC = () => {
const router = useRouter();
const searchParams = useSearchParams();
const region = "none";
// Memoize selected keys based on the URL
const selectedKeys = useMemo(() => {
const params = searchParams.get("filter[regions]");
return params ? params.split(",") : [];
}, [searchParams]);
const applyRegionFilter = useCallback(
(values: string[]) => {
const params = new URLSearchParams(searchParams.toString());
if (values.length > 0) {
params.set("filter[regions]", values.join(","));
} else {
params.delete("filter[regions]");
}
router.push(`?${params.toString()}`, { scroll: false });
},
[router, searchParams],
);
return (
<Select
label="Region"
aria-label="Select a Region"
placeholder="Select a region"
classNames={{
selectorIcon: "right-2",
}}
className="w-full"
size="sm"
selectedKeys={selectedKeys}
onSelectionChange={(keys) =>
applyRegionFilter(Array.from(keys) as string[])
}
>
<SelectItem key={region}>{region}</SelectItem>
</Select>
);
};
@@ -1,128 +0,0 @@
"use client";
import { Select, SelectItem } from "@heroui/select";
import { useRouter, useSearchParams } from "next/navigation";
import { ReactElement } from "react";
import { PROVIDER_TYPES, ProviderType } from "@/types/providers";
import {
CustomProviderInputAlibabaCloud,
CustomProviderInputAWS,
CustomProviderInputAzure,
CustomProviderInputGCP,
CustomProviderInputGitHub,
CustomProviderInputIac,
CustomProviderInputKubernetes,
CustomProviderInputM365,
CustomProviderInputMongoDBAtlas,
CustomProviderInputOracleCloud,
} from "./custom-provider-inputs";
const providerDisplayData: Record<
ProviderType,
{ label: string; component: ReactElement }
> = {
aws: {
label: "Amazon Web Services",
component: <CustomProviderInputAWS />,
},
azure: {
label: "Microsoft Azure",
component: <CustomProviderInputAzure />,
},
gcp: {
label: "Google Cloud Platform",
component: <CustomProviderInputGCP />,
},
github: {
label: "GitHub",
component: <CustomProviderInputGitHub />,
},
iac: {
label: "Infrastructure as Code",
component: <CustomProviderInputIac />,
},
kubernetes: {
label: "Kubernetes",
component: <CustomProviderInputKubernetes />,
},
m365: {
label: "Microsoft 365",
component: <CustomProviderInputM365 />,
},
mongodbatlas: {
label: "MongoDB Atlas",
component: <CustomProviderInputMongoDBAtlas />,
},
oraclecloud: {
label: "Oracle Cloud Infrastructure",
component: <CustomProviderInputOracleCloud />,
},
alibabacloud: {
label: "Alibaba Cloud",
component: <CustomProviderInputAlibabaCloud />,
},
};
const dataInputsProvider = PROVIDER_TYPES.map((providerType) => ({
key: providerType,
label: providerDisplayData[providerType].label,
value: providerDisplayData[providerType].component,
}));
export const CustomSelectProvider = () => {
const router = useRouter();
const searchParams = useSearchParams();
const applyProviderFilter = (value: string) => {
const params = new URLSearchParams(searchParams.toString());
if (value) {
params.set("filter[provider_type]", value);
} else {
params.delete("filter[provider_type]");
}
router.push(`?${params.toString()}`, { scroll: false });
};
const currentProvider = searchParams.get("filter[provider_type]") || "";
const selectedKeys = dataInputsProvider.some(
(provider) => provider.key === currentProvider,
)
? [currentProvider]
: [];
return (
<Select
items={dataInputsProvider}
aria-label="Select a Provider"
placeholder="Select a provider"
classNames={{
selectorIcon: "right-2",
label: "z-0! mb-2",
}}
label="Provider"
labelPlacement="inside"
size="sm"
onChange={(e) => {
const value = e.target.value;
applyProviderFilter(value);
}}
selectedKeys={selectedKeys}
renderValue={(items) => {
return items.map((item) => (
<div key={item.key} className="flex items-center gap-2">
{item.data?.value}
</div>
));
}}
>
{(item) => (
<SelectItem key={item.key} textValue={item.key} aria-label={item.label}>
<div className="flex items-center gap-2">{item.value}</div>
</SelectItem>
)}
</Select>
);
};
-20
View File
@@ -3,30 +3,15 @@
import { FilterOption } from "@/types";
import { DataTableFilterCustom } from "../ui/table";
import { CustomAccountSelection } from "./custom-account-selection";
import { CustomCheckboxMutedFindings } from "./custom-checkbox-muted-findings";
import { CustomDatePicker } from "./custom-date-picker";
import { CustomRegionSelection } from "./custom-region-selection";
import { CustomSearchInput } from "./custom-search-input";
import { CustomSelectProvider } from "./custom-select-provider";
export interface FilterControlsProps {
search?: boolean;
providers?: boolean;
date?: boolean;
regions?: boolean;
accounts?: boolean;
mutedFindings?: boolean;
customFilters?: FilterOption[];
}
export const FilterControls = ({
search = false,
providers = false,
date = false,
regions = false,
accounts = false,
mutedFindings = false,
customFilters,
}: FilterControlsProps) => {
return (
@@ -34,11 +19,6 @@ export const FilterControls = ({
<div className="mb-4 flex flex-col items-start gap-4 md:flex-row md:items-center">
<div className="grid w-full flex-1 grid-cols-1 items-center gap-x-4 gap-y-4 md:grid-cols-2 xl:grid-cols-4">
{search && <CustomSearchInput />}
{providers && <CustomSelectProvider />}
{date && <CustomDatePicker />}
{regions && <CustomRegionSelection />}
{accounts && <CustomAccountSelection />}
{mutedFindings && <CustomCheckboxMutedFindings />}
</div>
</div>
{customFilters && customFilters.length > 0 && (
-3
View File
@@ -1,9 +1,6 @@
export * from "./clear-filters-button";
export * from "./custom-account-selection";
export * from "./custom-checkbox-muted-findings";
export * from "./custom-date-picker";
export * from "./custom-provider-inputs";
export * from "./custom-region-selection";
export * from "./custom-select-provider";
export * from "./data-filters";
export * from "./filter-controls";
+67 -166
View File
@@ -1,10 +1,7 @@
"use client";
import { Input } from "@heroui/input";
import { Select, SelectItem } from "@heroui/select";
import { zodResolver } from "@hookform/resolvers/zod";
import type { Selection } from "@react-types/shared";
import { Search, Send } from "lucide-react";
import { Send } from "lucide-react";
import { type Dispatch, type SetStateAction, useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
@@ -14,16 +11,11 @@ import {
pollJiraDispatchTask,
sendFindingToJira,
} from "@/actions/integrations/jira-dispatch";
import { JiraIcon } from "@/components/icons/services/IconServices";
import { Modal } from "@/components/shadcn/modal";
import { EnhancedMultiSelect } from "@/components/shadcn/select/enhanced-multi-select";
import { useToast } from "@/components/ui";
import { CustomBanner } from "@/components/ui/custom/custom-banner";
import {
Form,
FormControl,
FormField,
FormMessage,
} from "@/components/ui/form";
import { Form, FormField, FormMessage } from "@/components/ui/form";
import { FormButtons } from "@/components/ui/form/form-buttons";
import { IntegrationProps } from "@/types/integrations";
@@ -42,15 +34,6 @@ const sendToJiraSchema = z.object({
type SendToJiraFormData = z.infer<typeof sendToJiraSchema>;
const selectorClassNames = {
trigger: "min-h-12",
popoverContent: "bg-bg-neutral-secondary",
listboxWrapper: "max-h-[300px] bg-bg-neutral-secondary",
listbox: "gap-0",
label: "tracking-tight font-light !text-text-neutral-secondary text-xs z-0!",
value: "text-text-neutral-secondary text-small",
};
// The commented code is related to issue types, which are not required for the first implementation, but will be used in the future
export const SendToJiraModal = ({
isOpen,
@@ -61,8 +44,6 @@ export const SendToJiraModal = ({
const { toast } = useToast();
const [integrations, setIntegrations] = useState<IntegrationProps[]>([]);
const [isFetchingIntegrations, setIsFetchingIntegrations] = useState(false);
const [searchProjectValue, setSearchProjectValue] = useState("");
// const [searchIssueTypeValue, setSearchIssueTypeValue] = useState("");
const form = useForm<SendToJiraFormData>({
resolver: zodResolver(sendToJiraSchema),
@@ -75,18 +56,11 @@ export const SendToJiraModal = ({
});
const selectedIntegration = form.watch("integration");
// const selectedProject = form.watch("project");
const hasConnectedIntegration = integrations.some(
(i) => i.attributes.connected === true,
);
const getSelectedValue = (keys: Selection): string => {
if (keys === "all") return "";
const first = Array.from(keys)[0];
return first !== null ? String(first) : "";
};
const setOpenForFormButtons: Dispatch<SetStateAction<boolean>> = (value) => {
const next = typeof value === "function" ? value(isOpen) : value;
onOpenChange(next);
@@ -129,8 +103,6 @@ export const SendToJiraModal = ({
} else {
// Reset form when modal closes
form.reset();
setSearchProjectValue("");
// setSearchIssueTypeValue("");
}
}, [isOpen, form, toast]);
@@ -187,32 +159,16 @@ export const SendToJiraModal = ({
({} as Record<string, string>);
const projectEntries = Object.entries(projects);
const shouldShowProjectSearch = projectEntries.length > 5;
// const issueTypes: string[] =
// selectedIntegrationData?.attributes.configuration.issue_types ||
// ([] as string[]);
// Filter projects based on search
const filteredProjects = (() => {
if (!searchProjectValue) return projectEntries;
const integrationOptions = integrations.map((integration) => ({
value: integration.id,
label: integration.attributes.configuration.domain || integration.id,
}));
const lowerSearch = searchProjectValue.toLowerCase();
return projectEntries.filter(
([key, name]) =>
key.toLowerCase().includes(lowerSearch) ||
name.toLowerCase().includes(lowerSearch),
);
})();
// Filter issue types based on search
// const filteredIssueTypes = useMemo(() => {
// if (!searchIssueTypeValue) return issueTypes;
// const lowerSearch = searchIssueTypeValue.toLowerCase();
// return issueTypes.filter((type) =>
// type.toLowerCase().includes(lowerSearch),
// );
// }, [issueTypes, searchIssueTypeValue]);
const projectOptions = projectEntries.map(([key, name]) => ({
value: key,
label: `${key} - ${name}`,
}));
return (
<Modal
@@ -236,127 +192,72 @@ export const SendToJiraModal = ({
control={form.control}
name="integration"
render={({ field }) => (
<>
<FormControl>
<Select
label="Jira Integration"
placeholder="Select a Jira integration"
selectedKeys={
field.value ? new Set([field.value]) : new Set()
}
onSelectionChange={(keys: Selection) => {
const value = getSelectedValue(keys);
field.onChange(value);
// Reset dependent fields
form.setValue("project", "");
// Keep issue type defaulting to Task
form.setValue("issueType", "Task");
setSearchProjectValue("");
// setSearchIssueTypeValue("");
}}
variant="bordered"
labelPlacement="inside"
isDisabled={isFetchingIntegrations}
isInvalid={!!form.formState.errors.integration}
startContent={<JiraIcon size={16} />}
classNames={selectorClassNames}
>
{integrations.map((integration) => (
<SelectItem
key={integration.id}
textValue={
integration.attributes.configuration.domain
}
>
<div className="flex items-center gap-2">
<JiraIcon size={16} />
<span>
{integration.attributes.configuration.domain}
</span>
</div>
</SelectItem>
))}
</Select>
</FormControl>
<div className="flex flex-col gap-1.5">
<label
htmlFor="jira-integration-select"
className="text-text-neutral-secondary text-xs font-light tracking-tight"
>
Jira Integration
</label>
<EnhancedMultiSelect
id="jira-integration-select"
options={integrationOptions}
onValueChange={(values) => {
const selectedValue = values.at(-1) ?? "";
field.onChange(selectedValue);
// Reset dependent fields
form.setValue("project", "");
form.setValue("issueType", "Task");
}}
defaultValue={field.value ? [field.value] : []}
placeholder="Select a Jira integration"
searchable={true}
emptyIndicator="No integrations found."
disabled={isFetchingIntegrations}
hideSelectAll={true}
maxCount={1}
closeOnSelect={true}
resetOnDefaultValueChange={true}
/>
<FormMessage className="text-text-error text-xs" />
</>
</div>
)}
/>
)}
{/* Project Selection - Enhanced Style */}
{selectedIntegration && Object.keys(projects).length > 0 && (
{/* Project Selection */}
{selectedIntegration && projectEntries.length > 0 && (
<FormField
control={form.control}
name="project"
render={({ field }) => (
<>
<FormControl>
<Select
label="Project"
placeholder="Select a Jira project"
selectedKeys={
field.value ? new Set([field.value]) : new Set()
}
onSelectionChange={(keys: Selection) => {
const value = getSelectedValue(keys);
field.onChange(value);
// Keep issue type defaulting to Task when project changes
form.setValue("issueType", "Task");
// setSearchIssueTypeValue("");
}}
variant="bordered"
labelPlacement="inside"
isInvalid={!!form.formState.errors.project}
classNames={selectorClassNames}
listboxProps={{
topContent: shouldShowProjectSearch ? (
<div className="sticky top-0 z-10 py-2">
<Input
isClearable
placeholder="Search projects..."
size="sm"
variant="bordered"
startContent={<Search size={16} />}
value={searchProjectValue}
onValueChange={setSearchProjectValue}
onClear={() => setSearchProjectValue("")}
classNames={{
inputWrapper:
"border-border-input-primary bg-bg-input-primary hover:bg-bg-neutral-secondary",
input: "text-small",
clearButton: "text-default-400",
}}
/>
</div>
) : null,
}}
>
{filteredProjects.map(([key, name]) => (
<SelectItem key={key} textValue={`${key} - ${name}`}>
<div className="flex w-full items-center justify-between py-1">
<div className="flex min-w-0 flex-1 items-center gap-3">
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className="text-small font-semibold">
{key}
</span>
<span className="text-tiny text-default-500">
-
</span>
<span className="text-small truncate">
{name}
</span>
</div>
</div>
</div>
</div>
</SelectItem>
))}
</Select>
</FormControl>
<div className="flex flex-col gap-1.5">
<label
htmlFor="jira-project-select"
className="text-text-neutral-secondary text-xs font-light tracking-tight"
>
Project
</label>
<EnhancedMultiSelect
id="jira-project-select"
options={projectOptions}
onValueChange={(values) => {
const selectedValue = values.at(-1) ?? "";
field.onChange(selectedValue);
// Keep issue type defaulting to Task when project changes
form.setValue("issueType", "Task");
}}
defaultValue={field.value ? [field.value] : []}
placeholder="Select a Jira project"
searchable={true}
emptyIndicator="No projects found."
hideSelectAll={true}
maxCount={1}
closeOnSelect={true}
resetOnDefaultValueChange={true}
/>
<FormMessage className="text-text-error text-xs" />
</>
</div>
)}
/>
)}
@@ -12,7 +12,7 @@ import {
} from "@/components/icons/providers-badge";
import { ProviderType } from "@/types";
const PROVIDER_ICONS = {
export const PROVIDER_ICONS = {
aws: AWSProviderBadge,
azure: AzureProviderBadge,
gcp: GCPProviderBadge,
-1
View File
@@ -1,4 +1,3 @@
export * from "../providers/enhanced-provider-selector";
export * from "./api-key/api-key-link-card";
export * from "./jira/jira-integration-card";
export * from "./jira/jira-integration-form";
@@ -8,12 +8,18 @@ import { useState } from "react";
import { Control, useForm } from "react-hook-form";
import { createIntegration, updateIntegration } from "@/actions/integrations";
import { EnhancedProviderSelector } from "@/components/providers/enhanced-provider-selector";
import { PROVIDER_ICONS } from "@/components/findings/table/provider-icon-cell";
import { AWSRoleCredentialsForm } from "@/components/providers/workflow/forms/select-credentials-type/aws/credentials-type/aws-role-credentials-form";
import { EnhancedMultiSelect } from "@/components/shadcn/select/enhanced-multi-select";
import { useToast } from "@/components/ui";
import { CustomInput } from "@/components/ui/custom";
import { CustomLink } from "@/components/ui/custom/custom-link";
import { Form } from "@/components/ui/form";
import {
Form,
FormControl,
FormField,
FormMessage,
} from "@/components/ui/form";
import { FormButtons } from "@/components/ui/form/form-buttons";
import { getAWSCredentialsTemplateLinks } from "@/lib";
import { AWSCredentialsRole } from "@/types";
@@ -272,18 +278,40 @@ export const S3IntegrationForm = ({
// Show configuration step (step 0 or editing configuration)
if (isEditingConfig || currentStep === 0) {
const providerOptions = providers.map((provider) => {
const Icon = PROVIDER_ICONS[provider.attributes.provider];
return {
value: provider.id,
label: provider.attributes.alias || provider.attributes.uid,
icon: Icon ? <Icon width={20} height={20} /> : undefined,
description: provider.attributes.connection.connected
? "Connected"
: "Disconnected",
};
});
return (
<>
{/* Provider Selection */}
<div className="flex flex-col gap-4">
<EnhancedProviderSelector
<FormField
control={form.control}
name="providers"
providers={providers}
label="Cloud Providers"
placeholder="Select providers to integrate with"
selectionMode="multiple"
enableSearch={true}
render={({ field }) => (
<>
<FormControl>
<EnhancedMultiSelect
options={providerOptions}
onValueChange={field.onChange}
defaultValue={field.value || []}
placeholder="Select providers to integrate with"
searchable={true}
maxCount={1}
/>
</FormControl>
<FormMessage className="text-text-error max-w-full text-xs" />
</>
)}
/>
</div>

Some files were not shown because too many files have changed in this diff Show More