Compare commits

...

1 Commits

Author SHA1 Message Date
Andoni A.
271c1bcb2f feat(aws): add link to resource in AWS findings 2025-11-06 07:58:12 +01:00
4 changed files with 131 additions and 34 deletions

View File

@@ -10,6 +10,7 @@ All notable changes to the **Prowler UI** are documented in this file.
- Customer Support menu item [(#9143)](https://github.com/prowler-cloud/prowler/pull/9143)
- IaC (Infrastructure as Code) provider support for scanning remote repositories [(#8751)](https://github.com/prowler-cloud/prowler/pull/8751)
- External resource link to IaC findings for direct navigation to source code in Git repositories [(#9151)](https://github.com/prowler-cloud/prowler/pull/9151)
- External resource link to AWS findings for direct navigation to AWS Console using resource ARN
### 🔄 Changed

View File

@@ -11,6 +11,7 @@ import { CustomLink } from "@/components/ui/custom/custom-link";
import { EntityInfoShort, InfoField } from "@/components/ui/entities";
import { DateWithTime } from "@/components/ui/entities/date-with-time";
import { SeverityBadge } from "@/components/ui/table/severity-badge";
import { buildAwsConsoleUrl } from "@/lib/aws-utils";
import { buildGitFileUrl, extractLineRangeFromUid } from "@/lib/iac-utils";
import { FindingProps, ProviderType } from "@/types";
@@ -59,15 +60,17 @@ export const FindingDetail = ({
params.set("id", findingDetails.id);
const url = `${window.location.origin}${currentUrl.pathname}?${params.toString()}`;
// Build Git URL for IaC findings
const gitUrl =
// Build external resource URL based on provider
const externalUrl =
providerDetails.provider === "iac"
? buildGitFileUrl(
providerDetails.uid,
resource.name,
extractLineRangeFromUid(attributes.uid) || "",
)
: null;
: providerDetails.provider === "aws" && resource.uid
? buildAwsConsoleUrl(resource.uid)
: null;
return (
<div className="flex flex-col gap-6 rounded-lg">
@@ -257,22 +260,31 @@ export const FindingDetail = ({
{/* Resource Details */}
<CustomSection
title={
providerDetails.provider === "iac" ? (
externalUrl ? (
<span className="flex items-center gap-2">
Resource Details
{gitUrl && (
<Tooltip content="Go to Resource in the Repository" size="sm">
<a
href={gitUrl}
target="_blank"
rel="noopener noreferrer"
className="text-bg-data-info inline-flex cursor-pointer"
aria-label="Open resource in repository"
>
<ExternalLink size={16} className="inline" />
</a>
</Tooltip>
)}
<Tooltip
content={
providerDetails.provider === "iac"
? "Go to Resource in the Repository"
: "Go to Resource in AWS Console"
}
size="sm"
>
<a
href={externalUrl}
target="_blank"
rel="noopener noreferrer"
className="text-bg-data-info inline-flex cursor-pointer"
aria-label={
providerDetails.provider === "iac"
? "Open resource in repository"
: "Open resource in AWS Console"
}
>
<ExternalLink size={16} className="inline" />
</a>
</Tooltip>
</span>
) : (
"Resource Details"

View File

@@ -18,6 +18,7 @@ import {
} from "@/components/ui/entities";
import { SeverityBadge, StatusFindingBadge } from "@/components/ui/table";
import { createDict } from "@/lib";
import { buildAwsConsoleUrl } from "@/lib/aws-utils";
import { buildGitFileUrl } from "@/lib/iac-utils";
import { FindingProps, ProviderType, ResourceProps } from "@/types";
@@ -164,11 +165,13 @@ export const ResourceDetail = ({
const providerData = resource.relationships.provider.data.attributes;
const allFindings = findingsData;
// Build Git URL for IaC resources
const gitUrl =
// Build external resource URL based on provider
const externalUrl =
providerData.provider === "iac"
? buildGitFileUrl(providerData.uid, attributes.name, "")
: null;
: providerData.provider === "aws" && attributes.uid
? buildAwsConsoleUrl(attributes.uid)
: null;
if (selectedFindingId) {
const findingTitle =
@@ -196,22 +199,31 @@ export const ResourceDetail = ({
{/* Resource Details section */}
<CustomSection
title={
providerData.provider === "iac" ? (
externalUrl ? (
<span className="flex items-center gap-2">
Resource Details
{gitUrl && (
<Tooltip content="Go to Resource in the Repository" size="sm">
<a
href={gitUrl}
target="_blank"
rel="noopener noreferrer"
className="text-bg-data-info inline-flex cursor-pointer"
aria-label="Open resource in repository"
>
<ExternalLink size={16} className="inline" />
</a>
</Tooltip>
)}
<Tooltip
content={
providerData.provider === "iac"
? "Go to Resource in the Repository"
: "Go to Resource in AWS Console"
}
size="sm"
>
<a
href={externalUrl}
target="_blank"
rel="noopener noreferrer"
className="text-bg-data-info inline-flex cursor-pointer"
aria-label={
providerData.provider === "iac"
? "Open resource in repository"
: "Open resource in AWS Console"
}
>
<ExternalLink size={16} className="inline" />
</a>
</Tooltip>
</span>
) : (
"Resource Details"

72
ui/lib/aws-utils.ts Normal file
View File

@@ -0,0 +1,72 @@
/**
* Builds an AWS Console URL for a given resource ARN
* Uses AWS Console's go/view service which accepts ARNs directly
*
* @param resourceArn - The AWS resource ARN
* @returns Complete URL to view the resource in AWS Console, or null if ARN is invalid
*/
export function buildAwsConsoleUrl(resourceArn: string): string | null {
if (!resourceArn || !resourceArn.startsWith("arn:")) {
return null;
}
try {
// AWS Console provides a universal URL that works with any ARN
// Format: https://console.aws.amazon.com/go/view?arn=<ARN>
const encodedArn = encodeURIComponent(resourceArn);
return `https://console.aws.amazon.com/go/view?arn=${encodedArn}`;
} catch (error) {
console.error("Error building AWS Console URL:", error);
return null;
}
}
/**
* Extracts the AWS region from an ARN
* ARN format: arn:partition:service:region:account-id:resource
*
* @param resourceArn - The AWS resource ARN
* @returns AWS region code or null if not found
*/
export function extractAwsRegionFromArn(resourceArn: string): string | null {
if (!resourceArn || !resourceArn.startsWith("arn:")) {
return null;
}
try {
const parts = resourceArn.split(":");
if (parts.length >= 4) {
const region = parts[3];
return region || null;
}
return null;
} catch (error) {
console.error("Error extracting region from ARN:", error);
return null;
}
}
/**
* Extracts the AWS service from an ARN
* ARN format: arn:partition:service:region:account-id:resource
*
* @param resourceArn - The AWS resource ARN
* @returns AWS service name or null if not found
*/
export function extractAwsServiceFromArn(resourceArn: string): string | null {
if (!resourceArn || !resourceArn.startsWith("arn:")) {
return null;
}
try {
const parts = resourceArn.split(":");
if (parts.length >= 3) {
const service = parts[2];
return service || null;
}
return null;
} catch (error) {
console.error("Error extracting service from ARN:", error);
return null;
}
}