feat: 前端源码

master
chaol 4 months ago
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,357 @@
import React, { useState, useEffect } from 'react';
import {
Card,
Table,
Tag,
Space,
Button,
Progress,
message,
Modal,
Form,
Select,
Input
} from 'antd';
import {
PlayCircleOutlined,
StopOutlined,
EyeOutlined,
DownloadOutlined,
ReloadOutlined
} from '@ant-design/icons';
import { scanService, projectService, ScanDto, ProjectDto } from '../services/api';
const { Option } = Select;
const { TextArea } = Input;
interface Scan {
id: number;
project_id: number;
project_name?: string;
scan_type: string;
status: string;
total_files: number;
scanned_files: number;
total_vulnerabilities: number;
started_at: string;
completed_at: string;
created_at: string;
}
interface Project {
id: number;
name: string;
}
const Scans: React.FC = () => {
const [scans, setScans] = useState<Scan[]>([]);
const [projects, setProjects] = useState<Project[]>([]);
const [loading, setLoading] = useState(false);
const [modalVisible, setModalVisible] = useState(false);
const [form] = Form.useForm();
useEffect(() => {
fetchScans();
fetchProjects();
}, []);
const fetchScans = async () => {
setLoading(true);
try {
const data = await scanService.getScans();
setScans(data as unknown as Scan[]);
} catch (error) {
message.error('获取扫描列表失败');
console.error(error);
} finally {
setLoading(false);
}
};
const fetchProjects = async () => {
try {
const data = await projectService.getProjects();
setProjects(data as unknown as Project[]);
} catch (error) {
console.error('获取项目列表失败:', error);
}
};
const handleStartScan = async (values: any) => {
try {
await scanService.createScan(values);
message.success('扫描任务已启动');
setModalVisible(false);
form.resetFields();
fetchScans();
} catch (error) {
message.error('启动扫描失败');
console.error(error);
}
};
const handleCancelScan = async (scanId: number) => {
try {
await scanService.cancelScan(scanId);
message.success('扫描已取消');
fetchScans();
} catch (error) {
message.error('取消扫描失败');
console.error(error);
}
};
const handleViewReport = (scanId: number) => {
window.open(`/reports/scan/${scanId}`, '_blank');
};
const handleDownloadReport = (scanId: number, format: string) => {
const url = `http://localhost:8000/api/reports/scan/${scanId}?format=${format}`;
window.open(url, '_blank');
};
const getStatusColor = (status: string) => {
const colorMap: { [key: string]: string } = {
pending: 'blue',
running: 'orange',
completed: 'green',
failed: 'red',
cancelled: 'gray',
};
return colorMap[status] || 'default';
};
const getStatusText = (status: string) => {
const textMap: { [key: string]: string } = {
pending: '等待中',
running: '运行中',
completed: '已完成',
failed: '失败',
cancelled: '已取消',
};
return textMap[status] || status;
};
const columns = [
{
title: '扫描ID',
dataIndex: 'id',
key: 'id',
width: 80,
},
{
title: '项目',
dataIndex: 'project_name',
key: 'project_name',
render: (text: string, record: Scan) => {
const project = projects.find(p => p.id === record.project_id);
return project?.name || `项目 ${record.project_id}`;
},
},
{
title: '扫描类型',
dataIndex: 'scan_type',
key: 'scan_type',
render: (type: string) => {
const typeMap: { [key: string]: string } = {
full: '全量扫描',
incremental: '增量扫描',
custom: '自定义扫描',
};
return typeMap[type] || type;
},
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
render: (status: string) => (
<Tag color={getStatusColor(status)}>
{getStatusText(status)}
</Tag>
),
},
{
title: '进度',
key: 'progress',
render: (_: unknown, record: Scan) => {
if (record.status === 'running') {
const progress = record.total_files > 0
? (record.scanned_files / record.total_files) * 100
: 0;
return (
<Progress
percent={Math.round(progress)}
size="small"
status="active"
/>
);
} else if (record.status === 'completed') {
return <Progress percent={100} size="small" status="success" />;
} else {
return '-';
}
},
},
{
title: '漏洞数',
dataIndex: 'total_vulnerabilities',
key: 'total_vulnerabilities',
render: (count: number) => count || 0,
},
{
title: '创建时间',
dataIndex: 'created_at',
key: 'created_at',
render: (date: string) => new Date(date).toLocaleString(),
},
{
title: '操作',
key: 'action',
render: (_: unknown, record: Scan) => (
<Space size="small">
{record.status === 'completed' && (
<>
<Button
type="link"
size="small"
icon={<EyeOutlined />}
onClick={() => handleViewReport(record.id)}
>
</Button>
<Button
type="link"
size="small"
icon={<DownloadOutlined />}
onClick={() => handleDownloadReport(record.id, 'pdf')}
>
PDF
</Button>
</>
)}
{record.status === 'running' && (
<Button
type="link"
size="small"
danger
icon={<StopOutlined />}
onClick={() => handleCancelScan(record.id)}
>
</Button>
)}
</Space>
),
},
];
return (
<div>
<Card
title="扫描管理"
extra={
<Space>
<Button
icon={<ReloadOutlined />}
onClick={fetchScans}
>
</Button>
<Button
type="primary"
icon={<PlayCircleOutlined />}
onClick={() => setModalVisible(true)}
>
</Button>
</Space>
}
>
<Table
columns={columns}
dataSource={scans}
loading={loading}
rowKey="id"
pagination={{
pageSize: 10,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total, range) =>
`${range[0]}-${range[1]} 条/共 ${total}`,
}}
/>
</Card>
<Modal
title="新建扫描任务"
open={modalVisible}
onCancel={() => {
setModalVisible(false);
form.resetFields();
}}
footer={null}
width={600}
>
<Form
form={form}
layout="vertical"
onFinish={handleStartScan}
>
<Form.Item
name="project_id"
label="选择项目"
rules={[{ required: true, message: '请选择项目' }]}
>
<Select placeholder="请选择要扫描的项目">
{projects.map(project => (
<Option key={project.id} value={project.id}>
{project.name}
</Option>
))}
</Select>
</Form.Item>
<Form.Item
name="scan_type"
label="扫描类型"
rules={[{ required: true, message: '请选择扫描类型' }]}
initialValue="full"
>
<Select>
<Option value="full"></Option>
<Option value="incremental"></Option>
<Option value="custom"></Option>
</Select>
</Form.Item>
<Form.Item
name="scan_config"
label="扫描配置"
>
<TextArea
rows={4}
placeholder="请输入扫描配置JSON格式"
/>
</Form.Item>
<Form.Item>
<Space>
<Button type="primary" htmlType="submit">
</Button>
<Button onClick={() => {
setModalVisible(false);
form.resetFields();
}}>
</Button>
</Space>
</Form.Item>
</Form>
</Modal>
</div>
);
};
export default Scans;

@ -0,0 +1,145 @@
import axios, { AxiosResponse } from 'axios';
// 通用类型定义(与后端模型对齐的最小字段)
export interface ProjectDto {
id: number;
name: string;
description?: string;
language: string;
repository_url?: string;
project_path?: string;
created_at: string;
updated_at?: string;
}
export interface ScanDto {
id: number;
project_id: number;
scan_type: string;
status: string;
total_files: number;
scanned_files: number;
total_vulnerabilities: number;
started_at?: string;
completed_at?: string;
created_at: string;
}
export interface VulnerabilityDto {
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;
}
export interface VulnerabilityStatsDto {
total: number;
by_severity: Record<string, number>;
by_category: Record<string, number>;
}
// 创建axios实例
const api = axios.create({
baseURL: 'http://localhost:8000/api',
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
});
// 请求拦截器
api.interceptors.request.use(
(config) => {
// 可以在这里添加认证token等
return config;
},
(error) => {
return Promise.reject(error);
}
);
// 响应拦截器
api.interceptors.response.use(
(response: AxiosResponse) => {
return response.data;
},
(error) => {
console.error('API请求错误:', error);
return Promise.reject(error);
}
);
// 项目相关API
export const projectService = {
getProjects: (): Promise<ProjectDto[]> => api.get<ProjectDto[]>('/projects'),
getProject: (id: number): Promise<ProjectDto> => api.get<ProjectDto>(`/projects/${id}`),
createProject: (data: Partial<ProjectDto>): Promise<ProjectDto> => api.post<ProjectDto>('/projects', data),
updateProject: (id: number, data: Partial<ProjectDto>): Promise<ProjectDto> => api.put<ProjectDto>(`/projects/${id}`, data),
deleteProject: (id: number): Promise<void> => api.delete(`/projects/${id}`),
};
// 扫描相关API
export const scanService = {
getScans: (params?: any): Promise<ScanDto[]> => api.get<ScanDto[]>('/scans', { params }),
getScan: (id: number): Promise<ScanDto> => api.get<ScanDto>(`/scans/${id}`),
createScan: (data: any): Promise<ScanDto> => api.post<ScanDto>('/scans', data),
getScanStatus: (id: number): Promise<any> => api.get(`/scans/${id}/status`),
cancelScan: (id: number): Promise<any> => api.post(`/scans/${id}/cancel`),
};
// 漏洞相关API
export const vulnerabilityService = {
getVulnerabilities: (params?: any): Promise<VulnerabilityDto[]> => api.get<VulnerabilityDto[]>('/vulnerabilities', { params }),
getVulnerability: (id: number): Promise<VulnerabilityDto> => api.get<VulnerabilityDto>(`/vulnerabilities/${id}`),
updateVulnerability: (id: number, data: Partial<VulnerabilityDto>): Promise<VulnerabilityDto> => api.put<VulnerabilityDto>(`/vulnerabilities/${id}`, data),
getVulnerabilityStats: (params?: any): Promise<VulnerabilityStatsDto> => api.get<VulnerabilityStatsDto>('/vulnerabilities/stats/summary', { params }),
};
// 报告相关API
export const reportService = {
generateScanReport: (scanId: number, format: string = 'html') =>
api.get(`/reports/scan/${scanId}?format=${format}`),
generateProjectReport: (projectId: number, format: string = 'html') =>
api.get(`/reports/project/${projectId}?format=${format}`),
downloadReport: (scanId: number, format: string) => {
const url = `http://localhost:8000/api/reports/scan/${scanId}?format=${format}`;
window.open(url, '_blank');
},
};
// 仪表板相关API
export const dashboardService = {
getSummary: () => api.get('/reports/dashboard/summary'),
};
export default api;
// 便捷方法:保证返回 Promise<T>,避免 AxiosResponse 类型外溢
export const http = {
get: async <T>(url: string, config?: any): Promise<T> => {
const res = await api.get<T>(url, config);
// 由于拦截器已返回 data这里类型上做一次收敛
return res as unknown as T;
},
post: async <T>(url: string, data?: any, config?: any): Promise<T> => {
const res = await api.post<T>(url, data, config);
return res as unknown as T;
},
put: async <T>(url: string, data?: any, config?: any): Promise<T> => {
const res = await api.put<T>(url, data, config);
return res as unknown as T;
},
del: async <T>(url: string, config?: any): Promise<T> => {
const res = await api.delete<T>(url, config);
return res as unknown as T;
}
};

@ -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…
Cancel
Save