parent
56a233bf72
commit
723b70fdbf
@ -0,0 +1,72 @@
|
||||
{
|
||||
"name": "code-vulnerability-scanner-frontend",
|
||||
"version": "1.0.0",
|
||||
"description": "代码漏洞检测系统前端",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"antd": "^5.12.8",
|
||||
"axios": "^1.6.2",
|
||||
"chart.js": "^4.4.0",
|
||||
"react": "^18.2.0",
|
||||
"react-chartjs-2": "^5.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.8.1",
|
||||
"react-scripts": "5.0.1",
|
||||
"typescript": "^4.9.4",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"proxy": "http://localhost:8000",
|
||||
"devDependencies": {
|
||||
"@types/aria-query": "^5.0.4",
|
||||
"@types/babel__core": "^7.20.5",
|
||||
"@types/babel__generator": "^7.27.0",
|
||||
"@types/babel__template": "^7.4.4",
|
||||
"@types/babel__traverse": "^7.28.0",
|
||||
"@types/bonjour": "^3.5.13",
|
||||
"@types/eslint": "^9.6.1",
|
||||
"@types/estree": "^1.0.8",
|
||||
"@types/graceful-fs": "^4.1.9",
|
||||
"@types/http-errors": "^2.0.5",
|
||||
"@types/http-proxy": "^1.17.16",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/node": "^24.6.0",
|
||||
"@types/prettier": "^3.0.0",
|
||||
"@types/qs": "^6.14.0",
|
||||
"@types/react": "^19.1.16",
|
||||
"@types/react-dom": "^19.1.9",
|
||||
"@types/retry": "^0.12.5",
|
||||
"@types/semver": "^7.7.1",
|
||||
"@types/send": "^0.17.5",
|
||||
"@types/serve-static": "^1.15.8",
|
||||
"@types/stack-utils": "^2.0.3",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@types/yargs": "^17.0.33"
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="代码漏洞检测系统 - 基于AI增强的代码安全分析平台"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<title>代码漏洞检测系统</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>您需要启用JavaScript才能运行此应用程序。</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,50 @@
|
||||
.App {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.App-logo {
|
||||
height: 40vmin;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.App-logo {
|
||||
animation: App-logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.App-header {
|
||||
background-color: #282c34;
|
||||
padding: 20px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.App-link {
|
||||
color: #61dafb;
|
||||
}
|
||||
|
||||
@keyframes App-logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* 自定义样式 */
|
||||
.page-header {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.vulnerability-table {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
margin: 24px 0;
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
|
||||
import { ConfigProvider } from 'antd';
|
||||
import zhCN from 'antd/locale/zh_CN';
|
||||
import Layout from './components/Layout/Layout';
|
||||
import Dashboard from './pages/Dashboard';
|
||||
import Projects from './pages/Projects';
|
||||
import Scans from './pages/Scans';
|
||||
import Reports from './pages/Reports';
|
||||
import CodeEditor from './pages/CodeEditor';
|
||||
import './App.css';
|
||||
|
||||
const App: React.FC = () => {
|
||||
return (
|
||||
<ConfigProvider locale={zhCN}>
|
||||
<Router>
|
||||
<Layout>
|
||||
<Routes>
|
||||
<Route path="/" element={<Dashboard />} />
|
||||
<Route path="/projects" element={<Projects />} />
|
||||
<Route path="/scans" element={<Scans />} />
|
||||
<Route path="/reports" element={<Reports />} />
|
||||
<Route path="/editor" element={<CodeEditor />} />
|
||||
<Route path="/editor/:projectId" element={<CodeEditor />} />
|
||||
</Routes>
|
||||
</Layout>
|
||||
</Router>
|
||||
</ConfigProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
@ -0,0 +1,172 @@
|
||||
.code-editor-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.editor-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.file-path {
|
||||
font-family: 'Monaco', 'Consolas', 'Courier New', monospace;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.editor-content {
|
||||
position: relative;
|
||||
display: flex;
|
||||
height: 600px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.vulnerability-sidebar {
|
||||
position: relative;
|
||||
width: 20px;
|
||||
background-color: #f5f5f5;
|
||||
border-right: 1px solid #d9d9d9;
|
||||
}
|
||||
|
||||
.vulnerability-marker {
|
||||
position: absolute;
|
||||
left: 2px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
border: 2px solid white;
|
||||
box-shadow: 0 0 4px rgba(0, 0, 0, 0.3);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.vulnerability-marker:hover {
|
||||
transform: scale(1.2);
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.line-numbers {
|
||||
background-color: #f5f5f5;
|
||||
color: #999;
|
||||
font-family: 'Monaco', 'Consolas', 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
padding: 8px 4px;
|
||||
border-right: 1px solid #d9d9d9;
|
||||
user-select: none;
|
||||
min-width: 40px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.line-number {
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.editor-main {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.code-textarea {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
outline: none;
|
||||
resize: none;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
padding: 8px;
|
||||
font-family: 'Monaco', 'Consolas', 'Courier New', monospace;
|
||||
background-color: #fff;
|
||||
tab-size: 2;
|
||||
}
|
||||
|
||||
.code-textarea:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.vulnerability-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.vulnerability-details {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.vulnerability-details p {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.ai-suggestion {
|
||||
margin-top: 16px;
|
||||
padding: 12px;
|
||||
background-color: #f6ffed;
|
||||
border: 1px solid #b7eb8f;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.suggestion-content {
|
||||
background-color: #fff;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
margin: 8px 0;
|
||||
font-family: 'Monaco', 'Consolas', 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
white-space: pre-wrap;
|
||||
border: 1px solid #d9d9d9;
|
||||
}
|
||||
|
||||
.confidence {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 语法高亮样式 */
|
||||
.keyword { color: #0000ff; }
|
||||
.string { color: #008000; }
|
||||
.comment { color: #808080; }
|
||||
.number { color: #ff0000; }
|
||||
.function { color: #800080; }
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.editor-content {
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
.file-path {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.code-textarea {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
.code-textarea::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.code-textarea::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
}
|
||||
|
||||
.code-textarea::-webkit-scrollbar-thumb {
|
||||
background: #c1c1c1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.code-textarea::-webkit-scrollbar-thumb:hover {
|
||||
background: #a8a8a8;
|
||||
}
|
||||
@ -0,0 +1,265 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { Card, Button, Space, Tag, Tooltip, message } from 'antd';
|
||||
import {
|
||||
SaveOutlined,
|
||||
ReloadOutlined,
|
||||
BugOutlined,
|
||||
CheckCircleOutlined,
|
||||
ExclamationCircleOutlined
|
||||
} from '@ant-design/icons';
|
||||
import { vulnerabilityService } from '../../services/api';
|
||||
import './CodeEditor.css';
|
||||
|
||||
interface Vulnerability {
|
||||
id: number;
|
||||
rule_id: string;
|
||||
message: string;
|
||||
severity: 'critical' | 'high' | 'medium' | 'low' | 'info';
|
||||
line_number: number;
|
||||
column_number?: number;
|
||||
end_line?: number;
|
||||
end_column?: number;
|
||||
ai_suggestion?: string;
|
||||
ai_confidence?: number;
|
||||
}
|
||||
|
||||
interface CodeEditorProps {
|
||||
filePath: string;
|
||||
content: string;
|
||||
language: string;
|
||||
vulnerabilities: Vulnerability[];
|
||||
onSave: (content: string) => Promise<void>;
|
||||
onRefresh: () => void;
|
||||
}
|
||||
|
||||
const CodeEditor: React.FC<CodeEditorProps> = ({
|
||||
filePath,
|
||||
content,
|
||||
language,
|
||||
vulnerabilities,
|
||||
onSave,
|
||||
onRefresh
|
||||
}) => {
|
||||
const [editedContent, setEditedContent] = useState(content);
|
||||
const [selectedVulnerability, setSelectedVulnerability] = useState<Vulnerability | null>(null);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setEditedContent(content);
|
||||
}, [content]);
|
||||
|
||||
const getSeverityColor = (severity: string) => {
|
||||
const colorMap: { [key: string]: string } = {
|
||||
critical: '#ff4d4f',
|
||||
high: '#ff7a45',
|
||||
medium: '#ffa940',
|
||||
low: '#73d13d',
|
||||
info: '#40a9ff',
|
||||
};
|
||||
return colorMap[severity] || '#d9d9d9';
|
||||
};
|
||||
|
||||
const getSeverityIcon = (severity: string) => {
|
||||
const iconMap: { [key: string]: React.ReactNode } = {
|
||||
critical: <ExclamationCircleOutlined style={{ color: '#ff4d4f' }} />,
|
||||
high: <ExclamationCircleOutlined style={{ color: '#ff7a45' }} />,
|
||||
medium: <BugOutlined style={{ color: '#ffa940' }} />,
|
||||
low: <CheckCircleOutlined style={{ color: '#73d13d' }} />,
|
||||
info: <CheckCircleOutlined style={{ color: '#40a9ff' }} />,
|
||||
};
|
||||
return iconMap[severity] || <BugOutlined />;
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
setIsSaving(true);
|
||||
try {
|
||||
await onSave(editedContent);
|
||||
message.success('代码保存成功');
|
||||
} catch (error) {
|
||||
message.error('代码保存失败');
|
||||
console.error(error);
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleVulnerabilityClick = (vulnerability: Vulnerability) => {
|
||||
setSelectedVulnerability(vulnerability);
|
||||
// 滚动到对应行
|
||||
if (textareaRef.current) {
|
||||
const lines = editedContent.split('\n');
|
||||
const targetLine = vulnerability.line_number;
|
||||
if (targetLine <= lines.length) {
|
||||
const lineHeight = 20; // 估算行高
|
||||
const scrollTop = (targetLine - 1) * lineHeight;
|
||||
textareaRef.current.scrollTop = scrollTop;
|
||||
textareaRef.current.focus();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const applyAIFix = async (vulnerability: Vulnerability) => {
|
||||
if (!vulnerability.ai_suggestion) {
|
||||
message.warning('该漏洞暂无AI修复建议');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 这里可以实现自动应用AI建议的逻辑
|
||||
// 目前先显示建议内容
|
||||
message.info(`AI修复建议: ${vulnerability.ai_suggestion}`);
|
||||
|
||||
// 标记漏洞为已修复
|
||||
await vulnerabilityService.updateVulnerability(vulnerability.id, {
|
||||
status: 'fixed'
|
||||
});
|
||||
|
||||
message.success('漏洞已标记为已修复');
|
||||
onRefresh(); // 刷新数据
|
||||
} catch (error) {
|
||||
message.error('修复失败');
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const renderLineNumbers = () => {
|
||||
const lines = editedContent.split('\n');
|
||||
return lines.map((_, index) => (
|
||||
<div key={index} className="line-number">
|
||||
{index + 1}
|
||||
</div>
|
||||
));
|
||||
};
|
||||
|
||||
const renderVulnerabilityMarkers = () => {
|
||||
return vulnerabilities.map((vuln) => (
|
||||
<div
|
||||
key={vuln.id}
|
||||
className="vulnerability-marker"
|
||||
style={{
|
||||
top: `${(vuln.line_number - 1) * 20 + 2}px`,
|
||||
backgroundColor: getSeverityColor(vuln.severity),
|
||||
}}
|
||||
onClick={() => handleVulnerabilityClick(vuln)}
|
||||
title={`${vuln.severity.toUpperCase()}: ${vuln.message}`}
|
||||
/>
|
||||
));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="code-editor-container">
|
||||
<Card
|
||||
title={
|
||||
<div className="editor-header">
|
||||
<span className="file-path">{filePath}</span>
|
||||
<Space>
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={onRefresh}
|
||||
size="small"
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SaveOutlined />}
|
||||
onClick={handleSave}
|
||||
loading={isSaving}
|
||||
size="small"
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
}
|
||||
extra={
|
||||
<Space>
|
||||
{vulnerabilities.length > 0 && (
|
||||
<Tag color="red">
|
||||
{vulnerabilities.length} 个漏洞
|
||||
</Tag>
|
||||
)}
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<div className="editor-content">
|
||||
{/* 漏洞标记侧边栏 */}
|
||||
<div className="vulnerability-sidebar">
|
||||
{renderVulnerabilityMarkers()}
|
||||
</div>
|
||||
|
||||
{/* 行号 */}
|
||||
<div className="line-numbers">
|
||||
{renderLineNumbers()}
|
||||
</div>
|
||||
|
||||
{/* 代码编辑器 */}
|
||||
<div className="editor-main">
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
className="code-textarea"
|
||||
value={editedContent}
|
||||
onChange={(e) => setEditedContent(e.target.value)}
|
||||
spellCheck={false}
|
||||
style={{ fontFamily: 'Monaco, Consolas, "Courier New", monospace' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 漏洞详情面板 */}
|
||||
{selectedVulnerability && (
|
||||
<Card
|
||||
title={
|
||||
<div className="vulnerability-header">
|
||||
{getSeverityIcon(selectedVulnerability.severity)}
|
||||
<span style={{ marginLeft: 8 }}>
|
||||
漏洞详情 - 第 {selectedVulnerability.line_number} 行
|
||||
</span>
|
||||
<Tag
|
||||
color={getSeverityColor(selectedVulnerability.severity)}
|
||||
style={{ marginLeft: 8 }}
|
||||
>
|
||||
{selectedVulnerability.severity.toUpperCase()}
|
||||
</Tag>
|
||||
</div>
|
||||
}
|
||||
size="small"
|
||||
style={{ marginTop: 16 }}
|
||||
extra={
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
onClick={() => applyAIFix(selectedVulnerability)}
|
||||
disabled={!selectedVulnerability.ai_suggestion}
|
||||
>
|
||||
应用AI修复
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<div className="vulnerability-details">
|
||||
<p><strong>规则ID:</strong> {selectedVulnerability.rule_id}</p>
|
||||
<p><strong>描述:</strong> {selectedVulnerability.message}</p>
|
||||
|
||||
{selectedVulnerability.ai_suggestion && (
|
||||
<div className="ai-suggestion">
|
||||
<p><strong>AI修复建议:</strong></p>
|
||||
<div className="suggestion-content">
|
||||
{selectedVulnerability.ai_suggestion}
|
||||
</div>
|
||||
{selectedVulnerability.ai_confidence && (
|
||||
<p className="confidence">
|
||||
<strong>置信度:</strong> {(selectedVulnerability.ai_confidence * 100).toFixed(1)}%
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CodeEditor;
|
||||
@ -0,0 +1,125 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Layout as AntLayout, Menu, Button } from 'antd';
|
||||
import {
|
||||
DashboardOutlined,
|
||||
ProjectOutlined,
|
||||
SearchOutlined,
|
||||
FileTextOutlined,
|
||||
EditOutlined,
|
||||
MenuFoldOutlined,
|
||||
MenuUnfoldOutlined
|
||||
} from '@ant-design/icons';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
|
||||
const { Header, Sider, Content } = AntLayout;
|
||||
|
||||
interface LayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const Layout: React.FC<LayoutProps> = ({ children }) => {
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
key: '/',
|
||||
icon: <DashboardOutlined />,
|
||||
label: '仪表板',
|
||||
},
|
||||
{
|
||||
key: '/projects',
|
||||
icon: <ProjectOutlined />,
|
||||
label: '项目管理',
|
||||
},
|
||||
{
|
||||
key: '/scans',
|
||||
icon: <SearchOutlined />,
|
||||
label: '扫描管理',
|
||||
},
|
||||
{
|
||||
key: '/reports',
|
||||
icon: <FileTextOutlined />,
|
||||
label: '报告中心',
|
||||
},
|
||||
{
|
||||
key: '/editor',
|
||||
icon: <EditOutlined />,
|
||||
label: '代码编辑器',
|
||||
},
|
||||
];
|
||||
|
||||
const handleMenuClick = ({ key }: { key: string }) => {
|
||||
navigate(key);
|
||||
};
|
||||
|
||||
return (
|
||||
<AntLayout style={{ minHeight: '100vh' }}>
|
||||
<Sider
|
||||
trigger={null}
|
||||
collapsible
|
||||
collapsed={collapsed}
|
||||
theme="dark"
|
||||
>
|
||||
<div style={{
|
||||
height: 32,
|
||||
margin: 16,
|
||||
background: 'rgba(255, 255, 255, 0.2)',
|
||||
borderRadius: 6,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: 'white',
|
||||
fontWeight: 'bold'
|
||||
}}>
|
||||
{collapsed ? 'CVS' : '代码漏洞检测'}
|
||||
</div>
|
||||
<Menu
|
||||
theme="dark"
|
||||
mode="inline"
|
||||
selectedKeys={[location.pathname]}
|
||||
items={menuItems}
|
||||
onClick={handleMenuClick}
|
||||
/>
|
||||
</Sider>
|
||||
<AntLayout>
|
||||
<Header style={{
|
||||
padding: 0,
|
||||
background: '#fff',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingRight: 24
|
||||
}}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
||||
onClick={() => setCollapsed(!collapsed)}
|
||||
style={{
|
||||
fontSize: '16px',
|
||||
width: 64,
|
||||
height: 64,
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
<span style={{ fontSize: 16, fontWeight: 'bold' }}>
|
||||
代码漏洞检测系统
|
||||
</span>
|
||||
</div>
|
||||
</Header>
|
||||
<Content style={{
|
||||
margin: '24px 16px',
|
||||
padding: 24,
|
||||
minHeight: 280,
|
||||
background: '#fff',
|
||||
borderRadius: 8
|
||||
}}>
|
||||
{children}
|
||||
</Content>
|
||||
</AntLayout>
|
||||
</AntLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
@ -0,0 +1,21 @@
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body, #root {
|
||||
height: 100%;
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById('root') as HTMLElement
|
||||
);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
@ -0,0 +1,298 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Layout, Tree, Card, message, Spin, Empty } from 'antd';
|
||||
import { FileOutlined, FolderOutlined, BugOutlined } from '@ant-design/icons';
|
||||
import { projectService, vulnerabilityService } from '../services/api';
|
||||
import CodeEditor from '../components/CodeEditor/CodeEditor';
|
||||
|
||||
const { Sider, Content } = Layout;
|
||||
|
||||
interface ProjectFile {
|
||||
name: string;
|
||||
path: string;
|
||||
is_directory: boolean;
|
||||
size: number;
|
||||
modified: number;
|
||||
}
|
||||
|
||||
interface Vulnerability {
|
||||
id: number;
|
||||
file_path: string;
|
||||
line_number?: number;
|
||||
severity: 'critical' | 'high' | 'medium' | 'low' | 'info';
|
||||
message: string;
|
||||
rule_id: string;
|
||||
ai_suggestion?: string;
|
||||
ai_confidence?: number;
|
||||
}
|
||||
|
||||
interface CodeEditorPageProps {
|
||||
projectId?: number;
|
||||
}
|
||||
|
||||
const CodeEditorPage: React.FC<CodeEditorPageProps> = ({ projectId }) => {
|
||||
const [projects, setProjects] = useState<any[]>([]);
|
||||
const [selectedProject, setSelectedProject] = useState<number | null>(projectId || null);
|
||||
const [files, setFiles] = useState<ProjectFile[]>([]);
|
||||
const [selectedFile, setSelectedFile] = useState<ProjectFile | null>(null);
|
||||
const [fileContent, setFileContent] = useState<string>('');
|
||||
const [vulnerabilities, setVulnerabilities] = useState<Vulnerability[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [fileLoading, setFileLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetchProjects();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedProject) {
|
||||
fetchFiles('');
|
||||
}
|
||||
}, [selectedProject]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedFile && selectedProject) {
|
||||
fetchFileContent();
|
||||
fetchFileVulnerabilities();
|
||||
}
|
||||
}, [selectedFile, selectedProject]);
|
||||
|
||||
const fetchProjects = async () => {
|
||||
try {
|
||||
const data = await projectService.getProjects();
|
||||
setProjects(data);
|
||||
if (data.length > 0 && !selectedProject) {
|
||||
setSelectedProject(data[0].id);
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('获取项目列表失败');
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchFiles = async (path: string = '') => {
|
||||
if (!selectedProject) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch(`http://localhost:8000/api/projects/${selectedProject}/files?path=${encodeURIComponent(path)}`);
|
||||
const data = await response.json();
|
||||
setFiles(data.files || []);
|
||||
} catch (error) {
|
||||
message.error('获取文件列表失败');
|
||||
console.error(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchFileContent = async () => {
|
||||
if (!selectedFile || !selectedProject) return;
|
||||
|
||||
setFileLoading(true);
|
||||
try {
|
||||
const response = await fetch(
|
||||
`http://localhost:8000/api/projects/${selectedProject}/files/content?file_path=${encodeURIComponent(selectedFile.path)}`
|
||||
);
|
||||
const data = await response.json();
|
||||
setFileContent(data.content || '');
|
||||
} catch (error) {
|
||||
message.error('读取文件内容失败');
|
||||
console.error(error);
|
||||
} finally {
|
||||
setFileLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchFileVulnerabilities = async () => {
|
||||
if (!selectedFile || !selectedProject) return;
|
||||
|
||||
try {
|
||||
const data = await vulnerabilityService.getVulnerabilities({
|
||||
project_id: selectedProject
|
||||
});
|
||||
|
||||
// 过滤出当前文件的漏洞
|
||||
const fileVulns = data.filter((vuln: any) =>
|
||||
vuln.file_path.includes(selectedFile.path) ||
|
||||
selectedFile.path.includes(vuln.file_path)
|
||||
);
|
||||
|
||||
setVulnerabilities(fileVulns);
|
||||
} catch (error) {
|
||||
console.error('获取漏洞信息失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveFile = async (content: string) => {
|
||||
if (!selectedFile || !selectedProject) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`http://localhost:8000/api/projects/${selectedProject}/files/content?file_path=${encodeURIComponent(selectedFile.path)}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ content })
|
||||
}
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
message.success('文件保存成功');
|
||||
// 重新获取漏洞信息
|
||||
fetchFileVulnerabilities();
|
||||
} else {
|
||||
throw new Error('保存失败');
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('保存文件失败');
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const handleRefresh = () => {
|
||||
if (selectedProject) {
|
||||
fetchFiles('');
|
||||
}
|
||||
if (selectedFile && selectedProject) {
|
||||
fetchFileContent();
|
||||
fetchFileVulnerabilities();
|
||||
}
|
||||
};
|
||||
|
||||
const getFileIcon = (file: ProjectFile) => {
|
||||
return file.is_directory ? <FolderOutlined /> : <FileOutlined />;
|
||||
};
|
||||
|
||||
const getLanguageFromExtension = (filename: string): string => {
|
||||
const ext = filename.split('.').pop()?.toLowerCase();
|
||||
const langMap: { [key: string]: string } = {
|
||||
'py': 'python',
|
||||
'js': 'javascript',
|
||||
'jsx': 'javascript',
|
||||
'ts': 'typescript',
|
||||
'tsx': 'typescript',
|
||||
'java': 'java',
|
||||
'cpp': 'cpp',
|
||||
'c': 'c',
|
||||
'h': 'c',
|
||||
'hpp': 'cpp',
|
||||
'cs': 'csharp',
|
||||
'php': 'php',
|
||||
'rb': 'ruby',
|
||||
'go': 'go',
|
||||
'rs': 'rust',
|
||||
'swift': 'swift',
|
||||
'kt': 'kotlin',
|
||||
'scala': 'scala',
|
||||
'sh': 'bash',
|
||||
'sql': 'sql',
|
||||
'html': 'html',
|
||||
'css': 'css',
|
||||
'scss': 'scss',
|
||||
'sass': 'sass',
|
||||
'less': 'less',
|
||||
'vue': 'vue',
|
||||
'json': 'json',
|
||||
'xml': 'xml',
|
||||
'yaml': 'yaml',
|
||||
'yml': 'yaml',
|
||||
'md': 'markdown',
|
||||
'txt': 'text'
|
||||
};
|
||||
return langMap[ext || ''] || 'text';
|
||||
};
|
||||
|
||||
const renderTreeData = (files: ProjectFile[]): any[] => {
|
||||
return files.map(file => ({
|
||||
title: (
|
||||
<div className="file-item">
|
||||
{getFileIcon(file)}
|
||||
<span style={{ marginLeft: 8 }}>{file.name}</span>
|
||||
{!file.is_directory && vulnerabilities.some(v => v.file_path.includes(file.path)) && (
|
||||
<BugOutlined style={{ color: '#ff4d4f', marginLeft: 8 }} />
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
key: file.path,
|
||||
icon: getFileIcon(file),
|
||||
isLeaf: !file.is_directory,
|
||||
data: file
|
||||
}));
|
||||
};
|
||||
|
||||
const handleTreeSelect = (selectedKeys: React.Key[], info: any) => {
|
||||
if (selectedKeys.length > 0 && info.node.data && !info.node.data.is_directory) {
|
||||
setSelectedFile(info.node.data);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout style={{ height: '100vh' }}>
|
||||
<Sider width={300} style={{ background: '#fff', borderRight: '1px solid #f0f0f0' }}>
|
||||
<Card
|
||||
title="项目文件"
|
||||
size="small"
|
||||
style={{ height: '100%' }}
|
||||
bodyStyle={{ padding: '8px', height: 'calc(100% - 57px)', overflow: 'auto' }}
|
||||
>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<select
|
||||
value={selectedProject || ''}
|
||||
onChange={(e) => setSelectedProject(Number(e.target.value))}
|
||||
style={{ width: '100%', padding: '4px 8px' }}
|
||||
>
|
||||
<option value="">选择项目</option>
|
||||
{projects.map(project => (
|
||||
<option key={project.id} value={project.id}>
|
||||
{project.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<Spin size="small" />
|
||||
) : files.length > 0 ? (
|
||||
<Tree
|
||||
treeData={renderTreeData(files)}
|
||||
onSelect={handleTreeSelect}
|
||||
defaultExpandAll
|
||||
showIcon
|
||||
/>
|
||||
) : (
|
||||
<Empty description="暂无文件" />
|
||||
)}
|
||||
</Card>
|
||||
</Sider>
|
||||
|
||||
<Content style={{ padding: 0 }}>
|
||||
{selectedFile ? (
|
||||
<div style={{ height: '100%' }}>
|
||||
<CodeEditor
|
||||
filePath={selectedFile.path}
|
||||
content={fileContent}
|
||||
language={getLanguageFromExtension(selectedFile.name)}
|
||||
vulnerabilities={vulnerabilities}
|
||||
onSave={handleSaveFile}
|
||||
onRefresh={handleRefresh}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: '#fafafa'
|
||||
}}>
|
||||
<Empty description="请选择一个文件进行编辑" />
|
||||
</div>
|
||||
)}
|
||||
</Content>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default CodeEditorPage;
|
||||
@ -0,0 +1,244 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, Row, Col, Statistic, Table, Tag, Button } from 'antd';
|
||||
import {
|
||||
ProjectOutlined,
|
||||
SearchOutlined,
|
||||
BugOutlined,
|
||||
CheckCircleOutlined
|
||||
} from '@ant-design/icons';
|
||||
import { Line, Doughnut } from 'react-chartjs-2';
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
ArcElement,
|
||||
} from 'chart.js';
|
||||
import { dashboardService } from '../services/api';
|
||||
|
||||
// 注册Chart.js组件
|
||||
ChartJS.register(
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
ArcElement
|
||||
);
|
||||
|
||||
const Dashboard: React.FC = () => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [summaryData, setSummaryData] = useState<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchDashboardData();
|
||||
}, []);
|
||||
|
||||
const fetchDashboardData = async () => {
|
||||
try {
|
||||
const data = await dashboardService.getSummary();
|
||||
setSummaryData(data);
|
||||
} catch (error) {
|
||||
console.error('获取仪表板数据失败:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 漏洞趋势图数据
|
||||
const vulnerabilityTrendData = {
|
||||
labels: ['1月', '2月', '3月', '4月', '5月', '6月'],
|
||||
datasets: [
|
||||
{
|
||||
label: '严重漏洞',
|
||||
data: [12, 19, 3, 5, 2, 3],
|
||||
borderColor: 'rgb(255, 99, 132)',
|
||||
backgroundColor: 'rgba(255, 99, 132, 0.2)',
|
||||
},
|
||||
{
|
||||
label: '高危漏洞',
|
||||
data: [2, 3, 20, 5, 1, 4],
|
||||
borderColor: 'rgb(54, 162, 235)',
|
||||
backgroundColor: 'rgba(54, 162, 235, 0.2)',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// 漏洞分布饼图数据
|
||||
const vulnerabilityDistributionData = {
|
||||
labels: ['安全', '性能', '可维护性', '可靠性'],
|
||||
datasets: [
|
||||
{
|
||||
data: [30, 25, 35, 10],
|
||||
backgroundColor: [
|
||||
'#FF6384',
|
||||
'#36A2EB',
|
||||
'#FFCE56',
|
||||
'#4BC0C0',
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const recentVulnerabilitiesColumns = [
|
||||
{
|
||||
title: '项目',
|
||||
dataIndex: 'project_name',
|
||||
key: 'project_name',
|
||||
},
|
||||
{
|
||||
title: '漏洞类型',
|
||||
dataIndex: 'category',
|
||||
key: 'category',
|
||||
render: (category: string) => {
|
||||
const colorMap: { [key: string]: string } = {
|
||||
security: 'red',
|
||||
performance: 'blue',
|
||||
maintainability: 'green',
|
||||
reliability: 'orange',
|
||||
};
|
||||
return <Tag color={colorMap[category] || 'default'}>{category}</Tag>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '严重程度',
|
||||
dataIndex: 'severity',
|
||||
key: 'severity',
|
||||
render: (severity: string) => {
|
||||
const colorMap: { [key: string]: string } = {
|
||||
critical: 'red',
|
||||
high: 'orange',
|
||||
medium: 'yellow',
|
||||
low: 'green',
|
||||
info: 'blue',
|
||||
};
|
||||
return <Tag color={colorMap[severity] || 'default'}>{severity}</Tag>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '文件路径',
|
||||
dataIndex: 'file_path',
|
||||
key: 'file_path',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
render: () => (
|
||||
<Button type="link" size="small">
|
||||
查看详情
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const chartOptions = {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'top' as const,
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: '漏洞趋势分析',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const doughnutOptions = {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'right' as const,
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: '漏洞分类分布',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <div>加载中...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="page-header">仪表板</h1>
|
||||
|
||||
{/* 统计卡片 */}
|
||||
<Row gutter={16} style={{ marginBottom: 24 }}>
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="活跃项目"
|
||||
value={summaryData?.projects || 0}
|
||||
prefix={<ProjectOutlined />}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="扫描次数"
|
||||
value={summaryData?.scans || 0}
|
||||
prefix={<SearchOutlined />}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="发现漏洞"
|
||||
value={summaryData?.vulnerabilities || 0}
|
||||
prefix={<BugOutlined />}
|
||||
valueStyle={{ color: '#cf1322' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="已修复"
|
||||
value={summaryData?.fixed || 0}
|
||||
prefix={<CheckCircleOutlined />}
|
||||
valueStyle={{ color: '#3f8600' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* 图表区域 */}
|
||||
<Row gutter={16} style={{ marginBottom: 24 }}>
|
||||
<Col span={16}>
|
||||
<Card title="漏洞趋势分析">
|
||||
<Line data={vulnerabilityTrendData} options={chartOptions} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Card title="漏洞分类分布">
|
||||
<Doughnut data={vulnerabilityDistributionData} options={doughnutOptions} />
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* 最近漏洞 */}
|
||||
<Card title="最近发现的漏洞">
|
||||
<Table
|
||||
columns={recentVulnerabilitiesColumns}
|
||||
dataSource={summaryData?.recent_vulnerabilities || []}
|
||||
pagination={{ pageSize: 10 }}
|
||||
size="small"
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
@ -0,0 +1,291 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Card,
|
||||
Button,
|
||||
Table,
|
||||
Tag,
|
||||
Space,
|
||||
Modal,
|
||||
Form,
|
||||
Input,
|
||||
Select,
|
||||
message,
|
||||
Popconfirm
|
||||
} from 'antd';
|
||||
import { PlusOutlined, EditOutlined, DeleteOutlined, PlayCircleOutlined, CodeOutlined } from '@ant-design/icons';
|
||||
import { projectService, ProjectDto } from '../services/api';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
const { TextArea } = Input;
|
||||
const { Option } = Select;
|
||||
|
||||
interface Project {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
language: string;
|
||||
repository_url: string;
|
||||
project_path: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
const Projects: React.FC = () => {
|
||||
const [projects, setProjects] = useState<Project[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [editingProject, setEditingProject] = useState<Project | null>(null);
|
||||
const [form] = Form.useForm();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
fetchProjects();
|
||||
}, []);
|
||||
|
||||
const fetchProjects = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await projectService.getProjects();
|
||||
setProjects(data as unknown as Project[]);
|
||||
} catch (error) {
|
||||
message.error('获取项目列表失败');
|
||||
console.error(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreate = () => {
|
||||
setEditingProject(null);
|
||||
form.resetFields();
|
||||
setModalVisible(true);
|
||||
};
|
||||
|
||||
const handleEdit = (project: Project) => {
|
||||
setEditingProject(project);
|
||||
form.setFieldsValue(project);
|
||||
setModalVisible(true);
|
||||
};
|
||||
|
||||
const handleDelete = async (projectId: number) => {
|
||||
try {
|
||||
await projectService.deleteProject(projectId);
|
||||
message.success('项目删除成功');
|
||||
fetchProjects();
|
||||
} catch (error) {
|
||||
message.error('项目删除失败');
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleStartScan = async (projectId: number) => {
|
||||
try {
|
||||
// 这里调用扫描API
|
||||
message.success('扫描任务已启动');
|
||||
} catch (error) {
|
||||
message.error('启动扫描失败');
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditCode = (projectId: number) => {
|
||||
navigate(`/editor/${projectId}`);
|
||||
};
|
||||
|
||||
const handleModalOk = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
|
||||
if (editingProject) {
|
||||
await projectService.updateProject(editingProject.id, values);
|
||||
message.success('项目更新成功');
|
||||
} else {
|
||||
await projectService.createProject(values);
|
||||
message.success('项目创建成功');
|
||||
}
|
||||
|
||||
setModalVisible(false);
|
||||
fetchProjects();
|
||||
} catch (error) {
|
||||
message.error('操作失败');
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleModalCancel = () => {
|
||||
setModalVisible(false);
|
||||
form.resetFields();
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '项目名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
render: (text: string, record: Project) => (
|
||||
<a onClick={() => handleEdit(record)}>{text}</a>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '编程语言',
|
||||
dataIndex: 'language',
|
||||
key: 'language',
|
||||
render: (language: string) => {
|
||||
const colorMap: { [key: string]: string } = {
|
||||
python: 'green',
|
||||
cpp: 'blue',
|
||||
javascript: 'yellow',
|
||||
java: 'red',
|
||||
go: 'cyan',
|
||||
};
|
||||
return <Tag color={colorMap[language] || 'default'}>{language}</Tag>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '描述',
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'created_at',
|
||||
key: 'created_at',
|
||||
render: (date: string) => new Date(date).toLocaleString(),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
render: (_: unknown, record: Project) => (
|
||||
<Space size="middle">
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
icon={<PlayCircleOutlined />}
|
||||
onClick={() => handleStartScan(record.id)}
|
||||
>
|
||||
扫描
|
||||
</Button>
|
||||
<Button
|
||||
type="default"
|
||||
size="small"
|
||||
icon={<CodeOutlined />}
|
||||
onClick={() => handleEditCode(record.id)}
|
||||
>
|
||||
编辑代码
|
||||
</Button>
|
||||
<Button
|
||||
type="default"
|
||||
size="small"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => handleEdit(record)}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title="确定要删除这个项目吗?"
|
||||
onConfirm={() => handleDelete(record.id)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Button
|
||||
type="default"
|
||||
size="small"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Card
|
||||
title="项目管理"
|
||||
extra={
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={handleCreate}>
|
||||
新建项目
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={projects}
|
||||
loading={loading}
|
||||
rowKey="id"
|
||||
pagination={{
|
||||
pageSize: 10,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total, range) =>
|
||||
`第 ${range[0]}-${range[1]} 条/共 ${total} 条`,
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Modal
|
||||
title={editingProject ? '编辑项目' : '新建项目'}
|
||||
open={modalVisible}
|
||||
onOk={handleModalOk}
|
||||
onCancel={handleModalCancel}
|
||||
width={600}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
name="project_form"
|
||||
>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="项目名称"
|
||||
rules={[{ required: true, message: '请输入项目名称' }]}
|
||||
>
|
||||
<Input placeholder="请输入项目名称" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="description"
|
||||
label="项目描述"
|
||||
>
|
||||
<TextArea rows={3} placeholder="请输入项目描述" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="language"
|
||||
label="编程语言"
|
||||
rules={[{ required: true, message: '请选择编程语言' }]}
|
||||
>
|
||||
<Select placeholder="请选择编程语言">
|
||||
<Option value="python">Python</Option>
|
||||
<Option value="cpp">C++</Option>
|
||||
<Option value="javascript">JavaScript</Option>
|
||||
<Option value="java">Java</Option>
|
||||
<Option value="go">Go</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="repository_url"
|
||||
label="代码仓库地址"
|
||||
>
|
||||
<Input placeholder="请输入代码仓库地址" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="project_path"
|
||||
label="项目路径"
|
||||
rules={[{ required: true, message: '请输入项目路径' }]}
|
||||
>
|
||||
<Input placeholder="请输入项目本地路径" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Projects;
|
||||
@ -0,0 +1,418 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Card,
|
||||
Table,
|
||||
Tag,
|
||||
Space,
|
||||
Button,
|
||||
Input,
|
||||
Select,
|
||||
Modal,
|
||||
message,
|
||||
Statistic,
|
||||
Row,
|
||||
Col
|
||||
} from 'antd';
|
||||
import {
|
||||
SearchOutlined,
|
||||
DownloadOutlined,
|
||||
EyeOutlined,
|
||||
BugOutlined,
|
||||
FilterOutlined
|
||||
} from '@ant-design/icons';
|
||||
import { vulnerabilityService, VulnerabilityDto, VulnerabilityStatsDto } from '../services/api';
|
||||
|
||||
const { Option } = Select;
|
||||
const { Search } = Input;
|
||||
|
||||
interface Vulnerability {
|
||||
id: number;
|
||||
scan_id: number;
|
||||
rule_id: string;
|
||||
message: string;
|
||||
category: string;
|
||||
severity: string;
|
||||
file_path: string;
|
||||
line_number: number;
|
||||
status: string;
|
||||
ai_enhanced: boolean;
|
||||
ai_confidence: number;
|
||||
ai_suggestion: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
const Reports: React.FC = () => {
|
||||
const [vulnerabilities, setVulnerabilities] = useState<Vulnerability[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [stats, setStats] = useState<any>(null);
|
||||
const [detailModalVisible, setDetailModalVisible] = useState(false);
|
||||
const [selectedVulnerability, setSelectedVulnerability] = useState<Vulnerability | null>(null);
|
||||
|
||||
// 筛选条件
|
||||
const [filters, setFilters] = useState({
|
||||
severity: '',
|
||||
category: '',
|
||||
status: '',
|
||||
search: '',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
fetchVulnerabilities();
|
||||
fetchStats();
|
||||
}, [filters]);
|
||||
|
||||
const fetchVulnerabilities = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const params: any = {};
|
||||
if (filters.severity) params.severity = filters.severity;
|
||||
if (filters.category) params.category = filters.category;
|
||||
if (filters.status) params.status = filters.status;
|
||||
if (filters.search) params.search = filters.search;
|
||||
|
||||
const data = await vulnerabilityService.getVulnerabilities(params);
|
||||
setVulnerabilities(data as unknown as Vulnerability[]);
|
||||
} catch (error) {
|
||||
message.error('获取漏洞列表失败');
|
||||
console.error(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchStats = async () => {
|
||||
try {
|
||||
const data = await vulnerabilityService.getVulnerabilityStats();
|
||||
setStats(data as unknown as VulnerabilityStatsDto);
|
||||
} catch (error) {
|
||||
console.error('获取统计数据失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleViewDetail = (vulnerability: Vulnerability) => {
|
||||
setSelectedVulnerability(vulnerability);
|
||||
setDetailModalVisible(true);
|
||||
};
|
||||
|
||||
const handleExport = async (format: string) => {
|
||||
try {
|
||||
const url = `http://localhost:8000/api/vulnerabilities/export?format=${format}`;
|
||||
window.open(url, '_blank');
|
||||
message.success('导出成功');
|
||||
} catch (error) {
|
||||
message.error('导出失败');
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const getSeverityColor = (severity: string) => {
|
||||
const colorMap: { [key: string]: string } = {
|
||||
critical: 'red',
|
||||
high: 'orange',
|
||||
medium: 'yellow',
|
||||
low: 'green',
|
||||
info: 'blue',
|
||||
};
|
||||
return colorMap[severity] || 'default';
|
||||
};
|
||||
|
||||
const getCategoryColor = (category: string) => {
|
||||
const colorMap: { [key: string]: string } = {
|
||||
security: 'red',
|
||||
performance: 'blue',
|
||||
maintainability: 'green',
|
||||
reliability: 'orange',
|
||||
usability: 'purple',
|
||||
};
|
||||
return colorMap[category] || 'default';
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
const colorMap: { [key: string]: string } = {
|
||||
open: 'red',
|
||||
fixed: 'green',
|
||||
false_positive: 'gray',
|
||||
wont_fix: 'orange',
|
||||
};
|
||||
return colorMap[status] || 'default';
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
width: 60,
|
||||
},
|
||||
{
|
||||
title: '规则ID',
|
||||
dataIndex: 'rule_id',
|
||||
key: 'rule_id',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '严重程度',
|
||||
dataIndex: 'severity',
|
||||
key: 'severity',
|
||||
width: 100,
|
||||
render: (severity: string) => (
|
||||
<Tag color={getSeverityColor(severity)}>
|
||||
{severity.toUpperCase()}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '分类',
|
||||
dataIndex: 'category',
|
||||
key: 'category',
|
||||
width: 100,
|
||||
render: (category: string) => (
|
||||
<Tag color={getCategoryColor(category)}>
|
||||
{category}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '描述',
|
||||
dataIndex: 'message',
|
||||
key: 'message',
|
||||
ellipsis: true,
|
||||
width: 300,
|
||||
},
|
||||
{
|
||||
title: '文件路径',
|
||||
dataIndex: 'file_path',
|
||||
key: 'file_path',
|
||||
ellipsis: true,
|
||||
width: 200,
|
||||
render: (text: string) => (
|
||||
<span title={text}>{text}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '行号',
|
||||
dataIndex: 'line_number',
|
||||
key: 'line_number',
|
||||
width: 80,
|
||||
render: (line: number) => line || '-',
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 100,
|
||||
render: (status: string) => (
|
||||
<Tag color={getStatusColor(status)}>
|
||||
{status}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'AI增强',
|
||||
dataIndex: 'ai_enhanced',
|
||||
key: 'ai_enhanced',
|
||||
width: 80,
|
||||
render: (enhanced: boolean) => enhanced ? '是' : '否',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 120,
|
||||
render: (_: unknown, record: Vulnerability) => (
|
||||
<Space size="small">
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={() => handleViewDetail(record)}
|
||||
>
|
||||
详情
|
||||
</Button>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* 统计卡片 */}
|
||||
{stats && (
|
||||
<Row gutter={16} style={{ marginBottom: 24 }}>
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="总漏洞数"
|
||||
value={stats.total}
|
||||
prefix={<BugOutlined />}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
{Object.entries(stats.by_severity || {}).map(([severity, count]) => (
|
||||
<Col span={6} key={severity}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title={`${severity.toUpperCase()} 漏洞`}
|
||||
value={Number(count as unknown as number)}
|
||||
valueStyle={{ color: getSeverityColor(severity) }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
)}
|
||||
|
||||
<Card
|
||||
title="漏洞报告"
|
||||
extra={
|
||||
<Space>
|
||||
<Button
|
||||
icon={<DownloadOutlined />}
|
||||
onClick={() => handleExport('excel')}
|
||||
>
|
||||
导出Excel
|
||||
</Button>
|
||||
<Button
|
||||
icon={<DownloadOutlined />}
|
||||
onClick={() => handleExport('json')}
|
||||
>
|
||||
导出JSON
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
{/* 筛选器 */}
|
||||
<Space wrap style={{ marginBottom: 16 }}>
|
||||
<Search
|
||||
placeholder="搜索漏洞"
|
||||
style={{ width: 200 }}
|
||||
onSearch={(value) => setFilters({ ...filters, search: value })}
|
||||
onChange={(e) => {
|
||||
if (!e.target.value) {
|
||||
setFilters({ ...filters, search: '' });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Select
|
||||
placeholder="严重程度"
|
||||
style={{ width: 120 }}
|
||||
allowClear
|
||||
onChange={(value) => setFilters({ ...filters, severity: value || '' })}
|
||||
>
|
||||
<Option value="critical">Critical</Option>
|
||||
<Option value="high">High</Option>
|
||||
<Option value="medium">Medium</Option>
|
||||
<Option value="low">Low</Option>
|
||||
<Option value="info">Info</Option>
|
||||
</Select>
|
||||
<Select
|
||||
placeholder="分类"
|
||||
style={{ width: 120 }}
|
||||
allowClear
|
||||
onChange={(value) => setFilters({ ...filters, category: value || '' })}
|
||||
>
|
||||
<Option value="security">Security</Option>
|
||||
<Option value="performance">Performance</Option>
|
||||
<Option value="maintainability">Maintainability</Option>
|
||||
<Option value="reliability">Reliability</Option>
|
||||
<Option value="usability">Usability</Option>
|
||||
</Select>
|
||||
<Select
|
||||
placeholder="状态"
|
||||
style={{ width: 120 }}
|
||||
allowClear
|
||||
onChange={(value) => setFilters({ ...filters, status: value || '' })}
|
||||
>
|
||||
<Option value="open">Open</Option>
|
||||
<Option value="fixed">Fixed</Option>
|
||||
<Option value="false_positive">False Positive</Option>
|
||||
<Option value="wont_fix">Won't Fix</Option>
|
||||
</Select>
|
||||
</Space>
|
||||
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={vulnerabilities}
|
||||
loading={loading}
|
||||
rowKey="id"
|
||||
pagination={{
|
||||
pageSize: 20,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total, range) =>
|
||||
`第 ${range[0]}-${range[1]} 条/共 ${total} 条`,
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* 漏洞详情模态框 */}
|
||||
<Modal
|
||||
title="漏洞详情"
|
||||
open={detailModalVisible}
|
||||
onCancel={() => setDetailModalVisible(false)}
|
||||
footer={null}
|
||||
width={800}
|
||||
>
|
||||
{selectedVulnerability && (
|
||||
<div>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<p><strong>规则ID:</strong> {selectedVulnerability.rule_id}</p>
|
||||
<p><strong>严重程度:</strong>
|
||||
<Tag color={getSeverityColor(selectedVulnerability.severity)} style={{ marginLeft: 8 }}>
|
||||
{selectedVulnerability.severity.toUpperCase()}
|
||||
</Tag>
|
||||
</p>
|
||||
<p><strong>分类:</strong>
|
||||
<Tag color={getCategoryColor(selectedVulnerability.category)} style={{ marginLeft: 8 }}>
|
||||
{selectedVulnerability.category}
|
||||
</Tag>
|
||||
</p>
|
||||
<p><strong>状态:</strong>
|
||||
<Tag color={getStatusColor(selectedVulnerability.status)} style={{ marginLeft: 8 }}>
|
||||
{selectedVulnerability.status}
|
||||
</Tag>
|
||||
</p>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<p><strong>文件路径:</strong> {selectedVulnerability.file_path}</p>
|
||||
<p><strong>行号:</strong> {selectedVulnerability.line_number || 'N/A'}</p>
|
||||
<p><strong>AI增强:</strong> {selectedVulnerability.ai_enhanced ? '是' : '否'}</p>
|
||||
{selectedVulnerability.ai_confidence && (
|
||||
<p><strong>AI置信度:</strong> {(selectedVulnerability.ai_confidence * 100).toFixed(1)}%</p>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<p><strong>漏洞描述:</strong></p>
|
||||
<div style={{
|
||||
background: '#f5f5f5',
|
||||
padding: 12,
|
||||
borderRadius: 4,
|
||||
marginBottom: 16
|
||||
}}>
|
||||
{selectedVulnerability.message}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedVulnerability.ai_suggestion && (
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<p><strong>AI修复建议:</strong></p>
|
||||
<div style={{
|
||||
background: '#e6f7ff',
|
||||
border: '1px solid #91d5ff',
|
||||
padding: 12,
|
||||
borderRadius: 4
|
||||
}}>
|
||||
{selectedVulnerability.ai_suggestion}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Reports;
|
||||
@ -0,0 +1,35 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"es6"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"typeRoots": [
|
||||
"./node_modules/@types"
|
||||
],
|
||||
"types": [
|
||||
"node",
|
||||
"react",
|
||||
"react-dom",
|
||||
"jest"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
||||
Loading…
Reference in new issue