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>
559 lines
17 KiB
TypeScript
559 lines
17 KiB
TypeScript
import { getAllProviderGroups } from "@/actions/manage-groups/manage-groups";
|
|
import {
|
|
listOrganizationsSafe,
|
|
listOrganizationUnitsSafe,
|
|
} from "@/actions/organizations/organizations";
|
|
import { getAllProviders, getProviders } from "@/actions/providers";
|
|
import { PROVIDERS_FILTER_PARAM } from "@/actions/providers/providers-filters";
|
|
import { getSchedules } from "@/actions/schedules";
|
|
import {
|
|
extractFiltersAndQuery,
|
|
extractSortAndKey,
|
|
} from "@/lib/helper-filters";
|
|
import {
|
|
buildProviderScheduleSummary,
|
|
buildScheduleAttributesFromProvider,
|
|
buildSchedulesByProviderId,
|
|
isScheduleConfigured,
|
|
} from "@/lib/schedules";
|
|
import {
|
|
FilterEntity,
|
|
FilterOption,
|
|
OrganizationListResponse,
|
|
OrganizationUnitListResponse,
|
|
OrganizationUnitResource,
|
|
ProvidersApiResponse,
|
|
SearchParamsProps,
|
|
} from "@/types";
|
|
import {
|
|
PROVIDERS_GROUP_KIND,
|
|
PROVIDERS_PAGE_FILTER,
|
|
PROVIDERS_ROW_TYPE,
|
|
ProvidersAccountsViewData,
|
|
ProvidersOrganizationRow,
|
|
ProvidersProviderRow,
|
|
ProvidersTableRow,
|
|
ProvidersTableRowsInput,
|
|
} from "@/types/providers-table";
|
|
import { ScanScheduleSummary } from "@/types/scans";
|
|
import { ScheduleAttributes } from "@/types/schedules";
|
|
|
|
const PROVIDERS_STATUS_MAPPING = [
|
|
{
|
|
true: {
|
|
label: "Connected",
|
|
value: "true",
|
|
},
|
|
},
|
|
{
|
|
false: {
|
|
label: "Not connected",
|
|
value: "false",
|
|
},
|
|
},
|
|
] as Array<{ [key: string]: FilterEntity }>;
|
|
|
|
interface ProvidersAccountsViewInput {
|
|
isCloud: boolean;
|
|
searchParams: SearchParamsProps;
|
|
}
|
|
|
|
function hasActionError(result: unknown): result is {
|
|
error: unknown;
|
|
} {
|
|
return Boolean(
|
|
result &&
|
|
typeof result === "object" &&
|
|
"error" in (result as Record<string, unknown>) &&
|
|
(result as Record<string, unknown>).error !== null &&
|
|
(result as Record<string, unknown>).error !== undefined,
|
|
);
|
|
}
|
|
|
|
async function resolveActionResult<T>(
|
|
action: Promise<T | undefined>,
|
|
fallback?: T,
|
|
): Promise<T | undefined> {
|
|
try {
|
|
const result = await action;
|
|
|
|
if (hasActionError(result)) {
|
|
return fallback;
|
|
}
|
|
|
|
return result ?? fallback;
|
|
} catch {
|
|
return fallback;
|
|
}
|
|
}
|
|
|
|
const createProvidersFilters = (): FilterOption[] => {
|
|
return [
|
|
{
|
|
key: PROVIDERS_PAGE_FILTER.STATUS,
|
|
labelCheckboxGroup: "Status",
|
|
values: ["true", "false"],
|
|
valueLabelMapping: PROVIDERS_STATUS_MAPPING,
|
|
index: 0,
|
|
},
|
|
];
|
|
};
|
|
|
|
const createProviderGroupLookup = (
|
|
providersResponse?: ProvidersApiResponse,
|
|
): Map<string, string> => {
|
|
const lookup = new Map<string, string>();
|
|
|
|
for (const includedItem of providersResponse?.included ?? []) {
|
|
if (
|
|
includedItem.type === "provider-groups" &&
|
|
typeof includedItem.attributes?.name === "string"
|
|
) {
|
|
lookup.set(includedItem.id, includedItem.attributes.name);
|
|
}
|
|
}
|
|
|
|
return lookup;
|
|
};
|
|
|
|
// A schedule is backed by the Provider row itself, so its `/schedules` entry
|
|
// exists before the first scheduled Scan is materialized — only enabled,
|
|
// configured ones carry a displayable cadence summary.
|
|
const buildProviderScheduleSummaryFor = (
|
|
attributes: ScheduleAttributes | undefined,
|
|
now: Date,
|
|
): ScanScheduleSummary | undefined =>
|
|
attributes && attributes.scan_enabled && isScheduleConfigured(attributes)
|
|
? buildProviderScheduleSummary(attributes, now)
|
|
: undefined;
|
|
|
|
const getProviderLastScanAt = (
|
|
provider: ProvidersApiResponse["data"][number],
|
|
): string | null => {
|
|
if (
|
|
Object.prototype.hasOwnProperty.call(provider.attributes, "last_scan_at")
|
|
) {
|
|
return provider.attributes.last_scan_at ?? null;
|
|
}
|
|
|
|
return provider.attributes.connection.last_checked_at ?? null;
|
|
};
|
|
|
|
const enrichProviders = (
|
|
providersResponse: ProvidersApiResponse | undefined,
|
|
schedulesByProviderId: Record<string, ScheduleAttributes>,
|
|
): ProvidersProviderRow[] => {
|
|
const providerGroupLookup = createProviderGroupLookup(providersResponse);
|
|
const now = new Date();
|
|
|
|
return (providersResponse?.data ?? []).map((provider) => {
|
|
const providerScheduleAttributes = buildScheduleAttributesFromProvider(
|
|
provider.attributes,
|
|
);
|
|
const scheduleAttributes =
|
|
providerScheduleAttributes ?? schedulesByProviderId[provider.id];
|
|
const scheduleSummary = buildProviderScheduleSummaryFor(
|
|
scheduleAttributes,
|
|
now,
|
|
);
|
|
|
|
return {
|
|
...provider,
|
|
rowType: PROVIDERS_ROW_TYPE.PROVIDER,
|
|
groupNames:
|
|
provider.relationships.provider_groups.data.map(
|
|
(providerGroup: { id: string }) =>
|
|
providerGroupLookup.get(providerGroup.id) ?? "Unknown Group",
|
|
) ?? [],
|
|
// Provider scan_* fields are authoritative when present; otherwise we
|
|
// only fall back to the /schedules resource, never materialized scans.
|
|
hasSchedule: scheduleSummary !== undefined,
|
|
scheduleSummary,
|
|
lastScanAt: getProviderLastScanAt(provider),
|
|
};
|
|
});
|
|
};
|
|
|
|
const createOrganizationRow = ({
|
|
groupKind,
|
|
id,
|
|
name,
|
|
externalId,
|
|
organizationId,
|
|
parentExternalId,
|
|
providerIds,
|
|
subRows,
|
|
}: {
|
|
externalId: string | null;
|
|
groupKind: ProvidersOrganizationRow["groupKind"];
|
|
id: string;
|
|
name: string;
|
|
organizationId: string | null;
|
|
parentExternalId: string | null;
|
|
providerIds: string[];
|
|
subRows: ProvidersTableRow[];
|
|
}): ProvidersOrganizationRow => ({
|
|
id,
|
|
rowType: PROVIDERS_ROW_TYPE.ORGANIZATION,
|
|
groupKind,
|
|
name,
|
|
externalId,
|
|
organizationId,
|
|
parentExternalId,
|
|
providerCount: providerIds.length,
|
|
providerIds,
|
|
subRows,
|
|
});
|
|
|
|
function getRelationshipProviderIds(
|
|
relationships:
|
|
| {
|
|
providers?: {
|
|
data?: Array<{ id: string; type: string }>;
|
|
};
|
|
}
|
|
| undefined,
|
|
): string[] {
|
|
return relationships?.providers?.data?.map((provider) => provider.id) ?? [];
|
|
}
|
|
|
|
function getOrganizationUnitParentId(
|
|
organizationUnit: OrganizationUnitResource,
|
|
): string | null {
|
|
return organizationUnit.relationships.parent?.data?.id ?? null;
|
|
}
|
|
|
|
function getProviderRowsByIds({
|
|
providerIds,
|
|
providerLookup,
|
|
}: {
|
|
providerIds: string[];
|
|
providerLookup: Map<string, ProvidersProviderRow>;
|
|
}): ProvidersProviderRow[] {
|
|
return providerIds
|
|
.map((providerId) => providerLookup.get(providerId))
|
|
.filter((provider): provider is ProvidersProviderRow => Boolean(provider));
|
|
}
|
|
|
|
function dedupeIds(ids: string[]): string[] {
|
|
return Array.from(new Set(ids));
|
|
}
|
|
|
|
function collectOrganizationRowProviderIds(
|
|
rows: ProvidersOrganizationRow[],
|
|
): string[] {
|
|
return dedupeIds(rows.flatMap((row) => row.providerIds));
|
|
}
|
|
|
|
function getOrganizationUnitRelationshipId(
|
|
provider: ProvidersProviderRow,
|
|
): string | null {
|
|
return (
|
|
provider.relationships.organization_unit?.data?.id ??
|
|
provider.relationships.organizational_unit?.data?.id ??
|
|
null
|
|
);
|
|
}
|
|
|
|
function buildOrganizationUnitRows({
|
|
organizationId,
|
|
organizationUnits,
|
|
providerLookup,
|
|
providersByOrganizationUnitId,
|
|
useParentIdRelationships,
|
|
parentExternalId,
|
|
parentOrganizationUnitId,
|
|
maxDepth = 10,
|
|
}: {
|
|
organizationId: string;
|
|
organizationUnits: OrganizationUnitResource[];
|
|
parentExternalId: string | null;
|
|
parentOrganizationUnitId: string | null;
|
|
providerLookup: Map<string, ProvidersProviderRow>;
|
|
providersByOrganizationUnitId: Map<string, ProvidersProviderRow[]>;
|
|
useParentIdRelationships: boolean;
|
|
maxDepth?: number;
|
|
}): ProvidersOrganizationRow[] {
|
|
if (maxDepth <= 0) {
|
|
return [];
|
|
}
|
|
|
|
return organizationUnits
|
|
.filter(
|
|
(organizationUnit) =>
|
|
organizationUnit.relationships.organization.data.id ===
|
|
organizationId &&
|
|
(useParentIdRelationships
|
|
? getOrganizationUnitParentId(organizationUnit) ===
|
|
parentOrganizationUnitId
|
|
: organizationUnit.attributes.parent_external_id ===
|
|
parentExternalId),
|
|
)
|
|
.map((organizationUnit) => {
|
|
const childOrganizationUnitRows = buildOrganizationUnitRows({
|
|
organizationId,
|
|
organizationUnits,
|
|
parentOrganizationUnitId: organizationUnit.id,
|
|
parentExternalId: organizationUnit.attributes.external_id,
|
|
providerLookup,
|
|
providersByOrganizationUnitId,
|
|
useParentIdRelationships,
|
|
maxDepth: maxDepth - 1,
|
|
});
|
|
const providerRowsFromRelationships = getProviderRowsByIds({
|
|
providerIds: getRelationshipProviderIds(organizationUnit.relationships),
|
|
providerLookup,
|
|
});
|
|
const providerRows =
|
|
providerRowsFromRelationships.length > 0
|
|
? providerRowsFromRelationships
|
|
: (providersByOrganizationUnitId.get(organizationUnit.id) ?? []);
|
|
const subRows = [...childOrganizationUnitRows, ...providerRows];
|
|
const directProviderIds =
|
|
providerRowsFromRelationships.length > 0
|
|
? getRelationshipProviderIds(organizationUnit.relationships)
|
|
: providerRows.map((provider) => provider.id);
|
|
const childProviderIds = collectOrganizationRowProviderIds(
|
|
childOrganizationUnitRows,
|
|
);
|
|
|
|
return createOrganizationRow({
|
|
groupKind: PROVIDERS_GROUP_KIND.ORGANIZATION_UNIT,
|
|
id: organizationUnit.id,
|
|
name: organizationUnit.attributes.name,
|
|
externalId: organizationUnit.attributes.external_id,
|
|
organizationId,
|
|
parentExternalId: organizationUnit.attributes.parent_external_id,
|
|
providerIds: dedupeIds([...childProviderIds, ...directProviderIds]),
|
|
subRows,
|
|
});
|
|
})
|
|
.filter(
|
|
(organizationUnitRow) => organizationUnitRow.providerIds.length > 0,
|
|
);
|
|
}
|
|
|
|
export function buildProvidersTableRows({
|
|
isCloud,
|
|
organizations,
|
|
organizationUnits,
|
|
providers,
|
|
}: ProvidersTableRowsInput): ProvidersTableRow[] {
|
|
if (!isCloud) {
|
|
return providers;
|
|
}
|
|
|
|
const providerLookup = new Map(
|
|
providers.map((provider) => [provider.id, provider] as const),
|
|
);
|
|
const providersByOrganizationId = new Map<string, ProvidersProviderRow[]>();
|
|
const providersByOrganizationUnitId = new Map<
|
|
string,
|
|
ProvidersProviderRow[]
|
|
>();
|
|
|
|
for (const provider of providers) {
|
|
const organizationId =
|
|
provider.relationships.organization?.data?.id ?? null;
|
|
const organizationUnitId = getOrganizationUnitRelationshipId(provider);
|
|
|
|
if (organizationUnitId) {
|
|
const organizationUnitProviders =
|
|
providersByOrganizationUnitId.get(organizationUnitId) ?? [];
|
|
organizationUnitProviders.push(provider);
|
|
providersByOrganizationUnitId.set(
|
|
organizationUnitId,
|
|
organizationUnitProviders,
|
|
);
|
|
continue;
|
|
}
|
|
|
|
if (organizationId) {
|
|
const organizationProviders =
|
|
providersByOrganizationId.get(organizationId) ?? [];
|
|
organizationProviders.push(provider);
|
|
providersByOrganizationId.set(organizationId, organizationProviders);
|
|
}
|
|
}
|
|
|
|
const useParentIdRelationships = organizationUnits.some(
|
|
(organizationUnit) => organizationUnit.relationships.parent !== undefined,
|
|
);
|
|
|
|
// Build a set of provider IDs that are assigned to OUs, so we can
|
|
// exclude them from the org's direct children and avoid duplication.
|
|
const providersAssignedToOu = new Set(
|
|
Array.from(providersByOrganizationUnitId.values()).flatMap((providers) =>
|
|
providers.map((p) => p.id),
|
|
),
|
|
);
|
|
|
|
const organizationRows = organizations
|
|
.map((organization) => {
|
|
const organizationUnitRows = buildOrganizationUnitRows({
|
|
organizationId: organization.id,
|
|
organizationUnits,
|
|
parentOrganizationUnitId: null,
|
|
parentExternalId: organization.attributes.root_external_id,
|
|
providerLookup,
|
|
providersByOrganizationUnitId,
|
|
useParentIdRelationships,
|
|
});
|
|
|
|
// Collect all provider IDs already placed inside OUs to avoid duplication
|
|
// at the org level. This covers both relationship-based and fallback assignments.
|
|
const providersInOus = new Set<string>();
|
|
function collectOuProviderIds(rows: ProvidersTableRow[]) {
|
|
for (const row of rows) {
|
|
if (row.rowType === PROVIDERS_ROW_TYPE.PROVIDER) {
|
|
providersInOus.add(row.id);
|
|
} else {
|
|
collectOuProviderIds(row.subRows);
|
|
}
|
|
}
|
|
}
|
|
collectOuProviderIds(organizationUnitRows);
|
|
|
|
const organizationProvidersFromRelationships = getProviderRowsByIds({
|
|
providerIds: getRelationshipProviderIds(organization.relationships),
|
|
providerLookup,
|
|
}).filter(
|
|
(provider) =>
|
|
!providersAssignedToOu.has(provider.id) &&
|
|
!providersInOus.has(provider.id),
|
|
);
|
|
const organizationProviders =
|
|
organizationProvidersFromRelationships.length > 0
|
|
? organizationProvidersFromRelationships
|
|
: (providersByOrganizationId.get(organization.id) ?? []).filter(
|
|
(provider) => !providersInOus.has(provider.id),
|
|
);
|
|
const subRows = [...organizationProviders, ...organizationUnitRows];
|
|
const directProviderIds =
|
|
organizationProvidersFromRelationships.length > 0
|
|
? getRelationshipProviderIds(organization.relationships)
|
|
: organizationProviders.map((provider) => provider.id);
|
|
const organizationUnitProviderIds =
|
|
collectOrganizationRowProviderIds(organizationUnitRows);
|
|
|
|
return createOrganizationRow({
|
|
groupKind: PROVIDERS_GROUP_KIND.ORGANIZATION,
|
|
id: organization.id,
|
|
name: organization.attributes.name,
|
|
externalId: organization.attributes.external_id,
|
|
organizationId: organization.id,
|
|
parentExternalId: organization.attributes.root_external_id,
|
|
providerIds: dedupeIds([
|
|
...directProviderIds,
|
|
...organizationUnitProviderIds,
|
|
]),
|
|
subRows,
|
|
});
|
|
})
|
|
.filter((organizationRow) => organizationRow.providerIds.length > 0);
|
|
|
|
const assignedProviderIds = new Set<string>();
|
|
|
|
function collectAssignedProviderIds(rows: ProvidersTableRow[]) {
|
|
for (const row of rows) {
|
|
if (row.rowType === PROVIDERS_ROW_TYPE.PROVIDER) {
|
|
assignedProviderIds.add(row.id);
|
|
continue;
|
|
}
|
|
|
|
collectAssignedProviderIds(row.subRows);
|
|
}
|
|
}
|
|
|
|
collectAssignedProviderIds(organizationRows);
|
|
const orphanProviders = providers.filter(
|
|
(provider) => !assignedProviderIds.has(provider.id),
|
|
);
|
|
|
|
return [...organizationRows, ...orphanProviders];
|
|
}
|
|
|
|
export async function loadProvidersAccountsViewData({
|
|
isCloud,
|
|
searchParams,
|
|
}: ProvidersAccountsViewInput): Promise<ProvidersAccountsViewData> {
|
|
const page = parseInt(searchParams.page?.toString() ?? "1", 10);
|
|
const pageSize = parseInt(searchParams.pageSize?.toString() ?? "10", 10);
|
|
const { encodedSort } = extractSortAndKey(searchParams);
|
|
const { filters, query } = extractFiltersAndQuery(searchParams);
|
|
|
|
const providerFilters = { ...filters };
|
|
|
|
// Map provider_type__in (used by ProviderTypeSelector) to provider__in (API param)
|
|
const providerTypeFilter =
|
|
providerFilters[PROVIDERS_FILTER_PARAM.PROVIDER_TYPE];
|
|
if (providerTypeFilter) {
|
|
providerFilters[PROVIDERS_FILTER_PARAM.PROVIDER] = providerTypeFilter;
|
|
}
|
|
|
|
delete providerFilters[PROVIDERS_FILTER_PARAM.PROVIDER_TYPE];
|
|
|
|
const emptyOrganizationsResponse: OrganizationListResponse = {
|
|
data: [],
|
|
};
|
|
const emptyOrganizationUnitsResponse: OrganizationUnitListResponse = {
|
|
data: [],
|
|
};
|
|
|
|
const [
|
|
providersResponse,
|
|
allProvidersResponse,
|
|
allProviderGroupsResponse,
|
|
schedulesResponse,
|
|
organizationsResponse,
|
|
organizationUnitsResponse,
|
|
] = await Promise.all([
|
|
resolveActionResult(
|
|
getProviders({
|
|
filters: providerFilters,
|
|
page,
|
|
pageSize,
|
|
query,
|
|
sort: encodedSort,
|
|
}),
|
|
),
|
|
// Unfiltered fetch for ProviderTypeSelector — only needs distinct types;
|
|
// TODO: Replace with a dedicated lightweight endpoint when available.
|
|
resolveActionResult(getAllProviders()),
|
|
// Unfiltered fetch for the Provider Group selector dropdown.
|
|
resolveActionResult(getAllProviderGroups()),
|
|
// Fetch configured schedules as a fallback when provider scan_* fields are
|
|
// absent (best-effort: typically empty in OSS).
|
|
resolveActionResult(getSchedules()),
|
|
isCloud
|
|
? listOrganizationsSafe()
|
|
: Promise.resolve(emptyOrganizationsResponse),
|
|
isCloud
|
|
? listOrganizationUnitsSafe()
|
|
: Promise.resolve(emptyOrganizationUnitsResponse),
|
|
]);
|
|
|
|
const schedulesByProviderId = buildSchedulesByProviderId(schedulesResponse);
|
|
|
|
const orgs = organizationsResponse?.data ?? [];
|
|
const ous = organizationUnitsResponse?.data ?? [];
|
|
const providers = enrichProviders(providersResponse, schedulesByProviderId);
|
|
|
|
const rows = buildProvidersTableRows({
|
|
isCloud,
|
|
organizations: orgs,
|
|
organizationUnits: ous,
|
|
providers,
|
|
});
|
|
|
|
return {
|
|
filters: createProvidersFilters(),
|
|
metadata: providersResponse?.meta,
|
|
providers: allProvidersResponse?.data ?? [],
|
|
providerGroups: allProviderGroupsResponse?.data ?? [],
|
|
rows,
|
|
};
|
|
}
|
|
|
|
export { PROVIDERS_ROW_TYPE };
|