Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 105 additions & 2 deletions ui/actions/attack-paths/queries.adapter.test.ts
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -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
Expand Down
24 changes: 23 additions & 1 deletion ui/actions/attack-paths/queries.adapter.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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".
*/
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
*
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Describe only confirmed-empty queries as hidden.

This says only queries that returned data remain, but errored, parameterized, and precompute-missing queries intentionally remain visible. Update it to say that queries confirmed empty for the selected scan are hidden.

-In Prowler Cloud, the Attack Paths query selector now lists only queries that returned data for the selected scan, hiding empty ones
+In Prowler Cloud, the Attack Paths query selector now hides queries confirmed to have no results for the selected scan.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
In Prowler Cloud, the Attack Paths query selector now lists only queries that returned data for the selected scan, hiding empty ones
In Prowler Cloud, the Attack Paths query selector now hides queries confirmed to have no results for the selected scan.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/changelog.d/attack-paths-filter-empty-queries.added.md` at line 1, Update
the changelog statement to say that only queries confirmed empty for the
selected scan are hidden, while avoiding the broader claim that all queries
without returned data are removed.

10 changes: 10 additions & 0 deletions ui/types/attack-paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Comment on lines +136 to +138

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== ui/types/attack-paths.ts =="
nl -ba ui/types/attack-paths.ts | sed -n '1,240p'

echo
echo "== string-literal unions in ui/types =="
rg -n --glob 'ui/types/**/*.ts' '"[^"]+"\s*\|\s*"[^"]+"' ui/types

echo
echo "== const-object union patterns in ui/types =="
rg -n --glob 'ui/types/**/*.ts' 'as const;' ui/types

Repository: prowler-cloud/prowler

Length of output: 230


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="ui/types/attack-paths.ts"
if [ -f "$file" ]; then
  echo "== $file (around target lines) =="
  nl -ba "$file" | sed -n '120,180p'
else
  echo "missing file: $file" >&2
  exit 1
fi

echo
echo "== nearby union patterns in ui/types/attack-paths.ts =="
rg -n '"[^"]+"\s*\|\s*"[^"]+"' "$file" || true

echo
echo "== usages of AttackPathQueryResultSummary =="
rg -n 'AttackPathQueryResultSummary' ui || true

Repository: prowler-cloud/prowler

Length of output: 252


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== ui/types/attack-paths.ts (lines 120-180) =="
sed -n '120,180p' ui/types/attack-paths.ts | cat -n

echo
echo "== string-literal unions in ui/types =="
rg -n --glob 'ui/types/**/*.ts' '"[^"]+"\s*\|\s*"[^"]+"' ui/types || true

echo
echo "== const-object patterns in ui/types =="
rg -n --glob 'ui/types/**/*.ts' 'as const;' ui/types || true

echo
echo "== usages of AttackPathQueryResultSummary =="
rg -n 'AttackPathQueryResultSummary' ui || true

Repository: prowler-cloud/prowler

Length of output: 7091


Use the const-object pattern for status.

status: "ok" | "error" should be defined from a const object to match the UI type convention.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/types/attack-paths.ts` around lines 136 - 138, Update
AttackPathQueryResultSummary.status to use the UI’s const-object pattern: define
or reuse a const status object and derive the property type from its values
instead of declaring the inline "ok" | "error" union. Preserve the existing
allowed status values.

Sources: Coding guidelines, Path instructions

}

export interface AttackPathQueryAttributes {
name: string;
short_description: string;
Expand All @@ -137,6 +146,7 @@ export interface AttackPathQueryAttributes {
parameters: AttackPathQueryParameter[];
attribution: AttackPathQueryAttribution | null;
documentation_link?: AttackPathQueryDocumentationLink | null;
result_summary?: AttackPathQueryResultSummary | null;
}

export interface AttackPathQuery {
Expand Down
Loading