mirror of
https://github.com/chaitin/SafeLine.git
synced 2026-02-10 10:43:31 +08:00
Merge pull request #78 from DeronW/dev
feat(homepage): add online detection page
This commit is contained in:
3
homepage/.npmrc
Normal file
3
homepage/.npmrc
Normal file
@@ -0,0 +1,3 @@
|
||||
save-prefix=""
|
||||
engine-strict=true
|
||||
registry="https://registry.npmmirror.com"
|
||||
@@ -1,10 +1,32 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
reactStrictMode: false,
|
||||
const isProduction = process.env.NODE_ENV == "production";
|
||||
const isDevelopment = process.env.NODE_ENV == "development";
|
||||
|
||||
const prodConfig = {
|
||||
output: "export",
|
||||
images: {
|
||||
unoptimized: true,
|
||||
};
|
||||
|
||||
const devConfig = {
|
||||
async rewrites() {
|
||||
return [
|
||||
{
|
||||
source: "/api/poc/:path*",
|
||||
destination:
|
||||
"https://waf-ce.chaitin.cn/api/poc/:path*",
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
reactStrictMode: false,
|
||||
images: { unoptimized: true },
|
||||
};
|
||||
|
||||
Object.assign(
|
||||
nextConfig,
|
||||
isProduction && prodConfig,
|
||||
isDevelopment && devConfig
|
||||
);
|
||||
|
||||
module.exports = nextConfig;
|
||||
|
||||
6043
homepage/package-lock.json
generated
Normal file
6043
homepage/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -25,6 +25,7 @@
|
||||
"eslint-config-next": "13.3.0",
|
||||
"github-slugger": "^2.0.0",
|
||||
"gray-matter": "^4.0.3",
|
||||
"highlight.js": "11.8.0",
|
||||
"next": "13.3.0",
|
||||
"next-mdx-remote": "^4.4.1",
|
||||
"react": "18.2.0",
|
||||
|
||||
5818
homepage/pnpm-lock.yaml
generated
5818
homepage/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
39
homepage/src/api/detection.ts
Normal file
39
homepage/src/api/detection.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
export { submitSampleSet, getSampleSet, getSampleSetResult, getSampleDetail };
|
||||
|
||||
const BASE_API = "/api/poc/";
|
||||
|
||||
function submitSampleSet(data: Array<{ content: string; tag: string }>) {
|
||||
return fetch(BASE_API + "list", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
mode: "cors",
|
||||
// credentials: "include",
|
||||
body: JSON.stringify(data),
|
||||
}).then((res) => res.json());
|
||||
}
|
||||
|
||||
function getSampleSet(id: string) {
|
||||
return fetch(BASE_API + "list?id=" + id).then((res) => res.json());
|
||||
}
|
||||
|
||||
function getSampleDetail(id: string) {
|
||||
return fetch(BASE_API + "detail?id=" + id).then((res) => res.json());
|
||||
}
|
||||
|
||||
async function getSampleSetResult(id: string, timeout: number = 60) {
|
||||
const startAt = new Date().getTime();
|
||||
const isTimeout = () => new Date().getTime() - startAt > timeout * 1000;
|
||||
const maxRetry = 20;
|
||||
|
||||
for (let i = 0; i < maxRetry; i++) {
|
||||
const res = await fetch(BASE_API + "results?id=" + id).then((res) =>
|
||||
res.json()
|
||||
);
|
||||
if (res.code == 0 && res.data.data) {
|
||||
return { data: res.data.data, timeout: false };
|
||||
}
|
||||
if (isTimeout()) break;
|
||||
await new Promise((r) => setTimeout(r, 2000));
|
||||
}
|
||||
return { data: [], timeout: true };
|
||||
}
|
||||
46
homepage/src/components/detection/Result.tsx
Normal file
46
homepage/src/components/detection/Result.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import TableContainer from "@mui/material/TableContainer";
|
||||
import Box from "@mui/material/Box";
|
||||
import Table from "@mui/material/Table";
|
||||
import TableRow from "@mui/material/TableRow";
|
||||
import TableHead from "@mui/material/TableHead";
|
||||
import TableCell from "@mui/material/TableCell";
|
||||
import TableBody from "@mui/material/TableBody";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import Title from "@/components/Home/Title";
|
||||
import type { ResultRowsType } from "./types";
|
||||
|
||||
export default Result;
|
||||
|
||||
function Result({ rows }: { rows: ResultRowsType }) {
|
||||
return (
|
||||
<Box sx={{ mt: 4 }}>
|
||||
<Title title="测试结果" sx={{ fontSize: "16px", marginBottom: "16px" }} />
|
||||
<TableContainer component={Paper}>
|
||||
<Table sx={{ minWidth: 650 }}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>检测引擎</TableCell>
|
||||
<TableCell>版本</TableCell>
|
||||
<TableCell>检出率</TableCell>
|
||||
<TableCell>误报率</TableCell>
|
||||
<TableCell>准确率</TableCell>
|
||||
<TableCell>平均检查耗时</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{rows.map((row, index) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell>{row.engine}</TableCell>
|
||||
<TableCell>{row.version}</TableCell>
|
||||
<TableCell>{row.detectionRate}</TableCell>
|
||||
<TableCell>{row.failedRate}</TableCell>
|
||||
<TableCell>{row.accuracy}</TableCell>
|
||||
<TableCell>{row.cost}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
24
homepage/src/components/detection/SampleCount.tsx
Normal file
24
homepage/src/components/detection/SampleCount.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import Typography from "@mui/material/Typography";
|
||||
|
||||
export default SampleCount;
|
||||
|
||||
function SampleCount({
|
||||
total,
|
||||
normal,
|
||||
attack,
|
||||
}: {
|
||||
total: number;
|
||||
normal: number;
|
||||
attack: number;
|
||||
}) {
|
||||
return (
|
||||
<div style={{ display: "flex" }}>
|
||||
<Typography>共计 {total} 个 HTTP 请求样本,其中 </Typography>
|
||||
<Typography sx={{ color: "success.main" }}>
|
||||
普通样本 {normal} 个
|
||||
</Typography>
|
||||
、
|
||||
<Typography sx={{ color: "error.main" }}>攻击样本 {attack} 个</Typography>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
142
homepage/src/components/detection/SampleList.tsx
Normal file
142
homepage/src/components/detection/SampleList.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Table,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableBody,
|
||||
Typography,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
} from "@mui/material";
|
||||
import Accordion from "@mui/material/Accordion";
|
||||
import AccordionSummary from "@mui/material/AccordionSummary";
|
||||
import AccordionDetails from "@mui/material/AccordionDetails";
|
||||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||
import TableCell from "@mui/material/TableCell";
|
||||
import Title from "@/components/Home/Title";
|
||||
import SampleCount from "@/components/detection/SampleCount";
|
||||
import SamplesForm from "@/components/detection/SamplesForm";
|
||||
import hljs from "highlight.js";
|
||||
import { Message } from "@/components";
|
||||
import { getSampleDetail } from "@/api/detection";
|
||||
import { sizeLength } from "@/utils";
|
||||
|
||||
import type { RecordSamplesType } from "./types";
|
||||
|
||||
export default SampleList;
|
||||
|
||||
interface SampleListProps {
|
||||
value: RecordSamplesType;
|
||||
onSetIdChange: (id: string) => void;
|
||||
}
|
||||
|
||||
function SampleList({ value, onSetIdChange }: SampleListProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [detail, setDetail] = useState("");
|
||||
|
||||
const handleDetail = (id: string) => async () => {
|
||||
const res = await getSampleDetail(id);
|
||||
if (res.code != 0) {
|
||||
Message.error(res.msg || "获取详情失败");
|
||||
return;
|
||||
}
|
||||
const highlighted = hljs.highlight(res.data.content, {
|
||||
language: "http",
|
||||
});
|
||||
setDetail(highlighted.value);
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setDetail("");
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
marginBottom: "18px",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Title title="测试样本" sx={{ fontSize: "16px" }} />
|
||||
<SamplesForm onSetIdChange={onSetIdChange} />
|
||||
</Box>
|
||||
|
||||
<Accordion>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<SampleCount
|
||||
total={value.length}
|
||||
normal={value.filter((i) => !i.isAttack).length}
|
||||
attack={value.filter((i) => i.isAttack).length}
|
||||
/>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell width={150}>样本类型</TableCell>
|
||||
<TableCell width={150}>样本大小</TableCell>
|
||||
<TableCell>摘要</TableCell>
|
||||
<TableCell width={100}></TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{value.map((row, index) => (
|
||||
<TableRow
|
||||
key={index}
|
||||
sx={{ "&:last-child td, &:last-child th": { border: 0 } }}
|
||||
>
|
||||
<TableCell>
|
||||
{row.isAttack ? (
|
||||
<Typography sx={{ color: "error.main" }}>
|
||||
攻击样本
|
||||
</Typography>
|
||||
) : (
|
||||
<Typography sx={{ color: "success.main" }}>
|
||||
普通样本
|
||||
</Typography>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>{sizeLength(row.size)}</TableCell>
|
||||
<TableCell>
|
||||
<Typography
|
||||
title={row.summary}
|
||||
noWrap
|
||||
sx={{ width: "600px" }}
|
||||
>
|
||||
{row.summary}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button onClick={handleDetail(row.id)}>详情</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
|
||||
<Dialog open={open} onClose={handleClose}>
|
||||
<DialogContent sx={{ marginBottom: 0 }}>
|
||||
<Box
|
||||
component="code"
|
||||
style={{ whiteSpace: "pre-line", wordBreak: "break-all" }}
|
||||
dangerouslySetInnerHTML={{ __html: detail }}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose}>关闭</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
244
homepage/src/components/detection/SampleSteps.tsx
Normal file
244
homepage/src/components/detection/SampleSteps.tsx
Normal file
@@ -0,0 +1,244 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
RadioGroup,
|
||||
StepLabel,
|
||||
TextField,
|
||||
FormControlLabel,
|
||||
Radio,
|
||||
} from "@mui/material";
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogActions,
|
||||
} from "@mui/material";
|
||||
|
||||
import Stepper from "@mui/material/Stepper";
|
||||
import Step from "@mui/material/Step";
|
||||
import SampleCount from "@/components/detection/SampleCount";
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableBody,
|
||||
} from "@mui/material";
|
||||
import { sampleLength, sampleSummary } from "@/utils";
|
||||
|
||||
export default SampleSteps;
|
||||
|
||||
interface SampleStepsProps {
|
||||
// onDetect: (samples: Array<{ sample: string; isAttack: boolean }>) => void;
|
||||
onDetect: (value: { sample: string; isAttack: boolean }) => void;
|
||||
}
|
||||
|
||||
function SampleSteps({ onDetect }: SampleStepsProps) {
|
||||
const [activeStep, setActiveStep] = useState(0);
|
||||
const [completed, setCompleted] = useState([false, false, false]);
|
||||
const [sampleText, setSampleText] = useState("");
|
||||
const [sampleIsAttack, setSampleIsAttack] = useState(false);
|
||||
const [sampleTextError, setSampleTextError] = useState("");
|
||||
const [count, setCount] = useState({
|
||||
total: 0,
|
||||
normal: 0,
|
||||
attack: 0,
|
||||
});
|
||||
|
||||
const nextStep = () => {
|
||||
setActiveStep(activeStep + 1);
|
||||
completed[activeStep] = true;
|
||||
setCompleted([...completed]);
|
||||
};
|
||||
|
||||
const handleCommit = () => {
|
||||
if (sampleText == "") {
|
||||
setSampleTextError("样本内容不能为空");
|
||||
return;
|
||||
}
|
||||
nextStep();
|
||||
};
|
||||
|
||||
const handleMark = (v: Array<{ isAttack: boolean }>) => {
|
||||
const isAttack = v[0].isAttack;
|
||||
setSampleIsAttack(isAttack);
|
||||
setCount({ total: 1, normal: isAttack ? 0 : 1, attack: isAttack ? 1 : 0 });
|
||||
nextStep();
|
||||
};
|
||||
|
||||
const handleDetect = () => {
|
||||
onDetect({
|
||||
sample: sampleText,
|
||||
isAttack: sampleIsAttack,
|
||||
});
|
||||
};
|
||||
|
||||
const handleStep = (n: number) => {
|
||||
if(completed[n]) setActiveStep(n)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Stepper nonLinear activeStep={activeStep} sx={{ marginBottom: 2 }}>
|
||||
<Step completed={completed[0]} onClick={() => handleStep(0)}>
|
||||
<StepLabel>自定义样本</StepLabel>
|
||||
</Step>
|
||||
<Step completed={completed[1]} onClick={() => handleStep(1)}>
|
||||
<StepLabel>标记样本</StepLabel>
|
||||
</Step>
|
||||
<Step completed={completed[2]} onClick={() => handleStep(2)}>
|
||||
<StepLabel>开始测试</StepLabel>
|
||||
</Step>
|
||||
</Stepper>
|
||||
{activeStep == 0 && (
|
||||
<Box sx={{ p: 1 }}>
|
||||
<TextField
|
||||
multiline
|
||||
fullWidth
|
||||
minRows={4}
|
||||
error={sampleTextError != ""}
|
||||
helperText={sampleTextError}
|
||||
value={sampleText}
|
||||
onChange={(e) => setSampleText(e.target.value)}
|
||||
placeholder={`GET /path/api HTTP/1.1
|
||||
Host: example.com`}
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
<Button fullWidth variant="contained" onClick={handleCommit}>
|
||||
提交样本
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
{activeStep == 1 && (
|
||||
<SampleMarkable onChange={handleMark} samples={[sampleText]} />
|
||||
)}
|
||||
{activeStep == 2 && (
|
||||
<Box>
|
||||
<SampleCount
|
||||
total={count.total}
|
||||
normal={count.normal}
|
||||
attack={count.attack}
|
||||
/>
|
||||
<Button
|
||||
sx={{ mt: 2 }}
|
||||
fullWidth
|
||||
variant="contained"
|
||||
onClick={handleDetect}
|
||||
>
|
||||
开始检测
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function SampleMarkable({
|
||||
samples,
|
||||
onChange,
|
||||
}: {
|
||||
samples: Array<string>;
|
||||
onChange: (value: Array<{ sample: string; isAttack: boolean }>) => void;
|
||||
}) {
|
||||
const [sampleDetail, setSampleDetail] = useState("");
|
||||
const [rows, setRows] = useState(() => {
|
||||
const rows: Array<{
|
||||
isAttack: boolean;
|
||||
summary: string;
|
||||
size: string;
|
||||
raw: string;
|
||||
}> = [];
|
||||
|
||||
samples.forEach((i) => {
|
||||
rows.push({
|
||||
isAttack: false,
|
||||
summary: sampleSummary(i),
|
||||
raw: i,
|
||||
size: sampleLength(i),
|
||||
});
|
||||
});
|
||||
|
||||
return rows;
|
||||
});
|
||||
|
||||
const handle = () => {
|
||||
onChange(rows.map((i) => ({ sample: i.raw, isAttack: i.isAttack })));
|
||||
};
|
||||
|
||||
const handleType = (n: number) => (v: string) => {
|
||||
rows[n].isAttack = v == "attack";
|
||||
setRows([...rows]);
|
||||
};
|
||||
|
||||
const handleSampleDetail = (text: string) => () => {
|
||||
setSampleDetail(text);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setSampleDetail("");
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Table sx={{ mb: 2 }}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell width={350}>样本类型</TableCell>
|
||||
<TableCell width={150}>样本大小</TableCell>
|
||||
<TableCell>摘要</TableCell>
|
||||
<TableCell width={100}></TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{rows.map((row, index) => (
|
||||
<TableRow
|
||||
key={index}
|
||||
sx={{ "&:last-child td, &:last-child th": { border: 0 } }}
|
||||
>
|
||||
<TableCell>
|
||||
<RadioGroup
|
||||
value={row.isAttack ? "attack" : "normal"}
|
||||
onChange={(e) => handleType(index)(e.target.value)}
|
||||
sx={{ flexDirection: "row" }}
|
||||
>
|
||||
<FormControlLabel
|
||||
value="attack"
|
||||
control={<Radio sx={{ color: "divider" }} />}
|
||||
label="攻击样本"
|
||||
/>
|
||||
<FormControlLabel
|
||||
value="normal"
|
||||
control={<Radio sx={{ color: "divider" }} />}
|
||||
label="普通样本"
|
||||
/>
|
||||
</RadioGroup>
|
||||
</TableCell>
|
||||
<TableCell>{row.size}</TableCell>
|
||||
<TableCell>{row.summary}</TableCell>
|
||||
<TableCell>
|
||||
<Button onClick={handleSampleDetail(row.raw)}>详情</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<Button onClick={handle} fullWidth variant="contained">
|
||||
标记样本
|
||||
</Button>
|
||||
|
||||
<Dialog open={sampleDetail != ""} onClick={handleClose}>
|
||||
<DialogTitle>样本详情</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>{sampleDetail}</DialogContentText>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose}>关闭</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
111
homepage/src/components/detection/SamplesForm.tsx
Normal file
111
homepage/src/components/detection/SamplesForm.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import { useState } from "react";
|
||||
import { Message } from "@/components";
|
||||
import { submitSampleSet, getSampleSetResult } from "@/api/detection";
|
||||
import CircularProgress from "@mui/material/CircularProgress";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import SampleSteps from "@/components/detection/SampleSteps";
|
||||
import Button from "@mui/material/Button";
|
||||
import Modal from "@mui/material/Modal";
|
||||
import Box from "@mui/material/Box";
|
||||
import Title from "@/components/Home/Title";
|
||||
import type { RecordSamplesType } from "./types";
|
||||
|
||||
export default SamplesForm;
|
||||
|
||||
function SamplesForm({
|
||||
onSetIdChange,
|
||||
}: {
|
||||
onSetIdChange: (id: string) => void;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const testHandler = () => {
|
||||
setOpen(true);
|
||||
};
|
||||
const closeHandler = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const getSetId = async (content: string, tag: string) => {
|
||||
const res = await submitSampleSet([{ content, tag }]);
|
||||
if (res.code != 0) throw res.msg;
|
||||
if (res.data.total != 1) throw "样本数量错误";
|
||||
return res.data.id;
|
||||
};
|
||||
|
||||
const submit = async ({
|
||||
sample,
|
||||
isAttack,
|
||||
}: {
|
||||
sample: string;
|
||||
isAttack: boolean;
|
||||
}) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const setId = await getSetId(sample, isAttack ? "black" : "white");
|
||||
onSetIdChange(setId);
|
||||
} catch (e) {
|
||||
Message.error(("解析失败: " + e) as string);
|
||||
}
|
||||
setOpen(false);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button variant="contained" onClick={testHandler}>
|
||||
测试我的样本
|
||||
</Button>
|
||||
<Modal open={open} onClose={closeHandler}>
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute" as "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
width: 750,
|
||||
bgcolor: "background.paper",
|
||||
boxShadow: 24,
|
||||
borderRadius: "6px",
|
||||
p: 3,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
{loading && (
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: "0",
|
||||
left: "0",
|
||||
right: "0",
|
||||
bottom: "0",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
background: "rgba(0,0,0,0.3)",
|
||||
}}
|
||||
>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
)}
|
||||
<Title title="测试我的样本" sx={{ fontSize: "18px", mb: 2 }} />
|
||||
<IconButton onClick={closeHandler}>
|
||||
<CloseIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
<Box>
|
||||
<SampleSteps onDetect={submit} />
|
||||
</Box>
|
||||
</Box>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
107
homepage/src/components/detection/builtinSamples.ts
Normal file
107
homepage/src/components/detection/builtinSamples.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
const BuiltinAttackSamples = [
|
||||
`
|
||||
POST /doUpload.action HTTP/1.1
|
||||
Host: localhost:8080
|
||||
Content-Length: 1000000000
|
||||
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryXd004BVJN9pBYBL2
|
||||
|
||||
------WebKitFormBoundaryXd004BVJN9pBYBL2
|
||||
Content-Disposition: form-data; name="upload"; filename="ok"
|
||||
|
||||
|
||||
<%eval request("sb")%>
|
||||
------WebKitFormBoundaryXd004BVJN9pBYBL2--
|
||||
`,
|
||||
`GET /fe/Channel/25545911?tabNum=cat%20%2Fetc%2Fhosts HTTP/1.1
|
||||
Host: monster
|
||||
User-Agent: Chrome`,
|
||||
`GET /scripts/%2e%2e/%2e/Windows/System32/cmd.exe?/c+dir+c HTTP/1.1
|
||||
Host: a.cn
|
||||
User-Agent: Chrome
|
||||
Cookie: 2333;`,
|
||||
`GET /?s=/../../../etc/login\0.img HTTP/1.1
|
||||
Host: monster
|
||||
User-Agent: Chrome`,
|
||||
`POST / HTTP/1.1
|
||||
Host: monster
|
||||
User-Agent: Chrome
|
||||
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary4JGjXRl94NnI4Og7
|
||||
|
||||
------WebKitFormBoundary4JGjXRl94NnI4Og7
|
||||
Content-Disposition: form-data; name="file"; filename="hack.asp%00.png"
|
||||
Content-Type: application/octet-stream
|
||||
|
||||
<%@codepage=65000%><%response.codepage=65001:eval(request("key"))%>
|
||||
------WebKitFormBoundary4JGjXRl94NnI4Og7--"`,
|
||||
`GET /?s=%ac%ed%00%05%73%72%00%1a%63%6f%6d%2e%63%74%2e%61%72%61%6c%65%69%69%2e%74%65%73%74%2e%50%65%72%73%6f%6e%00%00%00%00%00%00%00%01%02%00%08%49%00%03%61%61%61%43%00%03%63%63%63%42%00%03%64%64%64%5a%00%03%65%65%65%4a%00%03%66%66%66%46%00%03%67%67%67%44%00%03%68%68%68%4c%00%03%62%62%62%74%00%12%4c%6a%61%76%61%2f%6c%61%6e%67%2f%53%74%72%69%6e%67%3b%78%70%00%00%00%01%00%62%65%01%00%00%00%00%00%00%00%01%3f%80%00%00%40%00%00%00%00%00%00%00%74%00%03%61%61%61 HTTP/1.1
|
||||
Host: monster
|
||||
User-Agent: Chrome`,
|
||||
`GET / HTTP/1.1
|
||||
Host: a.cn
|
||||
Content-Type: a
|
||||
Origin: b
|
||||
User-Agent: c
|
||||
X-Forwarded-For: d
|
||||
Cookie: FOOID=1;
|
||||
A: AAAAAAAAA
|
||||
Referer: %{(#nike='multipart/form-data').(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#cmd='ifconfig').(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win'))).(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd})).(#p=new java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true)).(#process=#p.start()).(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros)).(#ros.flush())}`,
|
||||
`GET / HTTP/1.1
|
||||
Host: monster
|
||||
User-Agent: Chrome
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
Content-Length: 55
|
||||
|
||||
<?php echo $a; ?>`,
|
||||
`GET / HTTP/1.1
|
||||
Host: monster
|
||||
User-Agent: Chrome
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
Content-Length: 55
|
||||
|
||||
a=O:9:"FileClass":1:{s:8:"filename";s:10:"config.php";}`,
|
||||
`GET / HTTP/1.1
|
||||
Host: monster
|
||||
User-Agent: () { :;}; echo; echo $(/bin/ls -al)
|
||||
Referer: Chrome
|
||||
Cookie: 2333;`,
|
||||
`GET /somepath?q=1'%20or%201=1 HTTP/1.1`,
|
||||
`GET /hello?content=hello&user=ftp://a.com HTTP/1.1
|
||||
Host: www.baidu.com
|
||||
User-Agent: Chrome
|
||||
Referer: http://www.qq.com/abc.html`,
|
||||
`GET /?flag=%7B%7Bconfig.__class__.__init__.__globals__%5B'os'%5D.popen('id').read()%7D%7D HTTP/1.1
|
||||
Host: 1.2.3.4
|
||||
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:97.0) Gecko/20100101 Firefox/97.0
|
||||
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
|
||||
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
|
||||
Accept-Encoding: gzip, deflate
|
||||
Connection: close`,
|
||||
`POST / HTTP/1.1
|
||||
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
|
||||
<xsl:template match="/fruits">
|
||||
<xsl:value-of select="system-property('xsl:vendor')"/>
|
||||
</xsl:template>
|
||||
</xsl:stylesheet>`,
|
||||
`GET /somepath?q=<script>alert('XSS')</script> HTTP/1.1`,
|
||||
`POST / HTTP/1.1
|
||||
|
||||
<?xml version="1.0"?>
|
||||
<!DOCTYPE data [
|
||||
<!ELEMENT data (#ANY)>
|
||||
<!ENTITY file SYSTEM "file:///etc/passwd">
|
||||
]>
|
||||
<data>&file;</data>`,
|
||||
];
|
||||
|
||||
const BuiltinNonAttackSamples = [
|
||||
`GET / HTTP/1.1
|
||||
Host: 1.2.3.4`,
|
||||
|
||||
`POST / HTTP/1.1
|
||||
|
||||
name=haha&submit=submit`,
|
||||
];
|
||||
|
||||
export { BuiltinAttackSamples, BuiltinNonAttackSamples };
|
||||
23
homepage/src/components/detection/types.ts
Normal file
23
homepage/src/components/detection/types.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
export type RecordSamplesType = Array<{
|
||||
id: string;
|
||||
size: number;
|
||||
isAttack: boolean;
|
||||
summary: string;
|
||||
}>;
|
||||
|
||||
export type SampleDetailType = {
|
||||
id: string;
|
||||
size: number;
|
||||
isAttack: boolean;
|
||||
raw: string;
|
||||
summary: string;
|
||||
};
|
||||
|
||||
export type ResultRowsType = Array<{
|
||||
engine: string;
|
||||
version: string;
|
||||
detectionRate: number;
|
||||
failedRate: number;
|
||||
accuracy: number;
|
||||
cost: string;
|
||||
}>;
|
||||
@@ -201,6 +201,17 @@ export default function DrawerAppBar(props: Props) {
|
||||
>
|
||||
技术文档
|
||||
</Box>
|
||||
{/* <Box
|
||||
sx={{
|
||||
color: pathname.startsWith("/detection")
|
||||
? "primary.main"
|
||||
: "#fff",
|
||||
}}
|
||||
component={Link}
|
||||
href="/detection"
|
||||
>
|
||||
防护效果
|
||||
</Box> */}
|
||||
<Box
|
||||
sx={{
|
||||
color: "#fff",
|
||||
|
||||
85
homepage/src/pages/detection.tsx
Normal file
85
homepage/src/pages/detection.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import Container from "@mui/material/Container";
|
||||
import Result from "@/components/detection/Result";
|
||||
import { useRouter } from "next/router";
|
||||
import "highlight.js/styles/a11y-light.css";
|
||||
import { getSampleSet, getSampleSetResult } from "@/api/detection";
|
||||
import { Message } from "@/components";
|
||||
import type {
|
||||
RecordSamplesType,
|
||||
ResultRowsType,
|
||||
} from "@/components/detection/types";
|
||||
import SampleList from "@/components/detection/SampleList";
|
||||
|
||||
export default Detection;
|
||||
|
||||
function Detection() {
|
||||
const router = useRouter();
|
||||
const [samples, setSamples] = useState<RecordSamplesType>([]);
|
||||
const [result, setResult] = useState<ResultRowsType>([]);
|
||||
|
||||
useEffect(() => {
|
||||
// useRouter 中获取 参数会有延迟,所以先判断有没有 id 参数
|
||||
const realSetId =
|
||||
new URLSearchParams(location.search).get("id") || "default";
|
||||
const setId = (router.query.id as string) || "default";
|
||||
if (setId !== realSetId) return;
|
||||
|
||||
// 查询样本集合
|
||||
getSampleSet(setId).then((res) => {
|
||||
if (res.code != 0) {
|
||||
Message.error("测试集合 " + setId + ": " + res.msg);
|
||||
return;
|
||||
}
|
||||
if (!res.data.data) {
|
||||
Message.error("测试集合 " + setId + ": 获取结果为空");
|
||||
return;
|
||||
}
|
||||
setSamples(
|
||||
res.data.data?.map((i: any) => ({
|
||||
id: i.id,
|
||||
summary: i.summary,
|
||||
size: i.length,
|
||||
isAttack: i.tag == "black",
|
||||
}))
|
||||
);
|
||||
});
|
||||
|
||||
// 查询样本集合结果
|
||||
getSampleSetResult(setId).then(({ data, timeout }) => {
|
||||
if (timeout) {
|
||||
Message.error("获取检测集结果超时");
|
||||
return;
|
||||
}
|
||||
setResult(
|
||||
data.map((i: any) => ({
|
||||
engine: i.engine,
|
||||
version: i.version,
|
||||
detectionRate: percent(i.recall),
|
||||
failedRate: percent(i.fdr),
|
||||
accuracy: percent(i.accuracy),
|
||||
cost: i.elapsed + "毫秒",
|
||||
}))
|
||||
);
|
||||
});
|
||||
}, [router.query.id]);
|
||||
|
||||
const handleSetId = (id: string) => {
|
||||
router.push({
|
||||
pathname: router.pathname,
|
||||
query: { id },
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Container sx={{ mb: 2 }}>
|
||||
<div style={{ height: "100px" }}></div>
|
||||
<SampleList value={samples} onSetIdChange={handleSetId} />
|
||||
<Result rows={result} />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
function percent(v: number) {
|
||||
return Math.round(v * 10000) / 100 + "%";
|
||||
}
|
||||
@@ -1 +1,16 @@
|
||||
export * from "./render";
|
||||
|
||||
export { sampleLength, sizeLength, sampleSummary };
|
||||
|
||||
function sampleLength(s: string) {
|
||||
const l = new Blob([s]).size;
|
||||
return l > 1024 * 2 ? Math.round(l / 1024) + "KB" : l + "B";
|
||||
}
|
||||
|
||||
function sizeLength(l: number) {
|
||||
return l > 1024 * 2 ? Math.round(l / 1024) + "KB" : l + "B";
|
||||
}
|
||||
|
||||
function sampleSummary(s: string) {
|
||||
return s.split("\n").slice(0, 2).join(" ").slice(0, 60);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user