From 9e62a5398f6e3bdccbda3be6e94b72e48141ba9a Mon Sep 17 00:00:00 2001 From: Chandrapal Badshah <12944530+Chan9390@users.noreply.github.com> Date: Thu, 29 May 2025 10:10:28 +0530 Subject: [PATCH 1/4] Add Lighthouse chat interface --- .env | 5 + ui/actions/lighthouse/compliances.ts | 87 + ui/actions/lighthouse/index.ts | 2 + ui/actions/lighthouse/lighthouse.ts | 142 + ui/app/(prowler)/lighthouse/config/layout.tsx | 19 + ui/app/(prowler)/lighthouse/config/page.tsx | 26 + ui/app/(prowler)/lighthouse/page.tsx | 13 + ui/app/api/lighthouse/analyst/route.ts | 94 + ui/components/lighthouse/chat.tsx | 215 ++ ui/components/lighthouse/chatbot-config.tsx | 188 ++ ui/components/lighthouse/index.ts | 2 + .../lighthouse/memoized-markdown.tsx | 32 + ui/lib/lighthouse/cache.ts | 260 ++ ui/lib/lighthouse/helpers/checks.ts | 962 +++++++ .../helpers/complianceframeworks.ts | 77 + ui/lib/lighthouse/prompts.ts | 481 ++++ ui/lib/lighthouse/tools/checks.ts | 17 + ui/lib/lighthouse/tools/compliances.ts | 55 + ui/lib/lighthouse/tools/findings.ts | 28 + ui/lib/lighthouse/tools/overview.ts | 48 + ui/lib/lighthouse/tools/providers.ts | 35 + ui/lib/lighthouse/tools/resources.ts | 29 + ui/lib/lighthouse/tools/roles.ts | 26 + ui/lib/lighthouse/tools/scans.ts | 30 + ui/lib/lighthouse/tools/users.ts | 29 + ui/lib/lighthouse/utils.ts | 48 + ui/lib/lighthouse/workflow.ts | 143 + ui/lib/menu-list.ts | 13 + ui/package-lock.json | 2293 ++++++++++++++++- ui/package.json | 6 + ui/tailwind.config.js | 1 + ui/types/lighthouse/checks.ts | 5 + ui/types/lighthouse/compliances.ts | 122 + ui/types/lighthouse/findings.ts | 381 +++ ui/types/lighthouse/index.ts | 9 + ui/types/lighthouse/overviews.ts | 184 ++ ui/types/lighthouse/providers.ts | 100 + ui/types/lighthouse/resources.ts | 172 ++ ui/types/lighthouse/roles.ts | 52 + ui/types/lighthouse/scans.ts | 133 + ui/types/lighthouse/users.ts | 79 + 41 files changed, 6547 insertions(+), 96 deletions(-) create mode 100644 ui/actions/lighthouse/compliances.ts create mode 100644 ui/actions/lighthouse/index.ts create mode 100644 ui/actions/lighthouse/lighthouse.ts create mode 100644 ui/app/(prowler)/lighthouse/config/layout.tsx create mode 100644 ui/app/(prowler)/lighthouse/config/page.tsx create mode 100644 ui/app/(prowler)/lighthouse/page.tsx create mode 100644 ui/app/api/lighthouse/analyst/route.ts create mode 100644 ui/components/lighthouse/chat.tsx create mode 100644 ui/components/lighthouse/chatbot-config.tsx create mode 100644 ui/components/lighthouse/index.ts create mode 100644 ui/components/lighthouse/memoized-markdown.tsx create mode 100644 ui/lib/lighthouse/cache.ts create mode 100644 ui/lib/lighthouse/helpers/checks.ts create mode 100644 ui/lib/lighthouse/helpers/complianceframeworks.ts create mode 100644 ui/lib/lighthouse/prompts.ts create mode 100644 ui/lib/lighthouse/tools/checks.ts create mode 100644 ui/lib/lighthouse/tools/compliances.ts create mode 100644 ui/lib/lighthouse/tools/findings.ts create mode 100644 ui/lib/lighthouse/tools/overview.ts create mode 100644 ui/lib/lighthouse/tools/providers.ts create mode 100644 ui/lib/lighthouse/tools/resources.ts create mode 100644 ui/lib/lighthouse/tools/roles.ts create mode 100644 ui/lib/lighthouse/tools/scans.ts create mode 100644 ui/lib/lighthouse/tools/users.ts create mode 100644 ui/lib/lighthouse/utils.ts create mode 100644 ui/lib/lighthouse/workflow.ts create mode 100644 ui/types/lighthouse/checks.ts create mode 100644 ui/types/lighthouse/compliances.ts create mode 100644 ui/types/lighthouse/findings.ts create mode 100644 ui/types/lighthouse/index.ts create mode 100644 ui/types/lighthouse/overviews.ts create mode 100644 ui/types/lighthouse/providers.ts create mode 100644 ui/types/lighthouse/resources.ts create mode 100644 ui/types/lighthouse/roles.ts create mode 100644 ui/types/lighthouse/scans.ts create mode 100644 ui/types/lighthouse/users.ts diff --git a/.env b/.env index 1f53b81153..54ada554f5 100644 --- a/.env +++ b/.env @@ -137,3 +137,8 @@ SOCIAL_GOOGLE_OAUTH_CLIENT_SECRET="" SOCIAL_GITHUB_OAUTH_CALLBACK_URL="${AUTH_URL}/api/auth/callback/github" SOCIAL_GITHUB_OAUTH_CLIENT_ID="" SOCIAL_GITHUB_OAUTH_CLIENT_SECRET="" + +LANGSMITH_TRACING=false +LANGSMITH_ENDPOINT="https://api.smith.langchain.com" +LANGSMITH_API_KEY="" +LANGCHAIN_PROJECT="" diff --git a/ui/actions/lighthouse/compliances.ts b/ui/actions/lighthouse/compliances.ts new file mode 100644 index 0000000000..7bbaddcaa7 --- /dev/null +++ b/ui/actions/lighthouse/compliances.ts @@ -0,0 +1,87 @@ +import { apiBaseUrl, getAuthHeaders, parseStringify } from "@/lib/helper"; + +export const getLighthouseCompliancesOverview = async ({ + scanId, // required + fields, + filters, + page, + pageSize, + sort, +}: { + scanId: string; + fields?: string[]; + filters?: Record; + page?: number; + pageSize?: number; + sort?: string; +}) => { + const headers = await getAuthHeaders({ contentType: false }); + const url = new URL(`${apiBaseUrl}/compliance-overviews`); + + // Required filter + url.searchParams.append("filter[scan_id]", scanId); + + // Handle optional fields + if (fields && fields.length > 0) { + url.searchParams.append("fields[compliance-overviews]", fields.join(",")); + } + + // Handle filters + if (filters) { + Object.entries(filters).forEach(([key, value]) => { + if (value !== "" && value !== null) { + url.searchParams.append(key, String(value)); + } + }); + } + + // Handle pagination + if (page) { + url.searchParams.append("page[number]", page.toString()); + } + if (pageSize) { + url.searchParams.append("page[size]", pageSize.toString()); + } + + // Handle sorting + if (sort) { + url.searchParams.append("sort", sort); + } + + try { + const compliances = await fetch(url.toString(), { + headers, + }); + const data = await compliances.json(); + const parsedData = parseStringify(data); + + return parsedData; + } catch (error) { + // eslint-disable-next-line no-console + console.error("Error fetching providers:", error); + return undefined; + } +}; + +export const getLighthouseComplianceOverview = async ({ + complianceId, + fields, +}: { + complianceId: string; + fields?: string[]; +}) => { + const headers = await getAuthHeaders({ contentType: false }); + const url = new URL(`${apiBaseUrl}/compliance-overviews/${complianceId}`); + + if (fields) { + url.searchParams.append("fields[compliance-overviews]", fields.join(",")); + } + const response = await fetch(url.toString(), { + headers, + }); + + const data = await response.json(); + const parsedData = parseStringify(data); + + return parsedData; +}; diff --git a/ui/actions/lighthouse/index.ts b/ui/actions/lighthouse/index.ts new file mode 100644 index 0000000000..48cb8e8651 --- /dev/null +++ b/ui/actions/lighthouse/index.ts @@ -0,0 +1,2 @@ +export * from "./compliances"; +export * from "./lighthouse"; diff --git a/ui/actions/lighthouse/lighthouse.ts b/ui/actions/lighthouse/lighthouse.ts new file mode 100644 index 0000000000..4b33b3693c --- /dev/null +++ b/ui/actions/lighthouse/lighthouse.ts @@ -0,0 +1,142 @@ +import { apiBaseUrl, getAuthHeaders } from "@/lib/helper"; + +const getLighthouseConfigId = async (): Promise => { + const headers = await getAuthHeaders({ contentType: false }); + const url = new URL(`${apiBaseUrl}/lighthouse-config?filter[name]=OpenAI`); + try { + const response = await fetch(url.toString(), { + method: "GET", + headers, + }); + + const data = await response.json(); + + // Check if data array exists and has at least one item + if (data?.data && data.data.length > 0) { + return data.data[0].id; + } + + // Return empty string if no configuration found + return ""; + } catch (error) { + console.error("[Server] Error in getOpenAIConfigurationId:", error); + return ""; + } +}; + +export const getAIKey = async (): Promise => { + const headers = await getAuthHeaders({ contentType: false }); + const configId = await getLighthouseConfigId(); + + if (!configId) { + return ""; + } + + const url = new URL( + `${apiBaseUrl}/lighthouse-config/${configId}?fields[lighthouse-config]=api_key`, + ); + const response = await fetch(url.toString(), { + method: "GET", + headers, + }); + + const data = await response.json(); + return data.data.attributes.api_key; +}; + +export const createLighthouseConfig = async (config: { + model: string; + apiKey: string; + businessContext: string; +}) => { + const headers = await getAuthHeaders({ contentType: true }); + const url = new URL(`${apiBaseUrl}/lighthouse-config`); + try { + const payload = { + data: { + type: "lighthouse-config", + attributes: { + name: "OpenAI", + model: config.model, + api_key: config.apiKey, + business_context: config.businessContext, + }, + }, + }; + + const response = await fetch(url.toString(), { + method: "POST", + headers, + body: JSON.stringify(payload), + }); + const data = await response.json(); + return data; + } catch (error) { + console.error("[Server] Error in createAIConfiguration:", error); + return undefined; + } +}; + +export const getLighthouseConfig = async () => { + const headers = await getAuthHeaders({ contentType: false }); + const configId = await getLighthouseConfigId(); + + if (!configId) { + return undefined; + } + + const url = new URL(`${apiBaseUrl}/lighthouse-config/${configId}`); + try { + const response = await fetch(url.toString(), { + method: "GET", + headers, + }); + const data = await response.json(); + return data; + } catch (error) { + console.error("[Server] Error in getLighthouseConfig:", error); + return undefined; + } +}; + +export const updateLighthouseConfig = async (config: { + model: string; + apiKey: string; + businessContext: string; +}) => { + const headers = await getAuthHeaders({ contentType: true }); + const configId = await getLighthouseConfigId(); + + if (!configId) { + return undefined; + } + + try { + const url = new URL(`${apiBaseUrl}/lighthouse-config/${configId}`); + + // Prepare the request payload following the JSONAPI format + const payload = { + data: { + type: "lighthouse-config", + id: configId, + attributes: { + model: config.model, + api_key: config.apiKey, + business_context: config.businessContext, + }, + }, + }; + + const response = await fetch(url.toString(), { + method: "PATCH", + headers, + body: JSON.stringify(payload), + }); + + const data = await response.json(); + return data; + } catch (error) { + console.error("[Server] Error in updateAIConfiguration:", error); + return undefined; + } +}; diff --git a/ui/app/(prowler)/lighthouse/config/layout.tsx b/ui/app/(prowler)/lighthouse/config/layout.tsx new file mode 100644 index 0000000000..270ce2529c --- /dev/null +++ b/ui/app/(prowler)/lighthouse/config/layout.tsx @@ -0,0 +1,19 @@ +import "@/styles/globals.css"; + +import React from "react"; + +import { ContentLayout } from "@/components/ui"; + +interface ChatbotConfigLayoutProps { + children: React.ReactNode; +} + +export default function ChatbotConfigLayout({ + children, +}: ChatbotConfigLayoutProps) { + return ( + + {children} + + ); +} diff --git a/ui/app/(prowler)/lighthouse/config/page.tsx b/ui/app/(prowler)/lighthouse/config/page.tsx new file mode 100644 index 0000000000..ebf1423009 --- /dev/null +++ b/ui/app/(prowler)/lighthouse/config/page.tsx @@ -0,0 +1,26 @@ +import { getLighthouseConfig } from "@/actions/lighthouse"; +import { ChatbotConfig } from "@/components/lighthouse"; + +export const dynamic = "force-dynamic"; + +export default async function ChatbotConfigPage() { + const response = await getLighthouseConfig(); + + const initialValues = response?.data?.attributes + ? { + model: response.data.attributes.model, + apiKey: response.data.attributes.api_key || "", + businessContext: response.data.attributes.business_context || "", + } + : { + model: "gpt-4o", + apiKey: "", + businessContext: "", + }; + + const configExists = !!response; + + return ( + + ); +} diff --git a/ui/app/(prowler)/lighthouse/page.tsx b/ui/app/(prowler)/lighthouse/page.tsx new file mode 100644 index 0000000000..469303ed44 --- /dev/null +++ b/ui/app/(prowler)/lighthouse/page.tsx @@ -0,0 +1,13 @@ +import { getAIKey } from "@/actions/lighthouse/lighthouse"; +import { Chat } from "@/components/lighthouse"; +import { ContentLayout } from "@/components/ui"; + +export default async function AIChatbot() { + const apiKey = await getAIKey(); + + return ( + + + + ); +} diff --git a/ui/app/api/lighthouse/analyst/route.ts b/ui/app/api/lighthouse/analyst/route.ts new file mode 100644 index 0000000000..8207120a9f --- /dev/null +++ b/ui/app/api/lighthouse/analyst/route.ts @@ -0,0 +1,94 @@ +import { LangChainAdapter, Message } from "ai"; + +import { getLighthouseConfig } from "@/actions/lighthouse/lighthouse"; +import { getCachedDataSection } from "@/lib/lighthouse/cache"; +import { + convertLangChainMessageToVercelMessage, + convertVercelMessageToLangChainMessage, +} from "@/lib/lighthouse/utils"; +import { initLighthouseWorkflow } from "@/lib/lighthouse/workflow"; + +export async function POST(req: Request) { + try { + const { + messages, + }: { + messages: Message[]; + } = await req.json(); + + if (!messages) { + return Response.json({ error: "No messages provided" }, { status: 400 }); + } + + // Create a new array for processed messages + const processedMessages = [...messages]; + + // Get AI configuration to access business context + const aiConfig = await getLighthouseConfig(); + const businessContext = aiConfig?.data?.attributes?.business_context; + + // Get cached data + const cachedData = await getCachedDataSection(); + + // Add context messages at the beginning + const contextMessages: Message[] = []; + + // Add business context if available + if (businessContext) { + contextMessages.push({ + id: "business-context", + role: "assistant", + content: `Business Context Information:\n${businessContext}`, + }); + } + + // Add cached data if available + if (cachedData) { + contextMessages.push({ + id: "cached-data", + role: "assistant", + content: cachedData, + }); + } + + // Insert all context messages at the beginning + processedMessages.unshift(...contextMessages); + + const app = await initLighthouseWorkflow(); + + const agentStream = app.streamEvents( + { + messages: processedMessages + .filter( + (message: Message) => + message.role === "user" || message.role === "assistant", + ) + .map(convertVercelMessageToLangChainMessage), + }, + { + streamMode: ["values", "messages", "custom"], + version: "v2", + }, + ); + + const stream = new ReadableStream({ + async start(controller) { + for await (const { event, data, tags } of agentStream) { + if (event === "on_chat_model_stream") { + if (data.chunk.content && !!tags && tags.includes("supervisor")) { + const chunk = data.chunk; + const aiMessage = convertLangChainMessageToVercelMessage(chunk); + controller.enqueue(aiMessage); + } + } + } + controller.close(); + }, + }); + + return LangChainAdapter.toDataStreamResponse(stream); + } catch (error) { + console.error("Error in POST request:", error); + return Response.json({ error: "An error occurred" }, { status: 500 }); + } +} diff --git a/ui/components/lighthouse/chat.tsx b/ui/components/lighthouse/chat.tsx new file mode 100644 index 0000000000..8893495088 --- /dev/null +++ b/ui/components/lighthouse/chat.tsx @@ -0,0 +1,215 @@ +"use client"; + +import { useChat } from "@ai-sdk/react"; +import Link from "next/link"; +import { useEffect, useRef } from "react"; + +import { MemoizedMarkdown } from "@/components/lighthouse/memoized-markdown"; + +interface SuggestedAction { + title: string; + label: string; + action: string; +} + +interface ChatProps { + hasApiKey: boolean; +} + +export const Chat = ({ hasApiKey }: ChatProps) => { + const { messages, input, handleSubmit, handleInputChange, append, status } = + useChat({ + api: "/api/lighthouse/analyst", + credentials: "same-origin", + experimental_throttle: 100, + sendExtraMessageFields: true, + onFinish: () => { + // Handle chat completion + }, + onError: () => { + console.log("An error occurred, please try again!"); + }, + }); + + const suggestedActions: SuggestedAction[] = [ + { + title: "Are there any exposed S3", + label: "buckets in my AWS accounts?", + action: "List exposed S3 buckets in my AWS accounts", + }, + { + title: "What is the risk of having", + label: "RDS databases unencrypted?", + action: "What is the risk of having RDS databases unencrypted?", + }, + { + title: "What is the CIS 1.10 compliance status", + label: "of my Kubernetes cluster?", + action: + "What is the CIS 1.10 compliance status of my Kubernetes cluster?", + }, + { + title: "List my highest privileged", + label: "AWS IAM users with full admin access?", + action: "List my highest privileged AWS IAM users with full admin access", + }, + ]; + + const textareaRef = useRef(null); + const messagesContainerRef = useRef(null); + const latestUserMsgRef = useRef(null); + + useEffect(() => { + if (messagesContainerRef.current && latestUserMsgRef.current) { + const container = messagesContainerRef.current; + const userMsg = latestUserMsgRef.current; + const containerPadding = 16; // p-4 in Tailwind = 16px + container.scrollTop = + userMsg.offsetTop - container.offsetTop - containerPadding; + } + }, [messages]); + + const handleAutoResizeInputChange = ( + e: React.ChangeEvent, + ) => { + handleInputChange(e); + const textarea = textareaRef.current; + if (textarea) { + textarea.style.height = "auto"; + textarea.style.height = textarea.scrollHeight + "px"; + if (textarea.scrollHeight > textarea.clientHeight + 1) { + textarea.style.overflowY = "auto"; + } else { + textarea.style.overflowY = "hidden"; + } + } + }; + + return ( +
+ {!hasApiKey && ( +
+
+

+ OpenAI API Key Required +

+

+ Please configure your OpenAI API key to use the Lighthouse Cloud + Security Analyst. +

+ + Configure API Key + +
+
+ )} + + {messages.length === 0 ? ( +
+
+

Suggestions

+
+ {suggestedActions.map((action, index) => ( + + ))} +
+
+
+ ) : ( +
+ {messages.map((message, idx) => { + const lastUserIdx = messages + .map((m, i) => (m.role === "user" ? i : -1)) + .filter((i) => i !== -1) + .pop(); + const isLatestUserMsg = + message.role === "user" && lastUserIdx === idx; + return ( +
+
+
+ +
+
+
+ ); + })} + {status === "submitted" && ( +
+
+
Thinking...
+
+
+ )} +
+ )} + +
+
+