mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-23 04:21:52 +00:00
feat: add lighthouse caching
This commit is contained in:
@@ -2,16 +2,55 @@ import { getLighthouseConfig } from "@/actions/lighthouse/lighthouse";
|
||||
import { LighthouseIcon } from "@/components/icons/Icons";
|
||||
import { Chat } from "@/components/lighthouse";
|
||||
import { ContentLayout } from "@/components/ui";
|
||||
import { CacheService } from "@/lib/lighthouse/cache";
|
||||
import { suggestedActions } from "@/lib/lighthouse/suggested-actions";
|
||||
|
||||
export default async function AIChatbot() {
|
||||
interface LighthousePageProps {
|
||||
searchParams: { cachedMessage?: string };
|
||||
}
|
||||
|
||||
export default async function AIChatbot({ searchParams }: LighthousePageProps) {
|
||||
const config = await getLighthouseConfig();
|
||||
|
||||
const hasConfig = !!config;
|
||||
const isActive = config?.attributes?.is_active ?? false;
|
||||
|
||||
// Fetch cached content if a cached message type is specified
|
||||
let cachedContent = null;
|
||||
if (searchParams.cachedMessage) {
|
||||
const cached = await CacheService.getCachedMessage(
|
||||
searchParams.cachedMessage,
|
||||
);
|
||||
cachedContent = cached.success ? cached.data : null;
|
||||
}
|
||||
|
||||
// Pre-fetch all question answers and processing status
|
||||
const isProcessing = await CacheService.isRecommendationProcessing();
|
||||
const questionAnswers: Record<string, string> = {};
|
||||
|
||||
if (!isProcessing) {
|
||||
for (const action of suggestedActions) {
|
||||
if (action.questionRef) {
|
||||
const cached = await CacheService.getCachedMessage(
|
||||
`question_${action.questionRef}`,
|
||||
);
|
||||
if (cached.success && cached.data) {
|
||||
questionAnswers[action.questionRef] = cached.data;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ContentLayout title="Lighthouse AI" icon={<LighthouseIcon />}>
|
||||
<Chat hasConfig={hasConfig} isActive={isActive} />
|
||||
<Chat
|
||||
hasConfig={hasConfig}
|
||||
isActive={isActive}
|
||||
cachedContent={cachedContent}
|
||||
messageType={searchParams.cachedMessage}
|
||||
isProcessing={isProcessing}
|
||||
questionAnswers={questionAnswers}
|
||||
/>
|
||||
</ContentLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ export const LighthouseBanner = async () => {
|
||||
) {
|
||||
return renderBanner({
|
||||
message: cachedRecommendations.data,
|
||||
href: "/lighthouse",
|
||||
href: "/lighthouse?cachedMessage=recommendation",
|
||||
gradient:
|
||||
"bg-gradient-to-r from-blue-500 to-purple-600 hover:from-blue-600 hover:to-purple-700 focus:ring-blue-500/50 dark:from-blue-600 dark:to-purple-700 dark:hover:from-blue-700 dark:hover:to-purple-800 dark:focus:ring-blue-400/50",
|
||||
});
|
||||
@@ -62,7 +62,7 @@ export const LighthouseBanner = async () => {
|
||||
if (isProcessing) {
|
||||
return renderBanner({
|
||||
message: "Lighthouse is reviewing your findings for insights",
|
||||
href: "/lighthouse",
|
||||
href: "",
|
||||
gradient:
|
||||
"bg-gradient-to-r from-orange-500 to-yellow-500 hover:from-orange-600 hover:to-yellow-600 focus:ring-orange-500/50 dark:from-orange-600 dark:to-yellow-600 dark:hover:from-orange-700 dark:hover:to-yellow-700 dark:focus:ring-orange-400/50",
|
||||
});
|
||||
|
||||
@@ -1,30 +1,40 @@
|
||||
"use client";
|
||||
|
||||
import { useChat } from "@ai-sdk/react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import { MemoizedMarkdown } from "@/components/lighthouse/memoized-markdown";
|
||||
import { CustomButton, CustomTextarea } from "@/components/ui/custom";
|
||||
import { CustomLink } from "@/components/ui/custom/custom-link";
|
||||
import { Form } from "@/components/ui/form";
|
||||
|
||||
interface SuggestedAction {
|
||||
title: string;
|
||||
label: string;
|
||||
action: string;
|
||||
}
|
||||
import {
|
||||
SuggestedAction,
|
||||
suggestedActions,
|
||||
} from "@/lib/lighthouse/suggested-actions";
|
||||
|
||||
interface ChatProps {
|
||||
hasConfig: boolean;
|
||||
isActive: boolean;
|
||||
cachedContent?: string | null;
|
||||
messageType?: string;
|
||||
isProcessing: boolean;
|
||||
questionAnswers: Record<string, string>;
|
||||
}
|
||||
|
||||
interface ChatFormData {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export const Chat = ({ hasConfig, isActive }: ChatProps) => {
|
||||
export const Chat = ({
|
||||
hasConfig,
|
||||
isActive,
|
||||
cachedContent,
|
||||
messageType,
|
||||
isProcessing,
|
||||
questionAnswers,
|
||||
}: ChatProps) => {
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const {
|
||||
@@ -74,6 +84,13 @@ export const Chat = ({ hasConfig, isActive }: ChatProps) => {
|
||||
},
|
||||
});
|
||||
|
||||
// State for cached response streaming simulation
|
||||
const [isStreamingCached, setIsStreamingCached] = useState(false);
|
||||
const [streamingMessageId, setStreamingMessageId] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [currentStreamText, setCurrentStreamText] = useState("");
|
||||
|
||||
const form = useForm<ChatFormData>({
|
||||
defaultValues: {
|
||||
message: "",
|
||||
@@ -108,6 +125,149 @@ export const Chat = ({ hasConfig, isActive }: ChatProps) => {
|
||||
}
|
||||
}, [errorMessage, form, setMessages]);
|
||||
|
||||
// Function to simulate streaming text
|
||||
const simulateStreaming = useCallback(
|
||||
async (text: string, messageId: string) => {
|
||||
setIsStreamingCached(true);
|
||||
setStreamingMessageId(messageId);
|
||||
setCurrentStreamText("");
|
||||
|
||||
// Stream word by word with realistic delays
|
||||
const words = text.split(" ");
|
||||
let currentText = "";
|
||||
|
||||
for (let i = 0; i < words.length; i++) {
|
||||
currentText += (i > 0 ? " " : "") + words[i];
|
||||
setCurrentStreamText(currentText);
|
||||
|
||||
// Shorter delay between words for faster streaming
|
||||
const delay = Math.random() * 80 + 40; // 40-120ms delay per word
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
}
|
||||
|
||||
setIsStreamingCached(false);
|
||||
setStreamingMessageId(null);
|
||||
setCurrentStreamText("");
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
// Function to handle cached response for suggested actions
|
||||
const handleCachedResponse = useCallback(
|
||||
async (action: SuggestedAction) => {
|
||||
if (!action.questionRef) {
|
||||
// No question ref, use normal flow
|
||||
append({
|
||||
role: "user",
|
||||
content: action.action,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (isProcessing) {
|
||||
// Processing in progress, fallback to real-time LLM
|
||||
append({
|
||||
role: "user",
|
||||
content: action.action,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we have cached answer
|
||||
const cachedAnswer = questionAnswers[action.questionRef];
|
||||
|
||||
if (cachedAnswer) {
|
||||
// Cache hit - use cached content with streaming simulation
|
||||
const userMessageId = `user-cached-${Date.now()}`;
|
||||
const assistantMessageId = `assistant-cached-${Date.now()}`;
|
||||
|
||||
const userMessage = {
|
||||
id: userMessageId,
|
||||
role: "user" as const,
|
||||
content: action.action,
|
||||
};
|
||||
|
||||
const assistantMessage = {
|
||||
id: assistantMessageId,
|
||||
role: "assistant" as const,
|
||||
content: "",
|
||||
};
|
||||
|
||||
const updatedMessages = [...messages, userMessage, assistantMessage];
|
||||
setMessages(updatedMessages);
|
||||
|
||||
// Start streaming simulation
|
||||
setTimeout(() => {
|
||||
simulateStreaming(cachedAnswer, assistantMessageId);
|
||||
}, 300);
|
||||
} else {
|
||||
// Cache miss/expired/error - fallback to real-time LLM
|
||||
append({
|
||||
role: "user",
|
||||
content: action.action,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error handling cached response:", error);
|
||||
// Fall back to normal API flow
|
||||
append({
|
||||
role: "user",
|
||||
content: action.action,
|
||||
});
|
||||
}
|
||||
},
|
||||
[
|
||||
messages,
|
||||
setMessages,
|
||||
append,
|
||||
simulateStreaming,
|
||||
isProcessing,
|
||||
questionAnswers,
|
||||
],
|
||||
);
|
||||
|
||||
// Load cached message on mount if cachedContent is provided
|
||||
useEffect(() => {
|
||||
const loadCachedMessage = () => {
|
||||
if (cachedContent && messages.length === 0) {
|
||||
// Create different user questions based on message type
|
||||
let userQuestion = "Tell me more about this";
|
||||
|
||||
if (messageType === "recommendation") {
|
||||
userQuestion =
|
||||
"Tell me more about the security issues Lighthouse found";
|
||||
}
|
||||
// Future: handle other message types
|
||||
// else if (messageType === "question_1") {
|
||||
// userQuestion = "Previously cached question here";
|
||||
// }
|
||||
|
||||
// Create message IDs
|
||||
const userMessageId = `user-cached-${messageType}-${Date.now()}`;
|
||||
const assistantMessageId = `assistant-cached-${messageType}-${Date.now()}`;
|
||||
|
||||
// Add user message
|
||||
const userMessage = {
|
||||
id: userMessageId,
|
||||
role: "user" as const,
|
||||
content: userQuestion,
|
||||
};
|
||||
|
||||
// Add assistant message with the cached content
|
||||
const assistantMessage = {
|
||||
id: assistantMessageId,
|
||||
role: "assistant" as const,
|
||||
content: cachedContent,
|
||||
};
|
||||
|
||||
setMessages([userMessage, assistantMessage]);
|
||||
}
|
||||
};
|
||||
|
||||
loadCachedMessage();
|
||||
}, [cachedContent, messageType, messages.length, setMessages]);
|
||||
|
||||
// Sync form value with chat input
|
||||
useEffect(() => {
|
||||
const syntheticEvent = {
|
||||
@@ -146,6 +306,19 @@ export const Chat = ({ hasConfig, isActive }: ChatProps) => {
|
||||
return () => document.removeEventListener("keydown", handleKeyDown);
|
||||
}, [messageValue, onFormSubmit]);
|
||||
|
||||
// Update assistant message content during streaming simulation
|
||||
useEffect(() => {
|
||||
if (isStreamingCached && streamingMessageId && currentStreamText) {
|
||||
setMessages((prevMessages) =>
|
||||
prevMessages.map((msg) =>
|
||||
msg.id === streamingMessageId
|
||||
? { ...msg, content: currentStreamText }
|
||||
: msg,
|
||||
),
|
||||
);
|
||||
}
|
||||
}, [currentStreamText, isStreamingCached, streamingMessageId, setMessages]);
|
||||
|
||||
useEffect(() => {
|
||||
if (messagesContainerRef.current && latestUserMsgRef.current) {
|
||||
const container = messagesContainerRef.current;
|
||||
@@ -156,30 +329,6 @@ export const Chat = ({ hasConfig, isActive }: ChatProps) => {
|
||||
}
|
||||
}, [messages]);
|
||||
|
||||
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",
|
||||
},
|
||||
];
|
||||
|
||||
// Determine if chat should be disabled
|
||||
const shouldDisableChat = !hasConfig || !isActive;
|
||||
|
||||
@@ -267,10 +416,7 @@ export const Chat = ({ hasConfig, isActive }: ChatProps) => {
|
||||
key={`suggested-action-${index}`}
|
||||
ariaLabel={`Send message: ${action.action}`}
|
||||
onPress={() => {
|
||||
append({
|
||||
role: "user",
|
||||
content: action.action,
|
||||
});
|
||||
handleCachedResponse(action); // Use cached response handler
|
||||
}}
|
||||
className="hover:bg-muted flex h-auto w-full flex-col items-start justify-start rounded-xl border bg-gray-50 px-4 py-3.5 text-left font-sans text-sm dark:bg-gray-900"
|
||||
>
|
||||
@@ -320,10 +466,12 @@ export const Chat = ({ hasConfig, isActive }: ChatProps) => {
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{status === "submitted" && (
|
||||
{(status === "submitted" || isStreamingCached) && (
|
||||
<div className="flex justify-start">
|
||||
<div className="bg-muted max-w-[80%] rounded-lg px-4 py-2">
|
||||
<div className="animate-pulse">Thinking...</div>
|
||||
<div className="animate-pulse">
|
||||
{isStreamingCached ? "" : "Thinking..."}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -358,10 +506,18 @@ export const Chat = ({ hasConfig, isActive }: ChatProps) => {
|
||||
ariaLabel={
|
||||
status === "submitted" ? "Stop generation" : "Send message"
|
||||
}
|
||||
isDisabled={status === "submitted" || !messageValue?.trim()}
|
||||
isDisabled={
|
||||
status === "submitted" ||
|
||||
isStreamingCached ||
|
||||
!messageValue?.trim()
|
||||
}
|
||||
className="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg bg-primary p-2 text-primary-foreground hover:bg-primary/90 disabled:opacity-50 dark:bg-primary/90"
|
||||
>
|
||||
{status === "submitted" ? <span>■</span> : <span>➤</span>}
|
||||
{status === "submitted" || isStreamingCached ? (
|
||||
<span>■</span>
|
||||
) : (
|
||||
<span>➤</span>
|
||||
)}
|
||||
</CustomButton>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
+89
-24
@@ -2,11 +2,16 @@ import Valkey from "iovalkey";
|
||||
|
||||
import { auth } from "@/auth.config";
|
||||
|
||||
import { generateRecommendation } from "./recommendations";
|
||||
import {
|
||||
generateBannerFromDetailed,
|
||||
generateDetailedRecommendation,
|
||||
generateQuestionAnswers,
|
||||
} from "./recommendations";
|
||||
import { suggestedActions } from "./suggested-actions";
|
||||
import {
|
||||
compareProcessedScanIds,
|
||||
generateSecurityScanSummary,
|
||||
getCompletedScansLast24h,
|
||||
compareProcessedScanIds,
|
||||
} from "./summary";
|
||||
|
||||
let valkeyClient: Valkey | null = null;
|
||||
@@ -136,18 +141,12 @@ export class CacheService {
|
||||
await this.setProcessedScanIds(scanIds);
|
||||
|
||||
// Generate and cache recommendations asynchronously
|
||||
this.generateAndCacheRecommendations(scanSummary)
|
||||
.then((result) => {
|
||||
if (result.success && result.data) {
|
||||
console.log("Background recommendation generated successfully");
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(
|
||||
"Background recommendation generation failed:",
|
||||
error,
|
||||
);
|
||||
});
|
||||
this.generateAndCacheRecommendations(scanSummary).catch((error) => {
|
||||
console.error(
|
||||
"Background recommendation generation failed:",
|
||||
error,
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
@@ -207,10 +206,6 @@ export class CacheService {
|
||||
}
|
||||
}
|
||||
|
||||
static async getCachedMessage(): Promise<string | null> {
|
||||
return await this.get("scan-summary");
|
||||
}
|
||||
|
||||
static async getRecommendations(): Promise<{
|
||||
success: boolean;
|
||||
data?: string;
|
||||
@@ -245,6 +240,7 @@ export class CacheService {
|
||||
|
||||
const lockKey = "recommendations-processing";
|
||||
const dataKey = `_lighthouse:${tenantId}:recommendations`;
|
||||
const detailedDataKey = `_lighthouse:${tenantId}:cached-messages:recommendation`;
|
||||
|
||||
try {
|
||||
const client = await getValkeyClient();
|
||||
@@ -280,17 +276,40 @@ export class CacheService {
|
||||
};
|
||||
}
|
||||
|
||||
// Generate recommendation using LLM
|
||||
const recommendation = await generateRecommendation(scanSummary);
|
||||
// Generate detailed recommendation first
|
||||
const detailedRecommendation =
|
||||
await generateDetailedRecommendation(scanSummary);
|
||||
|
||||
// Only cache non-empty recommendations
|
||||
if (recommendation.trim()) {
|
||||
await client.set(dataKey, recommendation);
|
||||
if (!detailedRecommendation.trim()) {
|
||||
return { success: true, data: "" };
|
||||
}
|
||||
|
||||
// Generate banner from detailed content
|
||||
const bannerRecommendation = await generateBannerFromDetailed(
|
||||
detailedRecommendation,
|
||||
);
|
||||
|
||||
// Both must succeed - no point in detailed without banner
|
||||
if (!bannerRecommendation.trim()) {
|
||||
return { success: true, data: "" };
|
||||
}
|
||||
|
||||
// Generate question answers
|
||||
const questionAnswers = await generateQuestionAnswers(suggestedActions);
|
||||
|
||||
// Cache both versions
|
||||
await client.set(dataKey, bannerRecommendation);
|
||||
await client.set(detailedDataKey, detailedRecommendation);
|
||||
|
||||
// Cache question answers with 24h TTL
|
||||
for (const [questionRef, answer] of Object.entries(questionAnswers)) {
|
||||
const questionKey = `_lighthouse:${tenantId}:cached-messages:question_${questionRef}`;
|
||||
await client.set(questionKey, answer, "EX", 86400); // 24 hours
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: recommendation,
|
||||
data: bannerRecommendation,
|
||||
};
|
||||
} finally {
|
||||
await this.releaseProcessingLock(tenantId, lockKey);
|
||||
@@ -315,6 +334,52 @@ export class CacheService {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// New method to get cached message by type
|
||||
static async getCachedMessage(messageType: string): Promise<{
|
||||
success: boolean;
|
||||
data?: string;
|
||||
}> {
|
||||
const tenantId = await this.getTenantId();
|
||||
if (!tenantId) return { success: false };
|
||||
|
||||
try {
|
||||
const client = await getValkeyClient();
|
||||
const dataKey = `_lighthouse:${tenantId}:cached-messages:${messageType}`;
|
||||
|
||||
const cachedData = await client.get(dataKey);
|
||||
if (cachedData) {
|
||||
return {
|
||||
success: true,
|
||||
data: cachedData.toString(),
|
||||
};
|
||||
}
|
||||
|
||||
return { success: true, data: undefined };
|
||||
} catch (error) {
|
||||
console.error(`Error getting cached message ${messageType}:`, error);
|
||||
return { success: false };
|
||||
}
|
||||
}
|
||||
|
||||
// New method to set cached message by type
|
||||
static async setCachedMessage(
|
||||
messageType: string,
|
||||
content: string,
|
||||
): Promise<boolean> {
|
||||
const tenantId = await this.getTenantId();
|
||||
if (!tenantId) return false;
|
||||
|
||||
try {
|
||||
const client = await getValkeyClient();
|
||||
const dataKey = `_lighthouse:${tenantId}:cached-messages:${messageType}`;
|
||||
await client.set(dataKey, content);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`Error caching message type ${messageType}:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function initializeTenantCache(): Promise<{
|
||||
|
||||
@@ -2,7 +2,10 @@ import { ChatOpenAI } from "@langchain/openai";
|
||||
|
||||
import { getAIKey, getLighthouseConfig } from "@/actions/lighthouse/lighthouse";
|
||||
|
||||
export const generateRecommendation = async (
|
||||
import { type SuggestedAction } from "./suggested-actions";
|
||||
import { initLighthouseWorkflow } from "./workflow";
|
||||
|
||||
export const generateDetailedRecommendation = async (
|
||||
scanSummary: string,
|
||||
): Promise<string> => {
|
||||
try {
|
||||
@@ -11,52 +14,56 @@ export const generateRecommendation = async (
|
||||
return "";
|
||||
}
|
||||
|
||||
// Get lighthouse configuration
|
||||
const lighthouseConfig = await getLighthouseConfig();
|
||||
if (!lighthouseConfig?.attributes) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const config = lighthouseConfig.attributes;
|
||||
const finalBusinessContext = config.business_context || "";
|
||||
const businessContext = config.business_context || "";
|
||||
|
||||
const llm = new ChatOpenAI({
|
||||
model: config.model || "gpt-4o",
|
||||
temperature: config.temperature || 0,
|
||||
maxTokens: 150,
|
||||
maxTokens: 1500,
|
||||
apiKey: apiKey,
|
||||
});
|
||||
|
||||
// Build the prompt with business context awareness
|
||||
let systemPrompt = `You are a cloud security analyst creating concise business recommendations for a banner notification.
|
||||
let systemPrompt = `You are a cloud security analyst providing focused, actionable recommendations.
|
||||
|
||||
IMPORTANT: Your response must be a single, short sentence (max 80 characters) that would make a user want to click on a banner to learn more.
|
||||
IMPORTANT: Focus on ONE of these high-impact opportunities:
|
||||
1. The most CRITICAL finding that needs immediate attention
|
||||
2. A pattern where fixing one check ID resolves many findings (e.g., "Fix aws_s3_bucket_public_access_block to resolve 15 findings")
|
||||
3. The issue with highest business impact
|
||||
|
||||
GUIDELINES:
|
||||
- Frame recommendations in business terms, not technical jargon
|
||||
- Focus on actionable insights
|
||||
- Make it clickable and engaging
|
||||
- Don't use phrases like "Lighthouse says" or "Lighthouse recommends"
|
||||
- Be specific about the type of improvement when possible
|
||||
- Use only information from the security scan summary to generate the recommendation
|
||||
- Add words like "Lighthouse" to the recommendation
|
||||
- Don't end with a question mark or full stop
|
||||
- Don't use words like "urges" or "requires"
|
||||
- Don't wrap the message in double quotes or single quotes
|
||||
- Use words like "detected" or "found" to describe the issue
|
||||
Your response should be a comprehensive analysis of this ONE focus area including:
|
||||
|
||||
EXAMPLES OF GOOD RESPONSES:
|
||||
- Lighthouse detected critical issues in authentication services
|
||||
- Lighthouse found a new exposed S3 bucket in recent scan
|
||||
- Lighthouse identified fixing one check could resolve 30 open findings
|
||||
**Issue Description:**
|
||||
- What exactly is the problem
|
||||
- Why it's critical or high-impact
|
||||
- How many findings it affects
|
||||
|
||||
Based on the below security scan summary, generate ONE short business recommendation:`;
|
||||
**Affected Resources:**
|
||||
- Specific resources, services, or configurations involved
|
||||
- Number of affected resources
|
||||
|
||||
if (finalBusinessContext) {
|
||||
systemPrompt += `\n\nBUSINESS CONTEXT: ${finalBusinessContext}`;
|
||||
**Business Impact:**
|
||||
- Security risks and potential consequences
|
||||
- Compliance violations (mention specific frameworks if applicable)
|
||||
- Operational impact
|
||||
|
||||
**Remediation Steps:**
|
||||
- Clear, step-by-step instructions
|
||||
- Specific commands or configuration changes where applicable
|
||||
- Expected outcome after fix
|
||||
|
||||
Be specific with numbers (e.g., "affects 12 S3 buckets", "resolves 15 findings"). Focus on actionable guidance that will have the biggest security improvement.`;
|
||||
|
||||
if (businessContext) {
|
||||
systemPrompt += `\n\nBUSINESS CONTEXT: ${businessContext}`;
|
||||
}
|
||||
|
||||
systemPrompt += `\n\nBased on this security scan summary, generate 1 engaging banner message:\n\n${scanSummary}`;
|
||||
systemPrompt += `\n\nSecurity Scan Summary:\n${scanSummary}`;
|
||||
|
||||
const response = await llm.invoke([
|
||||
{
|
||||
@@ -65,11 +72,128 @@ Based on the below security scan summary, generate ONE short business recommenda
|
||||
},
|
||||
]);
|
||||
|
||||
const recommendation = response.content.toString().trim();
|
||||
|
||||
return recommendation.length > 0 ? recommendation : "";
|
||||
return response.content.toString().trim();
|
||||
} catch (error) {
|
||||
console.error("Error generating recommendation:", error);
|
||||
console.error("Error generating detailed recommendation:", error);
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
export const generateBannerFromDetailed = async (
|
||||
detailedRecommendation: string,
|
||||
): Promise<string> => {
|
||||
try {
|
||||
const apiKey = await getAIKey();
|
||||
if (!apiKey) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const lighthouseConfig = await getLighthouseConfig();
|
||||
if (!lighthouseConfig?.attributes) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const config = lighthouseConfig.attributes;
|
||||
|
||||
const llm = new ChatOpenAI({
|
||||
model: config.model || "gpt-4o",
|
||||
temperature: config.temperature || 0,
|
||||
maxTokens: 100,
|
||||
apiKey: apiKey,
|
||||
});
|
||||
|
||||
const systemPrompt = `Create a short, engaging banner message from this detailed security analysis.
|
||||
|
||||
REQUIREMENTS:
|
||||
- Maximum 80 characters
|
||||
- Include "Lighthouse" in the message
|
||||
- Focus on the key insight or opportunity
|
||||
- Make it clickable and business-focused
|
||||
- Use action words like "detected", "found", "identified"
|
||||
- Don't end with punctuation
|
||||
|
||||
EXAMPLES:
|
||||
- Lighthouse found fixing 1 S3 check resolves 15 findings
|
||||
- Lighthouse detected critical RDS encryption gaps
|
||||
- Lighthouse identified 3 exposed databases needing attention
|
||||
|
||||
Based on this detailed analysis, create one engaging banner message:
|
||||
|
||||
${detailedRecommendation}`;
|
||||
|
||||
const response = await llm.invoke([
|
||||
{
|
||||
role: "system",
|
||||
content: systemPrompt,
|
||||
},
|
||||
]);
|
||||
|
||||
return response.content.toString().trim();
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"Error generating banner from detailed recommendation:",
|
||||
error,
|
||||
);
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
// Legacy function for backward compatibility
|
||||
export const generateRecommendation = async (
|
||||
scanSummary: string,
|
||||
): Promise<string> => {
|
||||
const detailed = await generateDetailedRecommendation(scanSummary);
|
||||
if (!detailed) return "";
|
||||
|
||||
return await generateBannerFromDetailed(detailed);
|
||||
};
|
||||
|
||||
export const generateQuestionAnswers = async (
|
||||
questions: SuggestedAction[],
|
||||
): Promise<Record<string, string>> => {
|
||||
const answers: Record<string, string> = {};
|
||||
|
||||
try {
|
||||
const apiKey = await getAIKey();
|
||||
if (!apiKey) {
|
||||
return answers;
|
||||
}
|
||||
|
||||
// Initialize the workflow system
|
||||
const workflow = await initLighthouseWorkflow();
|
||||
|
||||
for (const question of questions) {
|
||||
if (!question.questionRef) continue;
|
||||
|
||||
try {
|
||||
// Use the existing workflow to answer the question
|
||||
const result = await workflow.invoke({
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: question.action,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Extract the final message content
|
||||
const finalMessage = result.messages[result.messages.length - 1];
|
||||
if (finalMessage?.content) {
|
||||
answers[question.questionRef] = finalMessage.content
|
||||
.toString()
|
||||
.trim();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Error generating answer for question ${question.questionRef}:`,
|
||||
error,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error generating question answers:", error);
|
||||
}
|
||||
|
||||
return answers;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user