fix(ui): handle missing relationships in FindingDetail to prevent crash (#10314)

This commit is contained in:
Alejandro Bailo
2026-03-12 11:38:03 +01:00
committed by GitHub
parent 4dc3765670
commit 5346222be2
3 changed files with 146 additions and 125 deletions

View File

@@ -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)

View File

@@ -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}

View File

@@ -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>