mirror of
https://github.com/prowler-cloud/prowler.git
synced 2025-12-19 05:17:47 +00:00
275 lines
8.1 KiB
TypeScript
275 lines
8.1 KiB
TypeScript
"use client";
|
|
|
|
import { Textarea } from "@heroui/input";
|
|
import {
|
|
Dispatch,
|
|
SetStateAction,
|
|
useActionState,
|
|
useEffect,
|
|
useState,
|
|
} from "react";
|
|
|
|
import {
|
|
createMutedFindingsConfig,
|
|
deleteMutedFindingsConfig,
|
|
getMutedFindingsConfig,
|
|
updateMutedFindingsConfig,
|
|
} from "@/actions/processors";
|
|
import { DeleteIcon } from "@/components/icons";
|
|
import { Button } from "@/components/shadcn";
|
|
import { useToast } from "@/components/ui";
|
|
import { CustomLink } from "@/components/ui/custom/custom-link";
|
|
import { FormButtons } from "@/components/ui/form";
|
|
import { fontMono } from "@/config/fonts";
|
|
import {
|
|
convertToYaml,
|
|
defaultMutedFindingsConfig,
|
|
parseYamlValidation,
|
|
} from "@/lib/yaml";
|
|
import {
|
|
MutedFindingsConfigActionState,
|
|
ProcessorData,
|
|
} from "@/types/processors";
|
|
|
|
interface MutedFindingsConfigFormProps {
|
|
setIsOpen: Dispatch<SetStateAction<boolean>>;
|
|
onCancel?: () => void;
|
|
}
|
|
|
|
export const MutedFindingsConfigForm = ({
|
|
setIsOpen,
|
|
onCancel,
|
|
}: MutedFindingsConfigFormProps) => {
|
|
const [config, setConfig] = useState<ProcessorData | null>(null);
|
|
const [configText, setConfigText] = useState("");
|
|
const [showDeleteConfirmation, setShowDeleteConfirmation] = useState(false);
|
|
const [isDeleting, setIsDeleting] = useState(false);
|
|
const [yamlValidation, setYamlValidation] = useState<{
|
|
isValid: boolean;
|
|
error?: string;
|
|
}>({ isValid: true });
|
|
const [hasUserStartedTyping, setHasUserStartedTyping] = useState(false);
|
|
|
|
const [state, formAction, isPending] = useActionState<
|
|
MutedFindingsConfigActionState,
|
|
FormData
|
|
>(config ? updateMutedFindingsConfig : createMutedFindingsConfig, null);
|
|
|
|
const { toast } = useToast();
|
|
|
|
useEffect(() => {
|
|
getMutedFindingsConfig().then((result) => {
|
|
setConfig(result || null);
|
|
const yamlConfig = convertToYaml(result?.attributes.configuration || "");
|
|
setConfigText(yamlConfig);
|
|
setHasUserStartedTyping(false); // Reset when loading initial config
|
|
if (yamlConfig) {
|
|
setYamlValidation(parseYamlValidation(yamlConfig));
|
|
}
|
|
});
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (state?.success) {
|
|
toast({
|
|
title: "Configuration saved successfully",
|
|
description: state.success,
|
|
});
|
|
setIsOpen(false);
|
|
} else if (state?.errors?.general) {
|
|
toast({
|
|
variant: "destructive",
|
|
title: "Oops! Something went wrong",
|
|
description: state.errors.general,
|
|
});
|
|
} else if (state?.errors?.configuration) {
|
|
// Reset typing state when there are new server errors
|
|
setHasUserStartedTyping(false);
|
|
}
|
|
}, [state, toast, setIsOpen]);
|
|
|
|
const handleConfigChange = (value: string) => {
|
|
setConfigText(value);
|
|
// Clear server errors when user starts typing
|
|
setHasUserStartedTyping(true);
|
|
// Validate YAML in real-time
|
|
const validation = parseYamlValidation(value);
|
|
setYamlValidation(validation);
|
|
};
|
|
|
|
const handleDelete = async () => {
|
|
if (!config) return;
|
|
|
|
setIsDeleting(true);
|
|
const formData = new FormData();
|
|
formData.append("id", config.id);
|
|
|
|
try {
|
|
const result = await deleteMutedFindingsConfig(null, formData);
|
|
if (result?.success) {
|
|
toast({
|
|
title: "Configuration deleted successfully",
|
|
description: result.success,
|
|
});
|
|
setIsOpen(false);
|
|
} else if (result?.errors?.general) {
|
|
toast({
|
|
variant: "destructive",
|
|
title: "Oops! Something went wrong",
|
|
description: result.errors.general,
|
|
});
|
|
}
|
|
} catch (error) {
|
|
toast({
|
|
variant: "destructive",
|
|
title: "Oops! Something went wrong",
|
|
description: "Error deleting configuration. Please try again.",
|
|
});
|
|
} finally {
|
|
setIsDeleting(false);
|
|
setShowDeleteConfirmation(false);
|
|
}
|
|
};
|
|
|
|
if (showDeleteConfirmation) {
|
|
return (
|
|
<div className="flex flex-col gap-4">
|
|
<h3 className="text-default-700 text-lg font-semibold">
|
|
Delete Mutelist Configuration
|
|
</h3>
|
|
<p className="text-default-600 text-sm">
|
|
Are you sure you want to delete this configuration? This action cannot
|
|
be undone.
|
|
</p>
|
|
<div className="flex w-full justify-center gap-6">
|
|
<Button
|
|
type="button"
|
|
aria-label="Cancel"
|
|
className="w-full bg-transparent"
|
|
variant="outline"
|
|
size="lg"
|
|
onClick={() => setShowDeleteConfirmation(false)}
|
|
disabled={isDeleting}
|
|
>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
aria-label="Delete"
|
|
className="w-full"
|
|
variant="destructive"
|
|
size="lg"
|
|
disabled={isDeleting}
|
|
onClick={handleDelete}
|
|
>
|
|
{isDeleting ? (
|
|
"Deleting"
|
|
) : (
|
|
<>
|
|
<DeleteIcon size={24} />
|
|
Delete
|
|
</>
|
|
)}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<form action={formAction} className="flex flex-col gap-4">
|
|
{config && <input type="hidden" name="id" value={config.id} />}
|
|
|
|
<div className="flex flex-col gap-4">
|
|
<div>
|
|
<ul className="text-default-600 mb-4 list-disc pl-5 text-sm">
|
|
<li>
|
|
<strong>
|
|
This Mutelist configuration will take effect on the next scan.
|
|
</strong>
|
|
</li>
|
|
<li>
|
|
Mutelist configuration can be modified at anytime on the Providers
|
|
and Scans pages.
|
|
</li>
|
|
<li>
|
|
Learn more about configuring the Mutelist{" "}
|
|
<CustomLink href="https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/prowler-app-mute-findings">
|
|
here
|
|
</CustomLink>
|
|
.
|
|
</li>
|
|
<li>
|
|
A default Mutelist is used, to exclude certain predefined
|
|
resources, if no Mutelist is provided.
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-2">
|
|
<label
|
|
htmlFor="configuration"
|
|
className="text-default-700 text-sm font-medium"
|
|
>
|
|
Mutelist Configuration
|
|
</label>
|
|
<div>
|
|
<Textarea
|
|
id="configuration"
|
|
name="configuration"
|
|
placeholder={defaultMutedFindingsConfig}
|
|
variant="bordered"
|
|
value={configText}
|
|
onChange={(e) => handleConfigChange(e.target.value)}
|
|
minRows={20}
|
|
maxRows={20}
|
|
isInvalid={
|
|
(!hasUserStartedTyping && !!state?.errors?.configuration) ||
|
|
!yamlValidation.isValid
|
|
}
|
|
errorMessage={
|
|
(!hasUserStartedTyping && state?.errors?.configuration) ||
|
|
(!yamlValidation.isValid ? yamlValidation.error : "")
|
|
}
|
|
classNames={{
|
|
input: fontMono.className + " text-sm",
|
|
base: "min-h-[400px]",
|
|
errorMessage: "whitespace-pre-wrap",
|
|
}}
|
|
/>
|
|
{yamlValidation.isValid && configText && hasUserStartedTyping && (
|
|
<div className="text-tiny text-success my-1 flex items-center px-1">
|
|
<span>Valid YAML format</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-4">
|
|
<FormButtons
|
|
setIsOpen={setIsOpen}
|
|
onCancel={onCancel}
|
|
submitText={config ? "Update" : "Save"}
|
|
isDisabled={!yamlValidation.isValid || !configText.trim()}
|
|
/>
|
|
|
|
{config && (
|
|
<Button
|
|
type="button"
|
|
aria-label="Delete Configuration"
|
|
className="w-full"
|
|
variant="outline"
|
|
size="default"
|
|
onClick={() => setShowDeleteConfirmation(true)}
|
|
disabled={isPending}
|
|
>
|
|
<DeleteIcon size={20} />
|
|
Delete Configuration
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</form>
|
|
);
|
|
};
|