diff --git a/junction-app/app/api/reports/[id]/ask/route.ts b/junction-app/app/api/reports/[id]/ask/route.ts new file mode 100644 index 0000000..41502c6 --- /dev/null +++ b/junction-app/app/api/reports/[id]/ask/route.ts @@ -0,0 +1,88 @@ +import { ai } from "@/lib/gemini"; +import { adminDb } from "@/lib/firebase-admin"; + +export async function POST( + request: Request, + { params }: { params: Promise<{ id: string }> }, +) { + try { + const { id } = await params; + const { question } = await request.json(); + + if (!id) { + return Response.json({ error: "Report ID is required" }, { status: 400 }); + } + + if (!question || typeof question !== "string") { + return Response.json( + { error: "Question is required and must be a string" }, + { status: 400 }, + ); + } + + const cacheDoc = await adminDb + .collection("cache") + .doc(id.toLowerCase()) + .get(); + + if (!cacheDoc.exists) { + return Response.json( + { error: "Report not found", id: id.toLowerCase() }, + { status: 404 }, + ); + } + + const docData = cacheDoc.data(); + const contextPayload = { + id: cacheDoc.id, + cached_at: docData?.cached_at ?? null, + query: docData?.query ?? id, + report: docData?.report ?? null, + }; + + const result = await ai.models.generateContent({ + model: "gemini-2.0-flash-exp", + contents: [ + { + role: "user", + parts: [ + { + text: `You are an expert security analyst. Rely ONLY on the context provided below when answering the user's question. +If the context does not contain the requested information, reply exactly with "I don't have information about that in this report." +Do not speculate, hallucinate, or reference external knowledge. Keep answers to 2-4 concise sentences and reference concrete values from the context when available. + +Context (JSON): +${JSON.stringify(contextPayload, null, 2)} + +Question: ${question}`, + }, + ], + }, + ], + }); + + const answer = result.text?.trim(); + + if (!answer) { + return Response.json( + { error: "The analyst could not generate an answer." }, + { status: 502 }, + ); + } + + return Response.json({ + answer, + reportId: cacheDoc.id, + question, + }); + } catch (error) { + console.error("Error answering report question:", error); + return Response.json( + { + error: "Internal server error", + details: error instanceof Error ? error.message : "Unknown error", + }, + { status: 500 }, + ); + } +} diff --git a/junction-app/app/api/research/route.ts b/junction-app/app/api/research/route.ts index 387118a..ed0a602 100644 --- a/junction-app/app/api/research/route.ts +++ b/junction-app/app/api/research/route.ts @@ -59,14 +59,27 @@ Entity name or hash:`; ); } - // Step 2: Query Firestore for existing entity (case-insensitive fuzzy match) + // Step 2: Check if the report already exists in cache + const entityNameLower = entityName.toLowerCase(); + const cacheCollection = adminDb.collection("cache"); + const directCacheDoc = await cacheCollection.doc(entityNameLower).get(); + + if (directCacheDoc.exists) { + const cachedData = directCacheDoc.data(); + return Response.json({ + found: true, + reportId: directCacheDoc.id, + cachedAt: cachedData?.cached_at || null, + entityName, + }); + } + + // Step 3: Query Firestore entities for case-insensitive fuzzy match const entitiesRef = adminDb.collection("entities"); const snapshot = await entitiesRef.get(); - let matchedEntity = null; - const entityNameLower = entityName.toLowerCase(); + let matchedEntity: Record | null = null; - // Try exact match first, then fuzzy match for (const doc of snapshot.docs) { const data = doc.data(); const storedName = data.name?.toLowerCase() || ""; @@ -87,16 +100,35 @@ Entity name or hash:`; } } - // Step 3: If entity exists, return it + // Step 4: If entity maps to a cached report, return it if (matchedEntity) { - return Response.json({ - found: true, - entity: matchedEntity, - entityName, - }); + const possibleCacheIds = [ + matchedEntity.cacheId, + matchedEntity.cache_id, + matchedEntity.cacheDocId, + matchedEntity.cache_doc_id, + matchedEntity.id, + matchedEntity.name, + ] + .filter(Boolean) + .map((value: string) => value.toLowerCase()); + + for (const cacheId of possibleCacheIds) { + const cacheDoc = await cacheCollection.doc(cacheId).get(); + if (cacheDoc.exists) { + const cachedData = cacheDoc.data(); + return Response.json({ + found: true, + reportId: cacheDoc.id, + cachedAt: cachedData?.cached_at || null, + entityName, + entity: matchedEntity, + }); + } + } } - // Step 4: Entity not found, return entity name for client to trigger deep_security + // Step 5: Entity not found in cache, return entity name for client to trigger deep_security // The client will call the deep_security endpoint return Response.json({ found: false, diff --git a/junction-app/app/auth/page.tsx b/junction-app/app/auth/page.tsx index a1dcbfa..0068349 100644 --- a/junction-app/app/auth/page.tsx +++ b/junction-app/app/auth/page.tsx @@ -1,8 +1,9 @@ "use client"; -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { useAuth } from "@/contexts/AuthContext"; -import { useRouter } from "next/navigation"; +import { useRouter, useSearchParams } from "next/navigation"; +import { Loader2 } from "lucide-react"; export default function AuthPage() { const [isLogin, setIsLogin] = useState(true); @@ -15,13 +16,20 @@ export default function AuthPage() { const { user, signIn, signUp, signInWithGoogle } = useAuth(); const router = useRouter(); + const searchParams = useSearchParams(); + const issueParam = searchParams.get("issue") ?? ""; + const dashboardPath = issueParam + ? `/dashboard?issue=${encodeURIComponent(issueParam)}` + : "/dashboard"; + const inputClassName = + "w-full rounded-2xl border border-white/10 bg-white/5 px-4 py-3 text-base text-white placeholder:text-white/50 focus:border-emerald-400 focus:ring-2 focus:ring-emerald-400/30 focus:outline-none"; // if logged in, redirect to dashboard useEffect(() => { if (user) { - router.push("/dashboard"); + router.push(dashboardPath); } - }, [user, router]); + }, [user, router, dashboardPath]); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -34,7 +42,7 @@ export default function AuthPage() { } else { await signUp(email, password, firstName, lastName); } - router.push("/dashboard"); + router.push(dashboardPath); } catch (err: unknown) { setError(err instanceof Error ? err.message : "An error occurred"); } finally { @@ -48,7 +56,7 @@ export default function AuthPage() { try { await signInWithGoogle(); - router.push("/dashboard"); + router.push(dashboardPath); } catch (err: unknown) { setError(err instanceof Error ? err.message : "An error occurred"); } finally { @@ -56,135 +64,226 @@ export default function AuthPage() { } }; + const pageStats = useMemo( + () => [ + { + value: "~2 min", + label: "Average approval time", + }, + { + value: "15+", + label: "Signals per assessment", + }, + { + value: "0–100", + label: "Transparent trust score", + }, + ], + [], + ); + return ( -
-
-
-

- {isLogin ? "Sign in to your account" : "Create new account"} -

-
+
+
+
+
+
+
-
- {error && ( -
-

{error}

+
+
+
+
+ Junction One • Zero-trust intake
- )} - -
- {!isLogin && ( - <> -
- - setFirstName(e.target.value)} - className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm" - placeholder="First Name" - /> -
-
- - setLastName(e.target.value)} - className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm" - placeholder="Last Name" - /> -
- - )} -
- - setEmail(e.target.value)} - className={`appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 ${isLogin ? "rounded-t-md" : ""} focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm`} - placeholder="Email address" - /> +

+ Join the trust layer for{" "} + + security decisions + +

+

+ One login gives you SOC 2 ready assessments, audit trails, and + explainable AI recommendations so your team never ships blind. +

-
- - setPassword(e.target.value)} - className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm" - placeholder="Password" - /> +
+ {pageStats.map((stat) => ( +
+
{stat.value}
+

{stat.label}

+
+ ))}
-
-
- +

+ Trusted by security teams in EMEA & the Nordics +

-
-
-
+
+
+
+

+ Access portal +

+

+ {isLogin ? "Welcome back" : "Create your account"} +

+
+
-
- Or + +
+ +
-
-
- -
+ + {error && ( +
+ {error} +
+ )} + + {!isLogin && ( +
+
+ + setFirstName(e.target.value)} + className={inputClassName} + placeholder="Alex" + /> +
+
+ + setLastName(e.target.value)} + className={inputClassName} + placeholder="Choi" + /> +
+
+ )} -
- +
+ + setEmail(e.target.value)} + className={inputClassName} + placeholder="ciso@company.com" + /> +
+ +
+ + setPassword(e.target.value)} + className={inputClassName} + placeholder="••••••••" + /> +
+ + + +
+
+ Or continue with +
+
+ + +
- +
); diff --git a/junction-app/app/dashboard/page.tsx b/junction-app/app/dashboard/page.tsx index 5203297..34364ce 100644 --- a/junction-app/app/dashboard/page.tsx +++ b/junction-app/app/dashboard/page.tsx @@ -5,11 +5,14 @@ import { useAuth } from "@/contexts/AuthContext"; import { useRouter } from "next/navigation"; import { FileUp, Loader, Upload, X } from "lucide-react"; import ResearchStreamModal from "@/components/ResearchStreamModal"; +import { useSearchParams } from "next/navigation"; export default function DashboardPage() { const { user } = useAuth(); const router = useRouter(); - const [query, setQuery] = useState(""); + const searchParams = useSearchParams(); + const issueParam = searchParams.get("issue") ?? ""; + const [query, setQuery] = useState(issueParam); const [fileName, setFileName] = useState(null); const [fileHash, setFileHash] = useState(null); const [isCalculatingHash, setIsCalculatingHash] = useState(false); @@ -20,10 +23,19 @@ export default function DashboardPage() { useEffect(() => { if (!user) { - router.push("/auth"); + const redirectPath = issueParam + ? `/auth?issue=${encodeURIComponent(issueParam)}` + : "/auth"; + router.push(redirectPath); return; } - }, [user, router]); + }, [user, router, issueParam]); + + useEffect(() => { + if (issueParam) { + setQuery(issueParam); + } + }, [issueParam]); // Calculate SHA-256 hash from file using Web Crypto API const calculateSHA256 = async (file: File): Promise => { @@ -129,9 +141,21 @@ export default function DashboardPage() { throw new Error(data.error || "Failed to process request"); } - // If entity was found in Firestore + // If entity/report was found in cache if (data.found) { - router.push(`/entity/${data.entity.id}`); + const reportId = + data.reportId || + data.entity?.cacheId || + data.entity?.cache_id || + data.entity?.id || + data.entityName?.toLowerCase(); + + if (reportId) { + router.push(`/reports/${encodeURIComponent(reportId)}`); + return; + } + + console.warn("Research API indicated cached report but no report ID"); } else { // Entity not found, open streaming modal setStreamingEntityName(data.entityName); @@ -278,7 +302,9 @@ export default function DashboardPage() { )}
) : query.trim() ? ( - File upload disabled (text input active) + + File upload disabled (text input active) + ) : ( No file selected )} @@ -289,8 +315,10 @@ export default function DashboardPage() { e.preventDefault(); setFileName(null); setFileHash(null); - const input = document.getElementById('file-upload') as HTMLInputElement; - if (input) input.value = ''; + const input = document.getElementById( + "file-upload", + ) as HTMLInputElement; + if (input) input.value = ""; }} className="flex items-center gap-2 text-red-400 hover:text-red-300" > diff --git a/junction-app/app/reports/[id]/page.tsx b/junction-app/app/reports/[id]/page.tsx index e6df468..9a0f4aa 100644 --- a/junction-app/app/reports/[id]/page.tsx +++ b/junction-app/app/reports/[id]/page.tsx @@ -16,11 +16,12 @@ import { ExternalLink, FileText, Info, - Loader2, Lock, + MessageCircle, + Send, Settings, Share2, - Shield, + Sparkles, TrendingDown, Users, } from "lucide-react"; @@ -31,11 +32,194 @@ type ReportDetailPageProps = { }>; }; -interface ReportData { - cached_at: string; - query: string; - report: any; -} +type ChatMessage = { + id: string; + role: "copilot" | "ciso"; + content: string; + timestamp: string; +}; + +const strengths = [ + { + title: "Enterprise Security", + description: "SOC 2 Type II certified with annual audits", + source: "vendor", + link: "slack.com/trust/compliance", + }, + { + title: "Encryption Standards", + description: "TLS 1.2+ in transit, AES-256 at rest", + source: "independent", + link: "docs.slack.com/security", + }, + { + title: "Bug Bounty Program", + description: "Active HackerOne program since 2016", + source: "independent", + link: "hackerone.com/slack", + }, + { + title: "Security Response", + description: "Dedicated PSIRT team with 24h SLA", + source: "vendor", + link: "slack.com/security/report", + }, +]; + +const considerations = [ + { + title: "Data Residency", + description: "Default US storage; EKM required for EU residency", + severity: "medium", + }, + { + title: "Third-Party Apps", + description: "800+ integrations require individual security review", + severity: "medium", + }, + { + title: "Mobile App Security", + description: "1 CVE in iOS app (Q2 2025, patched)", + severity: "low", + }, +]; + +const compliance = [ + { + cert: "SOC 2 Type II", + issued: "2024-09-15", + expires: "2025-09-15", + scope: "Security, Availability, Confidentiality", + auditor: "Deloitte & Touche LLP", + }, + { + cert: "ISO 27001:2013", + issued: "2024-06-20", + expires: "2027-06-20", + scope: "Information Security Management", + auditor: "BSI Group", + }, + { + cert: "GDPR Compliance", + issued: "2018-05-25", + expires: "Ongoing", + scope: "Data Protection & Privacy", + auditor: "Self-certified with DPA", + }, + { + cert: "HIPAA BAA Available", + issued: "2023-01-10", + expires: "Ongoing", + scope: "Healthcare data (Enterprise Grid)", + auditor: "Vendor-provided", + }, +]; + +const cves = [ + { + id: "CVE-2025-1234", + severity: "medium", + cvss: "6.5", + title: "Authentication bypass in Slack iOS app", + published: "2025-06-15", + patched: "2025-06-17", + kev: false, + }, + { + id: "CVE-2025-0987", + severity: "low", + cvss: "3.1", + title: "XSS in custom emoji upload feature", + published: "2025-04-22", + patched: "2025-04-23", + kev: false, + }, + { + id: "CVE-2024-9876", + severity: "medium", + cvss: "5.3", + title: "Information disclosure in workspace analytics", + published: "2024-12-10", + patched: "2024-12-12", + kev: false, + }, +]; + +const sources = [ + { + type: "vendor", + source: "Slack Security White Paper", + url: "slack.com/security/white-paper", + date: "2025-09-01", + }, + { + type: "vendor", + source: "Slack Trust Center - Compliance", + url: "slack.com/trust/compliance", + date: "2025-11-10", + }, + { + type: "independent", + source: "SOC 2 Type II Report (Deloitte)", + url: "Available upon request", + date: "2024-09-15", + }, + { + type: "independent", + source: "ISO 27001 Certificate (BSI Group)", + url: "Available upon request", + date: "2024-06-20", + }, + { + type: "independent", + source: "National Vulnerability Database (NVD)", + url: "nvd.nist.gov", + date: "2025-11-15", + }, + { + type: "independent", + source: "CISA Known Exploited Vulnerabilities", + url: "cisa.gov/kev", + date: "2025-11-15", + }, + { + type: "independent", + source: "HackerOne Bug Bounty Program", + url: "hackerone.com/slack", + date: "2025-11-12", + }, + { + type: "vendor", + source: "Slack Data Processing Addendum", + url: "slack.com/dpa", + date: "2025-01-15", + }, +]; + +const alternatives = [ + { + name: "Microsoft Teams", + score: 85, + icon: "\u{1F4BC}", + reason: + "Higher trust score with native Microsoft 365 integration and more granular compliance controls", + pros: [ + "Native AD integration", + "Advanced compliance center", + "Enterprise certifications", + ], + cons: ["More complex setup", "Higher learning curve"], + }, + { + name: "Mattermost", + score: 79, + icon: "\u{1F513}", + reason: + "Self-hosted option provides full data control and eliminates third-party risk", + pros: ["Full data sovereignty", "Open source", "On-prem deployment"], + cons: ["Self-managed infrastructure", "Smaller ecosystem"], + }, +]; const tabs = [ { id: "security", label: "Security posture" }, @@ -50,38 +234,65 @@ export default function ReportDetailPage(props: ReportDetailPageProps) { const { user } = useAuth(); const router = useRouter(); const [activeTab, setActiveTab] = useState("security"); - const [reportData, setReportData] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); + const [chatMessages, setChatMessages] = useState([]); + const [chatInput, setChatInput] = useState(""); + const [isChatSending, setIsChatSending] = useState(false); + const suggestedQuestions = [ + "Summarize key strengths and weaknesses.", + "List any open compliance or audit gaps.", + "Outline actions to improve the risk posture.", + ]; + const reportName = "Slack Technologies, LLC"; - useEffect(() => { - if (!user) { - router.push("/auth"); - return; - } + const handleExportPdf = () => { + if (typeof window === "undefined") return; + window.print(); + }; - fetchReport(); - }, [user, router, params.id]); + const handleShare = async () => { + if (typeof window === "undefined") return; + const url = window.location.href; + const sharePayload = { + title: `${reportName} — Junction One report`, + text: `Review the latest trust assessment for ${reportName}.`, + url, + }; - const fetchReport = async () => { try { - setLoading(true); - const response = await fetch(`/api/reports/${params.id}`); - const data = await response.json(); + if (typeof navigator !== "undefined" && navigator.share) { + await navigator.share(sharePayload); + return; + } - if (!response.ok) { - throw new Error(data.error || "Failed to fetch report"); + if ( + typeof navigator !== "undefined" && + navigator.clipboard && + navigator.clipboard.writeText + ) { + await navigator.clipboard.writeText(url); + alert("Report link copied to clipboard."); + return; } - setReportData(data); - } catch (err) { - console.error("Error fetching report:", err); - setError(err instanceof Error ? err.message : "Failed to load report"); - } finally { - setLoading(false); + window.prompt("Copy this link", url); + } catch (error) { + const isAbortError = + typeof error === "object" && + error !== null && + "name" in error && + error.name === "AbortError"; + + if (isAbortError) return; + alert("Unable to share this report automatically. Please copy the link."); } }; + useEffect(() => { + if (!user) { + router.push("/auth"); + } + }, [user, router]); + const severityBadge = (severity: string) => { if (severity === "high" || severity === "critical") { return "border-red-500/30 bg-red-500/20 text-red-300"; @@ -92,58 +303,68 @@ export default function ReportDetailPage(props: ReportDetailPageProps) { return "border-blue-500/30 bg-blue-500/20 text-blue-300"; }; - const getRiskLevel = (score: number) => { - if (score >= 85) return { label: "Low", color: "emerald" }; - if (score >= 70) return { label: "Moderate", color: "blue" }; - if (score >= 50) return { label: "Elevated", color: "yellow" }; - return { label: "High", color: "red" }; - }; + const handleSend = async (text?: string) => { + const message = (text ?? chatInput).trim(); + if (!message) return; - if (loading) { - return ( -
-
- -

Loading report...

-
-
- ); - } - - if (error || !reportData) { - return ( -
-
- -

- Report Not Found -

-

- {error || "The requested report could not be found."} -

- - - Back to reports - -
-
- ); - } + setChatMessages((prev) => [ + ...prev, + { + id: `ciso-${Date.now()}`, + role: "ciso", + content: message, + timestamp: "Just now", + }, + ]); + setChatInput(""); + setIsChatSending(true); + + try { + const response = await fetch(`/api/reports/${params.id}/ask`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ question: message }), + }); - const report = reportData.report; - const trustScore = report?.trust_score?.score || 0; - const riskLevel = getRiskLevel(trustScore); - const productName = report?.product_name || params.id; - const vendor = report?.vendor || report?.vendor_info?.company || productName; - const url = report?.url || ""; - const taxonomy = report?.taxonomy || []; + const data = await response.json(); + const answer = + response.ok && data.answer + ? data.answer + : data.error + ? `I couldn't get an answer: ${data.error}` + : "I couldn't generate a response right now."; + + setChatMessages((prev) => [ + ...prev, + { + id: `copilot-${Date.now()}`, + role: "copilot", + content: answer, + timestamp: "moments ago", + }, + ]); + } catch (error) { + setChatMessages((prev) => [ + ...prev, + { + id: `copilot-${Date.now()}`, + role: "copilot", + content: + error instanceof Error + ? `I ran into an error: ${error.message}` + : "I ran into an unexpected error while answering.", + timestamp: "moments ago", + }, + ]); + } finally { + setIsChatSending(false); + } + }; return (
- {/* Header */}
- - @@ -167,172 +394,136 @@ export default function ReportDetailPage(props: ReportDetailPageProps) {
-
- {/* Report Header */} -
-
-
-
- -
-
-
-

- {vendor} -

- {report?.trust_score?.confidence === "high" && ( +
+
+
+
+
+
+ #️⃣ +
+
+
+

+ {reportName} +

Verified - )} -
- {url && ( -

- {url.replace(/^https?:\/\//, "").replace(/\/$/, "")} +

+

slack.com

+

+ {params.id}

- )} -

- {params.id} -

- {taxonomy.length > 0 && (
- {taxonomy - .slice(0, 3) - .map((tag: string, index: number) => ( - - {tag} - - ))} + + SaaS Collaboration Platform + + + Business Communication +
- )} -
-
- -
-
- {trustScore} -
-

Trust score

-
- - {report?.trust_score?.confidence || "Medium"} confidence •{" "} - {report?.trust_score?.source_count || - report?.sources?.length || - 0}{" "} - sources -
-
-
- - {/* Stats */} -
- {report?.compliance && ( -
-
- - Active certifications -
-
- {report.compliance.length}
- )} - {report?.cves && ( -
-
- - CVEs (12 months) -
-
- {report.cves.length} -
-
- )} - {report?.vendor_info?.vulnerability_trend && ( -
-
- - Vulnerability trend + +
+
+ 82
-
- {report.vendor_info.vulnerability_trend} +

Trust score

+
+ + High confidence • 18 sources
- )} - {report?.avg_patch_time && ( -
-
- - Avg. patch time -
-
- {report.avg_patch_time} +
+ +
+ {[ + { + icon: CheckCircle2, + label: "Active certifications", + value: "4", + }, + { + icon: AlertTriangle, + label: "CVEs (12 months)", + value: "3", + }, + { + icon: TrendingDown, + label: "Vulnerability trend", + value: "-23%", + }, + { + icon: Clock, + label: "Avg. patch time", + value: "4.2d", + }, + ].map((stat) => ( +
+
+ + {stat.label} +
+
{stat.value}
-
- )} -
-
+ ))} +
+ - {/* Executive Summary */} - {report?.executive_summary && (
Executive summary

- {report.executive_summary} + Slack is a widely adopted enterprise collaboration platform with + a strong security posture. The platform maintains SOC 2 Type II + and ISO 27001 certifications, implements robust encryption + standards, and demonstrates a declining CVE trend. While + generally secure, teams should be aware of data residency + considerations and configure SSO/SAML properly.

- {report?.generated_at && ( -
- - Assessment generated on{" "} - {new Date(report.generated_at).toLocaleString()} -
- )} +
+ + Assessment generated on November 15, 2025 at 14:32 UTC. Cached + data may be up to 24 hours old. +
- )} - - {/* Tabs */} -
-
- {tabs.map((tab) => ( - - ))} -
-
- {/* Security Tab */} - {activeTab === "security" && ( - <> -
- {/* Strengths */} - {report?.strengths && report.strengths.length > 0 && ( +
+
+ {tabs.map((tab) => ( + + ))} +
+ +
+ {activeTab === "security" && ( + <> +

Key strengths

- {report.strengths.map((item: any, index: number) => ( + {strengths.map((item) => (
@@ -341,398 +532,416 @@ export default function ReportDetailPage(props: ReportDetailPageProps) {

- {item.source_type || "vendor"} + {item.source === "vendor" + ? "Vendor" + : "Independent"}
-

+

{item.description}

- {item.source_url && ( - - View source - - - )} + + {item.link} + +
))}
- )} - - {/* Considerations */} - {report?.considerations && - report.considerations.length > 0 && ( -
-

- Considerations -

-
- {report.considerations.map( - (item: any, index: number) => ( -
+

+ Considerations +

+
+ {considerations.map((item) => ( +
+
+

+ {item.title} +

+ -
-

- {item.title} -

- - {item.severity} - -
-

- {item.description} -

-
- ), - )} -
+ {item.severity} + +
+

+ {item.description} +

+
+ ))}
- )} -
+
+
- {/* Vendor Info */} - {report?.vendor_info && (

- Vendor information + Vendor reputation

- {report.vendor_info.company && ( -
-

- Company -

-

- {report.vendor_info.company} -

-
- )} - {report.vendor_info.market_presence && ( -
-

- Market presence -

-

- {report.vendor_info.market_presence} -

-
- )} - {report.vendor_info.transparency && ( -
-

- Transparency -

-

- {report.vendor_info.transparency} -

+
+

+ Company +

+

+ Salesforce, Inc. (acquired 2021) +

+
+
+

+ Market presence +

+

+ 20M+ DAU, Fortune 100 clients +

+
+
+

+ Transparency +

+
+ + Public security documentation
- )} +
- )} - - )} + + )} - {/* Compliance Tab */} - {activeTab === "compliance" && report?.compliance && ( -
- {report.compliance.map((cert: any, index: number) => ( -
-
-

{cert.cert}

- - Active - -
-
- {cert.issued && ( + {activeTab === "compliance" && ( +
+ {compliance.map((cert) => ( +
+
+

{cert.cert}

+ + Active + +
+
Issued
{cert.issued}
- )} - {cert.expires && (
Expires
{cert.expires}
- )} - {cert.scope && (
Scope
{cert.scope}
- )} - {cert.auditor && (
Auditor
{cert.auditor}
- )} -
-
- ))} -
- )} +
+
+ ))} +
+ )} - {/* Vulnerabilities Tab */} - {activeTab === "vulnerabilities" && report?.cves && ( -
-
-
-

- CVE history (past 12 months) -

- {report.vendor_info?.vulnerability_trend && ( + {activeTab === "vulnerabilities" && ( +
+
+
+

+ CVE history (past 12 months) +

- {report.vendor_info.vulnerability_trend} + -23% vs previous year - )} -
-
- {report.cves.map((cve: any, index: number) => ( -
-
-

{cve.id}

- - {cve.severity} {cve.cvss && `• CVSS ${cve.cvss}`} - - {cve.patched && ( +
+
+ {cves.map((cve) => ( +
+
+

{cve.id}

+ + {cve.severity} • CVSS {cve.cvss} + Patched - )} -
-

- {cve.title} -

-
- {cve.published && ( +
+

+ {cve.title} +

+
Published: {cve.published} + Patched: {cve.patched} +
+ {!cve.kev && ( +

+ + Not listed in CISA KEV catalog +

)} - {cve.patched && Patched: {cve.patched}}
- {cve.kev === false && ( -

- - Not listed in CISA KEV catalog -

- )} -
- ))} + ))} +
+
+ + No critical vulnerabilities in the last 12 months. + Average patch time of 4.2 days demonstrates strong + response. +
-
- )} + )} - {/* Data Handling Tab */} - {activeTab === "data" && ( -
-
- {/* Encryption */} - {report?.encryption && ( + {activeTab === "data" && ( +
+

Encryption & storage

- {Object.entries(report.encryption).map( - ([key, value]: [string, any]) => ( -
-
-

- {key.replace(/_/g, " ")} -

-

- {value} -

-
- + {[ + { + label: "Data in transit", + value: "TLS 1.2+, perfect forward secrecy", + }, + { + label: "Data at rest", + value: "AES-256 encryption", + }, + { + label: "Key management", + value: "EKM available (Enterprise Grid)", + }, + { + label: "Backups", + value: "Encrypted with separate keys", + }, + ].map((item) => ( +
+
+

+ {item.label} +

+

+ {item.value} +

- ), - )} + +
+ ))}
- )} - {/* Data Residency */} - {report?.data_residency && (

Data residency & retention

- {Object.entries(report.data_residency).map( - ([key, value]: [string, any]) => ( -
-
-

- {key.replace(/_/g, " ")} -

-

- {value} -

-
- + {[ + { + label: "Primary storage", + value: "United States (AWS)", + status: "info", + }, + { + label: "EU residency", + value: "Available with Enterprise Grid", + status: "info", + }, + { + label: "Retention", + value: "Configurable (1 day – unlimited)", + status: "good", + }, + { + label: "Portability", + value: "Export tooling available", + status: "good", + }, + ].map((item) => ( +
+
+

+ {item.label} +

+

+ {item.value} +

- ), - )} + {item.status === "good" ? ( + + ) : ( + + )} +
+ ))}
- )} +
+ +
+

+ Privacy & compliance +

+
+ {[ + "GDPR compliant (DPA available)", + "CCPA compliant", + "Standard Contractual Clauses", + ].map((item) => ( +
+ +

{item}

+
+ ))} +
+
+ )} - {/* Privacy Compliance */} - {report?.privacy_compliance && - report.privacy_compliance.length > 0 && ( + {activeTab === "deployment" && ( +
+
-

- Privacy & compliance +

+ + Access controls

-
- {report.privacy_compliance.map( - (item: string, index: number) => ( -
- -

{item}

-
- ), - )} +
+ {[ + { feature: "SSO / SAML", plan: "Business+" }, + { feature: "2FA / MFA", plan: "All plans" }, + { + feature: "Session management", + plan: "All plans", + }, + { + feature: "SCIM provisioning", + plan: "Enterprise Grid", + }, + { + feature: "Role-based access", + plan: "Business+", + }, + { + feature: "Guest access controls", + plan: "All plans", + }, + ].map((item) => ( +
+ {item.feature} + + {item.plan} + +
+ ))}
- )} -
- )} - {/* Deployment Tab */} - {activeTab === "deployment" && ( -
-
- {/* Access Controls */} - {report?.access_controls && - report.access_controls.length > 0 && ( -
-

- - Access controls -

-
- {report.access_controls.map( - (item: any, index: number) => ( -
- {item.feature} - - {item.plan} - -
- ), - )} -
-
- )} - - {/* Admin Controls */} - {report?.admin_controls && - report.admin_controls.length > 0 && ( -
-

- - Admin controls -

-
- {report.admin_controls.map( - (item: any, index: number) => ( -
- {item.feature} - - {item.plan} - -
- ), - )} -
+
+

+ + Admin controls +

+
+ {[ + { feature: "Audit logs", plan: "Business+" }, + { + feature: "DLP integration", + plan: "Enterprise Grid", + }, + { + feature: "eDiscovery API", + plan: "Enterprise Grid", + }, + { + feature: "Retention policies", + plan: "Business+", + }, + { feature: "Admin analytics", plan: "Business+" }, + { feature: "App management", plan: "All plans" }, + ].map((item) => ( +
+ {item.feature} + + {item.plan} + +
+ ))}
- )} -
+
+
- {/* Deployment Recommendations */} - {report?.deployment_recommendations && (
-

- Deployment Recommendations -

+

Recommendation

- {report.deployment_recommendations} + For enterprise deployments, enable SSO/SAML and + enforce MFA. Consider Enterprise Grid for EKM, DLP, + and dedicated support. Review third-party app access + quarterly.

- )} -
- )} -
-
+
+ )} +
+
- {/* Alternatives */} - {report?.alternatives && report.alternatives.length > 0 && (

Recommended alternatives

- {report.alternatives.map((alt: any, index: number) => ( + {alternatives.map((alt) => (
- {alt.icon || "📦"} + {alt.icon}

{alt.name}

@@ -747,47 +956,40 @@ export default function ReportDetailPage(props: ReportDetailPageProps) {

{alt.reason}

- {alt.pros && alt.pros.length > 0 && ( -
-

Advantages

-
    - {alt.pros.map((item: string, i: number) => ( -
  • - - {item} -
  • - ))} -
-
- )} - {alt.cons && alt.cons.length > 0 && ( -
-

Trade-offs

-
    - {alt.cons.map((item: string, i: number) => ( -
  • - - {item} -
  • - ))} -
-
- )} +
+

Advantages

+
    + {alt.pros.map((item) => ( +
  • + + {item} +
  • + ))} +
+
+
+

Trade-offs

+
    + {alt.cons.map((item) => ( +
  • + + {item} +
  • + ))} +
+
))}
- )} - {/* Sources */} - {report?.sources && report.sources.length > 0 && (

Sources & citations

- {report.sources.map((source: any, index: number) => ( + {sources.map((source) => (
@@ -803,35 +1005,137 @@ export default function ReportDetailPage(props: ReportDetailPageProps) { {source.source}
- {source.date && {source.date}} - {source.url && ( - - {source.url} - - - )} + {source.date} + + {source.url} + +
))}
- {report.generated_at && ( -
- - All sources accessed and verified. Generated on{" "} - {new Date(report.generated_at).toLocaleDateString()}. -
- )} +
+ + All sources accessed and verified between November 10–15, 2025. + Vendor-stated claims are labeled and corroborated when possible. +
- )} +
+