Merge pull request #245 from safe1ine/main

增加了代码扫描查看详情的功能
This commit is contained in:
safe1ine
2025-08-08 17:25:47 +08:00
committed by GitHub
6 changed files with 286 additions and 92 deletions

View File

@@ -11,11 +11,7 @@
*/
import { ContentType, RequestParams } from "./httpClient";
import {
DomainCodeSnippet,
InternalCodesnippetHandlerHttpV1GetContextReq,
WebResp,
} from "./types";
import { DomainCodeSnippet, V1GetContextReq, WebResp } from "./types";
/**
* @description 为IDE端提供代码片段上下文检索功能使用API Key认证。支持单个查询和批量查询。
@@ -32,7 +28,7 @@ import {
*/
export const postGetContext = (
request: InternalCodesnippetHandlerHttpV1GetContextReq,
request: V1GetContextReq,
params: RequestParams = {},
) =>
request<

View File

@@ -11,11 +11,7 @@
*/
import request, { ContentType, RequestParams } from "./httpClient";
import {
GithubComChaitinMonkeyCodeBackendProDomainLicenseResp,
V1LicenseCreatePayload,
WebResp,
} from "./types";
import { DomainLicenseResp, V1LicenseCreatePayload, WebResp } from "./types";
/**
* @description Get license
@@ -25,7 +21,7 @@ import {
* @summary Get license
* @request GET:/api/v1/license
* @response `200` `(WebResp & {
data?: GithubComChaitinMonkeyCodeBackendProDomainLicenseResp,
data?: DomainLicenseResp,
})` OK
*/
@@ -33,7 +29,7 @@ import {
export const v1LicenseList = (params: RequestParams = {}) =>
request<
WebResp & {
data?: GithubComChaitinMonkeyCodeBackendProDomainLicenseResp;
data?: DomainLicenseResp;
}
>({
path: `/api/v1/license`,
@@ -51,7 +47,7 @@ export const v1LicenseList = (params: RequestParams = {}) =>
* @summary Upload license
* @request POST:/api/v1/license
* @response `200` `(WebResp & {
data?: GithubComChaitinMonkeyCodeBackendProDomainLicenseResp,
data?: DomainLicenseResp,
})` OK
*/
@@ -62,7 +58,7 @@ export const v1LicenseCreate = (
) =>
request<
WebResp & {
data?: GithubComChaitinMonkeyCodeBackendProDomainLicenseResp;
data?: DomainLicenseResp;
}
>({
path: `/api/v1/license`,

View File

@@ -10,6 +10,12 @@
* ---------------------------------------------------------------
*/
export enum DomainLicenseEdition {
LicenseEditionFree = 0,
LicenseEditionContributor = 1,
LicenseEditionEnterprise = 2,
}
export enum DomainCodeLanguageType {
CodeLanguageTypeGo = "go",
CodeLanguageTypePython = "python",
@@ -557,6 +563,13 @@ export interface DomainInviteResp {
code?: string;
}
export interface DomainLicenseResp {
edition?: DomainLicenseEdition;
expired_at?: number;
started_at?: number;
state?: number;
}
export interface DomainListAdminLoginHistoryResp {
has_next_page?: boolean;
login_histories?: DomainAdminLoginHistory[];
@@ -824,6 +837,8 @@ export interface DomainSecurityScanningBrief {
export interface DomainSecurityScanningResult {
/** 扫描开始时间 */
created_at?: number;
/** 错误信息 */
error?: string;
/** 扫描任务id */
id?: string;
/** 扫描任务 */
@@ -841,10 +856,12 @@ export interface DomainSecurityScanningResult {
}
export interface DomainSecurityScanningRiskDetail {
/** 代码内容 */
content?: string;
/** 风险描述 */
desc?: string;
/** 风险代码行结束位置 */
end?: GithubComChaitinMonkeyCodeBackendEntTypesPosition;
end?: TypesPosition;
/** 风险文件名 */
filename?: string;
/** 修复建议 */
@@ -856,7 +873,7 @@ export interface DomainSecurityScanningRiskDetail {
/** 风险代码行 */
lines?: string;
/** 风险代码行开始位置 */
start?: GithubComChaitinMonkeyCodeBackendEntTypesPosition;
start?: TypesPosition;
}
export interface DomainSecurityScanningRiskResult {
@@ -1173,31 +1190,24 @@ export interface DomainWorkspaceFile {
workspace_id?: string;
}
export interface GithubComChaitinMonkeyCodeBackendEntTypesPosition {
export interface TypesPosition {
col?: number;
line?: number;
offset?: number;
}
export interface GithubComChaitinMonkeyCodeBackendProDomainLicenseResp {
edition?: number;
expired_at?: number;
started_at?: number;
state?: number;
}
export interface InternalCodesnippetHandlerHttpV1GetContextReq {
export interface V1GetContextReq {
/** 返回结果数量限制默认10 */
limit?: number;
/** 批量查询参数 */
queries?: InternalCodesnippetHandlerHttpV1Query[];
queries?: V1Query[];
/** 单个查询参数 */
query?: InternalCodesnippetHandlerHttpV1Query;
query?: V1Query;
/** 工作区路径(必填) */
workspacePath?: string;
}
export interface InternalCodesnippetHandlerHttpV1Query {
export interface V1Query {
/** 编程语言(可选) */
language?: string;
/** 代码片段名称(可选) */

View File

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

View File

@@ -2,7 +2,7 @@ import dayjs from 'dayjs';
import { useState } from 'react';
import { Ellipsis, Modal } from '@c-x/ui';
import { Box, Button, Link, Stack } from '@mui/material';
import { GithubComChaitinMonkeyCodeBackendProDomainLicenseResp } from '@/api/types';
import { DomainLicenseResp } from '@/api/types';
import ChangeLicense from './changeLicense';
interface LicenseModalProps {
@@ -10,7 +10,7 @@ interface LicenseModalProps {
onClose: () => void;
curVersion: string;
latestVersion: string;
license: GithubComChaitinMonkeyCodeBackendProDomainLicenseResp | undefined;
license: DomainLicenseResp | undefined;
}
const AboutModal = ({

View File

@@ -2,7 +2,7 @@ import dayjs from 'dayjs';
import { useState } from 'react';
import { Ellipsis, message, Modal } from '@c-x/ui';
import { Box, Button, Link, Stack, TextField } from '@mui/material';
import { GithubComChaitinMonkeyCodeBackendProDomainLicenseResp } from '@/api/types';
import { DomainLicenseResp } from '@/api/types';
import { v1LicenseCreate } from '@/api';
interface LicenseModalProps {
@@ -27,7 +27,7 @@ const ChangeLicense = ({
license_type: 'code',
license_code: code,
license_file: '' as any
}).then((resp: GithubComChaitinMonkeyCodeBackendProDomainLicenseResp) => {
}).then((resp: DomainLicenseResp) => {
message.success("切换授权成功");
console.log(resp)
setVerifing(false);