feat: 资产拦截接入执行链,新增任务级拦截/允许规则
在 agent 下发意图(add_intent)与插入资产(insert_assets)两处接入资产闸门;
并新增任务级规则(拦截 block + 允许 allow 白名单),创建任务时录入、任务详情
总览里可编辑。
- 匹配/执行(db/asset_intercept_match.go):MatchAssetInterceptRules 纯匹配
(exact/fuzzy 的 域名·IP·URL + CIDR,URL host 会拆出归类);EvaluateAssetGate
「先拦截后允许」闸门——命中拦截即禁止;未命中但存在启用的允许规则且都不命中→
不允许测试;无启用允许规则则不启用白名单。附单元测试。
- 执行点:add_intent 命中即不下发意图;insert_assets 命中即跳过 Upsert 及
AddAutoScope 副作用。均合并 全局∪任务级 规则。
- 任务级规则:表 task_intercept_rules(task_id FK CASCADE + action block/allow,
幂等 ALTER 补旧库);db/task_intercept.go 提供 List/Split/CRUD/事务插入;
创建任务随 intercept_rules 提交;CRUD 路由 /api/tasks/{id}/intercept-rules。
- 前端:新建任务抽屉多行录入(拦截/允许 + 类型,用 NativeSelect 避免 Sheet 内
下拉 portal 导致误关抽屉);任务详情总览新增规则管理卡片(增删改+启用开关)。
This commit is contained in:
1 parent
dfc9d2fa2e
commit
d5359e1f04
14 files changed
+1234
No files matched your search
@@ -26,8 +26,11 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { NativeSelect, NativeSelectOption } from "@/components/ui/native-select";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { api } from "@/lib/api";
|
||||
import type {
|
||||
AssetInterceptKind,
|
||||
AssetInterceptRule,
|
||||
Finding,
|
||||
ModelTokenStat,
|
||||
Stats,
|
||||
@@ -697,6 +700,7 @@ export function OverviewTab({ taskId }: { taskId: string }) {
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<TaskInterceptRulesCard taskId={taskId} />
|
||||
{coverage && coverage.enabled && coverage.scope_rows > 0 && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
@@ -1045,3 +1049,274 @@ export function OverviewTab({ taskId }: { taskId: string }) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const TASK_RULE_KIND_OPTIONS: { value: AssetInterceptKind; label: string; placeholder: string }[] = [
|
||||
{ value: "exact_domain", label: "域名(全等)", placeholder: "example.gov.cn" },
|
||||
{ value: "exact_ip", label: "IP(全等)", placeholder: "203.0.113.10" },
|
||||
{ value: "exact_url", label: "URL(全等)", placeholder: "https://example.com/login" },
|
||||
{ value: "fuzzy_domain", label: "域名(模糊)", placeholder: ".gov.cn" },
|
||||
{ value: "fuzzy_ip", label: "IP(模糊)", placeholder: "203.0.113." },
|
||||
{ value: "fuzzy_url", label: "URL(模糊)", placeholder: "/admin" },
|
||||
{ value: "cidr", label: "CIDR 网段", placeholder: "192.168.0.0/16" },
|
||||
];
|
||||
|
||||
const TASK_RULE_KIND_LABEL: Record<AssetInterceptKind, string> = Object.fromEntries(
|
||||
TASK_RULE_KIND_OPTIONS.map((o) => [o.value, o.label]),
|
||||
) as Record<AssetInterceptKind, string>;
|
||||
|
||||
// TaskInterceptRulesCard 在任务详情总览里管理「任务级资产拦截 / 允许规则」:
|
||||
// 列表 + 新增 + 行内编辑 + 删除 + 启用开关。规则仅本任务生效,不进全局表。
|
||||
function TaskInterceptRulesCard({ taskId }: { taskId: string }) {
|
||||
const [rules, setRules] = React.useState<AssetInterceptRule[]>([]);
|
||||
const [busy, setBusy] = React.useState(false);
|
||||
const [err, setErr] = React.useState("");
|
||||
const [newAction, setNewAction] = React.useState<"block" | "allow">("block");
|
||||
const [newKind, setNewKind] = React.useState<AssetInterceptKind>("fuzzy_domain");
|
||||
const [newPattern, setNewPattern] = React.useState("");
|
||||
const [newNote, setNewNote] = React.useState("");
|
||||
const [editId, setEditId] = React.useState<number | null>(null);
|
||||
const [editAction, setEditAction] = React.useState<"block" | "allow">("block");
|
||||
const [editKind, setEditKind] = React.useState<AssetInterceptKind>("fuzzy_domain");
|
||||
const [editPattern, setEditPattern] = React.useState("");
|
||||
const [editNote, setEditNote] = React.useState("");
|
||||
|
||||
const load = React.useCallback(async () => {
|
||||
try {
|
||||
setRules(await api.taskInterceptRules(taskId));
|
||||
} catch {
|
||||
// 忽略瞬时错误
|
||||
}
|
||||
}, [taskId]);
|
||||
|
||||
React.useEffect(() => {
|
||||
void load();
|
||||
}, [load]);
|
||||
|
||||
async function add() {
|
||||
if (!newPattern.trim()) return;
|
||||
setBusy(true);
|
||||
setErr("");
|
||||
try {
|
||||
await api.createTaskInterceptRule(taskId, {
|
||||
action: newAction,
|
||||
kind: newKind,
|
||||
pattern: newPattern.trim(),
|
||||
note: newNote.trim(),
|
||||
enabled: true,
|
||||
});
|
||||
setNewPattern("");
|
||||
setNewNote("");
|
||||
await load();
|
||||
} catch (e) {
|
||||
setErr((e as Error).message);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
function startEdit(r: AssetInterceptRule) {
|
||||
setEditId(r.id);
|
||||
setEditAction(r.action ?? "block");
|
||||
setEditKind(r.kind);
|
||||
setEditPattern(r.pattern);
|
||||
setEditNote(r.note);
|
||||
setErr("");
|
||||
}
|
||||
|
||||
async function saveEdit(r: AssetInterceptRule) {
|
||||
if (!editPattern.trim()) return;
|
||||
setBusy(true);
|
||||
setErr("");
|
||||
try {
|
||||
await api.updateTaskInterceptRule(taskId, r.id, {
|
||||
action: editAction,
|
||||
kind: editKind,
|
||||
pattern: editPattern.trim(),
|
||||
note: editNote.trim(),
|
||||
enabled: r.enabled,
|
||||
});
|
||||
setEditId(null);
|
||||
await load();
|
||||
} catch (e) {
|
||||
setErr((e as Error).message);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function remove(r: AssetInterceptRule) {
|
||||
setRules((prev) => prev.filter((x) => x.id !== r.id));
|
||||
try {
|
||||
await api.deleteTaskInterceptRule(taskId, r.id);
|
||||
} catch {
|
||||
await load();
|
||||
}
|
||||
}
|
||||
|
||||
async function toggle(r: AssetInterceptRule) {
|
||||
setRules((prev) => prev.map((x) => (x.id === r.id ? { ...x, enabled: !x.enabled } : x)));
|
||||
try {
|
||||
await api.toggleTaskInterceptRule(taskId, r.id, !r.enabled);
|
||||
} catch {
|
||||
await load();
|
||||
}
|
||||
}
|
||||
|
||||
const placeholder = TASK_RULE_KIND_OPTIONS.find((o) => o.value === newKind)?.placeholder ?? "";
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<ShieldCheckIcon className="size-4 text-sky-500" /> 任务级资产拦截 / 允许
|
||||
<span className="text-muted-foreground text-xs font-normal">
|
||||
(仅本任务生效,不进全局;先拦截后允许,共 {rules.length} 条)
|
||||
</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-3">
|
||||
{/* 新增表单 */}
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<NativeSelect size="sm" value={newAction} onChange={(e) => setNewAction(e.target.value as "block" | "allow")}>
|
||||
<NativeSelectOption value="block">拦截</NativeSelectOption>
|
||||
<NativeSelectOption value="allow">允许</NativeSelectOption>
|
||||
</NativeSelect>
|
||||
<NativeSelect size="sm" value={newKind} onChange={(e) => setNewKind(e.target.value as AssetInterceptKind)}>
|
||||
{TASK_RULE_KIND_OPTIONS.map((o) => (
|
||||
<NativeSelectOption key={o.value} value={o.value}>
|
||||
{o.label}
|
||||
</NativeSelectOption>
|
||||
))}
|
||||
</NativeSelect>
|
||||
<Input
|
||||
className="h-7 min-w-56 flex-1 text-sm"
|
||||
placeholder={placeholder}
|
||||
value={newPattern}
|
||||
onChange={(e) => setNewPattern(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") void add();
|
||||
}}
|
||||
disabled={busy}
|
||||
/>
|
||||
<Input
|
||||
className="h-7 w-36 text-sm"
|
||||
placeholder="备注(可选)"
|
||||
value={newNote}
|
||||
onChange={(e) => setNewNote(e.target.value)}
|
||||
disabled={busy}
|
||||
/>
|
||||
<Button size="sm" variant="outline" disabled={busy || !newPattern.trim()} onClick={() => void add()}>
|
||||
<PlusIcon className="size-3.5" /> 添加
|
||||
</Button>
|
||||
{err && <span className="text-xs text-red-500">{err}</span>}
|
||||
</div>
|
||||
{/* 规则列表 */}
|
||||
{rules.length > 0 ? (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
{rules.map((r) =>
|
||||
editId === r.id ? (
|
||||
<div key={r.id} className="flex flex-wrap items-center gap-2 rounded-md border px-2.5 py-1.5">
|
||||
<NativeSelect
|
||||
size="sm"
|
||||
value={editAction}
|
||||
onChange={(e) => setEditAction(e.target.value as "block" | "allow")}
|
||||
>
|
||||
<NativeSelectOption value="block">拦截</NativeSelectOption>
|
||||
<NativeSelectOption value="allow">允许</NativeSelectOption>
|
||||
</NativeSelect>
|
||||
<NativeSelect
|
||||
size="sm"
|
||||
value={editKind}
|
||||
onChange={(e) => setEditKind(e.target.value as AssetInterceptKind)}
|
||||
>
|
||||
{TASK_RULE_KIND_OPTIONS.map((o) => (
|
||||
<NativeSelectOption key={o.value} value={o.value}>
|
||||
{o.label}
|
||||
</NativeSelectOption>
|
||||
))}
|
||||
</NativeSelect>
|
||||
<Input
|
||||
className="h-7 min-w-56 flex-1 text-sm"
|
||||
value={editPattern}
|
||||
onChange={(e) => setEditPattern(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") void saveEdit(r);
|
||||
if (e.key === "Escape") setEditId(null);
|
||||
}}
|
||||
disabled={busy}
|
||||
autoFocus
|
||||
/>
|
||||
<Input
|
||||
className="h-7 w-36 text-sm"
|
||||
placeholder="备注(可选)"
|
||||
value={editNote}
|
||||
onChange={(e) => setEditNote(e.target.value)}
|
||||
disabled={busy}
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-6 shrink-0 px-1.5"
|
||||
disabled={busy || !editPattern.trim()}
|
||||
onClick={() => void saveEdit(r)}
|
||||
>
|
||||
<CheckIcon className="size-3.5 text-emerald-500" />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-6 shrink-0 px-1.5"
|
||||
disabled={busy}
|
||||
onClick={() => setEditId(null)}
|
||||
>
|
||||
<XIcon className="size-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div key={r.id} className="flex items-center gap-2 rounded-md border px-2.5 py-1.5 text-sm">
|
||||
<span
|
||||
className={`shrink-0 rounded px-1.5 py-0.5 text-xs ${
|
||||
r.action === "allow"
|
||||
? "bg-emerald-500/15 text-emerald-600 dark:text-emerald-400"
|
||||
: "bg-red-500/15 text-red-600 dark:text-red-400"
|
||||
}`}
|
||||
>
|
||||
{r.action === "allow" ? "允许" : "拦截"}
|
||||
</span>
|
||||
<span className="text-muted-foreground shrink-0 text-xs">{TASK_RULE_KIND_LABEL[r.kind]}</span>
|
||||
<code className="bg-muted min-w-0 flex-1 truncate rounded px-1.5 py-0.5 text-xs">{r.pattern}</code>
|
||||
{r.note && (
|
||||
<span className="text-muted-foreground max-w-[120px] shrink-0 truncate text-xs">{r.note}</span>
|
||||
)}
|
||||
<Switch checked={r.enabled} onCheckedChange={() => void toggle(r)} />
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-6 shrink-0 px-1.5"
|
||||
disabled={busy}
|
||||
onClick={() => startEdit(r)}
|
||||
>
|
||||
<PencilIcon className="size-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-6 shrink-0 px-1.5"
|
||||
disabled={busy}
|
||||
onClick={() => void remove(r)}
|
||||
>
|
||||
<Trash2Icon className="size-3.5 text-red-500" />
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
暂无任务级规则。「拦截」命中即禁止测试;「允许」为白名单——配置后本任务只允许命中允许规则的资产(未配置则不启用白名单)。
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -98,6 +98,7 @@ import {
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Item, ItemActions, ItemContent, ItemDescription, ItemGroup, ItemMedia, ItemTitle } from "@/components/ui/item";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { NativeSelect, NativeSelectOption } from "@/components/ui/native-select";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
@@ -121,6 +122,8 @@ import { api } from "@/lib/api";
|
||||
import { getLocalStorageValue, setLocalStorageValue } from "@/lib/local-storage.client";
|
||||
import { type SortDirection, useStoredSortPreference } from "@/lib/sort-preference";
|
||||
import type {
|
||||
AssetInterceptKind,
|
||||
AssetInterceptRuleInput,
|
||||
ChatAttachment,
|
||||
Company,
|
||||
DeleteTaskOptions,
|
||||
@@ -3032,6 +3035,92 @@ function CategoryManagementSheet({
|
||||
);
|
||||
}
|
||||
|
||||
const TASK_INTERCEPT_KIND_OPTIONS: { value: AssetInterceptKind; label: string; placeholder: string }[] = [
|
||||
{ value: "exact_domain", label: "域名(全等)", placeholder: "example.gov.cn" },
|
||||
{ value: "exact_ip", label: "IP(全等)", placeholder: "203.0.113.10" },
|
||||
{ value: "exact_url", label: "URL(全等)", placeholder: "https://example.com/login" },
|
||||
{ value: "fuzzy_domain", label: "域名(模糊)", placeholder: ".gov.cn" },
|
||||
{ value: "fuzzy_ip", label: "IP(模糊)", placeholder: "203.0.113." },
|
||||
{ value: "fuzzy_url", label: "URL(模糊)", placeholder: "/admin" },
|
||||
{ value: "cidr", label: "CIDR 网段", placeholder: "192.168.0.0/16" },
|
||||
];
|
||||
|
||||
// TaskInterceptRulesField 是创建任务时录入「任务级资产拦截规则」的多行编辑区。
|
||||
// 规则只对本任务生效、不进全局表;与全局规则合并后在 agent 执行时匹配。
|
||||
function TaskInterceptRulesField({
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
value: AssetInterceptRuleInput[];
|
||||
onChange: (v: AssetInterceptRuleInput[]) => void;
|
||||
}) {
|
||||
function update(i: number, patch: Partial<AssetInterceptRuleInput>) {
|
||||
onChange(value.map((r, idx) => (idx === i ? { ...r, ...patch } : r)));
|
||||
}
|
||||
function remove(i: number) {
|
||||
onChange(value.filter((_, idx) => idx !== i));
|
||||
}
|
||||
function add() {
|
||||
onChange([...value, { action: "block", kind: "fuzzy_domain", pattern: "", note: "", enabled: true }]);
|
||||
}
|
||||
return (
|
||||
<div className="grid gap-2">
|
||||
{value.map((r, i) => {
|
||||
const ph = TASK_INTERCEPT_KIND_OPTIONS.find((o) => o.value === r.kind)?.placeholder ?? "";
|
||||
return (
|
||||
<div key={i} className="flex items-center gap-2">
|
||||
<NativeSelect
|
||||
size="sm"
|
||||
className="w-[84px] shrink-0"
|
||||
value={r.action}
|
||||
onChange={(e) => update(i, { action: e.target.value as "block" | "allow" })}
|
||||
>
|
||||
<NativeSelectOption value="block">拦截</NativeSelectOption>
|
||||
<NativeSelectOption value="allow">允许</NativeSelectOption>
|
||||
</NativeSelect>
|
||||
<NativeSelect
|
||||
size="sm"
|
||||
className="w-[120px] shrink-0"
|
||||
value={r.kind}
|
||||
onChange={(e) => update(i, { kind: e.target.value as AssetInterceptKind })}
|
||||
>
|
||||
{TASK_INTERCEPT_KIND_OPTIONS.map((o) => (
|
||||
<NativeSelectOption key={o.value} value={o.value}>
|
||||
{o.label}
|
||||
</NativeSelectOption>
|
||||
))}
|
||||
</NativeSelect>
|
||||
<Input
|
||||
className="flex-1"
|
||||
placeholder={ph}
|
||||
value={r.pattern}
|
||||
onChange={(e) => update(i, { pattern: e.target.value })}
|
||||
/>
|
||||
<Input
|
||||
className="w-[120px] shrink-0"
|
||||
placeholder="备注(可选)"
|
||||
value={r.note}
|
||||
onChange={(e) => update(i, { note: e.target.value })}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="text-destructive hover:text-destructive size-8 shrink-0"
|
||||
onClick={() => remove(i)}
|
||||
>
|
||||
<Trash2Icon className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<Button type="button" size="sm" variant="outline" className="w-fit" onClick={add}>
|
||||
<PlusIcon className="size-4" /> 添加一条
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CreateTaskSheet({
|
||||
tasks,
|
||||
categories,
|
||||
@@ -3059,6 +3148,7 @@ function CreateTaskSheet({
|
||||
const [heartbeatMin, setHeartbeatMin] = React.useState("10"); // planner 心跳(分钟);默认10,下限10(与后端一致)
|
||||
const [seedFirstIntent, setSeedFirstIntent] = React.useState(false); // 创建时下发种子意图,worker 免等首轮 planner 直接开跑;默认关闭,走标准先规划再执行
|
||||
const [coverageEnabled, setCoverageEnabled] = React.useState(true); // 资产覆盖度功能;默认开。关闭=不计算/展示覆盖度、不累积范围、隐藏范围类工具(company 关联不受影响)
|
||||
const [interceptRules, setInterceptRules] = React.useState<AssetInterceptRuleInput[]>([]); // 任务级资产拦截规则(仅本任务生效,不进全局表)
|
||||
// 方式1 文件上传:建任务前把文件暂存到 drafts/<draftId>/uploads/,拿回绝对路径追加进描述。
|
||||
const [uploading, setUploading] = React.useState(false);
|
||||
const [uploadCount, setUploadCount] = React.useState(0);
|
||||
@@ -3125,6 +3215,9 @@ function CreateTaskSheet({
|
||||
seedFirstIntent,
|
||||
planHeartbeatSeconds: heartbeatSec,
|
||||
coverageEnabled,
|
||||
interceptRules: interceptRules
|
||||
.map((r) => ({ ...r, pattern: r.pattern.trim() }))
|
||||
.filter((r) => r.pattern !== ""),
|
||||
});
|
||||
toast.success("任务已创建");
|
||||
setName("");
|
||||
@@ -3139,6 +3232,7 @@ function CreateTaskSheet({
|
||||
setHeartbeatMin("10");
|
||||
setSeedFirstIntent(false);
|
||||
setCoverageEnabled(true);
|
||||
setInterceptRules([]);
|
||||
setUploadCount(0);
|
||||
draftIdRef.current = "";
|
||||
setOpen(false);
|
||||
@@ -3274,6 +3368,13 @@ function CreateTaskSheet({
|
||||
作为范围上下文;不会自动生成意图或强制改变执行目标。
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="task-intercept-rules">任务级资产拦截 / 允许规则(可选)</FieldLabel>
|
||||
<TaskInterceptRulesField value={interceptRules} onChange={setInterceptRules} />
|
||||
<FieldDescription>
|
||||
仅对本任务生效,不写入全局规则。判定顺序:先按「拦截」规则(含全局)匹配,命中即禁止测试;未命中且本任务配置了「允许」规则时,须命中某条允许规则才放行,否则同样不允许测试;未配置任何允许规则则不启用白名单。
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="llm-profiles">LLM 配置链</FieldLabel>
|
||||
<TaskLLMProfileChain
|
||||
|
||||
@@ -15,6 +15,7 @@ import type {
|
||||
ArchiveBatchItem,
|
||||
Asset,
|
||||
AssetInterceptRule,
|
||||
AssetInterceptRuleInput,
|
||||
Audit,
|
||||
BatchCategoryItem,
|
||||
BatchControlItem,
|
||||
@@ -243,6 +244,7 @@ export const api = {
|
||||
seedFirstIntent?: boolean;
|
||||
planHeartbeatSeconds?: number;
|
||||
coverageEnabled?: boolean;
|
||||
interceptRules?: AssetInterceptRuleInput[];
|
||||
}) =>
|
||||
post<Task>("/tasks", {
|
||||
name: input.name ?? "",
|
||||
@@ -256,6 +258,7 @@ export const api = {
|
||||
seed_first_intent: input.seedFirstIntent ?? false,
|
||||
plan_heartbeat_seconds: input.planHeartbeatSeconds ?? 0, // 0 = 后端归一到默认 600(10min)
|
||||
coverage_enabled: input.coverageEnabled ?? true, // 默认开;false=关闭资产覆盖度功能
|
||||
intercept_rules: input.interceptRules ?? [], // 任务级资产拦截规则
|
||||
}),
|
||||
taskCategories: () => get<{ categories: TaskCategory[] }>("/task-categories").then((r) => arr(r.categories)),
|
||||
updateTask: (id: string, input: { name?: string; pinned?: boolean }) => patch<Task>(`/tasks/${id}`, input),
|
||||
@@ -388,6 +391,18 @@ export const api = {
|
||||
deleteConstraint: (id: string, constraintId: string) =>
|
||||
del<{ ok: boolean }>(`/tasks/${id}/constraints/${constraintId}`),
|
||||
|
||||
// ---- 任务级资产拦截/允许规则(总览)----
|
||||
taskInterceptRules: (id: string) =>
|
||||
get<{ rules: AssetInterceptRule[] | null }>(`/tasks/${id}/intercept-rules`).then((r) => arr(r.rules)),
|
||||
createTaskInterceptRule: (id: string, rule: AssetInterceptRuleInput) =>
|
||||
post<AssetInterceptRule>(`/tasks/${id}/intercept-rules`, rule),
|
||||
updateTaskInterceptRule: (id: string, ruleId: number, rule: AssetInterceptRuleInput) =>
|
||||
put<AssetInterceptRule>(`/tasks/${id}/intercept-rules/${ruleId}`, rule),
|
||||
deleteTaskInterceptRule: (id: string, ruleId: number) =>
|
||||
del<{ deleted: boolean }>(`/tasks/${id}/intercept-rules/${ruleId}`),
|
||||
toggleTaskInterceptRule: (id: string, ruleId: number, enabled: boolean) =>
|
||||
post<{ ok: boolean; enabled: boolean }>(`/tasks/${id}/intercept-rules/${ruleId}/toggle`, { enabled }),
|
||||
|
||||
// 本任务测试范围列表(含继承自来源任务的范围)。
|
||||
taskScope: (id: string) => get<{ scope: TaskScopeRow[] }>(`/tasks/${id}/scope`),
|
||||
// 手动新增一条测试范围(kind=company/root_domain/subdomain/ip/cidr)。
|
||||
|
||||
@@ -1201,9 +1201,22 @@ export type AssetInterceptKind =
|
||||
| "fuzzy_url"
|
||||
| "cidr";
|
||||
|
||||
// action 仅用于任务级规则:block=拦截(禁止测试) allow=允许(白名单)。
|
||||
export type AssetInterceptAction = "block" | "allow";
|
||||
|
||||
// 任务级资产拦截/允许规则的录入项(创建任务、任务详情编辑使用)。
|
||||
export interface AssetInterceptRuleInput {
|
||||
action: AssetInterceptAction;
|
||||
kind: AssetInterceptKind;
|
||||
pattern: string;
|
||||
note: string;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface AssetInterceptRule {
|
||||
id: number;
|
||||
enabled: boolean;
|
||||
action?: AssetInterceptAction; // 全局规则不带此字段(恒为拦截);任务级规则区分 block/allow
|
||||
kind: AssetInterceptKind;
|
||||
pattern: string;
|
||||
note: string;
|
||||
|
||||
Reference in new issue
Block a user