mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-04-15 00:57:55 +00:00
Compare commits
2 Commits
dependabot
...
feat/ui-at
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fcbabca2c1 | ||
|
|
ac6901d4bb |
115
.opencode/package-lock.json
generated
Normal file
115
.opencode/package-lock.json
generated
Normal file
@@ -0,0 +1,115 @@
|
||||
{
|
||||
"name": ".opencode",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"@opencode-ai/plugin": "1.3.17"
|
||||
}
|
||||
},
|
||||
"node_modules/@opencode-ai/plugin": {
|
||||
"version": "1.3.17",
|
||||
"resolved": "https://registry.npmjs.org/@opencode-ai/plugin/-/plugin-1.3.17.tgz",
|
||||
"integrity": "sha512-N5lckFtYvEu2R8K1um//MIOTHsJHniF2kHoPIWPCrxKG5Jpismt1ISGzIiU3aKI2ht/9VgcqKPC5oZFLdmpxPw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "1.3.17",
|
||||
"zod": "4.1.8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentui/core": ">=0.1.96",
|
||||
"@opentui/solid": ">=0.1.96"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@opentui/core": {
|
||||
"optional": true
|
||||
},
|
||||
"@opentui/solid": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@opencode-ai/sdk": {
|
||||
"version": "1.3.17",
|
||||
"resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.3.17.tgz",
|
||||
"integrity": "sha512-2+MGgu7wynqTBwxezR01VAGhILXlpcHDY/pF7SWB87WOgLt3kD55HjKHNj6PWxyY8n575AZolR95VUC3gtwfmA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cross-spawn": "7.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
"shebang-command": "^2.0.0",
|
||||
"which": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/path-key": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"shebang-regex": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-regex": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"isexe": "^2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"node-which": "bin/node-which"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "4.1.8",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,10 @@ All notable changes to the **Prowler UI** are documented in this file.
|
||||
|
||||
## [1.23.1] (Prowler UNRELEASED)
|
||||
|
||||
### 🔄 Changed
|
||||
|
||||
- Attack Paths scan selection: contextual button labels based on graph availability, tooltips on disabled actions, green dot indicator for selectable scans, and a warning banner when viewing data from a previous scan cycle [(#10685)](https://github.com/prowler-cloud/prowler/pull/10685)
|
||||
|
||||
### 🐞 Fixed
|
||||
|
||||
- Compliance detail page header now reflects the actual provider, alias and UID of the selected scan instead of always defaulting to AWS [(#10674)](https://github.com/prowler-cloud/prowler/pull/10674)
|
||||
|
||||
@@ -24,6 +24,19 @@ vi.mock("next/navigation", () => ({
|
||||
useSearchParams: () => navigationState.searchParams,
|
||||
}));
|
||||
|
||||
vi.mock("@/components/shadcn/tooltip", () => ({
|
||||
Tooltip: ({ children }: { children: ReactNode }) => <>{children}</>,
|
||||
TooltipTrigger: ({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
asChild?: boolean;
|
||||
}) => <>{children}</>,
|
||||
TooltipContent: ({ children }: { children: ReactNode }) => (
|
||||
<span data-testid="tooltip-content">{children}</span>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/ui/entities/entity-info", () => ({
|
||||
EntityInfo: ({
|
||||
entityAlias,
|
||||
@@ -203,4 +216,93 @@ describe("ScanListTable", () => {
|
||||
expect(button).toBeDisabled();
|
||||
expect(button).toHaveTextContent("Failed");
|
||||
});
|
||||
|
||||
it("shows 'Scheduled' label for a scheduled scan without graph data", () => {
|
||||
const scheduledScan: AttackPathScan = {
|
||||
...createScan(1),
|
||||
attributes: {
|
||||
...createScan(1).attributes,
|
||||
state: "scheduled",
|
||||
progress: 0,
|
||||
graph_data_ready: false,
|
||||
completed_at: null,
|
||||
duration: null,
|
||||
},
|
||||
};
|
||||
|
||||
render(<ScanListTable scans={[scheduledScan]} />);
|
||||
|
||||
const button = screen.getByRole("button", { name: "Select scan" });
|
||||
expect(button).toBeDisabled();
|
||||
expect(button).toHaveTextContent("Scheduled");
|
||||
});
|
||||
|
||||
it("shows 'Running...' label for an executing scan without graph data", () => {
|
||||
const executingScan: AttackPathScan = {
|
||||
...createScan(1),
|
||||
attributes: {
|
||||
...createScan(1).attributes,
|
||||
state: "executing",
|
||||
progress: 45,
|
||||
graph_data_ready: false,
|
||||
completed_at: null,
|
||||
duration: null,
|
||||
},
|
||||
};
|
||||
|
||||
render(<ScanListTable scans={[executingScan]} />);
|
||||
|
||||
const button = screen.getByRole("button", { name: "Select scan" });
|
||||
expect(button).toBeDisabled();
|
||||
expect(button).toHaveTextContent("Running...");
|
||||
});
|
||||
|
||||
it("enables Select for a scheduled scan when graph data is ready from a previous cycle", async () => {
|
||||
const user = userEvent.setup();
|
||||
const scheduledWithGraph: AttackPathScan = {
|
||||
...createScan(1),
|
||||
attributes: {
|
||||
...createScan(1).attributes,
|
||||
state: "scheduled",
|
||||
progress: 0,
|
||||
graph_data_ready: true,
|
||||
},
|
||||
};
|
||||
|
||||
render(<ScanListTable scans={[scheduledWithGraph]} />);
|
||||
|
||||
const button = screen.getByRole("button", { name: "Select scan" });
|
||||
expect(button).toBeEnabled();
|
||||
expect(button).toHaveTextContent("Select");
|
||||
|
||||
await user.click(button);
|
||||
|
||||
expect(pushMock).toHaveBeenCalledWith(
|
||||
"/attack-paths?scanPage=1&scanPageSize=5&scanId=scan-1",
|
||||
);
|
||||
});
|
||||
|
||||
it("shows a green dot next to the account name when graph data is ready", () => {
|
||||
render(<ScanListTable scans={[createScan(1)]} />);
|
||||
|
||||
const dot = screen.getByLabelText("Graph data available");
|
||||
expect(dot).toBeInTheDocument();
|
||||
expect(dot).toHaveClass("bg-green-500");
|
||||
});
|
||||
|
||||
it("does not show a green dot when graph data is not ready", () => {
|
||||
const noGraphScan: AttackPathScan = {
|
||||
...createScan(1),
|
||||
attributes: {
|
||||
...createScan(1).attributes,
|
||||
graph_data_ready: false,
|
||||
},
|
||||
};
|
||||
|
||||
render(<ScanListTable scans={[noGraphScan]} />);
|
||||
|
||||
expect(
|
||||
screen.queryByLabelText("Graph data available"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,12 +4,18 @@ import { ColumnDef } from "@tanstack/react-table";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
|
||||
import { Button } from "@/components/shadcn/button/button";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/shadcn/tooltip";
|
||||
import { DateWithTime } from "@/components/ui/entities/date-with-time";
|
||||
import { EntityInfo } from "@/components/ui/entities/entity-info";
|
||||
import { DataTable, DataTableColumnHeader } from "@/components/ui/table";
|
||||
import { formatDuration } from "@/lib/date-utils";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { MetaDataProps, ProviderType } from "@/types";
|
||||
import type { AttackPathScan, ScanState } from "@/types/attack-paths";
|
||||
import type { AttackPathScan } from "@/types/attack-paths";
|
||||
import { SCAN_STATES } from "@/types/attack-paths";
|
||||
|
||||
import { ScanStatusBadge } from "./scan-status-badge";
|
||||
@@ -20,12 +26,6 @@ interface ScanListTableProps {
|
||||
|
||||
const DEFAULT_PAGE_SIZE = 5;
|
||||
const PAGE_SIZE_OPTIONS = [2, 5, 10, 15];
|
||||
const WAITING_STATES: readonly ScanState[] = [
|
||||
SCAN_STATES.SCHEDULED,
|
||||
SCAN_STATES.AVAILABLE,
|
||||
SCAN_STATES.EXECUTING,
|
||||
];
|
||||
|
||||
const parsePageParam = (value: string | null, fallback: number) => {
|
||||
if (!value) return fallback;
|
||||
|
||||
@@ -57,8 +57,16 @@ const getSelectButtonLabel = (
|
||||
return "Select";
|
||||
}
|
||||
|
||||
if (WAITING_STATES.includes(scan.attributes.state)) {
|
||||
return "Waiting...";
|
||||
if (scan.attributes.state === SCAN_STATES.SCHEDULED) {
|
||||
return "Scheduled";
|
||||
}
|
||||
|
||||
if (scan.attributes.state === SCAN_STATES.AVAILABLE) {
|
||||
return "Queued";
|
||||
}
|
||||
|
||||
if (scan.attributes.state === SCAN_STATES.EXECUTING) {
|
||||
return "Running...";
|
||||
}
|
||||
|
||||
if (scan.attributes.state === SCAN_STATES.FAILED) {
|
||||
@@ -68,6 +76,30 @@ const getSelectButtonLabel = (
|
||||
return "Select";
|
||||
};
|
||||
|
||||
const getDisabledTooltip = (scan: AttackPathScan): string | null => {
|
||||
if (scan.attributes.graph_data_ready) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (scan.attributes.state === SCAN_STATES.SCHEDULED) {
|
||||
return "Graph will be available once this scan runs and completes.";
|
||||
}
|
||||
|
||||
if (scan.attributes.state === SCAN_STATES.AVAILABLE) {
|
||||
return "This scan is queued. Graph will be available once it completes.";
|
||||
}
|
||||
|
||||
if (scan.attributes.state === SCAN_STATES.EXECUTING) {
|
||||
return "Scan is running. Graph will be available once it completes.";
|
||||
}
|
||||
|
||||
if (scan.attributes.state === SCAN_STATES.FAILED) {
|
||||
return "This scan failed. No graph data is available.";
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const getSelectedRowSelection = (
|
||||
scans: AttackPathScan[],
|
||||
selectedScanId: string | null,
|
||||
@@ -108,11 +140,26 @@ const getColumns = ({
|
||||
<DataTableColumnHeader column={column} title="Account" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<EntityInfo
|
||||
cloudProvider={row.original.attributes.provider_type as ProviderType}
|
||||
entityAlias={row.original.attributes.provider_alias}
|
||||
entityId={row.original.attributes.provider_uid}
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className={cn(
|
||||
"inline-block size-2 shrink-0 rounded-full",
|
||||
row.original.attributes.graph_data_ready
|
||||
? "bg-green-500"
|
||||
: "bg-transparent",
|
||||
)}
|
||||
aria-label={
|
||||
row.original.attributes.graph_data_ready
|
||||
? "Graph data available"
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<EntityInfo
|
||||
cloudProvider={row.original.attributes.provider_type as ProviderType}
|
||||
entityAlias={row.original.attributes.provider_alias}
|
||||
entityId={row.original.attributes.provider_uid}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
enableSorting: false,
|
||||
},
|
||||
@@ -170,21 +217,37 @@ const getColumns = ({
|
||||
header: () => <span className="sr-only">Actions</span>,
|
||||
cell: ({ row }) => {
|
||||
const isDisabled = isSelectDisabled(row.original, selectedScanId);
|
||||
const tooltip = getDisabledTooltip(row.original);
|
||||
|
||||
return (
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
type="button"
|
||||
aria-label="Select scan"
|
||||
disabled={isDisabled}
|
||||
variant={isDisabled ? "secondary" : "default"}
|
||||
onClick={() => onSelectScan(row.original.id)}
|
||||
className="w-full max-w-24"
|
||||
>
|
||||
{getSelectButtonLabel(row.original, selectedScanId)}
|
||||
</Button>
|
||||
</div>
|
||||
const button = (
|
||||
<Button
|
||||
type="button"
|
||||
aria-label="Select scan"
|
||||
disabled={isDisabled}
|
||||
variant={isDisabled ? "secondary" : "default"}
|
||||
onClick={() => onSelectScan(row.original.id)}
|
||||
className="w-full max-w-24"
|
||||
>
|
||||
{getSelectButtonLabel(row.original, selectedScanId)}
|
||||
</Button>
|
||||
);
|
||||
|
||||
if (isDisabled && tooltip) {
|
||||
return (
|
||||
<div className="flex justify-end">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="w-full max-w-24" tabIndex={0}>
|
||||
{button}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{tooltip}</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <div className="flex justify-end">{button}</div>;
|
||||
},
|
||||
enableSorting: false,
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { ArrowLeft, Info, Maximize2, X } from "lucide-react";
|
||||
import { ArrowLeft, Info, Maximize2, TriangleAlert, X } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { Suspense, useEffect, useRef, useState } from "react";
|
||||
@@ -37,7 +37,7 @@ import type {
|
||||
AttackPathScan,
|
||||
GraphNode,
|
||||
} from "@/types/attack-paths";
|
||||
import { ATTACK_PATH_QUERY_IDS } from "@/types/attack-paths";
|
||||
import { ATTACK_PATH_QUERY_IDS, SCAN_STATES } from "@/types/attack-paths";
|
||||
|
||||
import {
|
||||
AttackPathGraph,
|
||||
@@ -120,6 +120,13 @@ export default function AttackPathsPage() {
|
||||
scan.attributes.state === "scheduled",
|
||||
);
|
||||
|
||||
// Detect if the selected scan is showing data from a previous cycle
|
||||
const selectedScan = scans.find((scan) => scan.id === scanId);
|
||||
const isViewingPreviousCycleData =
|
||||
selectedScan &&
|
||||
selectedScan.attributes.graph_data_ready &&
|
||||
selectedScan.attributes.state !== SCAN_STATES.COMPLETED;
|
||||
|
||||
// Callback to refresh scans (used by AutoRefresh component)
|
||||
const refreshScans = async () => {
|
||||
try {
|
||||
@@ -373,6 +380,24 @@ export default function AttackPathsPage() {
|
||||
<ScanListTable scans={scans} />
|
||||
</Suspense>
|
||||
|
||||
{/* Banner: viewing data from a previous scan cycle */}
|
||||
{isViewingPreviousCycleData && (
|
||||
<Alert
|
||||
variant="default"
|
||||
className="border-border-warning-secondary bg-bg-warning-secondary"
|
||||
>
|
||||
<TriangleAlert className="text-text-warning-primary size-4" />
|
||||
<AlertTitle>Viewing data from a previous scan</AlertTitle>
|
||||
<AlertDescription>
|
||||
This scan is currently{" "}
|
||||
{selectedScan.attributes.state === SCAN_STATES.EXECUTING
|
||||
? `running (${selectedScan.attributes.progress}%)`
|
||||
: selectedScan.attributes.state}
|
||||
. The graph data shown is from the last completed cycle.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Query Builder Section - shown only after selecting a scan */}
|
||||
{scanId && (
|
||||
<div className="minimal-scrollbar rounded-large shadow-small border-border-neutral-secondary bg-bg-neutral-secondary relative z-0 flex w-full flex-col gap-4 overflow-auto border p-4">
|
||||
|
||||
Reference in New Issue
Block a user