mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-03-22 03:08:23 +00:00
feat(attack-paths): add graph_data_ready field to decouple query availability from scan state (#10089)
Co-authored-by: Alan Buscaglia <gentlemanprogramming@gmail.com>
This commit is contained in:
@@ -13,6 +13,7 @@ All notable changes to the **Prowler UI** are documented in this file.
|
||||
- Attack Paths: Query list now shows their name and short description, when one is selected it also shows a longer description and an attribution if it has it [(#9983)](https://github.com/prowler-cloud/prowler/pull/9983)
|
||||
- Updated GitHub provider form placeholder to clarify both username and organization names are valid inputs [(#9830)](https://github.com/prowler-cloud/prowler/pull/9830)
|
||||
- CSA CCM detailed view and small fix related with `Top Failed Sections` width [(#10018)](https://github.com/prowler-cloud/prowler/pull/10018)
|
||||
- Attack Paths: Show scan data availability status with badges and tooltips, allow selecting scans for querying while a new scan is in progress [(#10089)](https://github.com/prowler-cloud/prowler/pull/10089)
|
||||
|
||||
### 🔐 Security
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ import {
|
||||
} from "@/components/ui/table";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { ProviderType } from "@/types";
|
||||
import type { AttackPathScan } from "@/types/attack-paths";
|
||||
import type { AttackPathScan, ScanState } from "@/types/attack-paths";
|
||||
import { SCAN_STATES } from "@/types/attack-paths";
|
||||
|
||||
import { ScanStatusBadge } from "./scan-status-badge";
|
||||
@@ -42,6 +42,11 @@ interface ScanListTableProps {
|
||||
const TABLE_COLUMN_COUNT = 6;
|
||||
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 baseLinkClass =
|
||||
"relative block rounded border-0 bg-transparent px-3 py-1.5 text-button-primary outline-none transition-all duration-300 hover:bg-bg-neutral-tertiary hover:text-text-neutral-primary focus:shadow-none dark:hover:bg-bg-neutral-secondary dark:hover:text-text-neutral-primary";
|
||||
@@ -77,20 +82,17 @@ export const ScanListTable = ({ scans }: ScanListTableProps) => {
|
||||
};
|
||||
|
||||
const isSelectDisabled = (scan: AttackPathScan) => {
|
||||
return (
|
||||
scan.attributes.state !== SCAN_STATES.COMPLETED ||
|
||||
selectedScanId === scan.id
|
||||
);
|
||||
return !scan.attributes.graph_data_ready || selectedScanId === scan.id;
|
||||
};
|
||||
|
||||
const getSelectButtonLabel = (scan: AttackPathScan) => {
|
||||
if (selectedScanId === scan.id) {
|
||||
return "Selected";
|
||||
}
|
||||
if (scan.attributes.state === SCAN_STATES.SCHEDULED) {
|
||||
return "Scheduled";
|
||||
if (scan.attributes.graph_data_ready) {
|
||||
return "Select";
|
||||
}
|
||||
if (scan.attributes.state === SCAN_STATES.EXECUTING) {
|
||||
if (WAITING_STATES.includes(scan.attributes.state)) {
|
||||
return "Waiting...";
|
||||
}
|
||||
if (scan.attributes.state === SCAN_STATES.FAILED) {
|
||||
@@ -184,6 +186,7 @@ export const ScanListTable = ({ scans }: ScanListTableProps) => {
|
||||
<ScanStatusBadge
|
||||
status={scan.attributes.state}
|
||||
progress={scan.attributes.progress}
|
||||
graphDataReady={scan.attributes.graph_data_ready}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
@@ -342,8 +345,8 @@ export const ScanListTable = ({ scans }: ScanListTableProps) => {
|
||||
)}
|
||||
</div>
|
||||
<p className="text-text-neutral-secondary dark:text-text-neutral-secondary mt-6 text-xs">
|
||||
Only Attack Paths scans with "Completed" status can be
|
||||
selected. Scans in progress will update automatically.
|
||||
Scans can be selected when data is available. A new scan does not
|
||||
interrupt access to existing data.
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -3,57 +3,94 @@
|
||||
import { Loader2 } from "lucide-react";
|
||||
|
||||
import { Badge } from "@/components/shadcn/badge/badge";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/shadcn/tooltip";
|
||||
import type { ScanState } from "@/types/attack-paths";
|
||||
import { SCAN_STATES } from "@/types/attack-paths";
|
||||
|
||||
const BADGE_CONFIG: Record<
|
||||
ScanState,
|
||||
{ className: string; label: string; showGraphDot: boolean }
|
||||
> = {
|
||||
[SCAN_STATES.SCHEDULED]: {
|
||||
className: "bg-bg-neutral-tertiary text-text-neutral-primary",
|
||||
label: "Scheduled",
|
||||
showGraphDot: true,
|
||||
},
|
||||
[SCAN_STATES.AVAILABLE]: {
|
||||
className: "bg-bg-neutral-tertiary text-text-neutral-primary",
|
||||
label: "Queued",
|
||||
showGraphDot: true,
|
||||
},
|
||||
[SCAN_STATES.EXECUTING]: {
|
||||
className: "bg-bg-warning-secondary text-text-neutral-primary",
|
||||
label: "In Progress",
|
||||
showGraphDot: false,
|
||||
},
|
||||
[SCAN_STATES.COMPLETED]: {
|
||||
className: "bg-bg-pass-secondary text-text-success-primary",
|
||||
label: "Completed",
|
||||
showGraphDot: false,
|
||||
},
|
||||
[SCAN_STATES.FAILED]: {
|
||||
className: "bg-bg-fail-secondary text-text-error-primary",
|
||||
label: "Failed",
|
||||
showGraphDot: true,
|
||||
},
|
||||
};
|
||||
|
||||
interface ScanStatusBadgeProps {
|
||||
status: ScanState;
|
||||
progress?: number;
|
||||
graphDataReady?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Status badge for attack path scan status
|
||||
* Shows visual indicator and text for scan progress
|
||||
*/
|
||||
export const ScanStatusBadge = ({
|
||||
status,
|
||||
progress = 0,
|
||||
graphDataReady = false,
|
||||
}: ScanStatusBadgeProps) => {
|
||||
if (status === "scheduled") {
|
||||
return (
|
||||
<Badge className="bg-bg-neutral-tertiary text-text-neutral-primary gap-2">
|
||||
<span>Scheduled</span>
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
const config = BADGE_CONFIG[status];
|
||||
|
||||
if (status === "available") {
|
||||
return (
|
||||
<Badge className="bg-bg-neutral-tertiary text-text-neutral-primary gap-2">
|
||||
<span>Queued</span>
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
const graphDot = graphDataReady && config.showGraphDot && (
|
||||
<span className="inline-block size-2 rounded-full bg-green-500" />
|
||||
);
|
||||
|
||||
if (status === "executing") {
|
||||
return (
|
||||
<Badge className="bg-bg-warning-secondary text-text-neutral-primary gap-2">
|
||||
<Loader2 size={14} className="animate-spin" />
|
||||
<span>In Progress ({progress}%)</span>
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
const tooltipText = graphDataReady
|
||||
? "Graph available"
|
||||
: status === SCAN_STATES.FAILED || status === SCAN_STATES.COMPLETED
|
||||
? "Graph not available"
|
||||
: "Graph not available yet";
|
||||
|
||||
if (status === "completed") {
|
||||
return (
|
||||
<Badge className="bg-bg-pass-secondary text-text-success-primary gap-2">
|
||||
<span>Completed</span>
|
||||
</Badge>
|
||||
const icon =
|
||||
status === SCAN_STATES.EXECUTING ? (
|
||||
<Loader2
|
||||
size={14}
|
||||
className={
|
||||
graphDataReady ? "animate-spin text-green-500" : "animate-spin"
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
graphDot
|
||||
);
|
||||
}
|
||||
|
||||
const label =
|
||||
status === SCAN_STATES.EXECUTING
|
||||
? `${config.label} (${progress}%)`
|
||||
: config.label;
|
||||
|
||||
return (
|
||||
<Badge className="bg-bg-fail-secondary text-text-error-primary gap-2">
|
||||
<span>Failed</span>
|
||||
</Badge>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Badge className={`${config.className} gap-2`}>
|
||||
{icon}
|
||||
<span>{label}</span>
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{tooltipText}</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -43,6 +43,7 @@ export type ProviderType = (typeof PROVIDER_TYPES)[keyof typeof PROVIDER_TYPES];
|
||||
export interface AttackPathScanAttributes {
|
||||
state: ScanState;
|
||||
progress: number;
|
||||
graph_data_ready: boolean;
|
||||
provider_alias: string;
|
||||
provider_type: ProviderType;
|
||||
provider_uid: string;
|
||||
|
||||
@@ -1,93 +1,106 @@
|
||||
import { PaginationLinks, RelationshipWrapper } from "./attack-paths";
|
||||
import { ProviderType } from "./providers";
|
||||
|
||||
export interface ScannerArgs {
|
||||
only_logs?: boolean;
|
||||
excluded_checks?: string[];
|
||||
aws_retries_max_attempts?: number;
|
||||
}
|
||||
|
||||
export const SCAN_TRIGGER = {
|
||||
SCHEDULED: "scheduled",
|
||||
MANUAL: "manual",
|
||||
} as const;
|
||||
|
||||
export type ScanTrigger = (typeof SCAN_TRIGGER)[keyof typeof SCAN_TRIGGER];
|
||||
|
||||
export const SCAN_STATE = {
|
||||
AVAILABLE: "available",
|
||||
SCHEDULED: "scheduled",
|
||||
EXECUTING: "executing",
|
||||
COMPLETED: "completed",
|
||||
FAILED: "failed",
|
||||
CANCELLED: "cancelled",
|
||||
} as const;
|
||||
|
||||
export type ScanState = (typeof SCAN_STATE)[keyof typeof SCAN_STATE];
|
||||
|
||||
export interface ScanAttributes {
|
||||
name: string;
|
||||
trigger: ScanTrigger;
|
||||
state: ScanState;
|
||||
unique_resource_count: number;
|
||||
progress: number;
|
||||
scanner_args: ScannerArgs | null;
|
||||
duration: number;
|
||||
started_at: string;
|
||||
inserted_at: string;
|
||||
completed_at: string;
|
||||
scheduled_at: string;
|
||||
next_scan_at: string;
|
||||
}
|
||||
|
||||
export interface ScanRelationships {
|
||||
provider: RelationshipWrapper;
|
||||
task: RelationshipWrapper;
|
||||
}
|
||||
|
||||
export interface ScanProviderInfo {
|
||||
provider: ProviderType;
|
||||
uid: string;
|
||||
alias: string;
|
||||
}
|
||||
|
||||
export interface ScanProps {
|
||||
type: "scans";
|
||||
id: string;
|
||||
attributes: {
|
||||
name: string;
|
||||
trigger: "scheduled" | "manual";
|
||||
state:
|
||||
| "available"
|
||||
| "scheduled"
|
||||
| "executing"
|
||||
| "completed"
|
||||
| "failed"
|
||||
| "cancelled";
|
||||
unique_resource_count: number;
|
||||
progress: number;
|
||||
scanner_args: {
|
||||
only_logs?: boolean;
|
||||
excluded_checks?: string[];
|
||||
aws_retries_max_attempts?: number;
|
||||
} | null;
|
||||
duration: number;
|
||||
started_at: string;
|
||||
inserted_at: string;
|
||||
completed_at: string;
|
||||
scheduled_at: string;
|
||||
next_scan_at: string;
|
||||
};
|
||||
relationships: {
|
||||
provider: {
|
||||
data: {
|
||||
id: string;
|
||||
type: "providers";
|
||||
};
|
||||
};
|
||||
task: {
|
||||
data: {
|
||||
id: string;
|
||||
type: "tasks";
|
||||
};
|
||||
};
|
||||
};
|
||||
providerInfo?: {
|
||||
provider: ProviderType;
|
||||
uid: string;
|
||||
alias: string;
|
||||
};
|
||||
attributes: ScanAttributes;
|
||||
relationships: ScanRelationships;
|
||||
providerInfo?: ScanProviderInfo;
|
||||
}
|
||||
|
||||
export interface ScanEntityProviderInfo {
|
||||
provider: ProviderType;
|
||||
alias?: string;
|
||||
uid?: string;
|
||||
}
|
||||
|
||||
export interface ScanEntityAttributes {
|
||||
name?: string;
|
||||
completed_at: string;
|
||||
}
|
||||
|
||||
export interface ScanEntity {
|
||||
id: string;
|
||||
providerInfo: {
|
||||
provider: ProviderType;
|
||||
alias?: string;
|
||||
uid?: string;
|
||||
};
|
||||
attributes: {
|
||||
name?: string;
|
||||
completed_at: string;
|
||||
};
|
||||
providerInfo: ScanEntityProviderInfo;
|
||||
attributes: ScanEntityAttributes;
|
||||
}
|
||||
|
||||
export interface ExpandedScanData extends ScanProps {
|
||||
providerInfo: {
|
||||
provider: ProviderType;
|
||||
uid: string;
|
||||
alias: string;
|
||||
};
|
||||
providerInfo: ScanProviderInfo;
|
||||
}
|
||||
|
||||
export interface IncludedResource {
|
||||
type: string;
|
||||
id: string;
|
||||
attributes: any;
|
||||
relationships?: any;
|
||||
}
|
||||
|
||||
export interface ApiPagination {
|
||||
page: number;
|
||||
pages: number;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface ScansApiMeta {
|
||||
pagination: ApiPagination;
|
||||
version: string;
|
||||
}
|
||||
|
||||
export interface ScansApiResponse {
|
||||
links: {
|
||||
first: string;
|
||||
last: string;
|
||||
next: string | null;
|
||||
prev: string | null;
|
||||
};
|
||||
links: PaginationLinks;
|
||||
data: ScanProps[];
|
||||
included?: Array<{
|
||||
type: string;
|
||||
id: string;
|
||||
attributes: any;
|
||||
relationships?: any;
|
||||
}>;
|
||||
meta: {
|
||||
pagination: {
|
||||
page: number;
|
||||
pages: number;
|
||||
count: number;
|
||||
};
|
||||
version: string;
|
||||
};
|
||||
included?: IncludedResource[];
|
||||
meta: ScansApiMeta;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user