mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-04 19:21:51 +00:00
261 lines
7.3 KiB
TypeScript
261 lines
7.3 KiB
TypeScript
"use client";
|
|
|
|
import { BellPlusIcon } from "lucide-react";
|
|
import Link from "next/link";
|
|
import { useRouter } from "next/navigation";
|
|
import { useState } from "react";
|
|
|
|
import { createAlert, seedAlertRule } from "@/app/(prowler)/alerts/_actions";
|
|
import { AlertFormModal } from "@/app/(prowler)/alerts/_components/alert-form-modal";
|
|
import {
|
|
getFindingsFiltersFromAlertCondition,
|
|
toAlertPayload,
|
|
} from "@/app/(prowler)/alerts/_lib/alert-adapter";
|
|
import {
|
|
ALERT_SEVERITY_VALUES,
|
|
ALERT_TRIGGER_KINDS,
|
|
type AlertCondition,
|
|
type AlertsFilterBag,
|
|
} from "@/app/(prowler)/alerts/_types";
|
|
import type {
|
|
AlertFormSubmitResult,
|
|
AlertFormValues,
|
|
} from "@/app/(prowler)/alerts/_types/alert-form";
|
|
import { buildFindingsFilterChips } from "@/components/findings/findings-filters.utils";
|
|
import {
|
|
Button,
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipTrigger,
|
|
} from "@/components/shadcn";
|
|
import { CloudFeatureBadgeLink } from "@/components/shared/cloud-feature-badge";
|
|
import { ToastAction, useToast } from "@/components/ui";
|
|
import type { ScanEntity } from "@/types";
|
|
import type { ProviderProps } from "@/types/providers";
|
|
|
|
const DISABLED_FILTER_TOOLTIP =
|
|
"Apply at least one Findings filter to create an alert from filters.";
|
|
const ALERT_SEED_ERROR = "Apply at least one alert-compatible Findings filter.";
|
|
|
|
const NON_FILTER_QUERY_KEYS = new Set(["sort", "page", "pageSize"]);
|
|
const ALERT_COMPATIBLE_FILTER_KEYS = new Set([
|
|
"filter[provider_type__in]",
|
|
"filter[provider_id__in]",
|
|
"filter[severity__in]",
|
|
"filter[delta]",
|
|
"filter[region__in]",
|
|
"filter[service__in]",
|
|
"filter[resource_type__in]",
|
|
"filter[category__in]",
|
|
"filter[resource_groups__in]",
|
|
"filter[check_id__in]",
|
|
"filter[finding_group_id]",
|
|
"filter[resource_uid__in]",
|
|
]);
|
|
|
|
interface SeedFromFindingsButtonProps {
|
|
filterBag: AlertsFilterBag;
|
|
providers?: ProviderProps[];
|
|
scans?: Array<{ [scanId: string]: ScanEntity }>;
|
|
uniqueRegions?: string[];
|
|
uniqueServices?: string[];
|
|
uniqueResourceTypes?: string[];
|
|
uniqueCategories?: string[];
|
|
uniqueGroups?: string[];
|
|
className?: string;
|
|
size?: "sm" | "default" | "lg";
|
|
defaultName?: string;
|
|
isCloudEnabled?: boolean;
|
|
}
|
|
|
|
const toChipFilterMap = (
|
|
filterBag: AlertsFilterBag,
|
|
): Record<string, string[]> =>
|
|
Object.fromEntries(
|
|
Object.entries(filterBag)
|
|
.filter(([key]) => key.startsWith("filter["))
|
|
.map(([key, value]) => [
|
|
key,
|
|
(Array.isArray(value) ? value : value.split(","))
|
|
.map((entry) => entry.trim())
|
|
.filter(Boolean),
|
|
])
|
|
.filter(([, values]) => values.length > 0),
|
|
);
|
|
|
|
const hasFindingFilterValue = (filterBag: AlertsFilterBag): boolean =>
|
|
Object.entries(filterBag).some(([rawKey, value]) => {
|
|
if (!rawKey.startsWith("filter[") || NON_FILTER_QUERY_KEYS.has(rawKey)) {
|
|
return false;
|
|
}
|
|
|
|
const values = Array.isArray(value) ? value : [value];
|
|
return values.some((entry) =>
|
|
entry
|
|
.split(",")
|
|
.map((part) => part.trim())
|
|
.some(Boolean),
|
|
);
|
|
});
|
|
|
|
const hasAlertCompatibleFilterValue = (filterBag: AlertsFilterBag): boolean =>
|
|
Object.entries(filterBag).some(([rawKey, value]) => {
|
|
if (!ALERT_COMPATIBLE_FILTER_KEYS.has(rawKey)) return false;
|
|
|
|
const values = Array.isArray(value) ? value : [value];
|
|
return values.some((entry) =>
|
|
entry
|
|
.split(",")
|
|
.map((part) => part.trim())
|
|
.some(Boolean),
|
|
);
|
|
});
|
|
|
|
const withDefaultAlertSeedFilters = (
|
|
filterBag: AlertsFilterBag,
|
|
): AlertsFilterBag => {
|
|
if (hasAlertCompatibleFilterValue(filterBag)) return filterBag;
|
|
|
|
return {
|
|
...filterBag,
|
|
"filter[severity__in]": [...ALERT_SEVERITY_VALUES],
|
|
};
|
|
};
|
|
|
|
export const SeedFromFindingsButton = ({
|
|
filterBag,
|
|
providers = [],
|
|
scans = [],
|
|
uniqueRegions = [],
|
|
uniqueServices = [],
|
|
uniqueResourceTypes = [],
|
|
uniqueCategories = [],
|
|
uniqueGroups = [],
|
|
className,
|
|
size = "lg",
|
|
defaultName = "Findings filter alert",
|
|
isCloudEnabled = true,
|
|
}: SeedFromFindingsButtonProps) => {
|
|
const router = useRouter();
|
|
const { toast } = useToast();
|
|
const [modalOpen, setModalOpen] = useState(false);
|
|
const [seeding, setSeeding] = useState(false);
|
|
const [seededCondition, setSeededCondition] = useState<AlertCondition | null>(
|
|
null,
|
|
);
|
|
const [selectedFindingsFilterChips, setSelectedFindingsFilterChips] =
|
|
useState(() =>
|
|
buildFindingsFilterChips(toChipFilterMap(filterBag), {
|
|
providers,
|
|
scans,
|
|
}),
|
|
);
|
|
|
|
const canSeedFromFilters = hasFindingFilterValue(filterBag);
|
|
|
|
const handleClick = async () => {
|
|
if (!isCloudEnabled || !canSeedFromFilters) return;
|
|
setSeeding(true);
|
|
const result = await seedAlertRule(withDefaultAlertSeedFilters(filterBag));
|
|
setSeeding(false);
|
|
if (result?.error) {
|
|
toast({
|
|
variant: "destructive",
|
|
title: "Alert seed failed",
|
|
description: ALERT_SEED_ERROR,
|
|
});
|
|
return;
|
|
}
|
|
|
|
const condition = result.data.attributes.condition as AlertCondition;
|
|
setSeededCondition(condition);
|
|
setSelectedFindingsFilterChips(
|
|
buildFindingsFilterChips(
|
|
getFindingsFiltersFromAlertCondition(condition),
|
|
{ providers, scans },
|
|
),
|
|
);
|
|
setModalOpen(true);
|
|
};
|
|
|
|
const submitAlert = async (
|
|
values: AlertFormValues,
|
|
): Promise<AlertFormSubmitResult> => {
|
|
const result = await createAlert(toAlertPayload(values));
|
|
if (result?.error) return { ok: false, error: result.error };
|
|
toast({
|
|
title: "Alert created",
|
|
description: result.data.attributes.name,
|
|
action: (
|
|
<ToastAction altText="View alerts" asChild>
|
|
<Link href="/alerts">View Alerts</Link>
|
|
</ToastAction>
|
|
),
|
|
});
|
|
router.refresh();
|
|
return { ok: true, alertId: result.data.id };
|
|
};
|
|
|
|
const button = (
|
|
<Button
|
|
size={size}
|
|
variant="default"
|
|
onClick={handleClick}
|
|
disabled={!isCloudEnabled || !canSeedFromFilters || seeding}
|
|
className={className}
|
|
>
|
|
<BellPlusIcon size={14} />
|
|
{seeding ? "Preparing Alert" : "Create Alert"}
|
|
</Button>
|
|
);
|
|
|
|
if (isCloudEnabled && canSeedFromFilters) {
|
|
return (
|
|
<>
|
|
{button}
|
|
{seededCondition && (
|
|
<AlertFormModal
|
|
open={modalOpen}
|
|
defaultFrequency={ALERT_TRIGGER_KINDS.AFTER_SCAN}
|
|
providers={providers}
|
|
uniqueRegions={uniqueRegions}
|
|
uniqueServices={uniqueServices}
|
|
uniqueResourceTypes={uniqueResourceTypes}
|
|
uniqueCategories={uniqueCategories}
|
|
uniqueGroups={uniqueGroups}
|
|
seededCondition={seededCondition}
|
|
selectedFindingsFilterChips={selectedFindingsFilterChips}
|
|
defaultName={defaultName}
|
|
onOpenChange={setModalOpen}
|
|
onSubmit={submitAlert}
|
|
/>
|
|
)}
|
|
</>
|
|
);
|
|
}
|
|
|
|
if (!isCloudEnabled) {
|
|
return (
|
|
<span className="relative inline-flex" tabIndex={0}>
|
|
{button}
|
|
<span className="absolute top-0 right-0 z-10 translate-x-1/3 -translate-y-1/2">
|
|
<CloudFeatureBadgeLink />
|
|
</span>
|
|
</span>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<span className="relative inline-flex" tabIndex={0}>
|
|
{button}
|
|
</span>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="top" className="max-w-xs">
|
|
{DISABLED_FILTER_TOOLTIP}
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
);
|
|
};
|