Merge branch 'master' of https://github.com/prowler-cloud/prowler into PRWLR-7706-add-pydantic-validators-to-check-metadata-model-per-rfc-specification

This commit is contained in:
HugoPBrito
2025-08-27 12:09:10 +02:00
146 changed files with 11914 additions and 4297 deletions
+4
View File
@@ -8,6 +8,10 @@ If fixes an issue please add it with `Fix #XXXX`
Please include a summary of the change and which issue is fixed. List any dependencies that are required for this change.
### Steps to review
Please add a detailed description of how to review this PR.
### Checklist
- Are there new checks included in this PR? Yes / No
@@ -7,6 +7,7 @@ on:
- 'v3'
paths:
- 'docs/**'
- '.github/workflows/build-documentation-on-pr.yml'
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
@@ -16,9 +17,20 @@ jobs:
name: Documentation Link
runs-on: ubuntu-latest
steps:
- name: Leave PR comment with the Prowler Documentation URI
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
- name: Find existing documentation comment
uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3.1.0
id: find-comment
with:
issue-number: ${{ env.PR_NUMBER }}
comment-author: 'github-actions[bot]'
body-includes: '<!-- prowler-docs-link -->'
- name: Create or update PR comment with the Prowler Documentation URI
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
with:
comment-id: ${{ steps.find-comment.outputs.comment-id }}
issue-number: ${{ env.PR_NUMBER }}
body: |
<!-- prowler-docs-link -->
You can check the documentation for this PR here -> [Prowler Documentation](https://prowler-prowler-docs--${{ env.PR_NUMBER }}.com.readthedocs.build/projects/prowler-open-source/en/${{ env.PR_NUMBER }}/)
edit-mode: replace
+1 -1
View File
@@ -1,4 +1,4 @@
name: Create Backport Label
name: Prowler - Create Backport Label
on:
release:
+166
View File
@@ -0,0 +1,166 @@
name: Prowler - PR Conflict Checker
on:
pull_request:
types:
- opened
- synchronize
- reopened
branches:
- "master"
- "v5.*"
pull_request_target:
types:
- opened
- synchronize
- reopened
branches:
- "master"
- "v5.*"
jobs:
conflict-checker:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c # v46.0.5
with:
files: |
**
- name: Check for conflict markers
id: conflict-check
run: |
echo "Checking for conflict markers in changed files..."
CONFLICT_FILES=""
HAS_CONFLICTS=false
# Check each changed file for conflict markers
for file in ${{ steps.changed-files.outputs.all_changed_files }}; do
if [ -f "$file" ]; then
echo "Checking file: $file"
# Look for conflict markers
if grep -l "^<<<<<<<\|^=======\|^>>>>>>>" "$file" 2>/dev/null; then
echo "Conflict markers found in: $file"
CONFLICT_FILES="$CONFLICT_FILES$file "
HAS_CONFLICTS=true
fi
fi
done
if [ "$HAS_CONFLICTS" = true ]; then
echo "has_conflicts=true" >> $GITHUB_OUTPUT
echo "conflict_files=$CONFLICT_FILES" >> $GITHUB_OUTPUT
echo "Conflict markers detected in files: $CONFLICT_FILES"
else
echo "has_conflicts=false" >> $GITHUB_OUTPUT
echo "No conflict markers found in changed files"
fi
- name: Add conflict label
if: steps.conflict-check.outputs.has_conflicts == 'true'
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
github-token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
script: |
const { data: labels } = await github.rest.issues.listLabelsOnIssue({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const hasConflictLabel = labels.some(label => label.name === 'has-conflicts');
if (!hasConflictLabel) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: ['has-conflicts']
});
console.log('Added has-conflicts label');
} else {
console.log('has-conflicts label already exists');
}
- name: Remove conflict label
if: steps.conflict-check.outputs.has_conflicts == 'false'
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
script: |
try {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
name: 'has-conflicts'
});
console.log('Removed has-conflicts label');
} catch (error) {
if (error.status === 404) {
console.log('has-conflicts label was not present');
} else {
throw error;
}
}
- name: Find existing conflict comment
if: steps.conflict-check.outputs.has_conflicts == 'true'
uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3.1.0
id: find-comment
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: 'github-actions[bot]'
body-regex: '(⚠️ \*\*Conflict Markers Detected\*\*|✅ \*\*Conflict Markers Resolved\*\*)'
- name: Create or update conflict comment
if: steps.conflict-check.outputs.has_conflicts == 'true'
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
with:
comment-id: ${{ steps.find-comment.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}
edit-mode: replace
body: |
⚠️ **Conflict Markers Detected**
This pull request contains unresolved conflict markers in the following files:
```
${{ steps.conflict-check.outputs.conflict_files }}
```
Please resolve these conflicts by:
1. Locating the conflict markers: `<<<<<<<`, `=======`, and `>>>>>>>`
2. Manually editing the files to resolve the conflicts
3. Removing all conflict markers
4. Committing and pushing the changes
- name: Find existing conflict comment when resolved
if: steps.conflict-check.outputs.has_conflicts == 'false'
uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3.1.0
id: find-resolved-comment
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: 'github-actions[bot]'
body-regex: '(⚠️ \*\*Conflict Markers Detected\*\*|✅ \*\*Conflict Markers Resolved\*\*)'
- name: Update comment when conflicts resolved
if: steps.conflict-check.outputs.has_conflicts == 'false' && steps.find-resolved-comment.outputs.comment-id != ''
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
with:
comment-id: ${{ steps.find-resolved-comment.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}
edit-mode: replace
body: |
✅ **Conflict Markers Resolved**
All conflict markers have been successfully resolved in this pull request.
@@ -1,4 +1,4 @@
name: Prowler Release Preparation
name: Prowler - Release Preparation
run-name: Prowler Release Preparation for ${{ inputs.prowler_version }}
@@ -1,4 +1,4 @@
name: Check Changelog
name: Prowler - Check Changelog
on:
pull_request:
+3
View File
@@ -75,3 +75,6 @@ node_modules
# Persistent data
_data/
# Claude
CLAUDE.md
+57 -15
View File
@@ -1,23 +1,65 @@
# Security Policy
# Security
## Software Security
As an **AWS Partner** and we have passed the [AWS Foundation Technical Review (FTR)](https://aws.amazon.com/partners/foundational-technical-review/) and we use the following tools and automation to make sure our code is secure and dependencies up-to-dated:
## Reporting Vulnerabilities
- `bandit` for code security review.
- `safety` and `dependabot` for dependencies.
- `hadolint` and `dockle` for our containers security.
- `snyk` in Docker Hub.
- `clair` in Amazon ECR.
- `vulture`, `flake8`, `black` and `pylint` for formatting and best practices.
At Prowler, we consider the security of our open source software and systems a top priority. But no matter how much effort we put into system security, there can still be vulnerabilities present.
## Reporting a Vulnerability
If you discover a vulnerability, we would like to know about it so we can take steps to address it as quickly as possible. We would like to ask you to help us better protect our users, our clients and our systems.
If you would like to report a vulnerability or have a security concern regarding Prowler Open Source or ProwlerPro service, please submit the information by contacting to https://support.prowler.com.
When reporting vulnerabilities, please consider (1) attack scenario / exploitability, and (2) the security impact of the bug. The following issues are considered out of scope:
The information you share with ProwlerPro as part of this process is kept confidential within ProwlerPro. We will only share this information with a third party if the vulnerability you report is found to affect a third-party product, in which case we will share this information with the third-party product's author or manufacturer. Otherwise, we will only share this information as permitted by you.
- Social engineering support or attacks requiring social engineering.
- Clickjacking on pages with no sensitive actions.
- Cross-Site Request Forgery (CSRF) on unauthenticated forms or forms with no sensitive actions.
- Attacks requiring Man-In-The-Middle (MITM) or physical access to a user's device.
- Previously known vulnerable libraries without a working Proof of Concept (PoC).
- Comma Separated Values (CSV) injection without demonstrating a vulnerability.
- Missing best practices in SSL/TLS configuration.
- Any activity that could lead to the disruption of service (DoS).
- Rate limiting or brute force issues on non-authentication endpoints.
- Missing best practices in Content Security Policy (CSP).
- Missing HttpOnly or Secure flags on cookies.
- Configuration of or missing security headers.
- Missing email best practices, such as invalid, incomplete, or missing SPF/DKIM/DMARC records.
- Vulnerabilities only affecting users of outdated or unpatched browsers (less than two stable versions behind).
- Software version disclosure, banner identification issues, or descriptive error messages.
- Tabnabbing.
- Issues that require unlikely user interaction.
- Improper logout functionality and improper session timeout.
- CORS misconfiguration without an exploitation scenario.
- Broken link hijacking.
- Automated scanning results (e.g., sqlmap, Burp active scanner) that have not been manually verified.
- Content spoofing and text injection issues without a clear attack vector.
- Email spoofing without exploiting security flaws.
- Dead links or broken links.
- User enumeration.
We will review the submitted report, and assign it a tracking number. We will then respond to you, acknowledging receipt of the report, and outline the next steps in the process.
Testing guidelines:
- Do not run automated scanners on other customer projects. Running automated scanners can run up costs for our users. Aggressively configured scanners might inadvertently disrupt services, exploit vulnerabilities, lead to system instability or breaches and violate Terms of Service from our upstream providers. Our own security systems won't be able to distinguish hostile reconnaissance from whitehat research. If you wish to run an automated scanner, notify us at support@prowler.com and only run it on your own Prowler app project. Do NOT attack Prowler in usage of other customers.
- Do not take advantage of the vulnerability or problem you have discovered, for example by downloading more data than necessary to demonstrate the vulnerability or deleting or modifying other people's data.
You will receive a non-automated response to your initial contact within 24 hours, confirming receipt of your reported vulnerability.
Reporting guidelines:
- File a report through our Support Desk at https://support.prowler.com
- If it is about a lack of a security functionality, please file a feature request instead at https://github.com/prowler-cloud/prowler/issues
- Do provide sufficient information to reproduce the problem, so we will be able to resolve it as quickly as possible.
- If you have further questions and want direct interaction with the Prowler team, please contact us at via our Community Slack at goto.prowler.com/slack.
We will coordinate public notification of any validated vulnerability with you. Where possible, we prefer that our respective public disclosures be posted simultaneously.
Disclosure guidelines:
- In order to protect our users and customers, do not reveal the problem to others until we have researched, addressed and informed our affected customers.
- If you want to publicly share your research about Prowler at a conference, in a blog or any other public forum, you should share a draft with us for review and approval at least 30 days prior to the publication date. Please note that the following should not be included:
- Data regarding any Prowler user or customer projects.
- Prowler customers' data.
- Information about Prowler employees, contractors or partners.
What we promise:
- We will respond to your report within 5 business days with our evaluation of the report and an expected resolution date.
- If you have followed the instructions above, we will not take any legal action against you in regard to the report.
- We will handle your report with strict confidentiality, and not pass on your personal details to third parties without your permission.
- We will keep you informed of the progress towards resolving the problem.
- In the public information concerning the problem reported, we will give your name as the discoverer of the problem (unless you desire otherwise).
We strive to resolve all problems as quickly as possible, and we would like to play an active role in the ultimate publication on the problem after it is resolved.
---
For more information about our security policies, please refer to our [Security](https://docs.prowler.com/projects/prowler-open-source/en/latest/security/) section in our documentation.
+2
View File
@@ -6,6 +6,8 @@ All notable changes to the **Prowler API** are documented in this file.
### Added
- Lighthouse support for OpenAI GPT-5 [(#8527)](https://github.com/prowler-cloud/prowler/pull/8527)
- Integration with Amazon Security Hub, enabling sending findings to Security Hub [(#8365)](https://github.com/prowler-cloud/prowler/pull/8365)
- Generate ASFF output for AWS providers with SecurityHub integration enabled [(#8569)](https://github.com/prowler-cloud/prowler/pull/8569)
## [1.11.0] (Prowler 5.10.0)
+1485 -1241
View File
File diff suppressed because it is too large Load Diff
+3 -2
View File
@@ -30,7 +30,8 @@ dependencies = [
"sentry-sdk[django] (>=2.20.0,<3.0.0)",
"uuid6==2024.7.10",
"openai (>=1.82.0,<2.0.0)",
"xmlsec==1.3.14"
"xmlsec==1.3.14",
"h2 (==4.3.0)"
]
description = "Prowler's API (Django/DRF)"
license = "Apache-2.0"
@@ -38,7 +39,7 @@ name = "prowler-api"
package-mode = false
# Needed for the SDK compatibility
requires-python = ">=3.11,<3.13"
version = "1.11.0"
version = "1.12.0"
[project.scripts]
celery = "src.backend.config.settings.celery"
@@ -0,0 +1,33 @@
# Generated by Django 5.1.10 on 2025-08-20 09:04
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("api", "0045_alter_scan_output_location"),
]
operations = [
migrations.AlterField(
model_name="lighthouseconfiguration",
name="model",
field=models.CharField(
choices=[
("gpt-4o-2024-11-20", "GPT-4o v2024-11-20"),
("gpt-4o-2024-08-06", "GPT-4o v2024-08-06"),
("gpt-4o-2024-05-13", "GPT-4o v2024-05-13"),
("gpt-4o", "GPT-4o Default"),
("gpt-4o-mini-2024-07-18", "GPT-4o Mini v2024-07-18"),
("gpt-4o-mini", "GPT-4o Mini Default"),
("gpt-5-2025-08-07", "GPT-5 v2025-08-07"),
("gpt-5", "GPT-5 Default"),
("gpt-5-mini-2025-08-07", "GPT-5 Mini v2025-08-07"),
("gpt-5-mini", "GPT-5 Mini Default"),
],
default="gpt-4o-2024-08-06",
help_text="Must be one of the supported model names",
max_length=50,
),
),
]
@@ -0,0 +1,16 @@
# Generated by Django 5.1.10 on 2025-08-20 08:24
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("api", "0046_lighthouse_gpt5"),
]
operations = [
migrations.RemoveConstraint(
model_name="integration",
name="unique_configuration_per_tenant",
),
]
-4
View File
@@ -1372,10 +1372,6 @@ class Integration(RowLevelSecurityProtectedModel):
db_table = "integrations"
constraints = [
models.UniqueConstraint(
fields=("configuration", "tenant"),
name="unique_configuration_per_tenant",
),
RowLevelSecurityConstraint(
field="tenant_id",
name="rls_on_%(class)s",
+93 -1
View File
@@ -1,7 +1,7 @@
openapi: 3.0.3
info:
title: Prowler API
version: 1.11.0
version: 1.12.0
description: |-
Prowler API specification.
@@ -8918,6 +8918,19 @@ components:
default: output
required:
- bucket_name
- type: object
title: AWS Security Hub
properties:
send_only_fails:
type: boolean
default: false
description: If true, only findings with status 'FAIL' will
be sent to Security Hub.
archive_previous_findings:
type: boolean
default: false
description: If true, archives findings that are not present in
the current execution.
credentials:
oneOf:
- type: object
@@ -9064,6 +9077,19 @@ components:
default: output
required:
- bucket_name
- type: object
title: AWS Security Hub
properties:
send_only_fails:
type: boolean
default: false
description: If true, only findings with status 'FAIL' will
be sent to Security Hub.
archive_previous_findings:
type: boolean
default: false
description: If true, archives findings that are not present
in the current execution.
credentials:
oneOf:
- type: object
@@ -9225,6 +9251,19 @@ components:
default: output
required:
- bucket_name
- type: object
title: AWS Security Hub
properties:
send_only_fails:
type: boolean
default: false
description: If true, only findings with status 'FAIL' will
be sent to Security Hub.
archive_previous_findings:
type: boolean
default: false
description: If true, archives findings that are not present in
the current execution.
credentials:
oneOf:
- type: object
@@ -9777,6 +9816,10 @@ components:
- gpt-4o
- gpt-4o-mini-2024-07-18
- gpt-4o-mini
- gpt-5-2025-08-07
- gpt-5
- gpt-5-mini-2025-08-07
- gpt-5-mini
type: string
description: |-
Must be one of the supported model names
@@ -9787,6 +9830,10 @@ components:
* `gpt-4o` - GPT-4o Default
* `gpt-4o-mini-2024-07-18` - GPT-4o Mini v2024-07-18
* `gpt-4o-mini` - GPT-4o Mini Default
* `gpt-5-2025-08-07` - GPT-5 v2025-08-07
* `gpt-5` - GPT-5 Default
* `gpt-5-mini-2025-08-07` - GPT-5 Mini v2025-08-07
* `gpt-5-mini` - GPT-5 Mini Default
temperature:
type: number
format: double
@@ -9843,6 +9890,10 @@ components:
- gpt-4o
- gpt-4o-mini-2024-07-18
- gpt-4o-mini
- gpt-5-2025-08-07
- gpt-5
- gpt-5-mini-2025-08-07
- gpt-5-mini
type: string
description: |-
Must be one of the supported model names
@@ -9853,6 +9904,10 @@ components:
* `gpt-4o` - GPT-4o Default
* `gpt-4o-mini-2024-07-18` - GPT-4o Mini v2024-07-18
* `gpt-4o-mini` - GPT-4o Mini Default
* `gpt-5-2025-08-07` - GPT-5 v2025-08-07
* `gpt-5` - GPT-5 Default
* `gpt-5-mini-2025-08-07` - GPT-5 Mini v2025-08-07
* `gpt-5-mini` - GPT-5 Mini Default
temperature:
type: number
format: double
@@ -9915,6 +9970,10 @@ components:
- gpt-4o
- gpt-4o-mini-2024-07-18
- gpt-4o-mini
- gpt-5-2025-08-07
- gpt-5
- gpt-5-mini-2025-08-07
- gpt-5-mini
type: string
description: |-
Must be one of the supported model names
@@ -9925,6 +9984,10 @@ components:
* `gpt-4o` - GPT-4o Default
* `gpt-4o-mini-2024-07-18` - GPT-4o Mini v2024-07-18
* `gpt-4o-mini` - GPT-4o Mini Default
* `gpt-5-2025-08-07` - GPT-5 v2025-08-07
* `gpt-5` - GPT-5 Default
* `gpt-5-mini-2025-08-07` - GPT-5 Mini v2025-08-07
* `gpt-5-mini` - GPT-5 Mini Default
temperature:
type: number
format: double
@@ -9995,6 +10058,10 @@ components:
- gpt-4o
- gpt-4o-mini-2024-07-18
- gpt-4o-mini
- gpt-5-2025-08-07
- gpt-5
- gpt-5-mini-2025-08-07
- gpt-5-mini
type: string
description: |-
Must be one of the supported model names
@@ -10005,6 +10072,10 @@ components:
* `gpt-4o` - GPT-4o Default
* `gpt-4o-mini-2024-07-18` - GPT-4o Mini v2024-07-18
* `gpt-4o-mini` - GPT-4o Mini Default
* `gpt-5-2025-08-07` - GPT-5 v2025-08-07
* `gpt-5` - GPT-5 Default
* `gpt-5-mini-2025-08-07` - GPT-5 Mini v2025-08-07
* `gpt-5-mini` - GPT-5 Mini Default
temperature:
type: number
format: double
@@ -10584,6 +10655,19 @@ components:
default: output
required:
- bucket_name
- type: object
title: AWS Security Hub
properties:
send_only_fails:
type: boolean
default: false
description: If true, only findings with status 'FAIL' will
be sent to Security Hub.
archive_previous_findings:
type: boolean
default: false
description: If true, archives findings that are not present
in the current execution.
credentials:
oneOf:
- type: object
@@ -10779,6 +10863,10 @@ components:
- gpt-4o
- gpt-4o-mini-2024-07-18
- gpt-4o-mini
- gpt-5-2025-08-07
- gpt-5
- gpt-5-mini-2025-08-07
- gpt-5-mini
type: string
description: |-
Must be one of the supported model names
@@ -10789,6 +10877,10 @@ components:
* `gpt-4o` - GPT-4o Default
* `gpt-4o-mini-2024-07-18` - GPT-4o Mini v2024-07-18
* `gpt-4o-mini` - GPT-4o Mini Default
* `gpt-5-2025-08-07` - GPT-5 v2025-08-07
* `gpt-5` - GPT-5 Default
* `gpt-5-mini-2025-08-07` - GPT-5 Mini v2025-08-07
* `gpt-5-mini` - GPT-5 Mini Default
temperature:
type: number
format: double
+29 -1
View File
@@ -11,6 +11,7 @@ from api.models import Integration, Invitation, Processor, Provider, Resource
from api.v1.serializers import FindingMetadataSerializer
from prowler.providers.aws.aws_provider import AwsProvider
from prowler.providers.aws.lib.s3.s3 import S3
from prowler.providers.aws.lib.security_hub.security_hub import SecurityHub
from prowler.providers.azure.azure_provider import AzureProvider
from prowler.providers.common.models import Connection
from prowler.providers.gcp.gcp_provider import GcpProvider
@@ -196,7 +197,34 @@ def prowler_integration_connection_test(integration: Integration) -> Connection:
elif (
integration.integration_type == Integration.IntegrationChoices.AWS_SECURITY_HUB
):
pass
# Get the provider associated with this integration
provider_relationship = integration.integrationproviderrelationship_set.first()
if not provider_relationship:
return Connection(
is_connected=False, error="No provider associated with this integration"
)
credentials = (
integration.credentials
if integration.credentials
else provider_relationship.provider.secret.secret
)
connection = SecurityHub.test_connection(
aws_account_id=provider_relationship.provider.uid,
raise_on_exception=False,
**credentials,
)
# Only save regions if connection is successful
if connection.is_connected:
regions_status = {r: True for r in connection.enabled_regions}
regions_status.update({r: False for r in connection.disabled_regions})
# Save regions information in the integration configuration
integration.configuration["regions"] = regions_status
integration.save()
return connection
elif integration.integration_type == Integration.IntegrationChoices.JIRA:
pass
elif integration.integration_type == Integration.IntegrationChoices.SLACK:
@@ -52,6 +52,21 @@ class S3ConfigSerializer(BaseValidateSerializer):
resource_name = "integrations"
class SecurityHubConfigSerializer(BaseValidateSerializer):
send_only_fails = serializers.BooleanField(default=False)
archive_previous_findings = serializers.BooleanField(default=False)
regions = serializers.DictField(default=dict, read_only=True)
def to_internal_value(self, data):
validated_data = super().to_internal_value(data)
# Always initialize regions as empty dict
validated_data["regions"] = {}
return validated_data
class Meta:
resource_name = "integrations"
class AWSCredentialSerializer(BaseValidateSerializer):
role_arn = serializers.CharField(required=False)
external_id = serializers.CharField(required=False)
@@ -146,6 +161,22 @@ class IntegrationCredentialField(serializers.JSONField):
},
"required": ["bucket_name"],
},
{
"type": "object",
"title": "AWS Security Hub",
"properties": {
"send_only_fails": {
"type": "boolean",
"default": False,
"description": "If true, only findings with status 'FAIL' will be sent to Security Hub.",
},
"archive_previous_findings": {
"type": "boolean",
"default": False,
"description": "If true, archives findings that are not present in the current execution.",
},
},
},
]
}
)
+70 -14
View File
@@ -46,6 +46,7 @@ from api.v1.serializer_utils.integrations import (
IntegrationConfigField,
IntegrationCredentialField,
S3ConfigSerializer,
SecurityHubConfigSerializer,
)
from api.v1.serializer_utils.processors import ProcessorConfigField
from api.v1.serializer_utils.providers import ProviderSecretField
@@ -1951,13 +1952,44 @@ class ScheduleDailyCreateSerializer(serializers.Serializer):
class BaseWriteIntegrationSerializer(BaseWriteSerializer):
def validate(self, attrs):
if Integration.objects.filter(
configuration=attrs.get("configuration")
).exists():
if (
attrs.get("integration_type") == Integration.IntegrationChoices.AMAZON_S3
and Integration.objects.filter(
configuration=attrs.get("configuration")
).exists()
):
raise serializers.ValidationError(
{"name": "This integration already exists."}
{"configuration": "This integration already exists."}
)
# Check if any provider already has a SecurityHub integration
integration_type = attrs.get("integration_type")
if hasattr(self, "instance") and self.instance and not integration_type:
integration_type = self.instance.integration_type
if (
integration_type == Integration.IntegrationChoices.AWS_SECURITY_HUB
and "providers" in attrs
):
providers = attrs.get("providers", [])
tenant_id = self.context.get("tenant_id")
for provider in providers:
# For updates, exclude the current instance from the check
query = IntegrationProviderRelationship.objects.filter(
provider=provider,
integration__integration_type=Integration.IntegrationChoices.AWS_SECURITY_HUB,
tenant_id=tenant_id,
)
if hasattr(self, "instance") and self.instance:
query = query.exclude(integration=self.instance)
if query.exists():
raise serializers.ValidationError(
{
"providers": f"Provider {provider.id} already has a Security Hub integration. Only one Security Hub integration is allowed per provider."
}
)
return super().validate(attrs)
@staticmethod
@@ -1970,14 +2002,22 @@ class BaseWriteIntegrationSerializer(BaseWriteSerializer):
if integration_type == Integration.IntegrationChoices.AMAZON_S3:
config_serializer = S3ConfigSerializer
credentials_serializers = [AWSCredentialSerializer]
# TODO: This will be required for AWS Security Hub
# if providers and not all(
# provider.provider == Provider.ProviderChoices.AWS
# for provider in providers
# ):
# raise serializers.ValidationError(
# {"providers": "All providers must be AWS for the S3 integration."}
# )
elif integration_type == Integration.IntegrationChoices.AWS_SECURITY_HUB:
if providers:
if len(providers) > 1:
raise serializers.ValidationError(
{
"providers": "Only one provider is supported for the Security Hub integration."
}
)
if providers[0].provider != Provider.ProviderChoices.AWS:
raise serializers.ValidationError(
{
"providers": "The provider must be AWS type for the Security Hub integration."
}
)
config_serializer = SecurityHubConfigSerializer
credentials_serializers = [AWSCredentialSerializer]
else:
raise serializers.ValidationError(
{
@@ -2077,6 +2117,16 @@ class IntegrationCreateSerializer(BaseWriteIntegrationSerializer):
configuration = attrs.get("configuration")
credentials = attrs.get("credentials")
if (
not providers
and integration_type == Integration.IntegrationChoices.AWS_SECURITY_HUB
):
raise serializers.ValidationError(
{
"providers": "At least one provider is required for the Security Hub integration."
}
)
self.validate_integration_data(
integration_type, providers, configuration, credentials
)
@@ -2131,16 +2181,15 @@ class IntegrationUpdateSerializer(BaseWriteIntegrationSerializer):
}
def validate(self, attrs):
super().validate(attrs)
integration_type = self.instance.integration_type
providers = attrs.get("providers")
configuration = attrs.get("configuration") or self.instance.configuration
credentials = attrs.get("credentials") or self.instance.credentials
validated_attrs = super().validate(attrs)
self.validate_integration_data(
integration_type, providers, configuration, credentials
)
validated_attrs = super().validate(attrs)
return validated_attrs
def update(self, instance, validated_data):
@@ -2155,6 +2204,13 @@ class IntegrationUpdateSerializer(BaseWriteIntegrationSerializer):
]
IntegrationProviderRelationship.objects.bulk_create(new_relationships)
# Preserve regions field for Security Hub integrations
if instance.integration_type == Integration.IntegrationChoices.AWS_SECURITY_HUB:
if "configuration" in validated_data:
# Preserve the existing regions field if it exists
existing_regions = instance.configuration.get("regions", {})
validated_data["configuration"]["regions"] = existing_regions
return super().update(instance, validated_data)
+1 -1
View File
@@ -293,7 +293,7 @@ class SchemaView(SpectacularAPIView):
def get(self, request, *args, **kwargs):
spectacular_settings.TITLE = "Prowler API"
spectacular_settings.VERSION = "1.11.0"
spectacular_settings.VERSION = "1.12.0"
spectacular_settings.DESCRIPTION = (
"Prowler API specification.\n\nThis file is auto-generated."
)
+3
View File
@@ -13,8 +13,10 @@ from api.models import Scan
from prowler.config.config import (
csv_file_suffix,
html_file_suffix,
json_asff_file_suffix,
json_ocsf_file_suffix,
)
from prowler.lib.outputs.asff.asff import ASFF
from prowler.lib.outputs.compliance.aws_well_architected.aws_well_architected import (
AWSWellArchitected,
)
@@ -109,6 +111,7 @@ OUTPUT_FORMATS_MAPPING = {
"kwargs": {},
},
"json-ocsf": {"class": OCSF, "suffix": json_ocsf_file_suffix, "kwargs": {}},
"json-asff": {"class": ASFF, "suffix": json_asff_file_suffix, "kwargs": {}},
"html": {"class": HTML, "suffix": html_file_suffix, "kwargs": {"stats": {}}},
}
+269 -1
View File
@@ -2,15 +2,21 @@ import os
from glob import glob
from celery.utils.log import get_task_logger
from config.django.base import DJANGO_FINDINGS_BATCH_SIZE
from tasks.utils import batched
from api.db_utils import rls_transaction
from api.models import Integration
from api.models import Finding, Integration, Provider
from api.utils import initialize_prowler_provider
from prowler.lib.outputs.asff.asff import ASFF
from prowler.lib.outputs.compliance.generic.generic import GenericCompliance
from prowler.lib.outputs.csv.csv import CSV
from prowler.lib.outputs.finding import Finding as FindingOutput
from prowler.lib.outputs.html.html import HTML
from prowler.lib.outputs.ocsf.ocsf import OCSF
from prowler.providers.aws.aws_provider import AwsProvider
from prowler.providers.aws.lib.s3.s3 import S3
from prowler.providers.aws.lib.security_hub.security_hub import SecurityHub
from prowler.providers.common.models import Connection
logger = get_task_logger(__name__)
@@ -154,3 +160,265 @@ def upload_s3_integration(
except Exception as e:
logger.error(f"S3 integrations failed for provider {provider_id}: {str(e)}")
return False
def get_security_hub_client_from_integration(
integration: Integration, tenant_id: str, findings: list
) -> tuple[bool, SecurityHub | Connection]:
"""
Create and return a SecurityHub client using AWS credentials from an integration.
Args:
integration (Integration): The integration to get the Security Hub client from.
tenant_id (str): The tenant identifier.
findings (list): List of findings in ASFF format to send to Security Hub.
Returns:
tuple[bool, SecurityHub | Connection]: A tuple containing a boolean indicating
if the connection was successful and the SecurityHub client or connection object.
"""
# Get the provider associated with this integration
with rls_transaction(tenant_id):
provider_relationship = integration.integrationproviderrelationship_set.first()
if not provider_relationship:
return Connection(
is_connected=False, error="No provider associated with this integration"
)
provider_uid = provider_relationship.provider.uid
provider_secret = provider_relationship.provider.secret.secret
credentials = (
integration.credentials if integration.credentials else provider_secret
)
connection = SecurityHub.test_connection(
aws_account_id=provider_uid,
raise_on_exception=False,
**credentials,
)
if connection.is_connected:
all_security_hub_regions = AwsProvider.get_available_aws_service_regions(
"securityhub", connection.partition
)
# Create regions status dictionary
regions_status = {}
for region in set(all_security_hub_regions):
regions_status[region] = region in connection.enabled_regions
# Save regions information in the integration configuration
with rls_transaction(tenant_id):
integration.configuration["regions"] = regions_status
integration.save()
# Create SecurityHub client with all necessary parameters
security_hub = SecurityHub(
aws_account_id=provider_uid,
findings=findings,
send_only_fails=integration.configuration.get("send_only_fails", False),
aws_security_hub_available_regions=list(connection.enabled_regions),
**credentials,
)
return True, security_hub
return False, connection
def upload_security_hub_integration(
tenant_id: str, provider_id: str, scan_id: str
) -> bool:
"""
Upload findings to AWS Security Hub using configured integrations.
This function retrieves findings from the database, transforms them to ASFF format,
and sends them to AWS Security Hub using the configured integration credentials.
Args:
tenant_id (str): The tenant identifier.
provider_id (str): The provider identifier.
scan_id (str): The scan identifier for which to send findings.
Returns:
bool: True if all integrations executed successfully, False otherwise.
"""
logger.info(f"Processing Security Hub integrations for provider {provider_id}")
try:
with rls_transaction(tenant_id):
# Get Security Hub integrations for this provider
integrations = list(
Integration.objects.filter(
integrationproviderrelationship__provider_id=provider_id,
integration_type=Integration.IntegrationChoices.AWS_SECURITY_HUB,
enabled=True,
)
)
if not integrations:
logger.error(
f"No Security Hub integrations found for provider {provider_id}"
)
return False
# Get the provider object
provider = Provider.objects.get(id=provider_id)
# Initialize prowler provider for finding transformation
prowler_provider = initialize_prowler_provider(provider)
# Process each Security Hub integration
integration_executions = 0
total_findings_sent = {} # Track findings sent per integration
for integration in integrations:
try:
# Initialize Security Hub client for this integration
# We'll create the client once and reuse it for all batches
security_hub_client = None
send_only_fails = integration.configuration.get(
"send_only_fails", False
)
total_findings_sent[integration.id] = 0
# Process findings in batches to avoid memory issues
has_findings = False
batch_number = 0
with rls_transaction(tenant_id):
qs = (
Finding.all_objects.filter(tenant_id=tenant_id, scan_id=scan_id)
.order_by("uid")
.iterator()
)
for batch, _ in batched(qs, DJANGO_FINDINGS_BATCH_SIZE):
batch_number += 1
has_findings = True
# Transform findings for this batch
transformed_findings = [
FindingOutput.transform_api_finding(
finding, prowler_provider
)
for finding in batch
]
# Convert to ASFF format
asff_transformer = ASFF(
findings=transformed_findings,
file_path="",
file_extension="json",
)
asff_transformer.transform(transformed_findings)
# Get the batch of ASFF findings
batch_asff_findings = asff_transformer.data
if batch_asff_findings:
# Create Security Hub client for first batch or reuse existing
if not security_hub_client:
connected, security_hub = (
get_security_hub_client_from_integration(
integration, tenant_id, batch_asff_findings
)
)
if not connected:
logger.error(
f"Security Hub connection failed for integration {integration.id}: {security_hub.error}"
)
integration.connected = False
integration.save()
break # Skip this integration
security_hub_client = security_hub
logger.info(
f"Sending {'fail' if send_only_fails else 'all'} findings to Security Hub via integration {integration.id}"
)
else:
# Update findings in existing client for this batch
security_hub_client._findings_per_region = (
security_hub_client.filter(
batch_asff_findings, send_only_fails
)
)
# Send this batch to Security Hub
try:
findings_sent = (
security_hub_client.batch_send_to_security_hub()
)
total_findings_sent[integration.id] += findings_sent
if findings_sent > 0:
logger.debug(
f"Sent batch {batch_number} with {findings_sent} findings to Security Hub"
)
except Exception as batch_error:
logger.error(
f"Failed to send batch {batch_number} to Security Hub: {str(batch_error)}"
)
# Clear memory after processing each batch
asff_transformer._data.clear()
del batch_asff_findings
del transformed_findings
if not has_findings:
logger.info(
f"No findings to send to Security Hub for scan {scan_id}"
)
integration_executions += 1
elif security_hub_client:
if total_findings_sent[integration.id] > 0:
logger.info(
f"Successfully sent {total_findings_sent[integration.id]} total findings to Security Hub via integration {integration.id}"
)
integration_executions += 1
else:
logger.warning(
f"No findings were sent to Security Hub via integration {integration.id}"
)
# Archive previous findings if configured to do so
if integration.configuration.get(
"archive_previous_findings", False
):
logger.info(
f"Archiving previous findings in Security Hub via integration {integration.id}"
)
try:
findings_archived = (
security_hub_client.archive_previous_findings()
)
logger.info(
f"Successfully archived {findings_archived} previous findings in Security Hub"
)
except Exception as archive_error:
logger.warning(
f"Failed to archive previous findings: {str(archive_error)}"
)
except Exception as e:
logger.error(
f"Security Hub integration {integration.id} failed: {str(e)}"
)
continue
result = integration_executions == len(integrations)
if result:
logger.info(
f"All Security Hub integrations completed successfully for provider {provider_id}"
)
else:
logger.error(
f"Some Security Hub integrations failed for provider {provider_id}"
)
return result
except Exception as e:
logger.error(
f"Security Hub integrations failed for provider {provider_id}: {str(e)}"
)
return False
+58 -4
View File
@@ -21,7 +21,10 @@ from tasks.jobs.export import (
_generate_output_directory,
_upload_to_s3,
)
from tasks.jobs.integrations import upload_s3_integration
from tasks.jobs.integrations import (
upload_s3_integration,
upload_security_hub_integration,
)
from tasks.jobs.scan import (
aggregate_findings,
create_compliance_requirements,
@@ -62,6 +65,7 @@ def _perform_scan_complete_tasks(tenant_id: str, scan_id: str, provider_id: str)
check_integrations_task.si(
tenant_id=tenant_id,
provider_id=provider_id,
scan_id=scan_id,
),
).apply_async()
@@ -323,12 +327,30 @@ def generate_outputs_task(scan_id: str, provider_id: str, tenant_id: str):
ScanSummary.objects.filter(scan_id=scan_id)
)
qs = Finding.all_objects.filter(scan_id=scan_id).order_by("uid").iterator()
# Check if we need to generate ASFF output for AWS providers with SecurityHub integration
generate_asff = False
if provider_type == "aws":
security_hub_integrations = Integration.objects.filter(
integrationproviderrelationship__provider_id=provider_id,
integration_type=Integration.IntegrationChoices.AWS_SECURITY_HUB,
enabled=True,
)
generate_asff = security_hub_integrations.exists()
qs = (
Finding.all_objects.filter(tenant_id=tenant_id, scan_id=scan_id)
.order_by("uid")
.iterator()
)
for batch, is_last in batched(qs, DJANGO_FINDINGS_BATCH_SIZE):
fos = [FindingOutput.transform_api_finding(f, prowler_provider) for f in batch]
# Outputs
for mode, cfg in OUTPUT_FORMATS_MAPPING.items():
# Skip ASFF generation if not needed
if mode == "json-asff" and not generate_asff:
continue
cls = cfg["class"]
suffix = cfg["suffix"]
extra = cfg.get("kwargs", {}).copy()
@@ -474,17 +496,19 @@ def check_lighthouse_connection_task(lighthouse_config_id: str, tenant_id: str =
@shared_task(name="integration-check")
def check_integrations_task(tenant_id: str, provider_id: str):
def check_integrations_task(tenant_id: str, provider_id: str, scan_id: str = None):
"""
Check and execute all configured integrations for a provider.
Args:
tenant_id (str): The tenant identifier
provider_id (str): The provider identifier
scan_id (str, optional): The scan identifier for integrations that need scan data
"""
logger.info(f"Checking integrations for provider {provider_id}")
try:
integration_tasks = []
with rls_transaction(tenant_id):
integrations = Integration.objects.filter(
integrationproviderrelationship__provider_id=provider_id,
@@ -495,7 +519,16 @@ def check_integrations_task(tenant_id: str, provider_id: str):
logger.info(f"No integrations configured for provider {provider_id}")
return {"integrations_processed": 0}
integration_tasks = []
# Security Hub integration
security_hub_integrations = integrations.filter(
integration_type=Integration.IntegrationChoices.AWS_SECURITY_HUB
)
if security_hub_integrations.exists():
integration_tasks.append(
security_hub_integration_task.s(
tenant_id=tenant_id, provider_id=provider_id, scan_id=scan_id
)
)
# TODO: Add other integration types here
# slack_integrations = integrations.filter(
@@ -541,3 +574,24 @@ def s3_integration_task(
output_directory (str): Path to the directory containing output files
"""
return upload_s3_integration(tenant_id, provider_id, output_directory)
@shared_task(
base=RLSTask,
name="integration-security-hub",
queue="integrations",
)
def security_hub_integration_task(
tenant_id: str,
provider_id: str,
scan_id: str,
):
"""
Process Security Hub integrations for a provider.
Args:
tenant_id (str): The tenant identifier
provider_id (str): The provider identifier
scan_id (str): The scan identifier
"""
return upload_security_hub_integration(tenant_id, provider_id, scan_id)
File diff suppressed because it is too large Load Diff
+440 -9
View File
@@ -7,6 +7,7 @@ from tasks.tasks import (
check_integrations_task,
generate_outputs_task,
s3_integration_task,
security_hub_integration_task,
)
from api.models import Integration
@@ -521,31 +522,68 @@ class TestCheckIntegrationsTask:
enabled=True,
)
@patch("tasks.tasks.security_hub_integration_task")
@patch("tasks.tasks.group")
@patch("tasks.tasks.rls_transaction")
@patch("tasks.tasks.Integration.objects.filter")
def test_check_integrations_s3_success(
self, mock_integration_filter, mock_rls, mock_group
def test_check_integrations_security_hub_success(
self, mock_integration_filter, mock_rls, mock_group, mock_security_hub_task
):
# Mock that we have some integrations
mock_integration_filter.return_value.exists.return_value = True
"""Test that SecurityHub integrations are processed correctly."""
# Mock that we have SecurityHub integrations
mock_integrations = MagicMock()
mock_integrations.exists.return_value = True
# Mock SecurityHub integrations to return existing integrations
mock_security_hub_integrations = MagicMock()
mock_security_hub_integrations.exists.return_value = True
# Set up the filter chain
mock_integration_filter.return_value = mock_integrations
mock_integrations.filter.return_value = mock_security_hub_integrations
# Mock the task signature
mock_task_signature = MagicMock()
mock_security_hub_task.s.return_value = mock_task_signature
# Mock group job
mock_job = MagicMock()
mock_group.return_value = mock_job
# Ensure rls_transaction is mocked
mock_rls.return_value.__enter__.return_value = None
# Since the current implementation doesn't actually create tasks yet (TODO comment),
# we test that no tasks are created but the function returns the correct count
# Execute the function
result = check_integrations_task(
tenant_id=self.tenant_id,
provider_id=self.provider_id,
scan_id="test-scan-id",
)
assert result == {"integrations_processed": 0}
# Should process 1 SecurityHub integration
assert result == {"integrations_processed": 1}
# Verify the integration filter was called
mock_integration_filter.assert_called_once_with(
integrationproviderrelationship__provider_id=self.provider_id,
enabled=True,
)
# group should not be called since no integration tasks are created yet
mock_group.assert_not_called()
# Verify SecurityHub integrations were filtered
mock_integrations.filter.assert_called_once_with(
integration_type=Integration.IntegrationChoices.AWS_SECURITY_HUB
)
# Verify SecurityHub task was created with correct parameters
mock_security_hub_task.s.assert_called_once_with(
tenant_id=self.tenant_id,
provider_id=self.provider_id,
scan_id="test-scan-id",
)
# Verify group was called and job was executed
mock_group.assert_called_once_with([mock_task_signature])
mock_job.apply_async.assert_called_once()
@patch("tasks.tasks.rls_transaction")
@patch("tasks.tasks.Integration.objects.filter")
@@ -567,6 +605,369 @@ class TestCheckIntegrationsTask:
enabled=True,
)
@patch("tasks.tasks.s3_integration_task")
@patch("tasks.tasks.Integration.objects.filter")
@patch("tasks.tasks.ScanSummary.objects.filter")
@patch("tasks.tasks.Provider.objects.get")
@patch("tasks.tasks.initialize_prowler_provider")
@patch("tasks.tasks.Compliance.get_bulk")
@patch("tasks.tasks.get_compliance_frameworks")
@patch("tasks.tasks.Finding.all_objects.filter")
@patch("tasks.tasks._generate_output_directory")
@patch("tasks.tasks.FindingOutput._transform_findings_stats")
@patch("tasks.tasks.FindingOutput.transform_api_finding")
@patch("tasks.tasks._compress_output_files")
@patch("tasks.tasks._upload_to_s3")
@patch("tasks.tasks.Scan.all_objects.filter")
@patch("tasks.tasks.rmtree")
def test_generate_outputs_with_asff_for_aws_with_security_hub(
self,
mock_rmtree,
mock_scan_update,
mock_upload,
mock_compress,
mock_transform_finding,
mock_transform_stats,
mock_generate_dir,
mock_findings,
mock_get_frameworks,
mock_compliance_bulk,
mock_initialize_provider,
mock_provider_get,
mock_scan_summary,
mock_integration_filter,
mock_s3_task,
):
"""Test that ASFF output is generated for AWS providers with SecurityHub integration."""
# Setup
mock_scan_summary_qs = MagicMock()
mock_scan_summary_qs.exists.return_value = True
mock_scan_summary.return_value = mock_scan_summary_qs
# Mock AWS provider
mock_provider = MagicMock()
mock_provider.uid = "aws-account-123"
mock_provider.provider = "aws"
mock_provider_get.return_value = mock_provider
# Mock SecurityHub integration exists
mock_security_hub_integrations = MagicMock()
mock_security_hub_integrations.exists.return_value = True
mock_integration_filter.return_value = mock_security_hub_integrations
# Mock s3_integration_task
mock_s3_task.apply_async.return_value.get.return_value = True
# Mock other necessary components
mock_initialize_provider.return_value = MagicMock()
mock_compliance_bulk.return_value = {}
mock_get_frameworks.return_value = []
mock_generate_dir.return_value = ("out-dir", "comp-dir")
mock_transform_stats.return_value = {"stats": "data"}
# Mock findings
mock_finding = MagicMock()
mock_findings.return_value.order_by.return_value.iterator.return_value = [
[mock_finding],
True,
]
mock_transform_finding.return_value = MagicMock(compliance={})
# Track which output formats were created
created_writers = {}
def track_writer_creation(cls_type):
def factory(*args, **kwargs):
writer = MagicMock()
writer._data = []
writer.transform = MagicMock()
writer.batch_write_data_to_file = MagicMock()
created_writers[cls_type] = writer
return writer
return factory
# Mock OUTPUT_FORMATS_MAPPING with tracking
with patch(
"tasks.tasks.OUTPUT_FORMATS_MAPPING",
{
"csv": {
"class": track_writer_creation("csv"),
"suffix": ".csv",
"kwargs": {},
},
"json-asff": {
"class": track_writer_creation("asff"),
"suffix": ".asff.json",
"kwargs": {},
},
"json-ocsf": {
"class": track_writer_creation("ocsf"),
"suffix": ".ocsf.json",
"kwargs": {},
},
},
):
mock_compress.return_value = "/tmp/compressed.zip"
mock_upload.return_value = "s3://bucket/file.zip"
# Execute
result = generate_outputs_task(
scan_id=self.scan_id,
provider_id=self.provider_id,
tenant_id=self.tenant_id,
)
# Verify ASFF was created for AWS with SecurityHub
assert "asff" in created_writers, "ASFF writer should be created"
assert "csv" in created_writers, "CSV writer should be created"
assert "ocsf" in created_writers, "OCSF writer should be created"
# Verify SecurityHub integration was checked
assert mock_integration_filter.call_count == 2
mock_integration_filter.assert_any_call(
integrationproviderrelationship__provider_id=self.provider_id,
integration_type=Integration.IntegrationChoices.AWS_SECURITY_HUB,
enabled=True,
)
assert result == {"upload": True}
@patch("tasks.tasks.s3_integration_task")
@patch("tasks.tasks.Integration.objects.filter")
@patch("tasks.tasks.ScanSummary.objects.filter")
@patch("tasks.tasks.Provider.objects.get")
@patch("tasks.tasks.initialize_prowler_provider")
@patch("tasks.tasks.Compliance.get_bulk")
@patch("tasks.tasks.get_compliance_frameworks")
@patch("tasks.tasks.Finding.all_objects.filter")
@patch("tasks.tasks._generate_output_directory")
@patch("tasks.tasks.FindingOutput._transform_findings_stats")
@patch("tasks.tasks.FindingOutput.transform_api_finding")
@patch("tasks.tasks._compress_output_files")
@patch("tasks.tasks._upload_to_s3")
@patch("tasks.tasks.Scan.all_objects.filter")
@patch("tasks.tasks.rmtree")
def test_generate_outputs_no_asff_for_aws_without_security_hub(
self,
mock_rmtree,
mock_scan_update,
mock_upload,
mock_compress,
mock_transform_finding,
mock_transform_stats,
mock_generate_dir,
mock_findings,
mock_get_frameworks,
mock_compliance_bulk,
mock_initialize_provider,
mock_provider_get,
mock_scan_summary,
mock_integration_filter,
mock_s3_task,
):
"""Test that ASFF output is NOT generated for AWS providers without SecurityHub integration."""
# Setup
mock_scan_summary_qs = MagicMock()
mock_scan_summary_qs.exists.return_value = True
mock_scan_summary.return_value = mock_scan_summary_qs
# Mock AWS provider
mock_provider = MagicMock()
mock_provider.uid = "aws-account-123"
mock_provider.provider = "aws"
mock_provider_get.return_value = mock_provider
# Mock NO SecurityHub integration
mock_security_hub_integrations = MagicMock()
mock_security_hub_integrations.exists.return_value = False
mock_integration_filter.return_value = mock_security_hub_integrations
# Mock other necessary components
mock_initialize_provider.return_value = MagicMock()
mock_compliance_bulk.return_value = {}
mock_get_frameworks.return_value = []
mock_generate_dir.return_value = ("out-dir", "comp-dir")
mock_transform_stats.return_value = {"stats": "data"}
# Mock findings
mock_finding = MagicMock()
mock_findings.return_value.order_by.return_value.iterator.return_value = [
[mock_finding],
True,
]
mock_transform_finding.return_value = MagicMock(compliance={})
# Track which output formats were created
created_writers = {}
def track_writer_creation(cls_type):
def factory(*args, **kwargs):
writer = MagicMock()
writer._data = []
writer.transform = MagicMock()
writer.batch_write_data_to_file = MagicMock()
created_writers[cls_type] = writer
return writer
return factory
# Mock OUTPUT_FORMATS_MAPPING with tracking
with patch(
"tasks.tasks.OUTPUT_FORMATS_MAPPING",
{
"csv": {
"class": track_writer_creation("csv"),
"suffix": ".csv",
"kwargs": {},
},
"json-asff": {
"class": track_writer_creation("asff"),
"suffix": ".asff.json",
"kwargs": {},
},
"json-ocsf": {
"class": track_writer_creation("ocsf"),
"suffix": ".ocsf.json",
"kwargs": {},
},
},
):
mock_compress.return_value = "/tmp/compressed.zip"
mock_upload.return_value = "s3://bucket/file.zip"
# Execute
result = generate_outputs_task(
scan_id=self.scan_id,
provider_id=self.provider_id,
tenant_id=self.tenant_id,
)
# Verify ASFF was NOT created when no SecurityHub integration
assert "asff" not in created_writers, "ASFF writer should NOT be created"
assert "csv" in created_writers, "CSV writer should be created"
assert "ocsf" in created_writers, "OCSF writer should be created"
# Verify SecurityHub integration was checked
assert mock_integration_filter.call_count == 2
mock_integration_filter.assert_any_call(
integrationproviderrelationship__provider_id=self.provider_id,
integration_type=Integration.IntegrationChoices.AWS_SECURITY_HUB,
enabled=True,
)
assert result == {"upload": True}
@patch("tasks.tasks.ScanSummary.objects.filter")
@patch("tasks.tasks.Provider.objects.get")
@patch("tasks.tasks.initialize_prowler_provider")
@patch("tasks.tasks.Compliance.get_bulk")
@patch("tasks.tasks.get_compliance_frameworks")
@patch("tasks.tasks.Finding.all_objects.filter")
@patch("tasks.tasks._generate_output_directory")
@patch("tasks.tasks.FindingOutput._transform_findings_stats")
@patch("tasks.tasks.FindingOutput.transform_api_finding")
@patch("tasks.tasks._compress_output_files")
@patch("tasks.tasks._upload_to_s3")
@patch("tasks.tasks.Scan.all_objects.filter")
@patch("tasks.tasks.rmtree")
def test_generate_outputs_no_asff_for_non_aws_provider(
self,
mock_rmtree,
mock_scan_update,
mock_upload,
mock_compress,
mock_transform_finding,
mock_transform_stats,
mock_generate_dir,
mock_findings,
mock_get_frameworks,
mock_compliance_bulk,
mock_initialize_provider,
mock_provider_get,
mock_scan_summary,
):
"""Test that ASFF output is NOT generated for non-AWS providers (e.g., Azure, GCP)."""
# Setup
mock_scan_summary_qs = MagicMock()
mock_scan_summary_qs.exists.return_value = True
mock_scan_summary.return_value = mock_scan_summary_qs
# Mock Azure provider (non-AWS)
mock_provider = MagicMock()
mock_provider.uid = "azure-subscription-123"
mock_provider.provider = "azure" # Non-AWS provider
mock_provider_get.return_value = mock_provider
# Mock other necessary components
mock_initialize_provider.return_value = MagicMock()
mock_compliance_bulk.return_value = {}
mock_get_frameworks.return_value = []
mock_generate_dir.return_value = ("out-dir", "comp-dir")
mock_transform_stats.return_value = {"stats": "data"}
# Mock findings
mock_finding = MagicMock()
mock_findings.return_value.order_by.return_value.iterator.return_value = [
[mock_finding],
True,
]
mock_transform_finding.return_value = MagicMock(compliance={})
# Track which output formats were created
created_writers = {}
def track_writer_creation(cls_type):
def factory(*args, **kwargs):
writer = MagicMock()
writer._data = []
writer.transform = MagicMock()
writer.batch_write_data_to_file = MagicMock()
created_writers[cls_type] = writer
return writer
return factory
# Mock OUTPUT_FORMATS_MAPPING with tracking
with patch(
"tasks.tasks.OUTPUT_FORMATS_MAPPING",
{
"csv": {
"class": track_writer_creation("csv"),
"suffix": ".csv",
"kwargs": {},
},
"json-asff": {
"class": track_writer_creation("asff"),
"suffix": ".asff.json",
"kwargs": {},
},
"json-ocsf": {
"class": track_writer_creation("ocsf"),
"suffix": ".ocsf.json",
"kwargs": {},
},
},
):
mock_compress.return_value = "/tmp/compressed.zip"
mock_upload.return_value = "s3://bucket/file.zip"
# Execute
result = generate_outputs_task(
scan_id=self.scan_id,
provider_id=self.provider_id,
tenant_id=self.tenant_id,
)
# Verify ASFF was NOT created for non-AWS provider
assert (
"asff" not in created_writers
), "ASFF writer should NOT be created for non-AWS providers"
assert "csv" in created_writers, "CSV writer should be created"
assert "ocsf" in created_writers, "OCSF writer should be created"
assert result == {"upload": True}
@patch("tasks.tasks.upload_s3_integration")
def test_s3_integration_task_success(self, mock_upload):
mock_upload.return_value = True
@@ -598,3 +999,33 @@ class TestCheckIntegrationsTask:
mock_upload.assert_called_once_with(
self.tenant_id, self.provider_id, output_directory
)
@patch("tasks.tasks.upload_security_hub_integration")
def test_security_hub_integration_task_success(self, mock_upload):
"""Test successful SecurityHub integration task execution."""
mock_upload.return_value = True
scan_id = "test-scan-123"
result = security_hub_integration_task(
tenant_id=self.tenant_id,
provider_id=self.provider_id,
scan_id=scan_id,
)
assert result is True
mock_upload.assert_called_once_with(self.tenant_id, self.provider_id, scan_id)
@patch("tasks.tasks.upload_security_hub_integration")
def test_security_hub_integration_task_failure(self, mock_upload):
"""Test SecurityHub integration task handling failure."""
mock_upload.return_value = False
scan_id = "test-scan-123"
result = security_hub_integration_task(
tenant_id=self.tenant_id,
provider_id=self.provider_id,
scan_id=scan_id,
)
assert result is False
mock_upload.assert_called_once_with(self.tenant_id, self.provider_id, scan_id)
-24
View File
@@ -1,24 +0,0 @@
---
hide:
- toc
---
# About
## Author
Prowler was created by **Toni de la Fuente** in 2016.
| ![](img/toni.png)<br>[![Twitter URL](https://img.shields.io/twitter/url/https/twitter.com/toniblyx.svg?style=social&label=Follow%20%40toniblyx)](https://twitter.com/toniblyx) [![Twitter URL](https://img.shields.io/twitter/url/https/twitter.com/prowlercloud.svg?style=social&label=Follow%20%40prowlercloud)](https://twitter.com/prowlercloud)|
|:--:|
| <b>Toni de la Fuente </b>|
## Maintainers
Prowler is maintained by the Engineers of the **Prowler Team** :
| ![](img/nacho.png)[![Twitter URL](https://img.shields.io/twitter/url/https/twitter.com/NachoRivCor.svg?style=social&label=Follow%20%40NachoRivCor)](https://twitter.com/NachoRivCor) | ![](img/sergio.png)[![Twitter URL](https://img.shields.io/twitter/url/https/twitter.com/sergargar1.svg?style=social&label=Follow%20%40sergargar1)](https://twitter.com/sergargar1) |![](img/pepe.png)[![Twitter URL](https://img.shields.io/twitter/url/https/twitter.com/jfagoagas.svg?style=social&label=Follow%20%40jfagoagas)](https://twitter.com/jfagoagas) |
|:--:|:--:|:--:
| <b>Nacho Rivera</b>| <b>Sergio Garcia</b>| <b>Pepe Fagoaga</b>|
## License
Prowler is licensed as **Apache License 2.0** as specified in each file. You may obtain a copy of the License at
<http://www.apache.org/licenses/LICENSE-2.0>
+61
View File
@@ -0,0 +1,61 @@
## Access Prowler App
After [installation](../installation/prowler-app.md), navigate to [http://localhost:3000](http://localhost:3000) and sign up with email and password.
<img src="../../img/sign-up-button.png" alt="Sign Up Button" width="320"/>
<img src="../../img/sign-up.png" alt="Sign Up" width="285"/>
???+ note "User creation and default tenant behavior"
When creating a new user, the behavior depends on whether an invitation is provided:
- **Without an invitation**:
- A new tenant is automatically created.
- The new user is assigned to this tenant.
- A set of **RBAC admin permissions** is generated and assigned to the user for the newly-created tenant.
- **With an invitation**: The user is added to the specified tenant with the permissions defined in the invitation.
This mechanism ensures that the first user in a newly created tenant has administrative permissions within that tenant.
## Log In
Access Prowler App by logging in with **email and password**.
<img src="../../img/log-in.png" alt="Log In" width="285"/>
## Add Cloud Provider
Configure a cloud provider for scanning:
1. Navigate to `Settings > Cloud Providers` and click `Add Account`.
2. Select the cloud provider.
3. Enter the provider's identifier (Optional: Add an alias):
- **AWS**: Account ID
- **GCP**: Project ID
- **Azure**: Subscription ID
- **Kubernetes**: Cluster ID
- **M365**: Domain ID
4. Follow the guided instructions to add and authenticate your credentials.
## Start a Scan
Once credentials are successfully added and validated, Prowler initiates a scan of your cloud environment.
Click `Go to Scans` to monitor progress.
## View Results
Review findings during scan execution in the following sections:
- **Overview** Provides a high-level summary of your scans.
<img src="../../img/overview.png" alt="Overview" width="700"/>
- **Compliance** Displays compliance insights based on security frameworks.
<img src="../../img/compliance.png" alt="Compliance" width="700"/>
> For detailed usage instructions, refer to the [Prowler App Guide](../tutorials/prowler-app.md).
???+ note
Prowler will automatically scan all configured providers every **24 hours**, ensuring your cloud environment stays continuously monitored.
+257
View File
@@ -0,0 +1,257 @@
## Running Prowler
Running Prowler requires specifying the provider (e.g `aws`, `gcp`, `azure`, `m365`, `github` or `kubernetes`):
???+ note
If no provider is specified, AWS is used by default for backward compatibility with Prowler v2.
```console
prowler <provider>
```
![Prowler Execution](../img/short-display.png)
???+ note
Running the `prowler` command without options will uses environment variable credentials. Refer to the Authentication section of each provider for credential configuration details.
## Verbose Output
If you prefer the former verbose output, use: `--verbose`. This allows seeing more info while Prowler is running, minimal output is displayed unless verbosity is enabled.
## Report Generation
By default, Prowler generates CSV, JSON-OCSF, and HTML reports. To generate a JSON-ASFF report (used by AWS Security Hub), specify `-M` or `--output-modes`:
```console
prowler <provider> -M csv json-asff json-ocsf html
```
The HTML report is saved in the output directory, alongside other reports. It will look like this:
![Prowler Execution](../img/html-output.png)
## Listing Available Checks and Services
List all available checks or services within a provider using `-l`/`--list-checks` or `--list-services`.
```console
prowler <provider> --list-checks
prowler <provider> --list-services
```
## Running Specific Checks or Services
Execute specific checks or services using `-c`/`checks` or `-s`/`services`:
```console
prowler azure --checks storage_blob_public_access_level_is_disabled
prowler aws --services s3 ec2
prowler gcp --services iam compute
prowler kubernetes --services etcd apiserver
```
## Excluding Checks and Services
Checks and services can be excluded with `-e`/`--excluded-checks` or `--excluded-services`:
```console
prowler aws --excluded-checks s3_bucket_public_access
prowler azure --excluded-services defender iam
prowler gcp --excluded-services kms
prowler kubernetes --excluded-services controllermanager
```
## Additional Options
Explore more advanced time-saving execution methods in the [Miscellaneous](../tutorials/misc.md) section.
Access the help menu and view all available options with `-h`/`--help`:
```console
prowler --help
```
## AWS
Use a custom AWS profile with `-p`/`--profile` and/or specific AWS regions with `-f`/`--filter-region`:
```console
prowler aws --profile custom-profile -f us-east-1 eu-south-2
```
???+ note
By default, `prowler` will scan all AWS regions.
See more details about AWS Authentication in the [Authentication Section](../tutorials/aws/authentication.md) section.
## Azure
Azure requires specifying the auth method:
```console
# To use service principal authentication
prowler azure --sp-env-auth
# To use az cli authentication
prowler azure --az-cli-auth
# To use browser authentication
prowler azure --browser-auth --tenant-id "XXXXXXXX"
# To use managed identity auth
prowler azure --managed-identity-auth
```
See more details about Azure Authentication in the [Authentication Section](../tutorials/azure/authentication.md)
By default, Prowler scans all accessible subscriptions. Scan specific subscriptions using the following flag (using az cli auth as example):
```console
prowler azure --az-cli-auth --subscription-ids <subscription ID 1> <subscription ID 2> ... <subscription ID N>
```
## Google Cloud
- **User Account Credentials**
By default, Prowler uses **User Account credentials**. Configure accounts using:
- `gcloud init` Set up a new account.
- `gcloud config set account <account>` Switch to an existing account.
Once configured, obtain access credentials using: `gcloud auth application-default login`.
- **Service Account Authentication**
Alternatively, you can use Service Account credentials:
Generate and download Service Account keys in JSON format. Refer to [Google IAM documentation](https://cloud.google.com/iam/docs/creating-managing-service-account-keys) for details.
Provide the key file location using this argument:
```console
prowler gcp --credentials-file path
```
- **Scanning Specific GCP Projects**
By default, Prowler scans all accessible GCP projects. Scan specific projects with the `--project-ids` flag:
```console
prowler gcp --project-ids <Project ID 1> <Project ID 2> ... <Project ID N>
```
- **GCP Retry Configuration**
Configure the maximum number of retry attempts for Google Cloud SDK API calls with the `--gcp-retries-max-attempts` flag:
```console
prowler gcp --gcp-retries-max-attempts 5
```
This is useful when experiencing quota exceeded errors (HTTP 429) to increase the number of automatic retry attempts.
## Kubernetes
Prowler enables security scanning of Kubernetes clusters, supporting both **in-cluster** and **external** execution.
- **Non In-Cluster Execution**
```console
prowler kubernetes --kubeconfig-file path
```
???+ note
If no `--kubeconfig-file` is provided, Prowler will use the default KubeConfig file location (`~/.kube/config`).
- **In-Cluster Execution**
To run Prowler inside the cluster, apply the provided YAML configuration to deploy a job in a new namespace:
```console
kubectl apply -f kubernetes/prowler-sa.yaml
kubectl apply -f kubernetes/job.yaml
kubectl apply -f kubernetes/prowler-role.yaml
kubectl apply -f kubernetes/prowler-rolebinding.yaml
kubectl get pods --namespace prowler-ns --> prowler-XXXXX
kubectl logs prowler-XXXXX --namespace prowler-ns
```
???+ note
By default, Prowler scans all namespaces in the active Kubernetes context. Use the `--context`flag to specify the context to be scanned and `--namespaces` to restrict scanning to specific namespaces.
## Microsoft 365
Microsoft 365 requires specifying the auth method:
```console
# To use service principal authentication for MSGraph and PowerShell modules
prowler m365 --sp-env-auth
# To use both service principal (for MSGraph) and user credentials (for PowerShell modules)
prowler m365 --env-auth
# To use az cli authentication
prowler m365 --az-cli-auth
# To use browser authentication
prowler m365 --browser-auth --tenant-id "XXXXXXXX"
```
See more details about M365 Authentication in the [Authentication Section](../tutorials/microsoft365/authentication.md) section.
## GitHub
Prowler enables security scanning of your **GitHub account**, including **Repositories**, **Organizations** and **Applications**.
- **Supported Authentication Methods**
Authenticate using one of the following methods:
```console
# Personal Access Token (PAT):
prowler github --personal-access-token pat
# OAuth App Token:
prowler github --oauth-app-token oauth_token
# GitHub App Credentials:
prowler github --github-app-id app_id --github-app-key app_key
```
???+ note
If no login method is explicitly provided, Prowler will automatically attempt to authenticate using environment variables in the following order of precedence:
1. `GITHUB_PERSONAL_ACCESS_TOKEN`
2. `OAUTH_APP_TOKEN`
3. `GITHUB_APP_ID` and `GITHUB_APP_KEY`
## Infrastructure as Code (IaC)
Prowler's Infrastructure as Code (IaC) provider enables you to scan local or remote infrastructure code for security and compliance issues using [Trivy](https://trivy.dev/). This provider supports a wide range of IaC frameworks, allowing you to assess your code before deployment.
```console
# Scan a directory for IaC files
prowler iac --scan-path ./my-iac-directory
# Scan a remote GitHub repository (public or private)
prowler iac --scan-repository-url https://github.com/user/repo.git
# Authenticate to a private repo with GitHub username and PAT
prowler iac --scan-repository-url https://github.com/user/repo.git \
--github-username <username> --personal-access-token <token>
# Authenticate to a private repo with OAuth App Token
prowler iac --scan-repository-url https://github.com/user/repo.git \
--oauth-app-token <oauth_token>
# Specify frameworks to scan (default: all)
prowler iac --scan-path ./my-iac-directory --frameworks terraform kubernetes
# Exclude specific paths
prowler iac --scan-path ./my-iac-directory --exclude-path ./my-iac-directory/test,./my-iac-directory/examples
```
???+ note
- `--scan-path` and `--scan-repository-url` are mutually exclusive; only one can be specified at a time.
- For remote repository scans, authentication can be provided via CLI flags or environment variables (`GITHUB_OAUTH_APP_TOKEN`, `GITHUB_USERNAME`, `GITHUB_PERSONAL_ACCESS_TOKEN`). CLI flags take precedence.
- The IaC provider does not require cloud authentication for local scans.
- It is ideal for CI/CD pipelines and local development environments.
- For more details on supported scanners, see the [Trivy documentation](https://trivy.dev/latest/docs/scanner/vulnerability/)
See more details about IaC scanning in the [IaC Tutorial](../tutorials/iac/getting-started-iac.md) section.
+1 -1
View File
@@ -2,7 +2,7 @@
In this page you can find all the details about [Amazon Web Services (AWS)](https://aws.amazon.com/) provider implementation in Prowler.
By default, Prowler will audit just one account and organization settings per scan. To configure it, follow the [getting started](../index.md#aws) page.
By default, Prowler will audit just one account and organization settings per scan. To configure it, follow the [AWS getting started guide](../tutorials/aws/getting-started-aws.md).
## AWS Provider Classes Architecture
+1 -1
View File
@@ -2,7 +2,7 @@
In this page you can find all the details about [Microsoft Azure](https://azure.microsoft.com/) provider implementation in Prowler.
By default, Prowler will audit all the subscriptions that it is able to list in the Microsoft Entra tenant, and tenant Entra ID service. To configure it, follow the [getting started](../index.md#azure) page.
By default, Prowler will audit all the subscriptions that it is able to list in the Microsoft Entra tenant, and tenant Entra ID service. To configure it, follow the [Azure getting started guide](../tutorials/azure/getting-started-azure.md).
## Azure Provider Classes Architecture
+1 -1
View File
@@ -265,7 +265,7 @@ Below is a generic example of a check metadata file. **Do not include comments i
- For AWS this field must follow the [AWS Security Hub Types](https://docs.aws.amazon.com/securityhub/latest/userguide/asff-required-attributes.html#Types) format. So the common pattern to follow is `namespace/category/classifier`, refer to the attached documentation for the valid values for this fields.
- **ServiceName** — The name of the provider service being audited. This field **must** be in lowercase and match with the service folder name. For supported services refer to [Prowler Hub](https://hub.prowler.com/check) or directly to [Prowler Code](https://github.com/prowler-cloud/prowler/tree/master/prowler/providers).
- **SubServiceName** — The subservice or resource within the service, if applicable. For more information refer to the [Naming Format for Checks](#naming-format-for-checks) section.
- **ResourceIdTemplate** — A template for the unique resource identifier. For more information refer to the [Prowler's Resource Identification](#prowlers-resource-identification) section.
- **ResourceIdTemplate** — A template for the unique resource identifier. For more information refer to the [Resource Identification in Prowler](#resource-identification-in-prowler) section.
- **Severity** — The severity of the finding if the check fails. Must be one of: `critical`, `high`, `medium`, `low`, or `informational`, this field **must** be in lowercase. To get more information about the severity levels refer to the [Prowler's Check Severity Levels](#prowlers-check-severity-levels) section.
- **ResourceType** — The type of resource being audited. *For now this field is only standardized for the AWS provider*.
- For AWS use the [Security Hub resource types](https://docs.aws.amazon.com/securityhub/latest/userguide/asff-resources.html) or, if not available, the PascalCase version of the [CloudFormation type](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-template-resource-type-ref.html) (e.g., `AwsEc2Instance`). Use "Other" if no match exists.
+1 -1
View File
@@ -2,7 +2,7 @@
This page details the [Google Cloud Platform (GCP)](https://cloud.google.com/) provider implementation in Prowler.
By default, Prowler will audit all the GCP projects that the authenticated identity can access. To configure it, follow the [getting started](../index.md#google-cloud) page.
By default, Prowler will audit all the GCP projects that the authenticated identity can access. To configure it, follow the [GCP getting started guide](../tutorials/gcp/getting-started-gcp.md).
## GCP Provider Classes Architecture
+1 -1
View File
@@ -2,7 +2,7 @@
This page details the [GitHub](https://github.com/) provider implementation in Prowler.
By default, Prowler will audit the GitHub account - scanning all repositories, organizations, and applications that your configured credentials can access. To configure it, follow the [getting started](../index.md#github) page.
By default, Prowler will audit the GitHub account - scanning all repositories, organizations, and applications that your configured credentials can access. To configure it, follow the [GitHub getting started guide](../tutorials/github/getting-started-github.md).
## GitHub Provider Classes Architecture
+1 -1
View File
@@ -2,7 +2,7 @@
This page details the [Kubernetes](https://kubernetes.io/) provider implementation in Prowler.
By default, Prowler will audit all namespaces in the Kubernetes cluster accessible by the configured context. To configure it, follow the [getting started](../index.md#kubernetes) page.
By default, Prowler will audit all namespaces in the Kubernetes cluster accessible by the configured context. To configure it, see the [In-Cluster Execution](../tutorials/kubernetes/in-cluster.md) or [Non In-Cluster Execution](../tutorials/kubernetes/outside-cluster.md) guides.
## Kubernetes Provider Classes Architecture
+2 -2
View File
@@ -2,7 +2,7 @@
This page details the [Microsoft 365 (M365)](https://www.microsoft.com/en-us/microsoft-365) provider implementation in Prowler.
By default, Prowler will audit the Microsoft Entra ID tenant and its supported services. To configure it, follow the [getting started](../index.md#microsoft-365) page.
By default, Prowler will audit the Microsoft Entra ID tenant and its supported services. To configure it, follow the [M365 getting started guide](../tutorials/microsoft365/getting-started-m365.md).
---
@@ -15,7 +15,7 @@ By default, Prowler will audit the Microsoft Entra ID tenant and its supported s
- **Required modules:**
- [ExchangeOnlineManagement](https://www.powershellgallery.com/packages/ExchangeOnlineManagement/3.6.0) (≥ 3.6.0)
- [MicrosoftTeams](https://www.powershellgallery.com/packages/MicrosoftTeams/6.6.0) (≥ 6.6.0)
- If you use Prowler Cloud or the official containers, PowerShell is pre-installed. For local or pip installations, you must install PowerShell and the modules yourself. See [Requirements: Supported PowerShell Versions](../getting-started/requirements.md#supported-powershell-versions) and [Needed PowerShell Modules](../getting-started/requirements.md#needed-powershell-modules).
- If you use Prowler Cloud or the official containers, PowerShell is pre-installed. For local or pip installations, you must install PowerShell and the modules yourself. See [Authentication: Supported PowerShell Versions](../tutorials/microsoft365/authentication.md#supported-powershell-versions) and [Needed PowerShell Modules](../tutorials/microsoft365/authentication.md#required-powershell-modules).
- For more details and troubleshooting, see [Use of PowerShell in M365](../tutorials/microsoft365/use-of-powershell.md).
---
+5 -5
View File
@@ -231,11 +231,11 @@ Before implementing a new service, verify that Prowler's existing permissions fo
Provider-Specific Permissions Documentation:
- [AWS](../getting-started/requirements.md#authentication)
- [Azure](../getting-started/requirements.md#needed-permissions)
- [GCP](../getting-started/requirements.md#needed-permissions_1)
- [M365](../getting-started/requirements.md#needed-permissions_2)
- [GitHub](../getting-started/requirements.md#authentication_2)
- [AWS](../tutorials/aws/authentication.md#required-permissions)
- [Azure](../tutorials/azure/authentication.md#required-permissions)
- [GCP](../tutorials/gcp/authentication.md#required-permissions)
- [M365](../tutorials/microsoft365/authentication.md#required-permissions)
- [GitHub](../tutorials/github/authentication.md)
## Best Practices
+2 -2
View File
@@ -39,7 +39,7 @@ To execute the Prowler test suite, install the necessary dependencies listed in
### Prerequisites
If you have not installed Prowler yet, refer to the [developer guide introduction](./introduction.md#get-the-code-and-install-all-dependencies).
If you have not installed Prowler yet, refer to the [developer guide introduction](./introduction.md#getting-the-code-and-installing-all-dependencies).
### Executing Tests
@@ -520,7 +520,7 @@ Execute tests on the service `__init__` to ensure correct information retrieval.
While service tests resemble *Integration Tests*, as they assess how the service interacts with the provider, they ultimately fall under *Unit Tests*, due to the use of Moto or custom mock objects.
For detailed guidance on test creation and existing service tests, refer to the [AWS checks test](./unit-testing.md#checks) [documentation](https://github.com/prowler-cloud/prowler/tree/master/tests/providers/aws/services).
For detailed guidance on test creation and existing service tests, check the current [AWS checks implementation](https://github.com/prowler-cloud/prowler/tree/master/tests/providers/aws/services).
## GCP
-591
View File
@@ -1,591 +0,0 @@
# Prowler Requirements
Prowler is built in Python and utilizes the following SDKs:
- [AWS SDK (Boto3)](https://boto3.amazonaws.com/v1/documentation/api/latest/index.html#)
- [Azure SDK](https://azure.github.io/azure-sdk-for-python/)
- [GCP API Python Client](https://github.com/googleapis/google-api-python-client/)
- [Kubernetes SDK](https://github.com/kubernetes-client/python)
- [M365 Graph SDK](https://github.com/microsoftgraph/msgraph-sdk-python)
- [Github REST API SDK](https://github.com/PyGithub/PyGithub)
## AWS
Prowler requires AWS credentials to function properly. You can authenticate using any method outlined in the [AWS CLI configuration guide](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-quickstart.html#cli-configure-quickstart-precedence).
### Authentication Steps
Ensure your AWS CLI is correctly configured with valid credentials and region settings. You can achieve this via:
```console
aws configure
```
or
```console
export AWS_ACCESS_KEY_ID="ASXXXXXXX"
export AWS_SECRET_ACCESS_KEY="XXXXXXXXX"
export AWS_SESSION_TOKEN="XXXXXXXXX"
```
#### Required IAM Permissions
The credentials used must be associated with a user or role that has appropriate permissions for security checks. Attach the following AWS managed policies to ensure access:
- `arn:aws:iam::aws:policy/SecurityAudit`
- `arn:aws:iam::aws:policy/job-function/ViewOnlyAccess`
#### Additional Permissions
For certain checks, additional read-only permissions are required. Attach the following custom policy to your role:
[prowler-additions-policy.json](https://github.com/prowler-cloud/prowler/blob/master/permissions/prowler-additions-policy.json)
If you intend to send findings to
[AWS Security Hub](https://aws.amazon.com/security-hub), attach the following custom policy:
[prowler-security-hub.json](https://github.com/prowler-cloud/prowler/blob/master/permissions/prowler-security-hub.json).
### Multi-Factor Authentication (MFA)
If your IAM entity requires Multi-Factor Authentication (MFA), you can use the `--mfa` flag. Prowler will prompt you to enter the following values to initiate a new session:
- **ARN of your MFA device**
- **TOTP (Time-Based One-Time Password)**
## Azure
Prowler for Azure supports multiple authentication types. To use a specific method, pass the appropriate flag during execution:
- [**Service Principal Application**](https://learn.microsoft.com/en-us/entra/identity-platform/app-objects-and-service-principals?tabs=browser#service-principal-object) (**Recommended**)
- Existing **AZ CLI credentials**
- **Interactive browser authentication**
- [**Managed Identity**](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/overview) authentication
> ⚠️ **Important:** For Prowler App, only Service Principal authentication is supported.
### Service Principal Application Authentication
To allow Prowler to authenticate using a Service Principal Application, set up the following environment variables:
```console
export AZURE_CLIENT_ID="XXXXXXXXX"
export AZURE_TENANT_ID="XXXXXXXXX"
export AZURE_CLIENT_SECRET="XXXXXXX"
```
If you execute Prowler with the `--sp-env-auth` flag and these variables are not set or exported, execution will fail.
Refer to the [Create Prowler Service Principal](../tutorials/azure/create-prowler-service-principal.md#how-to-create-prowler-service-principal-application) guide for detailed setup instructions.
### Azure Authentication Methods
Prowler for Azure supports the following authentication methods:
- **AZ CLI Authentication (`--az-cli-auth`)** Automated authentication using stored AZ CLI credentials.
- **Managed Identity Authentication (`--managed-identity-auth`)** Automated authentication via Azure Managed Identity.
- **Browser Authentication (`--browser-auth`)** Requires the user to authenticate using the default browser. The `tenant-id` parameter is mandatory for this method.
### Required Permissions
Prowler for Azure requires two types of permission scopes:
#### Microsoft Entra ID Permissions
These permissions allow Prowler to retrieve metadata from the assumed identity and perform specific Entra checks. While not mandatory for execution, they enhance functionality.
Required permissions:
- `Directory.Read.All`
- `Policy.Read.All`
- `UserAuthenticationMethod.Read.All` (used for Entra multifactor authentication checks)
???+ note
You can replace `Directory.Read.All` with `Domain.Read.All` that is a more restrictive permission but you won't be able to run the Entra checks related with DirectoryRoles and GetUsers.
#### Subscription Scope Permissions
These permissions are required to perform security checks against Azure resources. The following **RBAC roles** must be assigned per subscription to the entity used by Prowler:
- `Reader` Grants read-only access to Azure resources.
- `ProwlerRole` A custom role with minimal permissions, defined in the [prowler-azure-custom-role](https://github.com/prowler-cloud/prowler/blob/master/permissions/prowler-azure-custom-role.json).
???+ note
The `assignableScopes` field in the JSON custom role file must be updated to reflect the correct subscription or management group. Use one of the following formats: `/subscriptions/<subscription-id>` or `/providers/Microsoft.Management/managementGroups/<management-group-id>`.
### Assigning Permissions
To properly configure permissions, follow these guides:
- [Microsoft Entra ID permissions](../tutorials/azure/create-prowler-service-principal.md#assigning-the-proper-permissions)
- [Azure subscription permissions](../tutorials/azure/subscriptions.md#assign-the-appropriate-permissions-to-the-identity-that-is-going-to-be-assumed-by-prowler)
???+ warning
Some permissions in `ProwlerRole` involve **write access**. If a `ReadOnly` lock is attached to certain resources, you may encounter errors, and findings for those checks will not be available.
#### Checks Requiring `ProwlerRole`
The following security checks require the `ProwlerRole` permissions for execution. Ensure the role is assigned to the identity assumed by Prowler before running these checks:
- `app_function_access_keys_configured`
- `app_function_ftps_deployment_disabled`
## Google Cloud
### Authentication
Prowler follows the same credential discovery process as the [Google authentication libraries](https://cloud.google.com/docs/authentication/application-default-credentials#search_order):
1. **Environment Variable Authentication** Uses the [`GOOGLE_APPLICATION_CREDENTIALS` environment variable](https://cloud.google.com/docs/authentication/application-default-credentials#GAC).
2. **Google Cloud CLI Credentials** Uses credentials configured via the [Google Cloud CLI](https://cloud.google.com/docs/authentication/application-default-credentials#personal).
3. **Service Account Authentication** Retrieves the attached service account credentials from the metadata server. More details [here](https://cloud.google.com/docs/authentication/application-default-credentials#attached-sa).
### Required Permissions
Prowler for Google Cloud requires the following permissions:
#### IAM Roles
- **Reader (`roles/reader`)** Must be granted at the **project, folder, or organization** level to allow scanning of target projects.
#### Project-Level Settings
At least one project must have the following configurations:
- **Identity and Access Management (IAM) API (`iam.googleapis.com`)** Must be enabled via:
- The [Google Cloud API UI](https://console.cloud.google.com/apis/api/iam.googleapis.com/metrics), or
- The `gcloud` CLI:
```sh
gcloud services enable iam.googleapis.com --project <your-project-id>
```
- **Service Usage Consumer (`roles/serviceusage.serviceUsageConsumer`)** IAM Role Required for resource scanning.
- **Quota Project Setting** Define a quota project using either:
- The `gcloud` CLI:
```sh
gcloud auth application-default set-quota-project <project-id>
```
- Setting an environment variable:
```sh
export GOOGLE_CLOUD_QUOTA_PROJECT=<project-id>
```
### Default Project Scanning
By default, Prowler scans **all accessible GCP projects**. To limit the scan to specific projects, use the `--project-ids` flag.
## Microsoft 365
Prowler for Microsoft 365 (M365) supports the following authentication methods:
- [**Service Principal Application**](https://learn.microsoft.com/en-us/entra/identity-platform/app-objects-and-service-principals?tabs=browser#service-principal-object) (**Recommended**)
- **Service Principal Application with Microsoft User Credentials**
- **Stored AZ CLI credentials**
- **Interactive browser authentication**
???+ warning
Prowler App supports the **Service Principal** authentication method and the **Service Principal with User Credentials** authentication method, but this last one will be deprecated in September once Microsoft will enforce MFA in all tenants not allowing User authentication without interactive method.
### Service Principal Authentication (Recommended)
**Authentication flag:** `--sp-env-auth`
To enable Prowler to authenticate as the **Service Principal Application**, configure the following environment variables:
```console
export AZURE_CLIENT_ID="XXXXXXXXX"
export AZURE_CLIENT_SECRET="XXXXXXXXX"
export AZURE_TENANT_ID="XXXXXXXXX"
```
If these variables are not set or exported, execution using `--sp-env-auth` will fail.
Refer to the [Create Prowler Service Principal](../tutorials/microsoft365/getting-started-m365.md#create-the-service-principal-app) guide for setup instructions.
If the external API permissions described in the mentioned section above are not added only checks that work through MS Graph will be executed. This means that the full provider will not be executed.
???+ note
In order to scan all the checks from M365 required permissions to the service principal application must be added. Refer to the [External API Permissions Assignment](../tutorials/microsoft365/getting-started-m365.md#grant-powershell-modules-permissions) section for more information.
### Service Principal and User Credentials Authentication
Authentication flag: `--env-auth`
???+ warning
This method is not recommended anymore, we recommend just use the **Service Principal Application** authentication method instead.
This method builds upon the Service Principal authentication by adding User Credentials. Configure the following environment variables: `M365_USER` and `M365_PASSWORD`.
```console
export AZURE_CLIENT_ID="XXXXXXXXX"
export AZURE_CLIENT_SECRET="XXXXXXXXX"
export AZURE_TENANT_ID="XXXXXXXXX"
export M365_USER="your_email@example.com"
export M365_PASSWORD="examplepassword"
```
These two new environment variables are **required** in this authentication method to execute the PowerShell modules needed to retrieve information from M365 services. Prowler uses Service Principal authentication to access Microsoft Graph and user credentials to authenticate to Microsoft PowerShell modules.
- `M365_USER` should be your Microsoft account email using the **assigned domain in the tenant**. This means it must look like `example@YourCompany.onmicrosoft.com` or `example@YourCompany.com`, but it must be the exact domain assigned to that user in the tenant.
???+ warning
If the user is newly created, you need to sign in with that account first, as Microsoft will prompt you to change the password. If you dont complete this step, user authentication will fail because Microsoft marks the initial password as expired.
???+ warning
If the user is newly created, you need to sign in with that account first, as Microsoft will prompt you to change the password. If you dont complete this step, user authentication will fail because Microsoft marks the initial password as expired.
???+ warning
The user must not be MFA capable. Microsoft does not allow MFA capable users to authenticate programmatically. See [Microsoft documentation](https://learn.microsoft.com/en-us/entra/identity-platform/scenario-desktop-acquire-token-username-password?tabs=dotnet) for more information.
???+ warning
Using a tenant domain other than the one assigned — even if it belongs to the same tenant — will cause Prowler to fail, as Microsoft authentication will not succeed.
Ensure you are using the right domain for the user you are trying to authenticate with.
![User Domains](../tutorials/microsoft365/img/user-domains.png)
- `M365_PASSWORD` must be the user password.
???+ note
Before we asked for a encrypted password, but now we ask for the user password directly. Prowler will now handle the password encryption for you.
### Interactive Browser Authentication
**Authentication flag:** `--browser-auth`
This authentication method requires the user to authenticate against Azure using the default browser to start the scan. The `--tenant-id` flag is also required.
With these credentials, you will only be able to run checks that rely on Microsoft Graph. This means you won't be able to run the entire provider. To perform a full M365 security scan, use the **recommended authentication method**.
Since this is a **delegated permission** authentication method, necessary permissions should be assigned to the user rather than the application.
### Required Permissions
To run the full Prowler provider, including PowerShell checks, two types of permission scopes must be set in **Microsoft Entra ID**.
#### For Service Principal Authentication (`--sp-env-auth`) - Recommended
When using service principal authentication, you need to add the following **Application Permissions** configured to:
**Microsoft Graph API Permissions:**
- `AuditLog.Read.All`: Required for Entra service.
- `Directory.Read.All`: Required for all services.
- `Policy.Read.All`: Required for all services.
- `SharePointTenantSettings.Read.All`: Required for SharePoint service.
- `User.Read` (IMPORTANT: this must be set as **delegated**): Required for the sign-in.
**External API Permissions:**
- `Exchange.ManageAsApp` from external API `Office 365 Exchange Online`: Required for Exchange PowerShell module app authentication. You also need to assign the `Global Reader` role to the app.
- `application_access` from external API `Skype and Teams Tenant Admin API`: Required for Teams PowerShell module app authentication.
???+ note
`Directory.Read.All` can be replaced with `Domain.Read.All` that is a more restrictive permission but you won't be able to run the Entra checks related with DirectoryRoles and GetUsers.
> If you do this you will need to add also the `Organization.Read.All` permission to the service principal application in order to authenticate.
???+ note
This is the **recommended authentication method** because it allows you to run the full M365 provider including PowerShell checks, providing complete coverage of all available security checks, same as the Service Principal Authentication + User Credentials Authentication but this last one will be deprecated in September once Microsoft will enforce MFA in all tenants not allowing User authentication without interactive method.
#### For Service Principal + User Credentials Authentication (`--env-auth`)
When using service principal with user credentials authentication, you need **both** sets of permissions:
**1. Service Principal Application Permissions**:
- You **will need** all the Microsoft Graph API permissions listed above.
- You **won't need** the External API permissions listed above.
**2. User-Level Permissions**: These are set at the `M365_USER` level, so the user used to run Prowler must have one of the following roles:
- `Global Reader` (recommended): this allows you to read all roles needed.
- `Exchange Administrator` and `Teams Administrator`: user needs both roles but with this [roles](https://learn.microsoft.com/en-us/exchange/permissions-exo/permissions-exo#microsoft-365-permissions-in-exchange-online) you can access to the same information as a Global Reader (since only read access is needed, Global Reader is recommended).
#### For Browser Authentication (`--browser-auth`)
When using browser authentication, permissions are delegated to the user, so the user must have the appropriate permissions rather than the application.
???+ warning
With browser authentication, you will only be able to run checks that work through MS Graph API. PowerShell module checks will not be executed.
### Assigning Permissions and Roles
For guidance on assigning the necessary permissions and roles, follow these instructions:
- [Grant API Permissions](../tutorials/microsoft365/getting-started-m365.md#grant-required-graph-api-permissions)
- [Assign Required Roles](../tutorials/microsoft365/getting-started-m365.md#if-using-user-authentication)
### Supported PowerShell Versions
PowerShell is required to run certain M365 checks.
**Supported versions:**
- **PowerShell 7.4 or higher** (7.5 is recommended)
#### Why Is PowerShell 7.4+ Required?
- **PowerShell 5.1** (default on some Windows systems) does not support required cmdlets.
- Older [cross-platform PowerShell versions](https://learn.microsoft.com/en-us/powershell/scripting/install/powershell-support-lifecycle?view=powershell-7.5) are **unsupported**, leading to potential errors.
???+ note
Installing PowerShell is only necessary if you install Prowler via **pip or other sources**. **SDK and API containers include PowerShell by default.**
### Installing PowerShell
Installing PowerShell is different depending on your OS.
- [Windows](https://learn.microsoft.com/es-es/powershell/scripting/install/installing-powershell-on-windows?view=powershell-7.5#install-powershell-using-winget-recommended): you will need to update PowerShell to +7.4 to be able to run prowler, if not some checks will not show findings and the provider could not work as expected. This version of PowerShell is [supported](https://learn.microsoft.com/es-es/powershell/scripting/install/installing-powershell-on-windows?view=powershell-7.4#supported-versions-of-windows) on Windows 10, Windows 11, Windows Server 2016 and higher versions.
```console
winget install --id Microsoft.PowerShell --source winget
```
- [MacOS](https://learn.microsoft.com/es-es/powershell/scripting/install/installing-powershell-on-macos?view=powershell-7.5#install-the-latest-stable-release-of-powershell): installing PowerShell on MacOS needs to have installed [brew](https://brew.sh/), once you have it is just running the command above, Pwsh is only supported in macOS 15 (Sequoia) x64 and Arm64, macOS 14 (Sonoma) x64 and Arm64, macOS 13 (Ventura) x64 and Arm64
```console
brew install powershell/tap/powershell
```
Once it's installed run `pwsh` on your terminal to verify it's working.
- Linux: installing PowerShell on Linux depends on the distro you are using:
- [Ubuntu](https://learn.microsoft.com/es-es/powershell/scripting/install/install-ubuntu?view=powershell-7.5#installation-via-package-repository-the-package-repository): The required version for installing PowerShell +7.4 on Ubuntu are Ubuntu 22.04 and Ubuntu 24.04. The recommended way to install it is downloading the package available on PMC. You just need to follow the following steps:
```console
###################################
# Prerequisites
# Update the list of packages
sudo apt-get update
# Install pre-requisite packages.
sudo apt-get install -y wget apt-transport-https software-properties-common
# Get the version of Ubuntu
source /etc/os-release
# Download the Microsoft repository keys
wget -q https://packages.microsoft.com/config/ubuntu/$VERSION_ID/packages-microsoft-prod.deb
# Register the Microsoft repository keys
sudo dpkg -i packages-microsoft-prod.deb
# Delete the Microsoft repository keys file
rm packages-microsoft-prod.deb
# Update the list of packages after we added packages.microsoft.com
sudo apt-get update
###################################
# Install PowerShell
sudo apt-get install -y powershell
# Start PowerShell
pwsh
```
- [Alpine](https://learn.microsoft.com/es-es/powershell/scripting/install/install-alpine?view=powershell-7.5#installation-steps): The only supported version for installing PowerShell +7.4 on Alpine is Alpine 3.20. The unique way to install it is downloading the tar.gz package available on [PowerShell github](https://github.com/PowerShell/PowerShell/releases/download/v7.5.0/powershell-7.5.0-linux-musl-x64.tar.gz). You just need to follow the following steps:
```console
# Install the requirements
sudo apk add --no-cache \
ca-certificates \
less \
ncurses-terminfo-base \
krb5-libs \
libgcc \
libintl \
libssl3 \
libstdc++ \
tzdata \
userspace-rcu \
zlib \
icu-libs \
curl
apk -X https://dl-cdn.alpinelinux.org/alpine/edge/main add --no-cache \
lttng-ust \
openssh-client \
# Download the powershell '.tar.gz' archive
curl -L https://github.com/PowerShell/PowerShell/releases/download/v7.5.0/powershell-7.5.0-linux-musl-x64.tar.gz -o /tmp/powershell.tar.gz
# Create the target folder where powershell will be placed
sudo mkdir -p /opt/microsoft/powershell/7
# Expand powershell to the target folder
sudo tar zxf /tmp/powershell.tar.gz -C /opt/microsoft/powershell/7
# Set execute permissions
sudo chmod +x /opt/microsoft/powershell/7/pwsh
# Create the symbolic link that points to pwsh
sudo ln -s /opt/microsoft/powershell/7/pwsh /usr/bin/pwsh
# Start PowerShell
pwsh
```
- [Debian](https://learn.microsoft.com/es-es/powershell/scripting/install/install-debian?view=powershell-7.5#installation-on-debian-11-or-12-via-the-package-repository): The required version for installing PowerShell +7.4 on Debian are Debian 11 and Debian 12. The recommended way to install it is downloading the package available on PMC. You just need to follow the following steps:
```console
###################################
# Prerequisites
# Update the list of packages
sudo apt-get update
# Install pre-requisite packages.
sudo apt-get install -y wget
# Get the version of Debian
source /etc/os-release
# Download the Microsoft repository GPG keys
wget -q https://packages.microsoft.com/config/debian/$VERSION_ID/packages-microsoft-prod.deb
# Register the Microsoft repository GPG keys
sudo dpkg -i packages-microsoft-prod.deb
# Delete the Microsoft repository GPG keys file
rm packages-microsoft-prod.deb
# Update the list of packages after we added packages.microsoft.com
sudo apt-get update
###################################
# Install PowerShell
sudo apt-get install -y powershell
# Start PowerShell
pwsh
```
- [Rhel](https://learn.microsoft.com/es-es/powershell/scripting/install/install-rhel?view=powershell-7.5#installation-via-the-package-repository): The required version for installing PowerShell +7.4 on Red Hat are RHEL 8 and RHEL 9. The recommended way to install it is downloading the package available on PMC. You just need to follow the following steps:
```console
###################################
# Prerequisites
# Get version of RHEL
source /etc/os-release
if [ ${VERSION_ID%.*} -lt 8 ]
then majorver=7
elif [ ${VERSION_ID%.*} -lt 9 ]
then majorver=8
else majorver=9
fi
# Download the Microsoft RedHat repository package
curl -sSL -O https://packages.microsoft.com/config/rhel/$majorver/packages-microsoft-prod.rpm
# Register the Microsoft RedHat repository
sudo rpm -i packages-microsoft-prod.rpm
# Delete the downloaded package after installing
rm packages-microsoft-prod.rpm
# Update package index files
sudo dnf update
# Install PowerShell
sudo dnf install powershell -y
```
- [Docker](https://learn.microsoft.com/es-es/powershell/scripting/install/powershell-in-docker?view=powershell-7.5#use-powershell-in-a-container): The following command download the latest stable versions of PowerShell:
```console
docker pull mcr.microsoft.com/dotnet/sdk:9.0
```
To start an interactive shell of Pwsh you just need to run:
```console
docker run -it mcr.microsoft.com/dotnet/sdk:9.0 pwsh
```
### Required PowerShell Modules
Prowler relies on several PowerShell cmdlets to retrieve necessary data.
These cmdlets come from different modules that must be installed.
#### Automatic Installation
The required modules are automatically installed when running Prowler with the `--init-modules` flag.
Example command:
```console
python3 prowler-cli.py m365 --verbose --log-level ERROR --env-auth --init-modules
```
If the modules are already installed, running this command will not cause issues—it will simply verify that the necessary modules are available.
???+ note
Prowler installs the modules using `-Scope CurrentUser`.
If you encounter any issues with services not working after the automatic installation, try installing the modules manually using `-Scope AllUsers` (administrator permissions are required for this).
The command needed to install a module manually is:
```powershell
Install-Module -Name "ModuleName" -Scope AllUsers -Force
```
#### Modules Version
- [ExchangeOnlineManagement](https://www.powershellgallery.com/packages/ExchangeOnlineManagement/3.6.0) (Minimum version: 3.6.0) Required for checks across Exchange, Defender, and Purview.
- [MicrosoftTeams](https://www.powershellgallery.com/packages/MicrosoftTeams/6.6.0) (Minimum version: 6.6.0) Required for all Teams checks.
- [MSAL.PS](https://www.powershellgallery.com/packages/MSAL.PS/4.32.0): Required for Exchange module via application authentication.
[MSAL.PS](https://www.powershellgallery.com/packages/MSAL.PS/4.32.0): Required for Exchange module via application authentication.
## GitHub
Prowler supports multiple [authentication methods for GitHub](https://docs.github.com/en/rest/authentication/authenticating-to-the-rest-api).
### Supported Authentication Methods
- **Personal Access Token (PAT)**
- **OAuth App Token**
- **GitHub App Credentials**
These options provide flexibility for scanning and analyzing your GitHub account, repositories, organizations, and applications. Choose the authentication method that best suits your security needs.
???+ note
GitHub App Credentials support less checks than other authentication methods.
## Infrastructure as Code (IaC)
Prowler's Infrastructure as Code (IaC) provider enables you to scan local or remote infrastructure code for security and compliance issues using [Checkov](https://www.checkov.io/). This provider supports a wide range of IaC frameworks and requires no cloud authentication for local scans.
### Authentication
- For local scans, no authentication is required.
- For remote repository scans, authentication can be provided via:
- [**GitHub Username and Personal Access Token (PAT)**](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-personal-access-token-classic)
- [**GitHub OAuth App Token**](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-personal-access-token)
- [**Git URL**](https://git-scm.com/docs/git-clone#_git_urls)
### Supported Frameworks
The IaC provider leverages Checkov to support multiple frameworks, including:
- Terraform
- CloudFormation
- Kubernetes
- ARM (Azure Resource Manager)
- Serverless
- Dockerfile
- YAML/JSON (generic IaC)
- Bicep
- Helm
- GitHub Actions, GitLab CI, Bitbucket Pipelines, Azure Pipelines, CircleCI, Argo Workflows
- Ansible
- Kustomize
- OpenAPI
- SAST, SCA (Software Composition Analysis)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 518 KiB

+10 -783
View File
@@ -1,3 +1,5 @@
# What is Prowler?
**Prowler** is the open source cloud security platform trusted by thousands to **automate security and compliance** in any cloud environment. With hundreds of ready-to-use checks and compliance frameworks, Prowler delivers real-time, customizable monitoring and seamless integrations, making cloud security simple, scalable, and cost-effective for organizations of any size.
The official supported providers right now are:
@@ -8,790 +10,15 @@ The official supported providers right now are:
- **Kubernetes**
- **M365**
- **Github**
- **IaC**
Unofficially, Prowler supports: NHN.
Prowler supports **auditing, incident response, continuous monitoring, hardening, forensic readiness, and remediation**.
### Prowler Components
### Products
- **Prowler CLI** (Command Line Interface) Known as **Prowler Open Source**.
- **Prowler Cloud** A managed service built on top of Prowler CLI.
More information: [Prowler Cloud](https://prowler.com)
## Prowler App
![Prowler App](img/overview.png)
Prowler App is a web application that simplifies running Prowler. It provides:
- A **user-friendly interface** for configuring and executing scans.
- A dashboard to **view results** and manage **security findings**.
### Installation Guide
Refer to the [Quick Start](#prowler-app-installation) section for installation steps.
## Prowler CLI
```console
prowler <provider>
```
![Prowler CLI Execution](img/short-display.png)
## Prowler Dashboard
```console
prowler dashboard
```
![Prowler Dashboard](img/dashboard.png)
Prowler includes hundreds of security controls aligned with widely recognized industry frameworks and standards, including:
- CIS Benchmarks (AWS, Azure, Microsoft 365, Kubernetes, GitHub)
- NIST SP 800-53 (rev. 4 and 5) and NIST SP 800-171
- NIST Cybersecurity Framework (CSF)
- CISA Guidelines
- FedRAMP Low & Moderate
- PCI DSS v3.2.1 and v4.0
- ISO/IEC 27001:2013 and 2022
- SOC 2
- GDPR (General Data Protection Regulation)
- HIPAA (Health Insurance Portability and Accountability Act)
- FFIEC (Federal Financial Institutions Examination Council)
- ENS RD2022 (Spanish National Security Framework)
- GxP 21 CFR Part 11 and EU Annex 11
- RBI Cybersecurity Framework (Reserve Bank of India)
- KISA ISMS-P (Korean Information Security Management System)
- MITRE ATT&CK
- AWS Well-Architected Framework (Security & Reliability Pillars)
- AWS Foundational Technical Review (FTR)
- Microsoft NIS2 Directive (EU)
- Custom threat scoring frameworks (prowler_threatscore)
- Custom security frameworks for enterprise needs
## Quick Start
### Prowler App Installation
Prowler App supports multiple installation methods based on your environment.
Refer to the [Prowler App Tutorial](tutorials/prowler-app.md) for detailed usage instructions.
=== "Docker Compose"
_Requirements_:
* `Docker Compose` installed: https://docs.docker.com/compose/install/.
_Commands_:
``` bash
curl -LO https://raw.githubusercontent.com/prowler-cloud/prowler/refs/heads/master/docker-compose.yml
curl -LO https://raw.githubusercontent.com/prowler-cloud/prowler/refs/heads/master/.env
docker compose up -d
```
> Containers are built for `linux/amd64`. If your workstation's architecture is different, please set `DOCKER_DEFAULT_PLATFORM=linux/amd64` in your environment or use the `--platform linux/amd64` flag in the docker command.
> Enjoy Prowler App at http://localhost:3000 by signing up with your email and password.
???+ note
You can change the environment variables in the `.env` file. Note that it is not recommended to use the default values in production environments.
???+ note
There is a development mode available, you can use the file https://github.com/prowler-cloud/prowler/blob/master/docker-compose-dev.yml to run the app in development mode.
???+ warning
Google and GitHub authentication is only available in [Prowler Cloud](https://prowler.com).
=== "GitHub"
_Requirements_:
* `git` installed.
* `poetry` installed: [poetry installation](https://python-poetry.org/docs/#installation).
* `npm` installed: [npm installation](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm).
* `Docker Compose` installed: https://docs.docker.com/compose/install/.
???+ warning
Make sure to have `api/.env` and `ui/.env.local` files with the required environment variables. You can find the required environment variables in the [`api/.env.template`](https://github.com/prowler-cloud/prowler/blob/master/api/.env.example) and [`ui/.env.template`](https://github.com/prowler-cloud/prowler/blob/master/ui/.env.template) files.
_Commands to run the API_:
``` bash
git clone https://github.com/prowler-cloud/prowler \
cd prowler/api \
poetry install \
eval $(poetry env activate) \
set -a \
source .env \
docker compose up postgres valkey -d \
cd src/backend \
python manage.py migrate --database admin \
gunicorn -c config/guniconf.py config.wsgi:application
```
???+ important
Starting from Poetry v2.0.0, `poetry shell` has been deprecated in favor of `poetry env activate`.
If your poetry version is below 2.0.0 you must keep using `poetry shell` to activate your environment.
In case you have any doubts, consult the Poetry environment activation guide: https://python-poetry.org/docs/managing-environments/#activating-the-environment
> Now, you can access the API documentation at http://localhost:8080/api/v1/docs.
_Commands to run the API Worker_:
``` bash
git clone https://github.com/prowler-cloud/prowler \
cd prowler/api \
poetry install \
eval $(poetry env activate) \
set -a \
source .env \
cd src/backend \
python -m celery -A config.celery worker -l info -E
```
_Commands to run the API Scheduler_:
``` bash
git clone https://github.com/prowler-cloud/prowler \
cd prowler/api \
poetry install \
eval $(poetry env activate) \
set -a \
source .env \
cd src/backend \
python -m celery -A config.celery beat -l info --scheduler django_celery_beat.schedulers:DatabaseScheduler
```
_Commands to run the UI_:
``` bash
git clone https://github.com/prowler-cloud/prowler \
cd prowler/ui \
npm install \
npm run build \
npm start
```
> Enjoy Prowler App at http://localhost:3000 by signing up with your email and password.
???+ warning
Google and GitHub authentication is only available in [Prowler Cloud](https://prowler.com).
### Prowler CLI Installation
Prowler is available as a project in [PyPI](https://pypi.org/project/prowler/). Consequently, it can be installed as Python package with `Python >= 3.9, <= 3.12`:
=== "pipx"
[pipx](https://pipx.pypa.io/stable/) is a tool to install Python applications in isolated environments. It is recommended to use `pipx` for a global installation.
_Requirements_:
* `Python >= 3.9, <= 3.12`
* `pipx` installed: [pipx installation](https://pipx.pypa.io/stable/installation/).
* AWS, GCP, Azure and/or Kubernetes credentials
_Commands_:
``` bash
pipx install prowler
prowler -v
```
To upgrade Prowler to the latest version, run:
``` bash
pipx upgrade prowler
```
=== "pip"
???+ warning
This method is not recommended because it will modify the environment which you choose to install. Consider using [pipx](https://docs.prowler.com/projects/prowler-open-source/en/latest/#__tabbed_1_1) for a global installation.
_Requirements_:
* `Python >= 3.9, <= 3.12`
* `Python pip >= 21.0.0`
* AWS, GCP, Azure, M365 and/or Kubernetes credentials
_Commands_:
``` bash
pip install prowler
prowler -v
```
To upgrade Prowler to the latest version, run:
``` bash
pip install --upgrade prowler
```
=== "Docker"
_Requirements_:
* Have `docker` installed: https://docs.docker.com/get-docker/.
* In the command below, change `-v` to your local directory path in order to access the reports.
* AWS, GCP, Azure and/or Kubernetes credentials
> Containers are built for `linux/amd64`. If your workstation's architecture is different, please set `DOCKER_DEFAULT_PLATFORM=linux/amd64` in your environment or use the `--platform linux/amd64` flag in the docker command.
_Commands_:
``` bash
docker run -ti --rm -v /your/local/dir/prowler-output:/home/prowler/output \
--name prowler \
--env AWS_ACCESS_KEY_ID \
--env AWS_SECRET_ACCESS_KEY \
--env AWS_SESSION_TOKEN toniblyx/prowler:latest
```
=== "GitHub"
_Requirements for Developers_:
* `git`
* `poetry` installed: [poetry installation](https://python-poetry.org/docs/#installation).
* AWS, GCP, Azure and/or Kubernetes credentials
_Commands_:
```
git clone https://github.com/prowler-cloud/prowler
cd prowler
poetry install
poetry run python prowler-cli.py -v
```
???+ note
If you want to clone Prowler from Windows, use `git config core.longpaths true` to allow long file paths.
=== "Amazon Linux 2"
_Requirements_:
* `Python >= 3.9, <= 3.12`
* AWS, GCP, Azure and/or Kubernetes credentials
_Commands_:
```
python3 -m pip install --user pipx
python3 -m pipx ensurepath
pipx install prowler
prowler -v
```
=== "Ubuntu"
_Requirements_:
* `Ubuntu 23.04` or above, if you are using an older version of Ubuntu check [pipx installation](https://docs.prowler.com/projects/prowler-open-source/en/latest/#__tabbed_1_1) and ensure you have `Python >= 3.9, <= 3.12`.
* `Python >= 3.9, <= 3.12`
* AWS, GCP, Azure and/or Kubernetes credentials
_Commands_:
``` bash
sudo apt update
sudo apt install pipx
pipx ensurepath
pipx install prowler
prowler -v
```
=== "Brew"
_Requirements_:
* `Brew` installed in your Mac or Linux
* AWS, GCP, Azure and/or Kubernetes credentials
_Commands_:
``` bash
brew install prowler
prowler -v
```
=== "AWS CloudShell"
After the migration of AWS CloudShell from Amazon Linux 2 to Amazon Linux 2023 [[1]](https://aws.amazon.com/about-aws/whats-new/2023/12/aws-cloudshell-migrated-al2023/) [[2]](https://docs.aws.amazon.com/cloudshell/latest/userguide/cloudshell-AL2023-migration.html), there is no longer a need to manually compile Python 3.9 as it is already included in AL2023. Prowler can thus be easily installed following the generic method of installation via pip. Follow the steps below to successfully execute Prowler v4 in AWS CloudShell:
_Requirements_:
* Open AWS CloudShell `bash`.
_Commands_:
```bash
sudo bash
adduser prowler
su prowler
python3 -m pip install --user pipx
python3 -m pipx ensurepath
pipx install prowler
cd /tmp
prowler aws
```
???+ note
To download the results from AWS CloudShell, select Actions -> Download File and add the full path of each file. For the CSV file it will be something like `/tmp/output/prowler-output-123456789012-20221220191331.csv`
=== "Azure CloudShell"
_Requirements_:
* Open Azure CloudShell `bash`.
_Commands_:
```bash
python3 -m pip install --user pipx
python3 -m pipx ensurepath
pipx install prowler
cd /tmp
prowler azure --az-cli-auth
```
### Prowler App Update
You have two options to upgrade your Prowler App installation:
#### Option 1: Change env file with the following values
Edit your `.env` file and change the version values:
```env
PROWLER_UI_VERSION="5.9.0"
PROWLER_API_VERSION="5.9.0"
```
#### Option 2: Run the following command
```bash
docker compose pull --policy always
```
The `--policy always` flag ensures that Docker pulls the latest images even if they already exist locally.
???+ note "What Gets Preserved During Upgrade"
Everything is preserved, nothing will be deleted after the update.
#### Troubleshooting
If containers don't start, check logs for errors:
```bash
# Check logs for errors
docker compose logs
# Verify image versions
docker images | grep prowler
```
If you encounter issues, you can rollback to the previous version by changing the `.env` file back to your previous version and running:
```bash
docker compose pull
docker compose up -d
```
## Prowler container versions
The available versions of Prowler CLI are the following:
- `latest`: in sync with `master` branch (please note that it is not a stable version)
- `v4-latest`: in sync with `v4` branch (please note that it is not a stable version)
- `v3-latest`: in sync with `v3` branch (please note that it is not a stable version)
- `<x.y.z>` (release): you can find the releases [here](https://github.com/prowler-cloud/prowler/releases), those are stable releases.
- `stable`: this tag always point to the latest release.
- `v4-stable`: this tag always point to the latest release for v4.
- `v3-stable`: this tag always point to the latest release for v3.
The container images are available here:
- Prowler CLI:
- [DockerHub](https://hub.docker.com/r/toniblyx/prowler/tags)
- [AWS Public ECR](https://gallery.ecr.aws/prowler-cloud/prowler)
- Prowler App:
- [DockerHub - Prowler UI](https://hub.docker.com/r/prowlercloud/prowler-ui/tags)
- [DockerHub - Prowler API](https://hub.docker.com/r/prowlercloud/prowler-api/tags)
## High level architecture
You can run Prowler from your workstation, a Kubernetes Job, a Google Compute Engine, an Azure VM, an EC2 instance, Fargate or any other container, CloudShell and many more.
![Architecture](img/architecture.png)
### Prowler App
The **Prowler App** consists of three main components:
- **Prowler UI**: A user-friendly web interface for running Prowler and viewing results, powered by Next.js.
- **Prowler API**: The backend API that executes Prowler scans and stores the results, built with Django REST Framework.
- **Prowler SDK**: A Python SDK that integrates with Prowler CLI for advanced functionality.
The app leverages the following supporting infrastructure:
- **PostgreSQL**: Used for persistent storage of scan results.
- **Celery Workers**: Facilitate asynchronous execution of Prowler scans.
- **Valkey**: An in-memory database serving as a message broker for the Celery workers.
![Prowler App Architecture](img/prowler-app-architecture.png)
## Deprecations from v3
The following are the deprecations carried out from v3.
### General
- `Allowlist` now is called `Mutelist`.
- The `--quiet` option has been deprecated. From now on use the `--status` flag to select the finding's status you want to get: PASS, FAIL or MANUAL.
- All `INFO` finding's status has changed to `MANUAL`.
- The CSV output format is common for all providers.
Some output formats are now deprecated:
- The native JSON is replaced for the JSON [OCSF](https://schema.ocsf.io/) v1.1.0, common for all the providers.
### AWS
- Deprecate the AWS flag `--sts-endpoint-region` since AWS STS regional tokens are used.
- To send only FAILS to AWS Security Hub, now you must use either `--send-sh-only-fails` or `--security-hub --status FAIL`.
## Basic Usage
### Prowler App
#### **Access the App**
Go to [http://localhost:3000](http://localhost:3000) after installing the app (see [Quick Start](#prowler-app-installation)). Sign up with your email and password.
<img src="img/sign-up-button.png" alt="Sign Up Button" width="320"/>
<img src="img/sign-up.png" alt="Sign Up" width="285"/>
???+ note "User creation and default tenant behavior"
When creating a new user, the behavior depends on whether an invitation is provided:
- **Without an invitation**:
- A new tenant is automatically created.
- The new user is assigned to this tenant.
- A set of **RBAC admin permissions** is generated and assigned to the user for the newly-created tenant.
- **With an invitation**: The user is added to the specified tenant with the permissions defined in the invitation.
This mechanism ensures that the first user in a newly created tenant has administrative permissions within that tenant.
#### Log In
Log in using your **email and password** to access the Prowler App.
<img src="img/log-in.png" alt="Log In" width="285"/>
#### Add a Cloud Provider
To configure a cloud provider for scanning:
1. Navigate to `Settings > Cloud Providers` and click `Add Account`.
2. Select the cloud provider you wish to scan (**AWS, GCP, Azure, Kubernetes**).
3. Enter the provider's identifier (Optional: Add an alias):
- **AWS**: Account ID
- **GCP**: Project ID
- **Azure**: Subscription ID
- **Kubernetes**: Cluster ID
- **M36**: Domain ID
4. Follow the guided instructions to add and authenticate your credentials.
#### Start a Scan
Once credentials are successfully added and validated, Prowler initiates a scan of your cloud environment.
Click `Go to Scans` to monitor progress.
#### View Results
While the scan is running, you can review findings in the following sections:
- **Overview** Provides a high-level summary of your scans.
<img src="img/overview.png" alt="Overview" width="700"/>
- **Compliance** Displays compliance insights based on security frameworks.
<img src="img/compliance.png" alt="Compliance" width="700"/>
> For detailed usage instructions, refer to the [Prowler App Guide](tutorials/prowler-app.md).
???+ note
Prowler will automatically scan all configured providers every **24 hours**, ensuring your cloud environment stays continuously monitored.
### Prowler CLI
#### Running Prowler
To run Prowler, you will need to specify the provider (e.g `aws`, `gcp`, `azure`, `m365`, `github` or `kubernetes`):
???+ note
If no provider is specified, AWS is used by default for backward compatibility with Prowler v2.
```console
prowler <provider>
```
![Prowler Execution](img/short-display.png)
???+ note
Running the `prowler` command without options will uses environment variable credentials. Refer to the [Requirements](./getting-started/requirements.md) section for credential configuration details.
#### Verbose Output
If you prefer the former verbose output, use: `--verbose`. This allows seeing more info while Prowler is running, minimal output is displayed unless verbosity is enabled.
#### Report Generation
By default, Prowler generates CSV, JSON-OCSF, and HTML reports. To generate a JSON-ASFF report (used by AWS Security Hub), specify `-M` or `--output-modes`:
```console
prowler <provider> -M csv json-asff json-ocsf html
```
The HTML report is saved in the output directory, alongside other reports. It will look like this:
![Prowler Execution](img/html-output.png)
#### Listing Available Checks and Services
To view all available checks or services within a provider:, use `-l`/`--list-checks` or `--list-services`.
```console
prowler <provider> --list-checks
prowler <provider> --list-services
```
#### Running Specific Checks or Services
Execute specific checks or services using `-c`/`checks` or `-s`/`services`:
```console
prowler azure --checks storage_blob_public_access_level_is_disabled
prowler aws --services s3 ec2
prowler gcp --services iam compute
prowler kubernetes --services etcd apiserver
```
#### Excluding Checks and Services
Checks and services can be excluded with `-e`/`--excluded-checks` or `--excluded-services`:
```console
prowler aws --excluded-checks s3_bucket_public_access
prowler azure --excluded-services defender iam
prowler gcp --excluded-services kms
prowler kubernetes --excluded-services controllermanager
```
#### Additional Options
Explore more advanced time-saving execution methods in the [Miscellaneous](tutorials/misc.md) section.
To access the help menu and view all available options, use: `-h`/`--help`:
```console
prowler --help
```
#### AWS
Use a custom AWS profile with `-p`/`--profile` and/or the AWS regions you want to audit with `-f`/`--filter-region`:
```console
prowler aws --profile custom-profile -f us-east-1 eu-south-2
```
???+ note
By default, `prowler` will scan all AWS regions.
See more details about AWS Authentication in the [Requirements](getting-started/requirements.md#aws) section.
#### Azure
Azure requires specifying the auth method:
```console
# To use service principal authentication
prowler azure --sp-env-auth
# To use az cli authentication
prowler azure --az-cli-auth
# To use browser authentication
prowler azure --browser-auth --tenant-id "XXXXXXXX"
# To use managed identity auth
prowler azure --managed-identity-auth
```
See more details about Azure Authentication in [Requirements](getting-started/requirements.md#azure)
By default, Prowler scans all the subscriptions for which it has permissions. To scan a single or various specific subscription you can use the following flag (using az cli auth as example):
```console
prowler azure --az-cli-auth --subscription-ids <subscription ID 1> <subscription ID 2> ... <subscription ID N>
```
#### Google Cloud
- **User Account Credentials**
By default, Prowler uses **User Account credentials**. You can configure your account using:
- `gcloud init` Set up a new account.
- `gcloud config set account <account>` Switch to an existing account.
Once configured, obtain access credentials using: `gcloud auth application-default login`.
- **Service Account Authentication**
Alternatively, you can use Service Account credentials:
Generate and download Service Account keys in JSON format. Refer to [Google IAM documentation](https://cloud.google.com/iam/docs/creating-managing-service-account-keys) for details.
Provide the key file location using this argument:
```console
prowler gcp --credentials-file path
```
- **Scanning Specific GCP Projects**
By default, Prowler scans all accessible GCP projects. To scan specific projects, use the `--project-ids` flag:
```console
prowler gcp --project-ids <Project ID 1> <Project ID 2> ... <Project ID N>
```
- **GCP Retry Configuration**
To configure the maximum number of retry attempts for Google Cloud SDK API calls, use the `--gcp-retries-max-attempts` flag:
```console
prowler gcp --gcp-retries-max-attempts 5
```
This is useful when experiencing quota exceeded errors (HTTP 429) to increase the number of automatic retry attempts.
#### Kubernetes
Prowler enables security scanning of Kubernetes clusters, supporting both **in-cluster** and **external** execution.
- **Non In-Cluster Execution**
```console
prowler kubernetes --kubeconfig-file path
```
???+ note
If no `--kubeconfig-file` is provided, Prowler will use the default KubeConfig file location (`~/.kube/config`).
- **In-Cluster Execution**
To run Prowler inside the cluster, apply the provided YAML configuration to deploy a job in a new namespace:
```console
kubectl apply -f kubernetes/prowler-sa.yaml
kubectl apply -f kubernetes/job.yaml
kubectl apply -f kubernetes/prowler-role.yaml
kubectl apply -f kubernetes/prowler-rolebinding.yaml
kubectl get pods --namespace prowler-ns --> prowler-XXXXX
kubectl logs prowler-XXXXX --namespace prowler-ns
```
???+ note
By default, Prowler scans all namespaces in the active Kubernetes context. Use the `--context`flag to specify the context to be scanned and `--namespaces` to restrict scanning to specific namespaces.
#### Microsoft 365
Microsoft 365 requires specifying the auth method:
```console
# To use service principal authentication for MSGraph and PowerShell modules
prowler m365 --sp-env-auth
# To use both service principal (for MSGraph) and user credentials (for PowerShell modules)
prowler m365 --env-auth
# To use az cli authentication
prowler m365 --az-cli-auth
# To use browser authentication
prowler m365 --browser-auth --tenant-id "XXXXXXXX"
```
See more details about M365 Authentication in the [Requirements](getting-started/requirements.md#microsoft-365) section.
#### GitHub
Prowler enables security scanning of your **GitHub account**, including **Repositories**, **Organizations** and **Applications**.
- **Supported Authentication Methods**
Authenticate using one of the following methods:
```console
# Personal Access Token (PAT):
prowler github --personal-access-token pat
# OAuth App Token:
prowler github --oauth-app-token oauth_token
# GitHub App Credentials:
prowler github --github-app-id app_id --github-app-key app_key
```
???+ note
If no login method is explicitly provided, Prowler will automatically attempt to authenticate using environment variables in the following order of precedence:
1. `GITHUB_PERSONAL_ACCESS_TOKEN`
2. `OAUTH_APP_TOKEN`
3. `GITHUB_APP_ID` and `GITHUB_APP_KEY`
#### Infrastructure as Code (IaC)
Prowler's Infrastructure as Code (IaC) provider enables you to scan local or remote infrastructure code for security and compliance issues using [Checkov](https://www.checkov.io/). This provider supports a wide range of IaC frameworks, allowing you to assess your code before deployment.
```console
# Scan a directory for IaC files
prowler iac --scan-path ./my-iac-directory
# Scan a remote GitHub repository (public or private)
prowler iac --scan-repository-url https://github.com/user/repo.git
# Authenticate to a private repo with GitHub username and PAT
prowler iac --scan-repository-url https://github.com/user/repo.git \
--github-username <username> --personal-access-token <token>
# Authenticate to a private repo with OAuth App Token
prowler iac --scan-repository-url https://github.com/user/repo.git \
--oauth-app-token <oauth_token>
# Specify frameworks to scan (default: all)
prowler iac --scan-path ./my-iac-directory --frameworks terraform kubernetes
# Exclude specific paths
prowler iac --scan-path ./my-iac-directory --exclude-path ./my-iac-directory/test,./my-iac-directory/examples
```
???+ note
- `--scan-path` and `--scan-repository-url` are mutually exclusive; only one can be specified at a time.
- For remote repository scans, authentication can be provided via CLI flags or environment variables (`GITHUB_OAUTH_APP_TOKEN`, `GITHUB_USERNAME`, `GITHUB_PERSONAL_ACCESS_TOKEN`). CLI flags take precedence.
- The IaC provider does not require cloud authentication for local scans.
- It is ideal for CI/CD pipelines and local development environments.
- For more details on supported frameworks and rules, see the [Checkov documentation](https://www.checkov.io/1.Welcome/Quick%20Start.html)
See more details about IaC scanning in the [IaC Tutorial](tutorials/iac/getting-started-iac.md) section.
## Prowler v2 Documentation
For **Prowler v2 Documentation**, refer to the [official repository](https://github.com/prowler-cloud/prowler/blob/8818f47333a0c1c1a457453c87af0ea5b89a385f/README.md).
- **Prowler CLI** (Command Line Interface)
- **Prowler App** (Web Application)
- [**Prowler Cloud**](https://cloud.prowler.com) A managed service built on top of Prowler App.
- [**Prowler Hub**](https://hub.prowler.com) A public library of versioned checks, cloud service artifacts, and compliance frameworks.
+173
View File
@@ -0,0 +1,173 @@
### Installation
Prowler App supports multiple installation methods based on your environment.
Refer to the [Prowler App Tutorial](../tutorials/prowler-app.md) for detailed usage instructions.
=== "Docker Compose"
_Requirements_:
* `Docker Compose` installed: https://docs.docker.com/compose/install/.
_Commands_:
``` bash
curl -LO https://raw.githubusercontent.com/prowler-cloud/prowler/refs/heads/master/docker-compose.yml
curl -LO https://raw.githubusercontent.com/prowler-cloud/prowler/refs/heads/master/.env
docker compose up -d
```
> Containers are built for `linux/amd64`. If your workstation's architecture is different, please set `DOCKER_DEFAULT_PLATFORM=linux/amd64` in your environment or use the `--platform linux/amd64` flag in the docker command.
> Enjoy Prowler App at http://localhost:3000 by signing up with your email and password.
???+ note
You can change the environment variables in the `.env` file. Note that it is not recommended to use the default values in production environments.
???+ note
There is a development mode available, you can use the file https://github.com/prowler-cloud/prowler/blob/master/docker-compose-dev.yml to run the app in development mode.
???+ warning
Google and GitHub authentication is only available in [Prowler Cloud](https://prowler.com).
=== "GitHub"
_Requirements_:
* `git` installed.
* `poetry` installed: [poetry installation](https://python-poetry.org/docs/#installation).
* `npm` installed: [npm installation](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm).
* `Docker Compose` installed: https://docs.docker.com/compose/install/.
???+ warning
Make sure to have `api/.env` and `ui/.env.local` files with the required environment variables. You can find the required environment variables in the [`api/.env.template`](https://github.com/prowler-cloud/prowler/blob/master/api/.env.example) and [`ui/.env.template`](https://github.com/prowler-cloud/prowler/blob/master/ui/.env.template) files.
_Commands to run the API_:
``` bash
git clone https://github.com/prowler-cloud/prowler \
cd prowler/api \
poetry install \
eval $(poetry env activate) \
set -a \
source .env \
docker compose up postgres valkey -d \
cd src/backend \
python manage.py migrate --database admin \
gunicorn -c config/guniconf.py config.wsgi:application
```
???+ important
Starting from Poetry v2.0.0, `poetry shell` has been deprecated in favor of `poetry env activate`.
If your poetry version is below 2.0.0 you must keep using `poetry shell` to activate your environment.
In case you have any doubts, consult the Poetry environment activation guide: https://python-poetry.org/docs/managing-environments/#activating-the-environment
> Now, you can access the API documentation at http://localhost:8080/api/v1/docs.
_Commands to run the API Worker_:
``` bash
git clone https://github.com/prowler-cloud/prowler \
cd prowler/api \
poetry install \
eval $(poetry env activate) \
set -a \
source .env \
cd src/backend \
python -m celery -A config.celery worker -l info -E
```
_Commands to run the API Scheduler_:
``` bash
git clone https://github.com/prowler-cloud/prowler \
cd prowler/api \
poetry install \
eval $(poetry env activate) \
set -a \
source .env \
cd src/backend \
python -m celery -A config.celery beat -l info --scheduler django_celery_beat.schedulers:DatabaseScheduler
```
_Commands to run the UI_:
``` bash
git clone https://github.com/prowler-cloud/prowler \
cd prowler/ui \
npm install \
npm run build \
npm start
```
> Enjoy Prowler App at http://localhost:3000 by signing up with your email and password.
???+ warning
Google and GitHub authentication is only available in [Prowler Cloud](https://prowler.com).
### Update Prowler App
Upgrade Prowler App installation using one of two options:
#### Option 1: Update Environment File
Edit the `.env` file and change version values:
```env
PROWLER_UI_VERSION="5.9.0"
PROWLER_API_VERSION="5.9.0"
```
#### Option 2: Use Docker Compose Pull
```bash
docker compose pull --policy always
```
The `--policy always` flag ensures that Docker pulls the latest images even if they already exist locally.
???+ note "What Gets Preserved During Upgrade"
Everything is preserved, nothing will be deleted after the update.
### Troubleshooting
If containers don't start, check logs for errors:
```bash
# Check logs for errors
docker compose logs
# Verify image versions
docker images | grep prowler
```
If you encounter issues, you can rollback to the previous version by changing the `.env` file back to your previous version and running:
```bash
docker compose pull
docker compose up -d
```
### Container versions
The available versions of Prowler CLI are the following:
- `latest`: in sync with `master` branch (please note that it is not a stable version)
- `v4-latest`: in sync with `v4` branch (please note that it is not a stable version)
- `v3-latest`: in sync with `v3` branch (please note that it is not a stable version)
- `<x.y.z>` (release): you can find the releases [here](https://github.com/prowler-cloud/prowler/releases), those are stable releases.
- `stable`: this tag always point to the latest release.
- `v4-stable`: this tag always point to the latest release for v4.
- `v3-stable`: this tag always point to the latest release for v3.
The container images are available here:
- Prowler App:
- [DockerHub - Prowler UI](https://hub.docker.com/r/prowlercloud/prowler-ui/tags)
- [DockerHub - Prowler API](https://hub.docker.com/r/prowlercloud/prowler-api/tags)
+208
View File
@@ -0,0 +1,208 @@
## Installation
Prowler is available as a project in [PyPI](https://pypi.org/project/prowler/). Install it as a Python package with `Python >= 3.9, <= 3.12`:
=== "pipx"
[pipx](https://pipx.pypa.io/stable/) installs Python applications in isolated environments. Use `pipx` for global installation.
_Requirements_:
* `Python >= 3.9, <= 3.12`
* `pipx` installed: [pipx installation](https://pipx.pypa.io/stable/installation/).
* AWS, GCP, Azure and/or Kubernetes credentials
_Commands_:
``` bash
pipx install prowler
prowler -v
```
Upgrade Prowler to the latest version:
``` bash
pipx upgrade prowler
```
=== "pip"
???+ warning
This method modifies the chosen installation environment. Consider using [pipx](https://docs.prowler.com/projects/prowler-open-source/en/latest/#__tabbed_1_1) for global installation.
_Requirements_:
* `Python >= 3.9, <= 3.12`
* `Python pip >= 21.0.0`
* AWS, GCP, Azure, M365 and/or Kubernetes credentials
_Commands_:
``` bash
pip install prowler
prowler -v
```
Upgrade Prowler to the latest version:
``` bash
pip install --upgrade prowler
```
=== "Docker"
_Requirements_:
* Have `docker` installed: https://docs.docker.com/get-docker/.
* In the command below, change `-v` to your local directory path in order to access the reports.
* AWS, GCP, Azure and/or Kubernetes credentials
> Containers are built for `linux/amd64`. If your workstation's architecture is different, please set `DOCKER_DEFAULT_PLATFORM=linux/amd64` in your environment or use the `--platform linux/amd64` flag in the docker command.
_Commands_:
``` bash
docker run -ti --rm -v /your/local/dir/prowler-output:/home/prowler/output \
--name prowler \
--env AWS_ACCESS_KEY_ID \
--env AWS_SECRET_ACCESS_KEY \
--env AWS_SESSION_TOKEN toniblyx/prowler:latest
```
=== "GitHub"
_Requirements for Developers_:
* `git`
* `poetry` installed: [poetry installation](https://python-poetry.org/docs/#installation).
* AWS, GCP, Azure and/or Kubernetes credentials
_Commands_:
```
git clone https://github.com/prowler-cloud/prowler
cd prowler
poetry install
poetry run python prowler-cli.py -v
```
???+ note
If you want to clone Prowler from Windows, use `git config core.longpaths true` to allow long file paths.
=== "Amazon Linux 2"
_Requirements_:
* `Python >= 3.9, <= 3.12`
* AWS, GCP, Azure and/or Kubernetes credentials
_Commands_:
```
python3 -m pip install --user pipx
python3 -m pipx ensurepath
pipx install prowler
prowler -v
```
=== "Ubuntu"
_Requirements_:
* `Ubuntu 23.04` or above, if you are using an older version of Ubuntu check [pipx installation](https://docs.prowler.com/projects/prowler-open-source/en/latest/#__tabbed_1_1) and ensure you have `Python >= 3.9, <= 3.12`.
* `Python >= 3.9, <= 3.12`
* AWS, GCP, Azure and/or Kubernetes credentials
_Commands_:
``` bash
sudo apt update
sudo apt install pipx
pipx ensurepath
pipx install prowler
prowler -v
```
=== "Brew"
_Requirements_:
* `Brew` installed in your Mac or Linux
* AWS, GCP, Azure and/or Kubernetes credentials
_Commands_:
``` bash
brew install prowler
prowler -v
```
=== "AWS CloudShell"
After the migration of AWS CloudShell from Amazon Linux 2 to Amazon Linux 2023 [[1]](https://aws.amazon.com/about-aws/whats-new/2023/12/aws-cloudshell-migrated-al2023/) [[2]](https://docs.aws.amazon.com/cloudshell/latest/userguide/cloudshell-AL2023-migration.html), there is no longer a need to manually compile Python 3.9 as it is already included in AL2023. Prowler can thus be easily installed following the generic method of installation via pip. Follow the steps below to successfully execute Prowler v4 in AWS CloudShell:
_Requirements_:
* Open AWS CloudShell `bash`.
_Commands_:
```bash
sudo bash
adduser prowler
su prowler
python3 -m pip install --user pipx
python3 -m pipx ensurepath
pipx install prowler
cd /tmp
prowler aws
```
???+ note
To download the results from AWS CloudShell, select Actions -> Download File and add the full path of each file. For the CSV file it will be something like `/tmp/output/prowler-output-123456789012-20221220191331.csv`
=== "Azure CloudShell"
_Requirements_:
* Open Azure CloudShell `bash`.
_Commands_:
```bash
python3 -m pip install --user pipx
python3 -m pipx ensurepath
pipx install prowler
cd /tmp
prowler azure --az-cli-auth
```
## Container versions
The available versions of Prowler CLI are the following:
- `latest`: in sync with `master` branch (please note that it is not a stable version)
- `v4-latest`: in sync with `v4` branch (please note that it is not a stable version)
- `v3-latest`: in sync with `v3` branch (please note that it is not a stable version)
- `<x.y.z>` (release): you can find the releases [here](https://github.com/prowler-cloud/prowler/releases), those are stable releases.
- `stable`: this tag always point to the latest release.
- `v4-stable`: this tag always point to the latest release for v4.
- `v3-stable`: this tag always point to the latest release for v3.
The container images are available here:
- Prowler CLI:
- [DockerHub](https://hub.docker.com/r/toniblyx/prowler/tags)
- [AWS Public ECR](https://gallery.ecr.aws/prowler-cloud/prowler)

Before

Width:  |  Height:  |  Size: 313 KiB

After

Width:  |  Height:  |  Size: 313 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 420 KiB

Before

Width:  |  Height:  |  Size: 192 KiB

After

Width:  |  Height:  |  Size: 192 KiB

+22
View File
@@ -0,0 +1,22 @@
Prowler App is a web application that simplifies running Prowler. It provides:
- **User-friendly interface** for configuring and executing scans
- Dashboard to **view results** and manage **security findings**
![Prowler App](img/overview.png)
## Components
Prowler App consists of three main components:
- **Prowler UI**: User-friendly web interface for running Prowler and viewing results, powered by Next.js
- **Prowler API**: Backend API that executes Prowler scans and stores results, built with Django REST Framework
- **Prowler SDK**: Python SDK that integrates with Prowler CLI for advanced functionality
Supporting infrastructure includes:
- **PostgreSQL**: Persistent storage of scan results
- **Celery Workers**: Asynchronous execution of Prowler scans
- **Valkey**: In-memory database serving as message broker for Celery workers
![Prowler App Architecture](img/prowler-app-architecture.png)
+37
View File
@@ -0,0 +1,37 @@
Prowler CLI is a command-line interface for running Prowler scans from the terminal.
```console
prowler <provider>
```
![Prowler CLI Execution](../img/short-display.png)
## Prowler Dashboard
```console
prowler dashboard
```
![Prowler Dashboard](img/dashboard.png)
Prowler includes hundreds of security controls aligned with widely recognized industry frameworks and standards, including:
- CIS Benchmarks (AWS, Azure, Microsoft 365, Kubernetes, GitHub)
- NIST SP 800-53 (rev. 4 and 5) and NIST SP 800-171
- NIST Cybersecurity Framework (CSF)
- CISA Guidelines
- FedRAMP Low & Moderate
- PCI DSS v3.2.1 and v4.0
- ISO/IEC 27001:2013 and 2022
- SOC 2
- GDPR (General Data Protection Regulation)
- HIPAA (Health Insurance Portability and Accountability Act)
- FFIEC (Federal Financial Institutions Examination Council)
- ENS RD2022 (Spanish National Security Framework)
- GxP 21 CFR Part 11 and EU Annex 11
- RBI Cybersecurity Framework (Reserve Bank of India)
- KISA ISMS-P (Korean Information Security Management System)
- MITRE ATT&CK
- AWS Well-Architected Framework (Security & Reliability Pillars)
- AWS Foundational Technical Review (FTR)
- Microsoft NIS2 Directive (EU)
- Custom threat scoring frameworks (prowler_threatscore)
- Custom security frameworks for enterprise needs
+76 -13
View File
@@ -1,24 +1,87 @@
# Security
## Compliance and Trust
We publish our live SOC 2 Type 2 Compliance data at [https://trust.prowler.com](https://trust.prowler.com)
As an **AWS Partner**, we have passed the [AWS Foundation Technical Review (FTR)](https://aws.amazon.com/partners/foundational-technical-review/).
## Encryption (Prowler Cloud)
We use encryption everywhere possible. The data and communications used by **Prowler Cloud** are **encrypted at-rest** and **in-transit**.
## Data Retention Policy (Prowler Cloud)
Prowler Cloud is GDPR compliant in regards to personal data and the ["right to be forgotten"](https://gdpr.eu/right-to-be-forgotten/). When a user deletes their account their user information will be deleted from Prowler Cloud online and backup systems within 10 calendar days.
## Software Security
As an **AWS Partner** and we have passed the [AWS Foundation Technical Review (FTR)](https://aws.amazon.com/partners/foundational-technical-review/) and we use the following tools and automation to make sure our code is secure and dependencies up-to-dated:
We follow a **security-by-design approach** throughout our software development lifecycle. All changes go through automated checks at every stage, from local development to production deployment.
- `bandit` for code security review.
- `safety` and `dependabot` for dependencies.
- `hadolint` and `dockle` for our containers security.
- `snyk` in Docker Hub.
- `clair` in Amazon ECR.
- `vulture`, `flake8`, `black` and `pylint` for formatting and best practices.
We enforce [pre-commit](https://github.com/prowler-cloud/prowler/blob/master/.pre-commit-config.yaml) validations to catch issues early, and [our CI/CD pipelines](https://github.com/prowler-cloud/prowler/tree/master/.github) include multiple security gates to ensure code quality, secure configurations, and compliance with internal standards.
## Reporting Vulnerabilities
Our container registries are continuously scanned for vulnerabilities, with findings automatically reported to our security team for assessment and remediation. This process evolves alongside our stack as we adopt new languages, frameworks, and technologies, ensuring our security practices remain comprehensive, proactive, and adaptable.
If you would like to report a vulnerability or have a security concern regarding Prowler Open Source or Prowler Cloud service, please submit the information by contacting to us via [**support.prowler.com**](http://support.prowler.com).
## Reporting Vulnerabilities
The information you share with the Prowler team as part of this process is kept confidential within Prowler. We will only share this information with a third party if the vulnerability you report is found to affect a third-party product, in which case we will share this information with the third-party product's author or manufacturer. Otherwise, we will only share this information as permitted by you.
At Prowler, we consider the security of our open source software and systems a top priority. But no matter how much effort we put into system security, there can still be vulnerabilities present.
We will review the submitted report, and assign it a tracking number. We will then respond to you, acknowledging receipt of the report, and outline the next steps in the process.
If you discover a vulnerability, we would like to know about it so we can take steps to address it as quickly as possible. We would like to ask you to help us better protect our users, our clients and our systems.
You will receive a non-automated response to your initial contact within 24 hours, confirming receipt of your reported vulnerability.
When reporting vulnerabilities, please consider (1) attack scenario / exploitability, and (2) the security impact of the bug. The following issues are considered out of scope:
We will coordinate public notification of any validated vulnerability with you. Where possible, we prefer that our respective public disclosures be posted simultaneously.
- Social engineering support or attacks requiring social engineering.
- Clickjacking on pages with no sensitive actions.
- Cross-Site Request Forgery (CSRF) on unauthenticated forms or forms with no sensitive actions.
- Attacks requiring Man-In-The-Middle (MITM) or physical access to a user's device.
- Previously known vulnerable libraries without a working Proof of Concept (PoC).
- Comma Separated Values (CSV) injection without demonstrating a vulnerability.
- Missing best practices in SSL/TLS configuration.
- Any activity that could lead to the disruption of service (DoS).
- Rate limiting or brute force issues on non-authentication endpoints.
- Missing best practices in Content Security Policy (CSP).
- Missing HttpOnly or Secure flags on cookies.
- Configuration of or missing security headers.
- Missing email best practices, such as invalid, incomplete, or missing SPF/DKIM/DMARC records.
- Vulnerabilities only affecting users of outdated or unpatched browsers (less than two stable versions behind).
- Software version disclosure, banner identification issues, or descriptive error messages.
- Tabnabbing.
- Issues that require unlikely user interaction.
- Improper logout functionality and improper session timeout.
- CORS misconfiguration without an exploitation scenario.
- Broken link hijacking.
- Automated scanning results (e.g., sqlmap, Burp active scanner) that have not been manually verified.
- Content spoofing and text injection issues without a clear attack vector.
- Email spoofing without exploiting security flaws.
- Dead links or broken links.
- User enumeration.
Testing guidelines:
- Do not run automated scanners on other customer projects. Running automated scanners can run up costs for our users. Aggressively configured scanners might inadvertently disrupt services, exploit vulnerabilities, lead to system instability or breaches and violate Terms of Service from our upstream providers. Our own security systems won't be able to distinguish hostile reconnaissance from whitehat research. If you wish to run an automated scanner, notify us at support@prowler.com and only run it on your own Prowler app project. Do NOT attack Prowler in usage of other customers.
- Do not take advantage of the vulnerability or problem you have discovered, for example by downloading more data than necessary to demonstrate the vulnerability or deleting or modifying other people's data.
Reporting guidelines:
- File a report through our Support Desk at https://support.prowler.com
- If it is about a lack of a security functionality, please file a feature request instead at https://github.com/prowler-cloud/prowler/issues
- Do provide sufficient information to reproduce the problem, so we will be able to resolve it as quickly as possible.
- If you have further questions and want direct interaction with the Prowler team, please contact us at via our Community Slack at goto.prowler.com/slack.
Disclosure guidelines:
- In order to protect our users and customers, do not reveal the problem to others until we have researched, addressed and informed our affected customers.
- If you want to publicly share your research about Prowler at a conference, in a blog or any other public forum, you should share a draft with us for review and approval at least 30 days prior to the publication date. Please note that the following should not be included:
- Data regarding any Prowler user or customer projects.
- Prowler customers' data.
- Information about Prowler employees, contractors or partners.
What we promise:
- We will respond to your report within 5 business days with our evaluation of the report and an expected resolution date.
- If you have followed the instructions above, we will not take any legal action against you in regard to the report.
- We will handle your report with strict confidentiality, and not pass on your personal details to third parties without your permission.
- We will keep you informed of the progress towards resolving the problem.
- In the public information concerning the problem reported, we will give your name as the discoverer of the problem (unless you desire otherwise).
We strive to resolve all problems as quickly as possible, and we would like to play an active role in the ultimate publication on the problem after it is resolved.
+18 -18
View File
@@ -1,6 +1,17 @@
# AWS Authentication in Prowler
Proper authentication is required for Prowler to perform security checks across AWS resources. Ensure that AWS-CLI is correctly configured or manually declare AWS credentials before running scans.
Prowler requires AWS credentials to function properly. Authentication is available through the following methods:
## Required Permissions
To ensure full functionality, attach the following AWS managed policies to the designated user or role:
- `arn:aws:iam::aws:policy/SecurityAudit`
- `arn:aws:iam::aws:policy/job-function/ViewOnlyAccess`
### Additional Permissions
For certain checks, additional read-only permissions are required. Attach the following custom policy to your role: [prowler-additions-policy.json](https://github.com/prowler-cloud/prowler/blob/master/permissions/prowler-additions-policy.json)
## Configure AWS Credentials
@@ -18,24 +29,13 @@ export AWS_SECRET_ACCESS_KEY="XXXXXXXXX"
export AWS_SESSION_TOKEN="XXXXXXXXX"
```
These credentials must be associated with a user or role with the necessary permissions to perform security checks.
These credentials must be associated with a user or role with the necessary permissions to perform security checks.
## Assign Required AWS Permissions
To ensure full functionality, attach the following AWS managed policies to the designated user or role:
- `arn:aws:iam::aws:policy/SecurityAudit`
- `arn:aws:iam::aws:policy/job-function/ViewOnlyAccess`
???+ note
Some security checks require read-only additional permissions. Attach the following custom policies to the role: [prowler-additions-policy.json](https://github.com/prowler-cloud/prowler/blob/master/permissions/prowler-additions-policy.json). If you want Prowler to send findings to [AWS Security Hub](https://aws.amazon.com/security-hub), make sure to also attach the custom policy: [prowler-security-hub.json](https://github.com/prowler-cloud/prowler/blob/master/permissions/prowler-security-hub.json).
## AWS Profiles
## AWS Profiles and Service Scanning in Prowler
Prowler supports authentication and security assessments using custom AWS profiles and can optionally scan unused services.
**Using Custom AWS Profiles**
Prowler allows you to specify a custom AWS profile using the following command:
Specify a custom AWS profile using the following command:
```console
prowler aws -p/--profile <profile_name>
@@ -43,7 +43,7 @@ prowler aws -p/--profile <profile_name>
## Multi-Factor Authentication (MFA)
If MFA enforcement is required for your IAM entity, you can use `--mfa`. Prowler will prompt you to enter the following in order to get a new session:
For IAM entities requiring Multi-Factor Authentication (MFA), use the `--mfa` flag. Prowler prompts for the following values to initiate a new session:
- ARN of your MFA device
- TOTP (Time-Based One-Time Password)
- **ARN of your MFA device**
- **TOTP (Time-Based One-Time Password)**
@@ -97,6 +97,9 @@ This method grants permanent access and is the recommended setup for production
![External ID](./img/prowler-cloud-external-id.png)
![Stack Data](./img/fill-stack-data.png)
!!! info
An **External ID** is required when assuming the *ProwlerScan* role to comply with AWS [confused deputy prevention](https://docs.aws.amazon.com/IAM/latest/UserGuide/confused-deputy.html).
6. Acknowledge the IAM resource creation warning and proceed
![Stack Creation Second Step](./img/stack-creation-second-step.png)
+70 -1
View File
@@ -1,5 +1,13 @@
# AWS Organizations in Prowler
Prowler can integrate with AWS Organizations to manage the visibility and onboarding of accounts centrally.
When trusted access is enabled with the Organization, Prowler can discover accounts as they are created and even automate deployment of the Prowler Scan IAM Role.
> ️ Trusted access can be enabled in the Management Account from the AWS Console under **AWS Organizations → Settings → Trusted access for AWS CloudFormation StackSets**.
When not using StackSets or Prowler and only needing to scan AWS Organization accounts using the CLI, it is possible to assume a role in each account manually or automate that logic with custom scripts.
## Retrieving AWS Account Details
If AWS Organizations is enabled, Prowler can fetch detailed account information during scans, including:
@@ -33,7 +41,7 @@ Prowler will scan the AWS account and get the account details from AWS Organizat
### Handling JSON Output
In Prowlers JSON output, tags are encoded in Base64 to prevent formatting errors in CSV or JSON outputs. This ensures compatibility when exporting findings.
In Prowler's JSON output, tags are encoded in Base64 to prevent formatting errors in CSV or JSON outputs. This ensures compatibility when exporting findings.
```json
"Account Email": "my-prod-account@domain.com",
@@ -51,6 +59,67 @@ The additional fields in CSV header output are as follows:
- ACCOUNT\_DETAILS\_ORG
- ACCOUNT\_DETAILS\_TAGS
## Deploying Prowler IAM Roles Across AWS Organizations
When onboarding multiple AWS accounts into Prowler Cloud, it is important to deploy the Prowler Scan IAM Role in each account. The most efficient way to do this across an AWS Organization is by leveraging AWS CloudFormation StackSets, which rolls out infrastructure—like IAM roles—to all accounts centrally from the Management or Delegated Admin account.
When using Infrastructure as Code (IaC), Terraform is recommended to manage this deployment systematically.
### Recommended Approach
- **Use StackSets** from the **Management Account** (or a Delegated Admin/Security Account).
- **Use Terraform** to orchestrate the deployment.
- **Use the official CloudFormation template** provided by Prowler.
- Target specific Organizational Units (OUs) or the entire Organization.
???+ note
A detailed community article this implementation is based on is available here:
[Deploy IAM Roles Across an AWS Organization as Code (Unicrons)](https://unicrons.cloud/en/2024/10/14/deploy-iam-roles-across-an-aws-organization-as-code/)
This guide has been adapted with permission and aligned with Prowlers IAM role requirements.
---
### Step-by-Step Guide Using Terraform
Below is a ready Terraform snippet that deploys the [Prowler Scan IAM Role CloudFormation template](https://github.com/prowler-cloud/prowler/blob/master/permissions/templates/cloudformation/prowler-scan-role.yml) across the AWS Organization using StackSets:
```hcl title="main.tf"
data "aws_caller_identity" "this" {}
data "aws_organizations_organization" "this" {}
module "prowler-scan-role" {
source = "unicrons/organization-iam-role/aws"
stack_set_name = "prowler-scan-role"
stack_set_description = "Deploy Prowler Scan IAM Role across all organization accounts"
template_path = "${path.root}/prowler-scan-role.yaml"
template_parameters = {
ExternalId = "<< external ID >>" # Replace with the External ID provided by Prowler Cloud
}
# Specific OU IDs can be specified instead of root
organizational_unit_ids = [data.aws_organizations_organization.this.roots[0].id]
}
```
#### `prowler-scan-role.yaml`
Download or reference the official CloudFormation template directly from GitHub:
- [prowler-scan-role.yml](https://github.com/prowler-cloud/prowler/blob/master/permissions/templates/cloudformation/prowler-scan-role.yml)
---
### IAM Role: External ID Support
Include the `ExternalId` parameter in the StackSet if required by the organization's Prowler Cloud setup. This ensures secure cross-account access for scanning.
---
When encountering issues during deployment or needing to target specific OUs or environments (e.g., dev/staging/prod), reach out to the Prowler team via [Slack Community](https://prowler.com/slack) or [Support](mailto:support@prowler.com).
## Extra: Run Prowler across all accounts in AWS Organizations by assuming roles
### Running Prowler Across All AWS Organization Accounts
+2 -2
View File
@@ -21,7 +21,7 @@ AWS Security Hub can be enabled using either of the following methods:
#### Enabling AWS Security Hub for Prowler Integration
If AWS Security Hub is already enabled, you can proceed to the [next section](#enable-prowler-integration).
If AWS Security Hub is already enabled, you can proceed to the [next section](#enabling-prowler-integration-in-aws-security-hub).
1. Enable AWS Security Hub via Console: Open the **AWS Security Hub** console: https://console.aws.amazon.com/securityhub/.
@@ -33,7 +33,7 @@ If AWS Security Hub is already enabled, you can proceed to the [next section](#e
#### Enabling Prowler Integration in AWS Security Hub
If the Prowler integration is already enabled in AWS Security Hub, you can proceed to the [next section](#send-findings) and begin sending findings.
If the Prowler integration is already enabled in AWS Security Hub, you can proceed to the [next section](#sending-findings-to-aws-security-hub) and begin sending findings.
Once **AWS Security Hub** is activated, **Prowler** must be enabled as partner integration to allow security findings to be sent to it.
+68 -19
View File
@@ -1,28 +1,77 @@
# Azure Authentication in Prowler
By default, Prowler utilizes the Azure Python SDK identity package for authentication, leveraging the classes `DefaultAzureCredential` and `InteractiveBrowserCredential`. This enables authentication against Azure using the following approaches:
Prowler for Azure supports multiple authentication types. To use a specific method, pass the appropriate flag during execution:
- Service principal authentication via environment variables (Enterprise Application)
- Currently stored AZ CLI credentials
- Interactive browser authentication
- Managed identity authentication
- [**Service Principal Application**](https://learn.microsoft.com/en-us/entra/identity-platform/app-objects-and-service-principals?tabs=browser#service-principal-object) (**Recommended**)
- Existing **AZ CLI credentials**
- **Interactive browser authentication**
- [**Managed Identity**](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/overview) authentication
Before launching the tool, specify the desired method using the following flags:
> ⚠️ **Important:** For Prowler App, only Service Principal authentication is supported.
### Service Principal Application Authentication
Enable Prowler authentication using a Service Principal Application by setting up the following environment variables:
```console
# Service principal authentication:
prowler azure --sp-env-auth
# AZ CLI authentication
prowler azure --az-cli-auth
# Browser authentication
prowler azure --browser-auth --tenant-id "XXXXXXXX"
# Managed identity authentication
prowler azure --managed-identity-auth
export AZURE_CLIENT_ID="XXXXXXXXX"
export AZURE_TENANT_ID="XXXXXXXXX"
export AZURE_CLIENT_SECRET="XXXXXXX"
```
## Permission Configuration
Execution with the `--sp-env-auth` flag fails if these variables are not set or exported.
To ensure Prowler can access the required resources within your Azure account, proper permissions must be configured. Refer to the [Requirements](../../getting-started/requirements.md) section for details on setting up necessary privileges.
Refer to the [Create Prowler Service Principal](create-prowler-service-principal.md) guide for detailed setup instructions.
### Azure Authentication Methods
Prowler for Azure supports the following authentication methods:
- **AZ CLI Authentication (`--az-cli-auth`)** Automated authentication using stored AZ CLI credentials.
- **Managed Identity Authentication (`--managed-identity-auth`)** Automated authentication via Azure Managed Identity.
- **Browser Authentication (`--browser-auth`)** Requires the user to authenticate using the default browser. The `tenant-id` parameter is mandatory for this method.
### Required Permissions
Prowler for Azure requires two types of permission scopes:
#### Microsoft Entra ID Permissions
These permissions allow Prowler to retrieve metadata from the assumed identity and perform specific Entra checks. While not mandatory for execution, they enhance functionality.
Required permissions:
- `Directory.Read.All`
- `Policy.Read.All`
- `UserAuthenticationMethod.Read.All` (used for Entra multifactor authentication checks)
???+ note
Replace `Directory.Read.All` with `Domain.Read.All` for more restrictive permissions. Note that Entra checks related to DirectoryRoles and GetUsers will not run with this permission.
#### Subscription Scope Permissions
These permissions are required to perform security checks against Azure resources. The following **RBAC roles** must be assigned per subscription to the entity used by Prowler:
- `Reader` Grants read-only access to Azure resources.
- `ProwlerRole` A custom role with minimal permissions, defined in the [prowler-azure-custom-role](https://github.com/prowler-cloud/prowler/blob/master/permissions/prowler-azure-custom-role.json).
???+ note
The `assignableScopes` field in the JSON custom role file must be updated to reflect the correct subscription or management group. Use one of the following formats: `/subscriptions/<subscription-id>` or `/providers/Microsoft.Management/managementGroups/<management-group-id>`.
### Assigning Permissions
To properly configure permissions, follow these guides:
- [Microsoft Entra ID permissions](create-prowler-service-principal.md#assigning-proper-permissions)
- [Azure subscription permissions](subscriptions.md)
???+ warning
Some permissions in `ProwlerRole` involve **write access**. If a `ReadOnly` lock is attached to certain resources, you may encounter errors, and findings for those checks will not be available.
#### Checks Requiring `ProwlerRole`
The following security checks require the `ProwlerRole` permissions for execution. Ensure the role is assigned to the identity assumed by Prowler before running these checks:
- `app_function_access_keys_configured`
- `app_function_ftps_deployment_disabled`
@@ -73,7 +73,7 @@ Permissions can be assigned via the Azure Portal or the Azure CLI.
7. Finally, search for "Directory", "Policy" and "UserAuthenticationMethod" select the following permissions:
- `Domain.Read.All`
- `Directory.Read.All`
- `Policy.Read.All`
+3 -3
View File
@@ -21,7 +21,7 @@ Prowler allows you to specify one or more subscriptions for scanning (up to N),
To perform scans, ensure that the identity assumed by Prowler has the appropriate permissions.
By default, Prowler scans all accessible subscriptions. If you need to audit specific subscriptions, you must assign the necessary role `Reader` for each one. For streamlined and less repetitive role assignments in multi-subscription environments, refer to the [following section](#recommendation-for-multiple-subscriptions).
By default, Prowler scans all accessible subscriptions. If you need to audit specific subscriptions, you must assign the necessary role `Reader` for each one. For streamlined and less repetitive role assignments in multi-subscription environments, refer to the [following section](#recommendation-for-managing-multiple-subscriptions).
### Assigning the Reader Role in Azure Portal
@@ -76,7 +76,7 @@ Navigate to the subscription you want to audit with Prowler.
Some read-only permissions required for specific security checks are not included in the built-in Reader role. To support these checks, Prowler utilizes a custom role, defined in [prowler-azure-custom-role](https://github.com/prowler-cloud/prowler/blob/master/permissions/prowler-azure-custom-role.json). Once created, this role can be assigned following the same process as the `Reader` role.
The checks requiring this `ProwlerRole` can be found in the [requirements section](../../getting-started/requirements.md#checks-that-require-prowlerrole).
The checks requiring this `ProwlerRole` can be found in this [section](../../tutorials/azure/authentication.md#checks-requiring-prowlerrole).
#### Create ProwlerRole via Azure Portal
@@ -152,7 +152,7 @@ Scanning multiple subscriptions requires creating and assigning roles for each,
![Create management group](../../img/create-management-group.gif)
2. **Assign Roles**: Assign necessary roles to the management group, similar to the [role assignment process](#assign-the-appropriate-permissions-to-the-identity-that-is-going-to-be-assumed-by-prowler).
2. **Assign Roles**: Assign necessary roles to the management group, similar to the [role assignment process](#assigning-permissions-for-subscription-scans).
Role assignment should be done at the management group level instead of per subscription.
@@ -0,0 +1,367 @@
# Bulk Provider Provisioning in Prowler
Prowler enables automated provisioning of multiple cloud providers through the Bulk Provider Provisioning tool. This approach streamlines the onboarding process for organizations managing numerous cloud accounts, subscriptions, and projects across AWS, Azure, GCP, Kubernetes, Microsoft 365, and GitHub.
The tool is available in the Prowler repository at: [util/prowler-bulk-provisioning](https://github.com/prowler-cloud/prowler/tree/master/util/prowler-bulk-provisioning)
![](./img/bulk-provider-provisioning.png)
## Overview
The Bulk Provider Provisioning tool automates the creation of cloud providers in Prowler App or Prowler Cloud by:
* Reading provider configurations from YAML files
* Creating providers with appropriate authentication credentials
* Testing connections to verify successful authentication
* Processing multiple providers concurrently for efficiency
## Prerequisites
### Requirements
* Python 3.7 or higher
* Prowler API token (from Prowler Cloud or self-hosted Prowler App)
* For self-hosted Prowler App, remember to [point to your API base URL](#custom-api-endpoints)
* Authentication credentials for target cloud providers
### Installation
Clone the repository and install the required dependencies:
```bash
git clone https://github.com/prowler-cloud/prowler.git
cd prowler/util/prowler-bulk-provisioning
pip install -r requirements.txt
```
### Authentication Setup
Configure your Prowler API token:
```bash
export PROWLER_API_TOKEN="your-prowler-api-token"
```
To obtain an API token programmatically:
```bash
export PROWLER_API_TOKEN=$(curl --location 'https://api.prowler.com/api/v1/tokens' \
--header 'Content-Type: application/vnd.api+json' \
--header 'Accept: application/vnd.api+json' \
--data-raw '{
"data": {
"type": "tokens",
"attributes": {
"email": "your@email.com",
"password": "your-password"
}
}
}' | jq -r .data.attributes.access)
```
## Configuration File Structure
Create a YAML file listing your cloud providers and credentials:
```yaml
# providers.yaml
- provider: aws
uid: "123456789012" # AWS Account ID
alias: "production-account"
auth_method: role
credentials:
role_arn: "arn:aws:iam::123456789012:role/ProwlerScanRole"
external_id: "prowler-external-id"
- provider: azure
uid: "00000000-1111-2222-3333-444444444444" # Subscription ID
alias: "azure-production"
auth_method: service_principal
credentials:
tenant_id: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
client_id: "ffffffff-1111-2222-3333-444444444444"
client_secret: "your-client-secret"
- provider: gcp
uid: "my-gcp-project" # Project ID
alias: "gcp-production"
auth_method: service_account
credentials:
service_account_key_json_path: "./service-account.json"
```
## Running the Bulk Provisioning Tool
### Basic Usage
To provision all providers from your configuration file:
```bash
python prowler_bulk_provisioning.py providers.yaml
```
The tool automatically tests each provider connection after creation (enabled by default).
### Dry Run Mode
Test your configuration without making API calls:
```bash
python prowler_bulk_provisioning.py providers.yaml --dry-run
```
### Skip Connection Testing
To provision providers without testing connections:
```bash
python prowler_bulk_provisioning.py providers.yaml --test-provider false
```
### Test Existing Providers Only
To verify connections for already provisioned providers:
```bash
python prowler_bulk_provisioning.py providers.yaml --test-provider-only
```
## Provider-Specific Configuration
### AWS Provider Configuration
#### Using IAM Role (Recommended)
```yaml
- provider: aws
uid: "123456789012"
alias: "aws-production"
auth_method: role
credentials:
role_arn: "arn:aws:iam::123456789012:role/ProwlerScanRole"
external_id: "optional-external-id"
session_name: "prowler-scan-session" # optional
duration_seconds: 3600 # optional
```
#### Using Access Keys
```yaml
- provider: aws
uid: "123456789012"
alias: "aws-development"
auth_method: credentials
credentials:
access_key_id: "AKIA..."
secret_access_key: "..."
session_token: "..." # optional for temporary credentials
```
### Azure Provider Configuration
```yaml
- provider: azure
uid: "subscription-uuid"
alias: "azure-production"
auth_method: service_principal
credentials:
tenant_id: "tenant-uuid"
client_id: "client-uuid"
client_secret: "client-secret"
```
### GCP Provider Configuration
#### Using Service Account JSON
```yaml
- provider: gcp
uid: "project-id"
alias: "gcp-production"
auth_method: service_account
credentials:
service_account_key_json_path: "/path/to/key.json"
```
#### Using OAuth2 Credentials
```yaml
- provider: gcp
uid: "project-id"
alias: "gcp-production"
auth_method: oauth2
credentials:
client_id: "123456789.apps.googleusercontent.com"
client_secret: "GOCSPX-xxxx"
refresh_token: "1//0exxxxxx"
```
### Kubernetes Provider Configuration
```yaml
- provider: kubernetes
uid: "context-name"
alias: "eks-production"
auth_method: kubeconfig
credentials:
kubeconfig_path: "~/.kube/config"
# OR inline configuration:
# kubeconfig_inline: |
# apiVersion: v1
# clusters: ...
```
### Microsoft 365 Provider Configuration
```yaml
- provider: m365
uid: "domain.onmicrosoft.com"
alias: "m365-tenant"
auth_method: service_principal
credentials:
tenant_id: "tenant-uuid"
client_id: "client-uuid"
client_secret: "client-secret"
```
### GitHub Provider Configuration
#### Using Personal Access Token
```yaml
- provider: github
uid: "organization-name"
alias: "github-org"
auth_method: personal_access_token
credentials:
token: "ghp_..."
```
#### Using GitHub App
```yaml
- provider: github
uid: "organization-name"
alias: "github-org"
auth_method: github_app
credentials:
app_id: "123456"
private_key_path: "/path/to/private-key.pem"
```
## Advanced Configuration
### Concurrent Processing
Adjust the number of concurrent provider creations:
```bash
python prowler_bulk_provisioning.py providers.yaml --concurrency 10
```
### Custom API Endpoints
For self-hosted Prowler App installations:
```bash
python prowler_bulk_provisioning.py providers.yaml \
--base-url http://localhost:8080/api/v1
```
### Timeout Configuration
Set custom timeout for API requests:
```bash
python prowler_bulk_provisioning.py providers.yaml --timeout 120
```
## Bulk Provider Management
### Deleting Multiple Providers
To remove all providers from your Prowler account:
```bash
python nuke_providers.py --confirm
```
Filter deletions by provider type:
```bash
python nuke_providers.py --confirm --filter-provider aws
```
Filter deletions by alias pattern:
```bash
python nuke_providers.py --confirm --filter-alias "test-*"
```
## Configuration File Format
The tool uses YAML format for provider configuration files. Each provider entry requires:
* `provider`: The cloud provider type (aws, azure, gcp, kubernetes, m365, github)
* `uid`: Unique identifier for the provider (account ID, subscription ID, project ID, etc.)
* `alias`: A friendly name for the provider
* `auth_method`: Authentication method to use
* `credentials`: Authentication credentials specific to the provider and method
Example YAML structure:
```yaml
- provider: aws
uid: "123456789012"
alias: "production"
auth_method: role
credentials:
role_arn: "arn:aws:iam::123456789012:role/ProwlerScan"
```
## Example Output
Successful provider provisioning:
```
[1] ✅ Created provider (id=db9a8985-f9ec-4dd8-b5a0-e05ab3880bed)
[1] ✅ Created secret (id=466f76c6-5878-4602-a4bc-13f9522c1fd2)
[1] ✅ Connection test: Connected
[2] ✅ Created provider (id=7a99f789-0cf5-4329-8279-2d443a962676)
[2] ✅ Created secret (id=c5702180-f7c4-40fd-be0e-f6433479b126)
[2] ⚠️ Connection test: Not connected
Done. Success: 2 Failures: 0
```
## Troubleshooting
### Invalid API Token
```
Error: 401 Unauthorized
Solution: Verify your PROWLER_API_TOKEN or --token parameter
```
### Network Timeouts
```
Error: Connection timeout
Solution: Increase timeout with --timeout 120
```
### Provider Already Exists
```
Error: Provider with this UID already exists
Solution: Use different UID or delete existing provider first
```
### Authentication Failures
```
Connection test: Not connected
Solution: Verify credentials and IAM permissions
```
+41 -40
View File
@@ -1,40 +1,40 @@
# GCP Authentication in Prowler
## Default Authentication
## Required Permissions
By default, Prowler uses your User Account credentials. You can configure authentication as follows:
Prowler for Google Cloud requires the following permissions:
- `gcloud init` to use a new account, or
- `gcloud config set account <account>` to use an existing account.
### IAM Roles
- **Reader (`roles/reader`)** Must be granted at the **project, folder, or organization** level to allow scanning of target projects.
Then, obtain your access credentials using: `gcloud auth application-default login`.
### Project-Level Settings
## Using Service Account Keys
At least one project must have the following configurations:
Alternatively, Service Account keys can be generated and downloaded in JSON format. Follow the steps in the Google Cloud IAM guide (https://cloud.google.com/iam/docs/creating-managing-service-account-keys) to create and manage service account keys. Provide the path to the key file using:
- **Identity and Access Management (IAM) API (`iam.googleapis.com`)** Must be enabled via:
```console
prowler gcp --credentials-file path
```
- The [Google Cloud API UI](https://console.cloud.google.com/apis/api/iam.googleapis.com/metrics), or
- The `gcloud` CLI:
```sh
gcloud services enable iam.googleapis.com --project <your-project-id>
```
- **Service Usage Consumer (`roles/serviceusage.serviceUsageConsumer`)** IAM Role Required for resource scanning.
- **Quota Project Setting** Define a quota project using either:
- The `gcloud` CLI:
```sh
gcloud auth application-default set-quota-project <project-id>
```
- Setting an environment variable:
```sh
export GOOGLE_CLOUD_QUOTA_PROJECT=<project-id>
```
???+ note
`prowler` will scan the GCP project associated with the credentials.
## Using an access token
If you already have an access token (e.g., generated with `gcloud auth print-access-token`), you can run Prowler with:
```bash
export CLOUDSDK_AUTH_ACCESS_TOKEN=$(gcloud auth print-access-token)
prowler gcp --project-ids <project-id>
```
???+ note
If using this method, it's recommended to also set the default project explicitly:
```bash
export GOOGLE_CLOUD_PROJECT=<project-id>
```
## Credentials lookup order
Prowler follows the same credential search process as [Google authentication libraries](https://cloud.google.com/docs/authentication/application-default-credentials#search_order), checking credentials in this order:
@@ -52,26 +52,27 @@ Prowler follows the same credential search process as [Google authentication lib
Prowler will use the enabled Google Cloud APIs to get the information needed to perform the checks.
## Required Permissions
To ensure full functionality, Prowler for Google Cloud needs the following permissions to be set:
- **Reader (`roles/reader`) IAM role**: granted at the project / folder / org level in order to scan the target projects
- **Project level settings**: you need to have at least one project with the below settings:
- Identity and Access Management (IAM) API (`iam.googleapis.com`) enabled by either using the
[Google Cloud API UI](https://console.cloud.google.com/apis/api/iam.googleapis.com/metrics) or
by using the gcloud CLI `gcloud services enable iam.googleapis.com --project <your-project-id>` command
- Set the quota project to be this project by either running `gcloud auth application-default set-quota-project <project-id>` or by setting an environment variable:
`export GOOGLE_CLOUD_QUOTA_PROJECT=<project-id>`
The above settings must be associated to a user or service account.
## Using an Access Token
For existing access tokens (e.g., generated with `gcloud auth print-access-token`), run Prowler with:
```bash
export CLOUDSDK_AUTH_ACCESS_TOKEN=$(gcloud auth print-access-token)
prowler gcp --project-ids <project-id>
```
???+ note
Prowler will use the enabled Google Cloud APIs to get the information needed to perform the checks.
When using this method, also set the default project explicitly:
```bash
export GOOGLE_CLOUD_PROJECT=<project-id>
```
## Impersonating a GCP Service Account in Prowler
## Impersonating a GCP Service Account
To impersonate a GCP service account, use the `--impersonate-service-account` argument followed by the service account email:
+3 -3
View File
@@ -1,4 +1,4 @@
# GitHub Authentication
# 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:
@@ -6,7 +6,7 @@ Prowler supports multiple methods to [authenticate with GitHub](https://docs.git
- **OAuth App Token**
- **GitHub App Credentials**
This flexibility allows you to scan and analyze your GitHub account, including repositories, organizations, and applications, using the method that best suits your use case.
This flexibility enables scanning and analysis of GitHub accounts, including repositories, organizations, and applications, using the method that best suits the use case.
## Supported Login Methods
@@ -44,4 +44,4 @@ If no login method is explicitly provided, Prowler will automatically attempt to
3. `GITHUB_APP_ID` and `GITHUB_APP_KEY` (where the key is the content of the private key file)
???+ note
Ensure the corresponding environment variables are set up before running Prowler for automatic detection if you don't plan to specify the login method.
Ensure the corresponding environment variables are set up before running Prowler for automatic detection when not specifying the login method.
@@ -11,7 +11,7 @@ This guide explains how to set up authentication with GitHub for Prowler. The do
### 1. Personal Access Token (PAT)
Personal Access Tokens provide the simplest GitHub authentication method and support individual user authentication or testing scenarios.
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"
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.
@@ -186,7 +186,10 @@ GitHub Apps provide the recommended integration method for accessing multiple re
- **Account permissions**:
- Email addresses (Read)
4. **Generate Private Key**
4. **Where can this GitHub App be installed?**
- Select "Any account" to be able to install the GitHub App in any organization.
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
+11
View File
@@ -0,0 +1,11 @@
# IaC Authentication in Prowler
Prowler's Infrastructure as Code (IaC) provider enables you to scan local or remote infrastructure code for security and compliance issues using [Trivy](https://trivy.dev/). This provider supports a wide range of IaC frameworks and requires no cloud authentication for local scans.
### Authentication
- For local scans, no authentication is required.
- For remote repository scans, authentication can be provided via:
- [**GitHub Username and Personal Access Token (PAT)**](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-personal-access-token-classic)
- [**GitHub OAuth App Token**](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-personal-access-token)
- [**Git URL**](https://git-scm.com/docs/git-clone#_git_urls)
+12 -22
View File
@@ -1,32 +1,22 @@
# Getting Started with the IaC Provider
Prowler's Infrastructure as Code (IaC) provider enables you to scan local or remote infrastructure code for security and compliance issues using [Checkov](https://www.checkov.io/). This provider supports a wide range of IaC frameworks, allowing you to assess your code before deployment.
Prowler's Infrastructure as Code (IaC) provider enables you to scan local or remote infrastructure code for security and compliance issues using [Trivy](https://trivy.dev/). This provider supports a wide range of IaC frameworks, allowing you to assess your code before deployment.
## Supported Frameworks
## Supported Scanners
The IaC provider leverages Checkov to support multiple frameworks, including:
The IaC provider leverages Trivy to support multiple scanners, including:
- Terraform
- CloudFormation
- Kubernetes
- ARM (Azure Resource Manager)
- Serverless
- Dockerfile
- YAML/JSON (generic IaC)
- Bicep
- Helm
- GitHub Actions, GitLab CI, Bitbucket Pipelines, Azure Pipelines, CircleCI, Argo Workflows
- Ansible
- Kustomize
- OpenAPI
- SAST, SCA (Software Composition Analysis)
- Vulnerability
- Misconfiguration
- Secret
- License
## How It Works
- The IaC provider scans your local directory (or a specified path) for supported IaC files, or scan a remote repository.
- No cloud credentials or authentication are required for local scans.
- For remote repository scans, authentication can be provided via [git URL](https://git-scm.com/docs/git-clone#_git_urls), CLI flags or environment variables.
- Mutelist logic is handled by Checkov, not Prowler.
- Mutelist logic is handled by Trivy, not Prowler.
- Results are output in the same formats as other Prowler providers (CSV, JSON, HTML, etc.).
## Usage
@@ -67,12 +57,12 @@ You can provide authentication for private repositories using one of the followi
#### Mutually Exclusive Flags
- `--scan-path` and `--scan-repository-url` are mutually exclusive. Only one can be specified at a time.
### Specify Frameworks
### Specify Scanners
Scan only Terraform and Kubernetes files:
Scan only vulnerability and misconfiguration scanners:
```sh
prowler iac --scan-path ./my-iac-directory --frameworks terraform kubernetes
prowler iac --scan-path ./my-iac-directory --scanners vuln misconfig
```
### Exclude Paths
@@ -95,4 +85,4 @@ prowler iac --scan-path ./iac --output-formats csv json html
- For remote repository scans, authentication is optional but required for private repos.
- CLI flags override environment variables for authentication.
- It is ideal for CI/CD pipelines and local development environments.
- For more details on supported frameworks and rules, see the [Checkov documentation](https://www.checkov.io/1.Welcome/Quick%20Start.html).
- For more details on supported scanners, see the [Trivy documentation](https://trivy.dev/latest/docs/scanner/vulnerability/).
Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

+351 -19
View File
@@ -1,29 +1,361 @@
# Microsoft 365 Authentication for Prowler
By default, Prowler utilizes the MsGraph Python SDK identity package for authentication, leveraging the class `ClientSecretCredential`. This enables authentication against Microsoft 365 using the following approaches:
Prowler for Microsoft 365 (M365) supports the following authentication methods:
- Service principal authentication by environment variables (Enterprise Application)
- Service principal and Microsoft user credentials by environment variabled (using PowerShell requires this authentication method)
- Current CLI credentials stored
- Interactive browser authentication
- [**Service Principal Application**](https://learn.microsoft.com/en-us/entra/identity-platform/app-objects-and-service-principals?tabs=browser#service-principal-object) (**Recommended**)
- **Service Principal Application with Microsoft User Credentials**
- **Stored AZ CLI credentials**
- **Interactive browser authentication**
???+ warning
Prowler App supports the **Service Principal** authentication method and the **Service Principal with User Credentials** authentication method, but this last one will be deprecated in September once Microsoft will enforce MFA in all tenants not allowing User authentication without interactive method.
To launch the tool first you need to specify which method is used through the following flags:
### Service Principal Authentication (Recommended)
**Authentication flag:** `--sp-env-auth`
Enable Prowler authentication as the **Service Principal Application** by configuring the following environment variables:
```console
# To use service principal (app) authentication and Microsoft user credentials
prowler m365 --env-auth
# To use service principal authentication
prowler m365 --sp-env-auth
# To use cli authentication
prowler m365 --az-cli-auth
# To use browser authentication
prowler m365 --browser-auth --tenant-id "XXXXXXXX"
export AZURE_CLIENT_ID="XXXXXXXXX"
export AZURE_CLIENT_SECRET="XXXXXXXXX"
export AZURE_TENANT_ID="XXXXXXXXX"
```
## Permission Configuration
If these variables are not set or exported, execution using `--sp-env-auth` will fail.
To ensure Prowler can access the required resources within your Microsoft 365 account, proper permissions must be configured. Refer to the [Requirements](../../getting-started/requirements.md#needed-permissions_2) section for details on setting up necessary privileges.
Refer to the [Create Prowler Service Principal](getting-started-m365.md#create-the-service-principal-app) guide for setup instructions.
If the external API permissions described in the mentioned section above are not added only checks that work through MS Graph will be executed. This means that the full provider will not be executed.
???+ note
In order to scan all the checks from M365 required permissions to the service principal application must be added. Refer to the [External API Permissions Assignment](getting-started-m365.md#grant-powershell-modules-permissions) section for more information.
### Service Principal and User Credentials Authentication
Authentication flag: `--env-auth`
???+ warning
This method is not recommended anymore, we recommend just use the **Service Principal Application** authentication method instead.
This method builds upon the Service Principal authentication by adding User Credentials. Configure the following environment variables: `M365_USER` and `M365_PASSWORD`.
```console
export AZURE_CLIENT_ID="XXXXXXXXX"
export AZURE_CLIENT_SECRET="XXXXXXXXX"
export AZURE_TENANT_ID="XXXXXXXXX"
export M365_USER="your_email@example.com"
export M365_PASSWORD="examplepassword"
```
These two new environment variables are **required** in this authentication method to execute the PowerShell modules needed to retrieve information from M365 services. Prowler uses Service Principal authentication to access Microsoft Graph and user credentials to authenticate to Microsoft PowerShell modules.
- `M365_USER` should be your Microsoft account email using the **assigned domain in the tenant**. This means it must look like `example@YourCompany.onmicrosoft.com` or `example@YourCompany.com`, but it must be the exact domain assigned to that user in the tenant.
???+ warning
Newly created users must sign in with the account first, as Microsoft prompts for password change. Without completing this step, user authentication fails because Microsoft marks the initial password as expired.
???+ warning
The user must not be MFA capable. Microsoft does not allow MFA capable users to authenticate programmatically. See [Microsoft documentation](https://learn.microsoft.com/en-us/entra/identity-platform/scenario-desktop-acquire-token-username-password?tabs=dotnet) for more information.
???+ warning
Using a tenant domain other than the one assigned — even if it belongs to the same tenant — will cause Prowler to fail, as Microsoft authentication will not succeed.
Ensure the correct domain is used for the authenticating user.
![User Domains](img/user-domains.png)
- `M365_PASSWORD` must be the user password.
???+ note
Previously an encrypted password was required, but now the user password is accepted directly. Prowler handles the password encryption.
### Interactive Browser Authentication
**Authentication flag:** `--browser-auth`
This authentication method requires authentication against Azure using the default browser to start the scan. The `--tenant-id` flag is also required.
These credentials only enable checks that rely on Microsoft Graph. The entire provider cannot be run with this method. To perform a full M365 security scan, use the **recommended authentication method**.
Since this is a **delegated permission** authentication method, necessary permissions should be assigned to the user rather than the application.
### Required Permissions
To run the full Prowler provider, including PowerShell checks, two types of permission scopes must be set in **Microsoft Entra ID**.
#### Service Principal Authentication (`--sp-env-auth`) - Recommended
When using service principal authentication, add the following **Application Permissions**:
**Microsoft Graph API Permissions:**
- `AuditLog.Read.All`: Required for Entra service.
- `Directory.Read.All`: Required for all services.
- `Policy.Read.All`: Required for all services.
- `SharePointTenantSettings.Read.All`: Required for SharePoint service.
- `User.Read` (IMPORTANT: this must be set as **delegated**): Required for the sign-in.
**External API Permissions:**
- `Exchange.ManageAsApp` from external API `Office 365 Exchange Online`: Required for Exchange PowerShell module app authentication. You also need to assign the `Global Reader` role to the app.
- `application_access` from external API `Skype and Teams Tenant Admin API`: Required for Teams PowerShell module app authentication.
???+ note
`Directory.Read.All` can be replaced with `Domain.Read.All` that is a more restrictive permission but you won't be able to run the Entra checks related with DirectoryRoles and GetUsers.
> If you do this you will need to add also the `Organization.Read.All` permission to the service principal application in order to authenticate.
???+ note
This is the **recommended authentication method** because it allows you to run the full M365 provider including PowerShell checks, providing complete coverage of all available security checks, same as the Service Principal Authentication + User Credentials Authentication but this last one will be deprecated in September once Microsoft will enforce MFA in all tenants not allowing User authentication without interactive method.
#### Service Principal + User Credentials Authentication (`--env-auth`)
When using service principal with user credentials authentication, you need **both** sets of permissions:
**1. Service Principal Application Permissions**:
- You **will need** all the Microsoft Graph API permissions listed above.
- You **won't need** the External API permissions listed above.
**2. User-Level Permissions**: These are set at the `M365_USER` level, so the user used to run Prowler must have one of the following roles:
- `Global Reader` (recommended): this allows you to read all roles needed.
- `Exchange Administrator` and `Teams Administrator`: user needs both roles but with this [roles](https://learn.microsoft.com/en-us/exchange/permissions-exo/permissions-exo#microsoft-365-permissions-in-exchange-online) you can access to the same information as a Global Reader (since only read access is needed, Global Reader is recommended).
#### Browser Authentication (`--browser-auth`)
When using browser authentication, permissions are delegated to the user, so the user must have the appropriate permissions rather than the application.
???+ warning
With browser authentication, you will only be able to run checks that work through MS Graph API. PowerShell module checks will not be executed.
### Assigning Permissions and Roles
For guidance on assigning the necessary permissions and roles, follow these instructions:
- [Grant API Permissions](getting-started-m365.md#grant-required-graph-api-permissions)
- [Assign Required Roles](getting-started-m365.md#if-using-user-authentication)
### Supported PowerShell Versions
PowerShell is required to run certain M365 checks.
**Supported versions:**
- **PowerShell 7.4 or higher** (7.5 is recommended)
#### Why Is PowerShell 7.4+ Required?
- **PowerShell 5.1** (default on some Windows systems) does not support required cmdlets.
- Older [cross-platform PowerShell versions](https://learn.microsoft.com/en-us/powershell/scripting/install/powershell-support-lifecycle?view=powershell-7.5) are **unsupported**, leading to potential errors.
???+ note
Installing PowerShell is only necessary if you install Prowler via **pip or other sources**. **SDK and API containers include PowerShell by default.**
### Installing PowerShell
Installing PowerShell is different depending on your OS.
- [Windows](https://learn.microsoft.com/es-es/powershell/scripting/install/installing-powershell-on-windows?view=powershell-7.5#install-powershell-using-winget-recommended): you will need to update PowerShell to +7.4 to be able to run prowler, if not some checks will not show findings and the provider could not work as expected. This version of PowerShell is [supported](https://learn.microsoft.com/es-es/powershell/scripting/install/installing-powershell-on-windows?view=powershell-7.4#supported-versions-of-windows) on Windows 10, Windows 11, Windows Server 2016 and higher versions.
```console
winget install --id Microsoft.PowerShell --source winget
```
- [MacOS](https://learn.microsoft.com/es-es/powershell/scripting/install/installing-powershell-on-macos?view=powershell-7.5#install-the-latest-stable-release-of-powershell): installing PowerShell on MacOS needs to have installed [brew](https://brew.sh/), once you have it is just running the command above, Pwsh is only supported in macOS 15 (Sequoia) x64 and Arm64, macOS 14 (Sonoma) x64 and Arm64, macOS 13 (Ventura) x64 and Arm64
```console
brew install powershell/tap/powershell
```
Once it's installed run `pwsh` on your terminal to verify it's working.
- Linux: installing PowerShell on Linux depends on the distro you are using:
- [Ubuntu](https://learn.microsoft.com/es-es/powershell/scripting/install/install-ubuntu?view=powershell-7.5#installation-via-package-repository-the-package-repository): The required version for installing PowerShell +7.4 on Ubuntu are Ubuntu 22.04 and Ubuntu 24.04. The recommended way to install it is downloading the package available on PMC. You just need to follow the following steps:
```console
###################################
# Prerequisites
# Update the list of packages
sudo apt-get update
# Install pre-requisite packages.
sudo apt-get install -y wget apt-transport-https software-properties-common
# Get the version of Ubuntu
source /etc/os-release
# Download the Microsoft repository keys
wget -q https://packages.microsoft.com/config/ubuntu/$VERSION_ID/packages-microsoft-prod.deb
# Register the Microsoft repository keys
sudo dpkg -i packages-microsoft-prod.deb
# Delete the Microsoft repository keys file
rm packages-microsoft-prod.deb
# Update the list of packages after we added packages.microsoft.com
sudo apt-get update
###################################
# Install PowerShell
sudo apt-get install -y powershell
# Start PowerShell
pwsh
```
- [Alpine](https://learn.microsoft.com/es-es/powershell/scripting/install/install-alpine?view=powershell-7.5#installation-steps): The only supported version for installing PowerShell +7.4 on Alpine is Alpine 3.20. The unique way to install it is downloading the tar.gz package available on [PowerShell github](https://github.com/PowerShell/PowerShell/releases/download/v7.5.0/powershell-7.5.0-linux-musl-x64.tar.gz). You just need to follow the following steps:
```console
# Install the requirements
sudo apk add --no-cache \
ca-certificates \
less \
ncurses-terminfo-base \
krb5-libs \
libgcc \
libintl \
libssl3 \
libstdc++ \
tzdata \
userspace-rcu \
zlib \
icu-libs \
curl
apk -X https://dl-cdn.alpinelinux.org/alpine/edge/main add --no-cache \
lttng-ust \
openssh-client \
# Download the powershell '.tar.gz' archive
curl -L https://github.com/PowerShell/PowerShell/releases/download/v7.5.0/powershell-7.5.0-linux-musl-x64.tar.gz -o /tmp/powershell.tar.gz
# Create the target folder where powershell will be placed
sudo mkdir -p /opt/microsoft/powershell/7
# Expand powershell to the target folder
sudo tar zxf /tmp/powershell.tar.gz -C /opt/microsoft/powershell/7
# Set execute permissions
sudo chmod +x /opt/microsoft/powershell/7/pwsh
# Create the symbolic link that points to pwsh
sudo ln -s /opt/microsoft/powershell/7/pwsh /usr/bin/pwsh
# Start PowerShell
pwsh
```
- [Debian](https://learn.microsoft.com/es-es/powershell/scripting/install/install-debian?view=powershell-7.5#installation-on-debian-11-or-12-via-the-package-repository): The required version for installing PowerShell +7.4 on Debian are Debian 11 and Debian 12. The recommended way to install it is downloading the package available on PMC. You just need to follow the following steps:
```console
###################################
# Prerequisites
# Update the list of packages
sudo apt-get update
# Install pre-requisite packages.
sudo apt-get install -y wget
# Get the version of Debian
source /etc/os-release
# Download the Microsoft repository GPG keys
wget -q https://packages.microsoft.com/config/debian/$VERSION_ID/packages-microsoft-prod.deb
# Register the Microsoft repository GPG keys
sudo dpkg -i packages-microsoft-prod.deb
# Delete the Microsoft repository GPG keys file
rm packages-microsoft-prod.deb
# Update the list of packages after we added packages.microsoft.com
sudo apt-get update
###################################
# Install PowerShell
sudo apt-get install -y powershell
# Start PowerShell
pwsh
```
- [Rhel](https://learn.microsoft.com/es-es/powershell/scripting/install/install-rhel?view=powershell-7.5#installation-via-the-package-repository): The required version for installing PowerShell +7.4 on Red Hat are RHEL 8 and RHEL 9. The recommended way to install it is downloading the package available on PMC. You just need to follow the following steps:
```console
###################################
# Prerequisites
# Get version of RHEL
source /etc/os-release
if [ ${VERSION_ID%.*} -lt 8 ]
then majorver=7
elif [ ${VERSION_ID%.*} -lt 9 ]
then majorver=8
else majorver=9
fi
# Download the Microsoft RedHat repository package
curl -sSL -O https://packages.microsoft.com/config/rhel/$majorver/packages-microsoft-prod.rpm
# Register the Microsoft RedHat repository
sudo rpm -i packages-microsoft-prod.rpm
# Delete the downloaded package after installing
rm packages-microsoft-prod.rpm
# Update package index files
sudo dnf update
# Install PowerShell
sudo dnf install powershell -y
```
- [Docker](https://learn.microsoft.com/es-es/powershell/scripting/install/powershell-in-docker?view=powershell-7.5#use-powershell-in-a-container): The following command download the latest stable versions of PowerShell:
```console
docker pull mcr.microsoft.com/dotnet/sdk:9.0
```
To start an interactive shell of Pwsh you just need to run:
```console
docker run -it mcr.microsoft.com/dotnet/sdk:9.0 pwsh
```
### Required PowerShell Modules
Prowler relies on several PowerShell cmdlets to retrieve necessary data.
These cmdlets come from different modules that must be installed.
#### Automatic Installation
The required modules are automatically installed when running Prowler with the `--init-modules` flag.
Example command:
```console
python3 prowler-cli.py m365 --verbose --log-level ERROR --env-auth --init-modules
```
If the modules are already installed, running this command will not cause issues—it will simply verify that the necessary modules are available.
???+ note
Prowler installs the modules using `-Scope CurrentUser`.
If you encounter any issues with services not working after the automatic installation, try installing the modules manually using `-Scope AllUsers` (administrator permissions are required for this).
The command needed to install a module manually is:
```powershell
Install-Module -Name "ModuleName" -Scope AllUsers -Force
```
#### Modules Version
- [ExchangeOnlineManagement](https://www.powershellgallery.com/packages/ExchangeOnlineManagement/3.6.0) (Minimum version: 3.6.0) Required for checks across Exchange, Defender, and Purview.
- [MicrosoftTeams](https://www.powershellgallery.com/packages/MicrosoftTeams/6.6.0) (Minimum version: 6.6.0) Required for all Teams checks.
- [MSAL.PS](https://www.powershellgallery.com/packages/MSAL.PS/4.32.0): Required for Exchange module via application authentication.
- [MSAL.PS](https://www.powershellgallery.com/packages/MSAL.PS/4.32.0): Required for Exchange module via application authentication.
@@ -257,7 +257,8 @@ This method is not recommended because it requires a user with MFA enabled and M
- `AZURE_CLIENT_SECRET` from earlier
If you are using user authentication, also add:
- `M365_USER` the user using the correct assigned domain, more info [here](../../getting-started/requirements.md#service-principal-and-user-credentials-authentication)
- `M365_USER` the user using the correct assigned domain, more info [here](../../tutorials/microsoft365/authentication.md#service-principal-and-user-credentials-authentication)
- `M365_PASSWORD` the password of the user
![Prowler Cloud M365 Credentials](./img/m365-credentials.png)
@@ -4,9 +4,9 @@ PowerShell is required by this provider because it is the only way to retrieve d
If you are using Prowler Cloud, you don't need to worry about PowerShell — it is already installed in our infrastructure.
However, if you want to run Prowler on your own, you must have PowerShell installed to execute the full M365 provider and retrieve all findings.
To learn more about how to install PowerShell and which versions are supported, click [here](../../getting-started/requirements.md#supported-powershell-versions).
To learn more about how to install PowerShell and which versions are supported, click [here](../../tutorials/microsoft365/authentication.md#supported-powershell-versions).
## Required Modules
The necessary modules will not be installed automatically by Prowler. Nevertheless, if you want Prowler to install them for you, you can execute the provider with the flag `--init-modules`, which will run the script to install and import them.
If you want to learn more about this process or you are running some issues with this, click [here](../../getting-started/requirements.md#needed-powershell-modules).
If you want to learn more about this process or you are running some issues with this, click [here](../../tutorials/microsoft365/authentication.md#required-powershell-modules).
+1 -1
View File
@@ -116,7 +116,7 @@ Each check must reside in a dedicated subfolder, following this structure:
???+ note
The check name must start with the service name followed by an underscore (e.g., ec2\_instance\_public\_ip).
To see more information about how to write checks, refer to the [Developer Guide](../developer-guide/checks.md#create-a-new-check-for-a-provider).
To see more information about how to write checks, refer to the [Developer Guide](../developer-guide/checks.md#creating-a-check).
???+ note
If you want to run ONLY your custom check(s), import it with -x (--checks-folder) and then run it with -c (--checks), e.g.: `console prowler aws -x s3://bucket/prowler/providers/aws/services/s3/s3_bucket_policy/ -c s3_bucket_policy`
+2 -2
View File
@@ -4,7 +4,7 @@
## Accessing Prowler App and API Documentation
After [installing](../index.md#prowler-app-installation) **Prowler App**, access it at [http://localhost:3000](http://localhost:3000). To view the auto-generated **Prowler API** documentation, navigate to [http://localhost:8080/api/v1/docs](http://localhost:8080/api/v1/docs). This documentation provides details on available endpoints, parameters, and responses.
After [installing](../installation/prowler-app.md) **Prowler App**, access it at [http://localhost:3000](http://localhost:3000). To view the auto-generated **Prowler API** documentation, navigate to [http://localhost:8080/api/v1/docs](http://localhost:8080/api/v1/docs). This documentation provides details on available endpoints, parameters, and responses.
???+ note
If you are a [Prowler Cloud](https://cloud.prowler.com/sign-in) user, you can access API docs at [https://api.prowler.com/api/v1/docs](https://api.prowler.com/api/v1/docs)
@@ -109,7 +109,7 @@ For AWS, enter your `AWS Account ID` and choose one of the following methods to
### **Step 4.2: Azure Credentials**:
For Azure, Prowler App uses a service principal application to authenticate. For more information about the process of creating and adding permissions to a service principal refer to this [section](../getting-started/requirements.md#azure). When you finish creating and adding the [Entra](./azure/create-prowler-service-principal.md#assigning-the-proper-permissions) and [Subscription](./azure/subscriptions.md#assign-the-appropriate-permissions-to-the-identity-that-is-going-to-be-assumed-by-prowler) scope permissions to the service principal, enter the `Tenant ID`, `Client ID` and `Client Secret` of the service principal application.
For Azure, Prowler App uses a service principal application to authenticate. For more information about the process of creating and adding permissions to a service principal refer to this [section](../tutorials/azure/authentication.md). When you finish creating and adding the [Entra](./azure/create-prowler-service-principal.md#assigning-proper-permissions) and [Subscription](./azure/subscriptions.md) scope permissions to the service principal, enter the `Tenant ID`, `Client ID` and `Client Secret` of the service principal application.
<img src="../../img/azure-credentials.png" alt="Azure Credentials" width="700"/>
+20 -7
View File
@@ -46,8 +46,19 @@ repo_name: prowler-cloud/prowler
nav:
- Getting Started:
- Overview: index.md
- Requirements: getting-started/requirements.md
- Overview:
- What is Prowler?: index.md
- Products:
- Prowler App: products/prowler-app.md
- Prowler CLI: products/prowler-cli.md
- Prowler Cloud 🔗: https://cloud.prowler.com
- Prowler Hub 🔗: https://hub.prowler.com
- Installation:
- Prowler App: installation/prowler-app.md
- Prowler CLI: installation/prowler-cli.md
- Basic Usage:
- Prowler App: basic-usage/prowler-app.md
- Prowler CLI: basic-usage/prowler-cli.md
- Tutorials:
- Prowler App:
- Getting Started: tutorials/prowler-app.md
@@ -57,6 +68,7 @@ nav:
- Mute findings: tutorials/prowler-app-mute-findings.md
- Amazon S3 Integration: tutorials/prowler-app-s3-integration.md
- Lighthouse: tutorials/prowler-app-lighthouse.md
- Bulk Provider Provisioning: tutorials/bulk-provider-provisioning.md
- CLI:
- Miscellaneous: tutorials/misc.md
- Reporting: tutorials/reporting.md
@@ -111,12 +123,13 @@ nav:
- Authentication: tutorials/microsoft365/authentication.md
- Use of PowerShell: tutorials/microsoft365/use-of-powershell.md
- GitHub:
- Authentication: tutorials/github/authentication.md
- Getting Started: tutorials/github/getting-started-github.md
- Authentication: tutorials/github/authentication.md
- IaC:
- Getting Started: tutorials/iac/getting-started-iac.md
- Authentication: tutorials/iac/authentication.md
- Developer Guide:
- General Concepts:
- Concepts:
- Introduction: developer-guide/introduction.md
- Providers: developer-guide/provider.md
- Services: developer-guide/services.md
@@ -125,7 +138,7 @@ nav:
- Integrations: developer-guide/integrations.md
- Compliance: developer-guide/security-compliance-framework.md
- Lighthouse: developer-guide/lighthouse.md
- Provider Specific Details:
- Providers:
- AWS: developer-guide/aws-details.md
- Azure: developer-guide/azure-details.md
- Google Cloud: developer-guide/gcp-details.md
@@ -142,8 +155,8 @@ nav:
- Security: security.md
- Contact Us: contact.md
- Troubleshooting: troubleshooting.md
- About: about.md
- Prowler Cloud: https://prowler.com
- About 🔗: https://prowler.com/about#team
- Release Notes 🔗: https://github.com/prowler-cloud/prowler/releases
# Customization
extra:
Generated
+6 -4
View File
@@ -2060,14 +2060,14 @@ files = [
[[package]]
name = "h2"
version = "4.2.0"
version = "4.3.0"
description = "Pure-Python HTTP/2 protocol implementation"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "h2-4.2.0-py3-none-any.whl", hash = "sha256:479a53ad425bb29af087f3458a61d30780bc818e4ebcf01f0b536ba916462ed0"},
{file = "h2-4.2.0.tar.gz", hash = "sha256:c8a52129695e88b1a0578d8d2cc6842bbd79128ac685463b887ee278126ad01f"},
{file = "h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd"},
{file = "h2-4.3.0.tar.gz", hash = "sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1"},
]
[package.dependencies]
@@ -2352,6 +2352,8 @@ python-versions = "*"
groups = ["dev"]
files = [
{file = "jsonpath-ng-1.7.0.tar.gz", hash = "sha256:f6f5f7fd4e5ff79c785f1573b394043b39849fb2bb47bcead935d12b00beab3c"},
{file = "jsonpath_ng-1.7.0-py2-none-any.whl", hash = "sha256:898c93fc173f0c336784a3fa63d7434297544b7198124a68f9a3ef9597b0ae6e"},
{file = "jsonpath_ng-1.7.0-py3-none-any.whl", hash = "sha256:f3d7f9e848cba1b6da28c55b1c26ff915dc9e0b1ba7e752a53d6da8d5cbd00b6"},
]
[package.dependencies]
@@ -5839,4 +5841,4 @@ type = ["pytest-mypy"]
[metadata]
lock-version = "2.1"
python-versions = ">3.9.1,<3.13"
content-hash = "a0635a7bb99427a5169b126b429d603079fc24c39ce6759a648fdffe74e50d6c"
content-hash = "fdd2cbdb6913d0dd8d05030ee41c0d36d3954e473783d43b730ec163e697ec15"
+11
View File
@@ -9,15 +9,22 @@ All notable changes to the **Prowler SDK** are documented in this file.
- `vm_sufficient_daily_backup_retention_period` check for Azure provider [(#8200)](https://github.com/prowler-cloud/prowler/pull/8200)
- `vm_jit_access_enabled` check for Azure provider [(#8202)](https://github.com/prowler-cloud/prowler/pull/8202)
- Bedrock AgentCore privilege escalation combination for AWS provider [(#8526)](https://github.com/prowler-cloud/prowler/pull/8526)
- Add User Email and APP name/installations information in GitHub provider [(#8501)](https://github.com/prowler-cloud/prowler/pull/8501)
- Remove standalone iam:PassRole from privesc detection and add missing patterns [(#8530)](https://github.com/prowler-cloud/prowler/pull/8530)
- Support session/profile/role/static credentials in Security Hub integration [(#8539)](https://github.com/prowler-cloud/prowler/pull/8539)
- `eks_cluster_deletion_protection_enabled` check for AWS provider [(#8536)](https://github.com/prowler-cloud/prowler/pull/8536)
- ECS privilege escalation patterns (StartTask and RunTask) for AWS provider [(#8541)](https://github.com/prowler-cloud/prowler/pull/8541)
- Resource Explorer enumeration v2 API actions in `cloudtrail_threat_detection_enumeration` check [(#8557)](https://github.com/prowler-cloud/prowler/pull/8557)
### Changed
- Refine kisa isms-p compliance mapping [(#8479)](https://github.com/prowler-cloud/prowler/pull/8479)
- Update AWS Neptune service metadata to new format [(#8494)](https://github.com/prowler-cloud/prowler/pull/8494)
- CheckMetadata Pydantic validators [(#8584)](https://github.com/prowler-cloud/prowler/pull/8584)
- Improve AWS Security Hub region check using multiple threads [(#8365)](https://github.com/prowler-cloud/prowler/pull/8365)
### Fixed
- Resource metadata error in `s3_bucket_shadow_resource_vulnerability` check [(#8572)](https://github.com/prowler-cloud/prowler/pull/8572)
---
@@ -26,6 +33,9 @@ All notable changes to the **Prowler SDK** are documented in this file.
### Fixed
- AWS resource-arn filtering [(#8533)](https://github.com/prowler-cloud/prowler/pull/8533)
- GitHub App authentication for GitHub provider [(#8529)](https://github.com/prowler-cloud/prowler/pull/8529)
- List all accessible organizations in GitHub provider [(#8535)](https://github.com/prowler-cloud/prowler/pull/8535)
- Only evaluate enabled accounts in `entra_users_mfa_capable` check [(#8544)](https://github.com/prowler-cloud/prowler/pull/8544)
- GitHub Personal Access Token authentication fails without `user:email` scope [(#8580)](https://github.com/prowler-cloud/prowler/pull/8580)
---
@@ -62,6 +72,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
- GitHub repository and organization scoping support with `--repository/respositories` and `--organization/organizations` flags [(#8329)](https://github.com/prowler-cloud/prowler/pull/8329)
- GCP provider retry configuration [(#8412)](https://github.com/prowler-cloud/prowler/pull/8412)
- `s3_bucket_shadow_resource_vulnerability` check for AWS provider [(#8398)](https://github.com/prowler-cloud/prowler/pull/8398)
- Use `trivy` as engine for IaC provider [(#8466)](https://github.com/prowler-cloud/prowler/pull/8466)
### Changed
- Handle some AWS errors as warnings instead of errors [(#8347)](https://github.com/prowler-cloud/prowler/pull/8347)
+1 -1
View File
@@ -20,7 +20,7 @@ def load_checks_to_execute(
) -> set:
"""Generate the list of checks to execute based on the cloud provider and the input arguments given"""
try:
# Bypass check loading for IAC provider since it uses Checkov directly
# Bypass check loading for IAC provider since it uses Trivy directly
if provider == "iac":
return set()
+19 -10
View File
@@ -219,11 +219,11 @@ class CheckMetadata(BaseModel):
return service_name
@validator("CheckID", pre=True, always=True)
def valid_check_id(cls, check_id):
def valid_check_id(cls, check_id, values):
if not check_id:
raise ValueError("CheckID must be a non-empty string")
if check_id:
if check_id and values.get("Provider") != "iac":
if "-" in check_id:
raise ValueError(
f"CheckID {check_id} contains a hyphen, which is not allowed"
@@ -746,25 +746,34 @@ class CheckReportM365(Check_Report):
@dataclass
class CheckReportIAC(Check_Report):
"""Contains the IAC Check's finding information using Checkov."""
"""Contains the IAC Check's finding information using Trivy."""
resource_name: str
resource_path: str
resource_line_range: str
def __init__(self, metadata: dict = {}, finding: dict = {}) -> None:
def __init__(
self, metadata: dict = {}, finding: dict = {}, file_path: str = ""
) -> None:
"""
Initialize the IAC Check's finding information from a Checkov failed_check dict.
Initialize the IAC Check's finding information from a Trivy misconfiguration dict.
Args:
metadata (Dict): Optional check metadata (can be None).
failed_check (dict): A single failed_check result from Checkov's JSON output.
finding (dict): A single misconfiguration result from Trivy's JSON output.
"""
super().__init__(metadata, finding)
self.resource_name = getattr(finding, "resource", "")
self.resource_path = getattr(finding, "file_path", "")
self.resource_line_range = getattr(finding, "file_line_range", "")
self.resource = finding
self.resource_name = file_path
self.resource_line_range = (
(
str(finding.get("CauseMetadata", {}).get("StartLine", ""))
+ ":"
+ str(finding.get("CauseMetadata", {}).get("EndLine", ""))
)
if finding.get("CauseMetadata", {}).get("StartLine", "")
else ""
)
@dataclass
+2 -2
View File
@@ -14,7 +14,7 @@ def recover_checks_from_provider(
Returns a list of tuples with the following format (check_name, check_path)
"""
try:
# Bypass check loading for IAC provider since it uses Checkov directly
# Bypass check loading for IAC provider since it uses Trivy directly
if provider == "iac":
return []
@@ -63,7 +63,7 @@ def recover_checks_from_service(service_list: list, provider: str) -> set:
Returns a set of checks from the given services
"""
try:
# Bypass check loading for IAC provider since it uses Checkov directly
# Bypass check loading for IAC provider since it uses Trivy directly
if provider == "iac":
return set()
+9 -7
View File
@@ -19,6 +19,7 @@ from prowler.lib.outputs.compliance.compliance import get_check_compliance
from prowler.lib.outputs.utils import unroll_tags
from prowler.lib.utils.utils import dict_to_lowercase, get_nested_attribute
from prowler.providers.common.provider import Provider
from prowler.providers.github.models import GithubAppIdentityInfo, GithubIdentityInfo
class Finding(BaseModel):
@@ -250,15 +251,16 @@ class Finding(BaseModel):
output_data["resource_name"] = check_output.resource_name
output_data["resource_uid"] = check_output.resource_id
if hasattr(provider.identity, "account_name"):
if isinstance(provider.identity, GithubIdentityInfo):
# GithubIdentityInfo (Personal Access Token, OAuth)
output_data["account_name"] = provider.identity.account_name
output_data["account_uid"] = provider.identity.account_id
elif hasattr(provider.identity, "app_id"):
output_data["account_email"] = provider.identity.account_email
elif isinstance(provider.identity, GithubAppIdentityInfo):
# GithubAppIdentityInfo (GitHub App)
# TODO: Get Github App name
output_data["account_name"] = f"app-{provider.identity.app_id}"
output_data["account_name"] = provider.identity.app_name
output_data["account_uid"] = provider.identity.app_id
output_data["installations"] = provider.identity.installations
output_data["region"] = check_output.owner
@@ -295,9 +297,9 @@ class Finding(BaseModel):
output_data["auth_method"] = provider.auth_method
output_data["account_uid"] = "iac"
output_data["account_name"] = "iac"
output_data["resource_name"] = check_output.resource["resource"]
output_data["resource_uid"] = check_output.resource["resource"]
output_data["region"] = check_output.resource_path
output_data["resource_name"] = check_output.resource_name
output_data["resource_uid"] = check_output.resource_name
output_data["region"] = check_output.resource_line_range
output_data["resource_line_range"] = check_output.resource_line_range
output_data["framework"] = check_output.check_metadata.ServiceName
+63 -14
View File
@@ -41,7 +41,7 @@ class HTML(Output):
<td>{finding_status}</td>
<td>{finding.metadata.Severity.value}</td>
<td>{finding.metadata.ServiceName}</td>
<td>{":".join([finding.resource_metadata['file_path'], "-".join(map(str, finding.resource_metadata['file_line_range']))]) if finding.metadata.Provider == "iac" else finding.region.lower()}</td>
<td>{finding.region.lower()}</td>
<td>{finding.metadata.CheckID.replace("_", "<wbr />_")}</td>
<td>{finding.metadata.CheckTitle}</td>
<td>{finding.resource_uid.replace("<", "&lt;").replace(">", "&gt;").replace("_", "<wbr />_")}</td>
@@ -204,7 +204,7 @@ class HTML(Output):
<th scope="col">Status</th>
<th scope="col">Severity</th>
<th scope="col">Service Name</th>
<th scope="col">{"File" if provider.type == "iac" else "Region"}</th>
<th scope="col">{"Line Range" if provider.type == "iac" else "Region"}</th>
<th style="width:20%" scope="col">Check ID</th>
<th style="width:20%" scope="col">Check Title</th>
<th scope="col">Resource ID</th>
@@ -558,10 +558,65 @@ class HTML(Output):
try:
if hasattr(provider.identity, "account_name"):
# GithubIdentityInfo (Personal Access Token, OAuth)
account_display = provider.identity.account_name
account_info_items = f"""
<li class="list-group-item">
<b>GitHub account:</b> {provider.identity.account_name}
</li>
"""
# Add email if available
if (
hasattr(provider.identity, "account_email")
and provider.identity.account_email
):
account_info_items += f"""
<li class="list-group-item">
<b>GitHub account email:</b> {provider.identity.account_email}
</li>"""
elif hasattr(provider.identity, "app_id"):
# GithubAppIdentityInfo (GitHub App)
account_display = f"app-{provider.identity.app_id}"
# Assessment items: App Name and Installations
account_info_items = f"""
<li class="list-group-item">
<b>GitHub App Name:</b> {provider.identity.app_name}
</li>"""
# Add installations if available
if (
hasattr(provider.identity, "installations")
and provider.identity.installations
):
installations_display = ", ".join(provider.identity.installations)
account_info_items += f"""
<li class="list-group-item">
<b>Installations:</b> {installations_display}
</li>"""
else:
account_info_items += """
<li class="list-group-item">
<b>Installations:</b> No installations found
</li>"""
# Credentials items: Authentication method and App ID
credentials_items = f"""
<li class="list-group-item">
<b>GitHub authentication method:</b> {provider.auth_method}
</li>
<li class="list-group-item">
<b>GitHub App ID:</b> {provider.identity.app_id}
</li>"""
else:
# Fallback for other identity types
account_info_items = ""
credentials_items = f"""
<li class="list-group-item">
<b>GitHub authentication method:</b> {provider.auth_method}
</li>"""
# For PAT/OAuth, use default credentials structure
if hasattr(provider.identity, "account_name"):
credentials_items = f"""
<li class="list-group-item">
<b>GitHub authentication method:</b> {provider.auth_method}
</li>"""
return f"""
<div class="col-md-2">
@@ -569,11 +624,8 @@ class HTML(Output):
<div class="card-header">
GitHub Assessment Summary
</div>
<ul class="list-group
list-group-flush">
<li class="list-group-item">
<b>GitHub account:</b> {account_display}
</li>
<ul class="list-group list-group-flush">
{account_info_items}
</ul>
</div>
</div>
@@ -582,11 +634,8 @@ class HTML(Output):
<div class="card-header">
GitHub Credentials
</div>
<ul class="list-group
list-group-flush">
<li class="list-group-item">
<b>GitHub authentication method:</b> {provider.auth_method}
</li>
<ul class="list-group list-group-flush">
{credentials_items}
</ul>
</div>
</div>"""
+2
View File
@@ -86,6 +86,8 @@ def display_summary_table(
"Muted": [],
}
pass_count = fail_count = muted_count = 0
# Sort findings by ServiceName
findings.sort(key=lambda x: x.check_metadata.ServiceName)
for finding in findings:
# If new service and not first, add previous row
if (
@@ -1362,6 +1362,7 @@
"ap-south-2",
"ap-southeast-1",
"ap-southeast-2",
"ap-southeast-3",
"ap-southeast-4",
"ca-central-1",
"eu-central-1",
@@ -1395,6 +1396,7 @@
"ap-south-2",
"ap-southeast-1",
"ap-southeast-2",
"ap-southeast-3",
"ap-southeast-4",
"ca-central-1",
"eu-central-1",
@@ -9666,7 +9668,10 @@
"us-west-2"
],
"aws-cn": [],
"aws-us-gov": []
"aws-us-gov": [
"us-gov-east-1",
"us-gov-west-1"
]
}
},
"s3": {
@@ -1,18 +1,47 @@
import os
from concurrent.futures import ThreadPoolExecutor, as_completed
from dataclasses import dataclass
from typing import Optional
from boto3 import Session
from botocore.client import ClientError
from botocore.exceptions import NoCredentialsError, ProfileNotFound
from prowler.config.config import timestamp_utc
from prowler.lib.logger import logger
from prowler.lib.outputs.asff.asff import AWSSecurityFindingFormat
from prowler.providers.aws.aws_provider import AwsProvider
from prowler.providers.aws.config import (
AWS_STS_GLOBAL_ENDPOINT_REGION,
ROLE_SESSION_NAME,
)
from prowler.providers.aws.exceptions.exceptions import (
AWSAccessKeyIDInvalidError,
AWSArgumentTypeValidationError,
AWSAssumeRoleError,
AWSIAMRoleARNEmptyResourceError,
AWSIAMRoleARNInvalidAccountIDError,
AWSIAMRoleARNInvalidResourceTypeError,
AWSIAMRoleARNPartitionEmptyError,
AWSIAMRoleARNRegionNotEmtpyError,
AWSIAMRoleARNServiceNotIAMnorSTSError,
AWSNoCredentialsError,
AWSProfileNotFoundError,
AWSSecretAccessKeyInvalidError,
AWSSessionTokenExpiredError,
AWSSetUpSessionError,
)
from prowler.providers.aws.lib.arguments.arguments import (
validate_role_session_name,
validate_session_duration,
)
from prowler.providers.aws.lib.arn.arn import parse_iam_credentials_arn
from prowler.providers.aws.lib.security_hub.exceptions.exceptions import (
SecurityHubInvalidRegionError,
SecurityHubNoEnabledRegionsError,
)
from prowler.providers.aws.lib.session.aws_set_up_session import AwsSetUpSession
from prowler.providers.aws.models import AWSAssumeRoleInfo
from prowler.providers.common.models import Connection
SECURITY_HUB_INTEGRATION_NAME = "prowler/prowler"
@@ -26,10 +55,12 @@ class SecurityHubConnection(Connection):
Attributes:
enabled_regions (set): Set of regions where Security Hub is enabled.
disabled_regions (set): Set of regions where Security Hub is disabled.
partition (str): AWS partition (e.g., aws, aws-cn, aws-us-gov) where SecurityHub is deployed.
"""
enabled_regions: set = None
disabled_regions: set = None
partition: str = ""
class SecurityHub:
@@ -61,15 +92,15 @@ class SecurityHub:
def __init__(
self,
aws_account_id: str,
aws_partition: str,
aws_partition: str = None,
aws_session: Session = None,
findings: list[AWSSecurityFindingFormat] = [],
aws_security_hub_available_regions: list[str] = [],
send_only_fails: bool = False,
role_arn: str = None,
session_duration: int = None,
session_duration: int = 3600,
external_id: str = None,
role_session_name: str = None,
role_session_name: str = ROLE_SESSION_NAME,
mfa: bool = None,
profile: str = None,
aws_access_key_id: str = None,
@@ -87,7 +118,7 @@ class SecurityHub:
- aws_partition (str): AWS partition (e.g., aws, aws-cn, aws-us-gov) where SecurityHub is deployed.
- findings (list[AWSSecurityFindingFormat]): List of findings to filter and send to Security Hub.
- aws_security_hub_available_regions (list[str]): List of regions where Security Hub is available.
- send_only_fails (bool): Flag indicating whether to send only findings with status 'FAILED'.
- send_only_fails (bool): Flag indicating whether to send only findings with status 'FAIL'.
- role_arn: The ARN of the IAM role to assume.
- session_duration: The duration of the session in seconds, between 900 and 43200.
- external_id: The external ID to use when assuming the IAM role.
@@ -116,8 +147,12 @@ class SecurityHub:
retries_max_attempts=retries_max_attempts,
regions=regions,
)
self._session = aws_setup_session._session
self._session = aws_setup_session._session.current_session
self._aws_account_id = aws_account_id
if not aws_partition:
aws_partition = AwsProvider.validate_credentials(
self._session, AWS_STS_GLOBAL_ENDPOINT_REGION
).arn.partition
self._aws_partition = aws_partition
self._enabled_regions = None
@@ -126,7 +161,7 @@ class SecurityHub:
if aws_security_hub_available_regions:
self._enabled_regions = self.verify_enabled_per_region(
aws_security_hub_available_regions,
aws_session,
self._session,
aws_account_id,
aws_partition,
)
@@ -143,7 +178,7 @@ class SecurityHub:
Args:
findings (list[AWSSecurityFindingFormat]): List of findings to filter.
send_only_fails (bool): Flag indicating whether to send only findings with status 'FAILED'.
send_only_fails (bool): Flag indicating whether to send only findings with status 'FAIL'.
Returns:
dict: A dictionary containing findings per region after applying the filtering criteria.
@@ -178,6 +213,69 @@ class SecurityHub:
)
return findings_per_region
@staticmethod
def _check_region_security_hub(
region: str,
session: Session,
aws_account_id: str,
aws_partition: str,
) -> tuple[str, Session | None]:
"""
Check if Security Hub is enabled in a specific region and if Prowler integration is active.
Args:
region (str): AWS region to check.
session (Session): AWS session object.
aws_account_id (str): AWS account ID.
aws_partition (str): AWS partition.
Returns:
tuple: (region, client or None) - Returns client if enabled, None otherwise.
"""
try:
logger.info(
f"Checking if the {SECURITY_HUB_INTEGRATION_NAME} is enabled in the {region} region."
)
# Check if security hub is enabled in current region
security_hub_client = session.client("securityhub", region_name=region)
security_hub_client.describe_hub()
# Check if Prowler integration is enabled in Security Hub
security_hub_prowler_integration_arn = f"arn:{aws_partition}:securityhub:{region}:{aws_account_id}:product-subscription/{SECURITY_HUB_INTEGRATION_NAME}"
if security_hub_prowler_integration_arn not in str(
security_hub_client.list_enabled_products_for_import()
):
logger.warning(
f"Security Hub is enabled in {region} but Prowler integration does not accept findings. More info: https://docs.prowler.cloud/en/latest/tutorials/aws/securityhub/"
)
return region, None
else:
return region, session.client("securityhub", region_name=region)
# Handle all the permissions / configuration errors
except ClientError as client_error:
# Check if Account is subscribed to Security Hub
error_code = client_error.response["Error"]["Code"]
error_message = client_error.response["Error"]["Message"]
if (
error_code == "InvalidAccessException"
and f"Account {aws_account_id} is not subscribed to AWS Security Hub"
in error_message
):
logger.warning(
f"{client_error.__class__.__name__} -- [{client_error.__traceback__.tb_lineno}]: {client_error}"
)
else:
logger.error(
f"{client_error.__class__.__name__} -- [{client_error.__traceback__.tb_lineno}]: {client_error}"
)
return region, None
except Exception as error:
logger.error(
f"{error.__class__.__name__} -- [{error.__traceback__.tb_lineno}]: {error}"
)
return region, None
@staticmethod
def verify_enabled_per_region(
aws_security_hub_available_regions: list[str],
@@ -195,49 +293,34 @@ class SecurityHub:
dict: A dictionary containing enabled regions with SecurityHub clients.
"""
enabled_regions = {}
for region in aws_security_hub_available_regions:
try:
logger.info(
f"Checking if the {SECURITY_HUB_INTEGRATION_NAME} is enabled in the {region} region."
)
# Check if security hub is enabled in current region
security_hub_client = session.client("securityhub", region_name=region)
security_hub_client.describe_hub()
# Check if Prowler integration is enabled in Security Hub
security_hub_prowler_integration_arn = f"arn:{aws_partition}:securityhub:{region}:{aws_account_id}:product-subscription/{SECURITY_HUB_INTEGRATION_NAME}"
if security_hub_prowler_integration_arn not in str(
security_hub_client.list_enabled_products_for_import()
):
logger.warning(
f"Security Hub is enabled in {region} but Prowler integration does not accept findings. More info: https://docs.prowler.cloud/en/latest/tutorials/aws/securityhub/"
)
else:
enabled_regions[region] = session.client(
"securityhub", region_name=region
)
# Use ThreadPoolExecutor to check regions in parallel
with ThreadPoolExecutor(
max_workers=min(len(aws_security_hub_available_regions), 20)
) as executor:
# Submit all region checks
future_to_region = {
executor.submit(
SecurityHub._check_region_security_hub,
region,
session,
aws_account_id,
aws_partition,
): region
for region in aws_security_hub_available_regions
}
# Handle all the permissions / configuration errors
except ClientError as client_error:
# Check if Account is subscribed to Security Hub
error_code = client_error.response["Error"]["Code"]
error_message = client_error.response["Error"]["Message"]
if (
error_code == "InvalidAccessException"
and f"Account {aws_account_id} is not subscribed to AWS Security Hub"
in error_message
):
logger.warning(
f"{client_error.__class__.__name__} -- [{client_error.__traceback__.tb_lineno}]: {client_error}"
)
else:
# Collect results as they complete
for future in as_completed(future_to_region):
try:
region, client = future.result()
if client is not None:
enabled_regions[region] = client
except Exception as error:
logger.error(
f"{client_error.__class__.__name__} -- [{client_error.__traceback__.tb_lineno}]: {client_error}"
f"Error checking region {future_to_region[future]}: {error.__class__.__name__} -- [{error.__traceback__.tb_lineno}]: {error}"
)
except Exception as error:
logger.error(
f"{error.__class__.__name__} -- [{error.__traceback__.tb_lineno}]: {error}"
)
return enabled_regions
def batch_send_to_security_hub(
@@ -363,34 +446,91 @@ class SecurityHub:
@staticmethod
def test_connection(
session: Session,
aws_account_id: str,
aws_partition: str,
aws_partition: str = None,
regions: set = None,
raise_on_exception: bool = True,
profile: str = None,
aws_region: str = AWS_STS_GLOBAL_ENDPOINT_REGION,
role_arn: str = None,
role_session_name: str = ROLE_SESSION_NAME,
session_duration: int = 3600,
external_id: str = None,
mfa_enabled: bool = False,
aws_access_key_id: str = None,
aws_secret_access_key: str = None,
aws_session_token: Optional[str] = None,
) -> SecurityHubConnection:
"""
Test the connection to AWS Security Hub by checking if Security Hub is enabled in the provided region
and if the Prowler integration is active.
Args:
session (Session): AWS session to use for authentication.
regions (set): Set of regions to check for Security Hub integration.
aws_account_id (str): AWS account ID to check for Prowler integration.
aws_partition (str): AWS partition (e.g., aws, aws-cn, aws-us-gov).
regions (set): Set of regions to check for Security Hub integration.
raise_on_exception (bool): Whether to raise an exception if an error occurs.
profile (str): AWS profile name to use for authentication.
aws_region (str): AWS region to use for the session.
role_arn (str): ARN of the IAM role to assume.
role_session_name (str): Name for the role session.
session_duration (int): Duration of the role session in seconds.
external_id (str): External ID to use when assuming the role.
mfa_enabled (bool): Whether MFA is enabled.
aws_access_key_id (str): AWS access key ID.
aws_secret_access_key (str): AWS secret access key.
aws_session_token (str): AWS session token.
Returns:
Connection: An object that contains the result of the test connection operation.
SecurityHubConnection: An object that contains the result of the test connection operation.
- is_connected (bool): Indicates whether the connection was successful.
- error (Exception): An exception object if an error occurs during the connection test.
enabled_regions (set): Set of regions where Security Hub is enabled.
disabled_regions (set): Set of regions where Security Hub is disabled.
- enabled_regions (set): Set of regions where Security Hub is enabled.
- disabled_regions (set): Set of regions where Security Hub is disabled.
"""
try:
disabled_regions = set()
enabled_regions = set()
# Set up AWS session
session = AwsProvider.setup_session(
mfa=mfa_enabled,
profile=profile,
aws_access_key_id=aws_access_key_id,
aws_secret_access_key=aws_secret_access_key,
aws_session_token=aws_session_token,
)
if not aws_partition:
aws_partition = AwsProvider.validate_credentials(
session, aws_region
).arn.partition
# Handle role assumption if role_arn is provided
if role_arn:
session_duration = validate_session_duration(session_duration)
role_session_name = validate_role_session_name(
role_session_name or ROLE_SESSION_NAME
)
role_arn = parse_iam_credentials_arn(role_arn)
assumed_role_information = AWSAssumeRoleInfo(
role_arn=role_arn,
session_duration=session_duration,
external_id=external_id,
mfa_enabled=mfa_enabled,
role_session_name=role_session_name,
)
assumed_role_credentials = AwsProvider.assume_role(
session,
assumed_role_information,
)
session = Session(
aws_access_key_id=assumed_role_credentials.aws_access_key_id,
aws_secret_access_key=assumed_role_credentials.aws_secret_access_key,
aws_session_token=assumed_role_credentials.aws_session_token,
region_name=aws_region,
profile_name=profile,
)
all_regions = AwsProvider.get_available_aws_service_regions(
service="securityhub", partition=aws_partition
)
@@ -427,6 +567,7 @@ class SecurityHub:
error=None,
enabled_regions=enabled_regions,
disabled_regions=disabled_regions,
partition=aws_partition,
)
if len(enabled_regions) == 0:
@@ -454,10 +595,206 @@ class SecurityHub:
error=None,
enabled_regions=enabled_regions,
disabled_regions=disabled_regions,
partition=aws_partition,
)
except AWSSetUpSessionError as setup_session_error:
logger.error(
f"{setup_session_error.__class__.__name__}[{setup_session_error.__traceback__.tb_lineno}]: {setup_session_error}"
)
if raise_on_exception:
raise setup_session_error
return SecurityHubConnection(
is_connected=False,
error=setup_session_error,
enabled_regions=set(),
disabled_regions=set(),
)
except AWSArgumentTypeValidationError as validation_error:
logger.error(
f"{validation_error.__class__.__name__}[{validation_error.__traceback__.tb_lineno}]: {validation_error}"
)
if raise_on_exception:
raise validation_error
return SecurityHubConnection(
is_connected=False,
error=validation_error,
enabled_regions=set(),
disabled_regions=set(),
)
except AWSIAMRoleARNRegionNotEmtpyError as arn_region_not_empty_error:
logger.error(
f"{arn_region_not_empty_error.__class__.__name__}[{arn_region_not_empty_error.__traceback__.tb_lineno}]: {arn_region_not_empty_error}"
)
if raise_on_exception:
raise arn_region_not_empty_error
return SecurityHubConnection(
is_connected=False,
error=arn_region_not_empty_error,
enabled_regions=set(),
disabled_regions=set(),
)
except AWSIAMRoleARNPartitionEmptyError as arn_partition_empty_error:
logger.error(
f"{arn_partition_empty_error.__class__.__name__}[{arn_partition_empty_error.__traceback__.tb_lineno}]: {arn_partition_empty_error}"
)
if raise_on_exception:
raise arn_partition_empty_error
return SecurityHubConnection(
is_connected=False,
error=arn_partition_empty_error,
enabled_regions=set(),
disabled_regions=set(),
)
except AWSIAMRoleARNServiceNotIAMnorSTSError as arn_service_not_iam_sts_error:
logger.error(
f"{arn_service_not_iam_sts_error.__class__.__name__}[{arn_service_not_iam_sts_error.__traceback__.tb_lineno}]: {arn_service_not_iam_sts_error}"
)
if raise_on_exception:
raise arn_service_not_iam_sts_error
return SecurityHubConnection(
is_connected=False,
error=arn_service_not_iam_sts_error,
enabled_regions=set(),
disabled_regions=set(),
)
except AWSIAMRoleARNInvalidAccountIDError as arn_invalid_account_id_error:
logger.error(
f"{arn_invalid_account_id_error.__class__.__name__}[{arn_invalid_account_id_error.__traceback__.tb_lineno}]: {arn_invalid_account_id_error}"
)
if raise_on_exception:
raise arn_invalid_account_id_error
return SecurityHubConnection(
is_connected=False,
error=arn_invalid_account_id_error,
enabled_regions=set(),
disabled_regions=set(),
)
except AWSIAMRoleARNInvalidResourceTypeError as arn_invalid_resource_type_error:
logger.error(
f"{arn_invalid_resource_type_error.__class__.__name__}[{arn_invalid_resource_type_error.__traceback__.tb_lineno}]: {arn_invalid_resource_type_error}"
)
if raise_on_exception:
raise arn_invalid_resource_type_error
return SecurityHubConnection(
is_connected=False,
error=arn_invalid_resource_type_error,
enabled_regions=set(),
disabled_regions=set(),
)
except AWSIAMRoleARNEmptyResourceError as arn_empty_resource_error:
logger.error(
f"{arn_empty_resource_error.__class__.__name__}[{arn_empty_resource_error.__traceback__.tb_lineno}]: {arn_empty_resource_error}"
)
if raise_on_exception:
raise arn_empty_resource_error
return SecurityHubConnection(
is_connected=False,
error=arn_empty_resource_error,
enabled_regions=set(),
disabled_regions=set(),
)
except AWSAssumeRoleError as assume_role_error:
logger.error(
f"{assume_role_error.__class__.__name__}[{assume_role_error.__traceback__.tb_lineno}]: {assume_role_error}"
)
if raise_on_exception:
raise assume_role_error
return SecurityHubConnection(
is_connected=False,
error=assume_role_error,
enabled_regions=set(),
disabled_regions=set(),
)
except ProfileNotFound as profile_not_found_error:
logger.error(
f"AWSProfileNotFoundError[{profile_not_found_error.__traceback__.tb_lineno}]: {profile_not_found_error}"
)
if raise_on_exception:
raise AWSProfileNotFoundError(
file=os.path.basename(__file__),
original_exception=profile_not_found_error,
) from profile_not_found_error
return SecurityHubConnection(
is_connected=False,
error=profile_not_found_error,
enabled_regions=set(),
disabled_regions=set(),
)
except NoCredentialsError as no_credentials_error:
logger.error(
f"AWSNoCredentialsError[{no_credentials_error.__traceback__.tb_lineno}]: {no_credentials_error}"
)
if raise_on_exception:
raise AWSNoCredentialsError(
file=os.path.basename(__file__),
original_exception=no_credentials_error,
) from no_credentials_error
return SecurityHubConnection(
is_connected=False,
error=no_credentials_error,
enabled_regions=set(),
disabled_regions=set(),
)
except AWSAccessKeyIDInvalidError as access_key_id_invalid_error:
logger.error(
f"{access_key_id_invalid_error.__class__.__name__}[{access_key_id_invalid_error.__traceback__.tb_lineno}]: {access_key_id_invalid_error}"
)
if raise_on_exception:
raise access_key_id_invalid_error
return SecurityHubConnection(
is_connected=False,
error=access_key_id_invalid_error,
enabled_regions=set(),
disabled_regions=set(),
)
except AWSSecretAccessKeyInvalidError as secret_access_key_invalid_error:
logger.error(
f"{secret_access_key_invalid_error.__class__.__name__}[{secret_access_key_invalid_error.__traceback__.tb_lineno}]: {secret_access_key_invalid_error}"
)
if raise_on_exception:
raise secret_access_key_invalid_error
return SecurityHubConnection(
is_connected=False,
error=secret_access_key_invalid_error,
enabled_regions=set(),
disabled_regions=set(),
)
except AWSSessionTokenExpiredError as session_token_expired:
logger.error(
f"{session_token_expired.__class__.__name__}[{session_token_expired.__traceback__.tb_lineno}]: {session_token_expired}"
)
if raise_on_exception:
raise session_token_expired
return SecurityHubConnection(
is_connected=False,
error=session_token_expired,
enabled_regions=set(),
disabled_regions=set(),
)
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
raise error
if raise_on_exception:
raise error
return SecurityHubConnection(
is_connected=False,
error=error,
enabled_regions=set(),
disabled_regions=set(),
)
@@ -6,6 +6,7 @@ from prowler.providers.aws.services.cloudtrail.cloudtrail_client import (
)
default_threat_detection_enumeration_actions = [
"CreateIndex",
"DescribeAccessEntry",
"DescribeAccountAttributes",
"DescribeAvailabilityZones",
@@ -86,6 +87,7 @@ default_threat_detection_enumeration_actions = [
"ListOrganizationalUnitsForParent",
"ListOriginationNumbers",
"ListPolicyVersions",
"ListResources",
"ListRoles",
"ListRoles",
"ListRules",
@@ -106,6 +106,18 @@ privilege_escalation_policies_combination = {
"bedrock-agentcore:CreateCodeInterpreter",
"bedrock-agentcore:InvokeCodeInterpreter",
},
# ECS-based privilege escalation patterns
# Reference: https://labs.reversec.com/posts/2025/08/another-ecs-privilege-escalation-path
"PassRole+ECS+StartTask": {
"iam:PassRole",
"ecs:StartTask",
"ecs:RegisterContainerInstance",
"ecs:DeregisterContainerInstance",
},
"PassRole+ECS+RunTask": {
"iam:PassRole",
"ecs:RunTask",
},
# TO-DO: We have to handle AssumeRole just if the resource is * and without conditions
# "sts:AssumeRole": {"sts:AssumeRole"},
}
@@ -3,7 +3,7 @@
"CheckID": "s3_bucket_shadow_resource_vulnerability",
"CheckTitle": "Check for S3 buckets vulnerable to Shadow Resource Hijacking (Bucket Monopoly)",
"CheckType": [
""
"Effects/Data Exposure"
],
"ServiceName": "s3",
"SubServiceName": "",
@@ -70,22 +70,12 @@ class s3_bucket_shadow_resource_vulnerability(Check):
)
# Check if this bucket exists in another account
if s3_client._head_bucket(bucket_name):
# Create a virtual bucket object for reporting
virtual_bucket = type(
"obj",
(object,),
{
"name": bucket_name,
"region": region,
"arn": f"arn:{s3_client.audited_partition}:s3:::{bucket_name}",
"tags": [],
},
)()
report = Check_Report_AWS(self.metadata(), resource=virtual_bucket)
report = Check_Report_AWS(self.metadata(), resource={})
report.region = region
report.resource_id = bucket_name
report.resource_arn = virtual_bucket.arn
report.resource_arn = (
f"arn:{s3_client.audited_partition}:s3:::{bucket_name}"
)
report.resource_tags = []
report.status = "FAIL"
report.status_extended = f"S3 bucket {bucket_name} for service {service} is a known shadow resource that exists and is owned by another account."
+1 -1
View File
@@ -252,7 +252,7 @@ class Provider(ABC):
provider_class(
scan_path=arguments.scan_path,
scan_repository_url=arguments.scan_repository_url,
frameworks=arguments.frameworks,
scanners=arguments.scanners,
exclude_path=arguments.exclude_path,
config_path=arguments.config_file,
fixer_config=fixer_config,
+26 -5
View File
@@ -350,10 +350,22 @@ class GithubProvider(Provider):
auth = Auth.Token(session.token)
g = Github(auth=auth, retry=retry_config)
try:
user = g.get_user()
# Try to get email if the token has the necessary scope
account_email = None
try:
emails = user.get_emails()
if emails:
account_email = emails[0].email
except Exception:
# Token doesn't have user:email scope or other API error
pass
identity = GithubIdentityInfo(
account_id=g.get_user().id,
account_name=g.get_user().login,
account_url=g.get_user().url,
account_id=user.id,
account_name=user.login,
account_url=user.url,
account_email=account_email,
)
return identity
@@ -371,8 +383,10 @@ class GithubProvider(Provider):
installation.raw_data.get("account", {}).get("login")
)
try:
app = gi.get_app()
identity = GithubAppIdentityInfo(
app_id=gi.get_app().id,
app_id=app.id,
app_name=app.name,
installations=installations,
)
return identity
@@ -401,11 +415,18 @@ class GithubProvider(Provider):
report_lines = [
f"GitHub Account: {Fore.YELLOW}{self.identity.account_name}{Style.RESET_ALL}",
f"GitHub Account ID: {Fore.YELLOW}{self.identity.account_id}{Style.RESET_ALL}",
f"Authentication Method: {Fore.YELLOW}{self.auth_method}{Style.RESET_ALL}",
]
if self.identity.account_email:
report_lines.append(
f"GitHub Account Email: {Fore.YELLOW}{self.identity.account_email}{Style.RESET_ALL}"
)
report_lines.append(
f"Authentication Method: {Fore.YELLOW}{self.auth_method}{Style.RESET_ALL}"
)
elif isinstance(self.identity, GithubAppIdentityInfo):
report_lines = [
f"GitHub App ID: {Fore.YELLOW}{self.identity.app_id}{Style.RESET_ALL}",
f"GitHub App Name: {Fore.YELLOW}{self.identity.app_name}{Style.RESET_ALL}",
f"Authentication Method: {Fore.YELLOW}{self.auth_method}{Style.RESET_ALL}",
]
report_title = (
+4
View File
@@ -1,3 +1,5 @@
from typing import Optional
from pydantic.v1 import BaseModel
from prowler.config.config import output_file_timestamp
@@ -14,10 +16,12 @@ class GithubIdentityInfo(BaseModel):
account_id: str
account_name: str
account_url: str
account_email: Optional[str] = None
class GithubAppIdentityInfo(BaseModel):
app_id: str
app_name: str
installations: list[str]
@@ -5,6 +5,7 @@ from pydantic.v1 import BaseModel
from prowler.lib.logger import logger
from prowler.providers.github.lib.service.service import GithubService
from prowler.providers.github.models import GithubAppIdentityInfo, GithubIdentityInfo
class Organization(GithubService):
@@ -113,8 +114,15 @@ class Organization(GithubService):
elif not self.provider.repositories:
# Default behavior: get all organizations the user is a member of
# Only when no repositories are specified
for org in client.get_user().get_orgs():
self._process_organization(org, organizations)
if isinstance(self.provider.identity, GithubIdentityInfo):
orgs = client.get_user().get_orgs()
for org in orgs:
self._process_organization(org, organizations)
elif isinstance(self.provider.identity, GithubAppIdentityInfo):
orgs = client.get_organizations()
if orgs.totalCount > 0:
for org in orgs:
self._process_organization(org, organizations)
except github.RateLimitExceededException as error:
logger.error(f"GitHub API rate limit exceeded: {error}")
+132 -73
View File
@@ -29,7 +29,7 @@ class IacProvider(Provider):
self,
scan_path: str = ".",
scan_repository_url: str = None,
frameworks: list[str] = ["all"],
scanners: list[str] = ["vuln", "misconfig", "secret"],
exclude_path: list[str] = [],
config_path: str = None,
config_content: dict = None,
@@ -42,7 +42,7 @@ class IacProvider(Provider):
self.scan_path = scan_path
self.scan_repository_url = scan_repository_url
self.frameworks = frameworks
self.scanners = scanners
self.exclude_path = exclude_path
self.region = "global"
self.audited_account = "local-iac"
@@ -90,7 +90,7 @@ class IacProvider(Provider):
# Fixer Config
self._fixer_config = fixer_config
# Mutelist (not needed for IAC since Checkov has its own mutelist logic)
# Mutelist (not needed for IAC since Trivy has its own mutelist logic)
self._mutelist = None
self.audit_metadata = Audit_Metadata(
@@ -131,41 +131,50 @@ class IacProvider(Provider):
return self._fixer_config
def setup_session(self):
"""IAC provider doesn't need a session since it uses Checkov directly"""
"""IAC provider doesn't need a session since it uses Trivy directly"""
return None
def _process_check(self, finding: dict, check: dict, status: str) -> CheckReportIAC:
def _process_finding(
self, finding: dict, file_path: str, type: str
) -> CheckReportIAC:
"""
Process a single check (failed or passed) and create a CheckReportIAC object.
Args:
finding: The finding object from Checkov output
check: The individual check data (failed_check or passed_check)
status: The status of the check ("FAIL" or "PASS")
finding: The finding object from Trivy output
file_path: The path to the file that contains the finding
type: The type of the finding
Returns:
CheckReportIAC: The processed check report
"""
try:
if "VulnerabilityID" in finding:
finding_id = finding["VulnerabilityID"]
finding_description = finding["Description"]
finding_status = finding.get("Status", "FAIL")
elif "RuleID" in finding:
finding_id = finding["RuleID"]
finding_description = finding["Title"]
finding_status = finding.get("Status", "FAIL")
else:
finding_id = finding["ID"]
finding_description = finding["Description"]
finding_status = finding["Status"]
metadata_dict = {
"Provider": "iac",
"CheckID": check.get("check_id", ""),
"CheckTitle": check.get("check_name", ""),
"CheckID": finding_id,
"CheckTitle": finding["Title"],
"CheckType": ["Infrastructure as Code"],
"ServiceName": finding["check_type"],
"ServiceName": type,
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": (
check.get("severity", "low").lower()
if check.get("severity")
else "low"
),
"Severity": finding["Severity"],
"ResourceType": "iac",
"Description": check.get("check_name", ""),
"Description": finding_description,
"Risk": "",
"RelatedUrl": (
check.get("guideline", "") if check.get("guideline") else ""
),
"RelatedUrl": finding.get("PrimaryURL", ""),
"Remediation": {
"Code": {
"NativeIaC": "",
@@ -174,10 +183,8 @@ class IacProvider(Provider):
"Other": "",
},
"Recommendation": {
"Text": "",
"Url": (
check.get("guideline", "") if check.get("guideline") else ""
),
"Text": finding.get("Resolution", ""),
"Url": finding.get("PrimaryURL", ""),
},
},
"Categories": [],
@@ -189,11 +196,16 @@ class IacProvider(Provider):
# Convert metadata dict to JSON string
metadata = json.dumps(metadata_dict)
report = CheckReportIAC(metadata=metadata, finding=check)
report.status = status
report.resource_tags = check.get("entity_tags", {})
report.status_extended = check.get("check_name", "")
if status == "MUTED":
report = CheckReportIAC(
metadata=metadata, finding=finding, file_path=file_path
)
report.status = finding_status
report.status_extended = (
finding.get("Message", "")
if finding.get("Message")
else finding.get("Description", "")
)
if finding_status == "MUTED":
report.muted = True
return report
except Exception as error:
@@ -213,6 +225,8 @@ class IacProvider(Provider):
Clone a git repository to a temporary directory, supporting GitHub authentication.
"""
try:
original_url = repository_url
if github_username and personal_access_token:
repository_url = repository_url.replace(
"https://github.com/",
@@ -226,7 +240,7 @@ class IacProvider(Provider):
temporary_directory = tempfile.mkdtemp()
logger.info(
f"Cloning repository {repository_url} into {temporary_directory}..."
f"Cloning repository {original_url} into {temporary_directory}..."
)
with alive_bar(
ctrl_c=False,
@@ -236,7 +250,7 @@ class IacProvider(Provider):
enrich_print=False,
) as bar:
try:
bar.title = f"-> Cloning {repository_url}..."
bar.title = f"-> Cloning {original_url}..."
porcelain.clone(repository_url, temporary_directory, depth=1)
bar.title = "-> Repository cloned successfully!"
except Exception as clone_error:
@@ -261,7 +275,7 @@ class IacProvider(Provider):
scan_dir = self.scan_path
try:
reports = self.run_scan(scan_dir, self.frameworks, self.exclude_path)
reports = self.run_scan(scan_dir, self.scanners, self.exclude_path)
finally:
if temp_dir:
logger.info(f"Removing temporary directory {temp_dir}...")
@@ -270,35 +284,76 @@ class IacProvider(Provider):
return reports
def run_scan(
self, directory: str, frameworks: list[str], exclude_path: list[str]
self, directory: str, scanners: list[str], exclude_path: list[str]
) -> List[CheckReportIAC]:
try:
logger.info(f"Running IaC scan on {directory} ...")
checkov_command = [
"checkov",
"-d",
trivy_command = [
"trivy",
"fs",
directory,
"-o",
"--format",
"json",
"-f",
",".join(frameworks),
"--scanners",
",".join(scanners),
"--parallel",
"0",
"--include-non-failures",
]
if exclude_path:
checkov_command.extend(["--skip-path", ",".join(exclude_path)])
# Run Checkov with JSON output
process = subprocess.run(
checkov_command,
capture_output=True,
text=True,
)
# Log Checkov's error output if any
trivy_command.extend(["--skip-dirs", ",".join(exclude_path)])
with alive_bar(
ctrl_c=False,
bar="blocks",
spinner="classic",
stats=False,
enrich_print=False,
) as bar:
try:
bar.title = f"-> Running IaC scan on {directory} ..."
# Run Trivy with JSON output
process = subprocess.run(
trivy_command,
capture_output=True,
text=True,
)
bar.title = "-> Scan completed!"
except Exception as error:
bar.title = "-> Scan failed!"
raise error
# Log Trivy's stderr output with preserved log levels
if process.stderr:
logger.error(process.stderr)
for line in process.stderr.strip().split("\n"):
if line.strip():
# Parse Trivy's log format to extract level and message
# Trivy format: timestamp level message
parts = line.split()
if len(parts) >= 3:
# Extract level and message
level = parts[1]
message = " ".join(parts[2:])
# Map Trivy log levels to Python logging levels
if level == "ERROR":
logger.error(f"{message}")
elif level == "WARN":
logger.warning(f"{message}")
elif level == "INFO":
logger.info(f"{message}")
elif level == "DEBUG":
logger.debug(f"{message}")
else:
# Default to info for unknown levels
logger.info(f"{message}")
else:
# If we can't parse the format, log as info
logger.info(f"{line}")
try:
output = json.loads(process.stdout)
output = json.loads(process.stdout)["Results"]
if not output:
logger.warning("No findings returned from Checkov scan")
logger.warning("No findings returned from Trivy scan")
return []
except Exception as error:
logger.critical(
@@ -308,37 +363,41 @@ class IacProvider(Provider):
reports = []
# If only one framework has findings, the output is a dict, otherwise it's a list of dicts
if isinstance(output, dict):
output = [output]
# Process all frameworks findings
# Process all trivy findings
for finding in output:
results = finding.get("results", {})
# Process failed checks
failed_checks = results.get("failed_checks", [])
for failed_check in failed_checks:
report = self._process_check(finding, failed_check, "FAIL")
# Process Misconfigurations
for misconfiguration in finding.get("Misconfigurations", []):
report = self._process_finding(
misconfiguration, finding["Target"], finding["Type"]
)
reports.append(report)
# Process passed checks
passed_checks = results.get("passed_checks", [])
for passed_check in passed_checks:
report = self._process_check(finding, passed_check, "PASS")
# Process Vulnerabilities
for vulnerability in finding.get("Vulnerabilities", []):
report = self._process_finding(
vulnerability, finding["Target"], finding["Type"]
)
reports.append(report)
# Process skipped checks (muted)
skipped_checks = results.get("skipped_checks", [])
for skipped_check in skipped_checks:
report = self._process_check(finding, skipped_check, "MUTED")
# Process Secrets
for secret in finding.get("Secrets", []):
report = self._process_finding(
secret, finding["Target"], finding["Class"]
)
reports.append(report)
# Process Licenses
for license in finding.get("Licenses", []):
report = self._process_finding(
license, finding["Target"], finding["Type"]
)
reports.append(report)
return reports
except Exception as error:
if "No such file or directory: 'checkov'" in str(error):
logger.critical("Please, install checkov using 'pip install checkov'")
if "No such file or directory: 'trivy'" in str(error):
logger.critical(
"Trivy binary not found. Please install Trivy from https://trivy.dev/latest/getting-started/installation/ or use your system package manager (e.g., 'brew install trivy' on macOS, 'apt-get install trivy' on Ubuntu)"
)
sys.exit(1)
logger.critical(
f"{error.__class__.__name__}:{error.__traceback__.tb_lineno} -- {error}"
@@ -367,7 +426,7 @@ class IacProvider(Provider):
)
report_lines.append(
f"Frameworks: {Fore.YELLOW}{', '.join(self.frameworks)}{Style.RESET_ALL}"
f"Scanners: {Fore.YELLOW}{', '.join(self.scanners)}{Style.RESET_ALL}"
)
report_lines.append(
@@ -1,33 +1,8 @@
FRAMEWORK_CHOICES = [
"ansible",
"argo_workflows",
"arm",
"azure_pipelines",
"bicep",
"bitbucket",
"bitbucket_pipelines",
"cdk",
"circleci_pipelines",
"cloudformation",
"dockerfile",
"github",
"github_actions",
"gitlab",
"gitlab_ci",
"helm",
"json_doc",
"kubernetes",
"kustomize",
"openapi",
"policies_3d",
"sast",
"sca_image",
"sca_package_2",
"secrets",
"serverless",
"terraform",
"terraform_json",
"yaml_doc",
SCANNERS_CHOICES = [
"vuln",
"misconfig",
"secret",
"license",
]
@@ -56,14 +31,13 @@ def init_parser(self):
)
iac_scan_subparser.add_argument(
"--frameworks",
"-f",
"--framework",
dest="frameworks",
"--scanners",
"--scanner",
dest="scanners",
nargs="+",
default=["all"],
choices=FRAMEWORK_CHOICES,
help="Comma-separated list of frameworks to scan. Default: all",
default=["vuln", "misconfig", "secret"],
choices=SCANNERS_CHOICES,
help="Comma-separated list of scanners to scan. Default: vuln, misconfig, secret",
)
iac_scan_subparser.add_argument(
"--exclude-path",
@@ -982,6 +982,20 @@ class M365PowerShell(PowerShellSession):
"""
return self.execute("Get-SharingPolicy | ConvertTo-Json", json_parse=True)
def get_user_account_status(self) -> dict:
"""
Get User Account Status.
Retrieves the current user account status settings for Exchange Online.
Returns:
dict: User account status settings in JSON format.
"""
return self.execute(
"$dict=@{}; Get-User -ResultSize Unlimited | ForEach-Object { $dict[$_.Id] = @{ AccountDisabled = $_.AccountDisabled } }; $dict | ConvertTo-Json",
json_parse=True,
)
# This function is used to install the required M365 PowerShell modules in Docker containers
def initialize_m365_powershell_modules():
@@ -14,7 +14,10 @@ from prowler.providers.m365.m365_provider import M365Provider
class Entra(M365Service):
def __init__(self, provider: M365Provider):
super().__init__(provider)
if self.powershell:
self.powershell.connect_exchange_online()
self.user_accounts_status = self.powershell.get_user_account_status()
self.powershell.close()
loop = get_event_loop()
@@ -36,6 +39,7 @@ class Entra(M365Service):
self.groups = attributes[3]
self.organizations = attributes[4]
self.users = attributes[5]
self.user_accounts_status = {}
async def _get_authorization_policy(self):
logger.info("Entra - Getting authorization policy...")
@@ -405,6 +409,9 @@ class Entra(M365Service):
if registration_details.get(user.id, None) is not None
else False
),
account_enabled=not self.user_accounts_status.get(user.id, {}).get(
"AccountDisabled", False
),
)
except Exception as error:
logger.error(
@@ -585,6 +592,7 @@ class User(BaseModel):
on_premises_sync_enabled: bool
directory_roles_ids: List[str] = []
is_mfa_capable: bool = False
account_enabled: bool = True
class InvitationsFrom(Enum):
@@ -26,20 +26,21 @@ class entra_users_mfa_capable(Check):
findings = []
for user in entra_client.users.values():
report = CheckReportM365(
metadata=self.metadata(),
resource=user,
resource_name=user.name,
resource_id=user.id,
)
if user.account_enabled:
report = CheckReportM365(
metadata=self.metadata(),
resource=user,
resource_name=user.name,
resource_id=user.id,
)
if not user.is_mfa_capable:
report.status = "FAIL"
report.status_extended = f"User {user.name} is not MFA capable."
else:
report.status = "PASS"
report.status_extended = f"User {user.name} is MFA capable."
if not user.is_mfa_capable:
report.status = "FAIL"
report.status_extended = f"User {user.name} is not MFA capable."
else:
report.status = "PASS"
report.status_extended = f"User {user.name} is MFA capable."
findings.append(report)
findings.append(report)
return findings
+2 -1
View File
@@ -62,7 +62,8 @@ dependencies = [
"slack-sdk==3.34.0",
"tabulate==0.9.0",
"tzlocal==5.3.1",
"py-iam-expand==0.1.0"
"py-iam-expand==0.1.0",
"h2 (==4.3.0)"
]
description = "Prowler is an Open Source security tool to perform AWS, GCP and Azure security best practices assessments, audits, incident response, continuous monitoring, hardening and forensics readiness. It contains hundreds of controls covering CIS, NIST 800, NIST CSF, CISA, RBI, FedRAMP, PCI-DSS, GDPR, HIPAA, FFIEC, SOC2, GXP, AWS Well-Architected Framework Security Pillar, AWS Foundational Technical Review (FTR), ENS (Spanish National Security Scheme) and your custom security frameworks."
license = "Apache-2.0"
+5 -5
View File
@@ -586,7 +586,7 @@ class TestFinding:
provider.type = "github"
# GitHub App identity only has app_id, not account_name/account_id
provider.identity = GithubAppIdentityInfo(
app_id=APP_ID, installations=["test-org"]
app_id=APP_ID, app_name="test-app", installations=["test-org"]
)
provider.auth_method = "GitHub App Token"
@@ -634,8 +634,8 @@ class TestFinding:
# Assert account information for GitHub App - this is the core of the bug fix
# Before the fix, this would fail because GithubAppIdentityInfo doesn't have account_name
# After the fix, it should use app_id with "app-" prefix
assert finding_output.account_name == f"app-{APP_ID}"
# After the fix, it should use app_name
assert finding_output.account_name == "test-app"
assert finding_output.account_uid == APP_ID
assert finding_output.account_email is None
assert finding_output.account_organization_uid is None
@@ -661,7 +661,7 @@ class TestFinding:
check_output.file_path = "/path/to/iac/file.tf"
check_output.resource_name = "aws_s3_bucket.example"
check_output.resource_path = "/path/to/iac/file.tf"
check_output.file_line_range = [1, 5]
check_output.resource_line_range = "1:5"
check_output.resource = {
"resource": "aws_s3_bucket.example",
"value": {},
@@ -685,7 +685,7 @@ class TestFinding:
assert finding_output.auth_method == "No auth"
assert finding_output.resource_name == "aws_s3_bucket.example"
assert finding_output.resource_uid == "aws_s3_bucket.example"
assert finding_output.region == "/path/to/iac/file.tf"
assert finding_output.region == "1:5"
assert finding_output.status == Status.PASS
assert finding_output.status_extended == "mock_status_extended"
assert finding_output.muted is False
+32 -12
View File
@@ -4,6 +4,7 @@ from io import StringIO
from mock import patch
from prowler.config.config import prowler_version, timestamp
from prowler.lib.logger import logger
from prowler.lib.outputs.html.html import HTML
from prowler.providers.github.models import GithubAppIdentityInfo
from tests.lib.outputs.fixtures.fixtures import generate_finding_output
@@ -231,11 +232,12 @@ github_personal_access_token_html_assessment_summary = """
<div class="card-header">
GitHub Assessment Summary
</div>
<ul class="list-group
list-group-flush">
<ul class="list-group list-group-flush">
<li class="list-group-item">
<b>GitHub account:</b> account-name
</li>
</ul>
</div>
</div>
@@ -244,8 +246,8 @@ github_personal_access_token_html_assessment_summary = """
<div class="card-header">
GitHub Credentials
</div>
<ul class="list-group
list-group-flush">
<ul class="list-group list-group-flush">
<li class="list-group-item">
<b>GitHub authentication method:</b> Personal Access Token
</li>
@@ -259,10 +261,12 @@ github_app_html_assessment_summary = """
<div class="card-header">
GitHub Assessment Summary
</div>
<ul class="list-group
list-group-flush">
<ul class="list-group list-group-flush">
<li class="list-group-item">
<b>GitHub account:</b> app-app-id
<b>GitHub App Name:</b> test-app
</li>
<li class="list-group-item">
<b>Installations:</b> test-org
</li>
</ul>
</div>
@@ -272,11 +276,13 @@ github_app_html_assessment_summary = """
<div class="card-header">
GitHub Credentials
</div>
<ul class="list-group
list-group-flush">
<ul class="list-group list-group-flush">
<li class="list-group-item">
<b>GitHub authentication method:</b> GitHub App Token
</li>
<li class="list-group-item">
<b>GitHub App ID:</b> app-id
</li>
</ul>
</div>
</div>"""
@@ -664,7 +670,12 @@ class TestHTML:
summary = output.get_assessment_summary(provider)
assert summary == github_personal_access_token_html_assessment_summary
# Check for expected content in the summary
assert "GitHub Assessment Summary" in summary
assert "GitHub Credentials" in summary
assert "<b>GitHub account:</b> account-name" in summary
assert "<b>GitHub authentication method:</b> Personal Access Token" in summary
# Note: account_email is None in the default fixture, so it shouldn't appear
def test_github_app_get_assessment_summary(self):
"""Test GitHub HTML assessment summary generation with GitHub App authentication."""
@@ -673,9 +684,18 @@ class TestHTML:
provider = set_mocked_github_provider(
auth_method="GitHub App Token",
identity=GithubAppIdentityInfo(app_id=APP_ID, installations=["test-org"]),
identity=GithubAppIdentityInfo(
app_id=APP_ID, app_name="test-app", installations=["test-org"]
),
)
summary = output.get_assessment_summary(provider)
logger.error(summary)
assert summary == github_app_html_assessment_summary
# Check for expected content in the summary
assert "GitHub Assessment Summary" in summary
assert "GitHub Credentials" in summary
assert "<b>GitHub App Name:</b> test-app" in summary
assert "<b>Installations:</b> test-org" in summary
assert "<b>GitHub authentication method:</b> GitHub App Token" in summary
assert f"<b>GitHub App ID:</b> {APP_ID}" in summary
@@ -412,14 +412,12 @@ class TestSecurityHub:
@patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call)
def test_security_hub_test_connection_success(self):
session_mock = session.Session(region_name=AWS_REGION_EU_WEST_1)
# Test successful connection
connection = SecurityHub.test_connection(
session=session_mock,
regions={AWS_REGION_EU_WEST_1},
aws_account_id=AWS_ACCOUNT_NUMBER,
aws_partition=AWS_COMMERCIAL_PARTITION,
regions={AWS_REGION_EU_WEST_1},
raise_on_exception=False,
)
@@ -444,14 +442,11 @@ class TestSecurityHub:
error_response, operation_name
)
session_mock = session.Session(region_name=AWS_REGION_EU_WEST_1)
# Test connection failure due to invalid access
connection = SecurityHub.test_connection(
session=session_mock,
regions={AWS_REGION_EU_WEST_1},
aws_account_id=AWS_ACCOUNT_NUMBER,
aws_partition=AWS_COMMERCIAL_PARTITION,
regions={AWS_REGION_EU_WEST_1},
raise_on_exception=False,
)
@@ -468,14 +463,11 @@ class TestSecurityHub:
"ProductSubscriptions": []
}
session_mock = session.Session(region_name=AWS_REGION_EU_WEST_1)
# Test connection failure due to missing Prowler subscription
connection = SecurityHub.test_connection(
session=session_mock,
regions={AWS_REGION_EU_WEST_1},
aws_account_id=AWS_ACCOUNT_NUMBER,
aws_partition=AWS_COMMERCIAL_PARTITION,
regions={AWS_REGION_EU_WEST_1},
raise_on_exception=False,
)
@@ -489,14 +481,11 @@ class TestSecurityHub:
# Mock unexpected exception
mock_security_hub_client.side_effect = Exception("Unexpected error")
session_mock = session.Session(region_name=AWS_REGION_EU_WEST_1)
# Test connection failure due to an unexpected exception
connection = SecurityHub.test_connection(
session=session_mock,
regions={AWS_REGION_EU_WEST_1},
aws_account_id=AWS_ACCOUNT_NUMBER,
aws_partition=AWS_COMMERCIAL_PARTITION,
regions={AWS_REGION_EU_WEST_1},
raise_on_exception=False,
)
@@ -510,11 +499,8 @@ class TestSecurityHub:
# Mock unexpected exception
mock_security_hub_client.side_effect = Exception("Unexpected error")
session_mock = session.Session(region_name=AWS_REGION_EU_WEST_1)
# Test connection failure due to an unexpected exception
connection = SecurityHub.test_connection(
session=session_mock,
aws_account_id=AWS_ACCOUNT_NUMBER,
aws_partition=AWS_COMMERCIAL_PARTITION,
raise_on_exception=False,
@@ -566,3 +552,734 @@ class TestSecurityHub:
str(e.value)
== "If no role ARN is provided, a profile, an AWS access key ID, or an AWS secret access key is required."
)
# Tests for new test_connection functionality - AWS Credential Management
@patch("prowler.providers.aws.aws_provider.AwsProvider.setup_session")
@patch(
"prowler.providers.aws.aws_provider.AwsProvider.get_available_aws_service_regions"
)
@patch(
"prowler.providers.aws.lib.security_hub.security_hub.SecurityHub.verify_enabled_per_region"
)
def test_security_hub_test_connection_with_profile(
self, mock_verify_enabled, mock_get_regions, mock_setup_session
):
# Mock session setup
mock_session = session.Session(region_name=AWS_REGION_EU_WEST_1)
mock_setup_session.return_value = mock_session
# Mock available regions
mock_get_regions.return_value = [AWS_REGION_EU_WEST_1]
# Mock enabled regions
mock_verify_enabled.return_value = {AWS_REGION_EU_WEST_1: mock_session}
# Test connection with profile
connection = SecurityHub.test_connection(
aws_account_id=AWS_ACCOUNT_NUMBER,
aws_partition=AWS_COMMERCIAL_PARTITION,
profile="test-profile",
raise_on_exception=False,
)
assert connection.is_connected is True
assert connection.error is None
mock_setup_session.assert_called_once_with(
mfa=False,
profile="test-profile",
aws_access_key_id=None,
aws_secret_access_key=None,
aws_session_token=None,
)
@patch("prowler.providers.aws.aws_provider.AwsProvider.setup_session")
@patch(
"prowler.providers.aws.aws_provider.AwsProvider.get_available_aws_service_regions"
)
@patch(
"prowler.providers.aws.lib.security_hub.security_hub.SecurityHub.verify_enabled_per_region"
)
def test_security_hub_test_connection_with_access_keys(
self, mock_verify_enabled, mock_get_regions, mock_setup_session
):
# Mock session setup
mock_session = session.Session(region_name=AWS_REGION_EU_WEST_1)
mock_setup_session.return_value = mock_session
# Mock available regions
mock_get_regions.return_value = [AWS_REGION_EU_WEST_1]
# Mock enabled regions
mock_verify_enabled.return_value = {AWS_REGION_EU_WEST_1: mock_session}
# Test connection with access keys
connection = SecurityHub.test_connection(
aws_account_id=AWS_ACCOUNT_NUMBER,
aws_partition=AWS_COMMERCIAL_PARTITION,
aws_access_key_id="test-key",
aws_secret_access_key="test-secret",
raise_on_exception=False,
)
assert connection.is_connected is True
assert connection.error is None
mock_setup_session.assert_called_once_with(
mfa=False,
profile=None,
aws_access_key_id="test-key",
aws_secret_access_key="test-secret",
aws_session_token=None,
)
@patch("prowler.providers.aws.aws_provider.AwsProvider.setup_session")
@patch(
"prowler.providers.aws.aws_provider.AwsProvider.get_available_aws_service_regions"
)
@patch(
"prowler.providers.aws.lib.security_hub.security_hub.SecurityHub.verify_enabled_per_region"
)
def test_security_hub_test_connection_with_session_token(
self, mock_verify_enabled, mock_get_regions, mock_setup_session
):
# Mock session setup
mock_session = session.Session(region_name=AWS_REGION_EU_WEST_1)
mock_setup_session.return_value = mock_session
# Mock available regions
mock_get_regions.return_value = [AWS_REGION_EU_WEST_1]
# Mock enabled regions
mock_verify_enabled.return_value = {AWS_REGION_EU_WEST_1: mock_session}
# Test connection with session token
connection = SecurityHub.test_connection(
aws_account_id=AWS_ACCOUNT_NUMBER,
aws_partition=AWS_COMMERCIAL_PARTITION,
aws_session_token="test-token",
raise_on_exception=False,
)
assert connection.is_connected is True
assert connection.error is None
mock_setup_session.assert_called_once_with(
mfa=False,
profile=None,
aws_access_key_id=None,
aws_secret_access_key=None,
aws_session_token="test-token",
)
@patch("prowler.providers.aws.aws_provider.AwsProvider.setup_session")
@patch(
"prowler.providers.aws.aws_provider.AwsProvider.get_available_aws_service_regions"
)
@patch(
"prowler.providers.aws.lib.security_hub.security_hub.SecurityHub.verify_enabled_per_region"
)
def test_security_hub_test_connection_with_mfa(
self, mock_verify_enabled, mock_get_regions, mock_setup_session
):
# Mock session setup
mock_session = session.Session(region_name=AWS_REGION_EU_WEST_1)
mock_setup_session.return_value = mock_session
# Mock available regions
mock_get_regions.return_value = [AWS_REGION_EU_WEST_1]
# Mock enabled regions
mock_verify_enabled.return_value = {AWS_REGION_EU_WEST_1: mock_session}
# Test connection with MFA enabled
connection = SecurityHub.test_connection(
aws_account_id=AWS_ACCOUNT_NUMBER,
aws_partition=AWS_COMMERCIAL_PARTITION,
mfa_enabled=True,
raise_on_exception=False,
)
assert connection.is_connected is True
assert connection.error is None
mock_setup_session.assert_called_once_with(
mfa=True,
profile=None,
aws_access_key_id=None,
aws_secret_access_key=None,
aws_session_token=None,
)
# Tests for Role Assumption functionality
@patch("prowler.providers.aws.aws_provider.AwsProvider.setup_session")
@patch("prowler.providers.aws.aws_provider.AwsProvider.assume_role")
@patch(
"prowler.providers.aws.aws_provider.AwsProvider.get_available_aws_service_regions"
)
@patch(
"prowler.providers.aws.lib.security_hub.security_hub.SecurityHub.verify_enabled_per_region"
)
def test_security_hub_test_connection_with_role_arn(
self,
mock_verify_enabled,
mock_get_regions,
mock_assume_role,
mock_setup_session,
):
# Mock initial session setup
mock_session = session.Session(region_name=AWS_REGION_EU_WEST_1)
mock_setup_session.return_value = mock_session
# Mock assumed role credentials
from datetime import datetime, timezone
from prowler.providers.aws.models import AWSCredentials
mock_credentials = AWSCredentials(
aws_access_key_id="assumed-key",
aws_secret_access_key="assumed-secret",
aws_session_token="assumed-token",
expiration=datetime.now(timezone.utc),
)
mock_assume_role.return_value = mock_credentials
# Mock available regions
mock_get_regions.return_value = [AWS_REGION_EU_WEST_1]
# Mock enabled regions
mock_verify_enabled.return_value = {AWS_REGION_EU_WEST_1: mock_session}
# Test connection with role ARN
connection = SecurityHub.test_connection(
aws_account_id=AWS_ACCOUNT_NUMBER,
aws_partition=AWS_COMMERCIAL_PARTITION,
role_arn="arn:aws:iam::123456789012:role/test-role",
external_id="test-external-id",
session_duration=7200,
role_session_name="test-session",
raise_on_exception=False,
)
assert connection.is_connected is True
assert connection.error is None
mock_assume_role.assert_called_once()
@patch("prowler.providers.aws.aws_provider.AwsProvider.setup_session")
@patch("prowler.providers.aws.aws_provider.AwsProvider.assume_role")
@patch(
"prowler.providers.aws.aws_provider.AwsProvider.get_available_aws_service_regions"
)
@patch(
"prowler.providers.aws.lib.security_hub.security_hub.SecurityHub.verify_enabled_per_region"
)
def test_security_hub_test_connection_with_role_arn_default_values(
self,
mock_verify_enabled,
mock_get_regions,
mock_assume_role,
mock_setup_session,
):
# Mock initial session setup
mock_session = session.Session(region_name=AWS_REGION_EU_WEST_1)
mock_setup_session.return_value = mock_session
# Mock assumed role credentials
from datetime import datetime, timezone
from prowler.providers.aws.models import AWSCredentials
mock_credentials = AWSCredentials(
aws_access_key_id="assumed-key",
aws_secret_access_key="assumed-secret",
aws_session_token="assumed-token",
expiration=datetime.now(timezone.utc),
)
mock_assume_role.return_value = mock_credentials
# Mock available regions
mock_get_regions.return_value = [AWS_REGION_EU_WEST_1]
# Mock enabled regions
mock_verify_enabled.return_value = {AWS_REGION_EU_WEST_1: mock_session}
# Test connection with role ARN using default values
connection = SecurityHub.test_connection(
aws_account_id=AWS_ACCOUNT_NUMBER,
aws_partition=AWS_COMMERCIAL_PARTITION,
role_arn="arn:aws:iam::123456789012:role/test-role",
external_id="test-external-id",
raise_on_exception=False,
)
assert connection.is_connected is True
assert connection.error is None
mock_assume_role.assert_called_once()
# Tests for Error Handling - Session Setup Errors
@patch("prowler.providers.aws.aws_provider.AwsProvider.setup_session")
def test_security_hub_test_connection_setup_session_error(self, mock_setup_session):
from prowler.providers.aws.exceptions.exceptions import AWSSetUpSessionError
# Mock session setup error
mock_setup_session.side_effect = AWSSetUpSessionError(
file="test_file.py", original_exception=Exception("Session setup failed")
)
# Test connection failure due to session setup error
connection = SecurityHub.test_connection(
aws_account_id=AWS_ACCOUNT_NUMBER,
aws_partition=AWS_COMMERCIAL_PARTITION,
raise_on_exception=False,
)
assert connection.is_connected is False
assert isinstance(connection.error, AWSSetUpSessionError)
@patch("prowler.providers.aws.aws_provider.AwsProvider.setup_session")
def test_security_hub_test_connection_setup_session_error_raise(
self, mock_setup_session
):
from prowler.providers.aws.exceptions.exceptions import AWSSetUpSessionError
# Mock session setup error
mock_setup_session.side_effect = AWSSetUpSessionError(
file="test_file.py", original_exception=Exception("Session setup failed")
)
# Test that error is raised when raise_on_exception=True
with pytest.raises(AWSSetUpSessionError):
SecurityHub.test_connection(
aws_account_id=AWS_ACCOUNT_NUMBER,
aws_partition=AWS_COMMERCIAL_PARTITION,
raise_on_exception=True,
)
# Tests for Error Handling - Argument Validation Errors
@patch("prowler.providers.aws.aws_provider.AwsProvider.setup_session")
def test_security_hub_test_connection_argument_validation_error(
self, mock_setup_session
):
from prowler.providers.aws.exceptions.exceptions import (
AWSArgumentTypeValidationError,
)
# Mock session setup error
mock_setup_session.side_effect = AWSArgumentTypeValidationError(
file="test_file.py", original_exception=ValueError("Invalid argument")
)
# Test connection failure due to argument validation error
connection = SecurityHub.test_connection(
aws_account_id=AWS_ACCOUNT_NUMBER,
aws_partition=AWS_COMMERCIAL_PARTITION,
raise_on_exception=False,
)
assert connection.is_connected is False
assert isinstance(connection.error, AWSArgumentTypeValidationError)
# Tests for Error Handling - Role ARN Validation Errors
@patch("prowler.providers.aws.aws_provider.AwsProvider.setup_session")
def test_security_hub_test_connection_role_arn_region_not_empty_error(
self, mock_setup_session
):
from prowler.providers.aws.exceptions.exceptions import (
AWSIAMRoleARNRegionNotEmtpyError,
)
# Mock session setup error
mock_setup_session.side_effect = AWSIAMRoleARNRegionNotEmtpyError(
file="test_file.py"
)
# Test connection failure due to role ARN region validation error
connection = SecurityHub.test_connection(
aws_account_id=AWS_ACCOUNT_NUMBER,
aws_partition=AWS_COMMERCIAL_PARTITION,
raise_on_exception=False,
)
assert connection.is_connected is False
assert isinstance(connection.error, AWSIAMRoleARNRegionNotEmtpyError)
@patch("prowler.providers.aws.aws_provider.AwsProvider.setup_session")
def test_security_hub_test_connection_role_arn_partition_empty_error(
self, mock_setup_session
):
from prowler.providers.aws.exceptions.exceptions import (
AWSIAMRoleARNPartitionEmptyError,
)
# Mock session setup error
mock_setup_session.side_effect = AWSIAMRoleARNPartitionEmptyError(
file="test_file.py"
)
# Test connection failure due to role ARN partition validation error
connection = SecurityHub.test_connection(
aws_account_id=AWS_ACCOUNT_NUMBER,
aws_partition=AWS_COMMERCIAL_PARTITION,
raise_on_exception=False,
)
assert connection.is_connected is False
assert isinstance(connection.error, AWSIAMRoleARNPartitionEmptyError)
@patch("prowler.providers.aws.aws_provider.AwsProvider.setup_session")
def test_security_hub_test_connection_role_arn_service_not_iam_sts_error(
self, mock_setup_session
):
from prowler.providers.aws.exceptions.exceptions import (
AWSIAMRoleARNServiceNotIAMnorSTSError,
)
# Mock session setup error
mock_setup_session.side_effect = AWSIAMRoleARNServiceNotIAMnorSTSError(
file="test_file.py"
)
# Test connection failure due to role ARN service validation error
connection = SecurityHub.test_connection(
aws_account_id=AWS_ACCOUNT_NUMBER,
aws_partition=AWS_COMMERCIAL_PARTITION,
raise_on_exception=False,
)
assert connection.is_connected is False
assert isinstance(connection.error, AWSIAMRoleARNServiceNotIAMnorSTSError)
@patch("prowler.providers.aws.aws_provider.AwsProvider.setup_session")
def test_security_hub_test_connection_role_arn_invalid_account_id_error(
self, mock_setup_session
):
from prowler.providers.aws.exceptions.exceptions import (
AWSIAMRoleARNInvalidAccountIDError,
)
# Mock session setup error
mock_setup_session.side_effect = AWSIAMRoleARNInvalidAccountIDError(
file="test_file.py"
)
# Test connection failure due to role ARN account ID validation error
connection = SecurityHub.test_connection(
aws_account_id=AWS_ACCOUNT_NUMBER,
aws_partition=AWS_COMMERCIAL_PARTITION,
raise_on_exception=False,
)
assert connection.is_connected is False
assert isinstance(connection.error, AWSIAMRoleARNInvalidAccountIDError)
@patch("prowler.providers.aws.aws_provider.AwsProvider.setup_session")
def test_security_hub_test_connection_role_arn_invalid_resource_type_error(
self, mock_setup_session
):
from prowler.providers.aws.exceptions.exceptions import (
AWSIAMRoleARNInvalidResourceTypeError,
)
# Mock session setup error
mock_setup_session.side_effect = AWSIAMRoleARNInvalidResourceTypeError(
file="test_file.py"
)
# Test connection failure due to role ARN resource type validation error
connection = SecurityHub.test_connection(
aws_account_id=AWS_ACCOUNT_NUMBER,
aws_partition=AWS_COMMERCIAL_PARTITION,
raise_on_exception=False,
)
assert connection.is_connected is False
assert isinstance(connection.error, AWSIAMRoleARNInvalidResourceTypeError)
@patch("prowler.providers.aws.aws_provider.AwsProvider.setup_session")
def test_security_hub_test_connection_role_arn_empty_resource_error(
self, mock_setup_session
):
from prowler.providers.aws.exceptions.exceptions import (
AWSIAMRoleARNEmptyResourceError,
)
# Mock session setup error
mock_setup_session.side_effect = AWSIAMRoleARNEmptyResourceError(
file="test_file.py"
)
# Test connection failure due to role ARN empty resource validation error
connection = SecurityHub.test_connection(
aws_account_id=AWS_ACCOUNT_NUMBER,
aws_partition=AWS_COMMERCIAL_PARTITION,
raise_on_exception=False,
)
assert connection.is_connected is False
assert isinstance(connection.error, AWSIAMRoleARNEmptyResourceError)
# Tests for Error Handling - Role Assumption Errors
@patch("prowler.providers.aws.aws_provider.AwsProvider.setup_session")
def test_security_hub_test_connection_assume_role_error(self, mock_setup_session):
from prowler.providers.aws.exceptions.exceptions import AWSAssumeRoleError
# Mock session setup error
mock_setup_session.side_effect = AWSAssumeRoleError(
file="test_file.py", original_exception=Exception("Role assumption failed")
)
# Test connection failure due to role assumption error
connection = SecurityHub.test_connection(
aws_account_id=AWS_ACCOUNT_NUMBER,
aws_partition=AWS_COMMERCIAL_PARTITION,
raise_on_exception=False,
)
assert connection.is_connected is False
assert isinstance(connection.error, AWSAssumeRoleError)
# Tests for Error Handling - Profile and Credential Errors
@patch("prowler.providers.aws.aws_provider.AwsProvider.setup_session")
def test_security_hub_test_connection_profile_not_found_error(
self, mock_setup_session
):
from botocore.exceptions import ProfileNotFound
# Mock session setup error
mock_setup_session.side_effect = ProfileNotFound(profile="test-profile")
# Test connection failure due to profile not found error
connection = SecurityHub.test_connection(
aws_account_id=AWS_ACCOUNT_NUMBER,
aws_partition=AWS_COMMERCIAL_PARTITION,
profile="test-profile",
raise_on_exception=False,
)
assert connection.is_connected is False
assert isinstance(connection.error, ProfileNotFound)
@patch("prowler.providers.aws.aws_provider.AwsProvider.setup_session")
def test_security_hub_test_connection_profile_not_found_error_raise(
self, mock_setup_session
):
from botocore.exceptions import ProfileNotFound
from prowler.providers.aws.exceptions.exceptions import AWSProfileNotFoundError
# Mock session setup error
mock_setup_session.side_effect = ProfileNotFound(profile="test-profile")
# Test that error is raised when raise_on_exception=True
with pytest.raises(AWSProfileNotFoundError):
SecurityHub.test_connection(
aws_account_id=AWS_ACCOUNT_NUMBER,
aws_partition=AWS_COMMERCIAL_PARTITION,
profile="test-profile",
raise_on_exception=True,
)
@patch("prowler.providers.aws.aws_provider.AwsProvider.setup_session")
def test_security_hub_test_connection_no_credentials_error(
self, mock_setup_session
):
from botocore.exceptions import NoCredentialsError
# Mock session setup error
mock_setup_session.side_effect = NoCredentialsError()
# Test connection failure due to no credentials error
connection = SecurityHub.test_connection(
aws_account_id=AWS_ACCOUNT_NUMBER,
aws_partition=AWS_COMMERCIAL_PARTITION,
raise_on_exception=False,
)
assert connection.is_connected is False
assert isinstance(connection.error, NoCredentialsError)
@patch("prowler.providers.aws.aws_provider.AwsProvider.setup_session")
def test_security_hub_test_connection_no_credentials_error_raise(
self, mock_setup_session
):
from botocore.exceptions import NoCredentialsError
from prowler.providers.aws.exceptions.exceptions import AWSNoCredentialsError
# Mock session setup error
mock_setup_session.side_effect = NoCredentialsError()
# Test that error is raised when raise_on_exception=True
with pytest.raises(AWSNoCredentialsError):
SecurityHub.test_connection(
aws_account_id=AWS_ACCOUNT_NUMBER,
aws_partition=AWS_COMMERCIAL_PARTITION,
raise_on_exception=True,
)
# Tests for Error Handling - Access Key and Secret Key Errors
@patch("prowler.providers.aws.aws_provider.AwsProvider.setup_session")
def test_security_hub_test_connection_access_key_id_invalid_error(
self, mock_setup_session
):
from prowler.providers.aws.exceptions.exceptions import (
AWSAccessKeyIDInvalidError,
)
# Mock session setup error
mock_setup_session.side_effect = AWSAccessKeyIDInvalidError(
file="test_file.py", original_exception=ValueError("Invalid access key ID")
)
# Test connection failure due to invalid access key ID error
connection = SecurityHub.test_connection(
aws_account_id=AWS_ACCOUNT_NUMBER,
aws_partition=AWS_COMMERCIAL_PARTITION,
raise_on_exception=False,
)
assert connection.is_connected is False
assert isinstance(connection.error, AWSAccessKeyIDInvalidError)
@patch("prowler.providers.aws.aws_provider.AwsProvider.setup_session")
def test_security_hub_test_connection_secret_access_key_invalid_error(
self, mock_setup_session
):
from prowler.providers.aws.exceptions.exceptions import (
AWSSecretAccessKeyInvalidError,
)
# Mock session setup error
mock_setup_session.side_effect = AWSSecretAccessKeyInvalidError(
file="test_file.py",
original_exception=ValueError("Invalid secret access key"),
)
# Test connection failure due to invalid secret access key error
connection = SecurityHub.test_connection(
aws_account_id=AWS_ACCOUNT_NUMBER,
aws_partition=AWS_COMMERCIAL_PARTITION,
raise_on_exception=False,
)
assert connection.is_connected is False
assert isinstance(connection.error, AWSSecretAccessKeyInvalidError)
@patch("prowler.providers.aws.aws_provider.AwsProvider.setup_session")
def test_security_hub_test_connection_session_token_expired_error(
self, mock_setup_session
):
from prowler.providers.aws.exceptions.exceptions import (
AWSSessionTokenExpiredError,
)
# Mock session setup error
mock_setup_session.side_effect = AWSSessionTokenExpiredError(
file="test_file.py", original_exception=ValueError("Session token expired")
)
# Test connection failure due to session token expired error
connection = SecurityHub.test_connection(
aws_account_id=AWS_ACCOUNT_NUMBER,
aws_partition=AWS_COMMERCIAL_PARTITION,
raise_on_exception=False,
)
assert connection.is_connected is False
assert isinstance(connection.error, AWSSessionTokenExpiredError)
# Tests for Error Handling - Generic Exception
@patch("prowler.providers.aws.aws_provider.AwsProvider.setup_session")
def test_security_hub_test_connection_generic_exception(self, mock_setup_session):
# Mock session setup error
mock_setup_session.side_effect = Exception("Generic error")
# Test connection failure due to generic exception
connection = SecurityHub.test_connection(
aws_account_id=AWS_ACCOUNT_NUMBER,
aws_partition=AWS_COMMERCIAL_PARTITION,
raise_on_exception=False,
)
assert connection.is_connected is False
assert isinstance(connection.error, Exception)
assert str(connection.error) == "Generic error"
@patch("prowler.providers.aws.aws_provider.AwsProvider.setup_session")
def test_security_hub_test_connection_generic_exception_raise(
self, mock_setup_session
):
# Mock session setup error
mock_setup_session.side_effect = Exception("Generic error")
# Test that error is raised when raise_on_exception=True
with pytest.raises(Exception) as exc_info:
SecurityHub.test_connection(
aws_account_id=AWS_ACCOUNT_NUMBER,
aws_partition=AWS_COMMERCIAL_PARTITION,
raise_on_exception=True,
)
assert str(exc_info.value) == "Generic error"
# Tests for Edge Cases and Parameter Validation
@patch("prowler.providers.aws.aws_provider.AwsProvider.setup_session")
@patch(
"prowler.providers.aws.aws_provider.AwsProvider.get_available_aws_service_regions"
)
@patch(
"prowler.providers.aws.lib.security_hub.security_hub.SecurityHub.verify_enabled_per_region"
)
def test_security_hub_test_connection_with_aws_region(
self, mock_verify_enabled, mock_get_regions, mock_setup_session
):
# Mock session setup
mock_session = session.Session(region_name=AWS_REGION_EU_WEST_1)
mock_setup_session.return_value = mock_session
# Mock available regions
mock_get_regions.return_value = [AWS_REGION_EU_WEST_1, AWS_REGION_EU_WEST_2]
# Mock enabled regions
mock_verify_enabled.return_value = {AWS_REGION_EU_WEST_1: mock_session}
# Test connection with specific AWS region
connection = SecurityHub.test_connection(
aws_account_id=AWS_ACCOUNT_NUMBER,
aws_partition=AWS_COMMERCIAL_PARTITION,
aws_region=AWS_REGION_EU_WEST_1,
raise_on_exception=False,
)
assert connection.is_connected is True
assert connection.error is None
assert AWS_REGION_EU_WEST_1 in connection.enabled_regions
assert AWS_REGION_EU_WEST_2 in connection.disabled_regions
@patch("prowler.providers.aws.aws_provider.AwsProvider.setup_session")
@patch(
"prowler.providers.aws.aws_provider.AwsProvider.get_available_aws_service_regions"
)
@patch(
"prowler.providers.aws.lib.security_hub.security_hub.SecurityHub.verify_enabled_per_region"
)
def test_security_hub_test_connection_no_regions_specified(
self, mock_verify_enabled, mock_get_regions, mock_setup_session
):
# Mock session setup
mock_session = session.Session(region_name=AWS_REGION_EU_WEST_1)
mock_setup_session.return_value = mock_session
# Mock available regions
mock_get_regions.return_value = [AWS_REGION_EU_WEST_1, AWS_REGION_EU_WEST_2]
# Mock enabled regions
mock_verify_enabled.return_value = {AWS_REGION_EU_WEST_1: mock_session}
# Test connection without specifying regions
connection = SecurityHub.test_connection(
aws_account_id=AWS_ACCOUNT_NUMBER,
aws_partition=AWS_COMMERCIAL_PARTITION,
raise_on_exception=False,
)
assert connection.is_connected is True
assert connection.error is None
assert len(connection.enabled_regions) == 1
assert len(connection.disabled_regions) == 1
@@ -13,13 +13,14 @@ from tests.providers.aws.utils import (
class Test_s3_bucket_shadow_resource_vulnerability:
@mock_aws
def test_no_buckets(self):
s3_client = mock.MagicMock
s3_client.buckets = {}
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
aws_provider.identity.identity_arn = f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:root"
s3_client = mock.MagicMock
s3_client = mock.MagicMock()
s3_client.buckets = {}
s3_client.provider = aws_provider
s3_client._head_bucket = mock.MagicMock(return_value=False)
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
@@ -36,28 +37,31 @@ class Test_s3_bucket_shadow_resource_vulnerability:
check = s3_bucket_shadow_resource_vulnerability()
result = check.execute()
assert len(result) == 0
@mock_aws
def test_bucket_owned_by_account(self):
s3_client = mock.MagicMock
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
aws_provider.identity.identity_arn = f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:root"
bucket_name = f"sagemaker-{AWS_REGION_US_EAST_1}-{AWS_ACCOUNT_NUMBER}"
s3_client.audited_account_id = AWS_ACCOUNT_NUMBER
s3_client.audited_identity_arn = f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:root"
s3_client = mock.MagicMock()
s3_client.audited_canonical_id = AWS_ACCOUNT_NUMBER
s3_client.audited_partition = "aws"
s3_client.buckets = {
bucket_name: Bucket(
name=bucket_name,
arn=f"arn:aws:s3:::{bucket_name}",
region=AWS_REGION_US_EAST_1,
owner_id=AWS_ACCOUNT_NUMBER,
tags=[{"Key": "Environment", "Value": "test"}],
)
}
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
aws_provider.identity.identity_arn = f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:root"
s3_client = mock.MagicMock
s3_client.provider = aws_provider
s3_client._head_bucket = mock.MagicMock(return_value=False)
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
@@ -74,32 +78,41 @@ class Test_s3_bucket_shadow_resource_vulnerability:
check = s3_bucket_shadow_resource_vulnerability()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
"is correctly owned by the audited account" in result[0].status_extended
)
report = result[0]
# Test all report attributes
assert report.status == "PASS"
assert report.region == AWS_REGION_US_EAST_1
assert report.resource_id == bucket_name
assert report.resource_arn == f"arn:aws:s3:::{bucket_name}"
assert report.resource_tags == [{"Key": "Environment", "Value": "test"}]
assert "is correctly owned by the audited account" in report.status_extended
assert "SageMaker" in report.status_extended
@mock_aws
def test_bucket_not_predictable(self):
s3_client = mock.MagicMock
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
aws_provider.identity.identity_arn = f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:root"
bucket_name = "my-non-predictable-bucket"
s3_client.audited_account_id = AWS_ACCOUNT_NUMBER
s3_client.audited_identity_arn = f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:root"
s3_client = mock.MagicMock()
s3_client.audited_canonical_id = AWS_ACCOUNT_NUMBER
s3_client.audited_partition = "aws"
s3_client.buckets = {
bucket_name: Bucket(
name=bucket_name,
arn=f"arn:aws:s3:::{bucket_name}",
region=AWS_REGION_US_EAST_1,
owner_id=AWS_ACCOUNT_NUMBER,
tags=[{"Key": "Project", "Value": "test-project"}],
)
}
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
aws_provider.identity.identity_arn = f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:root"
s3_client = mock.MagicMock
s3_client.provider = aws_provider
s3_client._head_bucket = mock.MagicMock(return_value=False)
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
@@ -116,18 +129,25 @@ class Test_s3_bucket_shadow_resource_vulnerability:
check = s3_bucket_shadow_resource_vulnerability()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert "is not a known shadow resource" in result[0].status_extended
report = result[0]
# Test all report attributes
assert report.status == "PASS"
assert report.region == AWS_REGION_US_EAST_1
assert report.resource_id == bucket_name
assert report.resource_arn == f"arn:aws:s3:::{bucket_name}"
assert report.resource_tags == [{"Key": "Project", "Value": "test-project"}]
assert "is not a known shadow resource" in report.status_extended
@mock_aws
def test_shadow_resource_in_other_account(self):
# Mock S3 client with no buckets in current account
s3_client = mock.MagicMock()
s3_client.buckets = {}
s3_client.audited_account_id = AWS_ACCOUNT_NUMBER
s3_client.audited_identity_arn = f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:root"
s3_client.audited_canonical_id = AWS_ACCOUNT_NUMBER
s3_client.audited_identity_arn = f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:root"
s3_client.audited_partition = "aws"
# Mock regional clients - this is what the check uses to determine regions to test
@@ -183,27 +203,31 @@ class Test_s3_bucket_shadow_resource_vulnerability:
finding.status == "FAIL"
and "shadow resource" in finding.status_extended
):
if (
"aws-glue-assets" in finding.status_extended
and "Glue" in finding.status_extended
):
found_services.add("Glue")
assert "us-west-2" in finding.status_extended
elif (
"sagemaker" in finding.status_extended
and "SageMaker" in finding.status_extended
):
found_services.add("SageMaker")
assert "us-east-1" in finding.status_extended
elif (
"aws-emr-studio" in finding.status_extended
and "EMR" in finding.status_extended
):
found_services.add("EMR")
assert "eu-west-1" in finding.status_extended
# Test all report attributes for cross-account findings
assert finding.status == "FAIL"
assert finding.resource_id in shadow_resources
assert finding.resource_arn == f"arn:aws:s3:::{finding.resource_id}"
assert finding.resource_tags == []
# Verify common attributes
assert "owned by another account" in finding.status_extended
# Determine service from resource_id and test exact status_extended
if "aws-glue-assets" in finding.resource_id:
service = "Glue"
expected_status = f"S3 bucket {finding.resource_id} for service {service} is a known shadow resource that exists and is owned by another account."
assert finding.status_extended == expected_status
found_services.add("Glue")
assert finding.region == "us-west-2"
elif "sagemaker" in finding.resource_id:
service = "SageMaker"
expected_status = f"S3 bucket {finding.resource_id} for service {service} is a known shadow resource that exists and is owned by another account."
assert finding.status_extended == expected_status
found_services.add("SageMaker")
assert finding.region == "us-east-1"
elif "aws-emr-studio" in finding.resource_id:
service = "EMR"
expected_status = f"S3 bucket {finding.resource_id} for service {service} is a known shadow resource that exists and is owned by another account."
assert finding.status_extended == expected_status
found_services.add("EMR")
assert finding.region == "eu-west-1"
# Verify we found all expected services
expected_services = {"Glue", "SageMaker", "EMR"}
@@ -12,6 +12,7 @@ ACCOUNT_URL = "/user"
PAT_TOKEN = "github-token"
OAUTH_TOKEN = "oauth-token"
APP_ID = "app-id"
APP_NAME = "app-name"
APP_KEY = "app-key"
@@ -27,6 +27,7 @@ from tests.providers.github.github_fixtures import (
ACCOUNT_URL,
APP_ID,
APP_KEY,
APP_NAME,
OAUTH_TOKEN,
PAT_TOKEN,
)
@@ -135,6 +136,7 @@ class TestGitHubProvider:
"prowler.providers.github.github_provider.GithubProvider.setup_identity",
return_value=GithubAppIdentityInfo(
app_id=APP_ID,
app_name=APP_NAME,
installations=["test-org"],
),
),
@@ -149,7 +151,7 @@ class TestGitHubProvider:
assert provider._type == "github"
assert provider.session == GithubSession(token="", id=APP_ID, key=APP_KEY)
assert provider.identity == GithubAppIdentityInfo(
app_id=APP_ID, installations=["test-org"]
app_id=APP_ID, app_name=APP_NAME, installations=["test-org"]
)
assert provider._audit_config == {
"inactive_not_archived_days_threshold": 180,
@@ -210,7 +212,7 @@ class TestGitHubProvider:
patch(
"prowler.providers.github.github_provider.GithubProvider.setup_identity",
return_value=GithubAppIdentityInfo(
app_id=APP_ID, installations=["test-org"]
app_id=APP_ID, app_name=APP_NAME, installations=["test-org"]
),
),
):
@@ -63,7 +63,12 @@ class Test_Organization_Scoping:
mock_client = MagicMock()
mock_user = MagicMock()
mock_user.get_orgs.return_value = [self.mock_org1, self.mock_org2]
mock_orgs = MagicMock()
mock_orgs.totalCount = 2
mock_orgs.__iter__ = MagicMock(
return_value=iter([self.mock_org1, self.mock_org2])
)
mock_user.get_orgs.return_value = mock_orgs
mock_client.get_user.return_value = mock_user
with patch(
@@ -81,6 +86,95 @@ class Test_Organization_Scoping:
assert orgs[1].name == "test-org1"
assert orgs[2].name == "test-org2"
def test_no_organizations_found_for_user(self):
"""Test that when user has no organizations, empty dict is returned"""
provider = set_mocked_github_provider()
provider.repositories = []
provider.organizations = []
mock_client = MagicMock()
mock_user = MagicMock()
mock_orgs = MagicMock()
mock_orgs.totalCount = 0
mock_user.get_orgs.return_value = mock_orgs
mock_client.get_user.return_value = mock_user
with patch(
"prowler.providers.github.services.organization.organization_service.GithubService.__init__"
):
organization_service = Organization(provider)
organization_service.clients = [mock_client]
organization_service.provider = provider
orgs = organization_service._list_organizations()
assert len(orgs) == 0
def test_github_app_organization_scoping(self):
"""Test GitHub App organization listing"""
from prowler.providers.github.models import GithubAppIdentityInfo
provider = set_mocked_github_provider()
provider.repositories = []
provider.organizations = []
# Set GitHub App identity
provider.identity = GithubAppIdentityInfo(
app_id="test-app-id",
app_name="test-app",
installations=["test-org1", "test-org2"],
)
mock_client = MagicMock()
mock_orgs = MagicMock()
mock_orgs.totalCount = 2
mock_orgs.__iter__ = MagicMock(
return_value=iter([self.mock_org1, self.mock_org2])
)
mock_client.get_organizations.return_value = mock_orgs
with patch(
"prowler.providers.github.services.organization.organization_service.GithubService.__init__"
):
organization_service = Organization(provider)
organization_service.clients = [mock_client]
organization_service.provider = provider
orgs = organization_service._list_organizations()
assert len(orgs) == 2
assert 1 in orgs
assert 2 in orgs
assert orgs[1].name == "test-org1"
assert orgs[2].name == "test-org2"
def test_github_app_no_organizations_found(self):
"""Test GitHub App when no organizations are accessible"""
from prowler.providers.github.models import GithubAppIdentityInfo
provider = set_mocked_github_provider()
provider.repositories = []
provider.organizations = []
# Set GitHub App identity
provider.identity = GithubAppIdentityInfo(
app_id="test-app-id", app_name="test-app", installations=[]
)
mock_client = MagicMock()
mock_orgs = MagicMock()
mock_orgs.totalCount = 0
mock_client.get_organizations.return_value = mock_orgs
with patch(
"prowler.providers.github.services.organization.organization_service.GithubService.__init__"
):
organization_service = Organization(provider)
organization_service.clients = [mock_client]
organization_service.provider = provider
orgs = organization_service._list_organizations()
assert len(orgs) == 0
def test_specific_organization_scoping(self):
"""Test that only specified organizations are returned"""
provider = set_mocked_github_provider()
+222 -99
View File
@@ -1,167 +1,290 @@
# IAC Provider Constants
DEFAULT_SCAN_PATH = "."
# Sample Checkov Output
SAMPLE_CHECKOV_OUTPUT = [
{
"check_type": "terraform",
"results": {
"failed_checks": [
# Sample Trivy Output
SAMPLE_TRIVY_OUTPUT = {
"Results": [
{
"Target": "main.tf",
"Type": "terraform",
"Misconfigurations": [
{
"check_id": "CKV_AWS_1",
"check_name": "Ensure S3 bucket has encryption enabled",
"guideline": "https://docs.bridgecrew.io/docs/s3_1-s3-bucket-has-encryption-enabled",
"severity": "low",
"ID": "AVD-AWS-0001",
"Title": "S3 bucket should have encryption enabled",
"Description": "S3 bucket should have encryption enabled",
"Message": "S3 bucket should have encryption enabled",
"Resolution": "Enable encryption on the S3 bucket",
"Severity": "LOW",
"PrimaryURL": "https://avd.aquasec.com/misconfig/aws/s3/avd-aws-0001",
"RuleID": "AVD-AWS-0001",
},
{
"check_id": "CKV_AWS_2",
"check_name": "Ensure S3 bucket has public access blocked",
"guideline": "https://docs.bridgecrew.io/docs/s3_2-s3-bucket-has-public-access-blocked",
"severity": "low",
"ID": "AVD-AWS-0002",
"Title": "S3 bucket should have public access blocked",
"Description": "S3 bucket should have public access blocked",
"Message": "S3 bucket should have public access blocked",
"Resolution": "Block public access on the S3 bucket",
"Severity": "LOW",
"PrimaryURL": "https://avd.aquasec.com/misconfig/aws/s3/avd-aws-0002",
"RuleID": "AVD-AWS-0002",
},
],
"passed_checks": [
"Vulnerabilities": [],
"Secrets": [],
"Licenses": [],
},
{
"Target": "main.tf",
"Type": "terraform",
"Misconfigurations": [
{
"check_id": "CKV_AWS_3",
"check_name": "Ensure S3 bucket has versioning enabled",
"guideline": "https://docs.bridgecrew.io/docs/s3_3-s3-bucket-has-versioning-enabled",
"severity": "low",
"ID": "AVD-AWS-0003",
"Title": "S3 bucket should have versioning enabled",
"Description": "S3 bucket should have versioning enabled",
"Message": "S3 bucket should have versioning enabled",
"Resolution": "Enable versioning on the S3 bucket",
"Severity": "LOW",
"PrimaryURL": "https://avd.aquasec.com/misconfig/aws/s3/avd-aws-0003",
"RuleID": "AVD-AWS-0003",
}
],
"Vulnerabilities": [],
"Secrets": [],
"Licenses": [],
},
}
]
]
}
# Sample Finding Data
SAMPLE_FINDING = SAMPLE_CHECKOV_OUTPUT[0]
SAMPLE_FINDING = SAMPLE_TRIVY_OUTPUT["Results"][0]
SAMPLE_FAILED_CHECK = {
"check_id": "CKV_AWS_1",
"check_name": "Ensure S3 bucket has encryption enabled",
"guideline": "https://docs.bridgecrew.io/docs/s3_1-s3-bucket-has-encryption-enabled",
"severity": "low",
"ID": "AVD-AWS-0001",
"Title": "S3 bucket should have encryption enabled",
"Description": "S3 bucket should have encryption enabled",
"Message": "S3 bucket should have encryption enabled",
"Resolution": "Enable encryption on the S3 bucket",
"Severity": "low",
"PrimaryURL": "https://avd.aquasec.com/misconfig/aws/s3/avd-aws-0001",
"RuleID": "AVD-AWS-0001",
}
SAMPLE_PASSED_CHECK = {
"check_id": "CKV_AWS_3",
"check_name": "Ensure S3 bucket has versioning enabled",
"guideline": "https://docs.bridgecrew.io/docs/s3_3-s3-bucket-has-versioning-enabled",
"severity": "low",
"ID": "AVD-AWS-0003",
"Title": "S3 bucket should have versioning enabled",
"Description": "S3 bucket should have versioning enabled",
"Message": "S3 bucket should have versioning enabled",
"Resolution": "Enable versioning on the S3 bucket",
"Severity": "low",
"PrimaryURL": "https://avd.aquasec.com/misconfig/aws/s3/avd-aws-0003",
"RuleID": "AVD-AWS-0003",
}
# Additional sample checks
SAMPLE_ANOTHER_FAILED_CHECK = {
"check_id": "CKV_AWS_4",
"check_name": "Ensure S3 bucket has logging enabled",
"guideline": "https://docs.bridgecrew.io/docs/s3_4-s3-bucket-has-logging-enabled",
"severity": "medium",
"ID": "AVD-AWS-0004",
"Title": "S3 bucket should have logging enabled",
"Description": "S3 bucket should have logging enabled",
"Message": "S3 bucket should have logging enabled",
"Resolution": "Enable logging on the S3 bucket",
"Severity": "medium",
"PrimaryURL": "https://avd.aquasec.com/misconfig/aws/s3/avd-aws-0004",
"RuleID": "AVD-AWS-0004",
}
SAMPLE_ANOTHER_PASSED_CHECK = {
"check_id": "CKV_AWS_5",
"check_name": "Ensure S3 bucket has lifecycle policy",
"guideline": "https://docs.bridgecrew.io/docs/s3_5-s3-bucket-has-lifecycle-policy",
"severity": "low",
"ID": "AVD-AWS-0005",
"Title": "S3 bucket should have lifecycle policy",
"Description": "S3 bucket should have lifecycle policy",
"Message": "S3 bucket should have lifecycle policy",
"Resolution": "Configure lifecycle policy on the S3 bucket",
"Severity": "low",
"PrimaryURL": "https://avd.aquasec.com/misconfig/aws/s3/avd-aws-0005",
"RuleID": "AVD-AWS-0005",
}
SAMPLE_ANOTHER_SKIPPED_CHECK = {
"check_id": "CKV_AWS_6",
"check_name": "Ensure S3 bucket has object lock enabled",
"guideline": "https://docs.bridgecrew.io/docs/s3_6-s3-bucket-has-object-lock-enabled",
"severity": "high",
"suppress_comment": "Not applicable for this use case",
"ID": "AVD-AWS-0006",
"Title": "S3 bucket should have object lock enabled",
"Description": "S3 bucket should have object lock enabled",
"Message": "S3 bucket should have object lock enabled",
"Resolution": "Enable object lock on the S3 bucket",
"Severity": "high",
"PrimaryURL": "https://avd.aquasec.com/misconfig/aws/s3/avd-aws-0006",
"RuleID": "AVD-AWS-0006",
"Status": "MUTED",
}
SAMPLE_SKIPPED_CHECK = {
"check_id": "CKV_AWS_7",
"check_name": "Ensure S3 bucket has server-side encryption",
"guideline": "https://docs.bridgecrew.io/docs/s3_7-s3-bucket-has-server-side-encryption",
"severity": "medium",
"suppress_comment": "Legacy bucket, will be migrated",
"ID": "AVD-AWS-0007",
"Title": "S3 bucket should have server-side encryption",
"Description": "S3 bucket should have server-side encryption",
"Message": "S3 bucket should have server-side encryption",
"Resolution": "Enable server-side encryption on the S3 bucket",
"Severity": "medium",
"PrimaryURL": "https://avd.aquasec.com/misconfig/aws/s3/avd-aws-0007",
"RuleID": "AVD-AWS-0007",
"Status": "MUTED",
}
SAMPLE_HIGH_SEVERITY_CHECK = {
"check_id": "CKV_AWS_8",
"check_name": "Ensure S3 bucket has public access blocked",
"guideline": "https://docs.bridgecrew.io/docs/s3_8-s3-bucket-has-public-access-blocked",
"severity": "high",
"ID": "AVD-AWS-0008",
"Title": "S3 bucket should have public access blocked",
"Description": "S3 bucket should have public access blocked",
"Message": "S3 bucket should have public access blocked",
"Resolution": "Block public access on the S3 bucket",
"Severity": "high",
"PrimaryURL": "https://avd.aquasec.com/misconfig/aws/s3/avd-aws-0008",
"RuleID": "AVD-AWS-0008",
}
# Dockerfile samples
SAMPLE_DOCKERFILE_REPORT = {
"check_type": "dockerfile",
"results": {
"failed_checks": [
{
"check_id": "CKV_DOCKER_1",
"check_name": "Ensure base image is not using latest tag",
"guideline": "https://docs.bridgecrew.io/docs/docker_1-base-image-not-using-latest-tag",
"severity": "medium",
}
],
"passed_checks": [],
},
"Target": "Dockerfile",
"Type": "dockerfile",
"Misconfigurations": [
{
"ID": "AVD-DOCKER-0001",
"Title": "Base image should not use latest tag",
"Description": "Base image should not use latest tag",
"Message": "Base image should not use latest tag",
"Resolution": "Use a specific version tag instead of latest",
"Severity": "medium",
"PrimaryURL": "https://avd.aquasec.com/misconfig/docker/dockerfile/avd-docker-0001",
"RuleID": "AVD-DOCKER-0001",
}
],
"Vulnerabilities": [],
"Secrets": [],
"Licenses": [],
}
SAMPLE_DOCKERFILE_CHECK = {
"check_id": "CKV_DOCKER_1",
"check_name": "Ensure base image is not using latest tag",
"guideline": "https://docs.bridgecrew.io/docs/docker_1-base-image-not-using-latest-tag",
"severity": "medium",
"ID": "AVD-DOCKER-0001",
"Title": "Base image should not use latest tag",
"Description": "Base image should not use latest tag",
"Message": "Base image should not use latest tag",
"Resolution": "Use a specific version tag instead of latest",
"Severity": "medium",
"PrimaryURL": "https://avd.aquasec.com/misconfig/docker/dockerfile/avd-docker-0001",
"RuleID": "AVD-DOCKER-0001",
}
# YAML samples
SAMPLE_YAML_REPORT = {
"check_type": "yaml",
"results": {
"failed_checks": [
{
"check_id": "CKV_K8S_1",
"check_name": "Ensure API server is not exposed",
"guideline": "https://docs.bridgecrew.io/docs/k8s_1-api-server-not-exposed",
"severity": "high",
}
],
"passed_checks": [],
},
"Target": "deployment.yaml",
"Type": "kubernetes",
"Misconfigurations": [
{
"ID": "AVD-K8S-0001",
"Title": "API server should not be exposed",
"Description": "API server should not be exposed",
"Message": "API server should not be exposed",
"Resolution": "Do not expose the API server",
"Severity": "high",
"PrimaryURL": "https://avd.aquasec.com/misconfig/kubernetes/avd-k8s-0001",
"RuleID": "AVD-K8S-0001",
}
],
"Vulnerabilities": [],
"Secrets": [],
"Licenses": [],
}
SAMPLE_YAML_CHECK = {
"check_id": "CKV_K8S_1",
"check_name": "Ensure API server is not exposed",
"guideline": "https://docs.bridgecrew.io/docs/k8s_1-api-server-not-exposed",
"severity": "high",
"ID": "AVD-K8S-0001",
"Title": "API server should not be exposed",
"Description": "API server should not be exposed",
"Message": "API server should not be exposed",
"Resolution": "Do not expose the API server",
"Severity": "high",
"PrimaryURL": "https://avd.aquasec.com/misconfig/kubernetes/avd-k8s-0001",
"RuleID": "AVD-K8S-0001",
}
# CloudFormation samples
SAMPLE_CLOUDFORMATION_CHECK = {
"check_id": "CKV_AWS_9",
"check_name": "Ensure CloudFormation stack has drift detection enabled",
"guideline": "https://docs.bridgecrew.io/docs/aws_9-cloudformation-stack-has-drift-detection-enabled",
"severity": "low",
"ID": "AVD-AWS-0009",
"Title": "CloudFormation stack should have drift detection enabled",
"Description": "CloudFormation stack should have drift detection enabled",
"Message": "CloudFormation stack should have drift detection enabled",
"Resolution": "Enable drift detection on the CloudFormation stack",
"Severity": "low",
"PrimaryURL": "https://avd.aquasec.com/misconfig/aws/cloudformation/avd-aws-0009",
"RuleID": "AVD-AWS-0009",
}
# Kubernetes samples
SAMPLE_KUBERNETES_CHECK = {
"check_id": "CKV_K8S_2",
"check_name": "Ensure RBAC is enabled",
"guideline": "https://docs.bridgecrew.io/docs/k8s_2-rbac-enabled",
"severity": "medium",
"ID": "AVD-K8S-0002",
"Title": "RBAC should be enabled",
"Description": "RBAC should be enabled",
"Message": "RBAC should be enabled",
"Resolution": "Enable RBAC on the cluster",
"Severity": "medium",
"PrimaryURL": "https://avd.aquasec.com/misconfig/kubernetes/avd-k8s-0002",
"RuleID": "AVD-K8S-0002",
}
# Sample Trivy output with vulnerabilities
SAMPLE_TRIVY_VULNERABILITY_OUTPUT = {
"Results": [
{
"Target": "package.json",
"Type": "nodejs",
"Misconfigurations": [],
"Vulnerabilities": [
{
"VulnerabilityID": "CVE-2023-1234",
"Title": "Example vulnerability",
"Description": "This is an example vulnerability",
"Severity": "high",
"PrimaryURL": "https://example.com/cve-2023-1234",
}
],
"Secrets": [],
"Licenses": [],
}
]
}
# Sample Trivy output with secrets
SAMPLE_TRIVY_SECRET_OUTPUT = {
"Results": [
{
"Target": "config.yaml",
"Class": "secret",
"Misconfigurations": [],
"Vulnerabilities": [],
"Secrets": [
{
"ID": "aws-access-key-id",
"Title": "AWS Access Key ID",
"Description": "AWS Access Key ID found in configuration",
"Severity": "critical",
"PrimaryURL": "https://example.com/secret-aws-access-key-id",
}
],
"Licenses": [],
}
]
}
def get_sample_checkov_json_output():
"""Return sample Checkov JSON output as string"""
def get_sample_trivy_json_output():
"""Return sample Trivy JSON output as string"""
import json
return json.dumps(SAMPLE_CHECKOV_OUTPUT)
return json.dumps(SAMPLE_TRIVY_OUTPUT)
def get_empty_checkov_output():
"""Return empty Checkov output as string"""
return "[]"
def get_empty_trivy_output():
"""Return empty Trivy output as string"""
import json
return json.dumps({"Results": []})
def get_invalid_checkov_output():
def get_invalid_trivy_output():
"""Return invalid JSON output as string"""
return "invalid json output"
+251 -186
View File
@@ -15,18 +15,15 @@ from tests.providers.iac.iac_fixtures import (
SAMPLE_ANOTHER_SKIPPED_CHECK,
SAMPLE_CLOUDFORMATION_CHECK,
SAMPLE_DOCKERFILE_CHECK,
SAMPLE_DOCKERFILE_REPORT,
SAMPLE_FAILED_CHECK,
SAMPLE_FINDING,
SAMPLE_HIGH_SEVERITY_CHECK,
SAMPLE_KUBERNETES_CHECK,
SAMPLE_PASSED_CHECK,
SAMPLE_SKIPPED_CHECK,
SAMPLE_YAML_CHECK,
SAMPLE_YAML_REPORT,
get_empty_checkov_output,
get_invalid_checkov_output,
get_sample_checkov_json_output,
get_empty_trivy_output,
get_invalid_trivy_output,
get_sample_trivy_json_output,
)
@@ -51,73 +48,85 @@ class TestIacProvider:
assert provider._type == "iac"
assert provider.scan_path == custom_path
def test_iac_provider_process_check_failed(self):
"""Test processing a failed check"""
def test_iac_provider_process_finding_failed(self):
"""Test processing a failed finding"""
provider = IacProvider()
report = provider._process_check(SAMPLE_FINDING, SAMPLE_FAILED_CHECK, "FAIL")
report = provider._process_finding(SAMPLE_FAILED_CHECK, "main.tf", "terraform")
assert isinstance(report, CheckReportIAC)
assert report.status == "FAIL"
assert report.check_metadata.Provider == "iac"
assert report.check_metadata.CheckID == SAMPLE_FAILED_CHECK["check_id"]
assert report.check_metadata.CheckTitle == SAMPLE_FAILED_CHECK["check_name"]
assert report.check_metadata.CheckID == SAMPLE_FAILED_CHECK["ID"]
assert report.check_metadata.CheckTitle == SAMPLE_FAILED_CHECK["Title"]
assert report.check_metadata.Severity == "low"
assert report.check_metadata.RelatedUrl == SAMPLE_FAILED_CHECK["guideline"]
assert report.check_metadata.RelatedUrl == SAMPLE_FAILED_CHECK["PrimaryURL"]
def test_iac_provider_process_check_passed(self):
"""Test processing a passed check"""
def test_iac_provider_process_finding_passed(self):
"""Test processing a passed finding"""
provider = IacProvider()
report = provider._process_check(SAMPLE_FINDING, SAMPLE_PASSED_CHECK, "PASS")
report = provider._process_finding(SAMPLE_PASSED_CHECK, "main.tf", "terraform")
assert isinstance(report, CheckReportIAC)
assert report.status == "PASS"
assert report.status == "FAIL" # Trivy findings are always FAIL by default
assert report.check_metadata.Provider == "iac"
assert report.check_metadata.CheckID == SAMPLE_PASSED_CHECK["check_id"]
assert report.check_metadata.CheckTitle == SAMPLE_PASSED_CHECK["check_name"]
assert report.check_metadata.CheckID == SAMPLE_PASSED_CHECK["ID"]
assert report.check_metadata.CheckTitle == SAMPLE_PASSED_CHECK["Title"]
assert report.check_metadata.Severity == "low"
@patch("subprocess.run")
def test_iac_provider_run_scan_success(self, mock_subprocess):
"""Test successful IAC scan with Checkov"""
"""Test successful IAC scan with Trivy"""
provider = IacProvider()
mock_subprocess.return_value = MagicMock(
stdout=get_sample_checkov_json_output(), stderr=""
stdout=get_sample_trivy_json_output(), stderr=""
)
reports = provider.run_scan("/test/directory", ["all"], [])
reports = provider.run_scan(
"/test/directory", ["vuln", "misconfig", "secret"], []
)
# Should have 2 failed checks + 1 passed check = 3 total reports
# Should have 3 misconfigurations from the sample output
assert len(reports) == 3
# Check that we have both failed and passed reports
# Check that we have failed reports (Trivy findings are always FAIL by default)
failed_reports = [r for r in reports if r.status == "FAIL"]
passed_reports = [r for r in reports if r.status == "PASS"]
assert len(failed_reports) == 2
assert len(passed_reports) == 1
assert len(failed_reports) == 3
# Verify subprocess was called correctly
mock_subprocess.assert_called_once_with(
["checkov", "-d", "/test/directory", "-o", "json", "-f", "all"],
[
"trivy",
"fs",
"/test/directory",
"--format",
"json",
"--scanners",
"vuln,misconfig,secret",
"--parallel",
"0",
"--include-non-failures",
],
capture_output=True,
text=True,
)
@patch("subprocess.run")
def test_iac_provider_run_scan_empty_output(self, mock_subprocess):
"""Test IAC scan with empty Checkov output"""
"""Test IAC scan with empty Trivy output"""
provider = IacProvider()
mock_subprocess.return_value = MagicMock(
stdout=get_empty_checkov_output(), stderr=""
stdout=get_empty_trivy_output(), stderr=""
)
reports = provider.run_scan("/test/directory", ["all"], [])
reports = provider.run_scan(
"/test/directory", ["vuln", "misconfig", "secret"], []
)
assert len(reports) == 0
def test_provider_run_local_scan(self):
@@ -127,7 +136,9 @@ class TestIacProvider:
"prowler.providers.iac.iac_provider.IacProvider.run_scan",
) as mock_run_scan:
provider.run()
mock_run_scan.assert_called_with(scan_path, ["all"], [])
mock_run_scan.assert_called_with(
scan_path, ["vuln", "misconfig", "secret"], []
)
@mock.patch.dict(os.environ, {}, clear=True)
def test_provider_run_remote_scan(self):
@@ -145,7 +156,9 @@ class TestIacProvider:
):
provider.run()
mock_clone.assert_called_with(scan_repository_url, None, None, None)
mock_run_scan.assert_called_with(temp_dir, ["all"], [])
mock_run_scan.assert_called_with(
temp_dir, ["vuln", "misconfig", "secret"], []
)
@mock.patch.dict(os.environ, {}, clear=True)
def test_print_credentials_local(self):
@@ -183,7 +196,7 @@ class TestIacProvider:
provider = IacProvider()
mock_subprocess.return_value = MagicMock(
stdout=get_invalid_checkov_output(), stderr=""
stdout=get_invalid_trivy_output(), stderr=""
)
with pytest.raises(SystemExit) as excinfo:
@@ -193,92 +206,102 @@ class TestIacProvider:
@patch("subprocess.run")
def test_iac_provider_run_scan_null_output(self, mock_subprocess):
"""Test IAC scan with null Checkov output"""
"""Test IAC scan with null Trivy output"""
provider = IacProvider()
mock_subprocess.return_value = MagicMock(stdout="null", stderr="")
reports = provider.run_scan("/test/directory", ["all"], [])
assert len(reports) == 0
with pytest.raises(SystemExit) as exc_info:
provider.run_scan("/test/directory", ["vuln", "misconfig", "secret"], [])
assert exc_info.value.code == 1
def test_iac_provider_process_check_dockerfile(self):
"""Test processing a Dockerfile check"""
def test_iac_provider_process_finding_dockerfile(self):
"""Test processing a Dockerfile finding"""
provider = IacProvider()
report = provider._process_check(
SAMPLE_DOCKERFILE_REPORT, SAMPLE_DOCKERFILE_CHECK, "FAIL"
report = provider._process_finding(
SAMPLE_DOCKERFILE_CHECK, "Dockerfile", "dockerfile"
)
assert isinstance(report, CheckReportIAC)
assert report.status == "FAIL"
assert report.check_metadata.ServiceName == "dockerfile"
assert report.check_metadata.CheckID == SAMPLE_DOCKERFILE_CHECK["check_id"]
assert report.check_metadata.CheckID == SAMPLE_DOCKERFILE_CHECK["ID"]
def test_iac_provider_process_check_yaml(self):
"""Test processing a YAML check"""
def test_iac_provider_process_finding_yaml(self):
"""Test processing a YAML finding"""
provider = IacProvider()
report = provider._process_check(SAMPLE_YAML_REPORT, SAMPLE_YAML_CHECK, "PASS")
report = provider._process_finding(
SAMPLE_YAML_CHECK, "deployment.yaml", "kubernetes"
)
assert isinstance(report, CheckReportIAC)
assert report.status == "PASS"
assert report.check_metadata.ServiceName == "yaml"
assert report.check_metadata.CheckID == SAMPLE_YAML_CHECK["check_id"]
assert report.status == "FAIL" # Trivy findings are always FAIL by default
assert report.check_metadata.ServiceName == "kubernetes"
assert report.check_metadata.CheckID == SAMPLE_YAML_CHECK["ID"]
@patch("subprocess.run")
def test_run_scan_success_with_failed_and_passed_checks(self, mock_subprocess):
"""Test successful run_scan with both failed and passed checks"""
provider = IacProvider()
# Create sample output with both failed and passed checks
sample_output = [
{
"check_type": "terraform",
"results": {
"failed_checks": [SAMPLE_FAILED_CHECK],
"passed_checks": [SAMPLE_PASSED_CHECK],
"skipped_checks": [],
},
}
]
# Create sample Trivy output with both failed and passed checks
sample_output = {
"Results": [
{
"Target": "main.tf",
"Type": "terraform",
"Misconfigurations": [SAMPLE_FAILED_CHECK, SAMPLE_PASSED_CHECK],
"Vulnerabilities": [],
"Secrets": [],
"Licenses": [],
}
]
}
mock_subprocess.return_value = MagicMock(
stdout=json.dumps(sample_output), stderr=""
)
result = provider.run_scan("/test/directory", ["terraform"], [])
result = provider.run_scan(
"/test/directory", ["vuln", "misconfig", "secret"], []
)
# Verify results
assert len(result) == 2
assert all(isinstance(report, CheckReportIAC) for report in result)
# Check that we have one FAIL and one PASS report
# Check that we have FAIL reports (Trivy findings are always FAIL by default)
statuses = [report.status for report in result]
assert "FAIL" in statuses
assert "PASS" in statuses
assert all(status == "FAIL" for status in statuses)
@patch("subprocess.run")
def test_run_scan_with_skipped_checks(self, mock_subprocess):
"""Test run_scan with skipped checks (muted)"""
provider = IacProvider()
# Create sample output with skipped checks
sample_output = [
{
"check_type": "terraform",
"results": {
"failed_checks": [],
"passed_checks": [],
"skipped_checks": [SAMPLE_SKIPPED_CHECK],
},
}
]
# Create sample Trivy output with skipped checks
sample_output = {
"Results": [
{
"Target": "main.tf",
"Type": "terraform",
"Misconfigurations": [SAMPLE_SKIPPED_CHECK],
"Vulnerabilities": [],
"Secrets": [],
"Licenses": [],
}
]
}
mock_subprocess.return_value = MagicMock(
stdout=json.dumps(sample_output), stderr=""
)
result = provider.run_scan("/test/directory", ["all"], ["exclude/path"])
result = provider.run_scan(
"/test/directory", ["vuln", "misconfig", "secret"], ["exclude/path"]
)
# Verify results
assert len(result) == 1
@@ -291,9 +314,13 @@ class TestIacProvider:
"""Test run_scan with no findings"""
provider = IacProvider()
mock_subprocess.return_value = MagicMock(stdout="[]", stderr="")
mock_subprocess.return_value = MagicMock(
stdout=json.dumps({"Results": []}), stderr=""
)
result = provider.run_scan("/test/directory", ["kubernetes"], [])
result = provider.run_scan(
"/test/directory", ["vuln", "misconfig", "secret"], []
)
# Verify results
assert len(result) == 0
@@ -303,40 +330,43 @@ class TestIacProvider:
"""Test run_scan with multiple reports from different frameworks"""
provider = IacProvider()
# Create sample output with multiple frameworks
sample_output = [
{
"check_type": "terraform",
"results": {
"failed_checks": [SAMPLE_FAILED_CHECK],
"passed_checks": [],
"skipped_checks": [],
# Create sample Trivy output with multiple frameworks
sample_output = {
"Results": [
{
"Target": "main.tf",
"Type": "terraform",
"Misconfigurations": [SAMPLE_FAILED_CHECK],
"Vulnerabilities": [],
"Secrets": [],
"Licenses": [],
},
},
{
"check_type": "kubernetes",
"results": {
"failed_checks": [],
"passed_checks": [SAMPLE_PASSED_CHECK],
"skipped_checks": [],
{
"Target": "deployment.yaml",
"Type": "kubernetes",
"Misconfigurations": [SAMPLE_PASSED_CHECK],
"Vulnerabilities": [],
"Secrets": [],
"Licenses": [],
},
},
]
]
}
mock_subprocess.return_value = MagicMock(
stdout=json.dumps(sample_output), stderr=""
)
result = provider.run_scan("/test/directory", ["terraform", "kubernetes"], [])
result = provider.run_scan(
"/test/directory", ["vuln", "misconfig", "secret"], []
)
# Verify results
assert len(result) == 2
assert all(isinstance(report, CheckReportIAC) for report in result)
# Check that we have one FAIL and one PASS report
# Check that we have FAIL reports (Trivy findings are always FAIL by default)
statuses = [report.status for report in result]
assert "FAIL" in statuses
assert "PASS" in statuses
assert all(status == "FAIL" for status in statuses)
@patch("subprocess.run")
def test_run_scan_exception_handling(self, mock_subprocess):
@@ -347,44 +377,49 @@ class TestIacProvider:
mock_subprocess.side_effect = Exception("Test exception")
with pytest.raises(SystemExit) as exc_info:
provider.run_scan("/test/directory", ["terraform"], [])
provider.run_scan("/test/directory", ["vuln", "misconfig", "secret"], [])
assert exc_info.value.code == 1
@patch("subprocess.run")
def test_run_scan_with_different_frameworks(self, mock_subprocess):
"""Test run_scan with different framework configurations"""
"""Test run_scan with different scanner configurations"""
provider = IacProvider()
sample_output = [
{
"check_type": "terraform",
"results": {
"failed_checks": [],
"passed_checks": [SAMPLE_PASSED_CHECK],
"skipped_checks": [],
},
}
]
sample_output = {
"Results": [
{
"Target": "main.tf",
"Type": "terraform",
"Misconfigurations": [SAMPLE_PASSED_CHECK],
"Vulnerabilities": [],
"Secrets": [],
"Licenses": [],
}
]
}
mock_subprocess.return_value = MagicMock(
stdout=json.dumps(sample_output), stderr=""
)
# Test with specific frameworks
frameworks = ["terraform", "kubernetes", "cloudformation"]
result = provider.run_scan("/test/directory", frameworks, [])
# Test with specific scanners
scanners = ["vuln", "misconfig", "secret"]
result = provider.run_scan("/test/directory", scanners, [])
# Verify subprocess was called with correct frameworks
# Verify subprocess was called with correct scanners
mock_subprocess.assert_called_once_with(
[
"checkov",
"-d",
"trivy",
"fs",
"/test/directory",
"-o",
"--format",
"json",
"-f",
",".join(frameworks),
"--scanners",
",".join(scanners),
"--parallel",
"0",
"--include-non-failures",
],
capture_output=True,
text=True,
@@ -392,23 +427,25 @@ class TestIacProvider:
# Verify results
assert len(result) == 1
assert result[0].status == "PASS"
assert result[0].status == "FAIL" # Trivy findings are always FAIL by default
@patch("subprocess.run")
def test_run_scan_with_exclude_paths(self, mock_subprocess):
"""Test run_scan with exclude paths"""
provider = IacProvider()
sample_output = [
{
"check_type": "terraform",
"results": {
"failed_checks": [],
"passed_checks": [SAMPLE_PASSED_CHECK],
"skipped_checks": [],
},
}
]
sample_output = {
"Results": [
{
"Target": "main.tf",
"Type": "terraform",
"Misconfigurations": [SAMPLE_PASSED_CHECK],
"Vulnerabilities": [],
"Secrets": [],
"Licenses": [],
}
]
}
mock_subprocess.return_value = MagicMock(
stdout=json.dumps(sample_output), stderr=""
@@ -416,18 +453,23 @@ class TestIacProvider:
# Test with exclude paths
exclude_paths = ["node_modules", ".git", "vendor"]
result = provider.run_scan("/test/directory", ["all"], exclude_paths)
result = provider.run_scan(
"/test/directory", ["vuln", "misconfig", "secret"], exclude_paths
)
# Verify subprocess was called with correct exclude paths
expected_command = [
"checkov",
"-d",
"trivy",
"fs",
"/test/directory",
"-o",
"--format",
"json",
"-f",
"all",
"--skip-path",
"--scanners",
"vuln,misconfig,secret",
"--parallel",
"0",
"--include-non-failures",
"--skip-dirs",
",".join(exclude_paths),
]
mock_subprocess.assert_called_once_with(
@@ -438,38 +480,47 @@ class TestIacProvider:
# Verify results
assert len(result) == 1
assert result[0].status == "PASS"
assert result[0].status == "FAIL" # Trivy findings are always FAIL by default
@patch("subprocess.run")
def test_run_scan_all_check_types(self, mock_subprocess):
"""Test run_scan with all types of checks (failed, passed, skipped)"""
provider = IacProvider()
sample_output = [
{
"check_type": "terraform",
"results": {
"failed_checks": [SAMPLE_FAILED_CHECK, SAMPLE_HIGH_SEVERITY_CHECK],
"passed_checks": [SAMPLE_PASSED_CHECK, SAMPLE_CLOUDFORMATION_CHECK],
"skipped_checks": [SAMPLE_SKIPPED_CHECK],
},
}
]
sample_output = {
"Results": [
{
"Target": "main.tf",
"Type": "terraform",
"Misconfigurations": [
SAMPLE_FAILED_CHECK,
SAMPLE_HIGH_SEVERITY_CHECK,
SAMPLE_PASSED_CHECK,
SAMPLE_CLOUDFORMATION_CHECK,
SAMPLE_SKIPPED_CHECK,
],
"Vulnerabilities": [],
"Secrets": [],
"Licenses": [],
}
]
}
mock_subprocess.return_value = MagicMock(
stdout=json.dumps(sample_output), stderr=""
)
result = provider.run_scan("/test/directory", ["all"], [])
result = provider.run_scan(
"/test/directory", ["vuln", "misconfig", "secret"], []
)
# Verify results
assert len(result) == 5 # 2 failed + 2 passed + 1 skipped
assert len(result) == 5 # 5 misconfigurations
# Check status distribution
statuses = [report.status for report in result]
assert statuses.count("FAIL") == 2
assert statuses.count("PASS") == 2
assert statuses.count("MUTED") == 1
assert statuses.count("FAIL") == 4 # 4 regular findings
assert statuses.count("MUTED") == 1 # 1 skipped finding
# Check that muted reports have muted=True
muted_reports = [report for report in result if report.status == "MUTED"]
@@ -481,9 +532,13 @@ class TestIacProvider:
provider = IacProvider()
# Return empty list of reports
mock_subprocess.return_value = MagicMock(stdout="[]", stderr="")
mock_subprocess.return_value = MagicMock(
stdout=json.dumps({"Results": []}), stderr=""
)
result = provider.run_scan("/test/directory", ["terraform"], [])
result = provider.run_scan(
"/test/directory", ["vuln", "misconfig", "secret"], []
)
# Verify results
assert len(result) == 0
@@ -493,60 +548,70 @@ class TestIacProvider:
"""Test run_scan with multiple frameworks and different types of checks"""
provider = IacProvider()
# Create sample output with multiple frameworks and different check types
sample_output = [
{
"check_type": "terraform",
"results": {
"failed_checks": [SAMPLE_FAILED_CHECK, SAMPLE_ANOTHER_FAILED_CHECK],
"passed_checks": [SAMPLE_PASSED_CHECK],
"skipped_checks": [],
# Create sample Trivy output with multiple frameworks and different check types
sample_output = {
"Results": [
{
"Target": "main.tf",
"Type": "terraform",
"Misconfigurations": [
SAMPLE_FAILED_CHECK,
SAMPLE_ANOTHER_FAILED_CHECK,
SAMPLE_PASSED_CHECK,
],
"Vulnerabilities": [],
"Secrets": [],
"Licenses": [],
},
},
{
"check_type": "kubernetes",
"results": {
"failed_checks": [SAMPLE_KUBERNETES_CHECK],
"passed_checks": [],
"skipped_checks": [SAMPLE_ANOTHER_SKIPPED_CHECK],
{
"Target": "deployment.yaml",
"Type": "kubernetes",
"Misconfigurations": [
SAMPLE_KUBERNETES_CHECK,
SAMPLE_ANOTHER_SKIPPED_CHECK,
],
"Vulnerabilities": [],
"Secrets": [],
"Licenses": [],
},
},
{
"check_type": "cloudformation",
"results": {
"failed_checks": [],
"passed_checks": [
{
"Target": "template.yaml",
"Type": "cloudformation",
"Misconfigurations": [
SAMPLE_CLOUDFORMATION_CHECK,
SAMPLE_ANOTHER_PASSED_CHECK,
],
"skipped_checks": [],
"Vulnerabilities": [],
"Secrets": [],
"Licenses": [],
},
},
]
]
}
mock_subprocess.return_value = MagicMock(
stdout=json.dumps(sample_output), stderr=""
)
result = provider.run_scan(
"/test/directory", ["terraform", "kubernetes", "cloudformation"], []
"/test/directory", ["vuln", "misconfig", "secret"], []
)
# Verify results
assert (
len(result) == 7
) # 2 failed + 1 passed (terraform) + 1 failed + 1 skipped (kubernetes) + 2 passed (cloudformation)
) # 3 terraform + 2 kubernetes + 2 cloudformation = 7 total
# Check status distribution
statuses = [report.status for report in result]
assert statuses.count("FAIL") == 3
assert statuses.count("PASS") == 3
assert statuses.count("MUTED") == 1
assert statuses.count("FAIL") == 6 # 6 regular findings
assert statuses.count("MUTED") == 1 # 1 skipped finding
def test_run_method_calls_run_scan(self):
"""Test that the run method calls run_scan with correct parameters"""
provider = IacProvider(
scan_path="/custom/path", frameworks=["terraform"], exclude_path=["exclude"]
scan_path="/custom/path",
scanners=["vuln", "misconfig"],
exclude_path=["exclude"],
)
with patch.object(provider, "run_scan") as mock_run_scan:
@@ -554,7 +619,7 @@ class TestIacProvider:
provider.run()
mock_run_scan.assert_called_once_with(
"/custom/path", ["terraform"], ["exclude"]
"/custom/path", ["vuln", "misconfig"], ["exclude"]
)
@mock.patch("prowler.providers.iac.iac_provider.porcelain.clone")

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