mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-03-22 03:08:23 +00:00
fix(ui): handle missing relationships in FindingDetail to prevent crash (#10314)
This commit is contained in:
@@ -11,6 +11,10 @@ All notable changes to the **Prowler UI** are documented in this file.
|
|||||||
- Providers page redesigned with cloud organization hierarchy, HeroUI-to-shadcn migration, organization and account group filters, and row selection for bulk actions [(#10292)](https://github.com/prowler-cloud/prowler/pull/10292)
|
- Providers page redesigned with cloud organization hierarchy, HeroUI-to-shadcn migration, organization and account group filters, and row selection for bulk actions [(#10292)](https://github.com/prowler-cloud/prowler/pull/10292)
|
||||||
- AWS Organizations onboarding now uses a clearer 3-step flow: deploy the ProwlerScan role in the management account via CloudFormation Stack, deploy to member accounts via StackSet with a copyable template URL, and confirm with the Role ARN [(#10274)](https://github.com/prowler-cloud/prowler/pull/10274)
|
- AWS Organizations onboarding now uses a clearer 3-step flow: deploy the ProwlerScan role in the management account via CloudFormation Stack, deploy to member accounts via StackSet with a copyable template URL, and confirm with the Role ARN [(#10274)](https://github.com/prowler-cloud/prowler/pull/10274)
|
||||||
|
|
||||||
|
### 🐞 Fixed
|
||||||
|
|
||||||
|
- Finding detail drawer crashing when resource, scan, or provider relationships are missing from the API response [(#10314)](https://github.com/prowler-cloud/prowler/pull/10314)
|
||||||
|
|
||||||
### 🔐 Security
|
### 🔐 Security
|
||||||
|
|
||||||
- npm transitive dependencies patched to resolve 11 Dependabot alerts (6 HIGH, 4 MEDIUM, 1 LOW): hono, @hono/node-server, fast-xml-parser, serialize-javascript, minimatch [(#10267)](https://github.com/prowler-cloud/prowler/pull/10267)
|
- npm transitive dependencies patched to resolve 11 Dependabot alerts (6 HIGH, 4 MEDIUM, 1 LOW): hono, @hono/node-server, fast-xml-parser, serialize-javascript, minimatch [(#10267)](https://github.com/prowler-cloud/prowler/pull/10267)
|
||||||
|
|||||||
@@ -33,20 +33,14 @@ const getResourceData = (
|
|||||||
row: { original: FindingProps },
|
row: { original: FindingProps },
|
||||||
field: keyof FindingProps["relationships"]["resource"]["attributes"],
|
field: keyof FindingProps["relationships"]["resource"]["attributes"],
|
||||||
) => {
|
) => {
|
||||||
return (
|
return row.original.relationships?.resource?.attributes?.[field] || "-";
|
||||||
row.original.relationships?.resource?.attributes?.[field] ||
|
|
||||||
`No ${field} found in resource`
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getProviderData = (
|
const getProviderData = (
|
||||||
row: { original: FindingProps },
|
row: { original: FindingProps },
|
||||||
field: keyof FindingProps["relationships"]["provider"]["attributes"],
|
field: keyof FindingProps["relationships"]["provider"]["attributes"],
|
||||||
) => {
|
) => {
|
||||||
return (
|
return row.original.relationships?.provider?.attributes?.[field] || "-";
|
||||||
row.original.relationships?.provider?.attributes?.[field] ||
|
|
||||||
`No ${field} found in provider`
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Component for finding title that opens the detail drawer
|
// Component for finding title that opens the detail drawer
|
||||||
@@ -186,6 +180,10 @@ export function getColumnFindings(
|
|||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const resourceName = getResourceData(row, "name");
|
const resourceName = getResourceData(row, "name");
|
||||||
|
|
||||||
|
if (resourceName === "-") {
|
||||||
|
return <p className="text-text-neutral-primary text-sm">-</p>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CodeSnippet
|
<CodeSnippet
|
||||||
value={resourceName as string}
|
value={resourceName as string}
|
||||||
|
|||||||
@@ -82,9 +82,9 @@ export const FindingDetail = ({
|
|||||||
}: FindingDetailProps) => {
|
}: FindingDetailProps) => {
|
||||||
const finding = findingDetails;
|
const finding = findingDetails;
|
||||||
const attributes = finding.attributes;
|
const attributes = finding.attributes;
|
||||||
const resource = finding.relationships.resource.attributes;
|
const resource = finding.relationships?.resource?.attributes;
|
||||||
const scan = finding.relationships.scan.attributes;
|
const scan = finding.relationships?.scan?.attributes;
|
||||||
const providerDetails = finding.relationships.provider.attributes;
|
const providerDetails = finding.relationships?.provider?.attributes;
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
@@ -97,7 +97,7 @@ export const FindingDetail = ({
|
|||||||
|
|
||||||
// Build Git URL for IaC findings
|
// Build Git URL for IaC findings
|
||||||
const gitUrl =
|
const gitUrl =
|
||||||
providerDetails.provider === "iac"
|
providerDetails?.provider === "iac" && resource
|
||||||
? buildGitFileUrl(
|
? buildGitFileUrl(
|
||||||
providerDetails.uid,
|
providerDetails.uid,
|
||||||
resource.name,
|
resource.name,
|
||||||
@@ -160,23 +160,24 @@ export const FindingDetail = ({
|
|||||||
<TabsTrigger value="scans">Scans</TabsTrigger>
|
<TabsTrigger value="scans">Scans</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<p className="text-text-neutral-primary mb-4 text-sm">
|
|
||||||
Here is an overview of this finding:
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* General Tab */}
|
{/* General Tab */}
|
||||||
<TabsContent value="general" className="flex flex-col gap-4">
|
<TabsContent value="general" className="flex flex-col gap-4">
|
||||||
|
<p className="text-text-neutral-primary text-sm">
|
||||||
|
Here is an overview of this finding:
|
||||||
|
</p>
|
||||||
<div className="flex flex-wrap gap-4">
|
<div className="flex flex-wrap gap-4">
|
||||||
<EntityInfo
|
{providerDetails && (
|
||||||
cloudProvider={providerDetails.provider as ProviderType}
|
<EntityInfo
|
||||||
entityAlias={providerDetails.alias}
|
cloudProvider={providerDetails.provider as ProviderType}
|
||||||
entityId={providerDetails.uid}
|
entityAlias={providerDetails.alias}
|
||||||
showConnectionStatus={providerDetails.connection.connected}
|
entityId={providerDetails.uid}
|
||||||
/>
|
showConnectionStatus={providerDetails.connection.connected}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<InfoField label="Service">
|
<InfoField label="Service">
|
||||||
{attributes.check_metadata.servicename}
|
{attributes.check_metadata.servicename}
|
||||||
</InfoField>
|
</InfoField>
|
||||||
<InfoField label="Region">{resource.region}</InfoField>
|
<InfoField label="Region">{resource?.region ?? "-"}</InfoField>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
@@ -305,119 +306,137 @@ export const FindingDetail = ({
|
|||||||
|
|
||||||
{/* Resources Tab */}
|
{/* Resources Tab */}
|
||||||
<TabsContent value="resources" className="flex flex-col gap-4">
|
<TabsContent value="resources" className="flex flex-col gap-4">
|
||||||
{providerDetails.provider === "iac" && gitUrl && (
|
{resource ? (
|
||||||
<div className="flex justify-end">
|
<>
|
||||||
<Tooltip>
|
{providerDetails?.provider === "iac" && gitUrl && (
|
||||||
<TooltipTrigger asChild>
|
<div className="flex justify-end">
|
||||||
<a
|
<Tooltip>
|
||||||
href={gitUrl}
|
<TooltipTrigger asChild>
|
||||||
target="_blank"
|
<a
|
||||||
rel="noopener noreferrer"
|
href={gitUrl}
|
||||||
className="text-bg-data-info inline-flex items-center gap-1 text-sm"
|
target="_blank"
|
||||||
aria-label="Open resource in repository"
|
rel="noopener noreferrer"
|
||||||
>
|
className="text-bg-data-info inline-flex items-center gap-1 text-sm"
|
||||||
<ExternalLink size={16} />
|
aria-label="Open resource in repository"
|
||||||
View in Repository
|
>
|
||||||
</a>
|
<ExternalLink size={16} />
|
||||||
</TooltipTrigger>
|
View in Repository
|
||||||
<TooltipContent>
|
</a>
|
||||||
Go to Resource in the Repository
|
</TooltipTrigger>
|
||||||
</TooltipContent>
|
<TooltipContent>
|
||||||
</Tooltip>
|
Go to Resource in the Repository
|
||||||
</div>
|
</TooltipContent>
|
||||||
)}
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
||||||
<InfoField label="Resource Name">
|
|
||||||
{renderValue(resource.name)}
|
|
||||||
</InfoField>
|
|
||||||
<InfoField label="Resource Type">
|
|
||||||
{renderValue(resource.type)}
|
|
||||||
</InfoField>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
||||||
<InfoField label="Service">
|
|
||||||
{renderValue(resource.service)}
|
|
||||||
</InfoField>
|
|
||||||
<InfoField label="Region">{renderValue(resource.region)}</InfoField>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
||||||
<InfoField label="Partition">
|
|
||||||
{renderValue(resource.partition)}
|
|
||||||
</InfoField>
|
|
||||||
<InfoField label="Details">
|
|
||||||
{renderValue(resource.details)}
|
|
||||||
</InfoField>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<InfoField label="Resource ID" variant="simple">
|
|
||||||
<CodeSnippet value={resource.uid} />
|
|
||||||
</InfoField>
|
|
||||||
|
|
||||||
{resource.tags && Object.entries(resource.tags).length > 0 && (
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<h4 className="text-text-neutral-secondary text-sm font-bold">
|
|
||||||
Tags
|
|
||||||
</h4>
|
|
||||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
{Object.entries(resource.tags).map(([key, value]) => (
|
<InfoField label="Resource Name">
|
||||||
<InfoField key={key} label={key}>
|
{renderValue(resource.name)}
|
||||||
{renderValue(value)}
|
</InfoField>
|
||||||
</InfoField>
|
<InfoField label="Resource Type">
|
||||||
))}
|
{renderValue(resource.type)}
|
||||||
|
</InfoField>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
<InfoField label="Created At">
|
<InfoField label="Service">
|
||||||
<DateWithTime inline dateTime={resource.inserted_at || "-"} />
|
{renderValue(resource.service)}
|
||||||
</InfoField>
|
</InfoField>
|
||||||
<InfoField label="Last Updated">
|
<InfoField label="Region">
|
||||||
<DateWithTime inline dateTime={resource.updated_at || "-"} />
|
{renderValue(resource.region)}
|
||||||
</InfoField>
|
</InfoField>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
|
<InfoField label="Partition">
|
||||||
|
{renderValue(resource.partition)}
|
||||||
|
</InfoField>
|
||||||
|
<InfoField label="Details">
|
||||||
|
{renderValue(resource.details)}
|
||||||
|
</InfoField>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<InfoField label="Resource ID" variant="simple">
|
||||||
|
<CodeSnippet value={resource.uid} />
|
||||||
|
</InfoField>
|
||||||
|
|
||||||
|
{resource.tags && Object.entries(resource.tags).length > 0 && (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<h4 className="text-text-neutral-secondary text-sm font-bold">
|
||||||
|
Tags
|
||||||
|
</h4>
|
||||||
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
|
{Object.entries(resource.tags).map(([key, value]) => (
|
||||||
|
<InfoField key={key} label={key}>
|
||||||
|
{renderValue(value)}
|
||||||
|
</InfoField>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
|
<InfoField label="Created At">
|
||||||
|
<DateWithTime inline dateTime={resource.inserted_at || "-"} />
|
||||||
|
</InfoField>
|
||||||
|
<InfoField label="Last Updated">
|
||||||
|
<DateWithTime inline dateTime={resource.updated_at || "-"} />
|
||||||
|
</InfoField>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<p className="text-text-neutral-tertiary text-sm">
|
||||||
|
Resource information is not available.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
{/* Scans Tab */}
|
{/* Scans Tab */}
|
||||||
<TabsContent value="scans" className="flex flex-col gap-4">
|
<TabsContent value="scans" className="flex flex-col gap-4">
|
||||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
{scan ? (
|
||||||
<InfoField label="Scan Name">{scan.name || "N/A"}</InfoField>
|
<>
|
||||||
<InfoField label="Resources Scanned">
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||||
{scan.unique_resource_count}
|
<InfoField label="Scan Name">{scan.name || "N/A"}</InfoField>
|
||||||
</InfoField>
|
<InfoField label="Resources Scanned">
|
||||||
<InfoField label="Progress">{scan.progress}%</InfoField>
|
{scan.unique_resource_count}
|
||||||
</div>
|
</InfoField>
|
||||||
|
<InfoField label="Progress">{scan.progress}%</InfoField>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||||
<InfoField label="Trigger">{scan.trigger}</InfoField>
|
<InfoField label="Trigger">{scan.trigger}</InfoField>
|
||||||
<InfoField label="State">{scan.state}</InfoField>
|
<InfoField label="State">{scan.state}</InfoField>
|
||||||
<InfoField label="Duration">
|
<InfoField label="Duration">
|
||||||
{formatDuration(scan.duration)}
|
{formatDuration(scan.duration)}
|
||||||
</InfoField>
|
</InfoField>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
<InfoField label="Started At">
|
<InfoField label="Started At">
|
||||||
<DateWithTime inline dateTime={scan.started_at || "-"} />
|
<DateWithTime inline dateTime={scan.started_at || "-"} />
|
||||||
</InfoField>
|
</InfoField>
|
||||||
<InfoField label="Completed At">
|
<InfoField label="Completed At">
|
||||||
<DateWithTime inline dateTime={scan.completed_at || "-"} />
|
<DateWithTime inline dateTime={scan.completed_at || "-"} />
|
||||||
</InfoField>
|
</InfoField>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
<InfoField label="Launched At">
|
<InfoField label="Launched At">
|
||||||
<DateWithTime inline dateTime={scan.inserted_at || "-"} />
|
<DateWithTime inline dateTime={scan.inserted_at || "-"} />
|
||||||
</InfoField>
|
</InfoField>
|
||||||
{scan.scheduled_at && (
|
{scan.scheduled_at && (
|
||||||
<InfoField label="Scheduled At">
|
<InfoField label="Scheduled At">
|
||||||
<DateWithTime inline dateTime={scan.scheduled_at} />
|
<DateWithTime inline dateTime={scan.scheduled_at} />
|
||||||
</InfoField>
|
</InfoField>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<p className="text-text-neutral-tertiary text-sm">
|
||||||
|
Scan information is not available.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user