mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-08-23 06:12:00 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f03d83872e | ||
|
|
bb620022f5 | ||
|
|
27a81defec | ||
|
|
a81293d2ea | ||
|
|
80427dd127 | ||
|
|
14e9506b87 | ||
|
|
3e72d575d4 | ||
|
|
79825d35fc | ||
|
|
6215c1ba46 |
@@ -32,7 +32,6 @@ Please add a detailed description of how to review this PR.
|
||||
#### API
|
||||
- [ ] Verify if API specs need to be regenerated.
|
||||
- [ ] Check if version updates are required (e.g., specs, Poetry, etc.).
|
||||
- [ ] Query performance validated with `EXPLAIN ANALYZE` for new/modified endpoints. See [Query Performance Guide](https://github.com/prowler-cloud/prowler/blob/master/api/docs/query-performance-guide.md).
|
||||
- [ ] Ensure new entries are added to [CHANGELOG.md](https://github.com/prowler-cloud/prowler/blob/master/api/CHANGELOG.md), if applicable.
|
||||
|
||||
### License
|
||||
|
||||
@@ -48,34 +48,8 @@ jobs:
|
||||
id: set-short-sha
|
||||
run: echo "short-sha=${GITHUB_SHA::7}" >> $GITHUB_OUTPUT
|
||||
|
||||
notify-release-started:
|
||||
if: github.repository == 'prowler-cloud/prowler' && (github.event_name == 'release' || github.event_name == 'workflow_dispatch')
|
||||
needs: setup
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
outputs:
|
||||
message-ts: ${{ steps.slack-notification.outputs.ts }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Notify container push started
|
||||
id: slack-notification
|
||||
uses: ./.github/actions/slack-notification
|
||||
env:
|
||||
SLACK_CHANNEL_ID: ${{ secrets.SLACK_PLATFORM_DEPLOYMENTS }}
|
||||
COMPONENT: API
|
||||
RELEASE_TAG: ${{ env.RELEASE_TAG }}
|
||||
GITHUB_SERVER_URL: ${{ github.server_url }}
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
GITHUB_RUN_ID: ${{ github.run_id }}
|
||||
with:
|
||||
slack-bot-token: ${{ secrets.SLACK_BOT_TOKEN }}
|
||||
payload-file-path: "./.github/scripts/slack-messages/container-release-started.json"
|
||||
|
||||
container-build-push:
|
||||
needs: [setup, notify-release-started]
|
||||
if: always() && needs.setup.result == 'success' && (needs.notify-release-started.result == 'success' || needs.notify-release-started.result == 'skipped')
|
||||
needs: setup
|
||||
runs-on: ${{ matrix.runner }}
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -104,6 +78,21 @@ jobs:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
|
||||
- name: Notify container push started
|
||||
id: slack-notification-started
|
||||
if: github.event_name == 'release' || github.event_name == 'workflow_dispatch'
|
||||
uses: ./.github/actions/slack-notification
|
||||
env:
|
||||
SLACK_CHANNEL_ID: ${{ secrets.SLACK_PLATFORM_DEPLOYMENTS }}
|
||||
COMPONENT: API
|
||||
RELEASE_TAG: ${{ env.RELEASE_TAG }}
|
||||
GITHUB_SERVER_URL: ${{ github.server_url }}
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
GITHUB_RUN_ID: ${{ github.run_id }}
|
||||
with:
|
||||
slack-bot-token: ${{ secrets.SLACK_BOT_TOKEN }}
|
||||
payload-file-path: "./.github/scripts/slack-messages/container-release-started.json"
|
||||
|
||||
- name: Build and push API container for ${{ matrix.arch }}
|
||||
id: container-push
|
||||
if: github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'workflow_dispatch'
|
||||
@@ -117,6 +106,23 @@ jobs:
|
||||
cache-from: type=gha,scope=${{ matrix.arch }}
|
||||
cache-to: type=gha,mode=max,scope=${{ matrix.arch }}
|
||||
|
||||
- name: Notify container push completed
|
||||
if: (github.event_name == 'release' || github.event_name == 'workflow_dispatch') && always()
|
||||
uses: ./.github/actions/slack-notification
|
||||
env:
|
||||
SLACK_CHANNEL_ID: ${{ secrets.SLACK_PLATFORM_DEPLOYMENTS }}
|
||||
MESSAGE_TS: ${{ steps.slack-notification-started.outputs.ts }}
|
||||
COMPONENT: API
|
||||
RELEASE_TAG: ${{ env.RELEASE_TAG }}
|
||||
GITHUB_SERVER_URL: ${{ github.server_url }}
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
GITHUB_RUN_ID: ${{ github.run_id }}
|
||||
with:
|
||||
slack-bot-token: ${{ secrets.SLACK_BOT_TOKEN }}
|
||||
payload-file-path: "./.github/scripts/slack-messages/container-release-completed.json"
|
||||
step-outcome: ${{ steps.container-push.outcome }}
|
||||
update-ts: ${{ steps.slack-notification-started.outputs.ts }}
|
||||
|
||||
# Create and push multi-architecture manifest
|
||||
create-manifest:
|
||||
needs: [setup, container-build-push]
|
||||
@@ -163,40 +169,6 @@ jobs:
|
||||
regctl tag delete "${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.short-sha }}-arm64" || true
|
||||
echo "Cleanup completed"
|
||||
|
||||
notify-release-completed:
|
||||
if: always() && needs.notify-release-started.result == 'success' && (github.event_name == 'release' || github.event_name == 'workflow_dispatch')
|
||||
needs: [setup, notify-release-started, container-build-push, create-manifest]
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Determine overall outcome
|
||||
id: outcome
|
||||
run: |
|
||||
if [[ "${{ needs.container-build-push.result }}" == "success" && "${{ needs.create-manifest.result }}" == "success" ]]; then
|
||||
echo "outcome=success" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "outcome=failure" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Notify container push completed
|
||||
uses: ./.github/actions/slack-notification
|
||||
env:
|
||||
SLACK_CHANNEL_ID: ${{ secrets.SLACK_PLATFORM_DEPLOYMENTS }}
|
||||
MESSAGE_TS: ${{ needs.notify-release-started.outputs.message-ts }}
|
||||
COMPONENT: API
|
||||
RELEASE_TAG: ${{ env.RELEASE_TAG }}
|
||||
GITHUB_SERVER_URL: ${{ github.server_url }}
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
GITHUB_RUN_ID: ${{ github.run_id }}
|
||||
with:
|
||||
slack-bot-token: ${{ secrets.SLACK_BOT_TOKEN }}
|
||||
payload-file-path: "./.github/scripts/slack-messages/container-release-completed.json"
|
||||
step-outcome: ${{ steps.outcome.outputs.outcome }}
|
||||
update-ts: ${{ needs.notify-release-started.outputs.message-ts }}
|
||||
|
||||
trigger-deployment:
|
||||
if: github.event_name == 'push'
|
||||
needs: [setup, container-build-push]
|
||||
|
||||
@@ -47,34 +47,8 @@ jobs:
|
||||
id: set-short-sha
|
||||
run: echo "short-sha=${GITHUB_SHA::7}" >> $GITHUB_OUTPUT
|
||||
|
||||
notify-release-started:
|
||||
if: github.repository == 'prowler-cloud/prowler' && (github.event_name == 'release' || github.event_name == 'workflow_dispatch')
|
||||
needs: setup
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
outputs:
|
||||
message-ts: ${{ steps.slack-notification.outputs.ts }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Notify container push started
|
||||
id: slack-notification
|
||||
uses: ./.github/actions/slack-notification
|
||||
env:
|
||||
SLACK_CHANNEL_ID: ${{ secrets.SLACK_PLATFORM_DEPLOYMENTS }}
|
||||
COMPONENT: MCP
|
||||
RELEASE_TAG: ${{ env.RELEASE_TAG }}
|
||||
GITHUB_SERVER_URL: ${{ github.server_url }}
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
GITHUB_RUN_ID: ${{ github.run_id }}
|
||||
with:
|
||||
slack-bot-token: ${{ secrets.SLACK_BOT_TOKEN }}
|
||||
payload-file-path: "./.github/scripts/slack-messages/container-release-started.json"
|
||||
|
||||
container-build-push:
|
||||
needs: [setup, notify-release-started]
|
||||
if: always() && needs.setup.result == 'success' && (needs.notify-release-started.result == 'success' || needs.notify-release-started.result == 'skipped')
|
||||
needs: setup
|
||||
runs-on: ${{ matrix.runner }}
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -102,6 +76,21 @@ jobs:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
|
||||
- name: Notify container push started
|
||||
id: slack-notification-started
|
||||
if: github.event_name == 'release' || github.event_name == 'workflow_dispatch'
|
||||
uses: ./.github/actions/slack-notification
|
||||
env:
|
||||
SLACK_CHANNEL_ID: ${{ secrets.SLACK_PLATFORM_DEPLOYMENTS }}
|
||||
COMPONENT: MCP
|
||||
RELEASE_TAG: ${{ env.RELEASE_TAG }}
|
||||
GITHUB_SERVER_URL: ${{ github.server_url }}
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
GITHUB_RUN_ID: ${{ github.run_id }}
|
||||
with:
|
||||
slack-bot-token: ${{ secrets.SLACK_BOT_TOKEN }}
|
||||
payload-file-path: "./.github/scripts/slack-messages/container-release-started.json"
|
||||
|
||||
- name: Build and push MCP container for ${{ matrix.arch }}
|
||||
id: container-push
|
||||
if: github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'workflow_dispatch'
|
||||
@@ -123,6 +112,23 @@ jobs:
|
||||
cache-from: type=gha,scope=${{ matrix.arch }}
|
||||
cache-to: type=gha,mode=max,scope=${{ matrix.arch }}
|
||||
|
||||
- name: Notify container push completed
|
||||
if: (github.event_name == 'release' || github.event_name == 'workflow_dispatch') && always()
|
||||
uses: ./.github/actions/slack-notification
|
||||
env:
|
||||
SLACK_CHANNEL_ID: ${{ secrets.SLACK_PLATFORM_DEPLOYMENTS }}
|
||||
MESSAGE_TS: ${{ steps.slack-notification-started.outputs.ts }}
|
||||
COMPONENT: MCP
|
||||
RELEASE_TAG: ${{ env.RELEASE_TAG }}
|
||||
GITHUB_SERVER_URL: ${{ github.server_url }}
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
GITHUB_RUN_ID: ${{ github.run_id }}
|
||||
with:
|
||||
slack-bot-token: ${{ secrets.SLACK_BOT_TOKEN }}
|
||||
payload-file-path: "./.github/scripts/slack-messages/container-release-completed.json"
|
||||
step-outcome: ${{ steps.container-push.outcome }}
|
||||
update-ts: ${{ steps.slack-notification-started.outputs.ts }}
|
||||
|
||||
# Create and push multi-architecture manifest
|
||||
create-manifest:
|
||||
needs: [setup, container-build-push]
|
||||
@@ -169,40 +175,6 @@ jobs:
|
||||
regctl tag delete "${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.short-sha }}-arm64" || true
|
||||
echo "Cleanup completed"
|
||||
|
||||
notify-release-completed:
|
||||
if: always() && needs.notify-release-started.result == 'success' && (github.event_name == 'release' || github.event_name == 'workflow_dispatch')
|
||||
needs: [setup, notify-release-started, container-build-push, create-manifest]
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Determine overall outcome
|
||||
id: outcome
|
||||
run: |
|
||||
if [[ "${{ needs.container-build-push.result }}" == "success" && "${{ needs.create-manifest.result }}" == "success" ]]; then
|
||||
echo "outcome=success" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "outcome=failure" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Notify container push completed
|
||||
uses: ./.github/actions/slack-notification
|
||||
env:
|
||||
SLACK_CHANNEL_ID: ${{ secrets.SLACK_PLATFORM_DEPLOYMENTS }}
|
||||
MESSAGE_TS: ${{ needs.notify-release-started.outputs.message-ts }}
|
||||
COMPONENT: MCP
|
||||
RELEASE_TAG: ${{ env.RELEASE_TAG }}
|
||||
GITHUB_SERVER_URL: ${{ github.server_url }}
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
GITHUB_RUN_ID: ${{ github.run_id }}
|
||||
with:
|
||||
slack-bot-token: ${{ secrets.SLACK_BOT_TOKEN }}
|
||||
payload-file-path: "./.github/scripts/slack-messages/container-release-completed.json"
|
||||
step-outcome: ${{ steps.outcome.outputs.outcome }}
|
||||
update-ts: ${{ needs.notify-release-started.outputs.message-ts }}
|
||||
|
||||
trigger-deployment:
|
||||
if: github.event_name == 'push'
|
||||
needs: [setup, container-build-push]
|
||||
|
||||
@@ -50,15 +50,30 @@ env:
|
||||
AWS_REGION: us-east-1
|
||||
|
||||
jobs:
|
||||
setup:
|
||||
container-build-push:
|
||||
if: github.repository == 'prowler-cloud/prowler'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
runs-on: ${{ matrix.runner }}
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- platform: linux/amd64
|
||||
runner: ubuntu-latest
|
||||
arch: amd64
|
||||
- platform: linux/arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
arch: arm64
|
||||
timeout-minutes: 45
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
outputs:
|
||||
prowler_version: ${{ steps.get-prowler-version.outputs.prowler_version }}
|
||||
prowler_version_major: ${{ steps.get-prowler-version.outputs.prowler_version_major }}
|
||||
latest_tag: ${{ steps.get-prowler-version.outputs.latest_tag }}
|
||||
stable_tag: ${{ steps.get-prowler-version.outputs.stable_tag }}
|
||||
env:
|
||||
POETRY_VIRTUALENVS_CREATE: 'false'
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
@@ -78,24 +93,32 @@ jobs:
|
||||
run: |
|
||||
PROWLER_VERSION="$(poetry version -s 2>/dev/null)"
|
||||
echo "prowler_version=${PROWLER_VERSION}" >> "${GITHUB_OUTPUT}"
|
||||
echo "PROWLER_VERSION=${PROWLER_VERSION}" >> "${GITHUB_ENV}"
|
||||
|
||||
# Extract major version
|
||||
PROWLER_VERSION_MAJOR="${PROWLER_VERSION%%.*}"
|
||||
echo "prowler_version_major=${PROWLER_VERSION_MAJOR}" >> "${GITHUB_OUTPUT}"
|
||||
echo "PROWLER_VERSION_MAJOR=${PROWLER_VERSION_MAJOR}" >> "${GITHUB_ENV}"
|
||||
|
||||
# Set version-specific tags
|
||||
case ${PROWLER_VERSION_MAJOR} in
|
||||
3)
|
||||
echo "LATEST_TAG=v3-latest" >> "${GITHUB_ENV}"
|
||||
echo "STABLE_TAG=v3-stable" >> "${GITHUB_ENV}"
|
||||
echo "latest_tag=v3-latest" >> "${GITHUB_OUTPUT}"
|
||||
echo "stable_tag=v3-stable" >> "${GITHUB_OUTPUT}"
|
||||
echo "✓ Prowler v3 detected - tags: v3-latest, v3-stable"
|
||||
;;
|
||||
4)
|
||||
echo "LATEST_TAG=v4-latest" >> "${GITHUB_ENV}"
|
||||
echo "STABLE_TAG=v4-stable" >> "${GITHUB_ENV}"
|
||||
echo "latest_tag=v4-latest" >> "${GITHUB_OUTPUT}"
|
||||
echo "stable_tag=v4-stable" >> "${GITHUB_OUTPUT}"
|
||||
echo "✓ Prowler v4 detected - tags: v4-latest, v4-stable"
|
||||
;;
|
||||
5)
|
||||
echo "LATEST_TAG=latest" >> "${GITHUB_ENV}"
|
||||
echo "STABLE_TAG=stable" >> "${GITHUB_ENV}"
|
||||
echo "latest_tag=latest" >> "${GITHUB_OUTPUT}"
|
||||
echo "stable_tag=stable" >> "${GITHUB_OUTPUT}"
|
||||
echo "✓ Prowler v5 detected - tags: latest, stable"
|
||||
@@ -106,53 +129,6 @@ jobs:
|
||||
;;
|
||||
esac
|
||||
|
||||
notify-release-started:
|
||||
if: github.repository == 'prowler-cloud/prowler' && (github.event_name == 'release' || github.event_name == 'workflow_dispatch')
|
||||
needs: setup
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
outputs:
|
||||
message-ts: ${{ steps.slack-notification.outputs.ts }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Notify container push started
|
||||
id: slack-notification
|
||||
uses: ./.github/actions/slack-notification
|
||||
env:
|
||||
SLACK_CHANNEL_ID: ${{ secrets.SLACK_PLATFORM_DEPLOYMENTS }}
|
||||
COMPONENT: SDK
|
||||
RELEASE_TAG: ${{ needs.setup.outputs.prowler_version }}
|
||||
GITHUB_SERVER_URL: ${{ github.server_url }}
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
GITHUB_RUN_ID: ${{ github.run_id }}
|
||||
with:
|
||||
slack-bot-token: ${{ secrets.SLACK_BOT_TOKEN }}
|
||||
payload-file-path: "./.github/scripts/slack-messages/container-release-started.json"
|
||||
|
||||
container-build-push:
|
||||
needs: [setup, notify-release-started]
|
||||
if: always() && needs.setup.result == 'success' && (needs.notify-release-started.result == 'success' || needs.notify-release-started.result == 'skipped')
|
||||
runs-on: ${{ matrix.runner }}
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- platform: linux/amd64
|
||||
runner: ubuntu-latest
|
||||
arch: amd64
|
||||
- platform: linux/arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
arch: arm64
|
||||
timeout-minutes: 45
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
@@ -171,6 +147,21 @@ jobs:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
|
||||
- name: Notify container push started
|
||||
id: slack-notification-started
|
||||
if: github.event_name == 'release' || github.event_name == 'workflow_dispatch'
|
||||
uses: ./.github/actions/slack-notification
|
||||
env:
|
||||
SLACK_CHANNEL_ID: ${{ secrets.SLACK_PLATFORM_DEPLOYMENTS }}
|
||||
COMPONENT: SDK
|
||||
RELEASE_TAG: ${{ env.PROWLER_VERSION }}
|
||||
GITHUB_SERVER_URL: ${{ github.server_url }}
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
GITHUB_RUN_ID: ${{ github.run_id }}
|
||||
with:
|
||||
slack-bot-token: ${{ secrets.SLACK_BOT_TOKEN }}
|
||||
payload-file-path: "./.github/scripts/slack-messages/container-release-started.json"
|
||||
|
||||
- name: Build and push SDK container for ${{ matrix.arch }}
|
||||
id: container-push
|
||||
if: github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'workflow_dispatch'
|
||||
@@ -181,13 +172,30 @@ jobs:
|
||||
push: true
|
||||
platforms: ${{ matrix.platform }}
|
||||
tags: |
|
||||
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.latest_tag }}-${{ matrix.arch }}
|
||||
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.LATEST_TAG }}-${{ matrix.arch }}
|
||||
cache-from: type=gha,scope=${{ matrix.arch }}
|
||||
cache-to: type=gha,mode=max,scope=${{ matrix.arch }}
|
||||
|
||||
- name: Notify container push completed
|
||||
if: (github.event_name == 'release' || github.event_name == 'workflow_dispatch') && always()
|
||||
uses: ./.github/actions/slack-notification
|
||||
env:
|
||||
SLACK_CHANNEL_ID: ${{ secrets.SLACK_PLATFORM_DEPLOYMENTS }}
|
||||
MESSAGE_TS: ${{ steps.slack-notification-started.outputs.ts }}
|
||||
COMPONENT: SDK
|
||||
RELEASE_TAG: ${{ env.PROWLER_VERSION }}
|
||||
GITHUB_SERVER_URL: ${{ github.server_url }}
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
GITHUB_RUN_ID: ${{ github.run_id }}
|
||||
with:
|
||||
slack-bot-token: ${{ secrets.SLACK_BOT_TOKEN }}
|
||||
payload-file-path: "./.github/scripts/slack-messages/container-release-completed.json"
|
||||
step-outcome: ${{ steps.container-push.outcome }}
|
||||
update-ts: ${{ steps.slack-notification-started.outputs.ts }}
|
||||
|
||||
# Create and push multi-architecture manifest
|
||||
create-manifest:
|
||||
needs: [setup, container-build-push]
|
||||
needs: [container-build-push]
|
||||
if: github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'workflow_dispatch'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -214,24 +222,24 @@ jobs:
|
||||
if: github.event_name == 'push'
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
-t ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.latest_tag }} \
|
||||
-t ${{ secrets.DOCKER_HUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.latest_tag }} \
|
||||
-t ${{ secrets.PUBLIC_ECR_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.latest_tag }} \
|
||||
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.latest_tag }}-amd64 \
|
||||
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.latest_tag }}-arm64
|
||||
-t ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.container-build-push.outputs.latest_tag }} \
|
||||
-t ${{ secrets.DOCKER_HUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.container-build-push.outputs.latest_tag }} \
|
||||
-t ${{ secrets.PUBLIC_ECR_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.container-build-push.outputs.latest_tag }} \
|
||||
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.container-build-push.outputs.latest_tag }}-amd64 \
|
||||
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.container-build-push.outputs.latest_tag }}-arm64
|
||||
|
||||
- name: Create and push manifests for release event
|
||||
if: github.event_name == 'release' || github.event_name == 'workflow_dispatch'
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
-t ${{ secrets.DOCKER_HUB_REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ needs.setup.outputs.prowler_version }} \
|
||||
-t ${{ secrets.DOCKER_HUB_REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ needs.setup.outputs.stable_tag }} \
|
||||
-t ${{ secrets.PUBLIC_ECR_REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ needs.setup.outputs.prowler_version }} \
|
||||
-t ${{ secrets.PUBLIC_ECR_REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ needs.setup.outputs.stable_tag }} \
|
||||
-t ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.prowler_version }} \
|
||||
-t ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.stable_tag }} \
|
||||
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.latest_tag }}-amd64 \
|
||||
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.latest_tag }}-arm64
|
||||
-t ${{ secrets.DOCKER_HUB_REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ needs.container-build-push.outputs.prowler_version }} \
|
||||
-t ${{ secrets.DOCKER_HUB_REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ needs.container-build-push.outputs.stable_tag }} \
|
||||
-t ${{ secrets.PUBLIC_ECR_REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ needs.container-build-push.outputs.prowler_version }} \
|
||||
-t ${{ secrets.PUBLIC_ECR_REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ needs.container-build-push.outputs.stable_tag }} \
|
||||
-t ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.container-build-push.outputs.prowler_version }} \
|
||||
-t ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.container-build-push.outputs.stable_tag }} \
|
||||
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.container-build-push.outputs.latest_tag }}-amd64 \
|
||||
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.container-build-push.outputs.latest_tag }}-arm64
|
||||
|
||||
- name: Install regctl
|
||||
if: always()
|
||||
@@ -241,47 +249,13 @@ jobs:
|
||||
if: always()
|
||||
run: |
|
||||
echo "Cleaning up intermediate tags..."
|
||||
regctl tag delete "${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.latest_tag }}-amd64" || true
|
||||
regctl tag delete "${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.latest_tag }}-arm64" || true
|
||||
regctl tag delete "${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.container-build-push.outputs.latest_tag }}-amd64" || true
|
||||
regctl tag delete "${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.container-build-push.outputs.latest_tag }}-arm64" || true
|
||||
echo "Cleanup completed"
|
||||
|
||||
notify-release-completed:
|
||||
if: always() && needs.notify-release-started.result == 'success' && (github.event_name == 'release' || github.event_name == 'workflow_dispatch')
|
||||
needs: [setup, notify-release-started, container-build-push, create-manifest]
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Determine overall outcome
|
||||
id: outcome
|
||||
run: |
|
||||
if [[ "${{ needs.container-build-push.result }}" == "success" && "${{ needs.create-manifest.result }}" == "success" ]]; then
|
||||
echo "outcome=success" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "outcome=failure" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Notify container push completed
|
||||
uses: ./.github/actions/slack-notification
|
||||
env:
|
||||
SLACK_CHANNEL_ID: ${{ secrets.SLACK_PLATFORM_DEPLOYMENTS }}
|
||||
MESSAGE_TS: ${{ needs.notify-release-started.outputs.message-ts }}
|
||||
COMPONENT: SDK
|
||||
RELEASE_TAG: ${{ needs.setup.outputs.prowler_version }}
|
||||
GITHUB_SERVER_URL: ${{ github.server_url }}
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
GITHUB_RUN_ID: ${{ github.run_id }}
|
||||
with:
|
||||
slack-bot-token: ${{ secrets.SLACK_BOT_TOKEN }}
|
||||
payload-file-path: "./.github/scripts/slack-messages/container-release-completed.json"
|
||||
step-outcome: ${{ steps.outcome.outputs.outcome }}
|
||||
update-ts: ${{ needs.notify-release-started.outputs.message-ts }}
|
||||
|
||||
dispatch-v3-deployment:
|
||||
if: needs.setup.outputs.prowler_version_major == '3'
|
||||
needs: [setup, container-build-push]
|
||||
if: needs.container-build-push.outputs.prowler_version_major == '3'
|
||||
needs: container-build-push
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
permissions:
|
||||
@@ -308,4 +282,4 @@ jobs:
|
||||
token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
|
||||
repository: ${{ secrets.DISPATCH_OWNER }}/${{ secrets.DISPATCH_REPO }}
|
||||
event-type: dispatch
|
||||
client-payload: '{"version":"release","tag":"${{ needs.setup.outputs.prowler_version }}"}'
|
||||
client-payload: '{"version":"release","tag":"${{ needs.container-build-push.outputs.prowler_version }}"}'
|
||||
|
||||
@@ -82,110 +82,9 @@ jobs:
|
||||
./tests/**/aws/**
|
||||
./poetry.lock
|
||||
|
||||
- name: Resolve AWS services under test
|
||||
if: steps.changed-aws.outputs.any_changed == 'true'
|
||||
id: aws-services
|
||||
shell: bash
|
||||
run: |
|
||||
python3 <<'PY'
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
dependents = {
|
||||
"acm": ["elb"],
|
||||
"autoscaling": ["dynamodb"],
|
||||
"awslambda": ["ec2", "inspector2"],
|
||||
"backup": ["dynamodb", "ec2", "rds"],
|
||||
"cloudfront": ["shield"],
|
||||
"cloudtrail": ["awslambda", "cloudwatch"],
|
||||
"cloudwatch": ["bedrock"],
|
||||
"ec2": ["dlm", "dms", "elbv2", "emr", "inspector2", "rds", "redshift", "route53", "shield", "ssm"],
|
||||
"ecr": ["inspector2"],
|
||||
"elb": ["shield"],
|
||||
"elbv2": ["shield"],
|
||||
"globalaccelerator": ["shield"],
|
||||
"iam": ["bedrock", "cloudtrail", "cloudwatch", "codebuild"],
|
||||
"kafka": ["firehose"],
|
||||
"kinesis": ["firehose"],
|
||||
"kms": ["kafka"],
|
||||
"organizations": ["iam", "servicecatalog"],
|
||||
"route53": ["shield"],
|
||||
"s3": ["bedrock", "cloudfront", "cloudtrail", "macie"],
|
||||
"ssm": ["ec2"],
|
||||
"vpc": ["awslambda", "ec2", "efs", "elasticache", "neptune", "networkfirewall", "rds", "redshift", "workspaces"],
|
||||
"waf": ["elbv2"],
|
||||
"wafv2": ["cognito", "elbv2"],
|
||||
}
|
||||
|
||||
changed_raw = """${{ steps.changed-aws.outputs.all_changed_files }}"""
|
||||
# all_changed_files is space-separated, not newline-separated
|
||||
# Strip leading "./" if present for consistent path handling
|
||||
changed_files = [Path(f.lstrip("./")) for f in changed_raw.split() if f]
|
||||
|
||||
services = set()
|
||||
run_all = False
|
||||
|
||||
for path in changed_files:
|
||||
path_str = path.as_posix()
|
||||
parts = path.parts
|
||||
if path_str.startswith("prowler/providers/aws/services/"):
|
||||
if len(parts) > 4 and "." not in parts[4]:
|
||||
services.add(parts[4])
|
||||
else:
|
||||
run_all = True
|
||||
elif path_str.startswith("tests/providers/aws/services/"):
|
||||
if len(parts) > 4 and "." not in parts[4]:
|
||||
services.add(parts[4])
|
||||
else:
|
||||
run_all = True
|
||||
elif path_str.startswith("prowler/providers/aws/") or path_str.startswith("tests/providers/aws/"):
|
||||
run_all = True
|
||||
|
||||
# Expand with direct dependent services (one level only)
|
||||
# We only test services that directly depend on the changed services,
|
||||
# not transitive dependencies (services that depend on dependents)
|
||||
original_services = set(services)
|
||||
for svc in original_services:
|
||||
for dep in dependents.get(svc, []):
|
||||
services.add(dep)
|
||||
|
||||
if run_all or not services:
|
||||
run_all = True
|
||||
services = set()
|
||||
|
||||
service_paths = " ".join(sorted(f"tests/providers/aws/services/{svc}" for svc in services))
|
||||
|
||||
output_lines = [
|
||||
f"run_all={'true' if run_all else 'false'}",
|
||||
f"services={' '.join(sorted(services))}",
|
||||
f"service_paths={service_paths}",
|
||||
]
|
||||
|
||||
with open(os.environ["GITHUB_OUTPUT"], "a") as gh_out:
|
||||
for line in output_lines:
|
||||
gh_out.write(line + "\n")
|
||||
|
||||
print(f"AWS changed files (filtered): {changed_raw or 'none'}")
|
||||
print(f"Run all AWS tests: {run_all}")
|
||||
if services:
|
||||
print(f"AWS service test paths: {service_paths}")
|
||||
else:
|
||||
print("AWS service test paths: none detected")
|
||||
PY
|
||||
|
||||
- name: Run AWS tests
|
||||
if: steps.changed-aws.outputs.any_changed == 'true'
|
||||
run: |
|
||||
echo "AWS run_all=${{ steps.aws-services.outputs.run_all }}"
|
||||
echo "AWS service_paths='${{ steps.aws-services.outputs.service_paths }}'"
|
||||
|
||||
if [ "${{ steps.aws-services.outputs.run_all }}" = "true" ]; then
|
||||
poetry run pytest -n auto --cov=./prowler/providers/aws --cov-report=xml:aws_coverage.xml tests/providers/aws
|
||||
elif [ -z "${{ steps.aws-services.outputs.service_paths }}" ]; then
|
||||
echo "No AWS service paths detected; skipping AWS tests."
|
||||
else
|
||||
poetry run pytest -n auto --cov=./prowler/providers/aws --cov-report=xml:aws_coverage.xml ${{ steps.aws-services.outputs.service_paths }}
|
||||
fi
|
||||
run: poetry run pytest -n auto --cov=./prowler/providers/aws --cov-report=xml:aws_coverage.xml tests/providers/aws
|
||||
|
||||
- name: Upload AWS coverage to Codecov
|
||||
if: steps.changed-aws.outputs.any_changed == 'true'
|
||||
|
||||
@@ -50,34 +50,8 @@ jobs:
|
||||
id: set-short-sha
|
||||
run: echo "short-sha=${GITHUB_SHA::7}" >> $GITHUB_OUTPUT
|
||||
|
||||
notify-release-started:
|
||||
if: github.repository == 'prowler-cloud/prowler' && (github.event_name == 'release' || github.event_name == 'workflow_dispatch')
|
||||
needs: setup
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
outputs:
|
||||
message-ts: ${{ steps.slack-notification.outputs.ts }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Notify container push started
|
||||
id: slack-notification
|
||||
uses: ./.github/actions/slack-notification
|
||||
env:
|
||||
SLACK_CHANNEL_ID: ${{ secrets.SLACK_PLATFORM_DEPLOYMENTS }}
|
||||
COMPONENT: UI
|
||||
RELEASE_TAG: ${{ env.RELEASE_TAG }}
|
||||
GITHUB_SERVER_URL: ${{ github.server_url }}
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
GITHUB_RUN_ID: ${{ github.run_id }}
|
||||
with:
|
||||
slack-bot-token: ${{ secrets.SLACK_BOT_TOKEN }}
|
||||
payload-file-path: "./.github/scripts/slack-messages/container-release-started.json"
|
||||
|
||||
container-build-push:
|
||||
needs: [setup, notify-release-started]
|
||||
if: always() && needs.setup.result == 'success' && (needs.notify-release-started.result == 'success' || needs.notify-release-started.result == 'skipped')
|
||||
needs: setup
|
||||
runs-on: ${{ matrix.runner }}
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -106,6 +80,21 @@ jobs:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
|
||||
- name: Notify container push started
|
||||
id: slack-notification-started
|
||||
if: github.event_name == 'release' || github.event_name == 'workflow_dispatch'
|
||||
uses: ./.github/actions/slack-notification
|
||||
env:
|
||||
SLACK_CHANNEL_ID: ${{ secrets.SLACK_PLATFORM_DEPLOYMENTS }}
|
||||
COMPONENT: UI
|
||||
RELEASE_TAG: ${{ env.RELEASE_TAG }}
|
||||
GITHUB_SERVER_URL: ${{ github.server_url }}
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
GITHUB_RUN_ID: ${{ github.run_id }}
|
||||
with:
|
||||
slack-bot-token: ${{ secrets.SLACK_BOT_TOKEN }}
|
||||
payload-file-path: "./.github/scripts/slack-messages/container-release-started.json"
|
||||
|
||||
- name: Build and push UI container for ${{ matrix.arch }}
|
||||
id: container-push
|
||||
if: github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'workflow_dispatch'
|
||||
@@ -122,6 +111,23 @@ jobs:
|
||||
cache-from: type=gha,scope=${{ matrix.arch }}
|
||||
cache-to: type=gha,mode=max,scope=${{ matrix.arch }}
|
||||
|
||||
- name: Notify container push completed
|
||||
if: (github.event_name == 'release' || github.event_name == 'workflow_dispatch') && always()
|
||||
uses: ./.github/actions/slack-notification
|
||||
env:
|
||||
SLACK_CHANNEL_ID: ${{ secrets.SLACK_PLATFORM_DEPLOYMENTS }}
|
||||
MESSAGE_TS: ${{ steps.slack-notification-started.outputs.ts }}
|
||||
COMPONENT: UI
|
||||
RELEASE_TAG: ${{ env.RELEASE_TAG }}
|
||||
GITHUB_SERVER_URL: ${{ github.server_url }}
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
GITHUB_RUN_ID: ${{ github.run_id }}
|
||||
with:
|
||||
slack-bot-token: ${{ secrets.SLACK_BOT_TOKEN }}
|
||||
payload-file-path: "./.github/scripts/slack-messages/container-release-completed.json"
|
||||
step-outcome: ${{ steps.container-push.outcome }}
|
||||
update-ts: ${{ steps.slack-notification-started.outputs.ts }}
|
||||
|
||||
# Create and push multi-architecture manifest
|
||||
create-manifest:
|
||||
needs: [setup, container-build-push]
|
||||
@@ -168,40 +174,6 @@ jobs:
|
||||
regctl tag delete "${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ needs.setup.outputs.short-sha }}-arm64" || true
|
||||
echo "Cleanup completed"
|
||||
|
||||
notify-release-completed:
|
||||
if: always() && needs.notify-release-started.result == 'success' && (github.event_name == 'release' || github.event_name == 'workflow_dispatch')
|
||||
needs: [setup, notify-release-started, container-build-push, create-manifest]
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Determine overall outcome
|
||||
id: outcome
|
||||
run: |
|
||||
if [[ "${{ needs.container-build-push.result }}" == "success" && "${{ needs.create-manifest.result }}" == "success" ]]; then
|
||||
echo "outcome=success" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "outcome=failure" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Notify container push completed
|
||||
uses: ./.github/actions/slack-notification
|
||||
env:
|
||||
SLACK_CHANNEL_ID: ${{ secrets.SLACK_PLATFORM_DEPLOYMENTS }}
|
||||
MESSAGE_TS: ${{ needs.notify-release-started.outputs.message-ts }}
|
||||
COMPONENT: UI
|
||||
RELEASE_TAG: ${{ env.RELEASE_TAG }}
|
||||
GITHUB_SERVER_URL: ${{ github.server_url }}
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
GITHUB_RUN_ID: ${{ github.run_id }}
|
||||
with:
|
||||
slack-bot-token: ${{ secrets.SLACK_BOT_TOKEN }}
|
||||
payload-file-path: "./.github/scripts/slack-messages/container-release-completed.json"
|
||||
step-outcome: ${{ steps.outcome.outputs.outcome }}
|
||||
update-ts: ${{ needs.notify-release-started.outputs.message-ts }}
|
||||
|
||||
trigger-deployment:
|
||||
if: github.event_name == 'push'
|
||||
needs: [setup, container-build-push]
|
||||
|
||||
@@ -1,457 +0,0 @@
|
||||
# Query Performance Guide
|
||||
|
||||
## Overview
|
||||
|
||||
This guide explains how to validate query performance when developing new endpoints or modifying existing ones. **This is part of the development process**, not a separate task—just like writing unit tests.
|
||||
|
||||
The goal is simple: ensure PostgreSQL uses indexes correctly for the queries your code generates.
|
||||
|
||||
## When to Validate
|
||||
|
||||
You **must** validate query performance when:
|
||||
|
||||
- Creating a new endpoint that queries the database
|
||||
- Modifying an existing query (adding filters, joins, or sorting)
|
||||
- Adding new indexes
|
||||
- Working on performance-critical endpoints (overviews, findings, resources)
|
||||
|
||||
## Profiling with Django Silk (Recommended)
|
||||
|
||||
[Django Silk](https://github.com/jazzband/django-silk) is the recommended way to profile queries because it captures the actual SQL generated by your code during real HTTP requests. This gives you the most accurate picture of what happens in production.
|
||||
|
||||
### Enabling Silk
|
||||
|
||||
Silk is installed as a dev dependency but disabled by default. To enable it temporarily for profiling:
|
||||
|
||||
#### 1. Add Silk to your local settings
|
||||
|
||||
In `api/src/backend/config/django/devel.py`, add at the end of the file:
|
||||
|
||||
```python
|
||||
# Silk profiler (temporary - remove after profiling)
|
||||
INSTALLED_APPS += ["silk"] # noqa: F405
|
||||
MIDDLEWARE += ["silk.middleware.SilkyMiddleware"] # noqa: F405
|
||||
```
|
||||
|
||||
#### 2. Add Silk URLs
|
||||
|
||||
In `api/src/backend/api/v1/urls.py`, add at the end:
|
||||
|
||||
```python
|
||||
from django.conf import settings
|
||||
|
||||
if settings.DEBUG:
|
||||
urlpatterns += [path("silk/", include("silk.urls", namespace="silk"))]
|
||||
```
|
||||
|
||||
#### 3. Run Silk migrations
|
||||
|
||||
```bash
|
||||
cd api/src/backend
|
||||
poetry run python manage.py migrate --database admin
|
||||
```
|
||||
|
||||
#### 4. Access Silk
|
||||
|
||||
Start the development server and navigate to `http://localhost:8000/api/v1/silk/`
|
||||
|
||||
### Using Silk
|
||||
|
||||
1. Make requests to the endpoint you want to profile
|
||||
2. Open Silk UI and find your request
|
||||
3. Click on the request to see all SQL queries executed
|
||||
4. For each query, you can see:
|
||||
- Execution time
|
||||
- Number of similar queries (N+1 detection)
|
||||
- The actual SQL with parameters
|
||||
- **EXPLAIN output** (click on a query to see it)
|
||||
|
||||
### Disabling Silk
|
||||
|
||||
After profiling, **remove the changes** you made to `devel.py` and `urls.py`. Don't commit Silk configuration to the repository.
|
||||
|
||||
## Manual Query Analysis with EXPLAIN ANALYZE
|
||||
|
||||
For quick checks or when you need more control, you can run `EXPLAIN ANALYZE` directly.
|
||||
|
||||
### 1. Get Your Query
|
||||
|
||||
#### Option A: Using Django Shell with RLS
|
||||
|
||||
This approach mirrors how queries run in production with Row Level Security enabled:
|
||||
|
||||
```bash
|
||||
cd api/src/backend
|
||||
poetry run python manage.py shell
|
||||
```
|
||||
|
||||
```python
|
||||
from django.db import connection
|
||||
from api.db_utils import rls_transaction
|
||||
from api.models import Finding
|
||||
|
||||
tenant_id = "your-tenant-uuid"
|
||||
|
||||
with rls_transaction(tenant_id):
|
||||
# Build your queryset
|
||||
qs = Finding.objects.filter(status="FAIL").order_by("-inserted_at")[:25]
|
||||
|
||||
# Force evaluation
|
||||
list(qs)
|
||||
|
||||
# Get the SQL
|
||||
print(connection.queries[-1]['sql'])
|
||||
```
|
||||
|
||||
#### Option B: Print Query Without Execution
|
||||
|
||||
```python
|
||||
from api.models import Finding
|
||||
|
||||
queryset = Finding.objects.filter(status="FAIL")
|
||||
print(queryset.query)
|
||||
```
|
||||
|
||||
> **Note:** This won't include RLS filters, so the actual production query will differ.
|
||||
|
||||
#### Option C: Enable SQL Logging
|
||||
|
||||
Set `DJANGO_LOGGING_LEVEL=DEBUG` in your environment:
|
||||
|
||||
```bash
|
||||
DJANGO_LOGGING_LEVEL=DEBUG poetry run python manage.py runserver
|
||||
```
|
||||
|
||||
### 2. Run EXPLAIN ANALYZE
|
||||
|
||||
Connect to PostgreSQL and run:
|
||||
|
||||
```sql
|
||||
EXPLAIN ANALYZE <your_query>;
|
||||
```
|
||||
|
||||
Or with more details:
|
||||
|
||||
```sql
|
||||
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) <your_query>;
|
||||
```
|
||||
|
||||
#### Running EXPLAIN with RLS Context
|
||||
|
||||
To test with RLS enabled (as it runs in production), set the tenant context first:
|
||||
|
||||
```sql
|
||||
-- Set tenant context
|
||||
SELECT set_config('api.tenant_id', 'your-tenant-uuid', TRUE);
|
||||
|
||||
-- Then run your EXPLAIN ANALYZE
|
||||
EXPLAIN ANALYZE SELECT * FROM findings WHERE status = 'FAIL' LIMIT 25;
|
||||
```
|
||||
|
||||
### 3. Interpret the Results
|
||||
|
||||
#### Good Signs (Index is being used)
|
||||
|
||||
```
|
||||
Index Scan using findings_tenant_status_idx on findings
|
||||
Index Cond: ((tenant_id = '...'::uuid) AND (status = 'FAIL'))
|
||||
Rows Removed by Filter: 0
|
||||
Actual Rows: 150
|
||||
Planning Time: 0.5 ms
|
||||
Execution Time: 2.3 ms
|
||||
```
|
||||
|
||||
#### Bad Signs (Sequential scan - no index)
|
||||
|
||||
```
|
||||
Seq Scan on findings
|
||||
Filter: ((tenant_id = '...'::uuid) AND (status = 'FAIL'))
|
||||
Rows Removed by Filter: 999850
|
||||
Actual Rows: 150
|
||||
Planning Time: 0.3 ms
|
||||
Execution Time: 450.2 ms
|
||||
```
|
||||
|
||||
## Quick Reference: What to Look For
|
||||
|
||||
| What You See | Meaning | Action |
|
||||
|--------------|---------|--------|
|
||||
| `Index Scan` | Index is being used | Good, no action needed |
|
||||
| `Index Only Scan` | Even better - data comes from index only | Good, no action needed |
|
||||
| `Bitmap Index Scan` | Index used, results combined | Usually fine |
|
||||
| `Seq Scan` on large tables | Full table scan, no index | **Needs investigation** |
|
||||
| `Rows Removed by Filter: <high number>` | Fetching too many rows | **Query or index issue** |
|
||||
| High `Execution Time` | Query is slow | **Needs optimization** |
|
||||
|
||||
## Common Issues and Fixes
|
||||
|
||||
### 1. Missing Index
|
||||
|
||||
**Problem:** `Seq Scan` on a filtered column
|
||||
|
||||
```sql
|
||||
-- Bad: No index on status
|
||||
EXPLAIN ANALYZE SELECT * FROM findings WHERE status = 'FAIL';
|
||||
-- Shows: Seq Scan on findings
|
||||
```
|
||||
|
||||
**Fix:** Add an index
|
||||
|
||||
```python
|
||||
# In your model
|
||||
class Meta:
|
||||
indexes = [
|
||||
models.Index(fields=['status'], name='findings_status_idx'),
|
||||
]
|
||||
```
|
||||
|
||||
### 2. Index Not Used Due to Type Mismatch
|
||||
|
||||
**Problem:** Index exists but PostgreSQL doesn't use it
|
||||
|
||||
```sql
|
||||
-- If tenant_id is UUID but you're passing a string without cast
|
||||
WHERE tenant_id = 'some-uuid-string'
|
||||
```
|
||||
|
||||
**Fix:** Ensure proper type casting in your queries
|
||||
|
||||
### 3. Index Not Used Due to Function Call
|
||||
|
||||
**Problem:** Wrapping column in a function prevents index usage
|
||||
|
||||
```sql
|
||||
-- Bad: Index on inserted_at won't be used
|
||||
WHERE DATE(inserted_at) = '2024-01-01'
|
||||
|
||||
-- Good: Use range instead
|
||||
WHERE inserted_at >= '2024-01-01' AND inserted_at < '2024-01-02'
|
||||
```
|
||||
|
||||
### 4. Wrong Index for Sorting
|
||||
|
||||
**Problem:** Query is sorted but index doesn't match sort order
|
||||
|
||||
```sql
|
||||
-- If you have ORDER BY inserted_at DESC
|
||||
-- You need an index with DESC or PostgreSQL will sort in memory
|
||||
```
|
||||
|
||||
**Fix:** Create index with matching sort order
|
||||
|
||||
```python
|
||||
class Meta:
|
||||
indexes = [
|
||||
models.Index(fields=['-inserted_at'], name='findings_inserted_desc_idx'),
|
||||
]
|
||||
```
|
||||
|
||||
### 5. Composite Index Column Order
|
||||
|
||||
**Problem:** Index exists but columns are in wrong order
|
||||
|
||||
```sql
|
||||
-- Index on (tenant_id, scan_id)
|
||||
-- This query WON'T use the index efficiently:
|
||||
WHERE scan_id = '...'
|
||||
|
||||
-- This query WILL use the index:
|
||||
WHERE tenant_id = '...' AND scan_id = '...'
|
||||
```
|
||||
|
||||
**Rule:** The leftmost columns in a composite index must be in your WHERE clause.
|
||||
|
||||
## RLS (Row Level Security) Considerations
|
||||
|
||||
Prowler uses Row Level Security via PostgreSQL's `set_config`. When analyzing queries, remember:
|
||||
|
||||
1. RLS policies add implicit `WHERE tenant_id = current_tenant()` to queries
|
||||
2. Always test with RLS enabled (how it runs in production)
|
||||
3. Ensure `tenant_id` is the **first column** in composite indexes
|
||||
|
||||
### Using rls_transaction in Code
|
||||
|
||||
The `rls_transaction` context manager from `api.db_utils` sets the tenant context for all queries within its scope:
|
||||
|
||||
```python
|
||||
from api.db_utils import rls_transaction
|
||||
from api.models import Finding
|
||||
|
||||
with rls_transaction(tenant_id):
|
||||
# All queries here will have RLS applied
|
||||
qs = Finding.objects.filter(status="FAIL")
|
||||
list(qs) # Execute
|
||||
```
|
||||
|
||||
### Using RLS in Raw SQL (psql)
|
||||
|
||||
```sql
|
||||
-- Set tenant context for the transaction
|
||||
SELECT set_config('api.tenant_id', 'your-tenant-uuid', TRUE);
|
||||
|
||||
-- Now RLS policies will filter by this tenant
|
||||
EXPLAIN ANALYZE SELECT * FROM findings WHERE status = 'FAIL';
|
||||
```
|
||||
|
||||
### Index Design for RLS
|
||||
|
||||
Since every query includes `tenant_id` via RLS, your composite indexes should **always start with `tenant_id`**:
|
||||
|
||||
```python
|
||||
class Meta:
|
||||
indexes = [
|
||||
# Good: tenant_id first
|
||||
models.Index(fields=['tenant_id', 'status', 'severity']),
|
||||
|
||||
# Bad: tenant_id not first - RLS queries won't use this efficiently
|
||||
models.Index(fields=['status', 'tenant_id']),
|
||||
]
|
||||
```
|
||||
|
||||
## Test Data Requirements
|
||||
|
||||
The amount of test data you need depends on what you're testing. PostgreSQL's query planner considers table statistics, index definitions, and data distribution when choosing execution plans.
|
||||
|
||||
### Important Considerations
|
||||
|
||||
1. **Small datasets may not use indexes**: PostgreSQL may choose a sequential scan over an index scan if the table is small enough that scanning it directly is faster. This is expected behavior.
|
||||
|
||||
2. **Data must exist in the tables you're querying**: If your endpoint queries `findings`, `resources`, `scans`, or other tables, ensure those tables have data. Use the `findings` management command to generate test data:
|
||||
|
||||
```bash
|
||||
cd api/src/backend
|
||||
poetry run python manage.py findings \
|
||||
--tenant <TENANT_ID> \
|
||||
--findings 1000 \
|
||||
--resources 500 \
|
||||
--batch 500
|
||||
```
|
||||
|
||||
3. **Update table statistics**: After inserting test data, run `ANALYZE` to update PostgreSQL's statistics:
|
||||
|
||||
```sql
|
||||
ANALYZE findings;
|
||||
ANALYZE resources;
|
||||
ANALYZE scans;
|
||||
-- Add other tables as needed
|
||||
```
|
||||
|
||||
4. **Test with realistic data distribution**: If your query filters by a specific value (e.g., `status='FAIL'`), ensure your test data includes a realistic mix of values.
|
||||
|
||||
### When Index Usage Matters Most
|
||||
|
||||
Focus on validating index usage when:
|
||||
|
||||
- The table will have thousands or millions of rows in production
|
||||
- The query is called frequently (list endpoints, dashboards)
|
||||
- The query has multiple filters or joins
|
||||
|
||||
For small lookup tables or infrequently-called endpoints, sequential scans may be acceptable.
|
||||
|
||||
## Performance Checklist for PRs
|
||||
|
||||
Before submitting a PR that adds or modifies database queries:
|
||||
|
||||
- [ ] Profiled queries with Silk or `EXPLAIN ANALYZE`
|
||||
- [ ] Verified indexes are being used (no unexpected `Seq Scan` on large tables)
|
||||
- [ ] Checked `Rows Removed by Filter` is reasonable
|
||||
- [ ] Tested with RLS enabled
|
||||
- [ ] For critical endpoints: documented the query plan in the PR
|
||||
|
||||
## Useful Commands
|
||||
|
||||
### Update Table Statistics
|
||||
|
||||
```sql
|
||||
ANALYZE findings;
|
||||
ANALYZE resources;
|
||||
```
|
||||
|
||||
### See Existing Indexes
|
||||
|
||||
```sql
|
||||
SELECT indexname, indexdef
|
||||
FROM pg_indexes
|
||||
WHERE tablename = 'findings';
|
||||
```
|
||||
|
||||
### See Index Usage Stats
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
schemaname,
|
||||
tablename,
|
||||
indexname,
|
||||
idx_scan,
|
||||
idx_tup_read,
|
||||
idx_tup_fetch
|
||||
FROM pg_stat_user_indexes
|
||||
WHERE tablename = 'findings'
|
||||
ORDER BY idx_scan DESC;
|
||||
```
|
||||
|
||||
### Check Table Size
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
relname as table_name,
|
||||
pg_size_pretty(pg_total_relation_size(relid)) as total_size
|
||||
FROM pg_catalog.pg_statio_user_tables
|
||||
WHERE relname IN ('findings', 'resources', 'scans')
|
||||
ORDER BY pg_total_relation_size(relid) DESC;
|
||||
```
|
||||
|
||||
## Working with Partitioned Tables
|
||||
|
||||
The `findings` and `resource_finding_mappings` tables are partitioned. When adding indexes, use the helper functions from `api.db_utils`:
|
||||
|
||||
### Adding Indexes to Partitions
|
||||
|
||||
```python
|
||||
# In a migration file
|
||||
from functools import partial
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
from api.db_utils import create_index_on_partitions, drop_index_on_partitions
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
atomic = False # Required for CONCURRENTLY
|
||||
|
||||
dependencies = [
|
||||
("api", "XXXX_previous_migration"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
partial(
|
||||
create_index_on_partitions,
|
||||
parent_table="findings",
|
||||
index_name="my_new_idx",
|
||||
columns="tenant_id, status, severity",
|
||||
all_partitions=False, # Only current/future partitions
|
||||
),
|
||||
reverse_code=partial(
|
||||
drop_index_on_partitions,
|
||||
parent_table="findings",
|
||||
index_name="my_new_idx",
|
||||
),
|
||||
),
|
||||
]
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
- `all_partitions=False` (default): Only creates indexes on current and future partitions. Use this for new indexes to avoid maintenance overhead on old data.
|
||||
- `all_partitions=True`: Creates indexes on all partitions. Use when migrating critical existing indexes.
|
||||
|
||||
See [Partitions Documentation](./partitions.md) for more details on partitioning strategy.
|
||||
|
||||
## Further Reading
|
||||
|
||||
- [Django Silk Documentation](https://github.com/jazzband/django-silk)
|
||||
- [PostgreSQL EXPLAIN Documentation](https://www.postgresql.org/docs/current/sql-explain.html)
|
||||
- [Using EXPLAIN](https://www.postgresql.org/docs/current/using-explain.html)
|
||||
- [Index Types in PostgreSQL](https://www.postgresql.org/docs/current/indexes-types.html)
|
||||
- [Prowler Partitions Documentation](./partitions.md)
|
||||
@@ -62,7 +62,7 @@ You are a code reviewer for the Prowler UI project. Analyze the full file conten
|
||||
**RULES TO CHECK:**
|
||||
1. React Imports: NO `import * as React` or `import React, {` → Use `import { useState }`
|
||||
2. TypeScript: NO union types like `type X = "a" | "b"` → Use const-based: `const X = {...} as const`
|
||||
3. Tailwind: NO `var()` or hex colors in className → Use Tailwind utilities and semantic color classes.
|
||||
3. Tailwind: NO `var()` or hex colors in className → Use Tailwind utilities and semantic color classes. Exception: `var()` is allowed when passing colors to chart/graph components that require CSS color strings (not Tailwind classes) for their APIs
|
||||
4. cn(): Use for merging multiple classes or for conditionals (handles Tailwind conflicts with twMerge) → `cn(BUTTON_STYLES.base, BUTTON_STYLES.active, isLoading && "opacity-50")`
|
||||
5. React 19: NO `useMemo`/`useCallback` without reason
|
||||
6. Zod v4: Use `.min(1)` not `.nonempty()`, `z.email()` not `z.string().email()`. All inputs must be validated with Zod.
|
||||
|
||||
+7
-1
@@ -6,7 +6,6 @@ All notable changes to the **Prowler UI** are documented in this file.
|
||||
|
||||
### 🚀 Added
|
||||
|
||||
- Risk Plot component with interactive legend and severity navigation to Overview page [(#9469)](https://github.com/prowler-cloud/prowler/pull/9469)
|
||||
- Navigation progress bar for page transitions using Next.js `onRouterTransitionStart` [(#9465)](https://github.com/prowler-cloud/prowler/pull/9465)
|
||||
- Finding Severity Over Time chart component to Overview page [(#9405)](https://github.com/prowler-cloud/prowler/pull/9405)
|
||||
- Attack Surface component to Overview page [(#9412)](https://github.com/prowler-cloud/prowler/pull/9412)
|
||||
@@ -24,6 +23,13 @@ All notable changes to the **Prowler UI** are documented in this file.
|
||||
|
||||
---
|
||||
|
||||
## [1.14.3] (Prowler Unreleased)
|
||||
|
||||
### 🐞 Fixed
|
||||
- Show top failed requirements in compliance specific view for compliance without sections [(#9471)](https://github.com/prowler-cloud/prowler/pull/9471)
|
||||
|
||||
---
|
||||
|
||||
## [1.14.2] (Prowler v5.14.2)
|
||||
|
||||
### 🐞 Fixed
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
"use server";
|
||||
|
||||
import {
|
||||
getDateFromForTimeRange,
|
||||
type TimeRange,
|
||||
} from "@/app/(prowler)/_new-overview/severity-over-time/_constants/time-range.constants";
|
||||
import { apiBaseUrl, getAuthHeaders } from "@/lib";
|
||||
import { handleApiResponse } from "@/lib/server-actions-helper";
|
||||
|
||||
@@ -13,6 +9,20 @@ import {
|
||||
FindingsSeverityOverTimeResponse,
|
||||
} from "./types";
|
||||
|
||||
const TIME_RANGE_VALUES = {
|
||||
FIVE_DAYS: "5D",
|
||||
ONE_WEEK: "1W",
|
||||
ONE_MONTH: "1M",
|
||||
} as const;
|
||||
|
||||
type TimeRange = (typeof TIME_RANGE_VALUES)[keyof typeof TIME_RANGE_VALUES];
|
||||
|
||||
const TIME_RANGE_DAYS: Record<TimeRange, number> = {
|
||||
"5D": 5,
|
||||
"1W": 7,
|
||||
"1M": 30,
|
||||
};
|
||||
|
||||
export type SeverityTrendsResult =
|
||||
| { status: "success"; data: AdaptedSeverityTrendsResponse }
|
||||
| { status: "empty" }
|
||||
@@ -66,9 +76,21 @@ export const getSeverityTrendsByTimeRange = async ({
|
||||
timeRange: TimeRange;
|
||||
filters?: Record<string, string | string[] | undefined>;
|
||||
}): Promise<SeverityTrendsResult> => {
|
||||
const days = TIME_RANGE_DAYS[timeRange];
|
||||
|
||||
if (!days) {
|
||||
console.error("Invalid time range provided");
|
||||
return { status: "error" };
|
||||
}
|
||||
|
||||
const endDate = new Date();
|
||||
const startDate = new Date(endDate.getTime() - days * 24 * 60 * 60 * 1000);
|
||||
|
||||
const dateFrom = startDate.toISOString().split("T")[0];
|
||||
|
||||
const dateFilters = {
|
||||
...filters,
|
||||
"filter[date_from]": getDateFromForTimeRange(timeRange),
|
||||
date_from: dateFrom,
|
||||
};
|
||||
|
||||
return getFindingsSeverityTrends({ filters: dateFilters });
|
||||
|
||||
+1
-2
@@ -13,7 +13,6 @@ import {
|
||||
SeverityLevel,
|
||||
} from "@/types/severities";
|
||||
|
||||
import { DEFAULT_TIME_RANGE } from "../_constants/time-range.constants";
|
||||
import { type TimeRange, TimeRangeSelector } from "./time-range-selector";
|
||||
|
||||
interface FindingSeverityOverTimeProps {
|
||||
@@ -25,7 +24,7 @@ export const FindingSeverityOverTime = ({
|
||||
}: FindingSeverityOverTimeProps) => {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const [timeRange, setTimeRange] = useState<TimeRange>(DEFAULT_TIME_RANGE);
|
||||
const [timeRange, setTimeRange] = useState<TimeRange>("5D");
|
||||
const [data, setData] = useState<LineDataPoint[]>(initialData);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
+7
-5
@@ -2,12 +2,14 @@
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import {
|
||||
TIME_RANGE_OPTIONS,
|
||||
type TimeRange,
|
||||
} from "../_constants/time-range.constants";
|
||||
const TIME_RANGE_OPTIONS = {
|
||||
FIVE_DAYS: "5D",
|
||||
ONE_WEEK: "1W",
|
||||
ONE_MONTH: "1M",
|
||||
} as const;
|
||||
|
||||
export type { TimeRange };
|
||||
export type TimeRange =
|
||||
(typeof TIME_RANGE_OPTIONS)[keyof typeof TIME_RANGE_OPTIONS];
|
||||
|
||||
interface TimeRangeSelectorProps {
|
||||
value: TimeRange;
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./time-range.constants";
|
||||
@@ -1,23 +0,0 @@
|
||||
export const TIME_RANGE_OPTIONS = {
|
||||
FIVE_DAYS: "5D",
|
||||
ONE_WEEK: "1W",
|
||||
ONE_MONTH: "1M",
|
||||
} as const;
|
||||
|
||||
export type TimeRange =
|
||||
(typeof TIME_RANGE_OPTIONS)[keyof typeof TIME_RANGE_OPTIONS];
|
||||
|
||||
export const TIME_RANGE_DAYS: Record<TimeRange, number> = {
|
||||
"5D": 5,
|
||||
"1W": 7,
|
||||
"1M": 30,
|
||||
};
|
||||
|
||||
export const DEFAULT_TIME_RANGE: TimeRange = "5D";
|
||||
|
||||
export const getDateFromForTimeRange = (timeRange: TimeRange): string => {
|
||||
const days = TIME_RANGE_DAYS[timeRange];
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() - days);
|
||||
return date.toISOString().split("T")[0];
|
||||
};
|
||||
+2
-7
@@ -1,11 +1,10 @@
|
||||
import { getSeverityTrendsByTimeRange } from "@/actions/overview/severity-trends";
|
||||
import { getFindingsSeverityTrends } from "@/actions/overview/severity-trends";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/shadcn";
|
||||
|
||||
import { pickFilterParams } from "../_lib/filter-params";
|
||||
import { SSRComponentProps } from "../_types";
|
||||
import { FindingSeverityOverTime } from "./_components/finding-severity-over-time";
|
||||
import { FindingSeverityOverTimeSkeleton } from "./_components/finding-severity-over-time.skeleton";
|
||||
import { DEFAULT_TIME_RANGE } from "./_constants/time-range.constants";
|
||||
|
||||
export { FindingSeverityOverTimeSkeleton };
|
||||
|
||||
@@ -26,11 +25,7 @@ export const FindingSeverityOverTimeSSR = async ({
|
||||
searchParams,
|
||||
}: SSRComponentProps) => {
|
||||
const filters = pickFilterParams(searchParams);
|
||||
|
||||
const result = await getSeverityTrendsByTimeRange({
|
||||
timeRange: DEFAULT_TIME_RANGE,
|
||||
filters,
|
||||
});
|
||||
const result = await getFindingsSeverityTrends({ filters });
|
||||
|
||||
if (result.status === "error") {
|
||||
return <EmptyState message="Failed to load severity trends data" />;
|
||||
|
||||
@@ -195,7 +195,7 @@ const SSRComplianceContent = async ({
|
||||
{ pass: 0, fail: 0, manual: 0 },
|
||||
);
|
||||
const accordionItems = mapper.toAccordionItems(data, scanId);
|
||||
const topFailedSections = mapper.getTopFailedSections(data);
|
||||
const topFailedResult = mapper.getTopFailedSections(data);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-8">
|
||||
@@ -205,7 +205,10 @@ const SSRComplianceContent = async ({
|
||||
fail={totalRequirements.fail}
|
||||
manual={totalRequirements.manual}
|
||||
/>
|
||||
<TopFailedSectionsCard sections={topFailedSections} />
|
||||
<TopFailedSectionsCard
|
||||
sections={topFailedResult.items}
|
||||
dataType={topFailedResult.type}
|
||||
/>
|
||||
{/* <SectionsFailureRateCard categories={categoryHeatmapData} /> */}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -3,14 +3,20 @@
|
||||
import { HorizontalBarChart } from "@/components/graphs/horizontal-bar-chart";
|
||||
import { BarDataPoint } from "@/components/graphs/types";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/shadcn";
|
||||
import { FailedSection } from "@/types/compliance";
|
||||
import {
|
||||
FailedSection,
|
||||
TOP_FAILED_DATA_TYPE,
|
||||
TopFailedDataType,
|
||||
} from "@/types/compliance";
|
||||
|
||||
interface TopFailedSectionsCardProps {
|
||||
sections: FailedSection[];
|
||||
dataType?: TopFailedDataType;
|
||||
}
|
||||
|
||||
export function TopFailedSectionsCard({
|
||||
sections,
|
||||
dataType = TOP_FAILED_DATA_TYPE.SECTIONS,
|
||||
}: TopFailedSectionsCardProps) {
|
||||
// Transform FailedSection[] to BarDataPoint[]
|
||||
const total = sections.reduce((sum, section) => sum + section.total, 0);
|
||||
@@ -22,13 +28,18 @@ export function TopFailedSectionsCard({
|
||||
color: "var(--bg-fail-primary)",
|
||||
}));
|
||||
|
||||
const title =
|
||||
dataType === TOP_FAILED_DATA_TYPE.REQUIREMENTS
|
||||
? "Top Failed Requirements"
|
||||
: "Top Failed Sections";
|
||||
|
||||
return (
|
||||
<Card
|
||||
variant="base"
|
||||
className="flex min-h-[372px] w-full flex-col sm:min-w-[500px]"
|
||||
>
|
||||
<CardHeader>
|
||||
<CardTitle>Top Failed Sections</CardTitle>
|
||||
<CardTitle>{title}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-1 items-center justify-start">
|
||||
<HorizontalBarChart data={barData} />
|
||||
|
||||
@@ -1,14 +1,65 @@
|
||||
import {
|
||||
Category,
|
||||
CategoryData,
|
||||
Control,
|
||||
FailedSection,
|
||||
Framework,
|
||||
Requirement,
|
||||
REQUIREMENT_STATUS,
|
||||
RequirementItemData,
|
||||
RequirementsData,
|
||||
RequirementStatus,
|
||||
TOP_FAILED_DATA_TYPE,
|
||||
TopFailedDataType,
|
||||
TopFailedResult,
|
||||
} from "@/types/compliance";
|
||||
|
||||
// Type for the internal map used in getTopFailedSections
|
||||
interface FailedSectionData {
|
||||
total: number;
|
||||
types: Record<string, number>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the TopFailedResult from the accumulated map data
|
||||
*/
|
||||
const buildTopFailedResult = (
|
||||
map: Map<string, FailedSectionData>,
|
||||
type: TopFailedDataType,
|
||||
): TopFailedResult => ({
|
||||
items: Array.from(map.entries())
|
||||
.map(([name, data]): FailedSection => ({ name, ...data }))
|
||||
.sort((a, b) => b.total - a.total)
|
||||
.slice(0, 5),
|
||||
type,
|
||||
});
|
||||
|
||||
/**
|
||||
* Checks if the framework uses a flat structure (requirements directly on framework)
|
||||
* vs hierarchical structure (categories -> controls -> requirements)
|
||||
*/
|
||||
const hasFlatStructure = (frameworks: Framework[]): boolean =>
|
||||
frameworks.some(
|
||||
(framework) =>
|
||||
(framework.requirements?.length ?? 0) > 0 &&
|
||||
framework.categories.length === 0,
|
||||
);
|
||||
|
||||
/**
|
||||
* Increments the failed count for a given name in the map
|
||||
*/
|
||||
const incrementFailedCount = (
|
||||
map: Map<string, FailedSectionData>,
|
||||
name: string,
|
||||
type: string,
|
||||
): void => {
|
||||
if (!map.has(name)) {
|
||||
map.set(name, { total: 0, types: {} });
|
||||
}
|
||||
const data = map.get(name)!;
|
||||
data.total += 1;
|
||||
data.types[type] = (data.types[type] || 0) + 1;
|
||||
};
|
||||
|
||||
export const updateCounters = (
|
||||
target: { pass: number; fail: number; manual: number },
|
||||
status: RequirementStatus,
|
||||
@@ -24,38 +75,45 @@ export const updateCounters = (
|
||||
|
||||
export const getTopFailedSections = (
|
||||
mappedData: Framework[],
|
||||
): FailedSection[] => {
|
||||
const failedSectionMap = new Map();
|
||||
): TopFailedResult => {
|
||||
const failedSectionMap = new Map<string, FailedSectionData>();
|
||||
|
||||
if (hasFlatStructure(mappedData)) {
|
||||
// Handle flat structure: count failed requirements directly
|
||||
mappedData.forEach((framework) => {
|
||||
const directRequirements = framework.requirements ?? [];
|
||||
|
||||
directRequirements.forEach((requirement) => {
|
||||
if (requirement.status === REQUIREMENT_STATUS.FAIL) {
|
||||
const type =
|
||||
typeof requirement.type === "string" ? requirement.type : "Fails";
|
||||
incrementFailedCount(failedSectionMap, requirement.name, type);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return buildTopFailedResult(
|
||||
failedSectionMap,
|
||||
TOP_FAILED_DATA_TYPE.REQUIREMENTS,
|
||||
);
|
||||
}
|
||||
|
||||
// Handle hierarchical structure: count by category (section)
|
||||
mappedData.forEach((framework) => {
|
||||
framework.categories.forEach((category) => {
|
||||
category.controls.forEach((control) => {
|
||||
control.requirements.forEach((requirement) => {
|
||||
if (requirement.status === REQUIREMENT_STATUS.FAIL) {
|
||||
const sectionName = category.name;
|
||||
|
||||
if (!failedSectionMap.has(sectionName)) {
|
||||
failedSectionMap.set(sectionName, { total: 0, types: {} });
|
||||
}
|
||||
|
||||
const sectionData = failedSectionMap.get(sectionName);
|
||||
sectionData.total += 1;
|
||||
|
||||
const type = requirement.type || "Fails";
|
||||
|
||||
sectionData.types[type as string] =
|
||||
(sectionData.types[type as string] || 0) + 1;
|
||||
const type =
|
||||
typeof requirement.type === "string" ? requirement.type : "Fails";
|
||||
incrementFailedCount(failedSectionMap, category.name, type);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Convert in descending order and slice top 5
|
||||
return Array.from(failedSectionMap.entries())
|
||||
.map(([name, data]) => ({ name, ...data }))
|
||||
.sort((a, b) => b.total - a.total)
|
||||
.slice(0, 5); // Top 5
|
||||
return buildTopFailedResult(failedSectionMap, TOP_FAILED_DATA_TYPE.SECTIONS);
|
||||
};
|
||||
|
||||
export const calculateCategoryHeatmapData = (
|
||||
@@ -146,9 +204,9 @@ export const findOrCreateFramework = (
|
||||
};
|
||||
|
||||
export const findOrCreateCategory = (
|
||||
categories: any[],
|
||||
categories: Category[],
|
||||
categoryName: string,
|
||||
) => {
|
||||
): Category => {
|
||||
let category = categories.find((c) => c.name === categoryName);
|
||||
if (!category) {
|
||||
category = {
|
||||
@@ -163,7 +221,10 @@ export const findOrCreateCategory = (
|
||||
return category;
|
||||
};
|
||||
|
||||
export const findOrCreateControl = (controls: any[], controlLabel: string) => {
|
||||
export const findOrCreateControl = (
|
||||
controls: Control[],
|
||||
controlLabel: string,
|
||||
): Control => {
|
||||
let control = controls.find((c) => c.label === controlLabel);
|
||||
if (!control) {
|
||||
control = {
|
||||
@@ -178,7 +239,7 @@ export const findOrCreateControl = (controls: any[], controlLabel: string) => {
|
||||
return control;
|
||||
};
|
||||
|
||||
export const calculateFrameworkCounters = (frameworks: Framework[]) => {
|
||||
export const calculateFrameworkCounters = (frameworks: Framework[]): void => {
|
||||
frameworks.forEach((framework) => {
|
||||
// Reset framework counters
|
||||
framework.pass = 0;
|
||||
@@ -186,9 +247,9 @@ export const calculateFrameworkCounters = (frameworks: Framework[]) => {
|
||||
framework.manual = 0;
|
||||
|
||||
// Handle flat structure (requirements directly in framework)
|
||||
const directRequirements = (framework as any).requirements || [];
|
||||
const directRequirements = framework.requirements ?? [];
|
||||
if (directRequirements.length > 0) {
|
||||
directRequirements.forEach((requirement: Requirement) => {
|
||||
directRequirements.forEach((requirement) => {
|
||||
updateCounters(framework, requirement.status);
|
||||
});
|
||||
return;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import { createElement, ReactNode } from "react";
|
||||
|
||||
import { AWSWellArchitectedCustomDetails } from "@/components/compliance/compliance-custom-details/aws-well-architected-details";
|
||||
import { C5CustomDetails } from "@/components/compliance/compliance-custom-details/c5-details";
|
||||
@@ -14,10 +14,10 @@ import { AccordionItemProps } from "@/components/ui/accordion/Accordion";
|
||||
import {
|
||||
AttributesData,
|
||||
CategoryData,
|
||||
FailedSection,
|
||||
Framework,
|
||||
Requirement,
|
||||
RequirementsData,
|
||||
TopFailedResult,
|
||||
} from "@/types/compliance";
|
||||
|
||||
import {
|
||||
@@ -74,9 +74,9 @@ export interface ComplianceMapper {
|
||||
data: Framework[],
|
||||
scanId: string | undefined,
|
||||
) => AccordionItemProps[];
|
||||
getTopFailedSections: (mappedData: Framework[]) => FailedSection[];
|
||||
getTopFailedSections: (mappedData: Framework[]) => TopFailedResult;
|
||||
calculateCategoryHeatmapData: (complianceData: Framework[]) => CategoryData[];
|
||||
getDetailsComponent: (requirement: Requirement) => React.ReactNode;
|
||||
getDetailsComponent: (requirement: Requirement) => ReactNode;
|
||||
}
|
||||
|
||||
const getDefaultMapper = (): ComplianceMapper => ({
|
||||
@@ -86,7 +86,7 @@ const getDefaultMapper = (): ComplianceMapper => ({
|
||||
calculateCategoryHeatmapData: (data: Framework[]) =>
|
||||
calculateCategoryHeatmapData(data),
|
||||
getDetailsComponent: (requirement: Requirement) =>
|
||||
React.createElement(GenericCustomDetails, { requirement }),
|
||||
createElement(GenericCustomDetails, { requirement }),
|
||||
});
|
||||
|
||||
const getComplianceMappers = (): Record<string, ComplianceMapper> => ({
|
||||
@@ -97,7 +97,7 @@ const getComplianceMappers = (): Record<string, ComplianceMapper> => ({
|
||||
calculateCategoryHeatmapData: (data: Framework[]) =>
|
||||
calculateCategoryHeatmapData(data),
|
||||
getDetailsComponent: (requirement: Requirement) =>
|
||||
React.createElement(C5CustomDetails, { requirement }),
|
||||
createElement(C5CustomDetails, { requirement }),
|
||||
},
|
||||
ENS: {
|
||||
mapComplianceData: mapENSComplianceData,
|
||||
@@ -106,7 +106,7 @@ const getComplianceMappers = (): Record<string, ComplianceMapper> => ({
|
||||
calculateCategoryHeatmapData: (data: Framework[]) =>
|
||||
calculateCategoryHeatmapData(data),
|
||||
getDetailsComponent: (requirement: Requirement) =>
|
||||
React.createElement(ENSCustomDetails, { requirement }),
|
||||
createElement(ENSCustomDetails, { requirement }),
|
||||
},
|
||||
ISO27001: {
|
||||
mapComplianceData: mapISOComplianceData,
|
||||
@@ -115,7 +115,7 @@ const getComplianceMappers = (): Record<string, ComplianceMapper> => ({
|
||||
calculateCategoryHeatmapData: (data: Framework[]) =>
|
||||
calculateCategoryHeatmapData(data),
|
||||
getDetailsComponent: (requirement: Requirement) =>
|
||||
React.createElement(ISOCustomDetails, { requirement }),
|
||||
createElement(ISOCustomDetails, { requirement }),
|
||||
},
|
||||
CIS: {
|
||||
mapComplianceData: mapCISComplianceData,
|
||||
@@ -124,7 +124,7 @@ const getComplianceMappers = (): Record<string, ComplianceMapper> => ({
|
||||
calculateCategoryHeatmapData: (data: Framework[]) =>
|
||||
calculateCategoryHeatmapData(data),
|
||||
getDetailsComponent: (requirement: Requirement) =>
|
||||
React.createElement(CISCustomDetails, { requirement }),
|
||||
createElement(CISCustomDetails, { requirement }),
|
||||
},
|
||||
"AWS-Well-Architected-Framework-Security-Pillar": {
|
||||
mapComplianceData: mapAWSWellArchitectedComplianceData,
|
||||
@@ -133,7 +133,7 @@ const getComplianceMappers = (): Record<string, ComplianceMapper> => ({
|
||||
calculateCategoryHeatmapData: (data: Framework[]) =>
|
||||
calculateCategoryHeatmapData(data),
|
||||
getDetailsComponent: (requirement: Requirement) =>
|
||||
React.createElement(AWSWellArchitectedCustomDetails, { requirement }),
|
||||
createElement(AWSWellArchitectedCustomDetails, { requirement }),
|
||||
},
|
||||
"AWS-Well-Architected-Framework-Reliability-Pillar": {
|
||||
mapComplianceData: mapAWSWellArchitectedComplianceData,
|
||||
@@ -142,7 +142,7 @@ const getComplianceMappers = (): Record<string, ComplianceMapper> => ({
|
||||
calculateCategoryHeatmapData: (data: Framework[]) =>
|
||||
calculateCategoryHeatmapData(data),
|
||||
getDetailsComponent: (requirement: Requirement) =>
|
||||
React.createElement(AWSWellArchitectedCustomDetails, { requirement }),
|
||||
createElement(AWSWellArchitectedCustomDetails, { requirement }),
|
||||
},
|
||||
"KISA-ISMS-P": {
|
||||
mapComplianceData: mapKISAComplianceData,
|
||||
@@ -151,7 +151,7 @@ const getComplianceMappers = (): Record<string, ComplianceMapper> => ({
|
||||
calculateCategoryHeatmapData: (data: Framework[]) =>
|
||||
calculateCategoryHeatmapData(data),
|
||||
getDetailsComponent: (requirement: Requirement) =>
|
||||
React.createElement(KISACustomDetails, { requirement }),
|
||||
createElement(KISACustomDetails, { requirement }),
|
||||
},
|
||||
"MITRE-ATTACK": {
|
||||
mapComplianceData: mapMITREComplianceData,
|
||||
@@ -159,7 +159,7 @@ const getComplianceMappers = (): Record<string, ComplianceMapper> => ({
|
||||
getTopFailedSections: getMITRETopFailedSections,
|
||||
calculateCategoryHeatmapData: calculateMITRECategoryHeatmapData,
|
||||
getDetailsComponent: (requirement: Requirement) =>
|
||||
React.createElement(MITRECustomDetails, { requirement }),
|
||||
createElement(MITRECustomDetails, { requirement }),
|
||||
},
|
||||
ProwlerThreatScore: {
|
||||
mapComplianceData: mapThetaComplianceData,
|
||||
@@ -168,7 +168,7 @@ const getComplianceMappers = (): Record<string, ComplianceMapper> => ({
|
||||
calculateCategoryHeatmapData: (complianceData: Framework[]) =>
|
||||
calculateCategoryHeatmapData(complianceData),
|
||||
getDetailsComponent: (requirement: Requirement) =>
|
||||
React.createElement(ThreatCustomDetails, { requirement }),
|
||||
createElement(ThreatCustomDetails, { requirement }),
|
||||
},
|
||||
CCC: {
|
||||
mapComplianceData: mapCCCComplianceData,
|
||||
@@ -177,7 +177,7 @@ const getComplianceMappers = (): Record<string, ComplianceMapper> => ({
|
||||
calculateCategoryHeatmapData: (data: Framework[]) =>
|
||||
calculateCategoryHeatmapData(data),
|
||||
getDetailsComponent: (requirement: Requirement) =>
|
||||
React.createElement(CCCCustomDetails, { requirement }),
|
||||
createElement(CCCCustomDetails, { requirement }),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
+58
-45
@@ -12,6 +12,8 @@ import {
|
||||
REQUIREMENT_STATUS,
|
||||
RequirementsData,
|
||||
RequirementStatus,
|
||||
TOP_FAILED_DATA_TYPE,
|
||||
TopFailedResult,
|
||||
} from "@/types/compliance";
|
||||
|
||||
import {
|
||||
@@ -20,6 +22,12 @@ import {
|
||||
findOrCreateFramework,
|
||||
} from "./commons";
|
||||
|
||||
// Type for the internal map used in getTopFailedSections
|
||||
interface FailedSectionData {
|
||||
total: number;
|
||||
types: Record<string, number>;
|
||||
}
|
||||
|
||||
export const mapComplianceData = (
|
||||
attributesData: AttributesData,
|
||||
requirementsData: RequirementsData,
|
||||
@@ -92,9 +100,9 @@ export const mapComplianceData = (
|
||||
}) || [],
|
||||
};
|
||||
|
||||
// Add requirement directly to framework (store in a special property)
|
||||
(framework as any).requirements = (framework as any).requirements || [];
|
||||
(framework as any).requirements.push(requirement);
|
||||
// Add requirement directly to framework (flat structure - no categories)
|
||||
framework.requirements = framework.requirements ?? [];
|
||||
framework.requirements.push(requirement);
|
||||
}
|
||||
|
||||
// Calculate counters using common helper (works with flat structure)
|
||||
@@ -108,63 +116,63 @@ export const toAccordionItems = (
|
||||
scanId: string | undefined,
|
||||
): AccordionItemProps[] => {
|
||||
return data.flatMap((framework) => {
|
||||
const requirements = (framework as any).requirements || [];
|
||||
const requirements = framework.requirements ?? [];
|
||||
|
||||
// Filter out requirements without metadata (can't be displayed in accordion)
|
||||
const displayableRequirements = requirements.filter(
|
||||
(requirement: Requirement) => requirement.hasMetadata !== false,
|
||||
(requirement) => requirement.hasMetadata !== false,
|
||||
);
|
||||
|
||||
return displayableRequirements.map(
|
||||
(requirement: Requirement, i: number) => {
|
||||
const itemKey = `${framework.name}-req-${i}`;
|
||||
return displayableRequirements.map((requirement, i) => {
|
||||
const itemKey = `${framework.name}-req-${i}`;
|
||||
|
||||
return {
|
||||
key: itemKey,
|
||||
title: (
|
||||
<ComplianceAccordionRequirementTitle
|
||||
type=""
|
||||
name={requirement.name}
|
||||
status={requirement.status as FindingStatus}
|
||||
/>
|
||||
),
|
||||
content: (
|
||||
<ClientAccordionContent
|
||||
key={`content-${itemKey}`}
|
||||
requirement={requirement}
|
||||
scanId={scanId || ""}
|
||||
framework={framework.name}
|
||||
disableFindings={
|
||||
requirement.check_ids.length === 0 && requirement.manual === 0
|
||||
}
|
||||
/>
|
||||
),
|
||||
items: [],
|
||||
};
|
||||
},
|
||||
);
|
||||
return {
|
||||
key: itemKey,
|
||||
title: (
|
||||
<ComplianceAccordionRequirementTitle
|
||||
type=""
|
||||
name={requirement.name}
|
||||
status={requirement.status as FindingStatus}
|
||||
/>
|
||||
),
|
||||
content: (
|
||||
<ClientAccordionContent
|
||||
key={`content-${itemKey}`}
|
||||
requirement={requirement}
|
||||
scanId={scanId || ""}
|
||||
framework={framework.name}
|
||||
disableFindings={
|
||||
requirement.check_ids.length === 0 && requirement.manual === 0
|
||||
}
|
||||
/>
|
||||
),
|
||||
items: [],
|
||||
};
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Custom function for MITRE to get top failed sections grouped by tactics
|
||||
export const getTopFailedSections = (
|
||||
mappedData: Framework[],
|
||||
): FailedSection[] => {
|
||||
const failedSectionMap = new Map();
|
||||
): TopFailedResult => {
|
||||
const failedSectionMap = new Map<string, FailedSectionData>();
|
||||
|
||||
mappedData.forEach((framework) => {
|
||||
const requirements = (framework as any).requirements || [];
|
||||
const requirements = framework.requirements ?? [];
|
||||
|
||||
requirements.forEach((requirement: Requirement) => {
|
||||
requirements.forEach((requirement) => {
|
||||
if (requirement.status === REQUIREMENT_STATUS.FAIL) {
|
||||
const tactics = (requirement.tactics as string[]) || [];
|
||||
const tactics = Array.isArray(requirement.tactics)
|
||||
? (requirement.tactics as string[])
|
||||
: [];
|
||||
|
||||
tactics.forEach((tactic) => {
|
||||
if (!failedSectionMap.has(tactic)) {
|
||||
failedSectionMap.set(tactic, { total: 0, types: {} });
|
||||
}
|
||||
|
||||
const sectionData = failedSectionMap.get(tactic);
|
||||
const sectionData = failedSectionMap.get(tactic)!;
|
||||
sectionData.total += 1;
|
||||
|
||||
const type = "Fails";
|
||||
@@ -175,10 +183,13 @@ export const getTopFailedSections = (
|
||||
});
|
||||
|
||||
// Convert in descending order and slice top 5
|
||||
return Array.from(failedSectionMap.entries())
|
||||
.map(([name, data]) => ({ name, ...data }))
|
||||
.sort((a, b) => b.total - a.total)
|
||||
.slice(0, 5); // Top 5
|
||||
return {
|
||||
items: Array.from(failedSectionMap.entries())
|
||||
.map(([name, data]): FailedSection => ({ name, ...data }))
|
||||
.sort((a, b) => b.total - a.total)
|
||||
.slice(0, 5),
|
||||
type: TOP_FAILED_DATA_TYPE.SECTIONS,
|
||||
};
|
||||
};
|
||||
|
||||
// Custom function for MITRE to calculate category heatmap data grouped by tactics
|
||||
@@ -197,10 +208,12 @@ export const calculateCategoryHeatmapData = (
|
||||
|
||||
// Aggregate data by tactics
|
||||
complianceData.forEach((framework) => {
|
||||
const requirements = (framework as any).requirements || [];
|
||||
const requirements = framework.requirements ?? [];
|
||||
|
||||
requirements.forEach((requirement: Requirement) => {
|
||||
const tactics = (requirement.tactics as string[]) || [];
|
||||
requirements.forEach((requirement) => {
|
||||
const tactics = Array.isArray(requirement.tactics)
|
||||
? (requirement.tactics as string[])
|
||||
: [];
|
||||
|
||||
tactics.forEach((tactic) => {
|
||||
const existing = tacticMap.get(tactic) || {
|
||||
|
||||
+17
-2
@@ -68,12 +68,27 @@ export interface Framework {
|
||||
fail: number;
|
||||
manual: number;
|
||||
categories: Category[];
|
||||
// Optional: flat structure for frameworks like MITRE that don't have categories
|
||||
requirements?: Requirement[];
|
||||
}
|
||||
|
||||
export interface FailedSection {
|
||||
name: string;
|
||||
total: number;
|
||||
types?: { [key: string]: number };
|
||||
types?: Record<string, number>;
|
||||
}
|
||||
|
||||
export const TOP_FAILED_DATA_TYPE = {
|
||||
SECTIONS: "sections",
|
||||
REQUIREMENTS: "requirements",
|
||||
} as const;
|
||||
|
||||
export type TopFailedDataType =
|
||||
(typeof TOP_FAILED_DATA_TYPE)[keyof typeof TOP_FAILED_DATA_TYPE];
|
||||
|
||||
export interface TopFailedResult {
|
||||
items: FailedSection[];
|
||||
type: TopFailedDataType;
|
||||
}
|
||||
|
||||
export interface RequirementsTotals {
|
||||
@@ -92,7 +107,7 @@ export interface ENSAttributesMetadata {
|
||||
Nivel: string;
|
||||
Dimensiones: string[];
|
||||
ModoEjecucion: string;
|
||||
Dependencias: any[];
|
||||
Dependencias: unknown[];
|
||||
}
|
||||
|
||||
export interface ISO27001AttributesMetadata {
|
||||
|
||||
Reference in New Issue
Block a user