diff --git a/junction-app/app/layout.tsx b/junction-app/app/layout.tsx index 6eabcf9..b5c6d54 100644 --- a/junction-app/app/layout.tsx +++ b/junction-app/app/layout.tsx @@ -16,7 +16,8 @@ const geistMono = Geist_Mono({ export const metadata: Metadata = { title: "Aegis", - description: "Aegis is a tool that helps you assess the security of your applications.", + description: + "Aegis is a tool that helps you assess the security of your applications.", }; export default function RootLayout({ diff --git a/junction-app/app/reports/[id]/page.tsx b/junction-app/app/reports/[id]/page.tsx index 9a0f4aa..ac33e22 100644 --- a/junction-app/app/reports/[id]/page.tsx +++ b/junction-app/app/reports/[id]/page.tsx @@ -1,9 +1,9 @@ "use client"; import Link from "next/link"; -import { useEffect, useState, use } from "react"; -import { useAuth } from "@/contexts/AuthContext"; +import { use, useEffect, useState } from "react"; import { useRouter } from "next/navigation"; +import { useAuth } from "@/contexts/AuthContext"; import { AlertCircle, AlertTriangle, @@ -16,11 +16,13 @@ import { ExternalLink, FileText, Info, + Loader2, Lock, MessageCircle, Send, Settings, Share2, + Shield, Sparkles, TrendingDown, Users, @@ -32,6 +34,12 @@ type ReportDetailPageProps = { }>; }; +interface ReportData { + cached_at: string; + query: string; + report: any; +} + type ChatMessage = { id: string; role: "copilot" | "ciso"; @@ -39,188 +47,6 @@ type ChatMessage = { 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" }, { id: "compliance", label: "Compliance" }, @@ -229,27 +55,77 @@ const tabs = [ { id: "deployment", label: "Deployment" }, ]; +const suggestedQuestions = [ + "Summarize key strengths and weaknesses.", + "List any open compliance or audit gaps.", + "Outline actions to improve the risk posture.", +]; + +const severityBadge = (severity: string) => { + if (severity === "high" || severity === "critical") { + return "border-red-500/30 bg-red-500/20 text-red-300"; + } + if (severity === "medium") { + return "border-yellow-500/30 bg-yellow-500/20 text-yellow-300"; + } + 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" }; +}; + export default function ReportDetailPage(props: ReportDetailPageProps) { const params = use(props.params); - const { user } = useAuth(); const router = useRouter(); + const { user } = useAuth(); 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 fetchReport = async () => { + try { + setLoading(true); + setError(null); + + const response = await fetch(`/api/reports/${params.id}`); + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.error || "Failed to fetch report"); + } + + setReportData(data); + } catch (err) { + console.error("Error fetching report:", err); + setError(err instanceof Error ? err.message : "Failed to load report"); + } finally { + setLoading(false); + } + }; + + fetchReport(); + }, [user, router, params.id]); const handleExportPdf = () => { if (typeof window === "undefined") return; window.print(); }; - const handleShare = async () => { + const handleShare = async (reportName: string) => { if (typeof window === "undefined") return; const url = window.location.href; const sharePayload = { @@ -275,34 +151,18 @@ export default function ReportDetailPage(props: ReportDetailPageProps) { } window.prompt("Copy this link", url); - } catch (error) { + } catch (shareError) { const isAbortError = - typeof error === "object" && - error !== null && - "name" in error && - error.name === "AbortError"; + typeof shareError === "object" && + shareError !== null && + "name" in shareError && + (shareError as { name: string }).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"; - } - if (severity === "medium") { - return "border-yellow-500/30 bg-yellow-500/20 text-yellow-300"; - } - return "border-blue-500/30 bg-blue-500/20 text-blue-300"; - }; - const handleSend = async (text?: string) => { const message = (text ?? chatInput).trim(); if (!message) return; @@ -322,12 +182,9 @@ export default function ReportDetailPage(props: ReportDetailPageProps) { try { const response = await fetch(`/api/reports/${params.id}/ask`, { method: "POST", - headers: { - "Content-Type": "application/json", - }, + headers: { "Content-Type": "application/json" }, body: JSON.stringify({ question: message }), }); - const data = await response.json(); const answer = response.ok && data.answer @@ -345,15 +202,15 @@ export default function ReportDetailPage(props: ReportDetailPageProps) { timestamp: "moments ago", }, ]); - } catch (error) { + } catch (sendError) { setChatMessages((prev) => [ ...prev, { id: `copilot-${Date.now()}`, role: "copilot", content: - error instanceof Error - ? `I ran into an error: ${error.message}` + sendError instanceof Error + ? `I ran into an error: ${sendError.message}` : "I ran into an unexpected error while answering.", timestamp: "moments ago", }, @@ -363,6 +220,49 @@ export default function ReportDetailPage(props: ReportDetailPageProps) { } }; + if (loading) { + return ( +
+
+ +

Loading report...

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

+ Report Not Found +

+

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

+ + + Back to reports + +
+
+ ); + } + + 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 shareName = vendor || "Vendor report"; + return (
@@ -376,7 +276,7 @@ export default function ReportDetailPage(props: ReportDetailPageProps) {
+