diff --git a/app/main/handlers/queryHTTPFlow.js b/app/main/handlers/queryHTTPFlow.js index ca23837703..720abbe175 100644 --- a/app/main/handlers/queryHTTPFlow.js +++ b/app/main/handlers/queryHTTPFlow.js @@ -329,6 +329,21 @@ module.exports = (win, getClient) => { return await asyncQueryMITMRuleExtractedData(params) }) + const asyncQueryMITMExtractedAggregate = (params) => { + return new Promise((resolve, reject) => { + getClient().QueryMITMExtractedAggregate(params, (err, data) => { + if (err) { + reject(err) + return + } + resolve(data) + }) + }) + } + ipcMain.handle('QueryMITMExtractedAggregate', async (e, params) => { + return await asyncQueryMITMExtractedAggregate(params) + }) + const asyncDeleteMITMRuleExtractedData = (params) => { return new Promise((resolve, reject) => { getClient().DeleteMITMRuleExtractedData(params, (err, data) => { diff --git a/app/renderer/src/main/public/locales/en/history.json b/app/renderer/src/main/public/locales/en/history.json index b401ede692..26482246c1 100644 --- a/app/renderer/src/main/public/locales/en/history.json +++ b/app/renderer/src/main/public/locales/en/history.json @@ -188,6 +188,13 @@ "deleteSuccess": "Delete successful", "deduplicateSuccess": "Deduplicate successful" }, + "HTTPFlowRuleDataFilter": { + "searchPlaceholder": "Search by IP, domain, URL", + "traceCount": "Flows", + "selectOrSearchFirst": "Please check rows or enter a keyword to limit the scope", + "deduplicateDone": "Dedup done: {{n}} duplicate record(s) removed", + "deduplicateNoRepeat": "Dedup done: no duplicates found in current scope" + }, "FuzzableParamList": { "parameterName": "Parameter Name", "parameterLocation": "Parameter Location", diff --git a/app/renderer/src/main/public/locales/zh/history.json b/app/renderer/src/main/public/locales/zh/history.json index 69625247ac..df4c1d4f97 100644 --- a/app/renderer/src/main/public/locales/zh/history.json +++ b/app/renderer/src/main/public/locales/zh/history.json @@ -188,6 +188,13 @@ "deleteSuccess": "删除成功", "deduplicateSuccess": "去重成功" }, + "HTTPFlowRuleDataFilter": { + "searchPlaceholder": "可输入ip、域名、url进行搜索", + "traceCount": "流量条数", + "selectOrSearchFirst": "请先勾选行或输入关键词以限定操作范围", + "deduplicateDone": "去重完成:已删除 {{n}} 条重复数据", + "deduplicateNoRepeat": "去重完成:当前范围内无重复项" + }, "FuzzableParamList": { "parameterName": "参数名", "parameterLocation": "参数位置", diff --git a/app/renderer/src/main/src/assets/icon/outline.tsx b/app/renderer/src/main/src/assets/icon/outline.tsx index 67f592b2aa..723fa748ef 100644 --- a/app/renderer/src/main/src/assets/icon/outline.tsx +++ b/app/renderer/src/main/src/assets/icon/outline.tsx @@ -6254,3 +6254,21 @@ const OutlineCheckCheck = () => ( export const OutlineCheckCheckIcon = (props: Partial) => { return } + +const OutlineFileSliders = () => ( + + + +) +/* + * @description Outline/fileSliders + */ +export const OutlineFileSlidersIcon = (props: Partial) => { + return +} diff --git a/app/renderer/src/main/src/components/HTTPFlowTable/HTTPFlowRuleDataFilter.module.scss b/app/renderer/src/main/src/components/HTTPFlowTable/HTTPFlowRuleDataFilter.module.scss new file mode 100644 index 0000000000..011f51f7c4 --- /dev/null +++ b/app/renderer/src/main/src/components/HTTPFlowTable/HTTPFlowRuleDataFilter.module.scss @@ -0,0 +1,74 @@ +.rule-data-filter { + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; + min-width: 0; + + .rule-data-filter-header-bar { + display: flex; + align-items: center; + gap: 8px; + padding: 6px; + border-bottom: 1px solid var(--Colors-Use-Neutral-Border); + margin-bottom: 8px; + + .rule-data-filter-search { + flex: 1; + } + + .rule-data-filter-actions { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 6px; + } + } + + .rule-data-filter-table { + flex: 1; + overflow: hidden; + padding: 0 4px 8px; + min-width: 0; + + .rule-data-filter-table-inner { + height: 100%; + min-width: 0; + overflow: hidden; + } + } +} + +.search-icon svg, +.filter-icon svg { + width: 14px; + height: 14px; +} + +.filter-icon { + cursor: pointer; +} + +.rule-data-cell { + display: flex; + align-items: center; + gap: 4px; + overflow: hidden; + flex: 1; + min-width: 0; + + span { + display: block; + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } +} + +.copy-action { + flex: none; + width: auto; + min-width: auto; +} diff --git a/app/renderer/src/main/src/components/HTTPFlowTable/HTTPFlowRuleDataFilter.tsx b/app/renderer/src/main/src/components/HTTPFlowTable/HTTPFlowRuleDataFilter.tsx new file mode 100644 index 0000000000..6f921ed040 --- /dev/null +++ b/app/renderer/src/main/src/components/HTTPFlowTable/HTTPFlowRuleDataFilter.tsx @@ -0,0 +1,560 @@ +import React, { useEffect, useMemo, useRef, useState } from 'react' +import { useDebounceEffect, useInViewport, useMemoizedFn, useSize } from 'ahooks' +import { OutlineSearchIcon } from '@/assets/icon/outline' +import { CopyComponents } from '@/components/yakitUI/YakitTag/YakitTag' +import { ColumnsTypeProps, FiltersItemProps } from '@/components/TableVirtualResize/TableVirtualResizeType' +import { TableVirtualResize } from '@/components/TableVirtualResize/TableVirtualResize' +import { MultipleSelect } from './HTTPFlowTable' +import { YakitButton } from '@/components/yakitUI/YakitButton/YakitButton' +import { YakitInput } from '@/components/yakitUI/YakitInput/YakitInput' +import { RefreshIcon } from '@/assets/newIcon' +import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' +import { yakitNotify } from '@/utils/notification' +import { openABSFileLocated } from '@/utils/openWebsite' +import { JSONParseLog } from '@/utils/tool' +import { + MitmExtractAggregateFlowFilterRow, + MitmExtractedAggregateRowNormalized, + YakQueryHTTPFlowRequest, + normalizeQueryMITMExtractedAggregateResponse, + stripMitmAggregateHttpFlowLiveWindow, + stripMitmAggregateTableFeedback, +} from '@/utils/yakQueryHTTPFlow' +import styles from './HTTPFlowRuleDataFilter.module.scss' +import { Tooltip } from 'antd' + +const { ipcRenderer } = window.require('electron') + +const PAGE_SIZE = 50 +const RULE_NAME_COLUMN_WIDTH = 130 +const TRACE_COUNT_COLUMN_WIDTH = 82 +const TABLE_HORIZONTAL_PADDING = 8 +const TABLE_EXTRA_RESERVED_WIDTH = 64 +const RULE_DATA_RESERVED_WIDTH = RULE_NAME_COLUMN_WIDTH + TRACE_COUNT_COLUMN_WIDTH + TABLE_EXTRA_RESERVED_WIDTH +const QUERY_DEBOUNCE_WAIT = 300 +const RULE_NAME_SELECT_MAX_HEIGHT = '40vh' +const RULE_NAME_OPTIONS_LIMIT = 999999 + +interface RuleSummaryItem { + RowKey: string + RuleName: string + SampleData: string + TraceCount: number + SampleTraceIds: string[] +} + +interface HTTPFlowRuleDataFilterProps { + baseParams?: YakQueryHTTPFlowRequest + queryparamsStr: string + onSetFilterRows: (rows: MitmExtractAggregateFlowFilterRow[]) => void + resetTableAndEditorShow?: (table: boolean, editor: boolean) => void +} + +const uniqStrings = (list: string[]) => Array.from(new Set(list.filter(Boolean))) + +const hasHTTPFlowFilterCriteria = (query: YakQueryHTTPFlowRequest | undefined): boolean => { + if (!query) return false + for (const [key, value] of Object.entries(query)) { + if (value === undefined || value === null || value === '') continue + if (Array.isArray(value) && value.length === 0) continue + if (typeof value === 'boolean' && value === false) continue + if (key === 'SourceType' || key === 'Full' || key === 'WithPayload') continue + return true + } + return false +} + +const buildRuleSummaryList = (rows: MitmExtractedAggregateRowNormalized[]): RuleSummaryItem[] => { + const rowMap = new Map() + + rows.forEach((row) => { + const ruleName = row.RuleVerbose || '' + if (!ruleName) return + + const sampleData = row.DisplayData || '' + const rowKey = `${ruleName}\0${sampleData}` + const existing = rowMap.get(rowKey) + + if (existing) { + existing.TraceCount += Number(row.HitCount || 0) + if (row.SampleTraceIds?.length) { + existing.SampleTraceIds = uniqStrings([...existing.SampleTraceIds, ...row.SampleTraceIds]) + } + return + } + + rowMap.set(rowKey, { + RowKey: rowKey, + RuleName: ruleName, + SampleData: sampleData, + TraceCount: Number(row.HitCount || 0), + SampleTraceIds: Array.isArray(row.SampleTraceIds) ? [...row.SampleTraceIds] : [], + }) + }) + + return Array.from(rowMap.values()) +} + +const buildScopeFilterFromRows = (rows: RuleSummaryItem[], keyword?: string) => ({ + TraceID: uniqStrings(rows.flatMap((item) => item.SampleTraceIds)), + RuleVerbose: uniqStrings(rows.map((item) => item.RuleName)), + Keyword: keyword || undefined, +}) + +export const HTTPFlowRuleDataFilter: React.FC = React.memo((props) => { + const { baseParams, queryparamsStr, onSetFilterRows, resetTableAndEditorShow } = props + const { t } = useI18nNamespaces(['history', 'yakitUi']) + const wrapperRef = useRef(null) + const tableContainerRef = useRef(null) + const tableContainerSize = useSize(tableContainerRef) + const [inViewport] = useInViewport(wrapperRef) + const [loading, setLoading] = useState(false) + const [page, setPage] = useState(1) + const [total, setTotal] = useState(0) + const [ruleList, setRuleList] = useState([]) + const [checkedRows, setCheckedRows] = useState([]) + const [ruleVerboseFilter, setRuleVerboseFilter] = useState([]) + const [ruleVerboseSearchVal, setRuleVerboseSearchVal] = useState('') + const [keywordFilter, setKeywordFilter] = useState('') + const [searchValue, setSearchValue] = useState('') + const [deduplicateLoading, setDeduplicateLoading] = useState(false) + const [exportLoading, setExportLoading] = useState(false) + const requestIdRef = useRef(0) + const ruleNameRequestIdRef = useRef(0) + const [searchQueryTick, setSearchQueryTick] = useState(0) + const [isRefresh, setIsRefresh] = useState(false) + const [ruleNameOptions, setRuleNameOptions] = useState([]) + + const flowFilterForRuleList = useMemo(() => { + const parsedQuery = + (queryparamsStr && + (JSONParseLog(queryparamsStr, { + page: 'HTTPFlowRuleDataFilter', + fun: 'buildHTTPFlowQuery', + }) as YakQueryHTTPFlowRequest)) || + {} + const nextQuery = { ...(baseParams || {}), ...parsedQuery } as Record + delete nextQuery.Pagination + delete nextQuery.AfterId + delete nextQuery.BeforeId + delete nextQuery.AnalyzedIds + const tableOnlyQuery = stripMitmAggregateTableFeedback(nextQuery as YakQueryHTTPFlowRequest) + return stripMitmAggregateHttpFlowLiveWindow((tableOnlyQuery || {}) as YakQueryHTTPFlowRequest) + }, [queryparamsStr, baseParams]) + + const queryKey = useMemo(() => JSON.stringify(flowFilterForRuleList), [flowFilterForRuleList]) + const hasFlowFilter = useMemo(() => hasHTTPFlowFilterCriteria(flowFilterForRuleList), [flowFilterForRuleList]) + const activeKeyword = searchValue.trim() || keywordFilter.trim() + const tableWrapWidth = Math.max(0, Math.floor(Number(tableContainerSize?.width || 0)) - TABLE_HORIZONTAL_PADDING) + const tableInitReady = tableWrapWidth > 0 + const sampleDataColumnWidth = useMemo(() => { + if (!tableWrapWidth) return undefined + const width = tableWrapWidth - RULE_DATA_RESERVED_WIDTH + return width > 0 ? width : undefined + }, [tableWrapWidth]) + const sampleDataColumnDataKey = useMemo( + () => `SampleData_${sampleDataColumnWidth ?? 'auto'}`, + [sampleDataColumnWidth], + ) + + const resetRuleNameOptions = useMemoizedFn(() => { + ruleNameRequestIdRef.current += 1 + setRuleNameOptions([]) + }) + + const resetTableState = useMemoizedFn(() => { + requestIdRef.current += 1 + setLoading(false) + setRuleList([]) + setCheckedRows([]) + }) + + const reloadFirstPage = useMemoizedFn(() => { + resetRuleNameOptions() + resetTableState() + if (page === 1) { + setSearchQueryTick((tick) => tick + 1) + return + } + setPage(1) + }) + + const fetchRuleNameOptions = useMemoizedFn(async () => { + const requestId = ruleNameRequestIdRef.current + 1 + ruleNameRequestIdRef.current = requestId + + try { + const req: Record = { + Pagination: { Page: 1, Limit: RULE_NAME_OPTIONS_LIMIT, OrderBy: 'hit_count', Order: 'desc' }, + IncludeDistinctRuleGroups: false, + } + if (hasFlowFilter) req.HttpFlowFilter = flowFilterForRuleList + + const rsp = await ipcRenderer.invoke('QueryMITMExtractedAggregate', req) + if (requestId !== ruleNameRequestIdRef.current) return + const { rows } = normalizeQueryMITMExtractedAggregateResponse(rsp) + setRuleNameOptions(uniqStrings(rows.map((row) => row.RuleVerbose))) + } catch (e) {} + }) + + const ruleNameTags = useMemo(() => { + return uniqStrings([...ruleNameOptions, ...ruleList.map((item) => item.RuleName), ...ruleVerboseFilter]).map( + (name) => ({ + label: name, + value: name, + }), + ) + }, [ruleNameOptions, ruleList, ruleVerboseFilter]) + + const checkedRowKeySet = useMemo(() => new Set(checkedRows.map((item) => item.RowKey)), [checkedRows]) + const selectedRowKeys = useMemo(() => checkedRows.map((item) => item.RowKey), [checkedRows]) + + const isAllSelected = useMemo( + () => !!ruleList.length && ruleList.every((item) => checkedRowKeySet.has(item.RowKey)), + [ruleList, checkedRowKeySet], + ) + + const checkedFilterRows: MitmExtractAggregateFlowFilterRow[] = useMemo( + () => checkedRows.map((item) => ({ RuleVerbose: item.RuleName, DisplayData: item.SampleData })), + [checkedRows], + ) + const checkedFilterRowsKey = useMemo(() => JSON.stringify(checkedFilterRows), [checkedFilterRows]) + const prevCheckedFilterRowsKeyRef = useRef(checkedFilterRowsKey) + + useEffect(() => { + if (checkedFilterRowsKey === prevCheckedFilterRowsKeyRef.current) return + prevCheckedFilterRowsKeyRef.current = checkedFilterRowsKey + onSetFilterRows(checkedFilterRows) + }, [checkedFilterRows, checkedFilterRowsKey, onSetFilterRows]) + + const refreshRuleData = useMemoizedFn(async (nextPage: number) => { + if (nextPage === 1) { + setLoading(true) + setIsRefresh((prev) => !prev) + } + const requestId = requestIdRef.current + 1 + requestIdRef.current = requestId + + try { + const req: Record = { + Pagination: { Page: nextPage, Limit: PAGE_SIZE, OrderBy: 'hit_count', Order: 'desc' }, + IncludeDistinctRuleGroups: false, + } + if (activeKeyword) req.RuleVerboseKeyword = activeKeyword + if (ruleVerboseFilter.length > 0) req.RuleVerbose = ruleVerboseFilter + if (hasFlowFilter) req.HttpFlowFilter = flowFilterForRuleList + + const rsp = await ipcRenderer.invoke('QueryMITMExtractedAggregate', req) + const { rows, total } = normalizeQueryMITMExtractedAggregateResponse(rsp) + + if (requestId !== requestIdRef.current) return + setTotal(total) + + const nextRuleList = buildRuleSummaryList(rows) + + if (nextPage === 1) { + setRuleList(nextRuleList) + setRuleNameOptions(uniqStrings([...nextRuleList.map((item) => item.RuleName), ...ruleVerboseFilter])) + return + } + + setRuleList((prev) => { + const mergedMap = new Map() + prev.forEach((item) => mergedMap.set(item.RowKey, item)) + nextRuleList.forEach((item) => mergedMap.set(item.RowKey, item)) + return Array.from(mergedMap.values()) + }) + } catch (error) { + if (requestId === requestIdRef.current) { + yakitNotify('error', `${error}`) + } + } finally { + if (requestId === requestIdRef.current) { + setLoading(false) + } + } + }) + + // 分页/搜索/onSearch 变化时查询;外部条件变化(queryKey)时自动重置并查询 + const prevQueryKeyRef = useRef('') + const skipFirstQueryRef = useRef(true) + useDebounceEffect( + () => { + if (!inViewport) return + + if (skipFirstQueryRef.current) { + skipFirstQueryRef.current = false + prevQueryKeyRef.current = queryKey + return + } + + if (queryKey !== prevQueryKeyRef.current) { + prevQueryKeyRef.current = queryKey + resetTableState() + resetRuleNameOptions() + if (page !== 1) { + setPage(1) + return + } + } + refreshRuleData(page) + }, + [inViewport, page, queryKey, refreshRuleData, resetRuleNameOptions, resetTableState, searchQueryTick], + { wait: QUERY_DEBOUNCE_WAIT }, + ) + + useDebounceEffect( + () => { + if (!inViewport) return + fetchRuleNameOptions() + }, + [fetchRuleNameOptions, inViewport, queryKey, page, searchQueryTick], + { wait: QUERY_DEBOUNCE_WAIT }, + ) + + const onResetRuleFilter = useMemoizedFn(() => { + reloadFirstPage() + }) + + const onChangeRuleSelection = useMemoizedFn((checked: boolean, rowKey: string) => { + const row = ruleList.find((item) => item.RowKey === rowKey) + if (!row) return + setCheckedRows((prev) => { + if (checked) { + const exists = prev.some((item) => item.RowKey === rowKey) + if (exists) return prev + return [...prev, row] + } else { + return prev.filter((item) => item.RowKey !== rowKey) + } + }) + resetTableAndEditorShow?.(true, false) + }) + + const onSelectAll = useMemoizedFn((_keys: string[], rows: RuleSummaryItem[], checked: boolean) => { + if (checked) { + setCheckedRows([...rows]) + } else { + setCheckedRows([]) + } + resetTableAndEditorShow?.(true, false) + }) + + const onRowClickToggle = useMemoizedFn((row: RuleSummaryItem) => { + setCheckedRows((prev) => { + const exists = prev.some((item) => item.RowKey === row.RowKey) + if (exists) return prev.filter((item) => item.RowKey !== row.RowKey) + return [...prev, row] + }) + resetTableAndEditorShow?.(true, false) + }) + + const onManualRefresh = useMemoizedFn(() => { + reloadFirstPage() + }) + + const onTableChange = useMemoizedFn((_page: number, _limit: number, _, filters: Record) => { + if (filters?.Keyword !== undefined && filters.Keyword !== keywordFilter) { + setKeywordFilter(filters.Keyword ?? '') + reloadFirstPage() + } + }) + + const buildScopeFilter = useMemoizedFn(() => { + if (checkedRows.length > 0) { + return buildScopeFilterFromRows(checkedRows, activeKeyword) + } + + const filter: { RuleVerbose?: string[]; Keyword?: string } = {} + if (ruleVerboseFilter.length > 0) filter.RuleVerbose = ruleVerboseFilter + if (activeKeyword) filter.Keyword = activeKeyword + return filter + }) + + const onExportRuleData = useMemoizedFn(async () => { + const filter = buildScopeFilter() + setExportLoading(true) + try { + const exportFilePath: string = await ipcRenderer.invoke('ExportMITMRuleExtractedData', { Filter: filter }) + if (exportFilePath) openABSFileLocated(exportFilePath) + yakitNotify('success', t('YakitNotification.exportSuccess')) + } catch (error) { + yakitNotify('error', t('YakitNotification.exportFailed', { error: `${error}` })) + } finally { + setExportLoading(false) + } + }) + + const onDeduplicateRuleData = useMemoizedFn(async () => { + const filter = buildScopeFilter() + setDeduplicateLoading(true) + try { + const rsp = await ipcRenderer.invoke('DeduplicateMITMRuleExtractedData', { Filter: filter }) + const n = Number(rsp?.DeletedCount ?? rsp?.deletedCount ?? 0) + yakitNotify( + 'success', + n > 0 ? t('HTTPFlowRuleDataFilter.deduplicateDone', { n }) : t('HTTPFlowRuleDataFilter.deduplicateNoRepeat'), + ) + reloadFirstPage() + } catch (error) { + yakitNotify('error', `${error}`) + } finally { + setDeduplicateLoading(false) + } + }) + + const columns = useMemo( + () => [ + { + title: t('HTTPFlowExtractedDataTable.ruleName'), + dataKey: 'RuleName', + width: RULE_NAME_COLUMN_WIDTH, + ellipsis: true, + enableDrag: true, + filterProps: { + filterKey: 'RuleVerbose', + filterMultiple: true, + filterIcon: , + filterRender: (closePopover: () => void) => ( + , + allowClear: true, + }, + }} + originalList={ruleNameTags} + searchVal={ruleVerboseSearchVal} + onChangeSearchVal={setRuleVerboseSearchVal} + value={ruleVerboseFilter} + onSelect={(v: any) => { + if (Array.isArray(v)) setRuleVerboseFilter(v) + }} + onClose={() => { + reloadFirstPage() + closePopover() + }} + onQuery={onResetRuleFilter} + selectContainerStyle={{ maxHeight: RULE_NAME_SELECT_MAX_HEIGHT }} + /> + ), + }, + }, + { + title: t('HTTPFlowExtractedDataTable.ruleData'), + dataKey: sampleDataColumnDataKey, + width: sampleDataColumnWidth, + ellipsis: true, + render: (_, item: RuleSummaryItem) => ( + +
+ {item.SampleData || '-'} + {item.SampleData && ( +
{ + e.stopPropagation() + }} + onMouseDown={(e) => { + e.stopPropagation() + }} + > + +
+ )} +
+
+ ), + filterProps: { + filterKey: 'Keyword', + filtersType: 'input', + filterIcon: , + }, + }, + { + title: t('HTTPFlowRuleDataFilter.traceCount'), + dataKey: 'TraceCount', + width: TRACE_COUNT_COLUMN_WIDTH, + }, + ], + [ + sampleDataColumnDataKey, + sampleDataColumnWidth, + ruleNameTags, + ruleVerboseSearchVal, + ruleVerboseFilter, + onResetRuleFilter, + reloadFirstPage, + t, + ], + ) + + return ( +
+
+
+ reloadFirstPage()} + onChange={(e) => setSearchValue(e.target.value)} + /> +
+
+ + {t('HTTPFlowDetail.deduplicate')} + + + {t('YakitButton.export')} + + } onClick={onManualRefresh} loading={loading} /> +
+
+ +
+
+ {tableInitReady && ( + + renderKey="RowKey" + isRefresh={isRefresh} + isShowTitle={false} + data={ruleList} + columns={columns} + query={{ Keyword: keywordFilter, RuleVerbose: ruleVerboseFilter }} + loading={loading} + rowSelection={{ + isAll: isAllSelected, + type: 'checkbox', + selectedRowKeys, + onSelectAll, + onChangeCheckboxSingle: onChangeRuleSelection, + }} + pagination={{ + page, + limit: PAGE_SIZE, + total, + onChange: (nextPage) => setPage(nextPage), + }} + onChange={onTableChange} + onRowClick={onRowClickToggle} + /> + )} +
+
+
+ ) +}) diff --git a/app/renderer/src/main/src/components/HTTPFlowTable/HTTPFlowTable.tsx b/app/renderer/src/main/src/components/HTTPFlowTable/HTTPFlowTable.tsx index 02ecbd9f23..947b22c323 100644 --- a/app/renderer/src/main/src/components/HTTPFlowTable/HTTPFlowTable.tsx +++ b/app/renderer/src/main/src/components/HTTPFlowTable/HTTPFlowTable.tsx @@ -1,6 +1,10 @@ import React, { ReactNode, Ref, useEffect, useMemo, useRef, useState, useContext } from 'react' import { Divider, Form, Input, Tooltip, Badge, Progress, Modal } from 'antd' -import { HistoryPluginSearchType, YakQueryHTTPFlowRequest } from '../../utils/yakQueryHTTPFlow' +import { + HistoryPluginSearchType, + MitmExtractAggregateFlowFilterRow, + YakQueryHTTPFlowRequest, +} from '../../utils/yakQueryHTTPFlow' import { PaginationSchema, YakScript } from '../../pages/invoker/schema' import { HTTPFlowDetail, HTTPFlowDetailProp } from '../HTTPFlowDetail' import { info, yakitNotify, yakitFailed } from '../../utils/notification' @@ -296,6 +300,7 @@ export interface HistoryTableTitleShow { export interface HTTPFlowTableProp extends HistoryTableTitleShow { onSelected?: (i?: HTTPFlow) => any params?: YakQueryHTTPFlowRequest + mitmAggregateFilterRows?: MitmExtractAggregateFlowFilterRow[] inViewport?: boolean onSearch?: (i: string) => any title?: string @@ -713,6 +718,7 @@ export const HTTPFlowTable = React.memo((props) => { const [color, setColor] = useState([]) const [onlyFavorite, setOnlyFavorite] = useState(false) const [isShowColor, setIsShowColor] = useState(false) + const mitmAggregateFilterRows = props.mitmAggregateFilterRows || [] const [params, setParams, getParams] = useGetSetState({ SourceType: props.params?.SourceType || 'mitm', ...getRunTimeIdObj(runTimeId), @@ -720,6 +726,14 @@ export const HTTPFlowTable = React.memo((props) => { Full: false, Tags: [], }) + + const campareMitmAggregateFilterRows = useCampare(mitmAggregateFilterRows) + useUpdateEffect(() => { + setParams((prev) => ({ + ...prev, + MitmExtractAggregateFilterRows: mitmAggregateFilterRows, + })) + }, [campareMitmAggregateFilterRows]) useEffect(() => { setParams((pre) => ({ ...pre, diff --git a/app/renderer/src/main/src/components/HTTPHistory.tsx b/app/renderer/src/main/src/components/HTTPHistory.tsx index 2d18395fd4..4ef3fa8203 100644 --- a/app/renderer/src/main/src/components/HTTPHistory.tsx +++ b/app/renderer/src/main/src/components/HTTPHistory.tsx @@ -17,7 +17,7 @@ import { useUpdateEffect, } from 'ahooks' import { useStore } from '@/store/mitmState' -import { YakQueryHTTPFlowRequest } from '@/utils/yakQueryHTTPFlow' +import { MitmExtractAggregateFlowFilterRow, YakQueryHTTPFlowRequest } from '@/utils/yakQueryHTTPFlow' import { YakitResizeBox } from './yakitUI/YakitResizeBox/YakitResizeBox' import { getRemoteValue, setRemoteValue } from '@/utils/kv' import { v4 as uuidv4 } from 'uuid' @@ -26,6 +26,7 @@ import emiter from '@/utils/eventBus/eventBus' import { WebTree } from './WebTree/WebTree' import { OutlineBotIcon, + OutlineFileSlidersIcon, OutlineFilterIcon, OutlineLog2Icon, OutlinePlusIcon, @@ -88,6 +89,7 @@ import YakitCollapse from './yakitUI/YakitCollapse/YakitCollapse' import { YakitPopover } from './yakitUI/YakitPopover/YakitPopover' import { yakitNotify } from '@/utils/notification' import { FiltersItemProps } from './TableVirtualResize/TableVirtualResizeType' +import { HTTPFlowRuleDataFilter } from './HTTPFlowTable/HTTPFlowRuleDataFilter' const { ipcRenderer } = window.require('electron') const { YakitPanel } = YakitCollapse @@ -129,6 +131,11 @@ export const HistoryTab: YakitTabsProps[] = [ label: 'RangeInputNumberTableWrapper.filter', value: 'process', }, + { + icon: , + label: 'HTTPFlowExtractedDataTable.ruleData', + value: 'rules', + }, { icon: , label: 'HTTPHistory.AI', @@ -175,13 +182,15 @@ const HTTPHistoryInner: React.FC = (props) => { secondRatio: '80%', } - if (openTabsFlag) { + if (activeKey === 'rules' && openTabsFlag) { + p.firstRatio = '470px' + } else if (openTabsFlag) { p.firstRatio = '20%' } else { p.firstRatio = '24px' } return p - }, [openTabsFlag]) + }, [openTabsFlag, activeKey]) // #endregion // #region 网站树、进程 @@ -194,6 +203,8 @@ const HTTPHistoryInner: React.FC = (props) => { const [curProcess, setCurProcess] = useState([]) const [processQueryparams, setProcessQueryparams] = useState('') const [curTags, setCurTags] = useState([]) + const [rulesQueryparams, setRulesQueryparams] = useState('') + const [mitmAggregateFilterRows, setMitmAggregateFilterRows] = useState([]) const mitmContent = useContext(MITMContext) @@ -213,6 +224,8 @@ const HTTPHistoryInner: React.FC = (props) => { delete processQuery.ProcessName delete processQuery.Tags setProcessQueryparams(JSON.stringify(processQuery)) + setRulesQueryparams(queryParams || '') + if (pageType === 'MITM') { emiter.emit( 'onMITMLogProcessQuery', @@ -305,6 +318,17 @@ const HTTPHistoryInner: React.FC = (props) => { }} > +
+ { + setOnlyShowFirstNode(table) + setSecondNodeVisible(editor) + }} + /> +
{activeKey === 'ai' && renderHistoryAIReActChat({ externalParameters: { @@ -357,6 +381,7 @@ const HTTPHistoryInner: React.FC = (props) => { includeInUrl={includeInUrl} curProcess={curProcess} curTags={curTags} + mitmAggregateFilterRows={mitmAggregateFilterRows} onQueryParams={onQueryParams} setOnlyShowFirstNode={setOnlyShowFirstNode} setSecondNodeVisible={setSecondNodeVisible} @@ -384,6 +409,7 @@ export const HTTPHistory: React.FC = (props) => ( interface HTTPFlowRealTimeTableAndEditorProps extends HistoryTableTitleShow { pageType: HTTPHistorySourcePageType runtimeId?: string + mitmAggregateFilterRows?: MitmExtractAggregateFlowFilterRow[] filterTagDom?: ReactNode httpHistoryTableTitleStyle?: CSSProperties containerClassName?: string @@ -409,6 +435,7 @@ export const HTTPFlowRealTimeTableAndEditor: React.FC = React.memo((pro secondRatio: '80%', } - if (openTabsFlag) { + if (activeKey === 'rules' && openTabsFlag) { + p.firstRatio = '470px' + } else if (openTabsFlag) { p.firstRatio = '20%' } else { p.firstRatio = '24px' } return p - }, [openTabsFlag]) + }, [openTabsFlag, activeKey]) // #endregion // #region 网站树、进程 @@ -220,6 +224,8 @@ const HTTPHistoryFilterInner: React.FC = React.memo((pro const [curProcess, setCurProcess] = useState([]) const [processQueryparams, setProcessQueryparams] = useState('') const [curTags, setCurTags] = useState([]) + const [rulesQueryparams, setRulesQueryparams] = useState('') + const [mitmAggregateFilterRows, setMitmAggregateFilterRows] = useState([]) // 表格参数改变 const onQueryParams = useMemoizedFn((queryParams, execFlag) => { @@ -234,6 +240,7 @@ const HTTPHistoryFilterInner: React.FC = React.memo((pro delete processQuery.ProcessName delete processQuery.Tags setProcessQueryparams(JSON.stringify(processQuery)) + setRulesQueryparams(queryParams || '') } catch (error) {} }) // #endregion @@ -291,6 +298,12 @@ const HTTPHistoryFilterInner: React.FC = React.memo((pro onSetCurProcess={setCurProcess} > +
+ +
{renderHistoryAIReActChat({ className: styles['ai-wrapper'], @@ -357,6 +370,7 @@ const HTTPHistoryFilterInner: React.FC = React.memo((pro sourceType={sourceType} webFuzzerPageId={webFuzzerPageId} closable={closable} + mitmAggregateFilterRows={mitmAggregateFilterRows} />
} @@ -414,6 +428,7 @@ interface HTTPFlowTableProps { sourceType?: string webFuzzerPageId?: string closable?: boolean + mitmAggregateFilterRows?: MitmExtractAggregateFlowFilterRow[] } const HTTPFlowFilterTable: React.FC = React.memo((props) => { const { @@ -433,6 +448,7 @@ const HTTPFlowFilterTable: React.FC = React.memo((props) => sourceType = 'mitm', webFuzzerPageId, closable = true, + mitmAggregateFilterRows = [], } = props const { t, i18n } = useI18nNamespaces(['yakitUi', 'history', 'yakitRoute']) const { currentPageTabRouteKey, queryPagesDataById } = usePageInfo( @@ -451,6 +467,7 @@ const HTTPFlowFilterTable: React.FC = React.memo((props) => WithPayload: toWebFuzzer, RuntimeIDs: runtimeId.length > 1 ? runtimeId : undefined, RuntimeId: runtimeId.length === 1 ? runtimeId[0] : undefined, + MitmExtractAggregateFilterRows: mitmAggregateFilterRows.length > 0 ? mitmAggregateFilterRows : [], }) const [color, setColor] = useState([]) const [isShowColor, setIsShowColor] = useState(false) @@ -505,6 +522,19 @@ const HTTPFlowFilterTable: React.FC = React.memo((props) => [campareTagsFilter, campareOnlyFavorite], { wait: 500 }, ) + const campareMitmAggregateFilterRows = useCampare(mitmAggregateFilterRows) + useDebounceEffect( + () => { + setQuery((prev) => { + return { + ...prev, + MitmExtractAggregateFilterRows: mitmAggregateFilterRows.length > 0 ? mitmAggregateFilterRows : [], + } + }) + }, + [campareMitmAggregateFilterRows], + { wait: 300 }, + ) // #endregion // #region 高级筛选 @@ -2346,6 +2376,7 @@ const HTTPFlowFilterTable: React.FC = React.memo((props) => refreshTabsContRef.current = true update(1) }) + useEffect(() => { emiter.on('onDeleteToUpdateHTTPHistoryFilter', onDeleteToUpdateHTTPHistoryFilter) return () => { diff --git a/app/renderer/src/main/src/utils/yakQueryHTTPFlow.tsx b/app/renderer/src/main/src/utils/yakQueryHTTPFlow.tsx index c66d8398b2..76bc1cb7a7 100644 --- a/app/renderer/src/main/src/utils/yakQueryHTTPFlow.tsx +++ b/app/renderer/src/main/src/utils/yakQueryHTTPFlow.tsx @@ -2,6 +2,13 @@ import React from 'react' import { yakitHTTPFlow } from '@/services/electronBridge' export type HistoryPluginSearchType = 'all' | 'request' | 'response' + +/** 与 grpc MITMExtractAggregateFlowFilterRow 一致,用于聚合左栏多选联动流量 */ +export interface MitmExtractAggregateFlowFilterRow { + RuleVerbose?: string + DisplayData?: string +} + export interface YakQueryHTTPFlowRequest { SourceType?: string Pagination?: Paging @@ -41,6 +48,62 @@ export interface YakQueryHTTPFlowRequest { ProcessName?: string[] ExcludeKeywords?: string[] AnalyzedIds?: number[] + /** 与 extracted_data.trace_id 对应的 http_flows.hidden_index */ + HiddenIndex?: string[] + /** MITM 提取聚合行多选 OR 过滤 */ + MitmExtractAggregateFilterRows?: MitmExtractAggregateFlowFilterRow[] +} + +/** QueryMITMExtractedAggregate 返回行(IPC/JSON 可能为 PascalCase 或 camelCase) */ +export interface MitmExtractedAggregateRowNormalized { + RuleVerbose: string + DisplayData: string + HitCount: number + LatestUpdatedAt: number + SampleTraceIds?: string[] +} + +/** + * MITM 实时表会用 AfterUpdatedAt 做增量窗口;聚合查询若原样带入 HttpFlowFilter, + * 容易与 extracted_data / join 范围不一致而出现「右侧有流量、左侧聚合空」。 + */ +export function stripMitmAggregateHttpFlowLiveWindow(f: YakQueryHTTPFlowRequest): YakQueryHTTPFlowRequest { + const o: YakQueryHTTPFlowRequest = { ...f } + delete o.AfterUpdatedAt + delete o.BeforeUpdatedAt + return o +} + +/** 流量表 onQueryParams 回传的 JSON 里可能仍带上一轮的聚合联动字段;合并进下一次 QueryHTTPFlows 前应先剥掉,由页面显式写入。 */ +export function stripMitmAggregateTableFeedback( + f: YakQueryHTTPFlowRequest | undefined, +): YakQueryHTTPFlowRequest | undefined { + if (!f) return undefined + const o: YakQueryHTTPFlowRequest = { ...f } + delete o.MitmExtractAggregateFilterRows + delete o.HiddenIndex + return o +} + +/** 兼容主进程 IPC 返回的字段大小写差异 */ +export function normalizeQueryMITMExtractedAggregateResponse(rsp: any): { + rows: MitmExtractedAggregateRowNormalized[] + total: number + distinctRuleGroups: string[] +} { + const rowsRaw = rsp?.Data ?? rsp?.data + const list = Array.isArray(rowsRaw) ? rowsRaw : [] + const rows: MitmExtractedAggregateRowNormalized[] = list.map((raw: any) => ({ + RuleVerbose: String(raw?.RuleVerbose ?? raw?.ruleVerbose ?? ''), + DisplayData: String(raw?.DisplayData ?? raw?.displayData ?? ''), + HitCount: Number(raw?.HitCount ?? raw?.hitCount ?? 0), + LatestUpdatedAt: Number(raw?.LatestUpdatedAt ?? raw?.latestUpdatedAt ?? 0), + SampleTraceIds: (raw?.SampleTraceIds ?? raw?.sampleTraceIds) as string[] | undefined, + })) + const total = Number(rsp?.Total ?? rsp?.total ?? 0) + const g = rsp?.DistinctRuleGroups ?? rsp?.distinctRuleGroups + const distinctRuleGroups = Array.isArray(g) ? g.map((x: any) => String(x)) : [] + return { rows, total, distinctRuleGroups } } export interface Paging {