diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..8ebf661 --- /dev/null +++ b/frontend/package.json @@ -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" + } +} diff --git a/frontend/public/index.html b/frontend/public/index.html new file mode 100644 index 0000000..7f6fa85 --- /dev/null +++ b/frontend/public/index.html @@ -0,0 +1,20 @@ + + + + + + + + + + + 代码漏洞检测系统 + + + +
+ + diff --git a/frontend/src/App.css b/frontend/src/App.css new file mode 100644 index 0000000..7cc6f03 --- /dev/null +++ b/frontend/src/App.css @@ -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; +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..5b537b8 --- /dev/null +++ b/frontend/src/App.tsx @@ -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 ( + + + + + } /> + } /> + } /> + } /> + } /> + } /> + + + + + ); +}; + +export default App; diff --git a/frontend/src/components/CodeEditor/CodeEditor.css b/frontend/src/components/CodeEditor/CodeEditor.css new file mode 100644 index 0000000..3ef8d1a --- /dev/null +++ b/frontend/src/components/CodeEditor/CodeEditor.css @@ -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; +} diff --git a/frontend/src/components/CodeEditor/CodeEditor.tsx b/frontend/src/components/CodeEditor/CodeEditor.tsx new file mode 100644 index 0000000..17b6ab6 --- /dev/null +++ b/frontend/src/components/CodeEditor/CodeEditor.tsx @@ -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; + onRefresh: () => void; +} + +const CodeEditor: React.FC = ({ + filePath, + content, + language, + vulnerabilities, + onSave, + onRefresh +}) => { + const [editedContent, setEditedContent] = useState(content); + const [selectedVulnerability, setSelectedVulnerability] = useState(null); + const [isSaving, setIsSaving] = useState(false); + const textareaRef = useRef(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: , + high: , + medium: , + low: , + info: , + }; + return iconMap[severity] || ; + }; + + 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) => ( +
+ {index + 1} +
+ )); + }; + + const renderVulnerabilityMarkers = () => { + return vulnerabilities.map((vuln) => ( +
handleVulnerabilityClick(vuln)} + title={`${vuln.severity.toUpperCase()}: ${vuln.message}`} + /> + )); + }; + + return ( +
+ + {filePath} + + + + +
+ } + extra={ + + {vulnerabilities.length > 0 && ( + + {vulnerabilities.length} 个漏洞 + + )} + + } + > +
+ {/* 漏洞标记侧边栏 */} +
+ {renderVulnerabilityMarkers()} +
+ + {/* 行号 */} +
+ {renderLineNumbers()} +
+ + {/* 代码编辑器 */} +
+