refactor(ui): centralize provider type filter sanitization in server actions (#10043)

This commit is contained in:
Alejandro Bailo
2026-02-12 14:12:37 +01:00
committed by GitHub
parent c5707ae9f1
commit 5b038e631a
16 changed files with 219 additions and 151 deletions

View File

@@ -3,6 +3,7 @@
import { redirect } from "next/navigation";
import { apiBaseUrl, getAuthHeaders } from "@/lib";
import { appendSanitizedProviderTypeFilters } from "@/lib/provider-filters";
import { handleApiResponse } from "@/lib/server-actions-helper";
export const getFindings = async ({
page = 1,
@@ -24,12 +25,7 @@ export const getFindings = async ({
if (query) url.searchParams.append("filter[search]", query);
if (sort) url.searchParams.append("sort", sort);
Object.entries(filters).forEach(([key, value]) => {
// Skip filter[search] since it's already added via the `query` param above
if (key !== "filter[search]") {
url.searchParams.append(key, String(value));
}
});
appendSanitizedProviderTypeFilters(url, filters);
try {
const findings = await fetch(url.toString(), {
@@ -65,12 +61,7 @@ export const getLatestFindings = async ({
if (query) url.searchParams.append("filter[search]", query);
if (sort) url.searchParams.append("sort", sort);
Object.entries(filters).forEach(([key, value]) => {
// Skip filter[search] since it's already added via the `query` param above
if (key !== "filter[search]") {
url.searchParams.append(key, String(value));
}
});
appendSanitizedProviderTypeFilters(url, filters);
try {
const findings = await fetch(url.toString(), {
@@ -96,20 +87,13 @@ export const getMetadataInfo = async ({
if (query) url.searchParams.append("filter[search]", query);
if (sort) url.searchParams.append("sort", sort);
Object.entries(filters).forEach(([key, value]) => {
// Define filters to exclude
const excludedFilters = [
appendSanitizedProviderTypeFilters(url, filters, {
excludedKeyIncludes: [
"region__in",
"service__in",
"resource_type__in",
"resource_groups__in",
];
if (
key !== "filter[search]" &&
!excludedFilters.some((filter) => key.includes(filter))
) {
url.searchParams.append(key, String(value));
}
],
});
try {
@@ -136,20 +120,13 @@ export const getLatestMetadataInfo = async ({
if (query) url.searchParams.append("filter[search]", query);
if (sort) url.searchParams.append("sort", sort);
Object.entries(filters).forEach(([key, value]) => {
// Define filters to exclude
const excludedFilters = [
appendSanitizedProviderTypeFilters(url, filters, {
excludedKeyIncludes: [
"region__in",
"service__in",
"resource_type__in",
"resource_groups__in",
];
if (
key !== "filter[search]" &&
!excludedFilters.some((filter) => key.includes(filter))
) {
url.searchParams.append(key, String(value));
}
],
});
try {

View File

@@ -1,6 +1,7 @@
"use server";
import { apiBaseUrl, getAuthHeaders } from "@/lib";
import { appendSanitizedProviderTypeFilters } from "@/lib/provider-filters";
import { handleApiResponse } from "@/lib/server-actions-helper";
import { AttackSurfaceOverviewResponse } from "./types";
@@ -14,12 +15,7 @@ export const getAttackSurfaceOverview = async ({
const url = new URL(`${apiBaseUrl}/overviews/attack-surfaces`);
// Handle multiple filters
Object.entries(filters).forEach(([key, value]) => {
if (key !== "filter[search]" && value !== undefined) {
url.searchParams.append(key, String(value));
}
});
appendSanitizedProviderTypeFilters(url, filters);
try {
const response = await fetch(url.toString(), {

View File

@@ -1,6 +1,7 @@
"use server";
import { apiBaseUrl, getAuthHeaders } from "@/lib";
import { appendSanitizedProviderTypeFilters } from "@/lib/provider-filters";
import { handleApiResponse } from "@/lib/server-actions-helper";
import { ComplianceWatchlistResponse } from "./compliance-watchlist.types";
@@ -15,11 +16,7 @@ export const getComplianceWatchlist = async ({
// Append filter parameters (provider_id, provider_type, etc.)
// Exclude filter[search] as this endpoint doesn't support text search
Object.entries(filters).forEach(([key, value]) => {
if (key !== "filter[search]" && value !== undefined) {
url.searchParams.append(key, String(value));
}
});
appendSanitizedProviderTypeFilters(url, filters);
try {
const response = await fetch(url.toString(), { headers });

View File

@@ -3,6 +3,7 @@
import { redirect } from "next/navigation";
import { apiBaseUrl, getAuthHeaders } from "@/lib";
import { appendSanitizedProviderTypeFilters } from "@/lib/provider-filters";
import { handleApiResponse } from "@/lib/server-actions-helper";
import { FindingsSeverityOverviewResponse } from "./types";
@@ -28,17 +29,8 @@ export const getFindingsByStatus = async ({
if (query) url.searchParams.append("filter[search]", query);
if (sort) url.searchParams.append("sort", sort);
// Handle multiple filters, but exclude muted filter as overviews endpoint doesn't support it
Object.entries(filters).forEach(([key, value]) => {
// The overviews/findings endpoint does not support status or muted filters
// (allowed filters include date, region, provider fields). Exclude unsupported ones.
if (
key !== "filter[search]" &&
key !== "filter[muted]" &&
key !== "filter[status]"
) {
url.searchParams.append(key, String(value));
}
appendSanitizedProviderTypeFilters(url, filters, {
excludedKeys: ["filter[search]", "filter[muted]", "filter[status]"],
});
try {
@@ -62,15 +54,8 @@ export const getFindingsBySeverity = async ({
const url = new URL(`${apiBaseUrl}/overviews/findings_severity`);
// Handle multiple filters, but exclude unsupported filters
Object.entries(filters).forEach(([key, value]) => {
if (
key !== "filter[search]" &&
key !== "filter[muted]" &&
value !== undefined
) {
url.searchParams.append(key, String(value));
}
appendSanitizedProviderTypeFilters(url, filters, {
excludedKeys: ["filter[search]", "filter[muted]"],
});
try {

View File

@@ -3,6 +3,7 @@
import { redirect } from "next/navigation";
import { apiBaseUrl, getAuthHeaders } from "@/lib";
import { appendSanitizedProviderTypeFilters } from "@/lib/provider-filters";
import { handleApiResponse } from "@/lib/server-actions-helper";
import { ProvidersOverviewResponse } from "./types";
@@ -28,11 +29,7 @@ export const getProvidersOverview = async ({
if (query) url.searchParams.append("filter[search]", query);
if (sort) url.searchParams.append("sort", sort);
Object.entries(filters).forEach(([key, value]) => {
if (key !== "filter[search]" && value !== undefined) {
url.searchParams.append(key, String(value));
}
});
appendSanitizedProviderTypeFilters(url, filters);
try {
const response = await fetch(url.toString(), {

View File

@@ -1,6 +1,7 @@
"use server";
import { apiBaseUrl, getAuthHeaders } from "@/lib";
import { appendSanitizedProviderTypeFilters } from "@/lib/provider-filters";
import { handleApiResponse } from "@/lib/server-actions-helper";
import { RegionsOverviewResponse } from "./types";
@@ -14,12 +15,7 @@ export const getRegionsOverview = async ({
const url = new URL(`${apiBaseUrl}/overviews/regions`);
// Handle multiple filters
Object.entries(filters).forEach(([key, value]) => {
if (key !== "filter[search]" && value !== undefined) {
url.searchParams.append(key, String(value));
}
});
appendSanitizedProviderTypeFilters(url, filters);
try {
const response = await fetch(url.toString(), {

View File

@@ -1,6 +1,7 @@
"use server";
import { apiBaseUrl, getAuthHeaders } from "@/lib";
import { appendSanitizedProviderTypeFilters } from "@/lib/provider-filters";
import { handleApiResponse } from "@/lib/server-actions-helper";
import { ResourceGroupOverviewResponse } from "./types";
@@ -14,12 +15,7 @@ export const getResourceGroupOverview = async ({
const url = new URL(`${apiBaseUrl}/overviews/resource-groups`);
// Handle multiple filters
Object.entries(filters).forEach(([key, value]) => {
if (key !== "filter[search]" && value !== undefined) {
url.searchParams.append(key, String(value));
}
});
appendSanitizedProviderTypeFilters(url, filters);
try {
const response = await fetch(url.toString(), {

View File

@@ -1,6 +1,7 @@
"use server";
import { apiBaseUrl, getAuthHeaders } from "@/lib";
import { appendSanitizedProviderTypeFilters } from "@/lib/provider-filters";
import { handleApiResponse } from "@/lib/server-actions-helper";
import { CategoryOverviewResponse } from "./types";
@@ -14,12 +15,7 @@ export const getCategoryOverview = async ({
const url = new URL(`${apiBaseUrl}/overviews/categories`);
// Handle multiple filters
Object.entries(filters).forEach(([key, value]) => {
if (key !== "filter[search]" && value !== undefined) {
url.searchParams.append(key, String(value));
}
});
appendSanitizedProviderTypeFilters(url, filters);
try {
const response = await fetch(url.toString(), {

View File

@@ -1,6 +1,7 @@
"use server";
import { apiBaseUrl, getAuthHeaders } from "@/lib";
import { appendSanitizedProviderTypeFilters } from "@/lib/provider-filters";
import { handleApiResponse } from "@/lib/server-actions-helper";
import { ServicesOverviewResponse } from "./types";
@@ -14,11 +15,7 @@ export const getServicesOverview = async ({
const url = new URL(`${apiBaseUrl}/overviews/services`);
Object.entries(filters).forEach(([key, value]) => {
if (key !== "filter[search]" && value !== undefined) {
url.searchParams.append(key, String(value));
}
});
appendSanitizedProviderTypeFilters(url, filters);
try {
const response = await fetch(url.toString(), {

View File

@@ -5,6 +5,7 @@ import {
type TimeRange,
} from "@/app/(prowler)/_overview/severity-over-time/_constants/time-range.constants";
import { apiBaseUrl, getAuthHeaders } from "@/lib";
import { appendSanitizedProviderTypeFilters } from "@/lib/provider-filters";
import { handleApiResponse } from "@/lib/server-actions-helper";
import { adaptSeverityTrendsResponse } from "./severity-trends.adapter";
@@ -27,11 +28,7 @@ const getFindingsSeverityTrends = async ({
const url = new URL(`${apiBaseUrl}/overviews/findings_severity/timeseries`);
Object.entries(filters).forEach(([key, value]) => {
if (value !== undefined) {
url.searchParams.append(key, String(value));
}
});
appendSanitizedProviderTypeFilters(url, filters);
try {
const response = await fetch(url.toString(), {

View File

@@ -1,6 +1,7 @@
"use server";
import { apiBaseUrl, getAuthHeaders } from "@/lib";
import { appendSanitizedProviderTypeFilters } from "@/lib/provider-filters";
import { handleApiResponse } from "@/lib/server-actions-helper";
export const getThreatScore = async ({
@@ -12,12 +13,7 @@ export const getThreatScore = async ({
const url = new URL(`${apiBaseUrl}/overviews/threatscore`);
// Handle multiple filters
Object.entries(filters).forEach(([key, value]) => {
if (key !== "filter[search]") {
url.searchParams.append(key, String(value));
}
});
appendSanitizedProviderTypeFilters(url, filters);
try {
const response = await fetch(url.toString(), {

View File

@@ -6,6 +6,7 @@ import { redirect } from "next/navigation";
import { apiBaseUrl, getAuthHeaders, getFormValue, wait } from "@/lib";
import { buildSecretConfig } from "@/lib/provider-credentials/build-crendentials";
import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields";
import { appendSanitizedProviderInFilters } from "@/lib/provider-filters";
import { handleApiError, handleApiResponse } from "@/lib/server-actions-helper";
import { ProvidersApiResponse, ProviderType } from "@/types/providers";
@@ -15,6 +16,12 @@ export const getProviders = async ({
sort = "",
filters = {},
pageSize = 10,
}: {
page?: number;
query?: string;
sort?: string;
filters?: Record<string, string | string[] | undefined>;
pageSize?: number;
}): Promise<ProvidersApiResponse | undefined> => {
const headers = await getAuthHeaders({ contentType: false });
@@ -27,12 +34,7 @@ export const getProviders = async ({
if (query) url.searchParams.append("filter[search]", query);
if (sort) url.searchParams.append("sort", sort);
// Handle multiple filters
Object.entries(filters).forEach(([key, value]) => {
if (key !== "filter[search]") {
url.searchParams.append(key, String(value));
}
});
appendSanitizedProviderInFilters(url, filters);
try {
const response = await fetch(url.toString(), {
@@ -60,7 +62,7 @@ export const getAllProviders = async ({
}: {
query?: string;
sort?: string;
filters?: Record<string, unknown>;
filters?: Record<string, string | string[] | undefined>;
} = {}): Promise<ProvidersApiResponse | undefined> => {
const headers = await getAuthHeaders({ contentType: false });
const pageSize = 100; // Use larger page size to minimize API calls
@@ -79,11 +81,7 @@ export const getAllProviders = async ({
if (query) url.searchParams.append("filter[search]", query);
if (sort) url.searchParams.append("sort", sort);
Object.entries(filters).forEach(([key, value]) => {
if (key !== "filter[search]") {
url.searchParams.append(key, String(value));
}
});
appendSanitizedProviderInFilters(url, filters);
const response = await fetch(url.toString(), { headers });
const data = (await handleApiResponse(response)) as

View File

@@ -3,6 +3,7 @@
import { redirect } from "next/navigation";
import { apiBaseUrl, getAuthHeaders } from "@/lib";
import { appendSanitizedProviderTypeFilters } from "@/lib/provider-filters";
import { handleApiResponse } from "@/lib/server-actions-helper";
export const getResources = async ({
@@ -17,7 +18,7 @@ export const getResources = async ({
page?: number;
query?: string;
sort?: string;
filters?: Record<string, string>;
filters?: Record<string, string | string[] | undefined>;
pageSize?: number;
include?: string;
fields?: string[];
@@ -38,9 +39,7 @@ export const getResources = async ({
if (query) url.searchParams.append("filter[search]", query);
if (sort) url.searchParams.append("sort", sort);
Object.entries(filters).forEach(([key, value]) => {
url.searchParams.append(key, String(value));
});
appendSanitizedProviderTypeFilters(url, filters);
try {
const response = await fetch(url.toString(), {
@@ -66,7 +65,7 @@ export const getLatestResources = async ({
page?: number;
query?: string;
sort?: string;
filters?: Record<string, string>;
filters?: Record<string, string | string[] | undefined>;
pageSize?: number;
include?: string;
fields?: string[];
@@ -87,9 +86,7 @@ export const getLatestResources = async ({
if (query) url.searchParams.append("filter[search]", query);
if (sort) url.searchParams.append("sort", sort);
Object.entries(filters).forEach(([key, value]) => {
url.searchParams.append(key, String(value));
});
appendSanitizedProviderTypeFilters(url, filters);
try {
const response = await fetch(url.toString(), {
@@ -107,6 +104,10 @@ export const getMetadataInfo = async ({
query = "",
sort = "",
filters = {},
}: {
query?: string;
sort?: string;
filters?: Record<string, string | string[] | undefined>;
}) => {
const headers = await getAuthHeaders({ contentType: false });
@@ -115,9 +116,7 @@ export const getMetadataInfo = async ({
if (query) url.searchParams.append("filter[search]", query);
if (sort) url.searchParams.append("sort", sort);
Object.entries(filters).forEach(([key, value]) => {
url.searchParams.append(key, String(value));
});
appendSanitizedProviderTypeFilters(url, filters);
try {
const response = await fetch(url.toString(), {
@@ -135,6 +134,10 @@ export const getLatestMetadataInfo = async ({
query = "",
sort = "",
filters = {},
}: {
query?: string;
sort?: string;
filters?: Record<string, string | string[] | undefined>;
}) => {
const headers = await getAuthHeaders({ contentType: false });
@@ -143,9 +146,7 @@ export const getLatestMetadataInfo = async ({
if (query) url.searchParams.append("filter[search]", query);
if (sort) url.searchParams.append("sort", sort);
Object.entries(filters).forEach(([key, value]) => {
url.searchParams.append(key, String(value));
});
appendSanitizedProviderTypeFilters(url, filters);
try {
const response = await fetch(url.toString(), {

View File

@@ -7,6 +7,10 @@ import {
COMPLIANCE_REPORT_DISPLAY_NAMES,
type ComplianceReportType,
} from "@/lib/compliance/compliance-report-types";
import {
appendSanitizedProviderTypeFilters,
sanitizeProviderTypesCsv,
} from "@/lib/provider-filters";
import { addScanOperation } from "@/lib/sentry-breadcrumbs";
import { handleApiError, handleApiResponse } from "@/lib/server-actions-helper";
export const getScans = async ({
@@ -35,13 +39,7 @@ export const getScans = async ({
url.searchParams.append(`fields[${key}]`, String(value));
});
// Add dynamic filters (e.g., "filter[state]", "fields[scans]")
Object.entries(filters).forEach(([key, value]) => {
// Skip filter[search] since it's already added via the `query` param above
if (key !== "filter[search]") {
url.searchParams.append(key, String(value));
}
});
appendSanitizedProviderTypeFilters(url, filters);
try {
const response = await fetch(url.toString(), { headers });
@@ -58,6 +56,10 @@ export const getScansByState = async () => {
const url = new URL(`${apiBaseUrl}/scans`);
// Request only the necessary fields to optimize the response
url.searchParams.append("fields[scans]", "state");
url.searchParams.append(
"filter[provider_type__in]",
sanitizeProviderTypesCsv(),
);
try {
const response = await fetch(url.toString(), {

View File

@@ -13,7 +13,7 @@ import {
} from "@/components/providers/table";
import { ContentLayout } from "@/components/ui";
import { DataTable } from "@/components/ui/table";
import { PROVIDER_TYPES, ProviderProps, SearchParamsProps } from "@/types";
import { ProviderProps, SearchParamsProps } from "@/types";
export default async function Providers({
searchParams,
@@ -89,22 +89,15 @@ const ProvidersTable = async ({
return acc;
}, {}) || {};
// Exclude provider types not yet supported in the UI
const enrichedProviders =
providersData?.data
?.filter((provider: ProviderProps) =>
(PROVIDER_TYPES as readonly string[]).includes(
provider.attributes.provider,
),
)
.map((provider: ProviderProps) => {
const groupNames =
provider.relationships?.provider_groups?.data?.map(
(group: { id: string }) =>
providerGroupDict[group.id] || "Unknown Group",
) || [];
return { ...provider, groupNames };
}) || [];
providersData?.data?.map((provider: ProviderProps) => {
const groupNames =
provider.relationships?.provider_groups?.data?.map(
(group: { id: string }) =>
providerGroupDict[group.id] || "Unknown Group",
) || [];
return { ...provider, groupNames };
}) || [];
return (
<>

144
ui/lib/provider-filters.ts Normal file
View File

@@ -0,0 +1,144 @@
import { PROVIDER_TYPES, type ProviderType } from "@/types/providers";
export type ProviderFilterValue = string | string[] | undefined;
export type ProviderFilters = Record<string, ProviderFilterValue>;
interface AppendProviderFiltersOptions {
ensuredInFilterKey?: string;
excludedKeys?: string[];
excludedKeyIncludes?: string[];
}
type AppendSanitizedProviderTypeFiltersOptions = Omit<
AppendProviderFiltersOptions,
"ensuredInFilterKey"
>;
const SUPPORTED_PROVIDER_TYPES_CSV = PROVIDER_TYPES.join(",");
export const PROVIDER_IN_FILTER_KEY = "filter[provider__in]";
export const PROVIDER_TYPE_IN_FILTER_KEY = "filter[provider_type__in]";
const PROVIDER_TYPE_IN_KEYS = new Set([
PROVIDER_TYPE_IN_FILTER_KEY,
"provider_type__in",
]);
const PROVIDER_SINGLE_KEYS = new Set([
"filter[provider_type]",
"provider_type",
]);
const toCsvString = (value: unknown): string => {
if (Array.isArray(value)) return value.join(",");
if (typeof value === "string") return value;
return "";
};
const isSupportedProviderType = (value: string): value is ProviderType =>
(PROVIDER_TYPES as readonly string[]).includes(value);
export const sanitizeProviderTypesCsv = (value?: unknown): string => {
const rawValue = toCsvString(value);
if (!rawValue.trim()) return SUPPORTED_PROVIDER_TYPES_CSV;
const supportedProviderTypes = Array.from(
new Set(
rawValue
.split(",")
.map((providerType) => providerType.trim())
.filter(isSupportedProviderType),
),
);
return supportedProviderTypes.length > 0
? supportedProviderTypes.join(",")
: SUPPORTED_PROVIDER_TYPES_CSV;
};
export const sanitizeProviderType = (
value?: unknown,
): ProviderType | undefined => {
const rawValue = toCsvString(value);
if (!rawValue.trim()) return undefined;
return rawValue
.split(",")
.map((providerType) => providerType.trim())
.find(isSupportedProviderType);
};
export const sanitizeProviderFilters = (
filters: ProviderFilters = {},
ensuredInFilterKey?: string,
): ProviderFilters => {
const sanitizedFilters: ProviderFilters = { ...filters };
Object.keys(sanitizedFilters).forEach((key) => {
if (PROVIDER_TYPE_IN_KEYS.has(key)) {
sanitizedFilters[key] = sanitizeProviderTypesCsv(sanitizedFilters[key]);
return;
}
if (PROVIDER_SINGLE_KEYS.has(key)) {
const providerType = sanitizeProviderType(sanitizedFilters[key]);
if (providerType) {
sanitizedFilters[key] = providerType;
} else {
delete sanitizedFilters[key];
}
}
});
if (ensuredInFilterKey) {
sanitizedFilters[ensuredInFilterKey] = sanitizeProviderTypesCsv(
sanitizedFilters[ensuredInFilterKey],
);
}
return sanitizedFilters;
};
export const appendSanitizedProviderFilters = (
url: URL,
filters: ProviderFilters = {},
{
ensuredInFilterKey,
excludedKeys = ["filter[search]"],
excludedKeyIncludes = [],
}: AppendProviderFiltersOptions = {},
): void => {
const sanitizedFilters = sanitizeProviderFilters(filters, ensuredInFilterKey);
const excludedKeysSet = new Set(excludedKeys);
Object.entries(sanitizedFilters).forEach(([key, value]) => {
if (
value === undefined ||
excludedKeysSet.has(key) ||
excludedKeyIncludes.some((excludedKey) => key.includes(excludedKey))
) {
return;
}
url.searchParams.append(key, String(value));
});
};
export const appendSanitizedProviderTypeFilters = (
url: URL,
filters: ProviderFilters = {},
options: AppendSanitizedProviderTypeFiltersOptions = {},
): void =>
appendSanitizedProviderFilters(url, filters, {
...options,
ensuredInFilterKey: PROVIDER_TYPE_IN_FILTER_KEY,
});
export const appendSanitizedProviderInFilters = (
url: URL,
filters: ProviderFilters = {},
options: AppendSanitizedProviderTypeFiltersOptions = {},
): void =>
appendSanitizedProviderFilters(url, filters, {
...options,
ensuredInFilterKey: PROVIDER_IN_FILTER_KEY,
});