mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-08-19 09:30:21 +00:00
feat(ui): improve Lighthouse per-page suggested prompts (#12219)
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
This commit is contained in:
co-authored by
alejandrobailo
parent
8abd72e857
commit
60bc06271a
@@ -1,11 +1,26 @@
|
||||
"use client";
|
||||
|
||||
import { Cloud, FileCheck2, Network, ShieldAlert } from "lucide-react";
|
||||
import {
|
||||
Cloud,
|
||||
FileCheck2,
|
||||
type LucideIcon,
|
||||
Network,
|
||||
ShieldAlert,
|
||||
} from "lucide-react";
|
||||
import { type ReactNode, type SubmitEvent } from "react";
|
||||
|
||||
import { LighthouseIconWithAura } from "@/components/icons";
|
||||
import { Button } from "@/components/shadcn/button/button";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/shadcn/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type {
|
||||
LighthousePageSuggestion,
|
||||
LighthousePageSuggestions,
|
||||
} from "@/types/lighthouse-context";
|
||||
|
||||
import { ChatComposerPanel } from "./composer";
|
||||
import { DecryptedText } from "./decrypted-text";
|
||||
@@ -47,7 +62,7 @@ interface ChatEmptyStateProps {
|
||||
onInputChange: (value: string) => void;
|
||||
onSubmit: (event: SubmitEvent<HTMLFormElement>) => void;
|
||||
onSubmitText: (text: string) => Promise<void>;
|
||||
suggestions?: readonly string[];
|
||||
suggestions?: LighthousePageSuggestions;
|
||||
footer?: ReactNode;
|
||||
// Side-panel variant: smaller logo and static (non-animated) copy — the
|
||||
// decrypt animation reflows multi-line text in narrow widths.
|
||||
@@ -61,6 +76,9 @@ export function ChatEmptyState({
|
||||
compact = false,
|
||||
...composerPanelProps
|
||||
}: ChatEmptyStateProps) {
|
||||
const displayedSuggestions: readonly ChatSuggestion[] =
|
||||
suggestions ?? LIGHTHOUSE_V2_SUGGESTIONS;
|
||||
|
||||
return (
|
||||
<div className="flex min-h-0 flex-1 items-center justify-center px-4 py-10 md:px-8">
|
||||
<div className="mx-auto flex w-full max-w-5xl flex-col items-center gap-5">
|
||||
@@ -113,36 +131,46 @@ export function ChatEmptyState({
|
||||
<span className="text-text-neutral-secondary basis-full text-center text-sm font-medium">
|
||||
Try Lighthouse AI for...
|
||||
</span>
|
||||
{suggestions
|
||||
? suggestions.map((suggestion) => (
|
||||
<Button
|
||||
key={suggestion}
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onInputChange(suggestion)}
|
||||
>
|
||||
{suggestion}
|
||||
</Button>
|
||||
))
|
||||
: LIGHTHOUSE_V2_SUGGESTIONS.map((suggestion) => {
|
||||
const Icon = suggestion.icon;
|
||||
return (
|
||||
<Button
|
||||
key={suggestion.label}
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onInputChange(suggestion.prompt)}
|
||||
>
|
||||
<Icon className="size-4" />
|
||||
{suggestion.label}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
{displayedSuggestions.map((suggestion) => (
|
||||
<SuggestionButton
|
||||
key={suggestion.prompt}
|
||||
suggestion={suggestion}
|
||||
onSelect={onInputChange}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{footer ? <div className="w-full max-w-4xl">{footer}</div> : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface ChatSuggestion extends LighthousePageSuggestion {
|
||||
icon?: LucideIcon;
|
||||
}
|
||||
|
||||
interface SuggestionButtonProps {
|
||||
suggestion: ChatSuggestion;
|
||||
onSelect: (prompt: string) => void;
|
||||
}
|
||||
|
||||
function SuggestionButton({ suggestion, onSelect }: SuggestionButtonProps) {
|
||||
const Icon = suggestion.icon;
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onSelect(suggestion.prompt)}
|
||||
>
|
||||
{Icon ? <Icon className="size-4" /> : null}
|
||||
{suggestion.label}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent maxWidth="md">{suggestion.prompt}</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -202,6 +202,33 @@ describe("LighthousePanelChat", () => {
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows the full contextual prompt on hover and prefills it without sending", async () => {
|
||||
// Given
|
||||
const user = userEvent.setup();
|
||||
const prompt =
|
||||
"Give me the actual remediation for the top failing check here: the commands and IaC changes (if possible, with Terraform preferred), the blast radius of applying them, and what to verify afterward.";
|
||||
render(<LighthousePanelChat />);
|
||||
const suggestion = await screen.findByRole("button", {
|
||||
name: "Generate concrete remediation",
|
||||
});
|
||||
|
||||
// When
|
||||
await user.hover(suggestion);
|
||||
|
||||
// Then
|
||||
expect(await screen.findByRole("tooltip")).toHaveTextContent(prompt);
|
||||
|
||||
// When
|
||||
await user.click(suggestion);
|
||||
|
||||
// Then
|
||||
expect(screen.getByRole("textbox", { name: "Message" })).toHaveValue(
|
||||
prompt,
|
||||
);
|
||||
expect(createSessionMock).not.toHaveBeenCalled();
|
||||
expect(sendMessageMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("submits a queued contextual analysis when the panel chat becomes ready", async () => {
|
||||
// Given
|
||||
const context = {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Lighthouse contextual suggestions now show concise actions while preserving detailed prompts for chat
|
||||
@@ -0,0 +1,425 @@
|
||||
import {
|
||||
LIGHTHOUSE_PAGE_ID,
|
||||
type LighthousePageDefinitionInput,
|
||||
type LighthousePageSuggestions,
|
||||
} from "@/types/lighthouse-context";
|
||||
|
||||
const PROVIDER_SCOPE_PARAMS = [
|
||||
"filter[provider__in]",
|
||||
"filter[provider_id__in]",
|
||||
"filter[provider_uid]",
|
||||
"filter[provider_uid__in]",
|
||||
"filter[provider_type__in]",
|
||||
"filter[provider]",
|
||||
"filter[provider_type]",
|
||||
"filter[provider_groups__in]",
|
||||
] as const;
|
||||
|
||||
const COMMON_LIST_PARAMS = [
|
||||
"query",
|
||||
"search",
|
||||
"filter[search]",
|
||||
"sort",
|
||||
] as const;
|
||||
|
||||
export const LIGHTHOUSE_GLOBAL_SUGGESTIONS = [
|
||||
{
|
||||
label: "Identify biggest security risk",
|
||||
prompt:
|
||||
"Pull my findings, compliance gaps, and attack paths together into one picture. What's my single biggest security risk right now, and how confident are you?",
|
||||
},
|
||||
{
|
||||
label: "Review stale mute rules",
|
||||
prompt:
|
||||
"Which of my mute rules are hiding something that now matters — rules written for a resource that changed, or an exception nobody has revisited?",
|
||||
},
|
||||
{
|
||||
label: "Find missing alerts",
|
||||
prompt:
|
||||
"Look at my most serious findings from the last month. Which of them produced no alert from Prowler, and what rule would have caught them?",
|
||||
},
|
||||
{
|
||||
label: "Create Jira tickets",
|
||||
prompt:
|
||||
"Turn my top security work into Jira tickets: group related findings into one ticket per root cause, write each so an engineer can act on it without coming back to ask me questions, and tell me which project each should go to.",
|
||||
},
|
||||
] as const satisfies LighthousePageSuggestions;
|
||||
|
||||
export const LIGHTHOUSE_PAGE_DEFINITION_INPUTS = [
|
||||
{
|
||||
id: LIGHTHOUSE_PAGE_ID.OVERVIEW,
|
||||
label: "Overview",
|
||||
match: (pathname) => pathname === "/",
|
||||
allowedSearchParams: PROVIDER_SCOPE_PARAMS,
|
||||
suggestions: [
|
||||
{
|
||||
label: "Prioritize cross-surface risk",
|
||||
prompt:
|
||||
"Across my providers, services, and frameworks, which risk should I prioritize first, and why?",
|
||||
},
|
||||
{
|
||||
label: "Improve my ThreatScore",
|
||||
prompt:
|
||||
"According to the Prowler ThreatScore, what do I need to improve first based on its trend and weakest section, and why?",
|
||||
},
|
||||
{
|
||||
label: "Plan today's security work",
|
||||
prompt: "Build a practical security plan for today.",
|
||||
},
|
||||
{
|
||||
label: "Find coverage blind spots",
|
||||
prompt:
|
||||
"What's missing from this dashboard that should worry me — providers not onboarded, regions or services with no scan coverage, checks that never run?",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: LIGHTHOUSE_PAGE_ID.FINDINGS,
|
||||
label: "Findings",
|
||||
match: (pathname) => pathname === "/findings",
|
||||
allowedSearchParams: [
|
||||
...PROVIDER_SCOPE_PARAMS,
|
||||
...COMMON_LIST_PARAMS,
|
||||
"filter[region__in]",
|
||||
"filter[service__in]",
|
||||
"filter[severity__in]",
|
||||
"filter[status__in]",
|
||||
"filter[delta]",
|
||||
"filter[delta__in]",
|
||||
"filter[resource_type__in]",
|
||||
"filter[category__in]",
|
||||
"filter[resource_groups__in]",
|
||||
"filter[scan]",
|
||||
"filter[scan__in]",
|
||||
"filter[scan_id]",
|
||||
"filter[scan_id__in]",
|
||||
"filter[inserted_at]",
|
||||
"filter[muted]",
|
||||
],
|
||||
suggestions: [
|
||||
{
|
||||
label: "Generate concrete remediation",
|
||||
prompt:
|
||||
"Give me the actual remediation for the top failing check here: the commands and IaC changes (if possible, with Terraform preferred), the blast radius of applying them, and what to verify afterward.",
|
||||
},
|
||||
{
|
||||
label: "Re-rank by real exposure",
|
||||
prompt:
|
||||
"Severity here is the check's label, not my actual risk. Re-rank these by real exposure, whether the affected resource is internet-facing, production, or holds data; and give me the true top five.",
|
||||
},
|
||||
{
|
||||
label: "Group by shared fix",
|
||||
prompt:
|
||||
"Group these by the underlying change that fixes them, the same IAM policy, module, or baseline; not by check. Which single change clears the most findings?",
|
||||
},
|
||||
{
|
||||
label: "Build remediation plan",
|
||||
prompt: "Create a remediation plan for this findings view.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: LIGHTHOUSE_PAGE_ID.RESOURCES,
|
||||
label: "Resources",
|
||||
match: (pathname) => pathname === "/resources",
|
||||
allowedSearchParams: [
|
||||
...PROVIDER_SCOPE_PARAMS,
|
||||
...COMMON_LIST_PARAMS,
|
||||
"filter[region__in]",
|
||||
"filter[service__in]",
|
||||
"filter[type__in]",
|
||||
"filter[groups__in]",
|
||||
],
|
||||
suggestions: [
|
||||
{
|
||||
label: "Choose attacker's first target",
|
||||
prompt:
|
||||
"What's the first thing among these resources you'd target in an attack, what would it give you, and how far could you pivot from there?",
|
||||
},
|
||||
{
|
||||
label: "Find architectural security debt",
|
||||
prompt:
|
||||
"What does this inventory tell you about how this environment was built, and where does that design create security debt?",
|
||||
},
|
||||
{
|
||||
label: "Create team action lists",
|
||||
prompt:
|
||||
"Turn this inventory into a per-team action list. Infer likely owners from tags and naming, and give each team their top three fixes.",
|
||||
},
|
||||
{
|
||||
label: "Build hardening plan",
|
||||
prompt: "Recommend a hardening plan for this resource scope.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: LIGHTHOUSE_PAGE_ID.COMPLIANCE_DETAIL,
|
||||
label: "Compliance detail",
|
||||
match: (pathname) => pathname.startsWith("/compliance/"),
|
||||
allowedSearchParams: [
|
||||
...PROVIDER_SCOPE_PARAMS,
|
||||
"scanId",
|
||||
"scan_id",
|
||||
"complianceId",
|
||||
"section",
|
||||
"mode",
|
||||
"version",
|
||||
"filter[cis_profile_level]",
|
||||
"filter[region__in]",
|
||||
"filter[status__in]",
|
||||
],
|
||||
suggestions: [
|
||||
{
|
||||
label: "Prioritize failed requirements",
|
||||
prompt: "Which failed requirements need attention first?",
|
||||
},
|
||||
{
|
||||
label: "Identify manual evidence",
|
||||
prompt:
|
||||
"Which requirements here can't be satisfied by scanning at all? Tell me exactly what evidence to collect, who owns it, and what good evidence looks like.",
|
||||
},
|
||||
{
|
||||
label: "Rehearse weakest-section audit",
|
||||
prompt:
|
||||
"Play the auditor for my weakest section. What will you ask me, what evidence will you want to see, and where am I going to struggle?",
|
||||
},
|
||||
{
|
||||
label: "Improve framework score",
|
||||
prompt: "Create a plan to improve this framework score.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: LIGHTHOUSE_PAGE_ID.COMPLIANCE,
|
||||
label: "Compliance",
|
||||
match: (pathname) => pathname === "/compliance",
|
||||
allowedSearchParams: [
|
||||
...PROVIDER_SCOPE_PARAMS,
|
||||
"tab",
|
||||
"scanId",
|
||||
"scan_id",
|
||||
"framework",
|
||||
"version",
|
||||
"mode",
|
||||
"section",
|
||||
"filter[compliance_id]",
|
||||
"filter[region__in]",
|
||||
],
|
||||
suggestions: [
|
||||
{
|
||||
label: "Summarize compliance gaps",
|
||||
prompt: "Summarize the most important compliance gaps.",
|
||||
},
|
||||
{
|
||||
label: "Prioritize frameworks",
|
||||
prompt: "Which frameworks should I prioritize?",
|
||||
},
|
||||
{
|
||||
label: "Explain compliance score",
|
||||
prompt: "Explain the visible compliance score.",
|
||||
},
|
||||
{
|
||||
label: "Estimate path to passing",
|
||||
prompt:
|
||||
"How much work is it to take my weakest framework to passing? Break it down by provider account into a few sprints, with what lands first.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: LIGHTHOUSE_PAGE_ID.ATTACK_PATHS,
|
||||
label: "Attack Paths",
|
||||
match: (pathname) => pathname.startsWith("/attack-paths"),
|
||||
allowedSearchParams: ["scanId", "queryId"],
|
||||
suggestions: [
|
||||
{
|
||||
label: "Explain current attack path",
|
||||
prompt: "Explain the current attack path.",
|
||||
},
|
||||
{
|
||||
label: "Find critical graph nodes",
|
||||
prompt: "Which nodes are most critical in this graph?",
|
||||
},
|
||||
{
|
||||
label: "Choose first break point",
|
||||
prompt: "Where should I break this attack path first?",
|
||||
},
|
||||
{
|
||||
label: "Recommend attack path fixes",
|
||||
prompt: "Recommend remediations for the current attack path.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: LIGHTHOUSE_PAGE_ID.SCANS,
|
||||
label: "Scans",
|
||||
match: (pathname) => pathname.startsWith("/scans"),
|
||||
allowedSearchParams: [
|
||||
...PROVIDER_SCOPE_PARAMS,
|
||||
...COMMON_LIST_PARAMS,
|
||||
"tab",
|
||||
"scanId",
|
||||
"filter[state]",
|
||||
"filter[state__in]",
|
||||
"filter[trigger]",
|
||||
],
|
||||
suggestions: [
|
||||
{
|
||||
label: "Summarize scan activity",
|
||||
prompt: "Summarize recent scan activity.",
|
||||
},
|
||||
{
|
||||
label: "Check release regressions",
|
||||
prompt:
|
||||
"I'm shipping to production. Scan now and tell me if anything regressed versus the last run.",
|
||||
},
|
||||
{
|
||||
label: "Rescan stale providers",
|
||||
prompt:
|
||||
"Which providers have never been scanned or haven't been scanned recently? Rescan the ones that matter.",
|
||||
},
|
||||
{
|
||||
label: "Set recurring scans",
|
||||
prompt:
|
||||
"Which of my providers don't have a recurring scan? Tell me which ones should, and set up scheduled scans with the right frequency.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: LIGHTHOUSE_PAGE_ID.PROVIDERS,
|
||||
label: "Providers",
|
||||
match: (pathname) => pathname === "/providers",
|
||||
allowedSearchParams: [
|
||||
...PROVIDER_SCOPE_PARAMS,
|
||||
...COMMON_LIST_PARAMS,
|
||||
"tab",
|
||||
"filter[status]",
|
||||
"filter[connected]",
|
||||
],
|
||||
suggestions: [
|
||||
{
|
||||
label: "Prioritize providers",
|
||||
prompt: "Which visible providers need attention?",
|
||||
},
|
||||
{
|
||||
label: "Find unmonitored cloud accounts",
|
||||
prompt:
|
||||
"What parts of my cloud estate aren't onboarded? Provider accounts that exist but Prowler isn't scanning.",
|
||||
},
|
||||
{
|
||||
label: "Design provider groups",
|
||||
prompt:
|
||||
"Group these provider accounts by what they're actually for, based on names, tags, and what's running in them. Which of those groupings should exist as provider groups in Prowler but don't?",
|
||||
},
|
||||
{
|
||||
label: "Complete provider setup",
|
||||
prompt:
|
||||
"Which accounts were added recently but never fully configured — no schedule, no groups, no successful scan?",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: LIGHTHOUSE_PAGE_ID.ALERTS,
|
||||
label: "Alerts",
|
||||
match: (pathname) => pathname === "/alerts",
|
||||
allowedSearchParams: [
|
||||
"filter[search]",
|
||||
"sort",
|
||||
"filter[enabled]",
|
||||
"filter[trigger]",
|
||||
],
|
||||
suggestions: [
|
||||
{
|
||||
label: "Tune alert rules",
|
||||
prompt: "Which alert rules should I enable or tune?",
|
||||
},
|
||||
{
|
||||
label: "Find alerting gaps",
|
||||
prompt: "Summarize my alerting coverage and its gaps.",
|
||||
},
|
||||
{
|
||||
label: "Review noisy rules",
|
||||
prompt: "Could any alert rule be too noisy or too broad?",
|
||||
},
|
||||
{
|
||||
label: "Design critical alerts",
|
||||
prompt: "Design an alerting strategy for critical findings.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: LIGHTHOUSE_PAGE_ID.SERVICES,
|
||||
label: "Services",
|
||||
match: (pathname) => pathname === "/services",
|
||||
allowedSearchParams: [...PROVIDER_SCOPE_PARAMS, ...COMMON_LIST_PARAMS],
|
||||
suggestions: [
|
||||
{
|
||||
label: "Prioritize risky services",
|
||||
prompt: "Which services carry the most failing findings?",
|
||||
},
|
||||
{
|
||||
label: "Compare service risk",
|
||||
prompt: "Compare risk across my cloud services.",
|
||||
},
|
||||
{
|
||||
label: "Choose services to harden",
|
||||
prompt: "Which services should I harden first?",
|
||||
},
|
||||
{
|
||||
label: "Review service coverage",
|
||||
prompt: "Summarize service coverage across providers.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: LIGHTHOUSE_PAGE_ID.WORKLOADS,
|
||||
label: "Workloads",
|
||||
match: (pathname) => pathname === "/workloads",
|
||||
allowedSearchParams: [...PROVIDER_SCOPE_PARAMS],
|
||||
suggestions: [
|
||||
{
|
||||
label: "Find exposed workloads",
|
||||
prompt: "Which workloads look most exposed?",
|
||||
},
|
||||
{
|
||||
label: "Group workloads for triage",
|
||||
prompt: "How should I group workloads for triage?",
|
||||
},
|
||||
{
|
||||
label: "Prioritize workloads",
|
||||
prompt: "Which workloads need attention first?",
|
||||
},
|
||||
{
|
||||
label: "Build workload hardening plan",
|
||||
prompt: "Build a workload hardening plan.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: LIGHTHOUSE_PAGE_ID.MUTELIST,
|
||||
label: "Mute list",
|
||||
match: (pathname) => pathname === "/mutelist",
|
||||
allowedSearchParams: [...COMMON_LIST_PARAMS, "tab"],
|
||||
suggestions: [
|
||||
{
|
||||
label: "Explain active mute rules",
|
||||
prompt: "Which mute rules are active and why?",
|
||||
},
|
||||
{
|
||||
label: "Find hidden findings",
|
||||
prompt: "Could a mute rule be hiding important findings?",
|
||||
},
|
||||
{
|
||||
label: "Review risky exclusions",
|
||||
prompt: "Review my mute rules for risky exclusions.",
|
||||
},
|
||||
{
|
||||
label: "Structure mute rules safely",
|
||||
prompt: "How should I structure mute rules safely?",
|
||||
},
|
||||
],
|
||||
},
|
||||
] as const satisfies readonly LighthousePageDefinitionInput[];
|
||||
|
||||
export const LIGHTHOUSE_KNOWN_ROUTE_LABELS = {
|
||||
integrations: "Integrations",
|
||||
} as const;
|
||||
@@ -1,5 +1,7 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import type { LighthousePageSuggestions } from "@/types/lighthouse-context";
|
||||
|
||||
import { LIGHTHOUSE_CONTEXT_LIMIT } from "./constants";
|
||||
import {
|
||||
buildLighthousePageContext,
|
||||
@@ -27,7 +29,7 @@ describe("resolveLighthousePage", () => {
|
||||
|
||||
// Then
|
||||
expect(page.id).toBe(expectedPageId);
|
||||
expect(page.suggestions).toHaveLength(4);
|
||||
expectValidSuggestions(page.suggestions);
|
||||
});
|
||||
|
||||
it("should create a labeled fallback for other application pages", () => {
|
||||
@@ -37,7 +39,7 @@ describe("resolveLighthousePage", () => {
|
||||
// Then
|
||||
expect(page.id).toBe("other");
|
||||
expect(page.label).toBe("Integrations");
|
||||
expect(page.suggestions).toHaveLength(4);
|
||||
expectValidSuggestions(page.suggestions);
|
||||
});
|
||||
|
||||
it("should resolve encoded and decoded dynamic paths to the same scope", () => {
|
||||
@@ -61,6 +63,24 @@ describe("resolveLighthousePage", () => {
|
||||
});
|
||||
});
|
||||
|
||||
function expectValidSuggestions(suggestions: LighthousePageSuggestions) {
|
||||
expect(suggestions).toHaveLength(4);
|
||||
expect(new Set(suggestions.map(({ prompt }) => prompt)).size).toBe(
|
||||
suggestions.length,
|
||||
);
|
||||
|
||||
for (const suggestion of suggestions) {
|
||||
expect(suggestion).toEqual(
|
||||
expect.objectContaining({
|
||||
label: expect.any(String),
|
||||
prompt: expect.any(String),
|
||||
}),
|
||||
);
|
||||
expect(suggestion.label.trim()).not.toBe("");
|
||||
expect(suggestion.prompt.trim()).not.toBe("");
|
||||
}
|
||||
}
|
||||
|
||||
describe("buildLighthousePageContext", () => {
|
||||
it("should include only declared search parameters with semantic filter keys", () => {
|
||||
// Given
|
||||
@@ -123,6 +143,24 @@ describe("buildLighthousePageContext", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should preserve the filter names emitted by the alerts page", () => {
|
||||
const context = buildLighthousePageContext(
|
||||
"/alerts",
|
||||
new URLSearchParams({
|
||||
"filter[enabled]": "true",
|
||||
"filter[trigger]": "new_failing_findings",
|
||||
"filter[search]": "s3",
|
||||
edit: "alert-1",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(context.filters).toEqual({
|
||||
enabled: ["true"],
|
||||
search: ["s3"],
|
||||
trigger: ["new_failing_findings"],
|
||||
});
|
||||
});
|
||||
|
||||
it("should preserve the filter names emitted by list-page controls", () => {
|
||||
const findings = buildLighthousePageContext(
|
||||
"/findings",
|
||||
@@ -145,24 +183,6 @@ describe("buildLighthousePageContext", () => {
|
||||
expect(providers.filters).toEqual({ connected: ["true"] });
|
||||
});
|
||||
|
||||
it("should preserve the filter names emitted by the alerts page", () => {
|
||||
const context = buildLighthousePageContext(
|
||||
"/alerts",
|
||||
new URLSearchParams({
|
||||
"filter[enabled]": "true",
|
||||
"filter[trigger]": "new_failing_findings",
|
||||
"filter[search]": "s3",
|
||||
edit: "alert-1",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(context.filters).toEqual({
|
||||
enabled: ["true"],
|
||||
search: ["s3"],
|
||||
trigger: ["new_failing_findings"],
|
||||
});
|
||||
});
|
||||
|
||||
it("should discard sensitive values from allowed search parameters", () => {
|
||||
// Given
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
@@ -4,16 +4,17 @@ import {
|
||||
LIGHTHOUSE_CONTEXT_SOURCE,
|
||||
LIGHTHOUSE_PAGE_ID,
|
||||
type LighthouseContextFilters,
|
||||
type LighthousePageDefinitionInput,
|
||||
type LighthousePageContextItem,
|
||||
type LighthousePageId,
|
||||
type LighthousePageSuggestions,
|
||||
} from "@/types/lighthouse-context";
|
||||
|
||||
export type LighthousePageSuggestions = readonly [
|
||||
string,
|
||||
string,
|
||||
string,
|
||||
string,
|
||||
];
|
||||
import {
|
||||
LIGHTHOUSE_GLOBAL_SUGGESTIONS,
|
||||
LIGHTHOUSE_KNOWN_ROUTE_LABELS,
|
||||
LIGHTHOUSE_PAGE_DEFINITION_INPUTS,
|
||||
} from "./pages.constants";
|
||||
|
||||
export interface LighthousePageDefinition {
|
||||
id: LighthousePageId;
|
||||
@@ -27,255 +28,8 @@ export interface LighthousePageDefinition {
|
||||
) => LighthousePageContextItem;
|
||||
}
|
||||
|
||||
interface LighthousePageDefinitionInput {
|
||||
id: LighthousePageId;
|
||||
label: string;
|
||||
match: (pathname: string) => boolean;
|
||||
allowedSearchParams: readonly string[];
|
||||
suggestions: LighthousePageSuggestions;
|
||||
}
|
||||
|
||||
const PROVIDER_SCOPE_PARAMS = [
|
||||
"filter[provider__in]",
|
||||
"filter[provider_id__in]",
|
||||
"filter[provider_uid]",
|
||||
"filter[provider_uid__in]",
|
||||
"filter[provider_type__in]",
|
||||
"filter[provider]",
|
||||
"filter[provider_type]",
|
||||
"filter[provider_groups__in]",
|
||||
] as const;
|
||||
|
||||
const COMMON_LIST_PARAMS = [
|
||||
"query",
|
||||
"search",
|
||||
"filter[search]",
|
||||
"sort",
|
||||
] as const;
|
||||
|
||||
const GLOBAL_SUGGESTIONS = [
|
||||
"Summarize my most critical open findings and what to fix first.",
|
||||
"What are my highest-impact compliance gaps right now?",
|
||||
"Find risky attack paths and explain the exposure.",
|
||||
"How can I improve my cloud security posture today?",
|
||||
] as const satisfies LighthousePageSuggestions;
|
||||
|
||||
const PAGE_DEFINITIONS: readonly LighthousePageDefinition[] = [
|
||||
createPageDefinition({
|
||||
id: LIGHTHOUSE_PAGE_ID.OVERVIEW,
|
||||
label: "Overview",
|
||||
match: (pathname) => pathname === "/",
|
||||
allowedSearchParams: PROVIDER_SCOPE_PARAMS,
|
||||
suggestions: [
|
||||
"What should I prioritize from this overview?",
|
||||
"Explain my threat score, its trend, and weakest section.",
|
||||
"Which providers, services, or frameworks carry the most risk?",
|
||||
"Build a practical security plan for today.",
|
||||
],
|
||||
}),
|
||||
createPageDefinition({
|
||||
id: LIGHTHOUSE_PAGE_ID.FINDINGS,
|
||||
label: "Findings",
|
||||
match: (pathname) => pathname === "/findings",
|
||||
allowedSearchParams: [
|
||||
...PROVIDER_SCOPE_PARAMS,
|
||||
...COMMON_LIST_PARAMS,
|
||||
"filter[region__in]",
|
||||
"filter[service__in]",
|
||||
"filter[severity__in]",
|
||||
"filter[status__in]",
|
||||
"filter[delta]",
|
||||
"filter[delta__in]",
|
||||
"filter[resource_type__in]",
|
||||
"filter[category__in]",
|
||||
"filter[resource_groups__in]",
|
||||
"filter[scan]",
|
||||
"filter[scan__in]",
|
||||
"filter[scan_id]",
|
||||
"filter[scan_id__in]",
|
||||
"filter[inserted_at]",
|
||||
"filter[muted]",
|
||||
],
|
||||
suggestions: [
|
||||
"Which visible critical findings need attention first?",
|
||||
"Prioritize remediation for the current findings.",
|
||||
"Identify likely root causes across these findings.",
|
||||
"Create a remediation plan for this findings view.",
|
||||
],
|
||||
}),
|
||||
createPageDefinition({
|
||||
id: LIGHTHOUSE_PAGE_ID.RESOURCES,
|
||||
label: "Resources",
|
||||
match: (pathname) => pathname === "/resources",
|
||||
allowedSearchParams: [
|
||||
...PROVIDER_SCOPE_PARAMS,
|
||||
...COMMON_LIST_PARAMS,
|
||||
"filter[region__in]",
|
||||
"filter[service__in]",
|
||||
"filter[type__in]",
|
||||
"filter[groups__in]",
|
||||
],
|
||||
suggestions: [
|
||||
"Which visible resources carry the most risk?",
|
||||
"Find likely exposures among these resources.",
|
||||
"Identify security patterns across the current resources.",
|
||||
"Recommend a hardening plan for this resource scope.",
|
||||
],
|
||||
}),
|
||||
createPageDefinition({
|
||||
id: LIGHTHOUSE_PAGE_ID.COMPLIANCE_DETAIL,
|
||||
label: "Compliance detail",
|
||||
match: (pathname) => pathname.startsWith("/compliance/"),
|
||||
allowedSearchParams: [
|
||||
...PROVIDER_SCOPE_PARAMS,
|
||||
"scanId",
|
||||
"scan_id",
|
||||
"complianceId",
|
||||
"section",
|
||||
"mode",
|
||||
"version",
|
||||
"filter[cis_profile_level]",
|
||||
"filter[region__in]",
|
||||
"filter[status__in]",
|
||||
],
|
||||
suggestions: [
|
||||
"Which failed requirements need attention first?",
|
||||
"Prioritize remediation for this compliance framework.",
|
||||
"Which sections are weakest and why?",
|
||||
"Create a plan to improve this framework score.",
|
||||
],
|
||||
}),
|
||||
createPageDefinition({
|
||||
id: LIGHTHOUSE_PAGE_ID.COMPLIANCE,
|
||||
label: "Compliance",
|
||||
match: (pathname) => pathname === "/compliance",
|
||||
allowedSearchParams: [
|
||||
...PROVIDER_SCOPE_PARAMS,
|
||||
"tab",
|
||||
"scanId",
|
||||
"scan_id",
|
||||
"framework",
|
||||
"version",
|
||||
"mode",
|
||||
"section",
|
||||
"filter[compliance_id]",
|
||||
"filter[region__in]",
|
||||
],
|
||||
suggestions: [
|
||||
"Summarize the most important compliance gaps.",
|
||||
"Which frameworks should I prioritize?",
|
||||
"Explain the visible compliance score.",
|
||||
"Which controls need remediation first?",
|
||||
],
|
||||
}),
|
||||
createPageDefinition({
|
||||
id: LIGHTHOUSE_PAGE_ID.ATTACK_PATHS,
|
||||
label: "Attack Paths",
|
||||
match: (pathname) => pathname.startsWith("/attack-paths"),
|
||||
allowedSearchParams: ["scanId", "queryId"],
|
||||
suggestions: [
|
||||
"Explain the current attack path.",
|
||||
"Which nodes are most critical in this graph?",
|
||||
"Where should I break this attack path first?",
|
||||
"Recommend remediations for the current attack path.",
|
||||
],
|
||||
}),
|
||||
createPageDefinition({
|
||||
id: LIGHTHOUSE_PAGE_ID.SCANS,
|
||||
label: "Scans",
|
||||
match: (pathname) => pathname.startsWith("/scans"),
|
||||
allowedSearchParams: [
|
||||
...PROVIDER_SCOPE_PARAMS,
|
||||
...COMMON_LIST_PARAMS,
|
||||
"tab",
|
||||
"scanId",
|
||||
"filter[state]",
|
||||
"filter[state__in]",
|
||||
"filter[trigger]",
|
||||
],
|
||||
suggestions: [
|
||||
"Summarize recent scan activity.",
|
||||
"Which visible scans look problematic?",
|
||||
"Explain the most important scan failures.",
|
||||
"What should I investigate next from this scans view?",
|
||||
],
|
||||
}),
|
||||
createPageDefinition({
|
||||
id: LIGHTHOUSE_PAGE_ID.PROVIDERS,
|
||||
label: "Providers",
|
||||
match: (pathname) => pathname === "/providers",
|
||||
allowedSearchParams: [
|
||||
...PROVIDER_SCOPE_PARAMS,
|
||||
...COMMON_LIST_PARAMS,
|
||||
"tab",
|
||||
"filter[status]",
|
||||
"filter[connected]",
|
||||
],
|
||||
suggestions: [
|
||||
"Which visible providers need attention?",
|
||||
"Assess security coverage across these providers.",
|
||||
"Which providers may have stale scans?",
|
||||
"What should I improve in provider onboarding?",
|
||||
],
|
||||
}),
|
||||
createPageDefinition({
|
||||
id: LIGHTHOUSE_PAGE_ID.ALERTS,
|
||||
label: "Alerts",
|
||||
match: (pathname) => pathname === "/alerts",
|
||||
allowedSearchParams: [
|
||||
"filter[search]",
|
||||
"sort",
|
||||
"filter[enabled]",
|
||||
"filter[trigger]",
|
||||
],
|
||||
suggestions: [
|
||||
"Which alert rules should I enable or tune?",
|
||||
"Summarize my alerting coverage and its gaps.",
|
||||
"Could any alert rule be too noisy or too broad?",
|
||||
"Design an alerting strategy for critical findings.",
|
||||
],
|
||||
}),
|
||||
createPageDefinition({
|
||||
id: LIGHTHOUSE_PAGE_ID.SERVICES,
|
||||
label: "Services",
|
||||
match: (pathname) => pathname === "/services",
|
||||
allowedSearchParams: [...PROVIDER_SCOPE_PARAMS, ...COMMON_LIST_PARAMS],
|
||||
suggestions: [
|
||||
"Which services carry the most failing findings?",
|
||||
"Compare risk across my cloud services.",
|
||||
"Which services should I harden first?",
|
||||
"Summarize service coverage across providers.",
|
||||
],
|
||||
}),
|
||||
createPageDefinition({
|
||||
id: LIGHTHOUSE_PAGE_ID.WORKLOADS,
|
||||
label: "Workloads",
|
||||
match: (pathname) => pathname === "/workloads",
|
||||
allowedSearchParams: [...PROVIDER_SCOPE_PARAMS],
|
||||
suggestions: [
|
||||
"Which workloads look most exposed?",
|
||||
"How should I group workloads for triage?",
|
||||
"Which workloads need attention first?",
|
||||
"Build a workload hardening plan.",
|
||||
],
|
||||
}),
|
||||
createPageDefinition({
|
||||
id: LIGHTHOUSE_PAGE_ID.MUTELIST,
|
||||
label: "Mute list",
|
||||
match: (pathname) => pathname === "/mutelist",
|
||||
allowedSearchParams: [...COMMON_LIST_PARAMS, "tab"],
|
||||
suggestions: [
|
||||
"Which mute rules are active and why?",
|
||||
"Could a mute rule be hiding important findings?",
|
||||
"Review my mute rules for risky exclusions.",
|
||||
"How should I structure mute rules safely?",
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
const KNOWN_ROUTE_LABELS = {
|
||||
integrations: "Integrations",
|
||||
} as const;
|
||||
const PAGE_DEFINITIONS: readonly LighthousePageDefinition[] =
|
||||
LIGHTHOUSE_PAGE_DEFINITION_INPUTS.map(createPageDefinition);
|
||||
|
||||
function normalizeLighthousePath(pathname: string): string {
|
||||
const normalized = `/${pathname
|
||||
@@ -350,14 +104,15 @@ function buildLighthouseScopeKey(
|
||||
function createFallbackDefinition(pathname: string): LighthousePageDefinition {
|
||||
const segment = pathname.split("/").filter(Boolean)[0] ?? "overview";
|
||||
const label =
|
||||
KNOWN_ROUTE_LABELS[segment as keyof typeof KNOWN_ROUTE_LABELS] ??
|
||||
toTitleCase(segment);
|
||||
LIGHTHOUSE_KNOWN_ROUTE_LABELS[
|
||||
segment as keyof typeof LIGHTHOUSE_KNOWN_ROUTE_LABELS
|
||||
] ?? toTitleCase(segment);
|
||||
return createPageDefinition({
|
||||
id: LIGHTHOUSE_PAGE_ID.OTHER,
|
||||
label,
|
||||
match: () => true,
|
||||
allowedSearchParams: [],
|
||||
suggestions: GLOBAL_SUGGESTIONS,
|
||||
suggestions: LIGHTHOUSE_GLOBAL_SUGGESTIONS,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,26 @@ export type LighthouseContextTransport = z.infer<
|
||||
export type LighthousePageId =
|
||||
(typeof LIGHTHOUSE_PAGE_ID)[keyof typeof LIGHTHOUSE_PAGE_ID];
|
||||
|
||||
export interface LighthousePageSuggestion {
|
||||
label: string;
|
||||
prompt: string;
|
||||
}
|
||||
|
||||
export type LighthousePageSuggestions = readonly [
|
||||
LighthousePageSuggestion,
|
||||
LighthousePageSuggestion,
|
||||
LighthousePageSuggestion,
|
||||
LighthousePageSuggestion,
|
||||
];
|
||||
|
||||
export interface LighthousePageDefinitionInput {
|
||||
id: LighthousePageId;
|
||||
label: string;
|
||||
match: (pathname: string) => boolean;
|
||||
allowedSearchParams: readonly string[];
|
||||
suggestions: LighthousePageSuggestions;
|
||||
}
|
||||
|
||||
export type LighthouseContextFilters = z.infer<
|
||||
typeof lighthouseContextFiltersSchema
|
||||
>;
|
||||
|
||||
Reference in New Issue
Block a user