mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-05-20 03:02:43 +00:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9d8b69abda | |||
| 60aa601e92 | |||
| fc1fd538bd | |||
| 40c1761840 | |||
| 0ab0e8671d | |||
| 7a7c828fc7 | |||
| 5cbe473eb9 | |||
| caf2f61563 | |||
| 9dc4deccb6 | |||
| 476e7d1010 | |||
| cb01769237 | |||
| 4c802620c4 | |||
| 4fa8d5465e | |||
| 31b9619627 | |||
| d4a1bc10e9 | |||
| a1848747a3 |
@@ -145,7 +145,7 @@ SENTRY_RELEASE=local
|
||||
NEXT_PUBLIC_SENTRY_ENVIRONMENT=${SENTRY_ENVIRONMENT}
|
||||
|
||||
#### Prowler release version ####
|
||||
NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v5.27.0
|
||||
NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v5.28.0
|
||||
|
||||
# Social login credentials
|
||||
SOCIAL_GOOGLE_OAUTH_CALLBACK_URL="${AUTH_URL}/api/auth/callback/google"
|
||||
|
||||
@@ -122,6 +122,7 @@ jobs:
|
||||
github.com:443
|
||||
powershellinfraartifacts-gkhedzdeaghdezhr.z01.azurefd.net:443
|
||||
production.cloudflare.docker.com:443
|
||||
production.cloudfront.docker.com:443
|
||||
pypi.org:443
|
||||
registry-1.docker.io:443
|
||||
release-assets.githubusercontent.com:443
|
||||
@@ -132,11 +133,17 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Pin prowler SDK to latest master commit
|
||||
- name: Pin prowler SDK to latest master commit and refresh lockfile
|
||||
if: github.event_name == 'push'
|
||||
run: |
|
||||
set -e
|
||||
LATEST_SHA=$(git ls-remote https://github.com/prowler-cloud/prowler.git refs/heads/master | cut -f1)
|
||||
sed -i "s|prowler-cloud/prowler.git@master|prowler-cloud/prowler.git@${LATEST_SHA}|" api/pyproject.toml
|
||||
# Refresh api/uv.lock so it matches the pinned SHA above; the API
|
||||
# Dockerfile runs `uv sync --locked`, which aborts on any drift
|
||||
# between pyproject.toml and uv.lock.
|
||||
pip install --no-cache-dir "uv==0.11.14"
|
||||
(cd api && uv lock)
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
@@ -179,6 +186,7 @@ jobs:
|
||||
registry-1.docker.io:443
|
||||
auth.docker.io:443
|
||||
production.cloudflare.docker.com:443
|
||||
production.cloudfront.docker.com:443
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
with:
|
||||
|
||||
@@ -83,6 +83,7 @@ jobs:
|
||||
registry-1.docker.io:443
|
||||
auth.docker.io:443
|
||||
production.cloudflare.docker.com:443
|
||||
production.cloudfront.docker.com:443
|
||||
debian.map.fastlydns.net:80
|
||||
release-assets.githubusercontent.com:443
|
||||
objects.githubusercontent.com:443
|
||||
|
||||
@@ -139,6 +139,17 @@ jobs:
|
||||
sed -i "s|version = \"${CURRENT_API_VERSION}\"|version = \"${NEXT_API_VERSION}\"|" api/pyproject.toml
|
||||
sed -i "s| version: ${CURRENT_API_VERSION}| version: ${NEXT_API_VERSION}|" api/src/backend/api/specs/v1.yaml
|
||||
|
||||
- name: Regenerate lockfiles after version bump
|
||||
run: |
|
||||
set -e
|
||||
# The bumps above edit pyproject.toml / api/pyproject.toml but leave
|
||||
# uv.lock / api/uv.lock stale, which makes `uv sync --locked` fail in
|
||||
# the container builds. Refresh both with the uv version the images
|
||||
# pin (plain `uv lock`, no --upgrade: only the version line changes).
|
||||
pip install --no-cache-dir "uv==0.11.14"
|
||||
uv lock
|
||||
(cd api && uv lock)
|
||||
|
||||
- name: Bump UI version (.env)
|
||||
run: |
|
||||
set -e
|
||||
@@ -240,6 +251,17 @@ jobs:
|
||||
sed -i "s|version = \"${CURRENT_API_VERSION}\"|version = \"${FIRST_API_PATCH_VERSION}\"|" api/pyproject.toml
|
||||
sed -i "s| version: ${CURRENT_API_VERSION}| version: ${FIRST_API_PATCH_VERSION}|" api/src/backend/api/specs/v1.yaml
|
||||
|
||||
- name: Regenerate lockfiles after version bump
|
||||
run: |
|
||||
set -e
|
||||
# The bumps above edit pyproject.toml / api/pyproject.toml but leave
|
||||
# uv.lock / api/uv.lock stale, which makes `uv sync --locked` fail in
|
||||
# the container builds. Refresh both with the uv version the images
|
||||
# pin (plain `uv lock`, no --upgrade: only the version line changes).
|
||||
pip install --no-cache-dir "uv==0.11.14"
|
||||
uv lock
|
||||
(cd api && uv lock)
|
||||
|
||||
- name: Bump UI version (.env)
|
||||
run: |
|
||||
set -e
|
||||
@@ -341,6 +363,17 @@ jobs:
|
||||
sed -i "s|version = \"${CURRENT_API_VERSION}\"|version = \"${NEXT_API_PATCH_VERSION}\"|" api/pyproject.toml
|
||||
sed -i "s| version: ${CURRENT_API_VERSION}| version: ${NEXT_API_PATCH_VERSION}|" api/src/backend/api/specs/v1.yaml
|
||||
|
||||
- name: Regenerate lockfiles after version bump
|
||||
run: |
|
||||
set -e
|
||||
# The bumps above edit pyproject.toml / api/pyproject.toml but leave
|
||||
# uv.lock / api/uv.lock stale, which makes `uv sync --locked` fail in
|
||||
# the container builds. Refresh both with the uv version the images
|
||||
# pin (plain `uv lock`, no --upgrade: only the version line changes).
|
||||
pip install --no-cache-dir "uv==0.11.14"
|
||||
uv lock
|
||||
(cd api && uv lock)
|
||||
|
||||
- name: Bump UI version (.env)
|
||||
run: |
|
||||
set -e
|
||||
|
||||
Generated
+6
-6
@@ -66,7 +66,7 @@ jobs:
|
||||
title: ${{ steps.compute-text.outputs.title }}
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -135,7 +135,7 @@ jobs:
|
||||
secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }}
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -870,7 +870,7 @@ jobs:
|
||||
total_count: ${{ steps.missing_tool.outputs.total_count }}
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -982,7 +982,7 @@ jobs:
|
||||
success: ${{ steps.parse_results.outputs.success }}
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -1091,7 +1091,7 @@ jobs:
|
||||
activated: ${{ (steps.check_membership.outputs.is_team_member == 'true') && (steps.check_rate_limit.outputs.rate_limit_ok == 'true') }}
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -1164,7 +1164,7 @@ jobs:
|
||||
process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }}
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2.19.3
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
name: 'Docs: Markdown Lint'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
- 'v5.*'
|
||||
pull_request:
|
||||
branches:
|
||||
- 'master'
|
||||
- 'v5.*'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
markdown-lint:
|
||||
if: github.repository == 'prowler-cloud/prowler'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0
|
||||
with:
|
||||
egress-policy: block
|
||||
allowed-endpoints: >
|
||||
api.github.com:443
|
||||
github.com:443
|
||||
registry.npmjs.org:443
|
||||
release-assets.githubusercontent.com:443
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: ui/.nvmrc
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
||||
with:
|
||||
package_json_file: ui/package.json
|
||||
run_install: false
|
||||
|
||||
- name: Run markdownlint
|
||||
# Pin must match .pre-commit-config.yaml so prek and CI behave identically.
|
||||
# pnpm dlx doesn't accept --ignore-scripts as a flag; the env var
|
||||
# disables postinstall scripts on transitives the same way.
|
||||
env:
|
||||
pnpm_config_ignore_scripts: 'true'
|
||||
run: pnpm dlx markdownlint-cli@0.45.0 '**/*.md'
|
||||
@@ -114,6 +114,7 @@ jobs:
|
||||
registry-1.docker.io:443
|
||||
auth.docker.io:443
|
||||
production.cloudflare.docker.com:443
|
||||
production.cloudfront.docker.com:443
|
||||
ghcr.io:443
|
||||
pkg-containers.githubusercontent.com:443
|
||||
files.pythonhosted.org:443
|
||||
@@ -171,6 +172,7 @@ jobs:
|
||||
registry-1.docker.io:443
|
||||
auth.docker.io:443
|
||||
production.cloudflare.docker.com:443
|
||||
production.cloudfront.docker.com:443
|
||||
github.com:443
|
||||
release-assets.githubusercontent.com:443
|
||||
|
||||
|
||||
@@ -79,6 +79,7 @@ jobs:
|
||||
registry-1.docker.io:443
|
||||
auth.docker.io:443
|
||||
production.cloudflare.docker.com:443
|
||||
production.cloudfront.docker.com:443
|
||||
ghcr.io:443
|
||||
pkg-containers.githubusercontent.com:443
|
||||
files.pythonhosted.org:443
|
||||
|
||||
@@ -149,6 +149,7 @@ jobs:
|
||||
public.ecr.aws:443
|
||||
registry-1.docker.io:443
|
||||
production.cloudflare.docker.com:443
|
||||
production.cloudfront.docker.com:443
|
||||
auth.docker.io:443
|
||||
debian.map.fastlydns.net:80
|
||||
github.com:443
|
||||
@@ -216,6 +217,7 @@ jobs:
|
||||
auth.docker.io:443
|
||||
public.ecr.aws:443
|
||||
production.cloudflare.docker.com:443
|
||||
production.cloudfront.docker.com:443
|
||||
github.com:443
|
||||
release-assets.githubusercontent.com:443
|
||||
api.ecr-public.us-east-1.amazonaws.com:443
|
||||
|
||||
@@ -85,6 +85,7 @@ jobs:
|
||||
registry-1.docker.io:443
|
||||
auth.docker.io:443
|
||||
production.cloudflare.docker.com:443
|
||||
production.cloudfront.docker.com:443
|
||||
api.github.com:443
|
||||
mirror.gcr.io:443
|
||||
check.trivy.dev:443
|
||||
|
||||
@@ -46,6 +46,7 @@ jobs:
|
||||
schema.ocsf.io:443
|
||||
registry-1.docker.io:443
|
||||
production.cloudflare.docker.com:443
|
||||
production.cloudfront.docker.com:443
|
||||
powershellinfraartifacts-gkhedzdeaghdezhr.z01.azurefd.net:443
|
||||
o26192.ingest.us.sentry.io:443
|
||||
management.azure.com:443
|
||||
|
||||
@@ -116,6 +116,7 @@ jobs:
|
||||
allowed-endpoints: >
|
||||
registry-1.docker.io:443
|
||||
production.cloudflare.docker.com:443
|
||||
production.cloudfront.docker.com:443
|
||||
auth.docker.io:443
|
||||
registry.npmjs.org:443
|
||||
dl-cdn.alpinelinux.org:443
|
||||
@@ -172,6 +173,7 @@ jobs:
|
||||
registry-1.docker.io:443
|
||||
auth.docker.io:443
|
||||
production.cloudflare.docker.com:443
|
||||
production.cloudfront.docker.com:443
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
|
||||
@@ -80,6 +80,7 @@ jobs:
|
||||
registry-1.docker.io:443
|
||||
auth.docker.io:443
|
||||
production.cloudflare.docker.com:443
|
||||
production.cloudfront.docker.com:443
|
||||
registry.npmjs.org:443
|
||||
dl-cdn.alpinelinux.org:443
|
||||
fonts.googleapis.com:443
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"extends": "markdownlint/style/prettier",
|
||||
"first-line-h1": false,
|
||||
"no-duplicate-heading": {
|
||||
"siblings_only": true
|
||||
},
|
||||
"no-inline-html": false,
|
||||
"line-length": false,
|
||||
"no-bare-urls": false
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
node_modules/
|
||||
ui/node_modules/
|
||||
.git/
|
||||
.venv/
|
||||
**/.venv/
|
||||
dist/
|
||||
build/
|
||||
htmlcov/
|
||||
.next/
|
||||
ui/.next/
|
||||
ui/out/
|
||||
contrib/
|
||||
|
||||
# Auto-generated content (keepachangelog format legitimately repeats section headings).
|
||||
# Revisit with the team — see beads task on markdownlint rule triage.
|
||||
**/CHANGELOG.md
|
||||
@@ -125,13 +125,6 @@ repos:
|
||||
pass_filenames: false
|
||||
priority: 50
|
||||
|
||||
## MARKDOWN
|
||||
- repo: https://github.com/igorshubovych/markdownlint-cli
|
||||
rev: v0.45.0
|
||||
hooks:
|
||||
- id: markdownlint
|
||||
priority: 30
|
||||
|
||||
## CONTAINERS
|
||||
- repo: https://github.com/hadolint/hadolint
|
||||
rev: v2.14.0
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
Use these skills for detailed patterns on-demand:
|
||||
|
||||
### Generic Skills (Any Project)
|
||||
|
||||
| Skill | Description | URL |
|
||||
|-------|-------------|-----|
|
||||
| `typescript` | Const types, flat interfaces, utility types | [SKILL.md](skills/typescript/SKILL.md) |
|
||||
@@ -29,7 +28,6 @@ Use these skills for detailed patterns on-demand:
|
||||
| `tdd` | Test-Driven Development workflow | [SKILL.md](skills/tdd/SKILL.md) |
|
||||
|
||||
### Prowler-Specific Skills
|
||||
|
||||
| Skill | Description | URL |
|
||||
|-------|-------------|-----|
|
||||
| `prowler` | Project overview, component navigation | [SKILL.md](skills/prowler/SKILL.md) |
|
||||
|
||||
+3
-4
@@ -1,4 +1,4 @@
|
||||
# Do you want to learn on how to
|
||||
# Do you want to learn on how to...
|
||||
|
||||
- [Contribute with your code or fixes to Prowler](https://docs.prowler.com/developer-guide/introduction)
|
||||
- [Create a new provider](https://docs.prowler.com/developer-guide/provider)
|
||||
@@ -32,6 +32,5 @@ Provider-specific developer notes:
|
||||
|
||||
Want some swag as appreciation for your contribution?
|
||||
|
||||
## Prowler Developer Guide
|
||||
|
||||
<https://goto.prowler.com/devguide>
|
||||
# Prowler Developer Guide
|
||||
https://goto.prowler.com/devguide
|
||||
|
||||
+6
-6
@@ -76,11 +76,11 @@ USER prowler
|
||||
WORKDIR /home/prowler
|
||||
|
||||
# Copy necessary files
|
||||
COPY prowler/ /home/prowler/prowler/
|
||||
COPY dashboard/ /home/prowler/dashboard/
|
||||
COPY pyproject.toml uv.lock /home/prowler/
|
||||
COPY README.md /home/prowler/
|
||||
COPY prowler/providers/m365/lib/powershell/m365_powershell.py /home/prowler/prowler/providers/m365/lib/powershell/m365_powershell.py
|
||||
COPY --chown=prowler:prowler prowler/ /home/prowler/prowler/
|
||||
COPY --chown=prowler:prowler dashboard/ /home/prowler/dashboard/
|
||||
COPY --chown=prowler:prowler pyproject.toml uv.lock /home/prowler/
|
||||
COPY --chown=prowler:prowler README.md /home/prowler/
|
||||
COPY --chown=prowler:prowler prowler/providers/m365/lib/powershell/m365_powershell.py /home/prowler/prowler/providers/m365/lib/powershell/m365_powershell.py
|
||||
|
||||
# Install Python dependencies
|
||||
ENV HOME='/home/prowler'
|
||||
@@ -89,7 +89,7 @@ ENV PATH="${HOME}/.local/bin:${PATH}"
|
||||
RUN pip install --no-cache-dir --upgrade pip && \
|
||||
pip install --no-cache-dir uv==0.11.14
|
||||
|
||||
RUN uv sync --compile-bytecode && \
|
||||
RUN uv sync --locked --compile-bytecode && \
|
||||
rm -rf ~/.cache/uv
|
||||
|
||||
# Install PowerShell modules
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<p align="center">
|
||||
<img align="center" alt="Prowler logo" src="https://github.com/prowler-cloud/prowler/blob/master/docs/img/prowler-logo-black.png#gh-light-mode-only" width="50%" height="50%">
|
||||
<img align="center" alt="Prowler logo" src="https://github.com/prowler-cloud/prowler/blob/master/docs/img/prowler-logo-white.png#gh-dark-mode-only" width="50%" height="50%">
|
||||
<img align="center" src="https://github.com/prowler-cloud/prowler/blob/master/docs/img/prowler-logo-black.png#gh-light-mode-only" width="50%" height="50%">
|
||||
<img align="center" src="https://github.com/prowler-cloud/prowler/blob/master/docs/img/prowler-logo-white.png#gh-dark-mode-only" width="50%" height="50%">
|
||||
</p>
|
||||
<p align="center">
|
||||
<b><i>Prowler</b> is the Open 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.
|
||||
@@ -22,8 +22,8 @@
|
||||
<a href="https://pypistats.org/packages/prowler"><img alt="PyPI Downloads" src="https://img.shields.io/pypi/dw/prowler.svg?label=downloads"></a>
|
||||
<a href="https://hub.docker.com/r/toniblyx/prowler"><img alt="Docker Pulls" src="https://img.shields.io/docker/pulls/toniblyx/prowler"></a>
|
||||
<a href="https://gallery.ecr.aws/prowler-cloud/prowler"><img width="120" height=19" alt="AWS ECR Gallery" src="https://user-images.githubusercontent.com/3985464/151531396-b6535a68-c907-44eb-95a1-a09508178616.png"></a>
|
||||
<a href="https://codecov.io/gh/prowler-cloud/prowler"><img alt="Codecov coverage" src="https://codecov.io/gh/prowler-cloud/prowler/graph/badge.svg?token=OflBGsdpDl"/></a>
|
||||
<a href="https://insights.linuxfoundation.org/project/prowler-cloud-prowler"><img alt="Linux Foundation insights health score" src="https://insights.linuxfoundation.org/api/badge/health-score?project=prowler-cloud-prowler"/></a>
|
||||
<a href="https://codecov.io/gh/prowler-cloud/prowler"><img src="https://codecov.io/gh/prowler-cloud/prowler/graph/badge.svg?token=OflBGsdpDl"/></a>
|
||||
<a href="https://insights.linuxfoundation.org/project/prowler-cloud-prowler"><img src="https://insights.linuxfoundation.org/api/badge/health-score?project=prowler-cloud-prowler"/></a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://github.com/prowler-cloud/prowler/releases"><img alt="Version" src="https://img.shields.io/github/v/release/prowler-cloud/prowler"></a>
|
||||
@@ -36,7 +36,7 @@
|
||||
</p>
|
||||
<hr>
|
||||
<p align="center">
|
||||
<img align="center" alt="Prowler Cloud demo" src="/docs/img/prowler-cloud.gif" width="100%" height="100%">
|
||||
<img align="center" src="/docs/img/prowler-cloud.gif" width="100%" height="100%">
|
||||
</p>
|
||||
|
||||
# Description
|
||||
@@ -104,22 +104,22 @@ Every AWS provider scan will enqueue an Attack Paths ingestion job automatically
|
||||
|
||||
| Provider | Checks | Services | [Compliance Frameworks](https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/compliance/) | [Categories](https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/misc/#categories) | Support | Interface |
|
||||
|---|---|---|---|---|---|---|
|
||||
| AWS | 595 | 84 | 43 | 17 | Official | UI, API, CLI |
|
||||
| AWS | 600 | 84 | 44 | 18 | Official | UI, API, CLI |
|
||||
| Azure | 167 | 22 | 19 | 16 | Official | UI, API, CLI |
|
||||
| GCP | 102 | 18 | 17 | 12 | Official | UI, API, CLI |
|
||||
| Kubernetes | 83 | 7 | 7 | 11 | Official | UI, API, CLI |
|
||||
| GitHub | 24 | 3 | 1 | 5 | Official | UI, API, CLI |
|
||||
| M365 | 101 | 10 | 4 | 10 | Official | UI, API, CLI |
|
||||
| M365 | 102 | 10 | 4 | 10 | Official | UI, API, CLI |
|
||||
| OCI | 51 | 14 | 4 | 10 | Official | UI, API, CLI |
|
||||
| Alibaba Cloud | 61 | 9 | 4 | 9 | Official | UI, API, CLI |
|
||||
| Alibaba Cloud | 63 | 9 | 4 | 9 | Official | UI, API, CLI |
|
||||
| Cloudflare | 29 | 3 | 0 | 5 | Official | UI, API, CLI |
|
||||
| IaC | [See `trivy` docs.](https://trivy.dev/latest/docs/coverage/iac/) | N/A | N/A | N/A | Official | UI, API, CLI |
|
||||
| MongoDB Atlas | 10 | 3 | 0 | 8 | Official | UI, API, CLI |
|
||||
| LLM | [See `promptfoo` docs.](https://www.promptfoo.dev/docs/red-team/plugins/) | N/A | N/A | N/A | Official | CLI |
|
||||
| Image | N/A | N/A | N/A | N/A | Official | CLI, API |
|
||||
| Google Workspace | 25 | 4 | 2 | 4 | Official | UI, API, CLI |
|
||||
| Google Workspace | 39 | 5 | 2 | 5 | Official | UI, API, CLI |
|
||||
| OpenStack | 34 | 5 | 0 | 9 | Official | UI, API, CLI |
|
||||
| Vercel | 26 | 6 | 0 | 5 | Official | UI, API, CLI |
|
||||
| Vercel | 26 | 6 | 0 | 8 | Official | UI, API, CLI |
|
||||
| Okta | 1 | 1 | 0 | 1 | Official | CLI |
|
||||
| Scaleway [Contact us](https://prowler.com/contact) | 1 | 1 | 0 | 1 | Unofficial | CLI |
|
||||
| NHN | 6 | 2 | 1 | 0 | Unofficial | CLI |
|
||||
@@ -146,11 +146,11 @@ Prowler App offers flexible installation methods tailored to various environment
|
||||
|
||||
### Docker Compose
|
||||
|
||||
#### Requirements
|
||||
**Requirements**
|
||||
|
||||
- `Docker Compose` installed: https://docs.docker.com/compose/install/.
|
||||
* `Docker Compose` installed: https://docs.docker.com/compose/install/.
|
||||
|
||||
#### Commands
|
||||
**Commands**
|
||||
|
||||
``` console
|
||||
VERSION=$(curl -s https://api.github.com/repos/prowler-cloud/prowler/releases/latest | jq -r .tag_name)
|
||||
@@ -175,14 +175,14 @@ You can find more information in the [Troubleshooting](./docs/troubleshooting.md
|
||||
|
||||
### From GitHub
|
||||
|
||||
#### Requirements
|
||||
**Requirements**
|
||||
|
||||
- `git` installed.
|
||||
- `uv` installed: [uv installation](https://docs.astral.sh/uv/getting-started/installation/).
|
||||
- `pnpm` installed: [pnpm installation](https://pnpm.io/installation).
|
||||
- `Docker Compose` installed: https://docs.docker.com/compose/install/.
|
||||
* `git` installed.
|
||||
* `uv` installed: [uv installation](https://docs.astral.sh/uv/getting-started/installation/).
|
||||
* `pnpm` installed: [pnpm installation](https://pnpm.io/installation).
|
||||
* `Docker Compose` installed: https://docs.docker.com/compose/install/.
|
||||
|
||||
#### Commands to run the API
|
||||
**Commands to run the API**
|
||||
|
||||
``` console
|
||||
git clone https://github.com/prowler-cloud/prowler
|
||||
@@ -199,7 +199,7 @@ gunicorn -c config/guniconf.py config.wsgi:application
|
||||
|
||||
> After completing the setup, access the API documentation at http://localhost:8080/api/v1/docs.
|
||||
|
||||
#### Commands to run the API Worker
|
||||
**Commands to run the API Worker**
|
||||
|
||||
``` console
|
||||
git clone https://github.com/prowler-cloud/prowler
|
||||
@@ -212,7 +212,7 @@ cd src/backend
|
||||
python -m celery -A config.celery worker -l info -E
|
||||
```
|
||||
|
||||
#### Commands to run the API Scheduler
|
||||
**Commands to run the API Scheduler**
|
||||
|
||||
``` console
|
||||
git clone https://github.com/prowler-cloud/prowler
|
||||
@@ -225,7 +225,7 @@ cd src/backend
|
||||
python -m celery -A config.celery beat -l info --scheduler django_celery_beat.schedulers:DatabaseScheduler
|
||||
```
|
||||
|
||||
#### Commands to run the UI
|
||||
**Commands to run the UI**
|
||||
|
||||
``` console
|
||||
git clone https://github.com/prowler-cloud/prowler
|
||||
@@ -237,7 +237,7 @@ pnpm start
|
||||
|
||||
> Once configured, access the Prowler App at http://localhost:3000. Sign up using your email and password to get started.
|
||||
|
||||
#### Pre-commit Hooks Setup
|
||||
**Pre-commit Hooks Setup**
|
||||
|
||||
Some pre-commit hooks require tools installed on your system:
|
||||
|
||||
@@ -257,14 +257,14 @@ prowler -v
|
||||
|
||||
### Containers
|
||||
|
||||
#### Available Versions of Prowler CLI
|
||||
**Available Versions of Prowler CLI**
|
||||
|
||||
The following versions of Prowler CLI are available, depending on your requirements:
|
||||
|
||||
- `latest`: Synchronizes with the `master` branch. Note that this version is not stable.
|
||||
- `v4-latest`: Synchronizes with the `v4` branch. Note that this version is not stable.
|
||||
- `v3-latest`: Synchronizes with the `v3` branch. Note that this version is not stable.
|
||||
- `<x.y.z>` (release): Stable releases corresponding to specific versions. See the [complete list of Prowler releases](https://github.com/prowler-cloud/prowler/releases).
|
||||
- `<x.y.z>` (release): Stable releases corresponding to specific versions. You can find the complete list of releases [here](https://github.com/prowler-cloud/prowler/releases).
|
||||
- `stable`: Always points to the latest release.
|
||||
- `v4-stable`: Always points to the latest release for v4.
|
||||
- `v3-stable`: Always points to the latest release for v3.
|
||||
@@ -338,7 +338,7 @@ Full configuration, per-provider authentication, and SARIF examples: [Prowler Gi
|
||||
|
||||
## Prowler CLI
|
||||
|
||||
### Running Prowler
|
||||
**Running Prowler**
|
||||
|
||||
Prowler can be executed across various environments, offering flexibility to meet your needs. It can be run from:
|
||||
|
||||
|
||||
+4
-4
@@ -10,7 +10,7 @@
|
||||
> - [`jsonapi`](../skills/jsonapi/SKILL.md) - Strict JSON:API v1.1 spec compliance
|
||||
> - [`pytest`](../skills/pytest/SKILL.md) - Generic pytest patterns
|
||||
|
||||
## Auto-invoke Skills
|
||||
### Auto-invoke Skills
|
||||
|
||||
When performing these actions, ALWAYS invoke the corresponding skill FIRST:
|
||||
|
||||
@@ -81,7 +81,7 @@ When performing these actions, ALWAYS invoke the corresponding skill FIRST:
|
||||
## DECISION TREES
|
||||
|
||||
### Serializer Selection
|
||||
```text
|
||||
```
|
||||
Read → <Model>Serializer
|
||||
Create → <Model>CreateSerializer
|
||||
Update → <Model>UpdateSerializer
|
||||
@@ -89,7 +89,7 @@ Nested read → <Model>IncludeSerializer
|
||||
```
|
||||
|
||||
### Task vs View
|
||||
```text
|
||||
```
|
||||
< 100ms → View
|
||||
> 100ms or external API → Celery task
|
||||
Needs retry → Celery task
|
||||
@@ -105,7 +105,7 @@ Django 5.1.x | DRF 3.15.x | djangorestframework-jsonapi 7.x | Celery 5.4.x | Pos
|
||||
|
||||
## PROJECT STRUCTURE
|
||||
|
||||
```text
|
||||
```
|
||||
api/src/backend/
|
||||
├── api/ # Main Django app
|
||||
│ ├── v1/ # API version 1 (views, serializers, urls)
|
||||
|
||||
+5
-11
@@ -2,30 +2,24 @@
|
||||
|
||||
All notable changes to the **Prowler API** are documented in this file.
|
||||
|
||||
## [1.28.0] (Prowler UNRELEASED)
|
||||
## [1.28.0] (Prowler v5.27.0)
|
||||
|
||||
### 🚀 Added
|
||||
|
||||
- GIN index on `findings(categories, resource_services, resource_regions, resource_types)` to speed up `/api/v1/finding-groups` array filters [(#11001)](https://github.com/prowler-cloud/prowler/pull/11001)
|
||||
- `GET /health/live` and `GET /health/ready` Kubernetes-style probe endpoints following the IETF Health Check Response Format (`application/health+json`). Readiness verifies PostgreSQL, Valkey and Neo4j connectivity and returns 503 with per-dependency detail when any is unreachable; both endpoints centralize the API version on `config/version.py` (read from `pyproject.toml`) and are wired into the Helm charts and the Docker Compose healthcheck [(#11200)](https://github.com/prowler-cloud/prowler/pull/11200)
|
||||
- `GET /health/live` and `GET /health/ready` Kubernetes-style probe endpoints following the IETF Health Check Response Format (`application/health+json`). Readiness verifies PostgreSQL, Valkey and Neo4j connectivity and returns 503 with per-dependency detail when any is unreachable [(#11200)](https://github.com/prowler-cloud/prowler/pull/11200)
|
||||
|
||||
### 🔄 Changed
|
||||
|
||||
- Replace `poetry` with `uv` (`0.11.14`) as the API package manager; migrate `pyproject.toml` to `[dependency-groups]` and regenerate as `uv.lock` [(#10775)](https://github.com/prowler-cloud/prowler/pull/10775)
|
||||
- Replace `poetry` with `uv` as package manager [(#10775)](https://github.com/prowler-cloud/prowler/pull/10775)
|
||||
- Remove orphaned `gin_resources_search_idx` declaration from `Resource.Meta.indexes` (DB index dropped in `0072_drop_unused_indexes`) [(#11001)](https://github.com/prowler-cloud/prowler/pull/11001)
|
||||
- PDF compliance reports cap detail tables at 100 failed findings per check (configurable via `DJANGO_PDF_MAX_FINDINGS_PER_CHECK`) to bound worker memory on large scans [(#11160)](https://github.com/prowler-cloud/prowler/pull/11160)
|
||||
|
||||
### 🐞 Fixed
|
||||
|
||||
- `perform_scan_task` and `perform_scheduled_scan_task` now short-circuit with a warning and `return None` when the target provider no longer exists, instead of letting `handle_provider_deletion` raise `ProviderDeletedException`. `perform_scheduled_scan_task` also removes any orphan `PeriodicTask` it finds so beat stops re-firing scans for deleted providers. Prevents queued messages for deleted providers from being recorded as `FAILURE` and, in one-shot scan-worker deployments, from burning a fresh container per redelivery [(#11185)](https://github.com/prowler-cloud/prowler/pull/11185)
|
||||
- `perform_scan_task` and `perform_scheduled_scan_task` now short-circuit with a warning and `return None` when the target provider no longer exists, instead of letting `handle_provider_deletion` raise `ProviderDeletedException`. `perform_scheduled_scan_task` also removes any orphan `PeriodicTask` it finds so beat stops re-firing scans for deleted providers. Prevents queued messages for deleted providers from being recorded as `FAILURE` [(#11185)](https://github.com/prowler-cloud/prowler/pull/11185)
|
||||
- Attack Paths: `BEDROCK-001` and `BEDROCK-002` now target roles trusting `bedrock-agentcore.amazonaws.com` instead of `bedrock.amazonaws.com`, eliminating false positives against regular Bedrock service roles (Agents, Knowledge Bases, model invocation) [(#11141)](https://github.com/prowler-cloud/prowler/pull/11141)
|
||||
|
||||
---
|
||||
|
||||
## [1.27.2] (Prowler UNRELEASED)
|
||||
|
||||
### 🐞 Fixed
|
||||
|
||||
- Attack Paths: BEDROCK-001 and BEDROCK-002 now target roles trusting `bedrock-agentcore.amazonaws.com` instead of `bedrock.amazonaws.com`, eliminating false positives against regular Bedrock service roles (Agents, Knowledge Bases, model invocation) [(#11141)](https://github.com/prowler-cloud/prowler/pull/11141)
|
||||
|
||||
---
|
||||
|
||||
|
||||
+2
-2
@@ -89,7 +89,7 @@ WORKDIR /home/prowler
|
||||
# Ensure output directory exists
|
||||
RUN mkdir -p /tmp/prowler_api_output
|
||||
|
||||
COPY pyproject.toml uv.lock ./
|
||||
COPY --chown=prowler:prowler pyproject.toml uv.lock ./
|
||||
|
||||
RUN pip install --no-cache-dir --upgrade pip && \
|
||||
pip install --no-cache-dir uv==0.11.14
|
||||
@@ -97,7 +97,7 @@ RUN pip install --no-cache-dir --upgrade pip && \
|
||||
ENV PATH="/home/prowler/.local/bin:$PATH"
|
||||
|
||||
# Add `--no-install-project` to avoid installing the current project as a package
|
||||
RUN uv sync --no-install-project && \
|
||||
RUN uv sync --locked --no-install-project && \
|
||||
rm -rf ~/.cache/uv
|
||||
|
||||
RUN .venv/bin/python .venv/lib/python3.12/site-packages/prowler/providers/m365/lib/powershell/m365_powershell.py
|
||||
|
||||
+29
-29
@@ -2,7 +2,7 @@
|
||||
|
||||
This repository contains the JSON API and Task Runner components for Prowler, which facilitate a complete backend that interacts with the Prowler SDK and is used by the Prowler UI.
|
||||
|
||||
## Components
|
||||
# Components
|
||||
The Prowler API is composed of the following components:
|
||||
|
||||
- The JSON API, which is an API built with Django Rest Framework.
|
||||
@@ -10,13 +10,13 @@ The Prowler API is composed of the following components:
|
||||
- The PostgreSQL database, which is used to store the data.
|
||||
- The Valkey database, which is an in-memory database which is used as a message broker for the Celery workers.
|
||||
|
||||
### Note about Valkey
|
||||
## Note about Valkey
|
||||
|
||||
[Valkey](https://valkey.io/) is an open source (BSD) high performance key/value datastore.
|
||||
|
||||
Valkey exposes a Redis 7.2 compliant API. Any service that exposes the Redis API can be used with Prowler API.
|
||||
|
||||
## Modify environment variables
|
||||
# Modify environment variables
|
||||
|
||||
Under the root path of the project, you can find a file called `.env`. This file shows all the environment variables that the project uses. You should review it and set the values for the variables you want to change.
|
||||
|
||||
@@ -24,7 +24,7 @@ If you don’t set `DJANGO_TOKEN_SIGNING_KEY` or `DJANGO_TOKEN_VERIFYING_KEY`, t
|
||||
|
||||
**Important note**: Every Prowler version (or repository branches and tags) could have different variables set in its `.env` file. Please use the `.env` file that corresponds with each version.
|
||||
|
||||
### Local deployment
|
||||
## Local deployment
|
||||
Keep in mind if you export the `.env` file to use it with local deployment that you will have to do it within the context of the virtual environment, not before. Otherwise, variables will not be loaded properly.
|
||||
|
||||
To do this, you can run:
|
||||
@@ -34,12 +34,12 @@ set -a
|
||||
source .env
|
||||
```
|
||||
|
||||
## 🚀 Production deployment
|
||||
### Docker deployment
|
||||
# 🚀 Production deployment
|
||||
## Docker deployment
|
||||
|
||||
This method requires `docker` and `docker compose`.
|
||||
|
||||
#### Clone the repository
|
||||
### Clone the repository
|
||||
|
||||
```console
|
||||
# HTTPS
|
||||
@@ -50,13 +50,13 @@ git clone git@github.com:prowler-cloud/api.git
|
||||
|
||||
```
|
||||
|
||||
#### Build the base image
|
||||
### Build the base image
|
||||
|
||||
```console
|
||||
docker compose --profile prod build
|
||||
```
|
||||
|
||||
#### Run the production service
|
||||
### Run the production service
|
||||
|
||||
This command will start the Django production server and the Celery worker and also the Valkey and PostgreSQL databases.
|
||||
|
||||
@@ -68,7 +68,7 @@ You can access the server in `http://localhost:8080`.
|
||||
|
||||
> **NOTE:** notice how the port is different. When developing using docker, the port will be `8080` to prevent conflicts.
|
||||
|
||||
#### View the Production Server Logs
|
||||
### View the Production Server Logs
|
||||
|
||||
To view the logs for any component (e.g., Django, Celery worker), you can use the following command with a wildcard. This command will follow logs for any container that matches the specified pattern:
|
||||
|
||||
@@ -133,13 +133,13 @@ gunicorn -c config/guniconf.py config.wsgi:application
|
||||
|
||||
> By default, the Gunicorn server will try to use as many workers as your machine can handle. You can manually change that in the `src/backend/config/guniconf.py` file.
|
||||
|
||||
## 🧪 Development guide
|
||||
# 🧪 Development guide
|
||||
|
||||
### Local deployment
|
||||
## Local deployment
|
||||
|
||||
To use this method, you'll need to set up a Python virtual environment (version ">=3.11,<3.13") and keep dependencies updated. Additionally, ensure that `uv` and `docker compose` are installed.
|
||||
|
||||
#### Clone the repository
|
||||
### Clone the repository
|
||||
|
||||
```console
|
||||
# HTTPS
|
||||
@@ -150,7 +150,7 @@ git clone git@github.com:prowler-cloud/api.git
|
||||
|
||||
```
|
||||
|
||||
#### Start the PostgreSQL Database and Valkey
|
||||
### Start the PostgreSQL Database and Valkey
|
||||
|
||||
The PostgreSQL database (version 16.3) and Valkey (version 7) are required for the development environment. To make development easier, we have provided a `docker-compose` file that will start these components for you.
|
||||
|
||||
@@ -161,7 +161,7 @@ The PostgreSQL database (version 16.3) and Valkey (version 7) are required for t
|
||||
docker compose up postgres valkey -d
|
||||
```
|
||||
|
||||
#### Install the Python dependencies
|
||||
### Install the Python dependencies
|
||||
|
||||
> You must have uv installed
|
||||
|
||||
@@ -169,7 +169,7 @@ docker compose up postgres valkey -d
|
||||
uv sync
|
||||
```
|
||||
|
||||
#### Apply migrations
|
||||
### Apply migrations
|
||||
|
||||
For migrations, you need to force the `admin` database router. Assuming you have the correct environment variables and Python virtual environment, run:
|
||||
|
||||
@@ -178,7 +178,7 @@ cd src/backend
|
||||
python manage.py migrate --database admin
|
||||
```
|
||||
|
||||
#### Run the Django development server
|
||||
### Run the Django development server
|
||||
|
||||
```console
|
||||
cd src/backend
|
||||
@@ -188,7 +188,7 @@ python manage.py runserver
|
||||
You can access the server in `http://localhost:8000`.
|
||||
All changes in the code will be automatically reloaded in the server.
|
||||
|
||||
#### Run the Celery worker
|
||||
### Run the Celery worker
|
||||
|
||||
```console
|
||||
python -m celery -A config.celery worker -l info -E
|
||||
@@ -196,11 +196,11 @@ python -m celery -A config.celery worker -l info -E
|
||||
|
||||
The Celery worker does not detect and reload changes in the code, so you need to restart it manually when you make changes.
|
||||
|
||||
### Docker deployment
|
||||
## Docker deployment
|
||||
|
||||
This method requires `docker` and `docker compose`.
|
||||
|
||||
#### Clone the repository
|
||||
### Clone the repository
|
||||
|
||||
```console
|
||||
# HTTPS
|
||||
@@ -211,13 +211,13 @@ git clone git@github.com:prowler-cloud/api.git
|
||||
|
||||
```
|
||||
|
||||
#### Build the base image
|
||||
### Build the base image
|
||||
|
||||
```console
|
||||
docker compose --profile dev build
|
||||
```
|
||||
|
||||
#### Run the development service
|
||||
### Run the development service
|
||||
|
||||
This command will start the Django development server and the Celery worker and also the Valkey and PostgreSQL databases.
|
||||
|
||||
@@ -230,7 +230,7 @@ All changes in the code will be automatically reloaded in the server.
|
||||
|
||||
> **NOTE:** notice how the port is different. When developing using docker, the port will be `8080` to prevent conflicts.
|
||||
|
||||
#### View the development server logs
|
||||
### View the development server logs
|
||||
|
||||
To view the logs for any component (e.g., Django, Celery worker), you can use the following command with a wildcard. This command will follow logs for any container that matches the specified pattern:
|
||||
|
||||
@@ -238,7 +238,7 @@ To view the logs for any component (e.g., Django, Celery worker), you can use th
|
||||
docker logs -f $(docker ps --format "{{.Names}}" | grep 'api-')
|
||||
```
|
||||
|
||||
### Applying migrations
|
||||
## Applying migrations
|
||||
|
||||
For migrations, you need to force the `admin` database router. Assuming you have the correct environment variables and Python virtual environment, run:
|
||||
|
||||
@@ -247,7 +247,7 @@ cd src/backend
|
||||
uv run python manage.py migrate --database admin
|
||||
```
|
||||
|
||||
### Apply fixtures
|
||||
## Apply fixtures
|
||||
|
||||
Fixtures are used to populate the database with initial development data.
|
||||
|
||||
@@ -258,7 +258,7 @@ uv run python manage.py loaddata api/fixtures/0_dev_users.json --database admin
|
||||
|
||||
> The default credentials are `dev@prowler.com:Thisisapassword123@` or `dev2@prowler.com:Thisisapassword123@`
|
||||
|
||||
### Run tests
|
||||
## Run tests
|
||||
|
||||
Note that the tests will fail if you use the same `.env` file as the development environment.
|
||||
|
||||
@@ -269,7 +269,7 @@ cd src/backend
|
||||
uv run pytest
|
||||
```
|
||||
|
||||
## Custom commands
|
||||
# Custom commands
|
||||
|
||||
Django provides a way to create custom commands that can be run from the command line.
|
||||
|
||||
@@ -281,7 +281,7 @@ To run a custom command, you need to be in the `prowler/api/src/backend` directo
|
||||
uv run python manage.py <command_name>
|
||||
```
|
||||
|
||||
### Generate dummy data
|
||||
## Generate dummy data
|
||||
|
||||
```console
|
||||
python manage.py findings --tenant
|
||||
@@ -298,7 +298,7 @@ This command creates, for a given tenant, a provider, scan and a set of findings
|
||||
>
|
||||
> The last step is required to access the findings details, since the UI needs that to print all the information.
|
||||
|
||||
#### Example
|
||||
### Example
|
||||
|
||||
```console
|
||||
~/backend $ uv run python manage.py findings --tenant
|
||||
|
||||
+1
-1
@@ -68,7 +68,7 @@ name = "prowler-api"
|
||||
package-mode = false
|
||||
# Needed for the SDK compatibility
|
||||
requires-python = ">=3.11,<3.13"
|
||||
version = "1.28.0"
|
||||
version = "1.29.0"
|
||||
|
||||
[tool.uv]
|
||||
# Transitive pins matching master to avoid silent drift; bump deliberately.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: Prowler API
|
||||
version: 1.28.0
|
||||
version: 1.29.0
|
||||
description: |-
|
||||
Prowler API specification.
|
||||
|
||||
|
||||
Generated
+1
-1
@@ -4494,7 +4494,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "prowler-api"
|
||||
version = "1.28.0"
|
||||
version = "1.29.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "cartography" },
|
||||
|
||||
@@ -211,6 +211,7 @@ services:
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 60s
|
||||
|
||||
volumes:
|
||||
outputs:
|
||||
|
||||
@@ -176,6 +176,7 @@ services:
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 60s
|
||||
|
||||
volumes:
|
||||
output:
|
||||
|
||||
+2
-2
@@ -134,7 +134,7 @@ Example 1 is vague and even potentially ambiguous. Verbs state your purpose and
|
||||
|
||||
Explicit use of second-person pronouns (you) and possessives (your) should be minimized whenever possible. Those constructions are best reserved for cases when instructions are directly given in an imperative form:
|
||||
|
||||
### Example of Improvement Through Avoiding Second Person Pronouns
|
||||
**Example of Improvement Through Avoiding Second Person Pronouns**
|
||||
|
||||
**Original:**
|
||||
Prowler App can be installed in different ways, depending on your environment:
|
||||
@@ -236,7 +236,7 @@ The use of bullet points is highly recommended when:
|
||||
* Information can be logically divided into multiple categories, each sharing characteristics, features, or other relevant classifications.
|
||||
* Items are significant enough as standalone concepts to deserve their own bullet point.
|
||||
|
||||
#### Example of Improvement Through Bullet Points
|
||||
**Example of Improvement Through Bullet Points**
|
||||
|
||||
**Original:**
|
||||
It contains hundreds of controls covering CIS, NIST 800, NIST CSF, CISA, RBI, FedRAMS, 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.
|
||||
|
||||
@@ -118,8 +118,8 @@ To update the environment file:
|
||||
Edit the `.env` file and change version values:
|
||||
|
||||
```env
|
||||
PROWLER_UI_VERSION="5.26.1"
|
||||
PROWLER_API_VERSION="5.26.1"
|
||||
PROWLER_UI_VERSION="5.27.0"
|
||||
PROWLER_API_VERSION="5.27.0"
|
||||
```
|
||||
|
||||
<Note>
|
||||
|
||||
@@ -9,6 +9,8 @@ Prowler Cloud runs on AWS with high availability built in.
|
||||
| Region | URL | Location |
|
||||
|--------|-----|----------|
|
||||
| **EU** | [cloud.prowler.com](https://cloud.prowler.com) | Ireland (`eu-west-1`) |
|
||||
| **US** | On-Demand | On-Demand |
|
||||
|
||||
|
||||
## Business Continuity
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ All Prowler code goes through the same security pipeline, whether running on Pro
|
||||
Security tools and practices applied to all Prowler code.
|
||||
</Card>
|
||||
|
||||
## Prowler Cloud vs Self-Managed
|
||||
## Prowler Cloud vs Prowler OSS (Self-Managed)
|
||||
|
||||
| | Prowler Cloud | Self-Managed |
|
||||
|--|---------------|--------------|
|
||||
|
||||
@@ -2,97 +2,183 @@
|
||||
title: 'Software Security'
|
||||
---
|
||||
|
||||
Prowler follows a **security-by-design approach** throughout the software development lifecycle. All changes go through automated checks at every stage, from local development to production deployment.
|
||||
Prowler applies security-by-design across the development lifecycle. Every change passes automated checks at each stage: pre-commit hooks locally, multiple CI gates on pull requests, branch protection before merge, container scanning before publish, and registry monitoring after release.
|
||||
|
||||
[Pre-commit](https://github.com/prowler-cloud/prowler/blob/master/.pre-commit-config.yaml) validations catch issues early, and [CI/CD pipelines](https://github.com/prowler-cloud/prowler/tree/master/.github) include multiple security gates ensuring code quality, secure configurations, and compliance with internal standards.
|
||||
All security tooling and configuration lives in the [Prowler GitHub repository](https://github.com/prowler-cloud/prowler): [pre-commit hooks](https://github.com/prowler-cloud/prowler/blob/master/.pre-commit-config.yaml), [CI/CD workflows](https://github.com/prowler-cloud/prowler/tree/master/.github/workflows), and [Dependabot configuration](https://github.com/prowler-cloud/prowler/blob/master/.github/dependabot.yml).
|
||||
|
||||
Container registries are continuously scanned for vulnerabilities, with findings automatically reported to the security team for assessment and remediation. This process evolves alongside the stack as new languages, frameworks, and technologies are adopted, ensuring security practices remain comprehensive, proactive, and adaptable.
|
||||
## Coverage
|
||||
|
||||
Security controls cover six domains, each detailed below:
|
||||
|
||||
| Domain | What It Protects |
|
||||
|--------|------------------|
|
||||
| [**CI/CD**](#cicd-security) | GitHub Actions workflows, runners, third-party actions |
|
||||
| [**SAST**](#static-application-security-testing-sast) | Application source code |
|
||||
| [**SCA**](#software-composition-analysis-sca) | Third-party dependencies and their known vulnerabilities |
|
||||
| [**Supply-Chain Pinning**](#supply-chain-pinning) | Reproducible installs across Python, npm, GitHub Actions, container base images |
|
||||
| [**Containers**](#container-security) | Runtime images for UI, API, SDK, Model Context Protocol (MCP) Server |
|
||||
| [**Secrets**](#secrets-detection) | Credentials, tokens, API keys in code and git history |
|
||||
|
||||
## CI/CD Security
|
||||
|
||||
Every GitHub Actions workflow uses runner hardening, pinned action versions, and audited permissions.
|
||||
|
||||
### Runner Hardening With StepSecurity
|
||||
|
||||
- [**`step-security/harden-runner`**](https://github.com/step-security/harden-runner) runs as the first step in every workflow, pinned by commit SHA.
|
||||
- Workflows are being migrated to explicit egress controls: some already declare an egress allow-list with `egress-policy: block`, while others still run in `egress-policy: audit` until their allowed endpoints are fully defined.
|
||||
- **Global Block Policy** (StepSecurity) blocks known-malicious domains and IP addresses across every workflow run. This protection applies even in audit mode, so workflows that have not yet moved to `block` still resist known-bad egress.
|
||||
|
||||
### Third-Party Action Pinning
|
||||
|
||||
- Every third-party action reference uses a commit SHA with the version as a comment: `uses: org/action@<sha> # v1.2.3`.
|
||||
- Dependabot tracks the comment and proposes SHA-pinned upgrades on a monthly cadence.
|
||||
|
||||
### Workflow Permissions
|
||||
|
||||
- Workflows declare `permissions: {}` at the top level and grant the minimum required scopes per job.
|
||||
- Code review covers permission changes; zizmor enforces the rules (see below).
|
||||
|
||||
### Workflow Security Audit With Zizmor
|
||||
|
||||
- **[zizmor](https://github.com/zizmorcore/zizmor)** audits every workflow file for known security anti-patterns. Runs via [`ci-zizmor.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/ci-zizmor.yml).
|
||||
- Triggers on every push, every pull request that touches `.github/`, and on a daily schedule.
|
||||
- Results upload to the GitHub Security tab via Static Analysis Results Interchange Format (SARIF).
|
||||
- Key [audit rules](https://docs.zizmor.sh/audits/) the build gates on:
|
||||
- **[PWN Request](https://docs.zizmor.sh/audits/#dangerous-triggers)** (`dangerous-triggers`): unsafe use of `pull_request_target` with checked-out PR code.
|
||||
- **[Script Injection](https://docs.zizmor.sh/audits/#template-injection)** (`template-injection`): unsanitized `${{ github.event.* }}` expressions in `run:` blocks.
|
||||
- **[artipacked](https://docs.zizmor.sh/audits/#artipacked)**: credential leakage through artifacts.
|
||||
- **[Excessive permissions](https://docs.zizmor.sh/audits/#excessive-permissions)** (`excessive-permissions`): workflows with unneeded `write` scopes.
|
||||
|
||||
### Branch Protection
|
||||
|
||||
Pull requests to `master` and the active `v5.*` release branches must pass several required workflows before merge. These gates prevent specific classes of supply-chain and pipeline attacks from reaching the main branch:
|
||||
|
||||
- **Compromised packages:** the **npm Package Compromised Updates** and **PyPI Package Compromised Updates** checks (StepSecurity) fail any PR that introduces a package version present in the compromised-package feed. Layered on top of osv-scanner.
|
||||
- **Premature releases:** the **npm Package Cooldown** and **PyPI Package Cooldown** checks (StepSecurity) fail any PR that introduces a package version published within the cooldown window. Layered on top of pnpm's `minimumReleaseAge`.
|
||||
- **Workflow exploitation:** the **PWN Request** and **Script Injection** checks (StepSecurity) reject the corresponding zizmor-detected anti-patterns at PR time. Layered on top of zizmor's audit.
|
||||
- **Vulnerable code or dependencies:** CodeQL (UI, API, SDK), osv-scanner (SDK, API, UI), Bandit (SDK, API), and Trivy (container images) must all pass.
|
||||
|
||||
## Static Application Security Testing (SAST)
|
||||
|
||||
Multiple SAST tools are employed across the codebase to identify security vulnerabilities, code quality issues, and potential bugs during development.
|
||||
Multiple SAST tools run on every push and pull request to catch vulnerabilities and code-quality issues before merge.
|
||||
|
||||
### CodeQL Analysis
|
||||
### Cross-Language
|
||||
|
||||
- **Scope:** UI (JavaScript/TypeScript), API (Python), and SDK (Python)
|
||||
- **Frequency:** On every push and pull request, plus daily scheduled scans
|
||||
- **Integration:** Results uploaded to GitHub Security tab via SARIF format
|
||||
- **Purpose:** Identifies security vulnerabilities, coding errors, and potential exploits in source code
|
||||
- **CodeQL:** semantic code analysis for the UI (JavaScript/TypeScript), API (Python), and SDK (Python). Runs on every push and pull request, plus a daily scheduled scan, via [`sdk-codeql.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/sdk-codeql.yml), [`api-codeql.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/api-codeql.yml), and [`ui-codeql.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/ui-codeql.yml). Results upload to the GitHub Security tab via SARIF.
|
||||
|
||||
### Python Security Scanners
|
||||
### Python (SDK + API)
|
||||
|
||||
- **Bandit:** Detects common security issues in Python code (SQL injection, hardcoded passwords, etc.)
|
||||
- Configured to ignore test files and report only high-severity issues
|
||||
- Runs on both SDK and API codebases
|
||||
- **Pylint:** Static code analysis with security-focused checks
|
||||
- Integrated into pre-commit hooks and CI/CD pipelines
|
||||
- **Bandit:** detects common Python security issues (SQL injection, hardcoded credentials, insecure deserialization). Runs in pre-commit and on every PR/push in [`sdk-security.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/sdk-security.yml) and [`api-security.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/api-security.yml).
|
||||
- **Pylint:** analyzes your code without actually running it. It checks for errors, enforces a coding standard, looks for code smells, and can suggest refactors. Runs in pre-commit and on every PR/push in [`sdk-code-quality.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/sdk-code-quality.yml) and [`api-code-quality.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/api-code-quality.yml).
|
||||
- **Vulture:** dead-code detection at `--min-confidence 100`. Unused code can hide incomplete implementations or stale security paths. Runs in pre-commit and on every PR/push in `sdk-security.yml` and `api-security.yml`.
|
||||
- **Flake8:** style and correctness checks for the SDK. Runs in pre-commit and on every PR/push in [`sdk-code-quality.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/sdk-code-quality.yml).
|
||||
|
||||
### Code Quality & Dead Code Detection
|
||||
### JavaScript/TypeScript (UI)
|
||||
|
||||
- **Vulture:** Identifies unused code that could indicate incomplete implementations or security gaps
|
||||
- **Flake8:** Style guide enforcement with security-relevant checks
|
||||
- **Shellcheck:** Security and correctness checks for shell scripts
|
||||
- **TypeScript (`tsc`):** strict type checking for the UI. Catches whole classes of null/undefined and type-confusion bugs at build time. Runs on every PR/push via `pnpm run healthcheck` in [`ui-tests.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/ui-tests.yml).
|
||||
- **ESLint:** UI linting with a capped warning budget (`--max-warnings 40`). Runs on every PR/push via `pnpm run healthcheck` in `ui-tests.yml`.
|
||||
- **Knip:** dead-code and unused-export detection for the UI. The UI analogue to Vulture.
|
||||
|
||||
<Note>
|
||||
Knip runs locally on demand via `pnpm run lint:knip` and is not yet wired into CI.
|
||||
</Note>
|
||||
|
||||
### Shell
|
||||
|
||||
- **Shellcheck:** correctness and security checks for shell scripts in `.github/scripts/` and `scripts/`. Runs in pre-commit on staged files.
|
||||
|
||||
## Software Composition Analysis (SCA)
|
||||
|
||||
Dependencies are continuously monitored for known vulnerabilities with timely updates ensured.
|
||||
Dependencies are scanned against public vulnerability databases on every pull request and push, with results posted directly on the PR.
|
||||
|
||||
### Dependency Vulnerability Scanning
|
||||
### Cross-Language
|
||||
|
||||
- **osv-scanner:** Scans lockfiles against the [OSV.dev](https://osv.dev) vulnerability database
|
||||
- Runs in CI on every pull request and push for SDK, API, and UI
|
||||
- Fails the build on `HIGH`, `CRITICAL`, and `UNKNOWN` severity findings
|
||||
- Posts a per-lockfile report as a PR comment
|
||||
- Per-vulnerability ignores (with reason and expiry) live in `osv-scanner.toml` at the repo root
|
||||
- **Trivy:** Multi-purpose scanner for containers and dependencies
|
||||
- Scans all container images (UI, API, SDK, MCP Server)
|
||||
- Checks for vulnerabilities in OS packages and application dependencies
|
||||
- Reports findings to GitHub Security tab
|
||||
- **osv-scanner:** scans lockfiles against the [OSV.dev](https://osv.dev) vulnerability database for SDK (`uv.lock`), API (`api/uv.lock`), and UI (`ui/pnpm-lock.yaml`). Runs via [`sdk-security.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/sdk-security.yml), [`api-security.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/api-security.yml), and [`ui-security.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/ui-security.yml).
|
||||
- The action installs the `osv-scanner` binary and verifies its SHA-256 checksum against the upstream-signed `SHA256SUMS` manifest before running. Any mismatch aborts the scan.
|
||||
- Gates the build on `HIGH`, `CRITICAL`, and `UNKNOWN` severity findings.
|
||||
- Posts and updates a per-lockfile report as a pull request comment.
|
||||
- Per-vulnerability ignores live in [`osv-scanner.toml`](https://github.com/prowler-cloud/prowler/blob/master/osv-scanner.toml) at the repo root, each with a reason and an expiry date.
|
||||
- **Trivy:** scans container images for OS-package and application-dependency vulnerabilities. Runs in [`sdk-container-checks.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/sdk-container-checks.yml), [`api-container-checks.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/api-container-checks.yml), [`ui-container-checks.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/ui-container-checks.yml), and [`mcp-container-checks.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/mcp-container-checks.yml). Trivy uploads SARIF to the GitHub Security tab and posts a scan summary on the PR.
|
||||
- **Dependabot:** [configured](https://github.com/prowler-cloud/prowler/blob/master/.github/dependabot.yml) for monthly updates of the SDK Python dependencies, GitHub Actions, Docker base images, and pre-commit hooks. Dependabot opens pull requests for known security advisories, so critical patches reach the team without delay. A 7-day default cooldown reduces exposure to compromised package releases.
|
||||
|
||||
### Automated Dependency Updates
|
||||
### JavaScript/TypeScript (UI)
|
||||
|
||||
- **Dependabot:** Automated pull requests for dependency updates
|
||||
- **Python (pip):** Monthly updates for SDK
|
||||
- **GitHub Actions:** Monthly updates for workflow dependencies
|
||||
- **Docker:** Monthly updates for base images
|
||||
- Temporarily paused for API and UI to maintain stability during active development
|
||||
- **Security-first approach:** Even when paused, Dependabot automatically creates pull requests for security vulnerabilities, ensuring critical security patches are never delayed
|
||||
- **pnpm audit:** runs `pnpm audit --audit-level critical` on every UI pull request and push as part of `pnpm run audit` in [`ui-tests.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/ui-tests.yml). Cross-checks the npm registry's advisory database in addition to the OSV scan and surfaces npm-specific advisories that may not yet have an OSV identifier.
|
||||
|
||||
## Supply-Chain Pinning
|
||||
|
||||
Pinning runs across Python, npm, GitHub Actions, and container base images. Every install resolves to the exact set of versions already vetted in CI, and any drift fails loudly instead of slipping in silently.
|
||||
|
||||
### Python (uv)
|
||||
|
||||
The SDK, API, and MCP Server all use [uv](https://docs.astral.sh/uv/) for dependency management. Each component has its own project manifest and lock file:
|
||||
|
||||
| Component | Manifest | Lock File |
|
||||
|-----------|----------|-----------|
|
||||
| SDK | `pyproject.toml` | `uv.lock` |
|
||||
| API | `api/pyproject.toml` | `api/uv.lock` |
|
||||
| MCP Server | `mcp_server/pyproject.toml` | `mcp_server/uv.lock` |
|
||||
|
||||
The controls applied across all three:
|
||||
|
||||
- **Direct dependencies pinned to exact versions** (`==`). No version ranges in dependency lists.
|
||||
- **Transitive dependencies pinned** via `[tool.uv].constraint-dependencies` in the SDK and API manifests. The constraint set mirrors the versions locked in the corresponding `uv.lock`. A future `uv lock` preserves these versions instead of silently picking up newer releases, and the resolver fails when a constraint becomes infeasible, signaling that a deliberate bump is needed.
|
||||
- **Lock files committed.** CI installs strictly from the lock.
|
||||
- **uv itself pinned** in the [`setup-python-uv`](https://github.com/prowler-cloud/prowler/tree/master/.github/actions/setup-python-uv) composite action.
|
||||
|
||||
<Note>
|
||||
The MCP Server has a small direct-dependency surface and does not yet declare a separate constraint set. Its lock file is the source of truth.
|
||||
</Note>
|
||||
|
||||
### JavaScript/TypeScript (pnpm)
|
||||
|
||||
The UI uses [pnpm](https://pnpm.io) with supply-chain controls configured in [`ui/pnpm-workspace.yaml`](https://github.com/prowler-cloud/prowler/blob/master/ui/pnpm-workspace.yaml).
|
||||
|
||||
- **Minimum release age** (`minimumReleaseAge: 1440`): packages must publish at least 24 hours before install. This reduces exposure during the window when a compromised release has not yet been detected and yanked.
|
||||
- **Lifecycle script allow-list** (`strictDepBuilds: true` + `allowBuilds`): only explicitly approved packages may run `install` or `postinstall` scripts (currently `sharp`, `esbuild`, `@sentry/cli`, `@heroui/shared-utils`, `unrs-resolver`, `msw`). Any unlisted package with lifecycle scripts fails the install.
|
||||
- **Trust policy** (`trustPolicy: no-downgrade`): the install fails when a package's trust evidence drops, for example after a new publisher takes over.
|
||||
- **Block exotic subdeps** (`blockExoticSubdeps: true`): transitive dependencies cannot ship as git URLs or tarballs. Every package in the tree resolves from the configured registry.
|
||||
- **Transitive overrides** in [`ui/package.json`](https://github.com/prowler-cloud/prowler/blob/master/ui/package.json) force specific versions for transitive packages (`lodash`, `serialize-javascript`, `qs`, `rollup`, `minimatch`, `ajv`, and others).
|
||||
- **`pnpm-lock.yaml` committed** and CI installs strictly from the lock.
|
||||
- **pnpm itself pinned** via the `packageManager` field in `package.json` with an integrity hash.
|
||||
|
||||
### GitHub Actions
|
||||
|
||||
- Every third-party action reference uses a commit SHA, with the version in a trailing comment.
|
||||
- Dependabot opens monthly PRs to bump pinned SHAs.
|
||||
|
||||
### Container Base Images
|
||||
|
||||
- Every Dockerfile references base images by digest (`image@sha256:...`).
|
||||
- Dependabot opens monthly PRs to bump digests.
|
||||
|
||||
## Container Security
|
||||
|
||||
All container images are scanned before deployment.
|
||||
Container images get scanned twice: once in CI before they push to a registry, and continuously after publish by the registries themselves.
|
||||
|
||||
### Trivy Vulnerability Scanning
|
||||
### Pre-Publish (CI)
|
||||
|
||||
- Scans images for vulnerabilities and misconfigurations
|
||||
- Generates SARIF reports uploaded to GitHub Security tab
|
||||
- Creates PR comments with scan summaries
|
||||
- Configurable to fail builds on critical findings
|
||||
- Reports include CVE counts and remediation guidance
|
||||
- **Trivy** scans for OS-package and application-dependency vulnerabilities. Runs in [`sdk-container-checks.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/sdk-container-checks.yml), [`api-container-checks.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/api-container-checks.yml), [`ui-container-checks.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/ui-container-checks.yml), and [`mcp-container-checks.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/mcp-container-checks.yml). Trivy uploads SARIF to the GitHub Security tab and posts a summary on the PR. Builds can fail on critical findings when configured to.
|
||||
- **Hadolint** validates Dockerfile syntax and structure against secure-build best practices. Runs in pre-commit and in the same `*-container-checks.yml` workflows linked above.
|
||||
|
||||
### Hadolint
|
||||
### Post-Publish (Registries)
|
||||
|
||||
- Validates Dockerfile syntax and structure
|
||||
- Ensures secure image building practices
|
||||
- **Amazon ECR:** ECR continuously scans published images for vulnerabilities. New advisories disclosed after publish surface on the image without requiring a rebuild.
|
||||
- **Docker Hub:** Docker Hub continuously scans the same images mirrored from ECR.
|
||||
- The security team reviews findings from both registries for triage and remediation.
|
||||
|
||||
## Secrets Detection
|
||||
|
||||
Prowler protects against accidental exposure of sensitive credentials.
|
||||
|
||||
### TruffleHog
|
||||
|
||||
- Scans entire codebase and Git history for secrets
|
||||
- Runs on every push and pull request
|
||||
- Pre-commit hook prevents committing secrets
|
||||
- Detects high-entropy strings, API keys, tokens, and credentials
|
||||
- Configured to report verified and unknown findings
|
||||
- **[TruffleHog](https://github.com/trufflesecurity/trufflehog)** scans the codebase and git history on every push and pull request via [`find-secrets.yml`](https://github.com/prowler-cloud/prowler/blob/master/.github/workflows/find-secrets.yml). Detects high-entropy strings, API keys, tokens, and credentials, and reports verified and unknown findings.
|
||||
- A pre-commit hook runs the same check locally and blocks secrets before they leave the developer machine.
|
||||
|
||||
## Security Monitoring
|
||||
|
||||
- **GitHub Security Tab:** Centralized view of all security findings from CodeQL, Trivy, and other SARIF-compatible tools
|
||||
- **Artifact Retention:** Security scan reports retained for post-deployment analysis
|
||||
- **PR Comments:** Automated security feedback on pull requests for rapid remediation
|
||||
- **GitHub Security tab:** centralized view of findings from CodeQL, Trivy, zizmor, and any other SARIF-compatible tool.
|
||||
- **PR comments:** osv-scanner and Trivy post per-PR summaries so issues surface during review, not after merge.
|
||||
- **Artifact retention:** the build retains security scan reports for post-deployment analysis.
|
||||
|
||||
## Contact
|
||||
|
||||
For questions regarding software security, visit the [Support page](/support).
|
||||
For questions about software security, see the [Support page](/support). To report a vulnerability, follow the [responsible disclosure process](https://prowler.com/.well-known/security.txt).
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
> **Skills Reference**: See [`prowler-mcp`](../skills/prowler-mcp/SKILL.md)
|
||||
|
||||
## Auto-invoke Skills
|
||||
### Auto-invoke Skills
|
||||
|
||||
When performing these actions, ALWAYS invoke the corresponding skill FIRST:
|
||||
|
||||
@@ -68,7 +68,7 @@ Python 3.12+ | FastMCP 2.13.1 | httpx (async) | Pydantic | uv
|
||||
|
||||
## PROJECT STRUCTURE
|
||||
|
||||
```text
|
||||
```
|
||||
mcp_server/prowler_mcp_server/
|
||||
├── server.py # Main orchestration
|
||||
├── prowler_hub/server.py # Hub tools (no auth)
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
All notable changes to the **Prowler MCP Server** are documented in this file.
|
||||
|
||||
## [0.7.0] (Prowler UNRELEASED)
|
||||
## [0.7.0] (Prowler v5.27.0)
|
||||
|
||||
### 🚀 Added
|
||||
|
||||
- MCP Server tools for Prowler Finding Groups Management [(#11140)](https://github.com/prowler-cloud/prowler/pull/11140)
|
||||
- Finding Groups tools [(#11140)](https://github.com/prowler-cloud/prowler/pull/11140)
|
||||
|
||||
### 🔐 Security
|
||||
|
||||
|
||||
@@ -83,14 +83,14 @@ npm install --save-exact mcp-remote@0.1.38
|
||||
|
||||
### 2. Local STDIO Mode
|
||||
|
||||
Run the server locally on your machine:
|
||||
**Run the server locally on your machine**
|
||||
|
||||
- Runs as a subprocess of your MCP client
|
||||
- Requires Python 3.12+ or Docker
|
||||
|
||||
### 3. Self-Hosted HTTP Mode
|
||||
|
||||
Deploy your own remote MCP server:
|
||||
**Deploy your own remote MCP server**
|
||||
|
||||
- Full control over deployment
|
||||
- Requires Python 3.12+ or Docker
|
||||
@@ -132,7 +132,7 @@ All tools follow a consistent naming pattern with prefixes:
|
||||
|
||||
## Architecture
|
||||
|
||||
```text
|
||||
```
|
||||
prowler_mcp_server/
|
||||
├── server.py # Main orchestrator (imports sub-servers with prefixes)
|
||||
├── main.py # CLI entry point
|
||||
@@ -154,20 +154,17 @@ prowler_mcp_server/
|
||||
|
||||
The Prowler MCP Server enables powerful workflows through AI assistants:
|
||||
|
||||
### Security Operations
|
||||
|
||||
**Security Operations**
|
||||
- "Show me all critical findings from my AWS production accounts"
|
||||
- "Register my new AWS account in Prowler and run a scheduled scan every day"
|
||||
- "List all muted findings and detect what findgings are muted by a not enough good reason in relation to their severity"
|
||||
|
||||
### Security Research
|
||||
|
||||
**Security Research**
|
||||
- "Explain what the S3 bucket public access Prowler check does"
|
||||
- "Find all Prowler checks related to encryption at rest"
|
||||
- "What is the latest version of the CIS that Prowler is covering per provider?"
|
||||
|
||||
### Documentation & Learning
|
||||
|
||||
**Documentation & Learning**
|
||||
- "How do I configure Prowler to scan my GCP organization?"
|
||||
- "What authentication methods does Prowler support for Azure?"
|
||||
- "How can I contribute with a new security check to Prowler?"
|
||||
|
||||
@@ -41,12 +41,22 @@ async def setup_main_server():
|
||||
logger.error(f"Failed to import Prowler Documentation server: {e}")
|
||||
|
||||
|
||||
# Add health check endpoint
|
||||
# Response follows the IETF Health Check Response Format
|
||||
# (draft-inadarei-api-health-check-06). `version` is the contract version of
|
||||
# this endpoint; `releaseId` is the package build version.
|
||||
@prowler_mcp_server.custom_route("/health", methods=["GET"])
|
||||
async def health_check(request) -> JSONResponse:
|
||||
async def health_check(_request) -> JSONResponse:
|
||||
"""Health check endpoint."""
|
||||
return JSONResponse(
|
||||
{"status": "healthy", "service": "prowler-mcp-server", "version": __version__}
|
||||
{
|
||||
"status": "pass",
|
||||
"version": "1",
|
||||
"releaseId": __version__,
|
||||
"serviceId": "prowler-mcp-server",
|
||||
"description": "Prowler MCP Server",
|
||||
},
|
||||
media_type="application/health+json",
|
||||
headers={"Cache-Control": "no-store"},
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
build-backend = "setuptools.build_meta"
|
||||
requires = ["setuptools>=61.0", "wheel"]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"pytest==8.3.5"
|
||||
]
|
||||
|
||||
[project]
|
||||
dependencies = [
|
||||
"fastmcp==2.14.0",
|
||||
@@ -16,5 +21,8 @@ version = "0.5.0"
|
||||
[project.scripts]
|
||||
prowler-mcp = "prowler_mcp_server.main:main"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
|
||||
[tool.uv]
|
||||
package = true
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
"""Tests for the Prowler MCP Server health endpoint."""
|
||||
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from prowler_mcp_server import __version__
|
||||
from prowler_mcp_server.server import app
|
||||
|
||||
|
||||
def test_health_returns_ietf_pass_response():
|
||||
"""GET /health returns 200 with the IETF health-check body and headers."""
|
||||
client = TestClient(app)
|
||||
|
||||
response = client.get("/health")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.headers["content-type"] == "application/health+json"
|
||||
assert response.headers["cache-control"] == "no-store"
|
||||
assert response.json() == {
|
||||
"status": "pass",
|
||||
"version": "1",
|
||||
"releaseId": __version__,
|
||||
"serviceId": "prowler-mcp-server",
|
||||
"description": "Prowler MCP Server",
|
||||
}
|
||||
|
||||
|
||||
def test_health_release_id_matches_package_version():
|
||||
"""The endpoint must surface the current package __version__ as releaseId.
|
||||
|
||||
Drift between the response and the installed package would mislead any
|
||||
monitoring tool that uses releaseId to identify the running build.
|
||||
"""
|
||||
client = TestClient(app)
|
||||
|
||||
response = client.get("/health")
|
||||
|
||||
assert response.json()["releaseId"] == __version__
|
||||
|
||||
|
||||
def test_health_rejects_non_get_methods():
|
||||
"""The endpoint only exposes GET; other verbs return 405."""
|
||||
client = TestClient(app)
|
||||
|
||||
response = client.post("/health")
|
||||
|
||||
assert response.status_code == 405
|
||||
Generated
+50
@@ -443,6 +443,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jaraco-classes"
|
||||
version = "3.4.0"
|
||||
@@ -676,6 +685,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/df/d3f1ddf4bb4cb50ed9b1139cc7b1c54c34a1e7ce8fd1b9a37c0d1551a6bd/opentelemetry_api-1.39.1-py3-none-any.whl", hash = "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950", size = 66356, upload-time = "2025-12-11T13:32:17.304Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "26.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pathable"
|
||||
version = "0.4.4"
|
||||
@@ -703,6 +721,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3", size = 18651, upload-time = "2025-10-08T17:44:47.223Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.6.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prometheus-client"
|
||||
version = "0.24.1"
|
||||
@@ -721,12 +748,20 @@ dependencies = [
|
||||
{ name = "httpx" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "pytest" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "fastmcp", specifier = "==2.14.0" },
|
||||
{ name = "httpx", specifier = "==0.28.1" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [{ name = "pytest", specifier = "==8.3.5" }]
|
||||
|
||||
[[package]]
|
||||
name = "py-key-value-aio"
|
||||
version = "0.3.0"
|
||||
@@ -906,6 +941,21 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/bc/22540e73c5f5ae18f02924cd3954a6c9a4aa6b713c841a94c98335d333a1/pyperclip-1.10.0-py3-none-any.whl", hash = "sha256:596fbe55dc59263bff26e61d2afbe10223e2fccb5210c9c96a28d6887cfcc7ec", size = 11062, upload-time = "2025-09-18T00:53:59.252Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "8.3.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
{ name = "iniconfig" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pluggy" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-dotenv"
|
||||
version = "1.1.1"
|
||||
|
||||
@@ -28,12 +28,12 @@ This Terraform configuration creates the necessary IAM role and policies to allo
|
||||
|
||||
### Usage Examples
|
||||
|
||||
#### Basic deployment (without S3 integration)
|
||||
#### Basic deployment (without S3 integration):
|
||||
```bash
|
||||
terraform apply -var="external_id=your-external-id-here"
|
||||
```
|
||||
|
||||
#### With S3 integration enabled
|
||||
#### With S3 integration enabled:
|
||||
```bash
|
||||
terraform apply \
|
||||
-var="external_id=your-external-id-here" \
|
||||
@@ -42,14 +42,14 @@ terraform apply \
|
||||
-var="s3_integration_bucket_account_id=123456789012"
|
||||
```
|
||||
|
||||
#### Using terraform.tfvars file (Recommended)
|
||||
#### Using terraform.tfvars file (Recommended):
|
||||
```bash
|
||||
cp terraform.tfvars.example terraform.tfvars
|
||||
# Edit the file with your values
|
||||
terraform apply
|
||||
```
|
||||
|
||||
#### Command line variables (Alternative)
|
||||
#### Command line variables (Alternative):
|
||||
```bash
|
||||
terraform apply -var="external_id=your-external-id-here"
|
||||
```
|
||||
|
||||
+3
-3
@@ -7,7 +7,7 @@
|
||||
> - [`prowler-compliance`](../skills/prowler-compliance/SKILL.md) - Compliance framework structure
|
||||
> - [`pytest`](../skills/pytest/SKILL.md) - Generic pytest patterns
|
||||
|
||||
## Auto-invoke Skills
|
||||
### Auto-invoke Skills
|
||||
|
||||
When performing these actions, ALWAYS invoke the corresponding skill FIRST:
|
||||
|
||||
@@ -44,7 +44,7 @@ The Prowler SDK is the core Python engine powering cloud security assessments ac
|
||||
|
||||
### Provider Architecture
|
||||
|
||||
```text
|
||||
```
|
||||
prowler/providers/{provider}/
|
||||
├── {provider}_provider.py # Main provider class
|
||||
├── models.py # Provider-specific models
|
||||
@@ -91,7 +91,7 @@ Python 3.10+ | uv | pytest | moto (AWS mocking) | Pre-commit hooks (black, flake
|
||||
|
||||
## PROJECT STRUCTURE
|
||||
|
||||
```text
|
||||
```
|
||||
prowler/
|
||||
├── __main__.py # CLI entry point
|
||||
├── config/ # Global configuration
|
||||
|
||||
+23
-10
@@ -2,7 +2,25 @@
|
||||
|
||||
All notable changes to the **Prowler SDK** are documented in this file.
|
||||
|
||||
## [5.27.0] (Prowler UNRELEASED)
|
||||
## [5.28.0] (Prowler UNRELEASED)
|
||||
|
||||
### 🚀 Added
|
||||
|
||||
- `entra_app_registration_client_secret_unused` check for M365 provider [(#11232)](https://github.com/prowler-cloud/prowler/pull/11232)
|
||||
- `cloudsql_instance_cmek_encryption_enabled` check for GCP provider [(#11023)](https://github.com/prowler-cloud/prowler/pull/11023)
|
||||
|
||||
---
|
||||
|
||||
## [5.27.1] (Prowler UNRELEASED)
|
||||
|
||||
### 🐞 Fixed
|
||||
|
||||
- `s3_bucket_shadow_resource_vulnerability` no longer emits a tautological `PASS` finding for every bucket; a finding is now produced only when the bucket name matches one of the predictable service patterns (Glue, SageMaker, EMR, CodeStar) [(#11220)](https://github.com/prowler-cloud/prowler/pull/11220)
|
||||
- `sqlserver_tde_encrypted_with_cmk` check for Azure provider no longer reports a false `FAIL` for SQL Servers whose user databases are correctly encrypted with a customer-managed key, by excluding the system `master` database (always reports TDE `Disabled` and is not customer-controllable) from the TDE evaluation [(#11233)](https://github.com/prowler-cloud/prowler/pull/11233)
|
||||
|
||||
---
|
||||
|
||||
## [5.27.0] (Prowler v5.27.0)
|
||||
|
||||
### 🚀 Added
|
||||
|
||||
@@ -18,6 +36,8 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
|
||||
- `entra_emergency_access_exclusion` check for M365 provider now scopes the exclusion requirement to enabled Conditional Access policies with a `Block` grant control instead of every enabled policy, focusing on the lockout-relevant policy set [(#10849)](https://github.com/prowler-cloud/prowler/pull/10849)
|
||||
- AWS IAM customer-managed policy checks no longer emit `FAIL` on unattached policies unless `--scan-unused-services` is enabled [(#11150)](https://github.com/prowler-cloud/prowler/pull/11150)
|
||||
- Replace `poetry` with `uv` as package manager [(#11162)](https://github.com/prowler-cloud/prowler/pull/11162)
|
||||
- Replace `safety` with `osv-scanner` for dependency vulnerability scanning in SDK CI and pre-commit [(#11167)](https://github.com/prowler-cloud/prowler/pull/11167)
|
||||
|
||||
### 🐞 Fixed
|
||||
|
||||
@@ -26,16 +46,9 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
- `zone_waf_enabled` check for Cloudflare provider now appends a plan-aware hint to the FAIL `status_extended`: a possible-false-positive note on paid plans (Pro, Business, Enterprise) where the legacy `waf` zone setting can read `off` even though WAF managed rulesets are deployed via the dashboard, and a "not available on the Cloudflare Free plan" note on Free zones [(#9896)](https://github.com/prowler-cloud/prowler/pull/9896)
|
||||
- Google Workspace Gmail checks sharing a single resource row, causing the service field to be overwritten by the last check executed [(#11169)](https://github.com/prowler-cloud/prowler/pull/11169)
|
||||
- Google Workspace Drive and Calendar services missing server-side policy filters [(#11195)](https://github.com/prowler-cloud/prowler/pull/11195)
|
||||
- `VercelSession.token` is now excluded from serialization and representation to prevent the Vercel API token from leaking through `.dict()`, `.json()` or logs [(#11198)](https://github.com/prowler-cloud/prowler/pull/11198)
|
||||
|
||||
---
|
||||
|
||||
## [5.26.2] (Prowler UNRELEASED)
|
||||
|
||||
### 🐞 Fixed
|
||||
|
||||
- `entra_users_mfa_capable` and `entra_break_glass_account_fido2_security_key_registered` report a preventive FAIL per affected user (with the missing permission named) when the M365 service principal lacks `AuditLog.Read.All`, instead of mass false positives [(#10907)](https://github.com/prowler-cloud/prowler/pull/10907)
|
||||
- Update duplicated GCP CIS requirements IDs [(#11180)](https://github.com/prowler-cloud/prowler/pull/11180)
|
||||
- Duplicated GCP CIS requirements IDs [(#11180)](https://github.com/prowler-cloud/prowler/pull/11180)
|
||||
- `VercelSession.token` is now excluded from serialization and representation to prevent the Vercel API token from leaking through `.dict()`, `.json()` or logs [(#11198)](https://github.com/prowler-cloud/prowler/pull/11198)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ class _MutableTimestamp:
|
||||
|
||||
timestamp = _MutableTimestamp(datetime.today())
|
||||
timestamp_utc = _MutableTimestamp(datetime.now(timezone.utc))
|
||||
prowler_version = "5.27.0"
|
||||
prowler_version = "5.28.0"
|
||||
html_logo_url = "https://github.com/prowler-cloud/prowler/"
|
||||
square_logo_img = "https://raw.githubusercontent.com/prowler-cloud/prowler/dc7d2d5aeb92fdf12e8604f42ef6472cd3e8e889/docs/img/prowler-logo-black.png"
|
||||
aws_logo = "https://user-images.githubusercontent.com/38561120/235953920-3e3fba08-0795-41dc-b480-9bea57db9f2e.png"
|
||||
|
||||
+13
-13
@@ -24,30 +24,30 @@ class s3_bucket_shadow_resource_vulnerability(Check):
|
||||
|
||||
# First, check buckets in the current account
|
||||
for bucket in s3_client.buckets.values():
|
||||
report = Check_Report_AWS(self.metadata(), resource=bucket)
|
||||
report.region = bucket.region
|
||||
report.resource_id = bucket.name
|
||||
report.resource_arn = bucket.arn
|
||||
report.resource_tags = bucket.tags
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"S3 bucket {bucket.name} is not a known shadow resource."
|
||||
)
|
||||
|
||||
# Check if this bucket matches any predictable pattern
|
||||
# Only emit a finding when the bucket name actually matches one of
|
||||
# the predictable service patterns. A bucket whose name does not
|
||||
# match any pattern is, by definition, not a shadow resource, so a
|
||||
# PASS finding for it would be tautological and add no signal.
|
||||
for service, pattern_format in predictable_patterns.items():
|
||||
pattern = pattern_format.replace("<region>", bucket.region)
|
||||
|
||||
if re.match(pattern, bucket.name):
|
||||
report = Check_Report_AWS(self.metadata(), resource=bucket)
|
||||
report.region = bucket.region
|
||||
report.resource_id = bucket.name
|
||||
report.resource_arn = bucket.arn
|
||||
report.resource_tags = bucket.tags
|
||||
|
||||
if bucket.owner_id != s3_client.audited_canonical_id:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"S3 bucket {bucket.name} for service {service} is a known shadow resource and it is owned by another account ({bucket.owner_id})."
|
||||
else:
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"S3 bucket {bucket.name} for service {service} is a known shadow resource but it is correctly owned by the audited account."
|
||||
|
||||
findings.append(report)
|
||||
reported_buckets.add(bucket.name)
|
||||
break
|
||||
findings.append(report)
|
||||
reported_buckets.add(bucket.name)
|
||||
|
||||
# Now check for shadow resources in other accounts by testing predictable patterns
|
||||
# We'll test different regions to see if shadow resources exist
|
||||
|
||||
+7
-3
@@ -10,9 +10,13 @@ class sqlserver_tde_encrypted_with_cmk(Check):
|
||||
subscription, subscription
|
||||
)
|
||||
for sql_server in sql_servers:
|
||||
databases = (
|
||||
sql_server.databases if sql_server.databases is not None else []
|
||||
)
|
||||
databases = [
|
||||
database
|
||||
for database in (
|
||||
sql_server.databases if sql_server.databases is not None else []
|
||||
)
|
||||
if database.name.lower() != "master"
|
||||
]
|
||||
if len(databases) > 0:
|
||||
report = Check_Report_Azure(
|
||||
metadata=self.metadata(), resource=sql_server
|
||||
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"Provider": "gcp",
|
||||
"CheckID": "cloudsql_instance_cmek_encryption_enabled",
|
||||
"CheckTitle": "Cloud SQL instance is encrypted with a customer-managed key (CMEK)",
|
||||
"CheckType": [],
|
||||
"ServiceName": "cloudsql",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "sqladmin.googleapis.com/Instance",
|
||||
"Description": "**Cloud SQL instances** use **customer-managed encryption keys** (`CMEK`) via Cloud KMS for at-rest encryption. The evaluation identifies instances lacking a configured **Cloud KMS key**, indicating use of default Google-managed encryption instead.",
|
||||
"Risk": "Without CMEK, Google holds sole control of the encryption keys. If the organization must demonstrate key custody, meet data residency requirements, or immediately revoke access to data (e.g., upon contract termination), Google-managed keys are insufficient. This may violate ISMS-P 2.7.1 and regulatory requirements for sensitive or personal data.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://cloud.google.com/sql/docs/mysql/cmek",
|
||||
"https://cloud.google.com/sql/docs/postgres/cmek",
|
||||
"https://cloud.google.com/sql/docs/sqlserver/cmek",
|
||||
"https://cloud.google.com/kms/docs/resource-hierarchy"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "gcloud sql instances create <INSTANCE_NAME> \\\n --database-version=<DATABASE_VERSION> \\\n --region=<REGION> \\\n --disk-encryption-key=projects/<PROJECT>/locations/<REGION>/keyRings/<RING>/cryptoKeys/<KEY>",
|
||||
"NativeIaC": "",
|
||||
"Other": "CMEK must be configured at instance creation time. To migrate an existing instance:\n1. Create a new Cloud SQL instance with CMEK enabled.\n2. Export data from the existing instance.\n3. Import data into the new CMEK-enabled instance.\n4. Update application connection strings.",
|
||||
"Terraform": "```hcl\nresource \"google_sql_database_instance\" \"example\" {\n name = \"<instance_name>\"\n database_version = \"<database_version>\"\n region = \"<region>\"\n\n encryption_key_name = \"projects/<project>/locations/<region>/keyRings/<ring>/cryptoKeys/<key>\"\n\n settings {\n tier = \"db-custom-2-7680\"\n }\n}\n```"
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "For instances storing personal or sensitive data, create new Cloud SQL instances with CMEK using a Cloud KMS key in the same region. Ensure the Cloud SQL service account has the roles/cloudkms.cryptoKeyEncrypterDecrypter role on the key, and enable key rotation per your policy.",
|
||||
"Url": "https://hub.prowler.com/check/cloudsql_instance_cmek_encryption_enabled"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"encryption"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [
|
||||
"kms_key_rotation_enabled",
|
||||
"kms_key_not_publicly_accessible",
|
||||
"bigquery_dataset_cmk_encryption"
|
||||
],
|
||||
"Notes": "CMEK cannot be enabled on an existing Cloud SQL instance; it must be set at creation time. Existing instances require data migration to a new CMEK-enabled instance."
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
from prowler.lib.check.models import Check, Check_Report_GCP
|
||||
from prowler.providers.gcp.services.cloudsql.cloudsql_client import cloudsql_client
|
||||
|
||||
|
||||
class cloudsql_instance_cmek_encryption_enabled(Check):
|
||||
def execute(self) -> Check_Report_GCP:
|
||||
findings = []
|
||||
for instance in cloudsql_client.instances:
|
||||
if instance.instance_type != "CLOUD_SQL_INSTANCE":
|
||||
continue
|
||||
report = Check_Report_GCP(metadata=self.metadata(), resource=instance)
|
||||
if instance.cmek_key_name:
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"Database instance {instance.name} is encrypted with "
|
||||
f"customer-managed key: {instance.cmek_key_name}."
|
||||
)
|
||||
else:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"Database instance {instance.name} is not encrypted with a "
|
||||
f"customer-managed key (CMEK); Google-managed key is in use."
|
||||
)
|
||||
findings.append(report)
|
||||
return findings
|
||||
@@ -1,3 +1,5 @@
|
||||
from typing import Optional
|
||||
|
||||
from pydantic.v1 import BaseModel
|
||||
|
||||
from prowler.lib.logger import logger
|
||||
@@ -24,6 +26,8 @@ class CloudSQL(GCPService):
|
||||
for address in instance.get("ipAddresses", []):
|
||||
if address["type"] == "PRIMARY":
|
||||
public_ip = True
|
||||
settings = instance.get("settings", {})
|
||||
ip_config = settings.get("ipConfiguration", {})
|
||||
self.instances.append(
|
||||
Instance(
|
||||
name=instance["name"],
|
||||
@@ -31,19 +35,23 @@ class CloudSQL(GCPService):
|
||||
region=instance["region"],
|
||||
ip_addresses=instance.get("ipAddresses", []),
|
||||
public_ip=public_ip,
|
||||
require_ssl=instance["settings"]
|
||||
.get("ipConfiguration", {})
|
||||
.get("requireSsl", False),
|
||||
ssl_mode=instance["settings"]
|
||||
.get("ipConfiguration", {})
|
||||
.get("sslMode", "ALLOW_UNENCRYPTED_AND_ENCRYPTED"),
|
||||
automated_backups=instance["settings"]
|
||||
.get("backupConfiguration", {})
|
||||
.get("enabled", False),
|
||||
authorized_networks=instance["settings"]
|
||||
.get("ipConfiguration", {})
|
||||
.get("authorizedNetworks", []),
|
||||
flags=instance["settings"].get("databaseFlags", []),
|
||||
require_ssl=ip_config.get("requireSsl", False),
|
||||
ssl_mode=ip_config.get(
|
||||
"sslMode", "ALLOW_UNENCRYPTED_AND_ENCRYPTED"
|
||||
),
|
||||
automated_backups=settings.get(
|
||||
"backupConfiguration", {}
|
||||
).get("enabled", False),
|
||||
authorized_networks=ip_config.get(
|
||||
"authorizedNetworks", []
|
||||
),
|
||||
flags=settings.get("databaseFlags", []),
|
||||
instance_type=instance.get(
|
||||
"instanceType", "CLOUD_SQL_INSTANCE"
|
||||
),
|
||||
cmek_key_name=instance.get(
|
||||
"diskEncryptionConfiguration", {}
|
||||
).get("kmsKeyName"),
|
||||
project_id=project_id,
|
||||
)
|
||||
)
|
||||
@@ -68,4 +76,6 @@ class Instance(BaseModel):
|
||||
ssl_mode: str
|
||||
automated_backups: bool
|
||||
flags: list
|
||||
instance_type: str = "CLOUD_SQL_INSTANCE"
|
||||
cmek_key_name: Optional[str] = None
|
||||
project_id: str
|
||||
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"Provider": "m365",
|
||||
"CheckID": "entra_app_registration_client_secret_unused",
|
||||
"CheckTitle": "Application registrations should not use client secrets",
|
||||
"CheckType": [],
|
||||
"ServiceName": "entra",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "NotDefined",
|
||||
"ResourceGroup": "IAM",
|
||||
"Description": "Microsoft Entra **application registrations** should not have any **secret credentials** configured. This check audits the current state of every application registration and reports those that hold one or more **secrets**. Both **expired** and **active** secrets are reported, since expired entries are credential clutter that should be cleaned up.",
|
||||
"Risk": "**Secrets** are bearer credentials that are easy to leak (committed to repositories, copied into CI variables, shared via chat). Once leaked, a **secret** can be used from anywhere on the internet until it is rotated. Both **expired** and **active** secrets increase the attack surface and represent credential clutter.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://learn.microsoft.com/en-us/entra/identity-platform/certificate-credentials",
|
||||
"https://learn.microsoft.com/en-us/graph/api/resources/passwordcredential?view=graph-rest-1.0",
|
||||
"https://learn.microsoft.com/en-us/entra/workload-id/workload-identity-federation",
|
||||
"https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/overview"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "",
|
||||
"NativeIaC": "",
|
||||
"Other": "1. Navigate to Microsoft Entra admin center (https://entra.microsoft.com)\n2. Go to Applications > App registrations\n3. Select the flagged application\n4. Go to Certificates & secrets\n5. Delete all client secrets under the 'Client secrets' tab\n6. Add a certificate or configure federated identity credentials instead",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Remove every **secret** from the affected **application registrations** so they no longer rely on long-lived shared secrets. Enable the **default app management policy** to block new secrets from being added.",
|
||||
"Url": "https://hub.prowler.com/check/entra_app_registration_client_secret_unused"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"identity-access",
|
||||
"e3"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [
|
||||
"entra_default_app_management_policy_enabled"
|
||||
],
|
||||
"Notes": "This check audits the current state of password credentials on app registrations. The related check entra_default_app_management_policy_enabled audits the tenant-wide policy that prevents new secrets from being added. Both expired and active password credentials trigger a FAIL."
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
from prowler.lib.check.models import Check, CheckReportM365
|
||||
from prowler.providers.m365.services.entra.entra_client import entra_client
|
||||
|
||||
|
||||
class entra_app_registration_client_secret_unused(Check):
|
||||
"""
|
||||
Ensure that application registrations do not use password credentials (client secrets).
|
||||
|
||||
This check evaluates application registrations in Microsoft Entra ID to identify
|
||||
those with password credentials (client secrets). Applications should authenticate
|
||||
using certificates, federated identity credentials, or managed identities instead.
|
||||
Both expired and active password credentials are flagged, since expired entries are
|
||||
credential clutter that should be cleaned up.
|
||||
|
||||
- PASS: The application has no password credentials.
|
||||
- FAIL: The application has one or more password credentials that should be removed.
|
||||
"""
|
||||
|
||||
def execute(self) -> list[CheckReportM365]:
|
||||
findings = []
|
||||
|
||||
for app_id, app in entra_client.app_registrations.items():
|
||||
report = CheckReportM365(
|
||||
metadata=self.metadata(),
|
||||
resource=app,
|
||||
resource_name=app.name or app.app_id,
|
||||
resource_id=app_id,
|
||||
)
|
||||
|
||||
num_secrets = len(app.password_credentials)
|
||||
if num_secrets > 0:
|
||||
report.status = "FAIL"
|
||||
secret_details = []
|
||||
for cred in app.password_credentials:
|
||||
detail = cred.display_name or cred.key_id
|
||||
if cred.end_date_time:
|
||||
detail += f" (expires: {cred.end_date_time})"
|
||||
secret_details.append(detail)
|
||||
|
||||
if num_secrets > 5:
|
||||
displayed = ", ".join(secret_details[:5])
|
||||
displayed += f" (and {num_secrets - 5} more)"
|
||||
else:
|
||||
displayed = ", ".join(secret_details)
|
||||
|
||||
report.status_extended = (
|
||||
f"App registration {app.name} has {num_secrets} "
|
||||
f"password credential(s) (client secrets): {displayed}."
|
||||
)
|
||||
else:
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"App registration {app.name} does not use password credentials."
|
||||
)
|
||||
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||
@@ -90,6 +90,7 @@ class Entra(M365Service):
|
||||
self._get_directory_sync_settings(),
|
||||
self._get_authentication_method_configurations(),
|
||||
self._get_service_principals(),
|
||||
self._get_app_registrations(),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -106,6 +107,7 @@ class Entra(M365Service):
|
||||
str, AuthenticationMethodConfiguration
|
||||
] = attributes[9]
|
||||
self.service_principals: Dict[str, "ServicePrincipal"] = attributes[10]
|
||||
self.app_registrations: Dict[str, "AppRegistration"] = attributes[11]
|
||||
self.user_accounts_status = {}
|
||||
|
||||
if created_loop:
|
||||
@@ -1261,6 +1263,59 @@ OAuthAppInfo
|
||||
)
|
||||
return service_principals
|
||||
|
||||
async def _get_app_registrations(self) -> Dict[str, "AppRegistration"]:
|
||||
"""Retrieve application registrations from Microsoft Entra.
|
||||
|
||||
Fetches every application object and its password credentials (client
|
||||
secrets) across all pages. Customer-owned applications should
|
||||
authenticate using certificates, federated identity credentials, or
|
||||
managed identities, so any entry in ``passwordCredentials`` is reported
|
||||
by the related check.
|
||||
|
||||
Returns:
|
||||
Dict[str, AppRegistration]: Application registrations keyed by the
|
||||
application object ID.
|
||||
"""
|
||||
logger.info("Entra - Getting app registrations...")
|
||||
app_registrations: Dict[str, AppRegistration] = {}
|
||||
try:
|
||||
app_response = await self.client.applications.get()
|
||||
while app_response:
|
||||
for app in getattr(app_response, "value", []) or []:
|
||||
app_id = getattr(app, "app_id", None)
|
||||
object_id = getattr(app, "id", None)
|
||||
if not app_id or not object_id:
|
||||
continue
|
||||
|
||||
password_credentials = []
|
||||
for cred in getattr(app, "password_credentials", []) or []:
|
||||
password_credentials.append(
|
||||
PasswordCredential(
|
||||
key_id=str(getattr(cred, "key_id", "")),
|
||||
display_name=getattr(cred, "display_name", None),
|
||||
start_date_time=getattr(cred, "start_date_time", None),
|
||||
end_date_time=getattr(cred, "end_date_time", None),
|
||||
)
|
||||
)
|
||||
|
||||
app_registrations[object_id] = AppRegistration(
|
||||
id=object_id,
|
||||
app_id=app_id,
|
||||
name=getattr(app, "display_name", "") or "",
|
||||
password_credentials=password_credentials,
|
||||
)
|
||||
|
||||
next_link = getattr(app_response, "odata_next_link", None)
|
||||
if not next_link:
|
||||
break
|
||||
app_response = await self.client.applications.with_url(next_link).get()
|
||||
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
return app_registrations
|
||||
|
||||
|
||||
class ConditionalAccessPolicyState(Enum):
|
||||
ENABLED = "enabled"
|
||||
@@ -1700,12 +1755,15 @@ class PasswordCredential(BaseModel):
|
||||
Attributes:
|
||||
key_id: The unique identifier of the credential.
|
||||
display_name: The optional display name of the credential.
|
||||
start_date_time: The time at which the credential becomes valid.
|
||||
``None`` when the API does not report it.
|
||||
end_date_time: The expiration time of the credential. ``None`` indicates
|
||||
the secret has no recorded expiry and is treated as active.
|
||||
"""
|
||||
|
||||
key_id: str
|
||||
display_name: Optional[str] = None
|
||||
start_date_time: Optional[datetime] = None
|
||||
end_date_time: Optional[datetime] = None
|
||||
|
||||
def is_active(self, now: Optional[datetime] = None) -> bool:
|
||||
@@ -1783,3 +1841,20 @@ class ServicePrincipal(BaseModel):
|
||||
password_credentials: List[PasswordCredential] = []
|
||||
key_credentials: List[KeyCredential] = []
|
||||
directory_role_template_ids: List[str] = []
|
||||
|
||||
|
||||
class AppRegistration(BaseModel):
|
||||
"""Model representing a Microsoft Entra ID application registration.
|
||||
|
||||
Attributes:
|
||||
id: The application object's unique identifier.
|
||||
app_id: The application (client) ID.
|
||||
name: The application's display name.
|
||||
password_credentials: List of password credentials (client secrets)
|
||||
registered on the application.
|
||||
"""
|
||||
|
||||
id: str
|
||||
app_id: str = ""
|
||||
name: str = ""
|
||||
password_credentials: List[PasswordCredential] = []
|
||||
|
||||
+1
-1
@@ -120,7 +120,7 @@ maintainers = [{name = "Prowler Engineering", email = "engineering@prowler.com"}
|
||||
name = "prowler"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10,<3.13"
|
||||
version = "5.27.0"
|
||||
version = "5.28.0"
|
||||
|
||||
[project.scripts]
|
||||
prowler = "prowler.__main__:prowler"
|
||||
|
||||
+3
-3
@@ -36,7 +36,7 @@ After running setup, restart your AI coding assistant to load the skills.
|
||||
|
||||
Skills are automatically discovered by the AI agent. To manually load a skill during a session:
|
||||
|
||||
```text
|
||||
```
|
||||
Read skills/{skill-name}/SKILL.md
|
||||
```
|
||||
|
||||
@@ -90,7 +90,7 @@ Patterns tailored for Prowler development:
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```text
|
||||
```
|
||||
skills/
|
||||
├── {skill-name}/
|
||||
│ ├── SKILL.md # Required - main instrunsction and metadata
|
||||
@@ -118,7 +118,7 @@ This reads `metadata.scope` and `metadata.auto_invoke` from each `SKILL.md` and
|
||||
|
||||
Use the `skill-creator` skill for guidance:
|
||||
|
||||
```text
|
||||
```
|
||||
Read skills/skill-creator/SKILL.md
|
||||
```
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ When implementing a new endpoint, review these patterns in order:
|
||||
## Decision Trees
|
||||
|
||||
### Which Serializer?
|
||||
```text
|
||||
```
|
||||
GET list/retrieve → <Model>Serializer
|
||||
POST create → <Model>CreateSerializer
|
||||
PATCH update → <Model>UpdateSerializer
|
||||
@@ -62,7 +62,7 @@ PATCH update → <Model>UpdateSerializer
|
||||
```
|
||||
|
||||
### Which Base Serializer?
|
||||
```text
|
||||
```
|
||||
Read-only serializer → BaseModelSerializerV1
|
||||
Create with tenant_id → RLSSerializer + BaseWriteSerializer (auto-injects tenant_id on create)
|
||||
Update with validation → BaseWriteSerializer (tenant_id already exists on object)
|
||||
@@ -70,14 +70,14 @@ Non-model data → BaseSerializerV1
|
||||
```
|
||||
|
||||
### Which Filter Base?
|
||||
```text
|
||||
```
|
||||
Direct FK to Provider → BaseProviderFilter
|
||||
FK via Scan → BaseScanProviderFilter
|
||||
No provider relation → FilterSet
|
||||
```
|
||||
|
||||
### Which Base ViewSet?
|
||||
```text
|
||||
```
|
||||
RLS-protected model → BaseRLSViewSet (most common)
|
||||
Tenant operations → BaseTenantViewset
|
||||
User operations → BaseUserViewset
|
||||
@@ -85,7 +85,7 @@ No RLS required → BaseViewSet (rare)
|
||||
```
|
||||
|
||||
### Resource Name Format?
|
||||
```text
|
||||
```
|
||||
Single word model → plural lowercase (Provider → providers)
|
||||
Multi-word model → plural lowercase kebab (ProviderGroup → provider-groups)
|
||||
Through/join model → parent-child pattern (UserRoleRelationship → user-roles)
|
||||
@@ -490,7 +490,7 @@ When implementing or debugging, query these libraries via `mcp_context7_query-do
|
||||
| **drf-spectacular** | `/tfranzel/drf-spectacular` | OpenAPI schema, `@extend_schema` |
|
||||
|
||||
**Example queries:**
|
||||
```text
|
||||
```
|
||||
mcp_context7_query-docs(libraryId="/websites/django-rest-framework", query="ViewSet get_queryset best practices")
|
||||
mcp_context7_query-docs(libraryId="/tfranzel/drf-spectacular", query="extend_schema examples for custom actions")
|
||||
mcp_context7_query-docs(libraryId="/websites/djangoproject_en_5_2", query="model constraints and indexes")
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
## ViewSet Hierarchy
|
||||
|
||||
```text
|
||||
```
|
||||
BaseViewSet (minimal - no RLS/auth)
|
||||
│
|
||||
├── BaseRLSViewSet (+ tenant filtering, RLS-protected models)
|
||||
@@ -31,7 +31,7 @@ BaseViewSet (minimal - no RLS/auth)
|
||||
|
||||
## Serializer Hierarchy
|
||||
|
||||
```text
|
||||
```
|
||||
BaseModelSerializerV1 (JSON:API defaults, read_only_fields)
|
||||
│
|
||||
├── RLSSerializer (auto-injects tenant_id from request)
|
||||
@@ -47,7 +47,7 @@ BaseModelSerializerV1 (JSON:API defaults, read_only_fields)
|
||||
|
||||
## Filter Hierarchy
|
||||
|
||||
```text
|
||||
```
|
||||
FilterSet (django-filter)
|
||||
│
|
||||
├── CommonFindingFilters (mixin for date ranges, delta, status)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Content Type
|
||||
|
||||
```http
|
||||
```
|
||||
Content-Type: application/vnd.api+json
|
||||
Accept: application/vnd.api+json
|
||||
```
|
||||
|
||||
@@ -364,7 +364,7 @@ Batch utilities: `api/db_utils.py` (`batch_delete`, `create_objects_in_batches`,
|
||||
|
||||
## Decision tree
|
||||
|
||||
```text
|
||||
```
|
||||
Auto-generated migration?
|
||||
├── Yes → Split it following the rules below
|
||||
└── No → Review it against the rules below
|
||||
@@ -420,7 +420,7 @@ When implementing or debugging migration patterns, query these libraries via `mc
|
||||
| django-postgres-extra | `/SectorLabs/django-postgres-extra` | Partitioned models, `PostgresPartitionedModel`, partition management |
|
||||
|
||||
**Example queries:**
|
||||
```text
|
||||
```
|
||||
mcp_context7_query-docs(libraryId="/websites/djangoproject_en_5_1", query="migration operations AddIndex RunPython atomic")
|
||||
mcp_context7_query-docs(libraryId="/websites/djangoproject_en_5_1", query="database indexes Meta class concurrently")
|
||||
mcp_context7_query-docs(libraryId="/websites/postgresql_org_docs_current", query="CREATE INDEX CONCURRENTLY partitioned table")
|
||||
|
||||
@@ -30,7 +30,7 @@ allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch
|
||||
|
||||
## File Layout
|
||||
|
||||
```text
|
||||
```
|
||||
.github/
|
||||
├── workflows/
|
||||
│ ├── {name}.md # Frontmatter + thin context dispatcher
|
||||
@@ -308,7 +308,7 @@ After modifying any `.github/workflows/*.md`:
|
||||
|
||||
Add to repo root so lock files auto-resolve on merge:
|
||||
|
||||
```text
|
||||
```
|
||||
.github/workflows/*.lock.yml linguist-generated=true merge=ours
|
||||
```
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ This skill focuses on **spec compliance**. For **implementation patterns** (View
|
||||
|
||||
If Context7 MCP is available, query the JSON:API spec directly:
|
||||
|
||||
```text
|
||||
```
|
||||
mcp_context7_resolve-library-id(query="jsonapi specification")
|
||||
mcp_context7_query-docs(libraryId="<resolved-id>", query="[specific topic: relationships, errors, etc.]")
|
||||
```
|
||||
@@ -44,7 +44,7 @@ mcp_context7_query-docs(libraryId="<resolved-id>", query="[specific topic: relat
|
||||
|
||||
If Context7 is not available, fetch from the official spec:
|
||||
|
||||
```text
|
||||
```
|
||||
WebFetch(url="https://jsonapi.org/format/", prompt="Extract rules for [specific topic]")
|
||||
```
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch, WebSearch, Task
|
||||
|
||||
## App Router File Conventions
|
||||
|
||||
```text
|
||||
```
|
||||
app/
|
||||
├── layout.tsx # Root layout (required)
|
||||
├── page.tsx # Home page (/)
|
||||
|
||||
@@ -36,7 +36,7 @@ allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch, WebSearch, Task
|
||||
|
||||
## File Structure
|
||||
|
||||
```text
|
||||
```
|
||||
tests/
|
||||
├── base-page.ts # Parent class for ALL pages
|
||||
├── helpers.ts # Shared utilities
|
||||
@@ -182,14 +182,14 @@ export class SignUpPage extends BasePage {
|
||||
|
||||
## Refactoring Guidelines
|
||||
|
||||
### Move to `BasePage` when
|
||||
### Move to `BasePage` when:
|
||||
- ✅ Navigation helpers used by multiple pages (`waitForPageLoad()`, `getCurrentUrl()`)
|
||||
- ✅ Common UI interactions (notifications, modals, theme toggles)
|
||||
- ✅ Verification patterns repeated across pages (`isVisible()`, `waitForVisible()`)
|
||||
- ✅ Error handling that applies to all pages
|
||||
- ✅ Screenshot utilities for debugging
|
||||
|
||||
### Move to `helpers.ts` when
|
||||
### Move to `helpers.ts` when:
|
||||
- ✅ Test data generation (`generateUniqueEmail()`, `generateTestUser()`)
|
||||
- ✅ Setup/teardown utilities (`createTestUser()`, `cleanupTestData()`)
|
||||
- ✅ Custom assertions (`expectNotificationToContain()`)
|
||||
|
||||
@@ -225,7 +225,7 @@ Rebuild invalid indexes without locking writes:
|
||||
REINDEX INDEX CONCURRENTLY index_name;
|
||||
```
|
||||
|
||||
### Understanding _ccnew and_ccold artifacts
|
||||
### Understanding _ccnew and _ccold artifacts
|
||||
|
||||
When `CREATE INDEX CONCURRENTLY` or `REINDEX INDEX CONCURRENTLY` is interrupted, temporary indexes may remain:
|
||||
|
||||
@@ -377,8 +377,7 @@ VACUUM (ANALYZE) table_name;
|
||||
| PostgreSQL | `/websites/postgresql_org_docs_current` | Index types, EXPLAIN, partitioned table indexing, REINDEX |
|
||||
|
||||
**Example queries:**
|
||||
|
||||
```text
|
||||
```
|
||||
mcp_context7_query-docs(libraryId="/websites/postgresql_org_docs_current", query="CREATE INDEX CONCURRENTLY partitioned table")
|
||||
mcp_context7_query-docs(libraryId="/websites/postgresql_org_docs_current", query="EXPLAIN ANALYZE BUFFERS query plan")
|
||||
mcp_context7_query-docs(libraryId="/websites/postgresql_org_docs_current", query="partial index WHERE clause")
|
||||
|
||||
@@ -61,7 +61,7 @@ Provider.objects.filter(connected=True) # Requires rls_transaction context
|
||||
|
||||
### RLS Transaction Flow
|
||||
|
||||
```text
|
||||
```
|
||||
Request → Authentication → BaseRLSViewSet.initial()
|
||||
│
|
||||
├─ Extract tenant_id from JWT
|
||||
@@ -92,7 +92,7 @@ When implementing Prowler-specific API features:
|
||||
## Decision Trees
|
||||
|
||||
### Which Base Model?
|
||||
```text
|
||||
```
|
||||
Tenant-scoped data → RowLevelSecurityProtectedModel
|
||||
Global/shared data → models.Model + BaseSecurityConstraint (rare)
|
||||
Partitioned time-series → PostgresPartitionedModel + RowLevelSecurityProtectedModel
|
||||
@@ -100,14 +100,14 @@ Soft-deletable → Add is_deleted + ActiveProviderManager
|
||||
```
|
||||
|
||||
### Which Manager?
|
||||
```text
|
||||
```
|
||||
Normal queries → Model.objects (excludes deleted)
|
||||
Include deleted records → Model.all_objects
|
||||
Celery task context → Must use rls_transaction() first
|
||||
```
|
||||
|
||||
### Which Database?
|
||||
```text
|
||||
```
|
||||
Standard API queries → default (automatic via ViewSet)
|
||||
Read-only operations → replica (automatic for GET in BaseRLSViewSet)
|
||||
Auth/admin operations → MainRouter.admin_db
|
||||
@@ -115,7 +115,7 @@ Cross-tenant lookups → MainRouter.admin_db (use sparingly!)
|
||||
```
|
||||
|
||||
### Celery Task Decorator Order?
|
||||
```python
|
||||
```
|
||||
@shared_task(base=RLSTask, name="...", queue="...")
|
||||
@set_tenant # First: sets tenant context
|
||||
@handle_provider_deletion # Second: handles deleted providers
|
||||
@@ -496,7 +496,7 @@ When implementing or debugging Prowler-specific patterns, query these libraries
|
||||
| **Django** | `/websites/djangoproject_en_5_2` | Models, ORM, constraints, indexes |
|
||||
|
||||
**Example queries:**
|
||||
```text
|
||||
```
|
||||
mcp_context7_query-docs(libraryId="/websites/celeryq_dev_en_stable", query="shared_task decorator retry patterns")
|
||||
mcp_context7_query-docs(libraryId="/celery/django-celery-beat", query="periodic task database scheduler")
|
||||
mcp_context7_query-docs(libraryId="/websites/djangoproject_en_5_2", query="model constraints CheckConstraint UniqueConstraint")
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Settings File Structure
|
||||
|
||||
```text
|
||||
```
|
||||
api/src/backend/config/
|
||||
├── django/
|
||||
│ ├── base.py # Base settings (all environments)
|
||||
|
||||
@@ -247,7 +247,7 @@ class JSONAPIMeta:
|
||||
|
||||
## Decision Tree: New Model
|
||||
|
||||
```text
|
||||
```
|
||||
Is it tenant-scoped data?
|
||||
├── Yes → Inherit RowLevelSecurityProtectedModel
|
||||
│ Add RowLevelSecurityConstraint
|
||||
|
||||
@@ -228,7 +228,7 @@ AWS_QUERIES: list[AttackPathsQueryDefinition] = [
|
||||
|
||||
**FIRST**, read all files in the queries module to understand the structure, type definitions, registration, and existing style:
|
||||
|
||||
```text
|
||||
```
|
||||
api/src/backend/api/attack_paths/queries/
|
||||
├── __init__.py # Module exports
|
||||
├── types.py # AttackPathsQueryDefinition, AttackPathsQueryParameterDefinition
|
||||
@@ -250,7 +250,7 @@ grep cartography api/pyproject.toml
|
||||
|
||||
Build the schema URL (ALWAYS use the specific tag, not master/main):
|
||||
|
||||
```text
|
||||
```
|
||||
# Git dependency (prowler-cloud/cartography@0.126.1):
|
||||
https://raw.githubusercontent.com/prowler-cloud/cartography/refs/tags/0.126.1/docs/root/modules/{provider}/schema.md
|
||||
|
||||
@@ -283,7 +283,7 @@ Add the constant to the `{PROVIDER}_QUERIES` list.
|
||||
|
||||
### Query ID
|
||||
|
||||
```text
|
||||
```
|
||||
{provider}-{category}-{description}
|
||||
```
|
||||
|
||||
@@ -291,7 +291,7 @@ Examples: `aws-ec2-privesc-passrole-iam`, `aws-ec2-instances-internet-exposed`
|
||||
|
||||
### Query constant name
|
||||
|
||||
```text
|
||||
```
|
||||
{PROVIDER}_{CATEGORY}_{DESCRIPTION}
|
||||
```
|
||||
|
||||
|
||||
@@ -132,6 +132,19 @@ Before editing any `CHANGELOG.md`, always inspect the active release boundary:
|
||||
|
||||
**Do not trust the current topmost matching section name.** A released block can contain the same section heading (`### 🚀 Added`, `### 🔄 Changed`, etc.). Always anchor edits to the `Prowler UNRELEASED` version block first.
|
||||
|
||||
## Mandatory Human Confirmation Gate
|
||||
|
||||
Before creating or editing any changelog file (`CHANGELOG.md`), the agent MUST stop and get explicit user confirmation. This applies even when the changelog gate is failing, the required edit seems obvious, or the user asked to "fix the changelog".
|
||||
|
||||
Present the proposed changelog action before writing:
|
||||
|
||||
1. Target file path.
|
||||
2. Target version block and section.
|
||||
3. Exact entry to add, move, remove, or rewrite.
|
||||
4. Reason the changelog is needed.
|
||||
|
||||
Only proceed after an explicit approval such as "confirm", "approved", "sí", or equivalent. If the user rejects or does not answer, do not edit or create the changelog. Offer alternatives such as adding `no-changelog` when appropriate.
|
||||
|
||||
## Adding a Changelog Entry
|
||||
|
||||
### Step 1: Determine Affected Component(s)
|
||||
|
||||
@@ -28,7 +28,7 @@ metadata:
|
||||
|
||||
## Commit Format
|
||||
|
||||
```text
|
||||
```
|
||||
type(scope): concise description
|
||||
|
||||
- Key change 1
|
||||
@@ -68,7 +68,7 @@ type(scope): concise description
|
||||
|
||||
### Title Line
|
||||
|
||||
```text
|
||||
```
|
||||
# GOOD - Concise and clear
|
||||
feat(api): add provider connection retry logic
|
||||
fix(ui): resolve dashboard loading state
|
||||
@@ -83,7 +83,7 @@ fix(ui): fix the bug in dashboard component on line 45
|
||||
|
||||
### Body (Bullet Points)
|
||||
|
||||
```text
|
||||
```
|
||||
# GOOD - High-level changes
|
||||
- Add retry mechanism for failed connections
|
||||
- Document task composition patterns
|
||||
@@ -132,7 +132,7 @@ fix(ui): fix the bug in dashboard component on line 45
|
||||
|
||||
## Decision Tree
|
||||
|
||||
```text
|
||||
```
|
||||
Single file changed?
|
||||
├─ Yes → May omit body, title only
|
||||
└─ No → Include body with key changes
|
||||
|
||||
@@ -53,7 +53,7 @@ diff dashboard/compliance/{new_framework}.py \
|
||||
|
||||
## Decision Tree
|
||||
|
||||
```text
|
||||
```
|
||||
JSON Valid?
|
||||
├── No → FAIL: Fix JSON syntax errors
|
||||
└── Yes ↓
|
||||
|
||||
@@ -59,7 +59,7 @@ See "Compliance Framework Location" and "Framework-Specific Attribute Structures
|
||||
|
||||
**Every framework directory follows this exact convention** — do not deviate:
|
||||
|
||||
```text
|
||||
```
|
||||
{framework}/
|
||||
├── __init__.py
|
||||
├── {framework}.py # ONLY get_{framework}_table() — NO function docstring
|
||||
@@ -85,7 +85,7 @@ See "Compliance Framework Location" and "Framework-Specific Attribute Structures
|
||||
|
||||
### The CLI Pipeline (end-to-end)
|
||||
|
||||
```text
|
||||
```
|
||||
prowler aws --compliance ccc_aws
|
||||
↓
|
||||
Compliance.get_bulk("aws") → parses prowler/compliance/aws/*.json
|
||||
@@ -483,7 +483,6 @@ Prowler ThreatScore is a custom security scoring framework developed by Prowler
|
||||
## Available Compliance Frameworks
|
||||
|
||||
### AWS (41 frameworks)
|
||||
|
||||
| Framework | File Name |
|
||||
|-----------|-----------|
|
||||
| CIS 1.4, 1.5, 2.0, 3.0, 4.0, 5.0 | `cis_{version}_aws.json` |
|
||||
@@ -509,7 +508,6 @@ Prowler ThreatScore is a custom security scoring framework developed by Prowler
|
||||
| NIS2 | `nis2_aws.json` |
|
||||
|
||||
### Azure (15+ frameworks)
|
||||
|
||||
| Framework | File Name |
|
||||
|-----------|-----------|
|
||||
| CIS 2.0, 2.1, 3.0, 4.0 | `cis_{version}_azure.json` |
|
||||
@@ -520,7 +518,6 @@ Prowler ThreatScore is a custom security scoring framework developed by Prowler
|
||||
| NIST CSF 2.0 | `nist_csf_2.0_azure.json` |
|
||||
|
||||
### GCP (15+ frameworks)
|
||||
|
||||
| Framework | File Name |
|
||||
|-----------|-----------|
|
||||
| CIS 2.0, 3.0, 4.0 | `cis_{version}_gcp.json` |
|
||||
@@ -531,7 +528,6 @@ Prowler ThreatScore is a custom security scoring framework developed by Prowler
|
||||
| NIST CSF 2.0 | `nist_csf_2.0_gcp.json` |
|
||||
|
||||
### Kubernetes (6 frameworks)
|
||||
|
||||
| Framework | File Name |
|
||||
|-----------|-----------|
|
||||
| CIS 1.8, 1.10, 1.11 | `cis_{version}_kubernetes.json` |
|
||||
@@ -565,7 +561,7 @@ done
|
||||
|
||||
The sync tooling is split into three layers so adding a new framework only takes a YAML config (and optionally a new parser module for an unfamiliar upstream format):
|
||||
|
||||
```text
|
||||
```
|
||||
skills/prowler-compliance/assets/
|
||||
├── sync_framework.py # generic runner — works for any framework
|
||||
├── configs/
|
||||
@@ -906,7 +902,7 @@ Add fixtures to `tests/lib/outputs/compliance/fixtures.py`: one `Compliance` obj
|
||||
|
||||
**The table dispatcher file (`{framework}.py`) MUST NOT import `Finding`** (directly or transitively). The cycle is:
|
||||
|
||||
```text
|
||||
```
|
||||
compliance.compliance imports get_{framework}_table
|
||||
→ {framework}.py imports ComplianceOutput
|
||||
→ compliance_output imports Finding
|
||||
|
||||
@@ -46,7 +46,7 @@ Each framework type has a specific Pydantic model in `compliance_models.py`:
|
||||
|
||||
## File Naming Convention
|
||||
|
||||
```text
|
||||
```
|
||||
{framework}_{version}_{provider}.json
|
||||
```
|
||||
|
||||
|
||||
@@ -104,7 +104,7 @@ Reference without articles:
|
||||
|
||||
## Documentation Structure
|
||||
|
||||
```text
|
||||
```
|
||||
docs/
|
||||
├── getting-started/
|
||||
├── tutorials/
|
||||
|
||||
@@ -25,7 +25,7 @@ Use this skill when:
|
||||
|
||||
Every provider MUST follow this structure:
|
||||
|
||||
```text
|
||||
```
|
||||
prowler/providers/{provider}/
|
||||
├── __init__.py
|
||||
├── {provider}_provider.py # Main provider class
|
||||
|
||||
@@ -34,7 +34,7 @@ python3 prowler-cli.py <provider> --list-<metric>
|
||||
|
||||
The CLI output ends with a summary line like:
|
||||
|
||||
```text
|
||||
```
|
||||
There are 572 available checks.
|
||||
There is 1 available Compliance Framework.
|
||||
```
|
||||
|
||||
@@ -16,7 +16,7 @@ allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch, WebSearch, Task
|
||||
|
||||
## Check Structure
|
||||
|
||||
```text
|
||||
```
|
||||
prowler/providers/{provider}/services/{service}/{check_name}/
|
||||
├── __init__.py
|
||||
├── {check_name}.py
|
||||
@@ -90,7 +90,7 @@ See `prowler-test-sdk` skill for test patterns (PASS, FAIL, no resources, error
|
||||
|
||||
## Check Naming Convention
|
||||
|
||||
```text
|
||||
```
|
||||
{service}_{resource}_{security_control}
|
||||
```
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch, WebSearch, Task
|
||||
|
||||
## 1. Fixture Dependency Chain
|
||||
|
||||
```text
|
||||
```
|
||||
create_test_user (session) ─► tenants_fixture (function) ─► authenticated_client
|
||||
│
|
||||
└─► providers_fixture ─► scans_fixture ─► findings_fixture
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
## Fixture Dependency Graph
|
||||
|
||||
```text
|
||||
```
|
||||
create_test_user (session)
|
||||
│
|
||||
└─► tenants_fixture (function)
|
||||
|
||||
@@ -265,7 +265,7 @@ from tests.providers.kubernetes.kubernetes_fixtures import set_mocked_kubernetes
|
||||
|
||||
## Test File Structure
|
||||
|
||||
```text
|
||||
```
|
||||
tests/providers/{provider}/services/{service}/
|
||||
├── {service}_service_test.py # Service tests
|
||||
└── {check_name}/
|
||||
|
||||
@@ -19,7 +19,7 @@ allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch, WebSearch, Task
|
||||
|
||||
## Prowler UI Test Structure
|
||||
|
||||
```text
|
||||
```
|
||||
ui/tests/
|
||||
├── base-page.ts # Prowler-specific base page
|
||||
├── helpers.ts # Prowler test utilities
|
||||
@@ -35,13 +35,13 @@ ui/tests/
|
||||
|
||||
**⚠️ ALWAYS verify BEFORE completing any E2E task:**
|
||||
|
||||
### When CREATING new tests
|
||||
### When CREATING new tests:
|
||||
- [ ] `{page-name}-page.ts` - Page Object created/updated
|
||||
- [ ] `{page-name}.spec.ts` - Tests added with correct tags (@TEST-ID)
|
||||
- [ ] `{page-name}.md` - Documentation created with ALL test cases
|
||||
- [ ] Test IDs in `.md` match tags in `.spec.ts`
|
||||
|
||||
### When MODIFYING existing tests
|
||||
### When MODIFYING existing tests:
|
||||
- [ ] `{page-name}.md` MUST be updated if:
|
||||
- Test cases were added/removed
|
||||
- Test flow changed (steps)
|
||||
@@ -49,7 +49,7 @@ ui/tests/
|
||||
- Tags or priorities changed
|
||||
- [ ] Test IDs synchronized between `.md` and `.spec.ts`
|
||||
|
||||
### Quick validation
|
||||
### Quick validation:
|
||||
```bash
|
||||
# Verify .md exists for each test folder
|
||||
ls ui/tests/{feature}/{feature}.md
|
||||
@@ -59,8 +59,7 @@ grep -o "@[A-Z]*-E2E-[0-9]*" ui/tests/{feature}/{feature}.spec.ts | sort -u
|
||||
grep -o "\`[A-Z]*-E2E-[0-9]*\`" ui/tests/{feature}/{feature}.md | sort -u
|
||||
```
|
||||
|
||||
> [!IMPORTANT]
|
||||
> ❌ An E2E change is NOT considered complete without updating the corresponding `.md` file.
|
||||
**❌ An E2E change is NOT considered complete without updating the corresponding .md file**
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch, WebSearch, Task
|
||||
|
||||
## Tech Stack (Versions)
|
||||
|
||||
```text
|
||||
```
|
||||
Next.js 16.2.3 | React 19.2.5 | Tailwind 4.1.18 | shadcn/ui
|
||||
Zod 4.1.11 | React Hook Form 7.62.0 | Zustand 5.0.8
|
||||
NextAuth 5.0.0-beta.30 | Recharts 2.15.4
|
||||
@@ -43,7 +43,7 @@ HeroUI 2.8.4 (LEGACY - do not add new components)
|
||||
|
||||
### Component Placement
|
||||
|
||||
```text
|
||||
```
|
||||
New feature UI? → shadcn/ui + Tailwind
|
||||
Existing HeroUI feature? → Keep HeroUI (don't mix)
|
||||
Used 1 feature? → features/{feature}/components/
|
||||
@@ -54,7 +54,7 @@ Server component? → No directive needed
|
||||
|
||||
### Code Location
|
||||
|
||||
```text
|
||||
```
|
||||
Server action → actions/{feature}/{feature}.ts
|
||||
Data transform → actions/{feature}/{feature}.adapter.ts
|
||||
Types (shared 2+) → types/{domain}.ts
|
||||
@@ -69,7 +69,7 @@ HeroUI components → components/ui/ (LEGACY)
|
||||
|
||||
### Styling Decision
|
||||
|
||||
```text
|
||||
```
|
||||
Tailwind class exists? → className
|
||||
Dynamic value? → style prop
|
||||
Conditional styles? → cn()
|
||||
@@ -85,7 +85,7 @@ Recharts/library? → CHART_COLORS constant + var()
|
||||
|
||||
## Project Structure
|
||||
|
||||
```text
|
||||
```
|
||||
ui/
|
||||
├── app/
|
||||
│ ├── (auth)/ # Auth pages (login, signup)
|
||||
|
||||
@@ -29,7 +29,7 @@ Create a skill when:
|
||||
|
||||
## Skill Structure
|
||||
|
||||
```text
|
||||
```
|
||||
skills/{skill-name}/
|
||||
├── SKILL.md # Required - main skill file
|
||||
├── assets/ # Optional - templates, schemas, examples
|
||||
@@ -43,7 +43,7 @@ skills/{skill-name}/
|
||||
|
||||
## SKILL.md Template
|
||||
|
||||
````markdown
|
||||
```markdown
|
||||
---
|
||||
name: {skill-name}
|
||||
description: >
|
||||
@@ -77,7 +77,7 @@ metadata:
|
||||
|
||||
- **Templates**: See [assets/](assets/) for {description}
|
||||
- **Documentation**: See [references/](references/) for local docs
|
||||
````
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -94,7 +94,7 @@ metadata:
|
||||
|
||||
## Decision: assets/ vs references/
|
||||
|
||||
```text
|
||||
```
|
||||
Need code templates? → assets/
|
||||
Need JSON schemas? → assets/
|
||||
Need example configs? → assets/
|
||||
@@ -108,7 +108,7 @@ Link to external guides? → references/ (with local path)
|
||||
|
||||
## Decision: Prowler-Specific vs Generic
|
||||
|
||||
```text
|
||||
```
|
||||
Patterns apply to ANY project? → Generic skill (e.g., pytest, typescript)
|
||||
Patterns are Prowler-specific? → prowler-{name} skill
|
||||
Generic skill needs Prowler info? → Add references/ pointing to Prowler docs
|
||||
|
||||
@@ -38,7 +38,7 @@ Use this skill when:
|
||||
|
||||
## Decision Tree
|
||||
|
||||
```text
|
||||
```
|
||||
{Question 1}? → {Action A}
|
||||
{Question 2}? → {Action B}
|
||||
Otherwise → {Default action}
|
||||
|
||||
@@ -14,7 +14,7 @@ allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch, WebSearch, Task
|
||||
|
||||
## Styling Decision Tree
|
||||
|
||||
```text
|
||||
```
|
||||
Tailwind class exists? → className="..."
|
||||
Dynamic value? → style={{ width: `${x}%` }}
|
||||
Conditional styles? → cn("base", condition && "variant")
|
||||
|
||||
+12
-12
@@ -20,7 +20,7 @@ allowed-tools: Read, Edit, Write, Glob, Grep, Bash, Task
|
||||
|
||||
## TDD Cycle (MANDATORY)
|
||||
|
||||
```text
|
||||
```
|
||||
+-----------------------------------------+
|
||||
| RED -> GREEN -> REFACTOR |
|
||||
| ^ | |
|
||||
@@ -28,7 +28,7 @@ allowed-tools: Read, Edit, Write, Glob, Grep, Bash, Task
|
||||
+-----------------------------------------+
|
||||
```
|
||||
|
||||
The question is NOT "should I write tests?" but "what tests do I need?"
|
||||
**The question is NOT "should I write tests?" but "what tests do I need?"**
|
||||
|
||||
---
|
||||
|
||||
@@ -94,7 +94,7 @@ uv run pytest api/src/backend/api/tests/test_models.py -v
|
||||
|
||||
### Decision Tree (All Stacks)
|
||||
|
||||
```text
|
||||
```
|
||||
+------------------------------------------+
|
||||
| Does test file exist for this code? |
|
||||
+----------+-----------------------+-------+
|
||||
@@ -122,7 +122,7 @@ uv run pytest api/src/backend/api/tests/test_models.py -v
|
||||
|
||||
### For NEW Functionality
|
||||
|
||||
#### UI (Vitest)
|
||||
**UI (Vitest)**
|
||||
|
||||
```typescript
|
||||
describe("PriceCalculator", () => {
|
||||
@@ -139,7 +139,7 @@ describe("PriceCalculator", () => {
|
||||
});
|
||||
```
|
||||
|
||||
#### SDK (pytest)
|
||||
**SDK (pytest)**
|
||||
|
||||
```python
|
||||
class Test_ec2_ami_public:
|
||||
@@ -159,7 +159,7 @@ class Test_ec2_ami_public:
|
||||
assert len(result) == 0
|
||||
```
|
||||
|
||||
#### API (pytest-django)
|
||||
**API (pytest-django)**
|
||||
|
||||
```python
|
||||
@pytest.mark.django_db
|
||||
@@ -192,18 +192,18 @@ Write a test that **reproduces the bug** first:
|
||||
|
||||
**API:** `assert response.status_code == 403 # Currently returns 200`
|
||||
|
||||
Run -> Should FAIL (reproducing the bug).
|
||||
**Run -> Should FAIL (reproducing the bug)**
|
||||
|
||||
### For REFACTORING
|
||||
|
||||
Capture ALL current behavior BEFORE refactoring:
|
||||
|
||||
```text
|
||||
```
|
||||
# Any stack: run ALL existing tests, they should PASS
|
||||
# This is your safety net - if any fail after refactoring, you broke something
|
||||
```
|
||||
|
||||
Run -> All should PASS (baseline).
|
||||
**Run -> All should PASS (baseline)**
|
||||
|
||||
---
|
||||
|
||||
@@ -288,13 +288,13 @@ Tests GREEN -> Improve code quality WITHOUT changing behavior.
|
||||
- Add types/validation
|
||||
- Reduce duplication
|
||||
|
||||
Run tests after EACH change -> Must stay GREEN.
|
||||
**Run tests after EACH change -> Must stay GREEN**
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
```text
|
||||
```
|
||||
+------------------------------------------------+
|
||||
| TDD WORKFLOW |
|
||||
+------------------------------------------------+
|
||||
@@ -320,7 +320,7 @@ Run tests after EACH change -> Must stay GREEN.
|
||||
|
||||
## Anti-Patterns (NEVER DO)
|
||||
|
||||
```python
|
||||
```
|
||||
# ANY language:
|
||||
|
||||
# 1. Code first, tests after
|
||||
|
||||
@@ -181,7 +181,7 @@ expect(screen.getByRole("button")).toBeDisabled();
|
||||
|
||||
## File Organization
|
||||
|
||||
```text
|
||||
```
|
||||
components/
|
||||
├── Button/
|
||||
│ ├── Button.tsx
|
||||
|
||||
+54
-7
@@ -93,6 +93,8 @@ class Test_s3_bucket_shadow_resource_vulnerability:
|
||||
|
||||
@mock_aws
|
||||
def test_bucket_not_predictable(self):
|
||||
# A bucket whose name does not match any predictable service pattern
|
||||
# is not a shadow resource and must not produce any finding.
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
|
||||
aws_provider.identity.identity_arn = f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:root"
|
||||
|
||||
@@ -113,6 +115,55 @@ class Test_s3_bucket_shadow_resource_vulnerability:
|
||||
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",
|
||||
return_value=aws_provider,
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.aws.services.s3.s3_bucket_shadow_resource_vulnerability.s3_bucket_shadow_resource_vulnerability.s3_client",
|
||||
new=s3_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.aws.services.s3.s3_bucket_shadow_resource_vulnerability.s3_bucket_shadow_resource_vulnerability import (
|
||||
s3_bucket_shadow_resource_vulnerability,
|
||||
)
|
||||
|
||||
check = s3_bucket_shadow_resource_vulnerability()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 0
|
||||
|
||||
@mock_aws
|
||||
def test_only_predictable_bucket_reported_among_many(self):
|
||||
# With a mix of buckets, only the one matching a predictable pattern
|
||||
# must produce a finding; the rest must be silent.
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
|
||||
aws_provider.identity.identity_arn = f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:root"
|
||||
|
||||
predictable_bucket = f"sagemaker-{AWS_REGION_US_EAST_1}-{AWS_ACCOUNT_NUMBER}"
|
||||
plain_buckets = [
|
||||
"config-bucket-data",
|
||||
"my-app-data-bucket",
|
||||
"guardduty-findings-store",
|
||||
]
|
||||
|
||||
s3_client = mock.MagicMock()
|
||||
s3_client.audited_canonical_id = AWS_ACCOUNT_NUMBER
|
||||
s3_client.audited_partition = "aws"
|
||||
s3_client.buckets = {
|
||||
name: Bucket(
|
||||
name=name,
|
||||
arn=f"arn:aws:s3:::{name}",
|
||||
region=AWS_REGION_US_EAST_1,
|
||||
owner_id=AWS_ACCOUNT_NUMBER,
|
||||
tags=[],
|
||||
)
|
||||
for name in [predictable_bucket, *plain_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",
|
||||
@@ -132,14 +183,10 @@ class Test_s3_bucket_shadow_resource_vulnerability:
|
||||
|
||||
assert len(result) == 1
|
||||
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
|
||||
assert report.resource_id == predictable_bucket
|
||||
assert "SageMaker" in report.status_extended
|
||||
assert "is correctly owned by the audited account" in report.status_extended
|
||||
|
||||
@mock_aws
|
||||
def test_shadow_resource_in_other_account(self):
|
||||
|
||||
+197
@@ -264,3 +264,200 @@ class Test_sqlserver_tde_encrypted_with_cmk:
|
||||
assert result[0].resource_name == sql_server_name
|
||||
assert result[0].resource_id == sql_server_id
|
||||
assert result[0].location == "location"
|
||||
|
||||
def test_sql_servers_master_database_disabled_user_database_enabled(self):
|
||||
# System "master" database always reports TDE Disabled in Azure SQL
|
||||
# and is not customer-controllable. It must not fail a server whose
|
||||
# user databases are correctly encrypted with CMK (PROWLER-1760).
|
||||
sqlserver_client = mock.MagicMock
|
||||
sqlserver_client.subscriptions = {
|
||||
AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME
|
||||
}
|
||||
sql_server_name = "SQL Server Name"
|
||||
sql_server_id = str(uuid4())
|
||||
master_database = Database(
|
||||
id="master_id",
|
||||
name="master",
|
||||
type="type",
|
||||
location="location",
|
||||
managed_by="managed_by",
|
||||
tde_encryption=TransparentDataEncryption(status="Disabled"),
|
||||
)
|
||||
user_database = Database(
|
||||
id="user_id",
|
||||
name="DynamicBudgets_Intacct",
|
||||
type="type",
|
||||
location="location",
|
||||
managed_by="managed_by",
|
||||
tde_encryption=TransparentDataEncryption(status="Enabled"),
|
||||
)
|
||||
sqlserver_client.sql_servers = {
|
||||
AZURE_SUBSCRIPTION_ID: [
|
||||
Server(
|
||||
id=sql_server_id,
|
||||
name=sql_server_name,
|
||||
location="location",
|
||||
public_network_access="",
|
||||
minimal_tls_version="",
|
||||
administrators=None,
|
||||
auditing_policies=None,
|
||||
firewall_rules=None,
|
||||
databases=[master_database, user_database],
|
||||
encryption_protector=EncryptionProtector(
|
||||
server_key_type="AzureKeyVault"
|
||||
),
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_azure_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.azure.services.sqlserver.sqlserver_tde_encrypted_with_cmk.sqlserver_tde_encrypted_with_cmk.sqlserver_client",
|
||||
new=sqlserver_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.azure.services.sqlserver.sqlserver_tde_encrypted_with_cmk.sqlserver_tde_encrypted_with_cmk import (
|
||||
sqlserver_tde_encrypted_with_cmk,
|
||||
)
|
||||
|
||||
check = sqlserver_tde_encrypted_with_cmk()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has TDE enabled with CMK."
|
||||
)
|
||||
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
|
||||
assert result[0].resource_name == sql_server_name
|
||||
assert result[0].resource_id == sql_server_id
|
||||
assert result[0].location == "location"
|
||||
|
||||
def test_sql_servers_only_master_database(self):
|
||||
# A server whose only database is the system "master" has no user
|
||||
# databases to evaluate, so it must not produce a finding.
|
||||
sqlserver_client = mock.MagicMock
|
||||
sqlserver_client.subscriptions = {
|
||||
AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME
|
||||
}
|
||||
sql_server_name = "SQL Server Name"
|
||||
sql_server_id = str(uuid4())
|
||||
master_database = Database(
|
||||
id="master_id",
|
||||
name="MASTER",
|
||||
type="type",
|
||||
location="location",
|
||||
managed_by="managed_by",
|
||||
tde_encryption=TransparentDataEncryption(status="Disabled"),
|
||||
)
|
||||
sqlserver_client.sql_servers = {
|
||||
AZURE_SUBSCRIPTION_ID: [
|
||||
Server(
|
||||
id=sql_server_id,
|
||||
name=sql_server_name,
|
||||
location="location",
|
||||
public_network_access="",
|
||||
minimal_tls_version="",
|
||||
administrators=None,
|
||||
auditing_policies=None,
|
||||
firewall_rules=None,
|
||||
databases=[master_database],
|
||||
encryption_protector=EncryptionProtector(
|
||||
server_key_type="AzureKeyVault"
|
||||
),
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_azure_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.azure.services.sqlserver.sqlserver_tde_encrypted_with_cmk.sqlserver_tde_encrypted_with_cmk.sqlserver_client",
|
||||
new=sqlserver_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.azure.services.sqlserver.sqlserver_tde_encrypted_with_cmk.sqlserver_tde_encrypted_with_cmk import (
|
||||
sqlserver_tde_encrypted_with_cmk,
|
||||
)
|
||||
|
||||
check = sqlserver_tde_encrypted_with_cmk()
|
||||
result = check.execute()
|
||||
assert len(result) == 0
|
||||
|
||||
def test_sql_servers_master_disabled_user_database_disabled(self):
|
||||
# Filtering out "master" must not mask a genuinely failing user
|
||||
# database: a disabled user DB still fails even with CMK.
|
||||
sqlserver_client = mock.MagicMock
|
||||
sqlserver_client.subscriptions = {
|
||||
AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME
|
||||
}
|
||||
sql_server_name = "SQL Server Name"
|
||||
sql_server_id = str(uuid4())
|
||||
master_database = Database(
|
||||
id="master_id",
|
||||
name="master",
|
||||
type="type",
|
||||
location="location",
|
||||
managed_by="managed_by",
|
||||
tde_encryption=TransparentDataEncryption(status="Disabled"),
|
||||
)
|
||||
user_database = Database(
|
||||
id="user_id",
|
||||
name="DynamicBudgets_Intacct",
|
||||
type="type",
|
||||
location="location",
|
||||
managed_by="managed_by",
|
||||
tde_encryption=TransparentDataEncryption(status="Disabled"),
|
||||
)
|
||||
sqlserver_client.sql_servers = {
|
||||
AZURE_SUBSCRIPTION_ID: [
|
||||
Server(
|
||||
id=sql_server_id,
|
||||
name=sql_server_name,
|
||||
location="location",
|
||||
public_network_access="",
|
||||
minimal_tls_version="",
|
||||
administrators=None,
|
||||
auditing_policies=None,
|
||||
firewall_rules=None,
|
||||
databases=[master_database, user_database],
|
||||
encryption_protector=EncryptionProtector(
|
||||
server_key_type="AzureKeyVault"
|
||||
),
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_azure_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.azure.services.sqlserver.sqlserver_tde_encrypted_with_cmk.sqlserver_tde_encrypted_with_cmk.sqlserver_client",
|
||||
new=sqlserver_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.azure.services.sqlserver.sqlserver_tde_encrypted_with_cmk.sqlserver_tde_encrypted_with_cmk import (
|
||||
sqlserver_tde_encrypted_with_cmk,
|
||||
)
|
||||
|
||||
check = sqlserver_tde_encrypted_with_cmk()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"SQL Server {sql_server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has TDE disabled with CMK."
|
||||
)
|
||||
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
|
||||
assert result[0].resource_name == sql_server_name
|
||||
assert result[0].resource_id == sql_server_id
|
||||
assert result[0].location == "location"
|
||||
|
||||
@@ -41,7 +41,7 @@ def set_mocked_gcp_provider(
|
||||
return provider
|
||||
|
||||
|
||||
def mock_api_client(GCPService, service, api_version, _):
|
||||
def mock_api_client(_GCPService, service, _api_version, _):
|
||||
client = MagicMock()
|
||||
|
||||
mock_api_projects_calls(client)
|
||||
@@ -703,6 +703,9 @@ def mock_api_instances_calls(client: MagicMock, service: str):
|
||||
"databaseVersion": "MYSQL_5_7",
|
||||
"region": "us-central1",
|
||||
"ipAddresses": [{"type": "PRIMARY", "ipAddress": "66.66.66.66"}],
|
||||
"diskEncryptionConfiguration": {
|
||||
"kmsKeyName": "projects/123/locations/us-central1/keyRings/keyring1/cryptoKeys/key1"
|
||||
},
|
||||
"settings": {
|
||||
"ipConfiguration": {
|
||||
"requireSsl": True,
|
||||
@@ -1323,6 +1326,7 @@ def mock_api_images_calls(client: MagicMock):
|
||||
client.images().list_next.return_value = None
|
||||
|
||||
def mock_get_image_iam_policy(project, resource):
|
||||
del project
|
||||
return_value = MagicMock()
|
||||
if resource == "test-image-1":
|
||||
return_value.execute.return_value = {
|
||||
|
||||
+277
@@ -0,0 +1,277 @@
|
||||
from unittest import mock
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from tests.providers.gcp.gcp_fixtures import (
|
||||
GCP_EU1_LOCATION,
|
||||
GCP_PROJECT_ID,
|
||||
mock_is_api_active,
|
||||
set_mocked_gcp_provider,
|
||||
)
|
||||
|
||||
|
||||
class Test_cloudsql_instance_cmek_encryption_enabled:
|
||||
def test_no_instances(self):
|
||||
cloudsql_client = mock.MagicMock()
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_gcp_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.gcp.services.cloudsql.cloudsql_instance_cmek_encryption_enabled.cloudsql_instance_cmek_encryption_enabled.cloudsql_client",
|
||||
new=cloudsql_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.gcp.services.cloudsql.cloudsql_instance_cmek_encryption_enabled.cloudsql_instance_cmek_encryption_enabled import (
|
||||
cloudsql_instance_cmek_encryption_enabled,
|
||||
)
|
||||
|
||||
cloudsql_client.instances = []
|
||||
check = cloudsql_instance_cmek_encryption_enabled()
|
||||
result = check.execute()
|
||||
assert len(result) == 0
|
||||
|
||||
def test_instance_cmek_enabled(self):
|
||||
cloudsql_client = mock.MagicMock()
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_gcp_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.gcp.services.cloudsql.cloudsql_instance_cmek_encryption_enabled.cloudsql_instance_cmek_encryption_enabled.cloudsql_client",
|
||||
new=cloudsql_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.gcp.services.cloudsql.cloudsql_instance_cmek_encryption_enabled.cloudsql_instance_cmek_encryption_enabled import (
|
||||
cloudsql_instance_cmek_encryption_enabled,
|
||||
)
|
||||
from prowler.providers.gcp.services.cloudsql.cloudsql_service import (
|
||||
Instance,
|
||||
)
|
||||
|
||||
cloudsql_client.instances = [
|
||||
Instance(
|
||||
name="db-cmek",
|
||||
version="POSTGRES_15",
|
||||
ip_addresses=[],
|
||||
region=GCP_EU1_LOCATION,
|
||||
public_ip=False,
|
||||
require_ssl=False,
|
||||
ssl_mode="ENCRYPTED_ONLY",
|
||||
automated_backups=True,
|
||||
authorized_networks=[],
|
||||
flags=[],
|
||||
project_id=GCP_PROJECT_ID,
|
||||
cmek_key_name="projects/123456789012/locations/europe-west1/keyRings/my-ring/cryptoKeys/my-key",
|
||||
)
|
||||
]
|
||||
check = cloudsql_instance_cmek_encryption_enabled()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert result[0].resource_id == "db-cmek"
|
||||
assert result[0].location == GCP_EU1_LOCATION
|
||||
assert result[0].project_id == GCP_PROJECT_ID
|
||||
|
||||
def test_instance_cmek_not_configured(self):
|
||||
cloudsql_client = mock.MagicMock()
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_gcp_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.gcp.services.cloudsql.cloudsql_instance_cmek_encryption_enabled.cloudsql_instance_cmek_encryption_enabled.cloudsql_client",
|
||||
new=cloudsql_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.gcp.services.cloudsql.cloudsql_instance_cmek_encryption_enabled.cloudsql_instance_cmek_encryption_enabled import (
|
||||
cloudsql_instance_cmek_encryption_enabled,
|
||||
)
|
||||
from prowler.providers.gcp.services.cloudsql.cloudsql_service import (
|
||||
Instance,
|
||||
)
|
||||
|
||||
cloudsql_client.instances = [
|
||||
Instance(
|
||||
name="db-google-managed",
|
||||
version="POSTGRES_15",
|
||||
ip_addresses=[],
|
||||
region=GCP_EU1_LOCATION,
|
||||
public_ip=False,
|
||||
require_ssl=False,
|
||||
ssl_mode="ENCRYPTED_ONLY",
|
||||
automated_backups=True,
|
||||
authorized_networks=[],
|
||||
flags=[],
|
||||
project_id=GCP_PROJECT_ID,
|
||||
cmek_key_name=None,
|
||||
)
|
||||
]
|
||||
check = cloudsql_instance_cmek_encryption_enabled()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert result[0].resource_id == "db-google-managed"
|
||||
assert result[0].location == GCP_EU1_LOCATION
|
||||
assert result[0].project_id == GCP_PROJECT_ID
|
||||
|
||||
def test_instance_cmek_empty_string(self):
|
||||
cloudsql_client = mock.MagicMock()
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_gcp_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.gcp.services.cloudsql.cloudsql_instance_cmek_encryption_enabled.cloudsql_instance_cmek_encryption_enabled.cloudsql_client",
|
||||
new=cloudsql_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.gcp.services.cloudsql.cloudsql_instance_cmek_encryption_enabled.cloudsql_instance_cmek_encryption_enabled import (
|
||||
cloudsql_instance_cmek_encryption_enabled,
|
||||
)
|
||||
from prowler.providers.gcp.services.cloudsql.cloudsql_service import (
|
||||
Instance,
|
||||
)
|
||||
|
||||
cloudsql_client.instances = [
|
||||
Instance(
|
||||
name="db-empty-key",
|
||||
version="POSTGRES_15",
|
||||
ip_addresses=[],
|
||||
region=GCP_EU1_LOCATION,
|
||||
public_ip=False,
|
||||
require_ssl=False,
|
||||
ssl_mode="ENCRYPTED_ONLY",
|
||||
automated_backups=True,
|
||||
authorized_networks=[],
|
||||
flags=[],
|
||||
project_id=GCP_PROJECT_ID,
|
||||
cmek_key_name="",
|
||||
)
|
||||
]
|
||||
check = cloudsql_instance_cmek_encryption_enabled()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert result[0].resource_id == "db-empty-key"
|
||||
|
||||
def test_unsupported_instance_type_skipped(self):
|
||||
cloudsql_client = mock.MagicMock()
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_gcp_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.gcp.services.cloudsql.cloudsql_instance_cmek_encryption_enabled.cloudsql_instance_cmek_encryption_enabled.cloudsql_client",
|
||||
new=cloudsql_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.gcp.services.cloudsql.cloudsql_instance_cmek_encryption_enabled.cloudsql_instance_cmek_encryption_enabled import (
|
||||
cloudsql_instance_cmek_encryption_enabled,
|
||||
)
|
||||
from prowler.providers.gcp.services.cloudsql.cloudsql_service import (
|
||||
Instance,
|
||||
)
|
||||
|
||||
cloudsql_client.instances = [
|
||||
Instance(
|
||||
name="external-primary",
|
||||
version="MYSQL_8_0",
|
||||
ip_addresses=[],
|
||||
region=GCP_EU1_LOCATION,
|
||||
public_ip=False,
|
||||
require_ssl=False,
|
||||
ssl_mode="ENCRYPTED_ONLY",
|
||||
automated_backups=False,
|
||||
authorized_networks=[],
|
||||
flags=[],
|
||||
project_id=GCP_PROJECT_ID,
|
||||
instance_type="ON_PREMISES_INSTANCE",
|
||||
cmek_key_name=None,
|
||||
),
|
||||
Instance(
|
||||
name="db-cmek",
|
||||
version="POSTGRES_15",
|
||||
ip_addresses=[],
|
||||
region=GCP_EU1_LOCATION,
|
||||
public_ip=False,
|
||||
require_ssl=False,
|
||||
ssl_mode="ENCRYPTED_ONLY",
|
||||
automated_backups=True,
|
||||
authorized_networks=[],
|
||||
flags=[],
|
||||
project_id=GCP_PROJECT_ID,
|
||||
instance_type="CLOUD_SQL_INSTANCE",
|
||||
cmek_key_name="projects/p/locations/europe-west1/keyRings/r/cryptoKeys/k",
|
||||
),
|
||||
]
|
||||
check = cloudsql_instance_cmek_encryption_enabled()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].resource_id == "db-cmek"
|
||||
assert result[0].status == "PASS"
|
||||
|
||||
def test_service_parser_missing_disk_encryption(self):
|
||||
"""Exercise the real service parser path when diskEncryptionConfiguration is absent."""
|
||||
|
||||
def mock_api_client_without_disk_encryption(*_args, **_kwargs):
|
||||
client = MagicMock()
|
||||
client.instances().list().execute.return_value = {
|
||||
"items": [
|
||||
{
|
||||
"name": "db-no-encryption-config",
|
||||
"databaseVersion": "POSTGRES_14",
|
||||
"region": "us-central1",
|
||||
"ipAddresses": [],
|
||||
"settings": {
|
||||
"ipConfiguration": {"requireSsl": True},
|
||||
"backupConfiguration": {"enabled": True},
|
||||
"databaseFlags": [],
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
client.instances().list_next.return_value = None
|
||||
return client
|
||||
|
||||
with (
|
||||
patch(
|
||||
"prowler.providers.gcp.lib.service.service.GCPService.__is_api_active__",
|
||||
new=mock_is_api_active,
|
||||
),
|
||||
patch(
|
||||
"prowler.providers.gcp.lib.service.service.GCPService.__generate_client__",
|
||||
new=mock_api_client_without_disk_encryption,
|
||||
),
|
||||
patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_gcp_provider(project_ids=[GCP_PROJECT_ID]),
|
||||
),
|
||||
):
|
||||
from prowler.providers.gcp.services.cloudsql.cloudsql_service import (
|
||||
CloudSQL,
|
||||
)
|
||||
|
||||
cloudsql_client = CloudSQL(
|
||||
set_mocked_gcp_provider(project_ids=[GCP_PROJECT_ID])
|
||||
)
|
||||
assert len(cloudsql_client.instances) == 1
|
||||
assert cloudsql_client.instances[0].cmek_key_name is None
|
||||
|
||||
with patch(
|
||||
"prowler.providers.gcp.services.cloudsql.cloudsql_instance_cmek_encryption_enabled.cloudsql_instance_cmek_encryption_enabled.cloudsql_client",
|
||||
new=cloudsql_client,
|
||||
):
|
||||
from prowler.providers.gcp.services.cloudsql.cloudsql_instance_cmek_encryption_enabled.cloudsql_instance_cmek_encryption_enabled import (
|
||||
cloudsql_instance_cmek_encryption_enabled,
|
||||
)
|
||||
|
||||
check = cloudsql_instance_cmek_encryption_enabled()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert result[0].resource_id == "db-no-encryption-config"
|
||||
@@ -43,6 +43,11 @@ class TestCloudSQLService:
|
||||
{"value": "test"}
|
||||
]
|
||||
assert cloudsql_client.instances[0].flags == []
|
||||
assert cloudsql_client.instances[0].instance_type == "CLOUD_SQL_INSTANCE"
|
||||
assert (
|
||||
cloudsql_client.instances[0].cmek_key_name
|
||||
== "projects/123/locations/us-central1/keyRings/keyring1/cryptoKeys/key1"
|
||||
)
|
||||
assert cloudsql_client.instances[0].project_id == GCP_PROJECT_ID
|
||||
|
||||
assert cloudsql_client.instances[1].name == "instance2"
|
||||
@@ -62,12 +67,14 @@ class TestCloudSQLService:
|
||||
{"value": "test"}
|
||||
]
|
||||
assert cloudsql_client.instances[1].flags == []
|
||||
assert cloudsql_client.instances[1].instance_type == "CLOUD_SQL_INSTANCE"
|
||||
assert cloudsql_client.instances[1].cmek_key_name is None
|
||||
assert cloudsql_client.instances[1].project_id == GCP_PROJECT_ID
|
||||
|
||||
def test_instances_without_backup_configuration(self):
|
||||
"""Test that CloudSQL service handles instances without backupConfiguration field"""
|
||||
|
||||
def mock_api_client_without_backup_config(*args, **kwargs):
|
||||
def mock_api_client_without_backup_config(*_args, **_kwargs):
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
client = MagicMock()
|
||||
@@ -119,7 +126,7 @@ class TestCloudSQLService:
|
||||
def test_instances_with_empty_backup_configuration(self):
|
||||
"""Test that CloudSQL service handles instances with empty backupConfiguration"""
|
||||
|
||||
def mock_api_client_with_empty_backup_config(*args, **kwargs):
|
||||
def mock_api_client_with_empty_backup_config(*_args, **_kwargs):
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
client = MagicMock()
|
||||
@@ -170,7 +177,7 @@ class TestCloudSQLService:
|
||||
def test_instances_without_settings_fields(self):
|
||||
"""Test that CloudSQL service handles instances with minimal settings"""
|
||||
|
||||
def mock_api_client_with_minimal_settings(*args, **kwargs):
|
||||
def mock_api_client_with_minimal_settings(*_args, **_kwargs):
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
client = MagicMock()
|
||||
|
||||
+282
@@ -0,0 +1,282 @@
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from unittest import mock
|
||||
from uuid import uuid4
|
||||
|
||||
from prowler.providers.m365.services.entra.entra_service import (
|
||||
AppRegistration,
|
||||
PasswordCredential,
|
||||
)
|
||||
from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider
|
||||
|
||||
|
||||
class Test_entra_app_registration_client_secret_unused:
|
||||
def test_no_app_registrations(self):
|
||||
"""No app registrations in tenant: no findings."""
|
||||
entra_client = mock.MagicMock
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.entra.entra_app_registration_client_secret_unused.entra_app_registration_client_secret_unused.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_app_registration_client_secret_unused.entra_app_registration_client_secret_unused import (
|
||||
entra_app_registration_client_secret_unused,
|
||||
)
|
||||
|
||||
entra_client.app_registrations = {}
|
||||
|
||||
check = entra_app_registration_client_secret_unused()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 0
|
||||
|
||||
def test_app_no_password_credentials(self):
|
||||
"""App with no password credentials: expected PASS."""
|
||||
app_id = str(uuid4())
|
||||
app_name = "Test App Clean"
|
||||
entra_client = mock.MagicMock
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.entra.entra_app_registration_client_secret_unused.entra_app_registration_client_secret_unused.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_app_registration_client_secret_unused.entra_app_registration_client_secret_unused import (
|
||||
entra_app_registration_client_secret_unused,
|
||||
)
|
||||
|
||||
entra_client.app_registrations = {
|
||||
app_id: AppRegistration(
|
||||
id=app_id,
|
||||
app_id=str(uuid4()),
|
||||
name=app_name,
|
||||
password_credentials=[],
|
||||
)
|
||||
}
|
||||
|
||||
check = entra_app_registration_client_secret_unused()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"App registration {app_name} does not use password credentials."
|
||||
)
|
||||
assert result[0].resource_name == app_name
|
||||
assert result[0].resource_id == app_id
|
||||
|
||||
def test_app_with_one_password_credential(self):
|
||||
"""App with one password credential: expected FAIL."""
|
||||
app_id = str(uuid4())
|
||||
app_name = "Test App With Secret"
|
||||
entra_client = mock.MagicMock
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.entra.entra_app_registration_client_secret_unused.entra_app_registration_client_secret_unused.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_app_registration_client_secret_unused.entra_app_registration_client_secret_unused import (
|
||||
entra_app_registration_client_secret_unused,
|
||||
)
|
||||
|
||||
entra_client.app_registrations = {
|
||||
app_id: AppRegistration(
|
||||
id=app_id,
|
||||
app_id=str(uuid4()),
|
||||
name=app_name,
|
||||
password_credentials=[
|
||||
PasswordCredential(
|
||||
key_id=str(uuid4()),
|
||||
display_name="My Secret",
|
||||
start_date_time="2024-01-01T00:00:00Z",
|
||||
end_date_time="2025-01-01T00:00:00Z",
|
||||
),
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
check = entra_app_registration_client_secret_unused()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert "1 password credential(s)" in result[0].status_extended
|
||||
assert "My Secret" in result[0].status_extended
|
||||
assert result[0].resource_name == app_name
|
||||
assert result[0].resource_id == app_id
|
||||
|
||||
def test_app_with_expired_password_credential_still_fails(self):
|
||||
"""App with an expired password credential: still expected FAIL."""
|
||||
app_id = str(uuid4())
|
||||
app_name = "Legacy App"
|
||||
entra_client = mock.MagicMock
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
|
||||
expired = datetime.now(timezone.utc) - timedelta(days=30)
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.entra.entra_app_registration_client_secret_unused.entra_app_registration_client_secret_unused.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_app_registration_client_secret_unused.entra_app_registration_client_secret_unused import (
|
||||
entra_app_registration_client_secret_unused,
|
||||
)
|
||||
|
||||
entra_client.app_registrations = {
|
||||
app_id: AppRegistration(
|
||||
id=app_id,
|
||||
app_id=str(uuid4()),
|
||||
name=app_name,
|
||||
password_credentials=[
|
||||
PasswordCredential(
|
||||
key_id=str(uuid4()),
|
||||
display_name="old-secret",
|
||||
end_date_time=expired,
|
||||
),
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
check = entra_app_registration_client_secret_unused()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert "1 password credential(s)" in result[0].status_extended
|
||||
assert result[0].resource_name == app_name
|
||||
|
||||
def test_app_with_multiple_password_credentials(self):
|
||||
"""App with multiple password credentials: expected FAIL."""
|
||||
app_id = str(uuid4())
|
||||
app_name = "Test App Multiple Secrets"
|
||||
entra_client = mock.MagicMock
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.entra.entra_app_registration_client_secret_unused.entra_app_registration_client_secret_unused.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_app_registration_client_secret_unused.entra_app_registration_client_secret_unused import (
|
||||
entra_app_registration_client_secret_unused,
|
||||
)
|
||||
|
||||
entra_client.app_registrations = {
|
||||
app_id: AppRegistration(
|
||||
id=app_id,
|
||||
app_id=str(uuid4()),
|
||||
name=app_name,
|
||||
password_credentials=[
|
||||
PasswordCredential(
|
||||
key_id=str(uuid4()),
|
||||
display_name="Secret 1",
|
||||
end_date_time="2025-06-01T00:00:00Z",
|
||||
),
|
||||
PasswordCredential(
|
||||
key_id=str(uuid4()),
|
||||
display_name="Secret 2",
|
||||
end_date_time="2025-12-01T00:00:00Z",
|
||||
),
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
check = entra_app_registration_client_secret_unused()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert "2 password credential(s)" in result[0].status_extended
|
||||
assert result[0].resource_name == app_name
|
||||
|
||||
def test_multiple_apps_mixed(self):
|
||||
"""Multiple apps: one clean, one with secrets."""
|
||||
app_id_pass = str(uuid4())
|
||||
app_name_pass = "Clean App"
|
||||
app_id_fail = str(uuid4())
|
||||
app_name_fail = "App With Secret"
|
||||
entra_client = mock.MagicMock
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.entra.entra_app_registration_client_secret_unused.entra_app_registration_client_secret_unused.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_app_registration_client_secret_unused.entra_app_registration_client_secret_unused import (
|
||||
entra_app_registration_client_secret_unused,
|
||||
)
|
||||
|
||||
entra_client.app_registrations = {
|
||||
app_id_pass: AppRegistration(
|
||||
id=app_id_pass,
|
||||
app_id=str(uuid4()),
|
||||
name=app_name_pass,
|
||||
password_credentials=[],
|
||||
),
|
||||
app_id_fail: AppRegistration(
|
||||
id=app_id_fail,
|
||||
app_id=str(uuid4()),
|
||||
name=app_name_fail,
|
||||
password_credentials=[
|
||||
PasswordCredential(
|
||||
key_id=str(uuid4()),
|
||||
display_name="Legacy Secret",
|
||||
),
|
||||
],
|
||||
),
|
||||
}
|
||||
|
||||
check = entra_app_registration_client_secret_unused()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 2
|
||||
|
||||
result_pass = next(r for r in result if r.resource_id == app_id_pass)
|
||||
result_fail = next(r for r in result if r.resource_id == app_id_fail)
|
||||
|
||||
assert result_pass.status == "PASS"
|
||||
assert result_fail.status == "FAIL"
|
||||
assert "Legacy Secret" in result_fail.status_extended
|
||||
+4
-4
@@ -15,7 +15,7 @@
|
||||
> - [`vitest`](../skills/vitest/SKILL.md) - Unit testing with React Testing Library
|
||||
> - [`tdd`](../skills/tdd/SKILL.md) - TDD workflow (MANDATORY for UI tasks)
|
||||
|
||||
## Auto-invoke Skills
|
||||
### Auto-invoke Skills
|
||||
|
||||
When performing these actions, ALWAYS invoke the corresponding skill FIRST:
|
||||
|
||||
@@ -89,7 +89,7 @@ When performing these actions, ALWAYS invoke the corresponding skill FIRST:
|
||||
|
||||
### Component Placement
|
||||
|
||||
```text
|
||||
```
|
||||
New/Existing UI? → shadcn/ui + Tailwind (NEVER HeroUI for new code)
|
||||
Used 1 feature? → features/{feature}/components | Used 2+? → components/{domain}/
|
||||
Needs state/hooks? → "use client" | Server component? → No directive
|
||||
@@ -97,7 +97,7 @@ Needs state/hooks? → "use client" | Server component? → No directive
|
||||
|
||||
### Code Location
|
||||
|
||||
```text
|
||||
```
|
||||
Server action → actions/{feature}/{feature}.ts
|
||||
Data transform → actions/{feature}/{feature}.adapter.ts
|
||||
Types (shared 2+) → types/{domain}.ts | Types (local 1) → {feature}/types.ts
|
||||
@@ -193,7 +193,7 @@ Zod 4.1.11 | React Hook Form 7.62.0 | Zustand 5.0.8 | NextAuth 5.0.0-beta.30 | R
|
||||
|
||||
## PROJECT STRUCTURE
|
||||
|
||||
```text
|
||||
```
|
||||
ui/
|
||||
├── app/(auth)/ # Auth pages
|
||||
├── app/(prowler)/ # Main app: compliance, findings, providers, scans
|
||||
|
||||
+11
-19
@@ -2,18 +2,18 @@
|
||||
|
||||
All notable changes to the **Prowler UI** are documented in this file.
|
||||
|
||||
## [1.27.0] (Prowler UNRELEASED)
|
||||
## [1.27.0] (Prowler v5.27.0)
|
||||
|
||||
### 🚀 Added
|
||||
|
||||
- UI health endpoint at `GET /api/health` for Docker Compose liveness checks [(#11145)](https://github.com/prowler-cloud/prowler/pull/11145)
|
||||
- AWS findings and resource details now expose a "View in AWS Console" link that opens the resource directly in the AWS Console via the universal `/go/view` ARN resolver. The per-provider external link is rendered by a new shared `ExternalResourceLink` component, which also covers the existing IaC repository link [(#9172)](https://github.com/prowler-cloud/prowler/pull/9172)
|
||||
- Health endpoint at `GET /api/health` for Docker Compose liveness checks [(#11145)](https://github.com/prowler-cloud/prowler/pull/11145)
|
||||
- AWS findings and resource details now expose a "View in AWS Console" link that opens the resource directly in the AWS Console via the universal `/go/view` ARN resolver [(#9172)](https://github.com/prowler-cloud/prowler/pull/9172)
|
||||
- Lighthouse AI: Prowler App Finding Groups MCP tools [(#11140)](https://github.com/prowler-cloud/prowler/pull/11140)
|
||||
|
||||
### 🔄 Changed
|
||||
|
||||
- Trimmed unused npm dependencies [(#11115)](https://github.com/prowler-cloud/prowler/pull/11115)
|
||||
- Faster, stricter pre-commit: prek lints and formats only staged UI files (husky removed), with Prettier and ESLint (`--max-warnings 40`, stale-disable detection) now covering the full UI workspace, including `public/` assets (only the auto-generated `public/mockServiceWorker.js` stays ignored) [(#11118)](https://github.com/prowler-cloud/prowler/pull/11118)
|
||||
- Lighthouse now accepts Prowler App Finding Groups MCP tools [(#11140)](https://github.com/prowler-cloud/prowler/pull/11140)
|
||||
- Trimmed unused `npm` dependencies [(#11115)](https://github.com/prowler-cloud/prowler/pull/11115)
|
||||
- Faster, stricter pre-commit: prek lints and formats only staged UI files (husky removed), with Prettier and ESLint (`--max-warnings 40`, stale-disable detection) now covering the full UI workspace, including `public/` assets [(#11118)](https://github.com/prowler-cloud/prowler/pull/11118)
|
||||
- Attack Paths graph now uses React Flow with improved layout, interactions, export, minimap, and browser test coverage [(#10686)](https://github.com/prowler-cloud/prowler/pull/10686)
|
||||
- SAML ACS URL is only shown if the email domain is configured [(#11144)](https://github.com/prowler-cloud/prowler/pull/11144)
|
||||
- "View Resource" action in the finding resource detail drawer is now an icon-only link rendered next to the resource name (instead of a text button in the UID row), keeping the "View in AWS Console" link unchanged [(#11193)](https://github.com/prowler-cloud/prowler/pull/11193)
|
||||
@@ -21,22 +21,14 @@ All notable changes to the **Prowler UI** are documented in this file.
|
||||
### 🐞 Fixed
|
||||
|
||||
- Mute Findings modal now enforces the 100-character limit on the rule name input with a live counter and inline error, matching the existing reason field behaviour [(#11158)](https://github.com/prowler-cloud/prowler/pull/11158)
|
||||
- Finding drawer no longer renders literal backticks around inline code in Risk, Description and Remediation sections [(#11142)](https://github.com/prowler-cloud/prowler/pull/11142)
|
||||
- Launch Scan first-provider wizard continues after provider creation instead of resetting the Scans page [(#11136)](https://github.com/prowler-cloud/prowler/pull/11136)
|
||||
- Attack Paths graph nodes now wrap long resource and finding labels, indicate truncated values with `…`, and show the full value in an immediate tooltip [(#11197)](https://github.com/prowler-cloud/prowler/pull/11197)
|
||||
|
||||
### 🔐 Security
|
||||
|
||||
- UI npm dependencies updated to patched versions for Next.js, Vite, LangChain, XML parsing, lodash, and related transitive packages [(#11171)](https://github.com/prowler-cloud/prowler/pull/11171)
|
||||
|
||||
---
|
||||
|
||||
## [1.26.2] (Prowler 5.26.2)
|
||||
|
||||
### 🐞 Fixed
|
||||
|
||||
- Finding drawer no longer renders literal backticks around inline code in Risk, Description and Remediation sections [(#11142)](https://github.com/prowler-cloud/prowler/pull/11142)
|
||||
|
||||
### 🐞 Fixed
|
||||
|
||||
- Launch Scan first-provider wizard continues after provider creation instead of resetting the Scans page [(#11136)](https://github.com/prowler-cloud/prowler/pull/11136)
|
||||
- `npm` dependencies updated to patched versions for Next.js, Vite, LangChain, XML parsing, lodash, and related transitive packages [(#11173)](https://github.com/prowler-cloud/prowler/pull/11173)
|
||||
- Hardened `npm` supply chain controls [(#11157)](https://github.com/prowler-cloud/prowler/pull/11157)
|
||||
|
||||
---
|
||||
|
||||
|
||||
+22
@@ -1,4 +1,5 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { type NodeProps, Position } from "@xyflow/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
@@ -91,5 +92,26 @@ describe("FindingNode", () => {
|
||||
expect(screen.getByText("logging")).toBeInTheDocument();
|
||||
expect(screen.getByText("medium")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should expose the full finding title as an immediate tooltip when truncated", async () => {
|
||||
// Given
|
||||
const title =
|
||||
"Ensure administrator access policies are rotated regularly";
|
||||
const props = buildNodeProps(buildFindingNode("high", title));
|
||||
|
||||
// When
|
||||
render(<FindingNode {...props} />);
|
||||
|
||||
// Then
|
||||
expect(screen.getByText("Ensure")).toBeInTheDocument();
|
||||
expect(screen.getByText("administrator")).toBeInTheDocument();
|
||||
expect(screen.getByText("access policies")).toBeInTheDocument();
|
||||
expect(screen.getByText("are rotated…")).toBeInTheDocument();
|
||||
expect(screen.getByText("high")).toBeInTheDocument();
|
||||
|
||||
await userEvent.hover(screen.getByTestId("attack-path-finding-node"));
|
||||
|
||||
expect(await screen.findAllByText(title)).not.toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+100
-78
@@ -2,21 +2,27 @@
|
||||
|
||||
import { type NodeProps, Position } from "@xyflow/react";
|
||||
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/shadcn/tooltip";
|
||||
import type { GraphNode } from "@/types/attack-paths";
|
||||
|
||||
import { resolveNodeColors, resolveNodeVisual } from "../../../_lib";
|
||||
import { FINDING_NODE_DIMENSIONS } from "../../../_lib/node-dimensions";
|
||||
import { getNodeLabelDisplay } from "../../../_lib/node-label-lines";
|
||||
import { HiddenHandles } from "./hidden-handles";
|
||||
import { getNodeLabelLines } from "./node-label-lines";
|
||||
|
||||
interface FindingNodeData {
|
||||
graphNode: GraphNode;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
const NODE_WIDTH = 150;
|
||||
const NODE_HEIGHT = 112;
|
||||
const TITLE_MAX_CHARS = 18;
|
||||
const TITLE_MAX_LINES = 2;
|
||||
const NODE_WIDTH = FINDING_NODE_DIMENSIONS.WIDTH;
|
||||
const NODE_HEIGHT = FINDING_NODE_DIMENSIONS.HEIGHT;
|
||||
const TITLE_MAX_CHARS = FINDING_NODE_DIMENSIONS.LABEL_MAX_CHARS;
|
||||
const TITLE_MAX_LINES = FINDING_NODE_DIMENSIONS.LABEL_MAX_LINES;
|
||||
const BADGE_SIZE = 44;
|
||||
const BADGE_RADIUS = BADGE_SIZE / 2;
|
||||
const BADGE_CENTER_X = NODE_WIDTH / 2;
|
||||
@@ -29,7 +35,7 @@ const ICON_Y = BADGE_CENTER_Y - ICON_SIZE / 2;
|
||||
const TEXT_X = BADGE_CENTER_X;
|
||||
const TITLE_Y = 66;
|
||||
const TITLE_LINE_HEIGHT = 13;
|
||||
const SEVERITY_Y = 94;
|
||||
const SEVERITY_Y = 118;
|
||||
|
||||
const severityLabel = (severity: unknown): string | undefined => {
|
||||
if (!severity) return undefined;
|
||||
@@ -59,7 +65,7 @@ export const FindingNode = ({ data, selected }: NodeProps) => {
|
||||
graphNode.properties?.id ||
|
||||
"Finding",
|
||||
);
|
||||
const displayTitleLines = getNodeLabelLines(
|
||||
const displayTitle = getNodeLabelDisplay(
|
||||
title,
|
||||
TITLE_MAX_CHARS,
|
||||
TITLE_MAX_LINES,
|
||||
@@ -72,6 +78,85 @@ export const FindingNode = ({ data, selected }: NodeProps) => {
|
||||
const badgeStrokeWidth = selected ? 4 : 2.5;
|
||||
const glowRadius = selected ? 32 : 30;
|
||||
const glowOpacity = selected ? 0.34 : 0.28;
|
||||
const nodeSvg = (
|
||||
<svg
|
||||
width={NODE_WIDTH}
|
||||
height={NODE_HEIGHT}
|
||||
className="overflow-visible"
|
||||
tabIndex={displayTitle.isTruncated ? 0 : undefined}
|
||||
data-testid="attack-path-finding-node"
|
||||
>
|
||||
<circle
|
||||
cx={BADGE_CENTER_X}
|
||||
cy={BADGE_CENTER_Y}
|
||||
r={glowRadius}
|
||||
stroke={borderColor}
|
||||
strokeOpacity={glowOpacity}
|
||||
strokeWidth={8}
|
||||
fill={borderColor}
|
||||
fillOpacity={glowOpacity / 2}
|
||||
pointerEvents="none"
|
||||
/>
|
||||
<circle
|
||||
cx={BADGE_CENTER_X}
|
||||
cy={BADGE_CENTER_Y}
|
||||
r={BADGE_RADIUS}
|
||||
fill={fillColor}
|
||||
fillOpacity={0.95}
|
||||
stroke={borderColor}
|
||||
strokeWidth={badgeStrokeWidth}
|
||||
className={selected ? "selected-node" : undefined}
|
||||
/>
|
||||
<g
|
||||
aria-label={iconLabel}
|
||||
data-testid={toFindingIconTestId(severity)}
|
||||
role="img"
|
||||
transform={`translate(${ICON_X}, ${ICON_Y})`}
|
||||
>
|
||||
<Icon
|
||||
aria-hidden="true"
|
||||
color="#ffffff"
|
||||
focusable="false"
|
||||
height={ICON_SIZE}
|
||||
role="presentation"
|
||||
size={ICON_SIZE}
|
||||
strokeWidth={2.4}
|
||||
width={ICON_SIZE}
|
||||
/>
|
||||
</g>
|
||||
<text
|
||||
x={TEXT_X}
|
||||
y={TITLE_Y}
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
fill="#ffffff"
|
||||
style={{ textShadow: "0 1px 2px rgba(0,0,0,0.5)" }}
|
||||
pointerEvents="none"
|
||||
>
|
||||
{displayTitle.lines.map((line, index) => (
|
||||
<tspan
|
||||
key={`${line}-${index}`}
|
||||
x={TEXT_X}
|
||||
y={TITLE_Y + index * TITLE_LINE_HEIGHT}
|
||||
fontSize="11px"
|
||||
fontWeight="600"
|
||||
>
|
||||
{line}
|
||||
</tspan>
|
||||
))}
|
||||
{severity && (
|
||||
<tspan
|
||||
x={TEXT_X}
|
||||
y={SEVERITY_Y}
|
||||
fontSize="9px"
|
||||
fill="rgba(255,255,255,0.82)"
|
||||
>
|
||||
{severity}
|
||||
</tspan>
|
||||
)}
|
||||
</text>
|
||||
</svg>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -81,77 +166,14 @@ export const FindingNode = ({ data, selected }: NodeProps) => {
|
||||
targetPosition={Position.Left}
|
||||
targetStyle={{ left: BADGE_LEFT_X, top: BADGE_CENTER_Y }}
|
||||
/>
|
||||
<svg width={NODE_WIDTH} height={NODE_HEIGHT} className="overflow-visible">
|
||||
<circle
|
||||
cx={BADGE_CENTER_X}
|
||||
cy={BADGE_CENTER_Y}
|
||||
r={glowRadius}
|
||||
stroke={borderColor}
|
||||
strokeOpacity={glowOpacity}
|
||||
strokeWidth={8}
|
||||
fill={borderColor}
|
||||
fillOpacity={glowOpacity / 2}
|
||||
pointerEvents="none"
|
||||
/>
|
||||
<circle
|
||||
cx={BADGE_CENTER_X}
|
||||
cy={BADGE_CENTER_Y}
|
||||
r={BADGE_RADIUS}
|
||||
fill={fillColor}
|
||||
fillOpacity={0.95}
|
||||
stroke={borderColor}
|
||||
strokeWidth={badgeStrokeWidth}
|
||||
className={selected ? "selected-node" : undefined}
|
||||
/>
|
||||
<g
|
||||
aria-label={iconLabel}
|
||||
data-testid={toFindingIconTestId(severity)}
|
||||
role="img"
|
||||
transform={`translate(${ICON_X}, ${ICON_Y})`}
|
||||
>
|
||||
<Icon
|
||||
aria-hidden="true"
|
||||
color="#ffffff"
|
||||
focusable="false"
|
||||
height={ICON_SIZE}
|
||||
role="presentation"
|
||||
size={ICON_SIZE}
|
||||
strokeWidth={2.4}
|
||||
width={ICON_SIZE}
|
||||
/>
|
||||
</g>
|
||||
<text
|
||||
x={TEXT_X}
|
||||
y={TITLE_Y}
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
fill="#ffffff"
|
||||
style={{ textShadow: "0 1px 2px rgba(0,0,0,0.5)" }}
|
||||
pointerEvents="none"
|
||||
>
|
||||
{displayTitleLines.map((line, index) => (
|
||||
<tspan
|
||||
key={`${line}-${index}`}
|
||||
x={TEXT_X}
|
||||
y={TITLE_Y + index * TITLE_LINE_HEIGHT}
|
||||
fontSize="11px"
|
||||
fontWeight="600"
|
||||
>
|
||||
{line}
|
||||
</tspan>
|
||||
))}
|
||||
{severity && (
|
||||
<tspan
|
||||
x={TEXT_X}
|
||||
y={SEVERITY_Y}
|
||||
fontSize="9px"
|
||||
fill="rgba(255,255,255,0.82)"
|
||||
>
|
||||
{severity}
|
||||
</tspan>
|
||||
)}
|
||||
</text>
|
||||
</svg>
|
||||
{displayTitle.isTruncated ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{nodeSvg}</TooltipTrigger>
|
||||
<TooltipContent>{title}</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
nodeSvg
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user