diff --git a/mock-server.js b/mock-server.js new file mode 100644 index 00000000..98422169 --- /dev/null +++ b/mock-server.js @@ -0,0 +1,39 @@ +// Simple mock server for testing version API +const http = require('http'); + +const server = http.createServer((req, res) => { + // Enable CORS + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); + + if (req.method === 'OPTIONS') { + res.writeHead(200); + res.end(); + return; + } + + if (req.url === '/api/v1/version') { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + version: '3.1.0', + components: { + engine: { version: 'v0.0.3-e5f6g7h' }, + webapp: { version: 'v0.0.3-9k8j7h6' }, + cache: { version: 'v0.0.3-4m3n2bl' } + } + })); + } else if (req.url === '/api/v1/user') { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ name: 'Test User' })); + } else { + res.writeHead(404); + res.end('Not Found'); + } +}); + +const PORT = 8080; +server.listen(PORT, () => { + console.log(`Mock server running on http://localhost:${PORT}`); + console.log(`Version API: http://localhost:${PORT}/api/v1/version`); +}); diff --git a/src/main/java/com/redhat/ecosystemappeng/exploitiq/rest/VersionResource.java b/src/main/java/com/redhat/ecosystemappeng/exploitiq/rest/VersionResource.java new file mode 100644 index 00000000..a8ca6845 --- /dev/null +++ b/src/main/java/com/redhat/ecosystemappeng/exploitiq/rest/VersionResource.java @@ -0,0 +1,44 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, Red Hat Inc. & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.redhat.ecosystemappeng.exploitiq.rest; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; + +import java.util.HashMap; +import java.util.Map; + +@Path("/version") +@Tag(name = "Version", description = "Application version information") +public class VersionResource { + + @GET + @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Get application version", description = "Returns version information for the Exploit Intelligence application") + public Response getVersion() { + Map versionInfo = new HashMap<>(); + + String version = System.getenv("EXPLOIT_IQ_VERSION"); + versionInfo.put("version", version != null ? version : "unknown"); + + return Response.ok(versionInfo).build(); + } +} diff --git a/src/main/webui/src/components/AboutModal.css b/src/main/webui/src/components/AboutModal.css new file mode 100644 index 00000000..0727ffde --- /dev/null +++ b/src/main/webui/src/components/AboutModal.css @@ -0,0 +1,16 @@ +/* We render the logo inside custom content, so remove the default brand/header + grid rows and let content start from the top alongside the close button. */ +.ei-about-modal.pf-v6-c-about-modal-box { + grid-template-areas: "content close" !important; + grid-template-rows: auto !important; + height: fit-content !important; +} + +.ei-about-modal.pf-v6-c-about-modal-box .pf-v6-c-about-modal-box__brand, +.ei-about-modal.pf-v6-c-about-modal-box .pf-v6-c-about-modal-box__header { + display: none !important; +} + +.ei-about-modal.pf-v6-c-about-modal-box .pf-v6-c-about-modal-box__close { + padding-block-start: var(--pf-t--global--spacer--xl); +} diff --git a/src/main/webui/src/components/AboutModal.tsx b/src/main/webui/src/components/AboutModal.tsx new file mode 100644 index 00000000..829299e5 --- /dev/null +++ b/src/main/webui/src/components/AboutModal.tsx @@ -0,0 +1,118 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, Red Hat Inc. & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import React from 'react'; +import { AboutModal as PFAboutModal } from '@patternfly/react-core'; +import { useVersion } from '../hooks/useVersion'; +import './AboutModal.css'; + +interface AboutModalProps { + isOpen: boolean; + onClose: () => void; +} + +const AboutModal: React.FC = ({ isOpen, onClose }) => { + const { versionInfo, loading } = useVersion(); + + const components = versionInfo?.components; + + return ( + +
+
+ Red Hat +

+ Red Hat Trusted Profile Analyzer +

+
+ +

+ Exploit Intelligence +

+ +
+

+ Exploit Intelligence is an AI-powered exploitability analysis engine that performs + deep, function-level analysis on application code paths to determine if detected + vulnerabilities (CVEs) are reachable and executable in your environment. +
+ For more information refer to{' '} + + Red Hat Exploit Intelligence documentation + +

+
+ +
+
+
Version
+
{loading ? 'Loading...' : versionInfo?.version || 'unknown'}
+
+ + {components && ( + <> +
+
Components
+
+ + {components.engine && ( +
+
EI engine
+
{components.engine.version}
+
+ )} + {components.webapp && ( +
+
EI webapp
+
{components.webapp.version}
+
+ )} + {components.cache && ( +
+
Nginx cache
+
{components.cache.version}
+
+ )} + + )} +
+
+
+ ); +}; + +export default AboutModal; diff --git a/src/main/webui/src/components/PageHeader.tsx b/src/main/webui/src/components/PageHeader.tsx index feb1b300..8cd31feb 100644 --- a/src/main/webui/src/components/PageHeader.tsx +++ b/src/main/webui/src/components/PageHeader.tsx @@ -10,12 +10,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -/** - * Page header (masthead) component with branding and user toolbar - * Based on reference implementation pattern - */ - -import React from 'react'; +import React, { useState } from 'react'; import { Masthead, MastheadMain, @@ -30,14 +25,14 @@ import { Flex, Stack, FlexItem, + Button, } from '@patternfly/react-core'; import { PageToggleButton } from '@patternfly/react-core'; import BarsIcon from '@patternfly/react-icons/dist/esm/icons/bars-icon'; +import QuestionCircleIcon from '@patternfly/react-icons/dist/esm/icons/question-circle-icon'; import UserAvatarDropdown from './UserAvatarDropdown'; +import AboutModal from './AboutModal'; -/** - * Brand component - displays Red Hat logo and product name - */ const Brand: React.FC = () => { return ( @@ -72,10 +67,12 @@ interface PageHeaderProps { } const PageHeader: React.FC = ({ isSidebarOpen, onSidebarToggle }) => { + const [isAboutModalOpen, setIsAboutModalOpen] = useState(false); + const headerToolbar = ( - @@ -104,32 +101,49 @@ const PageHeader: React.FC = ({ isSidebarOpen, onSidebarToggle md: 'visible', }} > - + + + + setIsAboutModalOpen(true)} /> ); return ( - - - - - - - - - - - - {headerToolbar} - + <> + + + + + + + + + + + + {headerToolbar} + + setIsAboutModalOpen(false)} /> + ); }; diff --git a/src/main/webui/src/components/UserAvatarDropdown.tsx b/src/main/webui/src/components/UserAvatarDropdown.tsx index 029dfff3..719d5625 100644 --- a/src/main/webui/src/components/UserAvatarDropdown.tsx +++ b/src/main/webui/src/components/UserAvatarDropdown.tsx @@ -29,7 +29,11 @@ import { import { UserIcon } from "@patternfly/react-icons"; import { useAuth, logout } from "../hooks/useAuth"; -const UserAvatarDropdown: React.FC = () => { +interface UserAvatarDropdownProps { + onAboutClick?: () => void; +} + +const UserAvatarDropdown: React.FC = ({ onAboutClick }) => { const [isOpen, setIsOpen] = useState(false); const { userName, loading, error } = useAuth(); @@ -41,6 +45,13 @@ const UserAvatarDropdown: React.FC = () => { setIsOpen(false); }; + const handleAbout = () => { + setIsOpen(false); + if (onAboutClick) { + onAboutClick(); + } + }; + const handleLogout = async () => { setIsOpen(false); try { @@ -86,9 +97,14 @@ const UserAvatarDropdown: React.FC = () => { } const userDropdownItems = ( - - Logout - + <> + + About + + + Logout + + ); return ( diff --git a/src/main/webui/src/hooks/useVersion.ts b/src/main/webui/src/hooks/useVersion.ts new file mode 100644 index 00000000..3ef12cb7 --- /dev/null +++ b/src/main/webui/src/hooks/useVersion.ts @@ -0,0 +1,58 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, Red Hat Inc. & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { useState, useEffect } from "react"; + +export interface VersionInfo { + version: string; + components?: { + webapp?: { version: string; image?: string }; + engine?: { version: string; image?: string }; + cache?: { version: string; image?: string }; + }; +} + +let cachedVersionInfo: VersionInfo | null = null; + +export const useVersion = () => { + const [versionInfo, setVersionInfo] = useState(cachedVersionInfo); + const [loading, setLoading] = useState(cachedVersionInfo === null); + const [error, setError] = useState(null); + + useEffect(() => { + if (cachedVersionInfo) { + return; + } + + const fetchVersion = async () => { + try { + const response = await fetch("/api/v1/version"); + if (!response.ok) { + throw new Error(`Failed to fetch version: ${response.statusText}`); + } + const data = await response.json(); + cachedVersionInfo = data; + setVersionInfo(data); + } catch (err) { + console.error("Error fetching version:", err); + setError(err instanceof Error ? err : new Error("Unknown error")); + setVersionInfo({ version: "unknown" }); + } finally { + setLoading(false); + } + }; + + fetchVersion(); + }, []); + + return { versionInfo, loading, error }; +}; diff --git a/src/main/webui/vite.config.ts b/src/main/webui/vite.config.ts index c2eb0242..f6e60ebf 100644 --- a/src/main/webui/vite.config.ts +++ b/src/main/webui/vite.config.ts @@ -61,7 +61,20 @@ const getConfig = () => { } return config; } else { - return baseConfig; + // Non-standalone mode: add proxy for local development + return { + ...baseConfig, + server: { + port: 5173, + proxy: { + '/api': { + target: 'http://localhost:8080', + changeOrigin: true, + secure: false, + } + } + } + }; } };