mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat(site): display xray scan result in the agent (#11955)
This commit is contained in:
+25
-1
@@ -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<TypesGen.JFrogXrayScan>(
|
||||
`/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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
};
|
||||
@@ -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",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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<AgentRowProps> = ({
|
||||
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<AgentRowProps> = ({
|
||||
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<AgentRowProps> = ({
|
||||
)}
|
||||
</header>
|
||||
|
||||
{xrayScanQuery.data && <XRayScanAlert scan={xrayScanQuery.data} />}
|
||||
|
||||
<div css={styles.content}>
|
||||
{agent.status === "connected" && (
|
||||
<section css={styles.apps}>
|
||||
@@ -276,7 +288,9 @@ export const AgentRow: FC<AgentRowProps> = ({
|
||||
|
||||
{hasStartupFeatures && (
|
||||
<section
|
||||
css={(theme) => ({ borderTop: `1px solid ${theme.palette.divider}` })}
|
||||
css={(theme) => ({
|
||||
borderTop: `1px solid ${theme.palette.divider}`,
|
||||
})}
|
||||
>
|
||||
<Collapse in={showLogs}>
|
||||
<AutoSizer disableHeight>
|
||||
@@ -571,6 +585,10 @@ const styles = {
|
||||
flexWrap: "wrap",
|
||||
lineHeight: "1.5",
|
||||
|
||||
"&:has(+ [role='alert'])": {
|
||||
paddingBottom: 16,
|
||||
},
|
||||
|
||||
[theme.breakpoints.down("md")]: {
|
||||
gap: 16,
|
||||
},
|
||||
|
||||
@@ -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 (
|
||||
<div role="alert" css={styles.root}>
|
||||
<ExternalImage
|
||||
alt="JFrog logo"
|
||||
src="/icon/jfrog.svg"
|
||||
css={{ width: 40, height: 40 }}
|
||||
/>
|
||||
<div>
|
||||
<span css={styles.title}>
|
||||
JFrog Xray detected new vulnerabilities for this agent
|
||||
</span>
|
||||
|
||||
<ul css={styles.issues}>
|
||||
{scan.critical > 0 && (
|
||||
<li css={[styles.critical, styles.issueItem]}>
|
||||
{scan.critical} critical
|
||||
</li>
|
||||
)}
|
||||
{scan.high > 0 && (
|
||||
<li css={[styles.high, styles.issueItem]}>{scan.high} high</li>
|
||||
)}
|
||||
{scan.medium > 0 && (
|
||||
<li css={[styles.medium, styles.issueItem]}>
|
||||
{scan.medium} medium
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
<div css={styles.link}>
|
||||
<Button
|
||||
component="a"
|
||||
size="small"
|
||||
variant="text"
|
||||
href={scan.results_url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Review results
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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<string, Interpolation<Theme>>;
|
||||
@@ -13,6 +13,10 @@ import type { Meta, StoryObj } from "@storybook/react";
|
||||
const meta: Meta<typeof VersionsTable> = {
|
||||
title: "pages/TemplatePage/VersionsTable",
|
||||
component: VersionsTable,
|
||||
args: {
|
||||
onPromoteClick: () => {},
|
||||
onArchiveClick: () => {},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
@@ -16,6 +16,8 @@ const Example: Story = {
|
||||
open: true,
|
||||
user: MockUser,
|
||||
newPassword: "somerandomstringhere",
|
||||
onConfirm: () => {},
|
||||
onClose: () => {},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user