diff --git a/site/src/api/api.ts b/site/src/api/api.ts index f21afbabf9f17..a4d52c2850204 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -1,4 +1,4 @@ -import axios from "axios"; +import axios, { isAxiosError } from "axios"; import dayjs from "dayjs"; import * as TypesGen from "./typesGenerated"; // This needs to include the `../`, otherwise it breaks when importing into @@ -1703,3 +1703,27 @@ export const putFavoriteWorkspace = async (workspaceID: string) => { export const deleteFavoriteWorkspace = async (workspaceID: string) => { await axios.delete(`/api/v2/workspaces/${workspaceID}/favorite`); }; + +export type GetJFrogXRayScanParams = { + workspaceId: string; + agentId: string; +}; + +export const getJFrogXRayScan = async (options: GetJFrogXRayScanParams) => { + const searchParams = new URLSearchParams({ + workspace_id: options.workspaceId, + agent_id: options.agentId, + }); + + try { + const res = await axios.get( + `/api/v2/integrations/jfrog/xray-scan?${searchParams}`, + ); + return res.data; + } catch (error) { + if (isAxiosError(error) && error.response?.status === 404) { + // react-query library does not allow undefined to be returned as a query result + return null; + } + } +}; diff --git a/site/src/api/queries/integrations.ts b/site/src/api/queries/integrations.ts new file mode 100644 index 0000000000000..72df1752f7169 --- /dev/null +++ b/site/src/api/queries/integrations.ts @@ -0,0 +1,9 @@ +import { GetJFrogXRayScanParams } from "api/api"; +import * as API from "api/api"; + +export const xrayScan = (params: GetJFrogXRayScanParams) => { + return { + queryKey: ["xray", params], + queryFn: () => API.getJFrogXRayScan(params), + }; +}; diff --git a/site/src/modules/resources/AgentRow.stories.tsx b/site/src/modules/resources/AgentRow.stories.tsx index 4b74c35eb9226..fea33c7c4acef 100644 --- a/site/src/modules/resources/AgentRow.stories.tsx +++ b/site/src/modules/resources/AgentRow.stories.tsx @@ -311,3 +311,24 @@ export const Deprecated: Story = { serverAPIVersion: "2.0", }, }; + +export const WithXRayScan: Story = { + parameters: { + queries: [ + { + key: [ + "xray", + { agentId: MockWorkspaceAgent.id, workspaceId: MockWorkspace.id }, + ], + data: { + workspace_id: MockWorkspace.id, + agent_id: MockWorkspaceAgent.id, + critical: 10, + high: 3, + medium: 5, + results_url: "http://localhost:8080", + }, + }, + ], + }, +}; diff --git a/site/src/modules/resources/AgentRow.tsx b/site/src/modules/resources/AgentRow.tsx index ac8ea7009a687..b20ad6649dfaf 100644 --- a/site/src/modules/resources/AgentRow.tsx +++ b/site/src/modules/resources/AgentRow.tsx @@ -37,6 +37,9 @@ import { PortForwardButton } from "./PortForwardButton"; import { SSHButton } from "./SSHButton/SSHButton"; import { TerminalLink } from "./TerminalLink/TerminalLink"; import { VSCodeDesktopButton } from "./VSCodeDesktopButton/VSCodeDesktopButton"; +import { useQuery } from "react-query"; +import { xrayScan } from "api/queries/integrations"; +import { XRayScanAlert } from "./XRayScanAlert"; // Logs are stored as the Line interface to make rendering // much more efficient. Instead of mapping objects each time, we're @@ -74,6 +77,12 @@ export const AgentRow: FC = ({ sshPrefix, storybookLogs, }) => { + // XRay integration + const xrayScanQuery = useQuery( + xrayScan({ workspaceId: workspace.id, agentId: agent.id }), + ); + + // Apps visibility const hasAppsToDisplay = !hideVSCodeDesktopButton || agent.apps.length > 0; const shouldDisplayApps = showApps && @@ -84,6 +93,7 @@ export const AgentRow: FC = ({ agent.display_apps.includes("vscode_insiders"); const showVSCode = hasVSCodeApp && !hideVSCodeDesktopButton; + // Agent runtime logs const logSourceByID = useMemo(() => { const sources: { [id: string]: WorkspaceAgentLogSource } = {}; for (const source of agent.log_sources) { @@ -216,6 +226,8 @@ export const AgentRow: FC = ({ )} + {xrayScanQuery.data && } +
{agent.status === "connected" && (
@@ -276,7 +288,9 @@ export const AgentRow: FC = ({ {hasStartupFeatures && (
({ borderTop: `1px solid ${theme.palette.divider}` })} + css={(theme) => ({ + borderTop: `1px solid ${theme.palette.divider}`, + })} > @@ -571,6 +585,10 @@ const styles = { flexWrap: "wrap", lineHeight: "1.5", + "&:has(+ [role='alert'])": { + paddingBottom: 16, + }, + [theme.breakpoints.down("md")]: { gap: 16, }, diff --git a/site/src/modules/resources/XRayScanAlert.tsx b/site/src/modules/resources/XRayScanAlert.tsx new file mode 100644 index 0000000000000..5510ad74ae11c --- /dev/null +++ b/site/src/modules/resources/XRayScanAlert.tsx @@ -0,0 +1,106 @@ +import { Interpolation, Theme } from "@emotion/react"; +import Button from "@mui/material/Button"; +import { JFrogXrayScan } from "api/typesGenerated"; +import { ExternalImage } from "components/ExternalImage/ExternalImage"; +import { FC } from "react"; + +export const XRayScanAlert: FC<{ scan: JFrogXrayScan }> = ({ scan }) => { + return ( +
+ +
+ + JFrog Xray detected new vulnerabilities for this agent + + +
    + {scan.critical > 0 && ( +
  • + {scan.critical} critical +
  • + )} + {scan.high > 0 && ( +
  • {scan.high} high
  • + )} + {scan.medium > 0 && ( +
  • + {scan.medium} medium +
  • + )} +
+
+
+ +
+
+ ); +}; + +const styles = { + root: (theme) => ({ + backgroundColor: theme.palette.background.paper, + border: `1px solid ${theme.palette.divider}`, + borderLeft: 0, + borderRight: 0, + fontSize: 14, + padding: "24px 16px 24px 32px", + lineHeight: "1.5", + display: "flex", + alignItems: "center", + gap: 24, + }), + title: { + display: "block", + fontWeight: 500, + }, + issues: { + listStyle: "none", + margin: 0, + padding: 0, + fontSize: 13, + display: "flex", + alignItems: "center", + gap: 16, + marginTop: 4, + }, + issueItem: { + display: "flex", + alignItems: "center", + gap: 8, + + "&:before": { + content: '""', + display: "block", + width: 6, + height: 6, + borderRadius: "50%", + backgroundColor: "currentColor", + }, + }, + critical: (theme) => ({ + color: theme.experimental.roles.error.fill.solid, + }), + high: (theme) => ({ + color: theme.experimental.roles.warning.fill.solid, + }), + medium: (theme) => ({ + color: theme.experimental.roles.notice.fill.solid, + }), + link: { + marginLeft: "auto", + alignSelf: "flex-start", + }, +} satisfies Record>; diff --git a/site/src/pages/TemplatePage/TemplateVersionsPage/VersionsTable.stories.tsx b/site/src/pages/TemplatePage/TemplateVersionsPage/VersionsTable.stories.tsx index 5c64a4b510ee8..44e1feba47c12 100644 --- a/site/src/pages/TemplatePage/TemplateVersionsPage/VersionsTable.stories.tsx +++ b/site/src/pages/TemplatePage/TemplateVersionsPage/VersionsTable.stories.tsx @@ -13,6 +13,10 @@ import type { Meta, StoryObj } from "@storybook/react"; const meta: Meta = { title: "pages/TemplatePage/VersionsTable", component: VersionsTable, + args: { + onPromoteClick: () => {}, + onArchiveClick: () => {}, + }, }; export default meta; diff --git a/site/src/pages/UsersPage/ResetPasswordDialog.stories.tsx b/site/src/pages/UsersPage/ResetPasswordDialog.stories.tsx index fab79ed6c6a9e..ffa8a2468415e 100644 --- a/site/src/pages/UsersPage/ResetPasswordDialog.stories.tsx +++ b/site/src/pages/UsersPage/ResetPasswordDialog.stories.tsx @@ -16,6 +16,8 @@ const Example: Story = { open: true, user: MockUser, newPassword: "somerandomstringhere", + onConfirm: () => {}, + onClose: () => {}, }, };