Files
MonkeyCode/ui/src/components/codescan/taskDetail.tsx
2025-08-08 17:17:37 +08:00

352 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Card from '@/components/card';
import { Ellipsis, Modal } from '@c-x/ui';
import { useEffect, useRef, useState } from 'react';
import { DomainSecurityScanningResult, DomainSecurityScanningRiskDetail } from '@/api/types';
import { getSecurityScanningDetail, getUserSecurityScanningDetail } from '@/api';
import { Box, CircularProgress, Grid2 as Grid, List, ListItem, ListItemButton, Stack, ToggleButton, ToggleButtonGroup } from '@mui/material';
import ListAltIcon from '@mui/icons-material/ListAlt';
import ViewSidebarOutlinedIcon from '@mui/icons-material/ViewSidebarOutlined';
import MonacoEditor from '@monaco-editor/react';
import type * as Monaco from 'monaco-editor';
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 getLanguageByFilename = (filename: string = ''): string => {
const extension = filename.split('.').pop()?.toLowerCase() || '';
const languageMap: Record<string, string> = {
js: 'javascript',
jsx: 'javascript',
ts: 'typescript',
tsx: 'typescript',
json: 'json',
html: 'html',
htm: 'html',
css: 'css',
scss: 'scss',
sass: 'sass',
less: 'less',
md: 'markdown',
markdown: 'markdown',
py: 'python',
pyw: 'python',
java: 'java',
c: 'c',
cpp: 'cpp',
cc: 'cpp',
cxx: 'cpp',
h: 'c',
hpp: 'cpp',
cs: 'csharp',
php: 'php',
php3: 'php',
php4: 'php',
php5: 'php',
phtml: 'php',
rb: 'ruby',
go: 'go',
rs: 'rust',
swift: 'swift',
kt: 'kotlin',
kts: 'kotlin',
scala: 'scala',
sh: 'shell',
bash: 'shell',
sql: 'sql',
yaml: 'yaml',
yml: 'yaml',
xml: 'xml',
vue: 'vue',
svelte: 'svelte',
};
return languageMap[extension] || 'plaintext';
};
const TaskDetail = ({
task,
open,
onClose,
admin,
}: {
task?: DomainSecurityScanningResult;
open: boolean;
onClose: () => void;
admin: boolean
}) => {
const [loading, setLoading] = useState(true);
const [vulns, setVulns] = useState<DomainSecurityScanningRiskDetail[]>([]);
const [vulDetail, setVulDetail] = useState<DomainSecurityScanningRiskDetail | undefined>(undefined);
const [viewMode, setViewMode] = useState<'list' | 'detail'>('detail');
const fetchData = async () => {
setLoading(true);
const resp = await (admin ? getSecurityScanningDetail : getUserSecurityScanningDetail)({
id: task?.id as string
});
setVulns(resp);
setLoading(false);
};
useEffect(() => {
setVulns([]);
setVulDetail(undefined);
setViewMode('detail');
console.log(!!vulDetail)
if (open) {
fetchData();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [task, open]);
// 保存编辑器实例的引用
const editorRef = useRef<Monaco.editor.IStandaloneCodeEditor | null>(null);
// 监听 vulDetail 的变化,重新高亮显示
useEffect(() => {
if (editorRef.current) {
highlightVulnerability(editorRef.current, vulDetail);
}
}, [vulDetail]);
// 高亮显示漏洞代码
const highlightVulnerability = (editor: Monaco.editor.IStandaloneCodeEditor, vulDetail: DomainSecurityScanningRiskDetail | undefined) => {
// 清除之前的装饰器
editor.deltaDecorations(editor.getModel()?.getAllDecorations().map(d => d.id) || [], []);
// 如果有 start 和 end 位置信息,则设置选区
if (vulDetail?.start && vulDetail?.end) {
const startLine = vulDetail.start.line ?? 1;
const startColumn = vulDetail.start.col ?? 1;
const endLine = vulDetail.end.line ?? startLine;
const endColumn = vulDetail.end.col ?? startColumn;
// 设置选区
const selection = {
startLineNumber: startLine,
startColumn: startColumn,
endLineNumber: endLine,
endColumn: endColumn
};
editor.setSelection(selection);
// 添加装饰器以增强高亮效果
editor.deltaDecorations([], [
{
range: selection,
options: {
isWholeLine: false,
className: 'highlighted-code',
inlineClassName: 'highlighted-code-inline',
overviewRuler: {
color: 'rgba(255, 255, 0, 0.5)',
position: 1 // Monaco.Editor.OverviewRulerLane.Center
}
}
}
]);
// 滚动到选区
editor.revealLineInCenter(startLine);
}
};
const handleEditorDidMount = (editor: Monaco.editor.IStandaloneCodeEditor) => {
// 保存编辑器实例
editorRef.current = editor;
// 初始高亮
highlightVulnerability(editor, vulDetail);
};
return (
<Modal title={
<Stack direction={'row'} >
{vulDetail && <ToggleButtonGroup
exclusive
value={viewMode}
size="small"
onChange={(e, value) => {
setViewMode(value)
}}
>
<ToggleButton value="list">
<ListAltIcon />
</ToggleButton>
<ToggleButton value="detail" disabled={!vulDetail}>
<ViewSidebarOutlinedIcon sx={{ transform: 'rotate(180deg)' }} />
</ToggleButton>
</ToggleButtonGroup>}
<Ellipsis
sx={{
fontWeight: 'bold',
fontSize: 20,
lineHeight: '40px',
width: 700,
ml: '20px'
}}
>
{task?.name} / {task?.project_name}
</Ellipsis>
</Stack>
}
width={1200}
open={open}
onCancel={onClose}
footer={null}
>
<Card sx={{ p: 0, background: 'transparent', boxShadow: 'none' }}>
<Grid container sx={{ height: '70vh' }}>
<Grid size={viewMode === 'detail' && !!vulDetail ? 5 : 12} sx={{
height: '100%',
overflow: 'auto'
}}>
<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
selected={vulDetail?.id === vuln.id}
onClick={() => {
setVulDetail(vuln);
}}
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>
{!!vulDetail && vulDetail?.id === vuln.id && <Box sx={{
fontSize: '12px',
mt: '6px',
}}> {vulDetail?.start?.line} {vulDetail?.start?.line !== vulDetail?.end?.line && `至第 ${vulDetail?.end?.line}`}</Box>}
{!!vulDetail && vulDetail?.id === vuln.id && <Box sx={{
fontSize: '12px',
mt: '6px',
}}>
<pre style={{
backgroundColor: '#fff',
padding: '10px',
borderRadius: '4px',
border: '1px solid #ccc',
overflow: 'hidden',
}}>{vulDetail?.lines}</pre>
</Box>}
{!!vulDetail && vulDetail?.id === vuln.id && <Box sx={{
fontSize: '12px',
mt: '6px',
}}>{vulDetail?.fix}</Box>}
</Stack>
</ListItemButton>
</ListItem>
))
)}
</List>
</Grid>
{viewMode === 'detail' && !!vulDetail && <Grid size={7} sx={{
height: '100%',
overflow: 'auto'
}}>
<Box sx={{
width: '100%',
height: '100%',
}}>
<style> {`.monaco-editor.vs-dark .highlighted-code { background-color: rgba(255, 0, 0, 0.3) !important; }`} </style>
<MonacoEditor
height="100%"
value={vulDetail?.content || ''}
theme='vs-dark'
language={getLanguageByFilename(vulDetail?.filename)}
options={{
readOnly: true,
minimap: {
enabled: false
}
}}
onMount={handleEditorDidMount}
></MonacoEditor>
</Box>
</Grid>}
</Grid>
</Card>
</Modal>
);
};
export default TaskDetail;