mirror of
https://github.com/chaitin/MonkeyCode.git
synced 2026-02-04 07:43:28 +08:00
增加了代码安全扫描的前端功能
This commit is contained in:
@@ -843,17 +843,26 @@ export interface DomainSecurityScanningResult {
|
||||
export interface DomainSecurityScanningRiskDetail {
|
||||
/** 风险描述 */
|
||||
desc?: string;
|
||||
/** 风险代码行结束位置 */
|
||||
end?: GithubComChaitinMonkeyCodeBackendEntTypesPosition;
|
||||
/** 风险文件名 */
|
||||
filename?: string;
|
||||
/** 修复建议 */
|
||||
fix?: string;
|
||||
/** 风险id */
|
||||
id?: string;
|
||||
/** 风险等级 */
|
||||
level?: ConstsSecurityScanningRiskLevel;
|
||||
/** 风险代码行 */
|
||||
lines?: string;
|
||||
/** 风险代码行开始位置 */
|
||||
start?: GithubComChaitinMonkeyCodeBackendEntTypesPosition;
|
||||
}
|
||||
|
||||
export interface DomainSecurityScanningRiskResult {
|
||||
/** 高危数 */
|
||||
critical_count?: number;
|
||||
id?: string;
|
||||
/** 严重数 */
|
||||
severe_count?: number;
|
||||
/** 建议数 */
|
||||
@@ -1164,6 +1173,12 @@ export interface DomainWorkspaceFile {
|
||||
workspace_id?: string;
|
||||
}
|
||||
|
||||
export interface GithubComChaitinMonkeyCodeBackendEntTypesPosition {
|
||||
col?: number;
|
||||
line?: number;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
export interface InternalCodesnippetHandlerHttpV1GetContextReq {
|
||||
/** 返回结果数量限制,默认10 */
|
||||
limit?: number;
|
||||
|
||||
157
ui/src/components/codescan/taskDetail.tsx
Normal file
157
ui/src/components/codescan/taskDetail.tsx
Normal file
@@ -0,0 +1,157 @@
|
||||
import Card from '@/components/card';
|
||||
import { Ellipsis, Modal } from '@c-x/ui';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { DomainSecurityScanningResult, DomainSecurityScanningRiskDetail } from '@/api/types';
|
||||
import { getSecurityScanningDetail } from '@/api';
|
||||
import { Box, CircularProgress, List, ListItem, ListItemButton, Stack } from '@mui/material';
|
||||
|
||||
interface RiskLevelBoxProps {
|
||||
level: 'severe' | 'critical' | 'suggest';
|
||||
}
|
||||
|
||||
const RiskLevelBox = ({ level }: RiskLevelBoxProps) => {
|
||||
const riskConfig = {
|
||||
severe: {
|
||||
text: '严重',
|
||||
color: 'risk.severe',
|
||||
},
|
||||
critical: {
|
||||
text: '高风险',
|
||||
color: 'risk.critical',
|
||||
},
|
||||
suggest: {
|
||||
text: '低风险',
|
||||
color: 'risk.suggest',
|
||||
},
|
||||
};
|
||||
|
||||
const config = riskConfig[level];
|
||||
|
||||
if (!config) return null;
|
||||
|
||||
return (
|
||||
<Box sx={{
|
||||
backgroundColor: config.color,
|
||||
color: '#fff',
|
||||
borderRadius: '4px',
|
||||
textAlign: 'center',
|
||||
width: '80px',
|
||||
minWidth: '80px',
|
||||
fontSize: '12px',
|
||||
lineHeight: '20px'
|
||||
}}>
|
||||
{config.text}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const TaskDetail = ({
|
||||
task,
|
||||
open,
|
||||
onClose,
|
||||
}: {
|
||||
task?: DomainSecurityScanningResult;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [vulns, setVulns] = useState<DomainSecurityScanningRiskDetail[]>([]);
|
||||
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
const resp = await getSecurityScanningDetail({
|
||||
id: task?.id as string
|
||||
});
|
||||
setVulns(resp);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
console.log(task)
|
||||
if (open) {
|
||||
fetchData();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [task, open]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
<Ellipsis
|
||||
sx={{
|
||||
fontWeight: 'bold',
|
||||
fontSize: 20,
|
||||
lineHeight: '22px',
|
||||
width: 700,
|
||||
}}
|
||||
>
|
||||
{task?.name} / {task?.project_name}
|
||||
</Ellipsis>
|
||||
}
|
||||
sx={{
|
||||
'.MuiDialog-paper': {
|
||||
maxWidth: 1300,
|
||||
},
|
||||
}}
|
||||
width={1200}
|
||||
open={open}
|
||||
onCancel={onClose}
|
||||
footer={null}
|
||||
>
|
||||
<Card sx={{ p: 0, background: 'transparent', boxShadow: 'none' }}>
|
||||
<List>
|
||||
{loading ? (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', padding: '20px' }}>
|
||||
<CircularProgress />
|
||||
</div>
|
||||
) : (
|
||||
vulns.map((vuln) => (
|
||||
<ListItem key={vuln.id} sx={{
|
||||
padding: 0,
|
||||
width: '100%'
|
||||
}}>
|
||||
<ListItemButton sx={{
|
||||
borderBottomWidth: '1px',
|
||||
borderBottomStyle: 'solid',
|
||||
borderBottomColor: 'background.paper',
|
||||
fontSize: '14px',
|
||||
width: '100%'
|
||||
}}>
|
||||
<Stack direction={"column"} sx={{
|
||||
width: '100%'
|
||||
}}>
|
||||
<Stack direction={"row"}>
|
||||
<RiskLevelBox level={vuln.level as 'severe' | 'critical' | 'suggest'} />
|
||||
<Box sx={{
|
||||
fontSize: '14px',
|
||||
ml: '20px',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
lineHeight: '20px'
|
||||
}}>{vuln.desc}</Box>
|
||||
</Stack>
|
||||
<Box sx={{
|
||||
color: 'text.tertiary',
|
||||
fontSize: '14px',
|
||||
mt: '6px',
|
||||
width: '100%',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}>
|
||||
{vuln.filename}:{vuln?.start?.line}
|
||||
</Box>
|
||||
</Stack>
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
))
|
||||
)}
|
||||
</List>
|
||||
</Card>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskDetail;
|
||||
343
ui/src/components/codescan/taskList.tsx
Normal file
343
ui/src/components/codescan/taskList.tsx
Normal file
@@ -0,0 +1,343 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Table } from '@c-x/ui';
|
||||
import { getSecurityScanningList } from '@/api/SecurityScanning';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import Card from '@/components/card';
|
||||
import {
|
||||
Autocomplete,
|
||||
Box,
|
||||
Stack,
|
||||
TextField,
|
||||
Tooltip,
|
||||
} from '@mui/material';
|
||||
|
||||
import { ColumnsType } from '@c-x/ui/dist/Table';
|
||||
import { DomainSecurityScanningResult, DomainSecurityScanningRiskResult, DomainUser } from '@/api/types';
|
||||
import User from '@/components/user';
|
||||
import TaskDetail from './taskDetail';
|
||||
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';
|
||||
import AutoModeIcon from '@mui/icons-material/AutoMode';
|
||||
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
|
||||
import { getUserSecurityScanningList } from '@/api';
|
||||
|
||||
const CodeScanTaskList = ({
|
||||
admin,
|
||||
users
|
||||
}: {
|
||||
admin: boolean,
|
||||
users: DomainUser[]
|
||||
}) => {
|
||||
const [page, setPage] = useState(1);
|
||||
const [size, setSize] = useState(20);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [dataSource, setDataSource] = useState<DomainSecurityScanningResult[]>([]);
|
||||
const [detail, setDetail] = useState<DomainSecurityScanningResult | undefined>();
|
||||
const [filterUser, setFilterUser] = useState('');
|
||||
|
||||
const fetchData = async (params: {
|
||||
page?: number;
|
||||
size?: number;
|
||||
work_mode?: string;
|
||||
author?: string;
|
||||
}) => {
|
||||
setLoading(true);
|
||||
const res = await (admin ? getSecurityScanningList : getUserSecurityScanningList)({
|
||||
page: params.page || page,
|
||||
size: params.size || size,
|
||||
author: params.author || filterUser,
|
||||
});
|
||||
setLoading(false);
|
||||
setTotal(res.total_count || 0);
|
||||
setDataSource(res.items || []);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setPage(1);
|
||||
fetchData({
|
||||
page: 1,
|
||||
author: filterUser
|
||||
});
|
||||
}, [filterUser]);
|
||||
|
||||
const columns: ColumnsType<DomainSecurityScanningResult> = [
|
||||
{
|
||||
dataIndex: 'name',
|
||||
title: '扫描任务',
|
||||
width: 240,
|
||||
render: (project_name, record) => {
|
||||
return (
|
||||
<Stack direction='column'>
|
||||
<Box sx={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
{record?.name}
|
||||
</Box>
|
||||
<Box sx={{
|
||||
color: 'text.tertiary',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
mt: '4px'
|
||||
}}>
|
||||
{(record.status === 'pending') && <Stack direction={'row'}>
|
||||
<AutoModeIcon sx={{
|
||||
width: '16px',
|
||||
height: '16px',
|
||||
color: 'info.main'
|
||||
}} />
|
||||
<Box sx={{
|
||||
lineHeight: '16px',
|
||||
ml: '4px'
|
||||
}}>等待扫描</Box>
|
||||
</Stack>}
|
||||
{(record.status === 'running') && <Stack direction={'row'}>
|
||||
<AutoModeIcon sx={{
|
||||
width: '16px',
|
||||
height: '16px',
|
||||
color: 'info.main',
|
||||
animation: 'spin 1s linear infinite',
|
||||
'@keyframes spin': {
|
||||
'0%': {
|
||||
transform: 'rotate(0deg)',
|
||||
},
|
||||
'100%': {
|
||||
transform: 'rotate(360deg)',
|
||||
},
|
||||
},
|
||||
}} />
|
||||
<Box sx={{
|
||||
lineHeight: '16px',
|
||||
ml: '4px'
|
||||
}}>正在扫描</Box>
|
||||
</Stack>}
|
||||
{(record.status === 'success') && <Stack direction={'row'}>
|
||||
<CheckCircleOutlineIcon sx={{
|
||||
width: '16px',
|
||||
height: '16px',
|
||||
color: 'success.main'
|
||||
}} />
|
||||
<Box sx={{
|
||||
lineHeight: '16px',
|
||||
ml: '4px'
|
||||
}}>扫描完成</Box>
|
||||
</Stack>}
|
||||
{(record.status === 'failed') && <Stack direction={'row'}>
|
||||
<ErrorOutlineIcon sx={{
|
||||
width: '16px',
|
||||
height: '16px',
|
||||
color: 'error.main'
|
||||
}} />
|
||||
<Box sx={{
|
||||
lineHeight: '16px',
|
||||
ml: '4px'
|
||||
}}>扫描失败</Box>
|
||||
</Stack>}
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '项目名称',
|
||||
dataIndex: 'project_name',
|
||||
render: (project_name, record) => {
|
||||
return (
|
||||
<Stack direction='column'>
|
||||
<Box sx={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
{record?.project_name}
|
||||
</Box>
|
||||
<Box sx={{ color: 'text.secondary', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
{record?.path}
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: 'risk',
|
||||
title: '扫描结果',
|
||||
width: 260,
|
||||
render(risk: DomainSecurityScanningRiskResult, record) {
|
||||
const hasNoRisk = record.status !== 'pending' &&
|
||||
(!risk.severe_count || risk.severe_count <= 0) &&
|
||||
(!risk.critical_count || risk.critical_count <= 0) &&
|
||||
(!risk.suggest_count || risk.suggest_count <= 0);
|
||||
|
||||
const tip = []
|
||||
if (risk.severe_count && risk.severe_count > 0) {
|
||||
tip.push(`严重安全告警 ${risk.severe_count} 个`)
|
||||
}
|
||||
if (risk.critical_count && risk.critical_count > 0) {
|
||||
tip.push(`高风险安全提醒 ${risk.critical_count} 个`)
|
||||
}
|
||||
if (risk.suggest_count && risk.suggest_count > 0) {
|
||||
tip.push(`低风险安全提醒 ${risk.suggest_count} 个`)
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip title={ hasNoRisk ? '暂无风险' : tip.join(', ')}>
|
||||
<Stack direction='row'
|
||||
onClick={() => {
|
||||
if (!hasNoRisk) {
|
||||
setDetail(record)
|
||||
}
|
||||
}}
|
||||
sx={{
|
||||
color: '#fff',
|
||||
fontSize: '12px',
|
||||
width: '200px',
|
||||
height: '24px',
|
||||
lineHeight: '24px',
|
||||
background: (record.status === 'pending' || record.status === 'running') ? 'repeating-linear-gradient(45deg, #f0f0f0, #f0f0f0 10px, #e0e0e0 10px, #e0e0e0 20px)' : '#F1F2F8',
|
||||
backgroundSize: '30px 30px',
|
||||
animation: 'stripes 1s linear infinite',
|
||||
borderRadius: '4px',
|
||||
overflow: 'hidden',
|
||||
transition: 'box-shadow 0.3s ease',
|
||||
userSelect: 'none',
|
||||
'&:hover': {
|
||||
cursor: "pointer",
|
||||
boxShadow: hasNoRisk ? '' : '0 0px 8px #FFCF62',
|
||||
},
|
||||
'@keyframes stripes': {
|
||||
'0%': {
|
||||
backgroundPosition: '0 0',
|
||||
},
|
||||
'100%': {
|
||||
backgroundPosition: '30px 0',
|
||||
},
|
||||
},
|
||||
}}>
|
||||
{((record.status === 'success' || record.status === 'failed') && hasNoRisk) ? (
|
||||
// 如果没有风险,显示"无风险"
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
color: 'disabled.main',
|
||||
}}>
|
||||
暂无风险
|
||||
</Box>
|
||||
) : (
|
||||
// 否则,显示原有的风险条
|
||||
<>
|
||||
{!!risk.severe_count && risk.severe_count > 0 && <Box sx={{
|
||||
backgroundColor: 'risk.severe',
|
||||
minWidth: '30px',
|
||||
width: (risk.severe_count || 0) * 100 / ((risk.critical_count || 0) + (risk.severe_count || 0) + (risk.suggest_count || 0)) + '%',
|
||||
textAlign: 'center'
|
||||
}}>{risk.severe_count}</Box>}
|
||||
{!!risk.critical_count && risk.critical_count > 0 && <Box sx={{
|
||||
backgroundColor: 'risk.critical',
|
||||
minWidth: '30px',
|
||||
width: (risk.critical_count || 0) * 100 / ((risk.critical_count || 0) + (risk.severe_count || 0) + (risk.suggest_count || 0)) + '%',
|
||||
textAlign: 'center'
|
||||
}}>{risk.critical_count}</Box>}
|
||||
{!!risk.suggest_count && risk.suggest_count > 0 && <Box sx={{
|
||||
backgroundColor: 'risk.suggest',
|
||||
minWidth: '30px',
|
||||
width: (risk.suggest_count || 0) * 100 / ((risk.critical_count || 0) + (risk.severe_count || 0) + (risk.suggest_count || 0)) + '%',
|
||||
textAlign: 'center'
|
||||
}}>{risk.suggest_count}</Box>}
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</Tooltip>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: 'user',
|
||||
title: '成员',
|
||||
width: 200,
|
||||
render(value: DomainUser) {
|
||||
return (
|
||||
<User
|
||||
id={value.id!}
|
||||
username={value.username!}
|
||||
email={value.email!}
|
||||
avatar={value.avatar_url!}
|
||||
deleted={value.is_deleted!}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '扫描时间',
|
||||
dataIndex: 'created_at',
|
||||
width: 160,
|
||||
render: (text) => {
|
||||
return (
|
||||
<Stack direction='column'>
|
||||
<Box sx={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
{dayjs.unix(text).format('YYYY-MM-DD')}
|
||||
</Box>
|
||||
<Box sx={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
{dayjs.unix(text).format('HH:mm:ss')}
|
||||
</Box>
|
||||
</Stack>
|
||||
)
|
||||
},
|
||||
},
|
||||
];
|
||||
return (
|
||||
<Card sx={{ flex: 1, height: '100%' }}>
|
||||
{admin && <Stack direction='row' spacing={2} sx={{ mb: 2 }}>
|
||||
<Autocomplete
|
||||
size='small'
|
||||
sx={{ minWidth: 220 }}
|
||||
options={users || []}
|
||||
getOptionLabel={(option) => option.username || ''}
|
||||
value={
|
||||
users?.find((item) => item.username === filterUser) ||
|
||||
null
|
||||
}
|
||||
onChange={(_, newValue) =>
|
||||
setFilterUser(newValue ? newValue.username! : '')
|
||||
}
|
||||
isOptionEqualToValue={(option, value) =>
|
||||
option.username === value.username
|
||||
}
|
||||
renderInput={(params) => <TextField {...params} label='成员' />}
|
||||
clearOnEscape
|
||||
/>
|
||||
</Stack>}
|
||||
<Table
|
||||
height={admin ? 'calc(100% - 52px)' : '100%'}
|
||||
sx={{ mx: -2 }}
|
||||
PaginationProps={{
|
||||
sx: {
|
||||
pt: 2,
|
||||
mx: 2,
|
||||
},
|
||||
}}
|
||||
loading={loading}
|
||||
columns={columns}
|
||||
dataSource={dataSource}
|
||||
rowKey='id'
|
||||
pagination={{
|
||||
page,
|
||||
pageSize: size,
|
||||
total,
|
||||
onChange: (page: number, size: number) => {
|
||||
setPage(page);
|
||||
setSize(size);
|
||||
fetchData({
|
||||
page: page,
|
||||
size: size,
|
||||
});
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<TaskDetail
|
||||
open={!!detail}
|
||||
onClose={() => setDetail(undefined)}
|
||||
task={detail}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default CodeScanTaskList;
|
||||
@@ -7,6 +7,7 @@ const ADMIN_BREADCRUMB_MAP: Record<string, { title: string; to: string }> = {
|
||||
dashboard: { title: '仪表盘', to: '/' },
|
||||
chat: { title: '对话记录', to: '/chat' },
|
||||
completion: { title: '补全记录', to: '/completion' },
|
||||
codescan: { title: '代码安全', to: '/codescan' },
|
||||
model: { title: '模型管理', to: '/model' },
|
||||
'member-management': { title: '成员管理', to: '/member-management' },
|
||||
admin: { title: '管理员', to: '/admin' },
|
||||
@@ -16,6 +17,7 @@ const USER_BREADCRUMB_MAP: Record<string, { title: string; to: string }> = {
|
||||
dashboard: { title: '仪表盘', to: '/user/dashboard' },
|
||||
chat: { title: '对话记录', to: '/user/chat' },
|
||||
completion: { title: '补全记录', to: '/user/completion' },
|
||||
codescan: { title: '代码安全', to: '/user/codescan' },
|
||||
};
|
||||
|
||||
const Bread = () => {
|
||||
|
||||
@@ -43,9 +43,8 @@ const Header = () => {
|
||||
>
|
||||
下载客户端
|
||||
</Button>
|
||||
<Tooltip title='退出登录' arrow>
|
||||
<Tooltip title='退出登录'>
|
||||
<IconButton
|
||||
title='退出登录'
|
||||
size='small'
|
||||
sx={{
|
||||
bgcolor: '#fff',
|
||||
|
||||
@@ -35,8 +35,8 @@ const ADMIN_MENUS = [
|
||||
},
|
||||
{
|
||||
label: '代码安全',
|
||||
value: '/code-security',
|
||||
pathname: 'code-security',
|
||||
value: '/codescan',
|
||||
pathname: 'codescan',
|
||||
icon: 'icon-daimaanquan1',
|
||||
show: true,
|
||||
disabled: false,
|
||||
@@ -92,6 +92,14 @@ const USER_MENUS = [
|
||||
show: true,
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
label: '代码安全',
|
||||
value: '/user/codescan',
|
||||
pathname: '/user/codescan',
|
||||
icon: 'icon-daimaanquan1',
|
||||
show: true,
|
||||
disabled: false,
|
||||
},
|
||||
// {
|
||||
// label: '设置',
|
||||
// value: '/user/setting',
|
||||
|
||||
@@ -1,279 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Table } from '@c-x/ui';
|
||||
import { getSecurityScanningList, getSecurityScanningDetail } from '@/api/SecurityScanning';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import Card from '@/components/card';
|
||||
import {
|
||||
Autocomplete,
|
||||
Box,
|
||||
Stack,
|
||||
TextField,
|
||||
Tooltip,
|
||||
} from '@mui/material';
|
||||
|
||||
import { ColumnsType } from '@c-x/ui/dist/Table';
|
||||
import { useRequest } from 'ahooks';
|
||||
import { getListUser } from '@/api/User';
|
||||
import { DomainSecurityScanningResult, DomainSecurityScanningRiskResult, DomainUser } from '@/api/types';
|
||||
import User from '@/components/user';
|
||||
|
||||
const StatusText = {
|
||||
pending: '等待扫描',
|
||||
running: '正在扫描',
|
||||
success: '扫描完成',
|
||||
failed: '扫描失败',
|
||||
}
|
||||
|
||||
const Chat = () => {
|
||||
const [page, setPage] = useState(1);
|
||||
const [size, setSize] = useState(20);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [dataSource, setDataSource] = useState<DomainSecurityScanningResult[]>([]);
|
||||
|
||||
const [filterUser, setFilterUser] = useState('');
|
||||
|
||||
|
||||
const { data: userOptions = { users: [] } } = useRequest(() =>
|
||||
getListUser({
|
||||
page: 1,
|
||||
size: 9999,
|
||||
})
|
||||
);
|
||||
|
||||
const fetchData = async (params: {
|
||||
page?: number;
|
||||
size?: number;
|
||||
work_mode?: string;
|
||||
author?: string;
|
||||
}) => {
|
||||
setLoading(true);
|
||||
const res = await getSecurityScanningList({
|
||||
page: params.page || page,
|
||||
size: params.size || size,
|
||||
author: params.author || filterUser,
|
||||
});
|
||||
setLoading(false);
|
||||
setTotal(res.total_count || 0);
|
||||
setDataSource(res.items || []);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setPage(1);
|
||||
fetchData({
|
||||
page: 1,
|
||||
author: filterUser
|
||||
});
|
||||
}, [filterUser]);
|
||||
|
||||
const columns: ColumnsType<DomainSecurityScanningResult> = [
|
||||
{
|
||||
dataIndex: 'name',
|
||||
title: '扫描任务',
|
||||
width: 180,
|
||||
render: (project_name, record) => {
|
||||
return (
|
||||
<Stack direction='column'>
|
||||
<Box sx={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
{record?.name}
|
||||
</Box>
|
||||
<Box sx={{ color: 'text.secondary', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
{record?.status ? StatusText[record.status] : '未知状态'}
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '项目名称',
|
||||
dataIndex: 'project_name',
|
||||
render: (project_name, record) => {
|
||||
return (
|
||||
<Stack direction='column'>
|
||||
<Box sx={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
{record?.project_name}
|
||||
</Box>
|
||||
<Box sx={{ color: 'text.secondary', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
{record?.path}
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: 'risk',
|
||||
title: '扫描结果',
|
||||
render(risk: DomainSecurityScanningRiskResult, record) {
|
||||
const hasNoRisk = record.status !== 'pending' &&
|
||||
(!risk.severe_count || risk.severe_count <= 0) &&
|
||||
(!risk.critical_count || risk.critical_count <= 0) &&
|
||||
(!risk.suggest_count || risk.suggest_count <= 0);
|
||||
|
||||
const tip = []
|
||||
if (risk.severe_count && risk.severe_count > 0) {
|
||||
tip.push(`严重安全告警 ${risk.severe_count} 个`)
|
||||
}
|
||||
if (risk.critical_count && risk.critical_count > 0) {
|
||||
tip.push(`高风险安全提醒 ${risk.critical_count} 个`)
|
||||
}
|
||||
if (risk.suggest_count && risk.suggest_count > 0) {
|
||||
tip.push(`低风险安全提醒 ${risk.suggest_count} 个`)
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip title={ hasNoRisk ? '暂无风险' : tip.join(', ')}>
|
||||
<Stack direction='row' sx={{
|
||||
color: '#fff',
|
||||
fontSize: '12px',
|
||||
width: '200px',
|
||||
height: '24px',
|
||||
lineHeight: '24px',
|
||||
background: record.status === 'pending' ? 'repeating-linear-gradient(45deg, #f0f0f0, #f0f0f0 10px, #e0e0e0 10px, #e0e0e0 20px)' : '#F1F2F8',
|
||||
backgroundSize: '30px 30px',
|
||||
animation: 'stripes 1s linear infinite',
|
||||
borderRadius: '4px',
|
||||
overflow: 'hidden',
|
||||
transition: 'box-shadow 0.3s ease',
|
||||
userSelect: 'none',
|
||||
'&:hover': {
|
||||
cursor: "pointer",
|
||||
boxShadow: hasNoRisk ? '' : '0 0px 8px #FFCF62',
|
||||
},
|
||||
'@keyframes stripes': {
|
||||
'0%': {
|
||||
backgroundPosition: '0 0',
|
||||
},
|
||||
'100%': {
|
||||
backgroundPosition: '30px 0',
|
||||
},
|
||||
},
|
||||
}}>
|
||||
{hasNoRisk ? (
|
||||
// 如果没有风险,显示"无风险"
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
color: 'disabled.main',
|
||||
}}>
|
||||
暂无风险
|
||||
</Box>
|
||||
) : (
|
||||
// 否则,显示原有的风险条
|
||||
<>
|
||||
{!!risk.severe_count && risk.severe_count > 0 && <Box sx={{
|
||||
backgroundColor: 'risk.severe',
|
||||
minWidth: '24px',
|
||||
width: (risk.severe_count || 0) * 100 / ((risk.critical_count || 0) + (risk.severe_count || 0) + (risk.suggest_count || 0)) + '%',
|
||||
textAlign: 'center'
|
||||
}}>{risk.severe_count}</Box>}
|
||||
{!!risk.critical_count && risk.critical_count > 0 && <Box sx={{
|
||||
backgroundColor: 'risk.critical',
|
||||
minWidth: '24px',
|
||||
width: (risk.critical_count || 0) * 100 / ((risk.critical_count || 0) + (risk.severe_count || 0) + (risk.suggest_count || 0)) + '%',
|
||||
textAlign: 'center'
|
||||
}}>{risk.critical_count}</Box>}
|
||||
{!!risk.suggest_count && risk.suggest_count > 0 && <Box sx={{
|
||||
backgroundColor: 'risk.suggest',
|
||||
minWidth: '24px',
|
||||
width: (risk.suggest_count || 0) * 100 / ((risk.critical_count || 0) + (risk.severe_count || 0) + (risk.suggest_count || 0)) + '%',
|
||||
textAlign: 'center'
|
||||
}}>{risk.suggest_count}</Box>}
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</Tooltip>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: 'user',
|
||||
title: '成员',
|
||||
width: 200,
|
||||
render(value: DomainUser) {
|
||||
return (
|
||||
<User
|
||||
id={value.id!}
|
||||
username={value.username!}
|
||||
email={value.email!}
|
||||
avatar={value.avatar_url!}
|
||||
deleted={value.is_deleted!}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '扫描时间',
|
||||
dataIndex: 'created_at',
|
||||
width: 160,
|
||||
render: (text) => {
|
||||
return (
|
||||
<Stack direction='column'>
|
||||
<Box sx={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
{dayjs.unix(text).format('YYYY-MM-DD')}
|
||||
</Box>
|
||||
<Box sx={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
{dayjs.unix(text).format('HH:mm:ss')}
|
||||
</Box>
|
||||
</Stack>
|
||||
)
|
||||
},
|
||||
},
|
||||
];
|
||||
return (
|
||||
<Card sx={{ flex: 1, height: '100%' }}>
|
||||
<Stack direction='row' spacing={2} sx={{ mb: 2 }}>
|
||||
<Autocomplete
|
||||
size='small'
|
||||
sx={{ minWidth: 220 }}
|
||||
options={userOptions.users || []}
|
||||
getOptionLabel={(option) => option.username || ''}
|
||||
value={
|
||||
userOptions.users?.find((item) => item.username === filterUser) ||
|
||||
null
|
||||
}
|
||||
onChange={(_, newValue) =>
|
||||
setFilterUser(newValue ? newValue.username! : '')
|
||||
}
|
||||
isOptionEqualToValue={(option, value) =>
|
||||
option.username === value.username
|
||||
}
|
||||
renderInput={(params) => <TextField {...params} label='成员' />}
|
||||
clearOnEscape
|
||||
/>
|
||||
</Stack>
|
||||
<Table
|
||||
height='calc(100% - 52px)'
|
||||
sx={{ mx: -2 }}
|
||||
PaginationProps={{
|
||||
sx: {
|
||||
pt: 2,
|
||||
mx: 2,
|
||||
},
|
||||
}}
|
||||
loading={loading}
|
||||
columns={columns}
|
||||
dataSource={dataSource}
|
||||
rowKey='id'
|
||||
pagination={{
|
||||
page,
|
||||
pageSize: size,
|
||||
total,
|
||||
onChange: (page: number, size: number) => {
|
||||
setPage(page);
|
||||
setSize(size);
|
||||
fetchData({
|
||||
page: page,
|
||||
size: size,
|
||||
});
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default Chat;
|
||||
19
ui/src/pages/codescan/index.tsx
Normal file
19
ui/src/pages/codescan/index.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { useRequest } from 'ahooks';
|
||||
import { getListUser } from '@/api/User';
|
||||
import CodeScanTaskList from '../../components/codescan/taskList';
|
||||
|
||||
const AdminCodeScanTaskList = (
|
||||
) => {
|
||||
const res = useRequest(() => {
|
||||
return getListUser({
|
||||
page: 1,
|
||||
size: 9999,
|
||||
})
|
||||
})
|
||||
|
||||
return (
|
||||
<CodeScanTaskList admin={true} users={res.data?.users || []}/>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminCodeScanTaskList;
|
||||
10
ui/src/pages/user/codescan/index.tsx
Normal file
10
ui/src/pages/user/codescan/index.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import CodeScanTaskList from '../../../components/codescan/taskList';
|
||||
|
||||
const AdminCodeScanTaskList = (
|
||||
) => {
|
||||
return (
|
||||
<CodeScanTaskList admin={false} users={[]}/>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminCodeScanTaskList;
|
||||
@@ -40,7 +40,8 @@ const Login = LazyLoadable(lazy(() => import('@/pages/login')));
|
||||
const UserLogin = LazyLoadable(lazy(() => import('@/pages/user/login')));
|
||||
const Expectation = LazyLoadable(lazy(() => import('@/pages/expectation')));
|
||||
const UserChat = LazyLoadable(lazy(() => import('@/pages/user/chat')));
|
||||
const CodeSecurity = LazyLoadable(lazy(() => import('@/pages/codeSecurity')));
|
||||
const AdminCodeScan = LazyLoadable(lazy(() => import('@/pages/codescan')));
|
||||
const UserCodeScan = LazyLoadable(lazy(() => import('@/pages/user/codescan')));
|
||||
const UserCompletion = LazyLoadable(
|
||||
lazy(() => import('@/pages/user/completion'))
|
||||
);
|
||||
@@ -68,8 +69,8 @@ const routerConfig = [
|
||||
element: <Chat />,
|
||||
},
|
||||
{
|
||||
path: 'code-security',
|
||||
element: <CodeSecurity />,
|
||||
path: 'codescan',
|
||||
element: <AdminCodeScan/>,
|
||||
},
|
||||
{
|
||||
path: 'completion',
|
||||
@@ -110,6 +111,10 @@ const routerConfig = [
|
||||
path: 'completion',
|
||||
element: <UserCompletion />,
|
||||
},
|
||||
{
|
||||
path: 'codescan',
|
||||
element: <UserCodeScan />,
|
||||
},
|
||||
{
|
||||
path: 'setting',
|
||||
element: <UserSetting />,
|
||||
|
||||
Reference in New Issue
Block a user