mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-24 04:51:51 +00:00
feat(ui): filter empty Attack Paths queries from the selector in Cloud (#12010)
This commit is contained in:
committed by
GitHub
parent
9f5ef80e69
commit
fb75146e34
@@ -1,12 +1,26 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { DOCS_URLS } from "@/lib/external-urls";
|
||||
import { isCloud } from "@/lib/shared/env";
|
||||
import {
|
||||
ATTACK_PATH_QUERY_IDS,
|
||||
type AttackPathQuery,
|
||||
type AttackPathQueryParameter,
|
||||
type AttackPathQueryResultSummary,
|
||||
} from "@/types/attack-paths";
|
||||
|
||||
import { buildAttackPathQueries } from "./queries.adapter";
|
||||
import {
|
||||
adaptAttackPathQueriesResponse,
|
||||
buildAttackPathQueries,
|
||||
} from "./queries.adapter";
|
||||
|
||||
vi.mock("@/lib/shared/env", () => ({ isCloud: vi.fn() }));
|
||||
|
||||
// Empty-query filtering only applies in Prowler Cloud; default the flag on for
|
||||
// the filtering suite and cover the self-hosted (flag off) case explicitly.
|
||||
beforeEach(() => {
|
||||
vi.mocked(isCloud).mockReturnValue(true);
|
||||
});
|
||||
|
||||
const presetQuery: AttackPathQuery = {
|
||||
type: "attack-paths-scans",
|
||||
@@ -21,6 +35,95 @@ const presetQuery: AttackPathQuery = {
|
||||
},
|
||||
};
|
||||
|
||||
const makeQuery = (
|
||||
id: string,
|
||||
overrides: {
|
||||
result_summary?: AttackPathQueryResultSummary | null;
|
||||
parameters?: AttackPathQueryParameter[];
|
||||
} = {},
|
||||
): AttackPathQuery => ({
|
||||
type: "attack-paths-scans",
|
||||
id,
|
||||
attributes: {
|
||||
name: id,
|
||||
short_description: "",
|
||||
description: "",
|
||||
provider: "aws",
|
||||
attribution: null,
|
||||
parameters: overrides.parameters ?? [],
|
||||
result_summary: overrides.result_summary,
|
||||
},
|
||||
});
|
||||
|
||||
describe("adaptAttackPathQueriesResponse filtering", () => {
|
||||
it("keeps only queries with data, plus everything without a definite empty verdict", () => {
|
||||
const response = {
|
||||
data: [
|
||||
makeQuery("has-data", {
|
||||
result_summary: { status: "ok", has_data: true },
|
||||
}),
|
||||
makeQuery("empty", {
|
||||
result_summary: { status: "ok", has_data: false },
|
||||
}),
|
||||
makeQuery("errored", {
|
||||
result_summary: { status: "error", has_data: null },
|
||||
}),
|
||||
makeQuery("parameterized", {
|
||||
parameters: [
|
||||
{
|
||||
name: "ip",
|
||||
label: "IP",
|
||||
data_type: "string",
|
||||
} as AttackPathQueryParameter,
|
||||
],
|
||||
result_summary: null,
|
||||
}),
|
||||
makeQuery("no-summary"), // scan predating the precompute step
|
||||
],
|
||||
};
|
||||
|
||||
const ids = adaptAttackPathQueriesResponse(response).data.map((q) => q.id);
|
||||
|
||||
// the only one hidden is the parameterless query known to be empty
|
||||
expect(ids).toEqual(["has-data", "errored", "parameterized", "no-summary"]);
|
||||
expect(ids).not.toContain("empty");
|
||||
});
|
||||
|
||||
it("returns an empty list for a missing response", () => {
|
||||
expect(adaptAttackPathQueriesResponse(undefined).data).toEqual([]);
|
||||
});
|
||||
|
||||
it("updates the pagination count to the filtered length", () => {
|
||||
const response = {
|
||||
data: [
|
||||
makeQuery("a", { result_summary: { status: "ok", has_data: true } }),
|
||||
makeQuery("b", { result_summary: { status: "ok", has_data: false } }),
|
||||
],
|
||||
};
|
||||
|
||||
const { metadata } = adaptAttackPathQueriesResponse(response);
|
||||
expect(metadata?.pagination.count).toBe(1);
|
||||
});
|
||||
|
||||
it("shows every query when not running in Cloud (feature flag off)", () => {
|
||||
vi.mocked(isCloud).mockReturnValue(false);
|
||||
const response = {
|
||||
data: [
|
||||
makeQuery("has-data", {
|
||||
result_summary: { status: "ok", has_data: true },
|
||||
}),
|
||||
makeQuery("empty", {
|
||||
result_summary: { status: "ok", has_data: false },
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
const ids = adaptAttackPathQueriesResponse(response).data.map((q) => q.id);
|
||||
// self-hosted keeps current behaviour: no filtering, even the empty one
|
||||
expect(ids).toEqual(["has-data", "empty"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildAttackPathQueries", () => {
|
||||
it("prepends a custom query that links to the Prowler documentation", () => {
|
||||
// When
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { DOCS_URLS } from "@/lib/external-urls";
|
||||
import { isCloud } from "@/lib/shared/env";
|
||||
import { MetaDataProps } from "@/types";
|
||||
import {
|
||||
ATTACK_PATH_QUERY_IDS,
|
||||
@@ -19,6 +20,27 @@ import {
|
||||
* - Maintains separation of concerns between API layer and business logic
|
||||
*/
|
||||
|
||||
/**
|
||||
* Decide whether a query should appear in the selector.
|
||||
*
|
||||
* Empty-query filtering is a Prowler Cloud feature: outside Cloud
|
||||
* (`isCloud()` is false) the queries API does not precompute result summaries,
|
||||
* so every query is shown, matching current self-hosted behaviour.
|
||||
*
|
||||
* In Cloud, hide only queries the scan precomputed and found empty
|
||||
* (`has_data === false`). Everything else stays visible: parameterized queries
|
||||
* (no summary, run live on demand), queries that errored during precompute
|
||||
* (`has_data` null), and scans that predate the precompute step (no summary at
|
||||
* all). A missing/null summary is therefore never treated as "empty".
|
||||
*/
|
||||
function shouldShowQuery(query: AttackPathQuery): boolean {
|
||||
if (!isCloud()) {
|
||||
return true;
|
||||
}
|
||||
const summary = query.attributes.result_summary;
|
||||
return !summary || summary.has_data !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapt attack path queries response with enriched data
|
||||
*
|
||||
@@ -36,7 +58,7 @@ export function adaptAttackPathQueriesResponse(
|
||||
}
|
||||
|
||||
// Enrich query data with computed properties
|
||||
const enrichedData = response.data.map((query) => ({
|
||||
const enrichedData = response.data.filter(shouldShowQuery).map((query) => ({
|
||||
...query,
|
||||
// Can add computed properties here, e.g.:
|
||||
// parameterCount: query.attributes.parameters.length,
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
In Prowler Cloud, the Attack Paths query selector now lists only queries that returned data for the selected scan, hiding empty ones
|
||||
@@ -129,6 +129,20 @@ export interface AttackPathQueryDocumentationLink {
|
||||
link: string;
|
||||
}
|
||||
|
||||
export const ATTACK_PATH_QUERY_STATUSES = {
|
||||
OK: "ok",
|
||||
ERROR: "error",
|
||||
} as const;
|
||||
|
||||
export type AttackPathQueryStatus =
|
||||
(typeof ATTACK_PATH_QUERY_STATUSES)[keyof typeof ATTACK_PATH_QUERY_STATUSES];
|
||||
|
||||
// Cloud-only precomputed query summary. Missing/null means no definitive empty verdict.
|
||||
export interface AttackPathQueryResultSummary {
|
||||
status: AttackPathQueryStatus;
|
||||
has_data: boolean | null;
|
||||
}
|
||||
|
||||
export interface AttackPathQueryAttributes {
|
||||
name: string;
|
||||
short_description: string;
|
||||
@@ -137,6 +151,7 @@ export interface AttackPathQueryAttributes {
|
||||
parameters: AttackPathQueryParameter[];
|
||||
attribution: AttackPathQueryAttribution | null;
|
||||
documentation_link?: AttackPathQueryDocumentationLink | null;
|
||||
result_summary?: AttackPathQueryResultSummary | null;
|
||||
}
|
||||
|
||||
export interface AttackPathQuery {
|
||||
|
||||
Reference in New Issue
Block a user