mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-04 19:21:51 +00:00
5b9824c379
Co-authored-by: Pablo F.G <pablo.fernandez@prowler.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
220 lines
6.8 KiB
TypeScript
220 lines
6.8 KiB
TypeScript
import { ProviderProps, ProvidersApiResponse, ScanProps } from "@/types";
|
|
import { ProviderGroup } from "@/types/components";
|
|
import { FilterEntity } from "@/types/filters";
|
|
import {
|
|
getProviderDisplayName,
|
|
GroupFilterEntity,
|
|
ProviderConnectionStatus,
|
|
} from "@/types/providers";
|
|
import { ScanEntity } from "@/types/scans";
|
|
|
|
/**
|
|
* Extracts normalized filters and search query from the URL search params.
|
|
* Used Server Side Rendering (SSR). There is a hook (useUrlFilters) for client side.
|
|
*/
|
|
export const extractFiltersAndQuery = (
|
|
searchParams: Record<string, unknown>,
|
|
) => {
|
|
const filters: Record<string, string> = {
|
|
...Object.fromEntries(
|
|
Object.entries(searchParams)
|
|
.filter(([key]) => key.startsWith("filter["))
|
|
.map(([key, value]) => [
|
|
key,
|
|
Array.isArray(value) ? value.join(",") : value?.toString() || "",
|
|
]),
|
|
),
|
|
};
|
|
|
|
const query = filters["filter[search]"] || "";
|
|
return { filters, query };
|
|
};
|
|
|
|
/**
|
|
* Returns true if there are any scan or inserted_at filters in the search params.
|
|
* Used to determine whether to call the full findings endpoint.
|
|
*/
|
|
export const hasDateOrScanFilter = (searchParams: Record<string, unknown>) =>
|
|
Object.keys(searchParams).some(
|
|
(key) =>
|
|
key.includes("inserted_at") ||
|
|
key.includes("scan__in") ||
|
|
key === "filter[scan]",
|
|
);
|
|
|
|
/**
|
|
* Returns true when finding views must use historical endpoints.
|
|
* Scan filters are resolved to inserted_at server-side, but client drill-downs
|
|
* still need to treat raw scan params as historical to stay aligned.
|
|
*/
|
|
export const hasHistoricalFindingFilter = (
|
|
searchParams: Record<string, unknown>,
|
|
) => hasDateOrScanFilter(searchParams);
|
|
|
|
/**
|
|
* Returns true when inserted_at filters are active.
|
|
* Used by resources drill-down endpoints that support date scoping but not scan filters.
|
|
*/
|
|
export const hasDateFilter = (searchParams: Record<string, unknown>) =>
|
|
Object.keys(searchParams).some((key) => key.includes("inserted_at"));
|
|
|
|
/**
|
|
* Encodes sort strings by removing leading "+" symbols.
|
|
*/
|
|
export const encodeSort = (sort?: string) => sort?.replace(/^\+/, "") || "";
|
|
|
|
/**
|
|
* Extracts the sort string and the stable key to use in Suspense boundaries.
|
|
*/
|
|
export const extractSortAndKey = (searchParams: Record<string, unknown>) => {
|
|
const searchParamsKey = JSON.stringify(searchParams || {});
|
|
const rawSort = searchParams.sort?.toString();
|
|
const encodedSort = encodeSort(rawSort);
|
|
|
|
return { searchParamsKey, rawSort, encodedSort };
|
|
};
|
|
|
|
/**
|
|
* Replaces a specific field name inside a filter-style key of an object.
|
|
* @param obj - The input object with filter-style keys (e.g., { 'filter[inserted_at]': '2025-05-21' }).
|
|
* @param oldField - The field name to be replaced (e.g., 'inserted_at').
|
|
* @param newField - The field name to replace with (e.g., 'updated_at').
|
|
* @returns A new object with the updated filter key if a match is found.
|
|
*/
|
|
export function replaceFieldKey(
|
|
obj: Record<string, string>,
|
|
oldField: string,
|
|
newField: string,
|
|
): Record<string, string> {
|
|
const fieldObj: Record<string, string> = {};
|
|
|
|
for (const key in obj) {
|
|
const match = key.match(/^filter\[(.+)\]$/);
|
|
if (match && match[1] === oldField) {
|
|
const newKey = `filter[${newField}]`;
|
|
fieldObj[newKey] = obj[key];
|
|
} else {
|
|
fieldObj[key] = obj[key];
|
|
}
|
|
}
|
|
|
|
return fieldObj;
|
|
}
|
|
export const isScanEntity = (entity: ScanEntity) => {
|
|
return entity && entity.providerInfo && entity.attributes;
|
|
};
|
|
|
|
/**
|
|
* Canonical human label for a scan entity: "{Provider name} - {scan name}".
|
|
* Provider name comes from `getProviderDisplayName` (e.g. "AWS", "Google Cloud"),
|
|
* never the account alias/uid — those identify the account, not the provider.
|
|
* Shared by the findings filter chips and the multi-select trigger badge so
|
|
* both surfaces stay in sync. Returns the provider name alone when the scan
|
|
* name is empty, or the scan name alone if the provider type doesn't resolve.
|
|
*/
|
|
export function getScanEntityLabel(scan: ScanEntity): string {
|
|
const providerLabel = getProviderDisplayName(scan.providerInfo.provider);
|
|
const scanName = scan.attributes.name || "";
|
|
|
|
if (providerLabel && scanName) return `${providerLabel} - ${scanName}`;
|
|
return providerLabel || scanName;
|
|
}
|
|
|
|
/**
|
|
* Resolves the display name for a provider group filter value, falling back to
|
|
* the raw id when the group can't be resolved. Shared by the findings and
|
|
* resources filter utils so their chips stay in sync.
|
|
*/
|
|
export function getProviderGroupDisplayValue(
|
|
groupId: string,
|
|
groups: ProviderGroup[],
|
|
): string {
|
|
const group = groups.find((item) => item.id === groupId);
|
|
return group?.attributes.name || groupId;
|
|
}
|
|
|
|
/**
|
|
* Creates a scan details mapping for filters from completed scans.
|
|
* Used to provide detailed information for scan filters in the UI.
|
|
*/
|
|
export const createScanDetailsMapping = (
|
|
completedScans: ScanProps[],
|
|
providersData?: ProvidersApiResponse,
|
|
) => {
|
|
if (!completedScans || completedScans.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
const scanMappings = completedScans.map((scan: ScanProps) => {
|
|
// Get provider info from providerInfo if available, or find from providers data
|
|
let providerInfo = scan.providerInfo;
|
|
|
|
if (!providerInfo && scan.relationships?.provider?.data?.id) {
|
|
const provider = providersData?.data?.find(
|
|
(p: ProviderProps) => p.id === scan.relationships.provider.data.id,
|
|
);
|
|
if (provider) {
|
|
providerInfo = {
|
|
provider: provider.attributes.provider,
|
|
alias: provider.attributes.alias,
|
|
uid: provider.attributes.uid,
|
|
};
|
|
}
|
|
}
|
|
|
|
return {
|
|
[scan.id]: {
|
|
id: scan.id,
|
|
providerInfo: {
|
|
provider: providerInfo?.provider || "aws",
|
|
alias: providerInfo?.alias,
|
|
uid: providerInfo?.uid,
|
|
},
|
|
attributes: {
|
|
name: scan.attributes.name,
|
|
completed_at: scan.attributes.completed_at,
|
|
},
|
|
},
|
|
};
|
|
});
|
|
|
|
return scanMappings;
|
|
};
|
|
|
|
// Helper to check if entity is a ProviderConnectionStatus (simple label/value object)
|
|
export const isConnectionStatus = (
|
|
entity: FilterEntity,
|
|
): entity is ProviderConnectionStatus => {
|
|
return !!(entity && "label" in entity && "value" in entity);
|
|
};
|
|
|
|
// Helper to check if entity is a GroupFilterEntity (organization or account group)
|
|
export const isGroupFilterEntity = (
|
|
entity: FilterEntity,
|
|
): entity is GroupFilterEntity => {
|
|
return !!(
|
|
entity &&
|
|
"name" in entity &&
|
|
!("provider" in entity) &&
|
|
!("label" in entity)
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Connection status mapping for provider filters.
|
|
* Maps boolean string values to user-friendly labels.
|
|
*/
|
|
export const CONNECTION_STATUS_MAPPING: Array<{
|
|
[key: string]: FilterEntity;
|
|
}> = [
|
|
{
|
|
true: { label: "Connected", value: "true" } as ProviderConnectionStatus,
|
|
},
|
|
{
|
|
false: {
|
|
label: "Disconnected",
|
|
value: "false",
|
|
} as ProviderConnectionStatus,
|
|
},
|
|
];
|