mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-03 17:54:05 +00:00
266 lines
7.7 KiB
TypeScript
266 lines
7.7 KiB
TypeScript
"use client";
|
|
|
|
import {
|
|
CalendarClock,
|
|
Download,
|
|
Eye,
|
|
Pencil,
|
|
ShieldCheck,
|
|
TriangleAlert,
|
|
} from "lucide-react";
|
|
import { useRouter } from "next/navigation";
|
|
import { useState } from "react";
|
|
|
|
import { getSchedule } from "@/actions/schedules";
|
|
import { getTask } from "@/actions/task";
|
|
import { getScanErrorDetails } from "@/actions/task/task.adapter";
|
|
import { EditAliasModal } from "@/components/scans/edit-alias-modal";
|
|
import {
|
|
ScanErrorDetailsModal,
|
|
type ScanErrorDetailsState,
|
|
} from "@/components/scans/scan-error-details-modal";
|
|
import {
|
|
EDIT_SCAN_SCHEDULE_STATE,
|
|
EditScanScheduleModal,
|
|
type EditScanScheduleState,
|
|
} from "@/components/scans/schedule/edit-scan-schedule-modal";
|
|
import { useToast } from "@/components/shadcn";
|
|
import {
|
|
ActionDropdown,
|
|
ActionDropdownItem,
|
|
} from "@/components/shadcn/dropdown";
|
|
import { useReportDownload } from "@/hooks/use-report-download";
|
|
import { buildPerScanComplianceHref } from "@/lib/compliance/compliance-tab-url";
|
|
import { downloadScanZip } from "@/lib/helper";
|
|
import { getScanScheduleCapability } from "@/lib/schedules";
|
|
import { isCloud } from "@/lib/shared/env";
|
|
import {
|
|
type ProviderType,
|
|
SCAN_JOBS_TAB,
|
|
type ScanJobsTab,
|
|
type ScanProps,
|
|
type ScheduleApiResponse,
|
|
} from "@/types";
|
|
import {
|
|
SCAN_SCHEDULE_CAPABILITY,
|
|
type ScanScheduleCapability,
|
|
type ScanScheduleProvider,
|
|
} from "@/types/schedules";
|
|
|
|
interface ScanJobsRowActionsProps {
|
|
scan: ScanProps;
|
|
/** The scan jobs tab this row is rendered in. */
|
|
tab: ScanJobsTab;
|
|
/**
|
|
* Schedule capability override. Only for Prowler Cloud.
|
|
*/
|
|
capability?: ScanScheduleCapability;
|
|
/** Prowler Cloud tenants without a paid plan cannot download reports. */
|
|
subscriptionOnly?: boolean;
|
|
}
|
|
|
|
export function ScanJobsRowActions({
|
|
scan,
|
|
tab,
|
|
capability,
|
|
subscriptionOnly = false,
|
|
}: ScanJobsRowActionsProps) {
|
|
const router = useRouter();
|
|
const runReportDownload = useReportDownload(subscriptionOnly);
|
|
const canEditSchedule =
|
|
(capability ?? getScanScheduleCapability(isCloud())) ===
|
|
SCAN_SCHEDULE_CAPABILITY.ADVANCED;
|
|
const { toast } = useToast();
|
|
const [editOpen, setEditOpen] = useState(false);
|
|
const [scheduleOpen, setScheduleOpen] = useState(false);
|
|
const [scheduleState, setScheduleState] = useState<EditScanScheduleState>({
|
|
kind: EDIT_SCAN_SCHEDULE_STATE.LOADING,
|
|
});
|
|
const [errorOpen, setErrorOpen] = useState(false);
|
|
const [errorState, setErrorState] = useState<ScanErrorDetailsState>({
|
|
kind: "idle",
|
|
});
|
|
const scanState = scan.attributes.state;
|
|
const isCompleted = scanState === "completed";
|
|
const isFailed = scanState === "failed";
|
|
// Prowler Cloud partial scans re-check a few resources: they compute no
|
|
// compliance and write no report files, so neither entry applies.
|
|
const isPartial = scan.attributes.is_partial === true;
|
|
const taskId = scan.relationships.task.data?.id;
|
|
// The findings page bounds the UTC day range with completed_at; without it the
|
|
// range collapses to the start day and can miss later findings.
|
|
const hasCompletedAt = Boolean(scan.attributes.completed_at);
|
|
const providerId = scan.relationships.provider.data?.id;
|
|
const scheduleProvider: ScanScheduleProvider | undefined = providerId
|
|
? {
|
|
providerId,
|
|
providerType: (scan.providerInfo?.provider ?? "aws") as ProviderType,
|
|
providerUid: scan.providerInfo?.uid ?? providerId,
|
|
providerAlias: scan.providerInfo?.alias ?? null,
|
|
}
|
|
: undefined;
|
|
|
|
const openFindings = () => {
|
|
if (!isCompleted || !hasCompletedAt) return;
|
|
router.push(
|
|
`/findings?filter[scan__in]=${scan.id}&filter[status__in]=FAIL`,
|
|
);
|
|
};
|
|
|
|
const openCompliance = () => {
|
|
if (!isCompleted) return;
|
|
router.push(buildPerScanComplianceHref({ scanId: scan.id }));
|
|
};
|
|
|
|
const openErrorDetails = async () => {
|
|
setErrorOpen(true);
|
|
setErrorState({ kind: "loading" });
|
|
|
|
if (!taskId) {
|
|
setErrorState({
|
|
kind: "error",
|
|
message: "Task ID is not available for this scan.",
|
|
});
|
|
return;
|
|
}
|
|
|
|
const response: unknown = await getTask(taskId);
|
|
|
|
if (
|
|
typeof response === "object" &&
|
|
response !== null &&
|
|
"error" in response &&
|
|
typeof (response as { error: unknown }).error === "string"
|
|
) {
|
|
setErrorState({
|
|
kind: "error",
|
|
message: (response as { error: string }).error,
|
|
});
|
|
return;
|
|
}
|
|
|
|
const details = getScanErrorDetails(response);
|
|
|
|
if (!details) {
|
|
setErrorState({
|
|
kind: "error",
|
|
message: "No error details were found for this failed scan.",
|
|
});
|
|
return;
|
|
}
|
|
|
|
setErrorState({ kind: "loaded", details });
|
|
};
|
|
|
|
const openScheduleEditor = async () => {
|
|
if (!providerId) {
|
|
setScheduleState({
|
|
kind: EDIT_SCAN_SCHEDULE_STATE.ERROR,
|
|
message: "Provider ID is not available for this scan.",
|
|
});
|
|
setScheduleOpen(true);
|
|
return;
|
|
}
|
|
|
|
setScheduleState({ kind: EDIT_SCAN_SCHEDULE_STATE.LOADING });
|
|
setScheduleOpen(true);
|
|
|
|
const response = (await getSchedule(providerId)) as
|
|
| ScheduleApiResponse
|
|
| { error?: string };
|
|
|
|
if (!response || ("error" in response && response.error)) {
|
|
setScheduleState({
|
|
kind: EDIT_SCAN_SCHEDULE_STATE.ERROR,
|
|
message:
|
|
response && "error" in response && response.error
|
|
? response.error
|
|
: "Failed to load scan schedule.",
|
|
});
|
|
return;
|
|
}
|
|
|
|
setScheduleState({
|
|
kind: EDIT_SCAN_SCHEDULE_STATE.LOADED,
|
|
schedule: "data" in response ? response.data : null,
|
|
});
|
|
};
|
|
|
|
return (
|
|
<div className="flex items-center justify-end">
|
|
<ActionDropdown>
|
|
{/* Schedule editing belongs only to the Scheduled tab. */}
|
|
{tab === SCAN_JOBS_TAB.SCHEDULED && canEditSchedule && (
|
|
<ActionDropdownItem
|
|
icon={<CalendarClock />}
|
|
label="Edit Scan Schedule"
|
|
onSelect={() => void openScheduleEditor()}
|
|
/>
|
|
)}
|
|
{/* Synthetic pending rows have no Scan to alias. */}
|
|
{!scan.pendingSchedule && (
|
|
<ActionDropdownItem
|
|
icon={<Pencil />}
|
|
label="Edit Scan Alias"
|
|
onSelect={() => setEditOpen(true)}
|
|
/>
|
|
)}
|
|
{isCompleted && (
|
|
<>
|
|
<ActionDropdownItem
|
|
icon={<Eye />}
|
|
label="View Findings"
|
|
onSelect={openFindings}
|
|
disabled={!isCompleted || !hasCompletedAt}
|
|
/>
|
|
{!isPartial && (
|
|
<ActionDropdownItem
|
|
icon={<ShieldCheck />}
|
|
label="View Compliance"
|
|
onSelect={openCompliance}
|
|
/>
|
|
)}
|
|
{!isPartial && (
|
|
<ActionDropdownItem
|
|
icon={<Download />}
|
|
label="Download Scan Reports"
|
|
onSelect={() =>
|
|
runReportDownload(() => downloadScanZip(scan.id, toast))
|
|
}
|
|
/>
|
|
)}
|
|
</>
|
|
)}
|
|
{isFailed && (
|
|
<ActionDropdownItem
|
|
icon={<TriangleAlert />}
|
|
label="View error details"
|
|
onSelect={() => void openErrorDetails()}
|
|
/>
|
|
)}
|
|
{/* TODO: Restore Cancel Scan once the backend exposes a public scan cancellation endpoint. */}
|
|
</ActionDropdown>
|
|
|
|
<EditAliasModal
|
|
open={editOpen}
|
|
onOpenChange={setEditOpen}
|
|
scanId={scan.id}
|
|
currentAlias={scan.attributes.name ?? ""}
|
|
/>
|
|
|
|
<ScanErrorDetailsModal
|
|
open={errorOpen}
|
|
onOpenChange={setErrorOpen}
|
|
state={errorState}
|
|
/>
|
|
|
|
<EditScanScheduleModal
|
|
open={scheduleOpen}
|
|
onOpenChange={setScheduleOpen}
|
|
provider={scheduleProvider}
|
|
state={scheduleState}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|