mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-03-22 03:08:23 +00:00
feat(ui): add finding-groups server actions and adapter
- Create server actions for finding-groups and resources endpoints - Add adapter to transform API responses to table row shapes - Map filter[search] to filter[check_id__icontains] for search support Ref: PROWLER-881
This commit is contained in:
128
ui/actions/finding-groups/finding-groups.adapter.ts
Normal file
128
ui/actions/finding-groups/finding-groups.adapter.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import {
|
||||
FindingGroupRow,
|
||||
FindingResourceRow,
|
||||
FINDINGS_ROW_TYPE,
|
||||
FindingStatus,
|
||||
ProviderType,
|
||||
Severity,
|
||||
} from "@/types";
|
||||
|
||||
/**
|
||||
* API response shape for a finding group (JSON:API).
|
||||
* Each group represents a unique check_id with aggregated counts.
|
||||
*
|
||||
* Fields come from FindingGroupSerializer which aggregates
|
||||
* FindingGroupDailySummary rows by check_id.
|
||||
*/
|
||||
interface FindingGroupApiItem {
|
||||
type: "finding-groups";
|
||||
id: string;
|
||||
attributes: {
|
||||
check_id: string;
|
||||
check_title: string | null;
|
||||
check_description: string | null;
|
||||
severity: string;
|
||||
status: string; // "FAIL" | "PASS" | "MUTED" (already uppercase)
|
||||
impacted_providers: string[];
|
||||
resources_total: number;
|
||||
resources_fail: number;
|
||||
pass_count: number;
|
||||
fail_count: number;
|
||||
muted_count: number;
|
||||
new_count: number;
|
||||
changed_count: number;
|
||||
first_seen_at: string | null;
|
||||
last_seen_at: string | null;
|
||||
failing_since: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the API response for finding groups into FindingGroupRow[].
|
||||
*/
|
||||
export function adaptFindingGroupsResponse(
|
||||
apiResponse: any,
|
||||
): FindingGroupRow[] {
|
||||
if (!apiResponse?.data || !Array.isArray(apiResponse.data)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return apiResponse.data.map((item: FindingGroupApiItem) => ({
|
||||
id: item.id,
|
||||
rowType: FINDINGS_ROW_TYPE.GROUP,
|
||||
checkId: item.attributes.check_id,
|
||||
checkTitle: item.attributes.check_title || item.attributes.check_id,
|
||||
severity: item.attributes.severity as Severity,
|
||||
status: item.attributes.status as FindingStatus,
|
||||
resourcesTotal: item.attributes.resources_total,
|
||||
resourcesFail: item.attributes.resources_fail,
|
||||
newCount: item.attributes.new_count,
|
||||
changedCount: item.attributes.changed_count,
|
||||
mutedCount: item.attributes.muted_count,
|
||||
providers: (item.attributes.impacted_providers || []) as ProviderType[],
|
||||
updatedAt: item.attributes.last_seen_at || "",
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* API response shape for a finding group resource (drill-down).
|
||||
* Endpoint: /finding-groups/{check_id}/resources
|
||||
*
|
||||
* Each item has nested `resource` and `provider` objects in attributes
|
||||
* (NOT JSON:API included — it's a custom serializer).
|
||||
*/
|
||||
interface FindingGroupResourceApiItem {
|
||||
type: "finding-group-resources";
|
||||
id: string;
|
||||
attributes: {
|
||||
resource: {
|
||||
uid: string;
|
||||
name: string;
|
||||
service: string;
|
||||
region: string;
|
||||
type: string;
|
||||
};
|
||||
provider: {
|
||||
type: string;
|
||||
uid: string;
|
||||
alias: string;
|
||||
};
|
||||
status: string;
|
||||
severity: string;
|
||||
first_seen_at: string | null;
|
||||
last_seen_at: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the API response for finding group resources (drill-down)
|
||||
* into FindingResourceRow[].
|
||||
*/
|
||||
export function adaptFindingGroupResourcesResponse(
|
||||
apiResponse: any,
|
||||
checkId: string,
|
||||
): FindingResourceRow[] {
|
||||
if (!apiResponse?.data || !Array.isArray(apiResponse.data)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return apiResponse.data.map((item: FindingGroupResourceApiItem) => ({
|
||||
id: item.id,
|
||||
rowType: FINDINGS_ROW_TYPE.RESOURCE,
|
||||
findingId: item.id,
|
||||
checkId,
|
||||
providerType: (item.attributes.provider?.type || "aws") as ProviderType,
|
||||
providerAlias: item.attributes.provider?.alias || "",
|
||||
providerUid: item.attributes.provider?.uid || "",
|
||||
resourceName: item.attributes.resource?.name || "-",
|
||||
resourceUid: item.attributes.resource?.uid || "-",
|
||||
service: item.attributes.resource?.service || "-",
|
||||
region: item.attributes.resource?.region || "-",
|
||||
severity: (item.attributes.severity || "informational") as Severity,
|
||||
status: item.attributes.status,
|
||||
isMuted: item.attributes.status === "MUTED",
|
||||
mutedReason: undefined,
|
||||
firstSeenAt: item.attributes.first_seen_at,
|
||||
lastSeenAt: item.attributes.last_seen_at,
|
||||
}));
|
||||
}
|
||||
148
ui/actions/finding-groups/finding-groups.ts
Normal file
148
ui/actions/finding-groups/finding-groups.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
"use server";
|
||||
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { apiBaseUrl, getAuthHeaders } from "@/lib";
|
||||
import { appendSanitizedProviderFilters } from "@/lib/provider-filters";
|
||||
import { handleApiResponse } from "@/lib/server-actions-helper";
|
||||
|
||||
/**
|
||||
* Maps filter[search] to filter[check_id__icontains] for finding-groups.
|
||||
* The finding-groups endpoint doesn't support filter[search] but supports
|
||||
* check_id__icontains for substring matching on check IDs and titles.
|
||||
*/
|
||||
function mapSearchFilter(
|
||||
filters: Record<string, string | string[] | undefined>,
|
||||
): Record<string, string | string[] | undefined> {
|
||||
const mapped = { ...filters };
|
||||
const searchValue = mapped["filter[search]"];
|
||||
if (searchValue) {
|
||||
mapped["filter[check_id__icontains]"] = searchValue;
|
||||
delete mapped["filter[search]"];
|
||||
}
|
||||
return mapped;
|
||||
}
|
||||
|
||||
export const getFindingGroups = async ({
|
||||
page = 1,
|
||||
pageSize = 10,
|
||||
sort = "",
|
||||
filters = {},
|
||||
}) => {
|
||||
const headers = await getAuthHeaders({ contentType: false });
|
||||
|
||||
if (isNaN(Number(page)) || page < 1) redirect("/findings");
|
||||
|
||||
const url = new URL(`${apiBaseUrl}/finding-groups`);
|
||||
|
||||
if (page) url.searchParams.append("page[number]", page.toString());
|
||||
if (pageSize) url.searchParams.append("page[size]", pageSize.toString());
|
||||
if (sort) url.searchParams.append("sort", sort);
|
||||
|
||||
appendSanitizedProviderFilters(url, mapSearchFilter(filters));
|
||||
console.log("[finding-groups] GET", decodeURIComponent(url.toString()));
|
||||
|
||||
try {
|
||||
const response = await fetch(url.toString(), { headers });
|
||||
console.log("[finding-groups]", response.status);
|
||||
return handleApiResponse(response);
|
||||
} catch (error) {
|
||||
console.error("Error fetching finding groups:", error);
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
export const getLatestFindingGroups = async ({
|
||||
page = 1,
|
||||
pageSize = 10,
|
||||
sort = "",
|
||||
filters = {},
|
||||
}) => {
|
||||
const headers = await getAuthHeaders({ contentType: false });
|
||||
|
||||
if (isNaN(Number(page)) || page < 1) redirect("/findings");
|
||||
|
||||
const url = new URL(`${apiBaseUrl}/finding-groups/latest`);
|
||||
|
||||
if (page) url.searchParams.append("page[number]", page.toString());
|
||||
if (pageSize) url.searchParams.append("page[size]", pageSize.toString());
|
||||
if (sort) url.searchParams.append("sort", sort);
|
||||
|
||||
appendSanitizedProviderFilters(url, mapSearchFilter(filters));
|
||||
console.log("[finding-groups/latest] GET", decodeURIComponent(url.toString()));
|
||||
|
||||
try {
|
||||
const response = await fetch(url.toString(), { headers });
|
||||
console.log("[finding-groups/latest]", response.status);
|
||||
return handleApiResponse(response);
|
||||
} catch (error) {
|
||||
console.error("Error fetching latest finding groups:", error);
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
export const getFindingGroupResources = async ({
|
||||
checkId,
|
||||
page = 1,
|
||||
pageSize = 20,
|
||||
filters = {},
|
||||
}: {
|
||||
checkId: string;
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
filters?: Record<string, string | string[] | undefined>;
|
||||
}) => {
|
||||
const headers = await getAuthHeaders({ contentType: false });
|
||||
|
||||
const url = new URL(`${apiBaseUrl}/finding-groups/${checkId}/resources`);
|
||||
|
||||
if (page) url.searchParams.append("page[number]", page.toString());
|
||||
if (pageSize) url.searchParams.append("page[size]", pageSize.toString());
|
||||
|
||||
appendSanitizedProviderFilters(url, filters);
|
||||
|
||||
try {
|
||||
const response = await fetch(url.toString(), {
|
||||
headers,
|
||||
});
|
||||
|
||||
return handleApiResponse(response);
|
||||
} catch (error) {
|
||||
console.error("Error fetching finding group resources:", error);
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
export const getLatestFindingGroupResources = async ({
|
||||
checkId,
|
||||
page = 1,
|
||||
pageSize = 20,
|
||||
filters = {},
|
||||
}: {
|
||||
checkId: string;
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
filters?: Record<string, string | string[] | undefined>;
|
||||
}) => {
|
||||
const headers = await getAuthHeaders({ contentType: false });
|
||||
|
||||
const url = new URL(
|
||||
`${apiBaseUrl}/finding-groups/latest/${checkId}/resources`,
|
||||
);
|
||||
|
||||
if (page) url.searchParams.append("page[number]", page.toString());
|
||||
if (pageSize) url.searchParams.append("page[size]", pageSize.toString());
|
||||
|
||||
appendSanitizedProviderFilters(url, filters);
|
||||
|
||||
try {
|
||||
const response = await fetch(url.toString(), {
|
||||
headers,
|
||||
});
|
||||
|
||||
return handleApiResponse(response);
|
||||
} catch (error) {
|
||||
console.error("Error fetching latest finding group resources:", error);
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
2
ui/actions/finding-groups/index.ts
Normal file
2
ui/actions/finding-groups/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./finding-groups";
|
||||
export * from "./finding-groups.adapter";
|
||||
Reference in New Issue
Block a user