Skip to content

Commit da85eec

Browse files
committed
feat(site): display xray scan result in the agent
1 parent e26ba1a commit da85eec

File tree

5 files changed

+180
-2
lines changed

5 files changed

+180
-2
lines changed

site/src/api/api.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import axios from "axios";
1+
import axios, { isAxiosError } from "axios";
22
import dayjs from "dayjs";
33
import * as TypesGen from "./typesGenerated";
44
// This needs to include the `../`, otherwise it breaks when importing into
@@ -1696,3 +1696,27 @@ export const putFavoriteWorkspace = async (workspaceID: string) => {
16961696
export const deleteFavoriteWorkspace = async (workspaceID: string) => {
16971697
await axios.delete(`/api/v2/workspaces/${workspaceID}/favorite`);
16981698
};
1699+
1700+
export type GetJFrogXRayScanParams = {
1701+
workspaceId: string;
1702+
agentId: string;
1703+
};
1704+
1705+
export const getJFrogXRayScan = async (options: GetJFrogXRayScanParams) => {
1706+
const searchParams = new URLSearchParams({
1707+
workspace_id: options.workspaceId,
1708+
agent_id: options.agentId,
1709+
});
1710+
1711+
try {
1712+
const res = await axios.get<TypesGen.JFrogXrayScan>(
1713+
`/api/v2/integrations/jfrog/xray-scan?${searchParams}`,
1714+
);
1715+
return res.data;
1716+
} catch (error) {
1717+
if (isAxiosError(error) && error.response?.status === 404) {
1718+
// react-query library does not allow undefined to be returned as a query result
1719+
return null;
1720+
}
1721+
}
1722+
};

site/src/api/queries/integrations.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { GetJFrogXRayScanParams } from "api/api";
2+
import * as API from "api/api";
3+
4+
export const xrayScan = (params: GetJFrogXRayScanParams) => {
5+
return {
6+
queryKey: ["xray", params],
7+
queryFn: () => API.getJFrogXRayScan(params),
8+
};
9+
};

site/src/modules/resources/AgentRow.stories.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,3 +311,24 @@ export const Deprecated: Story = {
311311
serverAPIVersion: "2.0",
312312
},
313313
};
314+
315+
export const WithXRayScan: Story = {
316+
parameters: {
317+
queries: [
318+
{
319+
key: [
320+
"xray",
321+
{ agentId: MockWorkspaceAgent.id, workspaceId: MockWorkspace.id },
322+
],
323+
data: {
324+
workspace_id: MockWorkspace.id,
325+
agent_id: MockWorkspaceAgent.id,
326+
critical: 10,
327+
high: 3,
328+
medium: 5,
329+
results_url: "http://localhost:8080",
330+
},
331+
},
332+
],
333+
},
334+
};

site/src/modules/resources/AgentRow.tsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ import { PortForwardButton } from "./PortForwardButton";
3737
import { SSHButton } from "./SSHButton/SSHButton";
3838
import { TerminalLink } from "./TerminalLink/TerminalLink";
3939
import { VSCodeDesktopButton } from "./VSCodeDesktopButton/VSCodeDesktopButton";
40+
import { useQuery } from "react-query";
41+
import { xrayScan } from "api/queries/integrations";
42+
import { XRayScanAlert } from "./XRayScanAlert";
4043

4144
// Logs are stored as the Line interface to make rendering
4245
// much more efficient. Instead of mapping objects each time, we're
@@ -74,6 +77,12 @@ export const AgentRow: FC<AgentRowProps> = ({
7477
sshPrefix,
7578
storybookLogs,
7679
}) => {
80+
// XRay integration
81+
const xrayScanQuery = useQuery(
82+
xrayScan({ workspaceId: workspace.id, agentId: agent.id }),
83+
);
84+
85+
// Apps visibility
7786
const hasAppsToDisplay = !hideVSCodeDesktopButton || agent.apps.length > 0;
7887
const shouldDisplayApps =
7988
showApps &&
@@ -84,6 +93,7 @@ export const AgentRow: FC<AgentRowProps> = ({
8493
agent.display_apps.includes("vscode_insiders");
8594
const showVSCode = hasVSCodeApp && !hideVSCodeDesktopButton;
8695

96+
// Agent runtime logs
8797
const logSourceByID = useMemo(() => {
8898
const sources: { [id: string]: WorkspaceAgentLogSource } = {};
8999
for (const source of agent.log_sources) {
@@ -216,6 +226,8 @@ export const AgentRow: FC<AgentRowProps> = ({
216226
)}
217227
</header>
218228

229+
{xrayScanQuery.data && <XRayScanAlert scan={xrayScanQuery.data} />}
230+
219231
<div css={styles.content}>
220232
{agent.status === "connected" && (
221233
<section css={styles.apps}>
@@ -276,7 +288,9 @@ export const AgentRow: FC<AgentRowProps> = ({
276288

277289
{hasStartupFeatures && (
278290
<section
279-
css={(theme) => ({ borderTop: `1px solid ${theme.palette.divider}` })}
291+
css={(theme) => ({
292+
borderTop: `1px solid ${theme.palette.divider}`,
293+
})}
280294
>
281295
<Collapse in={showLogs}>
282296
<AutoSizer disableHeight>
@@ -571,6 +585,10 @@ const styles = {
571585
flexWrap: "wrap",
572586
lineHeight: "1.5",
573587

588+
"&:has(+ [role='alert'])": {
589+
paddingBottom: 16,
590+
},
591+
574592
[theme.breakpoints.down("md")]: {
575593
gap: 16,
576594
},
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { Interpolation, Theme } from "@emotion/react";
2+
import Button from "@mui/material/Button";
3+
import { JFrogXrayScan } from "api/typesGenerated";
4+
import { ExternalImage } from "components/ExternalImage/ExternalImage";
5+
import { FC } from "react";
6+
7+
export const XRayScanAlert: FC<{ scan: JFrogXrayScan }> = ({ scan }) => {
8+
return (
9+
<div role="alert" css={styles.root}>
10+
<ExternalImage
11+
alt="JFrog logo"
12+
src="/icon/jfrog.svg"
13+
css={{ width: 40, height: 40 }}
14+
/>
15+
<div>
16+
<span css={styles.title}>
17+
JFrog Xray detected new vulnerabilities for this agent
18+
</span>
19+
20+
<ul css={styles.issues}>
21+
{scan.critical > 0 && (
22+
<li css={[styles.critical, styles.issueItem]}>
23+
{scan.critical} critical
24+
</li>
25+
)}
26+
{scan.high > 0 && (
27+
<li css={[styles.high, styles.issueItem]}>{scan.high} high</li>
28+
)}
29+
{scan.medium > 0 && (
30+
<li css={[styles.medium, styles.issueItem]}>
31+
{scan.medium} medium
32+
</li>
33+
)}
34+
</ul>
35+
</div>
36+
<div css={styles.link}>
37+
<Button
38+
component="a"
39+
size="small"
40+
variant="text"
41+
href={scan.results_url}
42+
target="_blank"
43+
rel="noreferrer"
44+
>
45+
Review results
46+
</Button>
47+
</div>
48+
</div>
49+
);
50+
};
51+
52+
const styles = {
53+
root: (theme) => ({
54+
backgroundColor: theme.palette.background.paper,
55+
border: `1px solid ${theme.palette.divider}`,
56+
borderLeft: 0,
57+
borderRight: 0,
58+
fontSize: 14,
59+
padding: "24px 16px 24px 32px",
60+
lineHeight: "1.5",
61+
display: "flex",
62+
alignItems: "center",
63+
gap: 24,
64+
}),
65+
title: {
66+
display: "block",
67+
fontWeight: 500,
68+
},
69+
issues: {
70+
listStyle: "none",
71+
margin: 0,
72+
padding: 0,
73+
fontSize: 13,
74+
display: "flex",
75+
alignItems: "center",
76+
gap: 16,
77+
marginTop: 4,
78+
},
79+
issueItem: {
80+
display: "flex",
81+
alignItems: "center",
82+
gap: 8,
83+
84+
"&:before": {
85+
content: '""',
86+
display: "block",
87+
width: 6,
88+
height: 6,
89+
borderRadius: "50%",
90+
backgroundColor: "currentColor",
91+
},
92+
},
93+
critical: (theme) => ({
94+
color: theme.experimental.roles.error.fill.solid,
95+
}),
96+
high: (theme) => ({
97+
color: theme.experimental.roles.warning.fill.solid,
98+
}),
99+
medium: (theme) => ({
100+
color: theme.experimental.roles.notice.fill.solid,
101+
}),
102+
link: {
103+
marginLeft: "auto",
104+
alignSelf: "flex-start",
105+
},
106+
} satisfies Record<string, Interpolation<Theme>>;

0 commit comments

Comments
 (0)