import { LIGHTHOUSE_CONTEXT_KIND, LIGHTHOUSE_CONTEXT_LIMIT, LIGHTHOUSE_CONTEXT_SOURCE, LIGHTHOUSE_PAGE_ID, type LighthouseContextFilters, type LighthousePageContextItem, type LighthousePageId, } from "@/types/lighthouse-context"; export type LighthousePageSuggestions = readonly [ string, string, string, string, ]; export interface LighthousePageDefinition { id: LighthousePageId; label: string; match: (pathname: string) => boolean; allowedSearchParams: readonly string[]; suggestions: LighthousePageSuggestions; buildPageContext: ( pathname: string, searchParams: URLSearchParams, ) => 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; function normalizeLighthousePath(pathname: string): string { const normalized = `/${pathname .split("?")[0] .split("/") .filter(Boolean) .map(decodePathSegment) .join("/")}`; return normalized === "/" ? normalized : normalized.slice(0, LIGHTHOUSE_CONTEXT_LIMIT.STRING_LENGTH); } export function resolveLighthousePage( pathname: string, ): LighthousePageDefinition { const normalizedPath = normalizeLighthousePath(pathname); return ( PAGE_DEFINITIONS.find((definition) => definition.match(normalizedPath)) ?? createFallbackDefinition(normalizedPath) ); } export function buildLighthousePageContext( pathname: string, searchParams: URLSearchParams, ): LighthousePageContextItem { const normalizedPath = normalizeLighthousePath(pathname); return resolveLighthousePage(normalizedPath).buildPageContext( normalizedPath, searchParams, ); } export function getLighthouseScopeKey(pathname: string): string { const normalizedPath = normalizeLighthousePath(pathname); const page = resolveLighthousePage(normalizedPath); return buildLighthouseScopeKey(page.id, normalizedPath); } function createPageDefinition( input: LighthousePageDefinitionInput, ): LighthousePageDefinition { return { ...input, buildPageContext: (pathname, searchParams) => { const filters = buildFilters(searchParams, input.allowedSearchParams); return { kind: LIGHTHOUSE_CONTEXT_KIND.PAGE, id: input.id, source: LIGHTHOUSE_CONTEXT_SOURCE.AUTOMATIC, scopeKey: buildLighthouseScopeKey(input.id, pathname), label: input.label, path: pathname, ...(Object.keys(filters).length > 0 ? { filters } : {}), }; }, }; } function buildLighthouseScopeKey( pageId: LighthousePageId, pathname: string, ): string { const prefix = `${pageId}:`; return `${prefix}${pathname.slice( 0, LIGHTHOUSE_CONTEXT_LIMIT.STRING_LENGTH - prefix.length, )}`; } 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); return createPageDefinition({ id: LIGHTHOUSE_PAGE_ID.OTHER, label, match: () => true, allowedSearchParams: [], suggestions: GLOBAL_SUGGESTIONS, }); } function buildFilters( searchParams: URLSearchParams, allowedSearchParams: readonly string[], ): LighthouseContextFilters { const filters: LighthouseContextFilters = {}; let remainingValues: number = LIGHTHOUSE_CONTEXT_LIMIT.FILTER_VALUES; for (const param of [...allowedSearchParams].sort()) { if (remainingValues === 0) break; const values = searchParams .getAll(param) .flatMap((value) => value.split(",")) .map((value) => value.trim()) .filter( (value) => value.length > 0 && !containsSensitiveLighthouseContextValue(value), ) .map((value) => value.slice(0, LIGHTHOUSE_CONTEXT_LIMIT.STRING_LENGTH)) .slice(0, remainingValues); if (values.length === 0) continue; const key = toContextFilterKey(param); filters[key] = [...(filters[key] ?? []), ...values]; remainingValues -= values.length; } return Object.fromEntries( Object.entries(filters).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0, ), ); } function toContextFilterKey(param: string): string { if (!param.startsWith("filter[")) return param === "query" ? "search" : param; return param.slice(7, -1).replace(/__in$/, ""); } export function containsSensitiveLighthouseContextValue( value: string, ): boolean { return ( /\b[^\s@]+@[^\s@]+\.[^\s@]+\b/.test(value) || /\b(?:\d{1,3}\.){3}\d{1,3}\b/.test(value) || containsIpv6Address(value) || /\bAKIA[A-Z0-9]{16}\b/.test(value) || /\bbearer\s+\S+/i.test(value) || /\b(?:password|secret|token|credential)\s*[:=]/i.test(value) ); } function containsIpv6Address(value: string): boolean { return value .split(/[^0-9A-Fa-f:]+/) .some((candidate) => isIpv6AddressCandidate(candidate)); } function isIpv6AddressCandidate(candidate: string): boolean { if (!candidate.includes(":") || candidate.includes(":::")) return false; const compressedParts = candidate.split("::"); if (compressedParts.length > 2) return false; const segments = candidate.split(":").filter(Boolean); if (segments.some((segment) => segment.length > 4)) return false; return compressedParts.length === 2 ? segments.length < 8 : segments.length === 8; } function toTitleCase(value: string): string { if (!value) return "Current page"; return value .split("-") .filter(Boolean) .map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`) .join(" ") .slice(0, LIGHTHOUSE_CONTEXT_LIMIT.STRING_LENGTH); } function decodePathSegment(segment: string): string { try { return decodeURIComponent(segment); } catch { return segment; } }