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
In Prowler Cloud, the Attack Paths query selector now lists only parameterless queries that returned data for the selected scan, using the precomputed per-query result summary from the queries API. Parameterized and custom queries are always shown (they run live on demand), and so are queries that errored during precompute or a scan predating the precompute step -- a query is hidden only when the scan computed it and found it empty. Gated behind NEXT_PUBLIC_IS_CLOUD_ENV: self-hosted deployments, whose API does not precompute result summaries, keep showing every query as before. Refs PROWLER-2195
This commit is contained in:
@@ -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,28 @@ 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
|
||||
* (`NEXT_PUBLIC_IS_CLOUD_ENV !== "true"`) 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".
|
||||
*/
|
||||
export 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 +59,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,15 @@ export interface AttackPathQueryDocumentationLink {
|
||||
link: string;
|
||||
}
|
||||
|
||||
// Precomputed per-query result summary from the scan (Prowler Cloud only).
|
||||
// `has_data` is null when the query errored; the whole object is null/absent
|
||||
// when the query was never precomputed (parameterized, a scan predating the
|
||||
// precompute step, or one the bounded pass did not reach).
|
||||
export interface AttackPathQueryResultSummary {
|
||||
status: "ok" | "error";
|
||||
has_data: boolean | null;
|
||||
}
|
||||
|
||||
export interface AttackPathQueryAttributes {
|
||||
name: string;
|
||||
short_description: string;
|
||||
@@ -137,6 +146,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