Compare commits
3 Commits
e4504daca0
...
ad1711029a
Author | SHA1 | Date |
---|---|---|
|
ad1711029a | 1 week ago |
|
f44fa1d565 | 3 weeks ago |
|
d075ede8b3 | 3 weeks ago |
78 changed files with 6928 additions and 3689 deletions
@ -1,321 +0,0 @@ |
|||
''' |
|||
渗透测试任务管理类 一次任务的闭合性要检查2025-3-10 一次任务后要清理LLM和InstrM的数据 |
|||
''' |
|||
from mycode.TargetManager import TargetManager # 从模块导入类 |
|||
#from LLMManager import LLMManager # 同理修正其他导入 |
|||
from mycode.ControlCenter import ControlCenter #控制中心替代LLM--控制中心要实现一定的基础逻辑和渗透测试树的维护。 |
|||
from myutils.FileManager import FileManager |
|||
from mycode.InstructionManager import InstructionManager |
|||
from mycode.DBManager import DBManager |
|||
from myutils.MyTime import get_local_timestr |
|||
from myutils.MyLogger_logger import LogHandler |
|||
import pickle |
|||
import queue |
|||
import time |
|||
import os |
|||
import threading |
|||
|
|||
class TaskManager: |
|||
|
|||
def __init__(self): |
|||
self.TargetM = TargetManager() |
|||
self.logger = LogHandler().get_logger("TaskManager") |
|||
# 生成功能对象 |
|||
self.DBM = DBManager() #主进程一个DBM |
|||
if not self.DBM.connect(): |
|||
self.logger.error("数据库连接失败!终止工作!") |
|||
return |
|||
self.CCM = ControlCenter(self.DBM,self) |
|||
self.InstrM = InstructionManager(self) # 类对象渗透,要约束只读取信息 |
|||
# 控制最大并发指令数量 |
|||
self.max_thread_num = 2 |
|||
self.task_id = 0 #任务id -- |
|||
self.workth_list = [] #线程句柄list |
|||
# self.long_instr_num = 0 #耗时指令数量 |
|||
# self.long_time_instr = ['nikto'] #耗时操作不计入批量执行的数量,不加不减 |
|||
self.node_queue = queue.Queue() |
|||
|
|||
self.lock = threading.Lock() #线程锁 |
|||
self.node_num = 0 #在处理Node线程的处理 |
|||
self.brun = True |
|||
self.cookie = "" #cookie参数 |
|||
|
|||
def res_in_quere(self,bres,instr,reslut,start_time,end_time,th_DBM,source_result,ext_params,work_node): |
|||
''' |
|||
执行结果入队列 |
|||
:param bres: |
|||
:param instr: |
|||
:param reslut: |
|||
:return: |
|||
''' |
|||
#入数据库 -- bres True和False 都入数据库2025-3-10---加node_path(2025-3-18)#? |
|||
if th_DBM.ok: |
|||
work_node.do_sn += 1 # 指令的执行序列是一个任务共用,要线程锁,错误问题也不大 |
|||
th_DBM.insetr_result(self.task_id, instr, reslut, work_node.do_sn, start_time, end_time, source_result, |
|||
ext_params, work_node.path) |
|||
else: |
|||
self.logger.error("数据库连接失败!!") |
|||
|
|||
#结果入队列---2025-3-18所有的指令均需返回给LLM便于节点状态的更新,所以bres作用要调整。 |
|||
res = {'执行指令':instr,'结果':reslut} |
|||
str_res = json.dumps(res,ensure_ascii=False) #直接字符串组合也可以-待验证 |
|||
work_node.llm_type = 1 |
|||
work_node.add_res(str_res) #入节点结果队列 |
|||
|
|||
def do_worker_th(self): |
|||
#线程的dbm需要一个线程一个 |
|||
th_DBM = DBManager() |
|||
th_DBM.connect() |
|||
while self.brun: |
|||
try: |
|||
work_node = self.node_queue.get(block=False) |
|||
# 测试时使用 |
|||
with self.lock: |
|||
self.node_num += 1 |
|||
# 开始执行指令 |
|||
while work_node.instr_queue: |
|||
#for instruction in work_node.instr_queue: #这里要重新调整#? |
|||
instruction = work_node.instr_queue.pop(0) |
|||
start_time = get_local_timestr() # 指令执行开始时间 |
|||
bres, instr, reslut, source_result, ext_params = self.InstrM.execute_instruction(instruction) |
|||
end_time = get_local_timestr() # 指令执行结束时间 |
|||
self.res_in_quere(bres, instr, reslut, start_time, end_time, th_DBM, source_result, |
|||
ext_params, work_node) # 执行结果入队列 |
|||
|
|||
# #针对一个节点的指令执行完成后,提交LLM规划下一步操作 |
|||
#self.CCM.llm_quere.put(work_node) |
|||
|
|||
# 保存记录--测试时使用--后期增加人为停止测试时可以使用 |
|||
with self.lock: |
|||
self.node_num -= 1 |
|||
if self.node_num == 0 and self.node_queue.empty(): # |
|||
self.logger.debug("此批次指令执行完成!") |
|||
with open("attack_tree", 'wb') as f: |
|||
pickle.dump(self.CCM.attack_tree, f) |
|||
|
|||
except queue.Empty: |
|||
time.sleep(20) |
|||
|
|||
def start_task(self,target_name,target_in): |
|||
''' |
|||
|
|||
:param target_name: 任务目标名字 |
|||
:param target_in: 任务目标访问地址 |
|||
:return: |
|||
''' |
|||
#判断目标合法性 |
|||
bok,target,type = self.TargetM.validate_and_extract(target_in) |
|||
if bok: |
|||
self.target = target |
|||
self.type = type #1-IP,2-domain |
|||
self.task_id = self.DBM.start_task(target_name,target_in) #数据库新增任务记录 |
|||
#获取基本信息: 读取数据库或预生成指令,获取基本的已知信息 |
|||
know_info = "无" #? |
|||
#启动--初始化指令 |
|||
self.CCM.start_do(target,self.task_id) |
|||
|
|||
#创建工作线程----2025-3-18调整为一个节点一个线程, |
|||
for i in range(self.max_thread_num): |
|||
w_th = threading.Thread(target=self.do_worker_th) |
|||
w_th.start() |
|||
self.workth_list.append(w_th) |
|||
|
|||
#等待线程结束--执行生成报告 |
|||
for t in self.workth_list: |
|||
t.join() |
|||
#生成报告 |
|||
pass |
|||
else: |
|||
return False,"{target}检测目标不合规,请检查!" |
|||
|
|||
def stop_task(self): |
|||
self.brun = False |
|||
self.CCM.stop_do() #清空一些全局变量 |
|||
self.InstrM.init_data() |
|||
#结束任务需要收尾处理#? |
|||
|
|||
|
|||
if __name__ == "__main__": |
|||
import json |
|||
TM = TaskManager() |
|||
FM = FileManager() |
|||
current_path = os.path.dirname(os.path.realpath(__file__)) |
|||
strMsg = FM.read_file("test",1) |
|||
|
|||
test_type = 2 |
|||
instr_index = 19 |
|||
iput_index = -1 # 0是根节点 |
|||
indexs = [] |
|||
if test_type == 0: #新目标测试 |
|||
# 启动--初始化指令 |
|||
node_list = TM.CCM.start_do("58.216.217.70", 1) |
|||
#异步处理,需要等待线程结束了 |
|||
for th in TM.CCM.llmth_list: |
|||
th.join() |
|||
elif test_type == 1: |
|||
#测试执行指令 |
|||
with open("attack_tree", "rb") as f: |
|||
TM.CCM.attack_tree = pickle.load(f) |
|||
# 遍历node,查看有instr的ndoe |
|||
nodes = TM.CCM.attack_tree.traverse_dfs() |
|||
if indexs: |
|||
for index in indexs: |
|||
node = nodes[index] |
|||
if node.instr_queue: # list |
|||
TM.node_queue.put(node) |
|||
else: |
|||
for node in nodes: |
|||
if node.instr_queue: |
|||
TM.node_queue.put(node) |
|||
|
|||
#创建线程执行指令 |
|||
for i in range(TM.max_thread_num): |
|||
w_th = threading.Thread(target=TM.do_worker_th) |
|||
w_th.start() |
|||
TM.workth_list.append(w_th) |
|||
# 等待线程结束--执行生成报告 |
|||
for t in TM.workth_list: |
|||
t.join() |
|||
elif test_type ==2: |
|||
#测试LLM返回下一步指令 |
|||
with open("attack_tree", "rb") as f: |
|||
TM.CCM.attack_tree = pickle.load(f) |
|||
#遍历node,查看有res的数据 |
|||
iput_max_num = 0 |
|||
iput_num = 0 |
|||
nodes = TM.CCM.attack_tree.traverse_dfs() |
|||
if indexs: |
|||
for index in indexs: |
|||
node = nodes[index] |
|||
if node.res_quere: |
|||
TM.CCM.llm_quere.put(node) |
|||
else: |
|||
for node in nodes: |
|||
if node.res_quere: |
|||
TM.CCM.llm_quere.put(node) |
|||
|
|||
#创建llm工作线程 |
|||
TM.CCM.brun = True |
|||
for i in range(TM.CCM.max_thread_num): |
|||
l_th = threading.Thread(target=TM.CCM.th_llm_worker()) |
|||
l_th.start() |
|||
TM.CCM.llmth_list.append(l_th) |
|||
# 等待线程结束 |
|||
for t in TM.CCM.llmth_list: |
|||
t.join() |
|||
elif test_type ==3: #执行指定指令 |
|||
with open("attack_tree", "rb") as f: |
|||
TM.CCM.attack_tree = pickle.load(f) |
|||
# 遍历node,查看有instr的ndoe |
|||
nodes = TM.CCM.attack_tree.traverse_dfs() |
|||
instrlist = nodes[instr_index].instr_queue |
|||
instrlist = [''' |
|||
mkdir -p /tmp/nfs_test && mount -t nfs -o nolock 192.168.204.137:/ /tmp/nfs_test && ls /tmp/nfs_test |
|||
'''] |
|||
for instr in instrlist: |
|||
start_time = get_local_timestr() # 指令执行开始时间 |
|||
bres, instr, reslut, source_result, ext_params = TM.InstrM.execute_instruction(instr) |
|||
end_time = get_local_timestr() # 指令执行结束时间 |
|||
|
|||
res = {'执行结果': reslut} |
|||
str_res = json.dumps(res,ensure_ascii=False) # 直接字符串组合也可以-待验证 |
|||
print(str_res) |
|||
elif test_type == 4: #修改Message |
|||
with open("attack_tree", "rb") as f: |
|||
TM.CCM.attack_tree = pickle.load(f) |
|||
#创建一个新的节点 |
|||
from mycode.AttackMap import TreeNode |
|||
testnode = TreeNode("test",0,0) |
|||
TM.CCM.LLM.build_initial_prompt(testnode)#新的Message |
|||
systems = testnode.messages[0]["content"] |
|||
#print(systems) |
|||
# 遍历node,查看有instr的ndoe |
|||
nodes = TM.CCM.attack_tree.traverse_bfs() |
|||
for node in nodes: |
|||
node.messages[0]["content"] = systems |
|||
with open("attack_tree", 'wb') as f: |
|||
pickle.dump(TM.CCM.attack_tree, f) |
|||
elif test_type ==5: #显示指令和结果list |
|||
with open("attack_tree", "rb") as f: |
|||
TM.CCM.attack_tree = pickle.load(f) |
|||
nodes = TM.CCM.attack_tree.traverse_dfs() |
|||
if iput_index == -1: |
|||
for node in nodes: |
|||
print(f"----{node.path}-{node.status}----\n****instr_quere") |
|||
print(f"{','.join(node.instr_queue)}\n****res_quere") |
|||
try: |
|||
print(f"{','.join(node.res_quere)}") |
|||
except: |
|||
print(f"{json.dumps(node.res_quere)}") |
|||
elif iput_index == -2:#只输出有instruction的数据 |
|||
index = 0 |
|||
for node in nodes: |
|||
if node.instr_queue: |
|||
print(f"----{index}--{node.path}--{node.status}----") |
|||
print(f"{','.join(node.instr_queue)}") |
|||
index += 1 |
|||
else: |
|||
print(f"********\n{','.join(nodes[iput_index].instr_queue)}\n********") |
|||
print(f"&&&&&&&&\n{','.join(nodes[iput_index].res_quere)}\n&&&&&&&&") |
|||
elif test_type == 6: #给指定节点添加测试指令 |
|||
with open("attack_tree", "rb") as f: |
|||
TM.CCM.attack_tree = pickle.load(f) |
|||
nodes = TM.CCM.attack_tree.traverse_dfs() |
|||
str_instr = "nmap -sV -p- 192.168.204.137 -T4 -oN nmap_full_scan.txt" |
|||
index = 9 |
|||
nodes[index].instr_queue.append(str_instr) |
|||
nodes[index].res_quere = [] |
|||
with open("attack_tree", 'wb') as f: |
|||
pickle.dump(TM.CCM.attack_tree, f) |
|||
elif test_type == 7: #给指定节点修改指令的执行结果 |
|||
with open("attack_tree", "rb") as f: |
|||
TM.CCM.attack_tree = pickle.load(f) |
|||
nodes = TM.CCM.attack_tree.traverse_dfs() |
|||
str_instr = "psql -h 192.168.204.137 -U postgres -c '\l'" |
|||
start_time = get_local_timestr() # 指令执行开始时间 |
|||
bres, instr, reslut, source_result, ext_params = TM.InstrM.execute_instruction(str_instr) |
|||
end_time = get_local_timestr() # 指令执行结束时间 |
|||
# 入数据库 -- bres True和False 都入数据库2025-3-10---加node_path(2025-3-18)#? |
|||
if TM.DBM.ok: |
|||
TM.DBM.insetr_result(0, instr, reslut, 0, start_time, end_time, source_result, |
|||
ext_params, "独立命令执行") |
|||
index = 9 |
|||
nodes[index].res_quere.clear() |
|||
nodes[index].res_quere.append(reslut) |
|||
|
|||
with open("attack_tree", 'wb') as f: |
|||
pickle.dump(TM.CCM.attack_tree, f) |
|||
elif test_type ==8: #显示有漏洞信息的数据 |
|||
with open("attack_tree", "rb") as f: |
|||
TM.CCM.attack_tree = pickle.load(f) |
|||
nodes = TM.CCM.attack_tree.traverse_dfs() |
|||
if nodes: |
|||
for node in nodes: |
|||
if node.vul_type != "未发现": |
|||
print(f"{node.path}----{node.vul_type}") |
|||
elif test_type == 9: #处理自定义llm回复内容 |
|||
with open("attack_tree", "rb") as f: |
|||
TM.CCM.attack_tree = pickle.load(f) |
|||
nodes = TM.CCM.attack_tree.traverse_dfs() |
|||
node = nodes[5] |
|||
strconent = ''' |
|||
{'role': 'assistant', 'content': '{"action":"update_status", "node": "25端口", "status": "已完成", "vulnerability": {"name":"SMTP用户枚举漏洞","risk":"中危","info":"VRFY命令可验证有效用户"}}\n\n```bash-[目标系统->192.168.204.137->25端口]\nsmtp-user-enum -M VRFY -U /usr/share/wordlists/metasploit/unix_users.txt -t 192.168.204.137\n```\n\n```bash-[目标系统->192.168.204.137->25端口]\nnc -nv 192.168.204.137 25 << EOF\nEXPN root\nMAIL FROM: attacker@example.com\nRCPT TO: external@example.com\nDATA\nTest open relay\n.\nQUIT\nEOF\n```'} |
|||
''' |
|||
strjson = json.loads(strconent) |
|||
node_cmds,commands = TM.CCM.LLM.fetch_instruction(strjson["content"]) |
|||
TM.CCM.tree_manager(node_cmds) |
|||
|
|||
# 更新tree |
|||
bok, new_commands = TM.CCM.tree_manager(node_cmds, node, commands, TM.DBM) |
|||
# 分析指令入对应节点 |
|||
if bok: # 节点指令若存在错误,测试指令都不处理,需要LLM重新生成 |
|||
node_list = TM.CCM.instr_in_node(new_commands, node) |
|||
|
|||
#报不保存待定-- |
|||
with open("attack_tree", 'wb') as f: |
|||
pickle.dump(TM.CCM.attack_tree, f) |
|||
|
|||
|
|||
else: |
|||
#完整过程测试---要设定终止条件 |
|||
pass |
@ -0,0 +1,4 @@ |
|||
class ClientSocket: |
|||
def __init__(self): |
|||
self.user_id = -1 |
|||
self.th_read = None |
@ -0,0 +1,97 @@ |
|||
#对llm返回的指令进行校验 |
|||
import re |
|||
class CommandVerify: |
|||
def __init__(self): |
|||
pass |
|||
|
|||
#验证节点指令的结构完整性--主要是判断JSON元素是否完整 |
|||
def verify_node_cmds(self,node_cmds): |
|||
''' |
|||
- 新增节点:{\"action\":\"add_node\", \"parent\": \"父节点\", \"nodes\": \"节点1,节点2\"}; |
|||
- 未生成指令节点列表:{\"action\": \"no_instruction\", \"nodes\": \"节点1,节点2\"}; |
|||
- 漏洞验证成功:{\"action\": \"find_vul\", \"node\": \"节点\",\"vulnerability\": {\"name\":\"漏洞名称\",\"risk\":\"风险等级(低危/中危/高危)\",\"info\":\"补充信息(没有可为空)\"}}; |
|||
- 完成测试:{\"action\": \"end_work\", \"node\": \"节点\"}; |
|||
''' |
|||
strerror = "" |
|||
for node_json in node_cmds: |
|||
if "action" not in node_json: |
|||
self.logger.error(f"缺少action节点:{node_json}") |
|||
strerror = {"节点指令错误":f"{node_json}缺少action节点,不符合格式要求!"} |
|||
break |
|||
|
|||
action = node_json["action"] |
|||
if action == "add_node": |
|||
if "parent" not in node_json or "nodes" not in node_json: |
|||
strerror = {"节点指令错误": f"{node_json}不符合格式要求,缺少节点!"} |
|||
break |
|||
elif action == "end_work": |
|||
if "node" not in node_json: |
|||
strerror = {"节点指令错误": f"{node_json}不符合格式要求,缺少节点!"} |
|||
break |
|||
elif action =="no_instruction": |
|||
if "nodes" not in node_json: |
|||
strerror = {"节点指令错误": f"{node_json}不符合格式要求,缺少节点!"} |
|||
break |
|||
elif action =="find_vul": |
|||
if "node" not in node_json or "vulnerability" not in node_json: |
|||
strerror = {"节点指令错误": f"{node_json}不符合格式要求,缺少节点!"} |
|||
break |
|||
else: |
|||
strerror = {"节点指令错误": f"{node_json}不可识别的action值!"} |
|||
break |
|||
if not strerror: |
|||
return True,strerror |
|||
return False,strerror |
|||
|
|||
# 验证节点数据的合规性 |
|||
def verify_node_data(self,node_cmds): |
|||
add_nodes = [] |
|||
no_instr_nodes = [] |
|||
for node_cmd in node_cmds: |
|||
do_type = node_cmd["action"] |
|||
if do_type == "add_node": |
|||
nodes = node_cmd["nodes"].split(",") |
|||
add_nodes.extend(nodes) |
|||
elif do_type == "no_instruction": |
|||
nodes = node_cmd["nodes"].split(",") |
|||
no_instr_nodes.extend(nodes) |
|||
else:# 其他类型暂时不验证 |
|||
pass |
|||
#核对指令是否有缺失 |
|||
had_inst_nodes = self._difference_a_simple(add_nodes,no_instr_nodes) #在新增节点,但不在没有指令列表,就是应该要有指令的节点数据 |
|||
no_add_nodes = self._difference_a_simple(no_instr_nodes,add_nodes) #在未新增指令的节点,但不在新增节点,就是没有add的节点,需要新增 |
|||
return had_inst_nodes,no_add_nodes |
|||
|
|||
#--------------辅助函数----------------- |
|||
def get_path_from_command(self,command): |
|||
pass |
|||
|
|||
def _difference_a(self,list_a: list, list_b: list) -> list: |
|||
"""获取 list_a 中存在但 list_b 中不存在的元素(去重版)""" |
|||
set_b = set(list_b) |
|||
return [x for x in list_a if x not in set_b] |
|||
|
|||
def _difference_b(self,list_a: list, list_b: list) -> list: |
|||
"""获取 list_b 中存在但 list_a 中不存在的元素(去重版)""" |
|||
set_a = set(list_a) |
|||
return [x for x in list_b if x not in set_a] |
|||
|
|||
def _difference_a_keep_duplicates(self,list_a: list, list_b: list) -> list: |
|||
"""获取 list_a 中存在但 list_b 中不存在的元素(保留所有重复项和顺序)""" |
|||
set_b = set(list_b) |
|||
return [x for x in list_a if x not in set_b] |
|||
|
|||
def _difference_b_keep_duplicates(self,list_a: list, list_b: list) -> list: |
|||
"""获取 list_b 中存在但 list_a 中不存在的元素(保留所有重复项和顺序)""" |
|||
set_a = set(list_a) |
|||
return [x for x in list_b if x not in set_a] |
|||
|
|||
def _difference_a_simple(self,list_a: list, list_b: list) -> list: |
|||
"""集合差集:list_a - list_b""" |
|||
return list(set(list_a) - set(list_b)) |
|||
|
|||
def _difference_b_simple(self,list_a: list, list_b: list) -> list: |
|||
"""集合差集:list_b - list_a""" |
|||
return list(set(list_b) - set(list_a)) |
|||
|
|||
g_CV = CommandVerify() |
@ -1,8 +0,0 @@ |
|||
import openai |
|||
from openai import OpenAI |
|||
#LLM的基类 |
|||
|
|||
class LLMBase: |
|||
def __init__(self): |
|||
pass |
|||
|
@ -0,0 +1,38 @@ |
|||
import queue |
|||
import threading |
|||
import time |
|||
from mycode.PythoncodeTool import PythoncodeTool |
|||
|
|||
|
|||
class PythonTManager: |
|||
def __init__(self,maxnum): |
|||
self.brun = True |
|||
self.cur_num = 0 |
|||
self.put_lock = threading.Lock() |
|||
# 构建进程池--Python 代码的执行在子进程完成-3个子进程 |
|||
self.maxnum = maxnum |
|||
self.python_tool = PythoncodeTool(maxnum) #python工具实例 |
|||
|
|||
def __del__(self): |
|||
self.python_tool.shutdown() |
|||
|
|||
def execute_instruction(self,instruction): |
|||
bwork = False |
|||
while self.brun: |
|||
with self.put_lock: |
|||
if self.cur_num < self.maxnum: |
|||
self.cur_num += 1 |
|||
bwork = True |
|||
if bwork:#还有空的子进程 |
|||
#提交给进程池执行 |
|||
_,instruction,analysis,_,ext_params = self.python_tool.execute_instruction(instruction) |
|||
#执行完成后,数量减一 |
|||
with self.put_lock: |
|||
self.cur_num -= 1 |
|||
#返回结果 |
|||
return instruction,analysis,analysis,ext_params |
|||
else: #如果没获取的许可,则等待N秒后再尝试---存在问题:多线程间没有先来先到的机制了,有可能第一个来排队的一直等到最后 |
|||
time.sleep(20) #休眠20秒 |
|||
|
|||
|
|||
|
@ -0,0 +1,230 @@ |
|||
#python代码动态执行 |
|||
import queue |
|||
import ast |
|||
import subprocess |
|||
import json |
|||
import builtins |
|||
import re |
|||
import paramiko |
|||
import impacket |
|||
import psycopg2 |
|||
import socket |
|||
import struct |
|||
import sys |
|||
import requests |
|||
import ssl |
|||
import mysql.connector |
|||
import telnetlib |
|||
import time |
|||
import uuid |
|||
import multiprocessing |
|||
import textwrap |
|||
from mycode.Result_merge import my_merge |
|||
from ftplib import FTP |
|||
from requests.auth import HTTPBasicAuth |
|||
|
|||
from myutils.ReturnParams import ReturnParams |
|||
from concurrent.futures import ProcessPoolExecutor, TimeoutError |
|||
|
|||
# -------------------------------------------- |
|||
# 1) 全局 helper:放在模块顶层,才能被子进程 picklable 调用 |
|||
# -------------------------------------------- |
|||
def _execute_dynamic(instruction_str): |
|||
""" |
|||
在子进程中执行 instruction_str 所描述的 dynamic_fun, |
|||
并返回 (status: bool, output: str)。 |
|||
""" |
|||
# 允许的内置函数白名单 |
|||
allowed_builtins = { |
|||
'__name__': __name__, |
|||
'__import__': builtins.__import__, |
|||
'abs': abs, 'all': all, 'any': any, 'bool': bool, |
|||
'chr': chr, 'dict': dict, 'enumerate': enumerate, |
|||
'float': float, 'int': int, 'len': len, 'list': list, |
|||
'max': max, 'min': min, 'print': print, 'range': range, |
|||
'set': set, 'str': str, 'sum': sum, 'type': type, |
|||
'open': open, 'Exception': Exception, 'locals': locals |
|||
} |
|||
# 构造安全的 globals |
|||
safe_globals = { |
|||
'__builtins__': allowed_builtins, |
|||
'subprocess': subprocess, |
|||
'json': json, |
|||
're': re, |
|||
'paramiko': paramiko, |
|||
'impacket': impacket, |
|||
'psycopg2': psycopg2, |
|||
'socket': socket, |
|||
'mysql': mysql, |
|||
'mysql.connector': mysql.connector, |
|||
'struct': struct, |
|||
'sys': sys, |
|||
'requests': requests, |
|||
'ssl': ssl, |
|||
'FTP': FTP, |
|||
'HTTPBasicAuth': HTTPBasicAuth, |
|||
'telnetlib': telnetlib, |
|||
'time': time, |
|||
'uuid':uuid, |
|||
} |
|||
safe_locals = {} |
|||
try: |
|||
# 编译并执行用户提供的 code 字符串 |
|||
compiled = compile(instruction_str, '<dynamic>', 'exec') |
|||
exec(compiled, safe_globals, safe_locals) |
|||
|
|||
# dynamic_fun 必须存在 |
|||
if 'dynamic_fun' not in safe_locals: |
|||
return False, "Function dynamic_fun() 未定义" |
|||
|
|||
# 调用它并返回结果 |
|||
res = safe_locals['dynamic_fun']() |
|||
if not (isinstance(res, tuple) and len(res) == 2 and isinstance(res[0], bool)): |
|||
return False, "dynamic_fun 返回值格式不对" |
|||
return res |
|||
except MemoryError: |
|||
return False, "内存溢出" |
|||
except RecursionError: |
|||
return False, "递归深度过深" |
|||
except Exception as e: |
|||
return False, f"子进程执行出错: {e}" |
|||
|
|||
class PythoncodeTool(): |
|||
def __init__(self,max_num): |
|||
self.proc_pool = ProcessPoolExecutor(max_workers=max_num) |
|||
|
|||
def preprocess(self,code: str) -> str: |
|||
# 去掉最外层空行 |
|||
code = code.strip('\n') |
|||
# 去除多余缩进 |
|||
return textwrap.dedent(code) |
|||
|
|||
def is_safe_code(self,code): |
|||
# List of high-risk functions to block (can be adjusted based on requirements) |
|||
# 只屏蔽这些“完整”函数调用 |
|||
HIGH_RISK = { |
|||
'eval', # eval(...) |
|||
'exec', # exec(...) |
|||
'os.system', # os.system(...) |
|||
'subprocess.call', # subprocess.call(...) |
|||
'subprocess.Popen' # subprocess.Popen(...) |
|||
} |
|||
try: |
|||
tree = ast.parse(code) |
|||
for node in ast.walk(tree): |
|||
if isinstance(node, ast.Call): |
|||
fn = node.func |
|||
# 1) 裸 exec/eval |
|||
if isinstance(fn, ast.Name): |
|||
if fn.id in ('exec', 'eval'): |
|||
return False,"有高风险函数,暂不执行!" |
|||
|
|||
# 2) 模块级别的 os.system、subprocess.call、subprocess.Popen |
|||
elif isinstance(fn, ast.Attribute): |
|||
# value 必须是 Name,才算"模块.方法" |
|||
if isinstance(fn.value, ast.Name): |
|||
fullname = f"{fn.value.id}.{fn.attr}" |
|||
if fullname in HIGH_RISK: |
|||
return False,"有高风险函数,暂不执行!" |
|||
|
|||
return True,"" |
|||
|
|||
except SyntaxError as se: |
|||
# 语法都不通过,也算不安全 |
|||
print("解析失败!", se, "第", se.lineno, "行") |
|||
print("出错的那行是:", code.splitlines()[se.lineno - 1]) |
|||
return False,str(se) |
|||
|
|||
def validate_instruction(self, instruction): |
|||
#指令过滤 |
|||
timeout = 60*10 |
|||
instr = instruction.replace("python_code ","") |
|||
instr = instr.replace("python-code ", "") |
|||
instr = self.preprocess(instr) |
|||
# Safety check |
|||
bsafe,error = self.is_safe_code((instr)) |
|||
if not bsafe: |
|||
return "", timeout,error |
|||
return instr,timeout,"" |
|||
|
|||
def safe_import(self,name,*args,**kwargs): |
|||
ALLOWED_MODULES = ['subprocess', 'json','re'] |
|||
if name not in ALLOWED_MODULES: |
|||
raise ImportError(f"Import of '{name}' is not allowed") |
|||
return builtins.__import__(name, *args, **kwargs) |
|||
|
|||
def _run_dynamic(self, safe_locals, q): |
|||
"""子进程执行 dynamic_fun 并把结果放入队列""" |
|||
try: |
|||
fn = safe_locals['dynamic_fun'] |
|||
res = fn() |
|||
q.put(res) |
|||
except Exception as e: |
|||
q.put((False, f"执行出错: {e}")) |
|||
|
|||
def execute_instruction(self, instruction_old): |
|||
''' |
|||
执行指令:验证合法性 -> 执行 -> 分析结果 |
|||
:param instruction_old: |
|||
:return: |
|||
bool:true-正常返回给大模型,false-结果不返回给大模型 |
|||
str:执行的指令 |
|||
str:执行指令的结果 |
|||
''' |
|||
ext_params = ReturnParams() |
|||
ext_params["is_user"] = False # 是否要提交用户确认 -- 默认False |
|||
ext_params["is_vulnerability"] = False # 是否是脆弱点 |
|||
|
|||
# 第一步:验证指令合法性 |
|||
instruction,time_out,error = self.validate_instruction(instruction_old) |
|||
if not instruction: |
|||
return False, instruction_old, error,"",ext_params |
|||
# 过滤修改后的指令是否需要判重?同样指令再执行结果一致?待定---#? |
|||
|
|||
# 第二步:执行指令 |
|||
future = self.proc_pool.submit(_execute_dynamic, instruction) |
|||
try: |
|||
# 在主进程中等待结果,超时则抛 TimeoutError |
|||
status, tmpout = future.result(timeout=time_out) #这里是阻塞的 |
|||
except TimeoutError: |
|||
# 超时处理 |
|||
future.cancel() |
|||
status, tmpout = False, f"执行超时({time_out} 秒)" |
|||
except Exception as e: |
|||
# 其他异常 |
|||
status, tmpout = False, f"提交子进程运行出错: {e}" |
|||
output = f"status:{status},output:{tmpout}" |
|||
|
|||
# 第三步:分析执行结果 |
|||
analysis = self.analyze_result(output, instruction,"","") |
|||
# 指令和结果入数据库 |
|||
# ? |
|||
if not analysis: # analysis为“” 不提交LLM |
|||
return False, instruction, analysis,"",ext_params |
|||
return True, instruction, analysis,"",ext_params |
|||
|
|||
def analyze_result(self, result,instruction,stderr,stdout): |
|||
#指令结果分析 --- 要不要限定一个max_len? |
|||
if "enum4linux " in instruction: #存在指令包装成Python代码返回的情况 |
|||
result = my_merge("enum4linux",result) |
|||
else: |
|||
if len(result) > 3000: #超过2000长度时,尝试去重重复行 |
|||
lines = result.splitlines() |
|||
seen = set() |
|||
unique_lines = [] |
|||
for line in lines: |
|||
if line not in seen: |
|||
seen.add(line) |
|||
unique_lines.append(line) |
|||
return "\n".join(unique_lines) |
|||
return result |
|||
|
|||
#关闭进程池 |
|||
def shutdown(self): |
|||
self.proc_pool.shutdown(wait=True) |
|||
|
|||
if __name__ == "__main__": |
|||
llm_code = """ |
|||
def run_test(): |
|||
return 'Penetration test executed successfully!' |
|||
""" |
@ -0,0 +1,5 @@ |
|||
|
|||
class RsikManager: |
|||
def __init__(self): |
|||
pass |
|||
|
@ -0,0 +1,233 @@ |
|||
from myutils.ConfigManager import myCongif |
|||
from mycode.TaskObject import TaskObject |
|||
from mycode.DBManager import app_DBM |
|||
from myutils.PickleManager import g_PKM |
|||
|
|||
import threading |
|||
|
|||
class TaskManager: |
|||
def __init__(self): |
|||
self.tasks = {} # 执行中的任务,test_id为key |
|||
self.num_threads = myCongif.get_data("Task_max_threads") |
|||
#获取系统信息 -- 用户可修改的都放在DB中,不修改的放config |
|||
data = app_DBM.get_system_info() |
|||
self.local_ip = data[0] |
|||
self.version = data[1] |
|||
self.tasks_lock = threading.Lock() #加个线程锁?不使用1,quart要使用的异步锁,2.限制只允许一个用户登录,3.遍历到删除的问题不大 |
|||
self.web_cur_task = 0 #web端当前显示的 |
|||
|
|||
|
|||
#判断目标是不是在当前执行任务中,---没加锁,最多跟删除有冲突,问题应该不大 |
|||
def is_target_in_tasks(self,task_target): |
|||
for task in self.tasks.values(): |
|||
if task_target == task.target: |
|||
return True |
|||
return False |
|||
|
|||
#程序启动后,加载未完成的测试任务 |
|||
def load_tasks(self): |
|||
'''程序启动时,加载未执行完成的任务''' |
|||
datas = app_DBM.get_run_tasks() |
|||
for data in datas: |
|||
task_id = data[0] |
|||
task_target = data[1] |
|||
task_status = data[2] |
|||
work_type = data[3] |
|||
cookie_info = data[4] |
|||
llm_type = data[5] |
|||
# 创建任务对象 |
|||
task = TaskObject(task_target, cookie_info, work_type, llm_type, self.num_threads, self.local_ip,self) |
|||
#读取attact_tree |
|||
attack_tree = g_PKM.ReadData(str(task_id)) |
|||
#开始任务 ---会根据task_status来判断是否需要启动工作线程 |
|||
task.start_task(task_id,task_status,attack_tree) |
|||
# 保留task对象 |
|||
self.tasks[task_id] = task |
|||
|
|||
#新建测试任务 |
|||
def create_task(self, test_target,cookie_info,llm_type,work_type): |
|||
"""创建新任务--create和load复用?-- |
|||
1.创建和初始化task_object; |
|||
2.创建task_id |
|||
3.启动工作线程 |
|||
return T/F |
|||
""" |
|||
if self.is_target_in_tasks(test_target): |
|||
raise ValueError(f"Task {test_target} already exists") |
|||
#创建任务对象 |
|||
task = TaskObject(test_target,cookie_info,work_type,llm_type,self.num_threads,self.local_ip,self) |
|||
#获取task_id -- test_target,cookie_info,work_type,llm_type 入数据库 |
|||
task_id = app_DBM.start_task(test_target,cookie_info,work_type,llm_type) |
|||
if task_id >0: |
|||
#创建后启动工作--同时赋值task_id |
|||
task.start_task(task_id) |
|||
#保留task对象 |
|||
self.tasks[task_id] = task |
|||
return True |
|||
else: |
|||
return False |
|||
|
|||
def over_task(self,task_id): |
|||
task = self.tasks[task_id] |
|||
if task: |
|||
task.brun = False |
|||
#修改数据库数据 |
|||
bsuccess = app_DBM.over_task(task_id) |
|||
if bsuccess: |
|||
del self.tasks[task_id] #删除缓存 |
|||
return bsuccess,"" |
|||
else: |
|||
return False,"没有找到对应的任务" |
|||
|
|||
def del_task(self,task_id): |
|||
if g_PKM.DelData(str(task_id)): |
|||
bsuccess = app_DBM.del_task(task_id) |
|||
return bsuccess,"" |
|||
else: |
|||
return False,"删除对应文件失败" |
|||
|
|||
#控制task启停----线程不停 |
|||
def control_taks(self,task_id): |
|||
task = self.tasks[task_id] |
|||
if task: |
|||
if task.task_status == 0: # 0-暂停,1-执行中,2-已完成 |
|||
task.task_status = 1 |
|||
elif task.task_status == 1: |
|||
task.task_status = 0 |
|||
else: |
|||
return False,"当前任务状态不允许修改,请联系管理员!",task.task_status |
|||
else: |
|||
return False,"没有找到对应的任务",None |
|||
return True,"",task.task_status |
|||
|
|||
# 获取任务list |
|||
def get_task_list(self): |
|||
tasks = [] |
|||
for task in self.tasks.values(): |
|||
one_json = {"taskID": task.task_id, "testTarget": task.target, "taskStatus": task.task_status, "safeRank": task.safe_rank, |
|||
"workType": task.work_type} |
|||
tasks.append(one_json) |
|||
rejson = {"tasks": tasks} |
|||
return rejson |
|||
|
|||
#获取节点树 |
|||
def get_node_tree(self,task_id): |
|||
task = self.tasks[task_id] |
|||
if task: |
|||
self.web_cur_task = task_id |
|||
tree_dict = task.attack_tree.get_node_dict() |
|||
return tree_dict |
|||
return None |
|||
|
|||
#获取历史节点树数据 |
|||
def get_his_node_tree(self,task_id): |
|||
attack_tree = g_PKM.ReadData(str(task_id)) |
|||
if attack_tree: |
|||
tree_dict = attack_tree.get_node_dict() |
|||
return tree_dict |
|||
return None |
|||
|
|||
#修改任务的工作模式,只有在暂停状态才能修改 |
|||
def update_task_work_type(self,task_id,new_work_type): |
|||
task = self.tasks[task_id] |
|||
if task: |
|||
if task.task_status == 0: |
|||
task.work_type = new_work_type |
|||
return True |
|||
return False |
|||
|
|||
#控制节点的工作状态 |
|||
def node_bwork_control(self,task_id,node_path): |
|||
task = self.tasks[task_id] |
|||
if task: |
|||
bsuccess,new_bwork = task.attack_tree.update_node_bwork(node_path) |
|||
if bsuccess: |
|||
pass #是否要更新IO数据?----待验证是否有只修改部分数据的方案 |
|||
return bsuccess,new_bwork |
|||
return False,False |
|||
|
|||
#节点单步--只允許web端调用 |
|||
async def node_one_step(self,task_id,node_path): |
|||
task = self.tasks[task_id] |
|||
node = task.attack_tree.find_node_by_nodepath(node_path) |
|||
#web端触发的任务,需要判断当前的执行状态 |
|||
bsuccess,error = await task.put_one_node(node) |
|||
return bsuccess,error |
|||
|
|||
#任务单点--只允许web端调用 |
|||
async def task_one_step(self,task_id): |
|||
task = self.tasks[task_id] |
|||
if task: |
|||
bsuccess,error = await task.put_one_task() |
|||
return bsuccess,error |
|||
else: |
|||
return False,"task_id值存在问题!" |
|||
|
|||
#获取节点待执行任务 |
|||
def get_task_node_todo_instr(self,task_id,nodepath): |
|||
todoinstr = [] |
|||
task = self.tasks[task_id] |
|||
if task: |
|||
node = task.attack_tree.find_node_by_nodepath(nodepath) |
|||
if node: |
|||
todoinstr = node.get_instr_user() |
|||
return todoinstr |
|||
|
|||
#获取节点的MSG信息 |
|||
def get_task_node_MSG(self,task_id,nodepath): |
|||
task = self.tasks[task_id] |
|||
if task: |
|||
node = task.attack_tree.find_node_by_nodepath(nodepath) |
|||
if node: |
|||
tmpMsg = node.get_res_user() |
|||
if tmpMsg: |
|||
return node.messages,tmpMsg[0] #待提交消息正常应该只有一条 |
|||
else: |
|||
return node.messages,{} |
|||
return [],{} |
|||
|
|||
def update_node_MSG(self,task_id,nodepath,newtype,newcontent): |
|||
task = self.tasks[task_id] |
|||
if task: |
|||
node = task.attack_tree.find_node_by_nodepath(nodepath) |
|||
if node: |
|||
work_status = node.get_work_status() |
|||
if work_status == 0 or work_status == 3: |
|||
bsuccess,error = node.updatemsg(newtype,newcontent,0) #取的第一条,也就修改第一条 |
|||
return bsuccess,error |
|||
else: |
|||
return False,"当前节点的工作状态不允许修改MSG!" |
|||
return False,"找不到对应节点!" |
|||
|
|||
def del_node_instr(self,task_id,nodepath,instr): |
|||
task = self.tasks[task_id] |
|||
if task: |
|||
node = task.attack_tree.find_node_by_nodepath(nodepath) |
|||
if node: |
|||
return node.del_instr(instr) |
|||
return False,"找不到对应节点!" |
|||
|
|||
def get_his_tasks(self,target_name,safe_rank,llm_type,start_time,end_time): |
|||
tasks = app_DBM.get_his_tasks(target_name,safe_rank,llm_type,start_time,end_time) |
|||
return tasks |
|||
|
|||
|
|||
#------------以下函数还未验证处理----------- |
|||
|
|||
def start_task(self, task_id): |
|||
"""启动指定任务""" |
|||
task = self.tasks.get(task_id) |
|||
if task: |
|||
task.start(self.num_threads) |
|||
else: |
|||
print(f"Task {task_id} not found") |
|||
|
|||
def stop_task(self, task_id): |
|||
"""停止指定任务""" |
|||
task = self.tasks.get(task_id) |
|||
if task: |
|||
task.stop() |
|||
else: |
|||
print(f"Task {task_id} not found") |
|||
|
|||
g_TaskM = TaskManager() #单一实例 |
@ -0,0 +1,670 @@ |
|||
''' |
|||
渗透测试任务管理类 一次任务的闭合性要检查2025-3-10 一次任务后要清理LLM和InstrM的数据 |
|||
''' |
|||
from mycode.TargetManager import TargetManager # 从模块导入类 |
|||
#from LLMManager import LLMManager # 同理修正其他导入 |
|||
from mycode.ControlCenter import ControlCenter #控制中心替代LLM--控制中心要实现一定的基础逻辑和渗透测试树的维护。 |
|||
from mycode.InstructionManager import g_instrM |
|||
from mycode.DBManager import DBManager,app_DBM |
|||
from mycode.LLMManager import LLMManager |
|||
from mycode.AttackMap import AttackTree,TreeNode |
|||
from myutils.MyTime import get_local_timestr |
|||
from myutils.MyLogger_logger import LogHandler |
|||
from myutils.PickleManager import g_PKM |
|||
from myutils.ConfigManager import myCongif |
|||
from mycode.WebSocketManager import g_WSM |
|||
from mycode.CommandVerify import g_CV |
|||
from mycode.PythonTManager import PythonTManager |
|||
import asyncio |
|||
import queue |
|||
import time |
|||
import os |
|||
import re |
|||
import threading |
|||
import json |
|||
|
|||
class TaskObject: |
|||
|
|||
def __init__(self,test_target,cookie_info,work_type,llm_type,num_threads,local_ip,taskM): |
|||
#功能类相关 |
|||
self.taskM = taskM |
|||
self.logger = LogHandler().get_logger("TaskObject") |
|||
self.InstrM = g_instrM # 类对象渗透,要约束只读取信息,且不允许有类全局对象--持续检查 |
|||
self.PythonM = PythonTManager(myCongif.get_data("Python_max_procs")) |
|||
self.CCM = ControlCenter() #一个任务一个CCM |
|||
self.LLM = LLMManager(llm_type) # LLM对象调整为一个任务一个对象,这样可以为不同的任务选择不同的LLM |
|||
#全局变量 |
|||
self.app_work_type = myCongif.get_data("App_Work_type") #app工作为0时,只允许单步模式工作,是附加规则,不影响正常逻辑处理 |
|||
self.brun = False #任务的停止可以用该变量来控制 |
|||
self.sleep_time = myCongif.get_data("sleep_time") |
|||
self.target = test_target |
|||
self.cookie = cookie_info |
|||
self.work_type = work_type #工作模式 0-人工,1-自动 |
|||
self.task_id = None |
|||
self.task_status = 0 #0-暂停,1-执行中,2-已完成 |
|||
self.local_ip = local_ip |
|||
self.attack_tree = None #任务节点树 |
|||
self.safe_rank = 0 #安全级别 0-9 #?暂时还没实现更新逻辑 |
|||
|
|||
#指令执行相关------- |
|||
self.max_thread_num = num_threads #指令执行线程数量 |
|||
self.workth_list = [None] * num_threads #线程句柄list |
|||
self.doing_instr_list= [""] * num_threads |
|||
self.instr_node_queue = queue.Queue() #待执行指令的节点队列 |
|||
self.node_num = 0 #在处理Node线程的处理 |
|||
#llm执行相关-------- |
|||
self.llm_max_nums = myCongif.get_data("LLM_max_threads") # 控制最大并发指令数量 --- 多线程的话节点树需要加锁 |
|||
self.llmth_list = [None] * self.llm_max_nums # llm线程list |
|||
self.doing_llm_list = [""] * self.llm_max_nums |
|||
self.llm_node_queue = queue.Queue() #待提交LLM的节点队列 |
|||
#自检线程-------- |
|||
self.check_th = None #自检线程句柄 |
|||
#-----四队列----- |
|||
self.run_instr_lock = threading.Lock() # 线程锁 |
|||
self.runing_instr = {} #执行中指令记录 |
|||
|
|||
#---------------三个线程------------ |
|||
#测试指令执行线程 |
|||
def mill_instr_preprocess(self,instructions,str_split): |
|||
new_instr = [] |
|||
instrs = instructions.split(str_split) |
|||
index = 0 |
|||
for instr in instrs: |
|||
if instr.strip().startswith("curl"): |
|||
if " --max-time " not in instr: |
|||
out_time = g_instrM.get_tool_out_time("curl") |
|||
instr = instr.strip() + f" --max-time {str(out_time)}" |
|||
#instr = instr.strip() + " --max-time 10" |
|||
new_instr.append(instr) |
|||
index += 1 |
|||
new_star_instr = f"{str_split}".join(new_instr) |
|||
print(new_star_instr) |
|||
return new_star_instr |
|||
|
|||
def do_worker_th(self,index): |
|||
#线程的dbm需要一个线程一个 |
|||
th_DBM = DBManager() |
|||
th_DBM.connect() |
|||
th_index = index |
|||
while self.brun: |
|||
if self.task_status == 1: |
|||
try: |
|||
work_node = self.instr_node_queue.get(block=False)#正常一个队列中一个节点只会出现一次,进而也就只有一个线程在处理 |
|||
# 开始执行指令 |
|||
results = [] |
|||
while True: |
|||
instruction = work_node.get_instr() |
|||
if not instruction: |
|||
break |
|||
instruction = instruction.strip() |
|||
#对多shell指令的情况进行处理--也有风险 |
|||
if "python-code" not in instruction: |
|||
if "&&" in instruction: |
|||
instruction = self.mill_instr_preprocess(instruction, "&&") |
|||
elif "||" in instruction: |
|||
instruction = self.mill_instr_preprocess(instruction, "||") |
|||
|
|||
start_time = get_local_timestr() # 指令执行开始时间 |
|||
self.doing_instr_list[th_index] = instruction |
|||
if instruction.startswith("python-code"):#python代码--超过子进程数会阻塞等待,但不开始超时记时 |
|||
instr, reslut, source_result, ext_params = self.PythonM.execute_instruction(instruction) |
|||
else:#shell指令 |
|||
instr, reslut, source_result, ext_params = self.InstrM.execute_instruction(instruction) |
|||
self.doing_instr_list[th_index] = "" |
|||
end_time = get_local_timestr() # 指令执行结束时间 |
|||
|
|||
# 入数据库 -- bres True和False 都入数据库2025-3-10---加node_path(2025-3-18)#? |
|||
if th_DBM.ok: |
|||
work_node.do_sn += 1 |
|||
th_DBM.insetr_result(self.task_id, instr, reslut, work_node.do_sn, start_time, end_time, |
|||
source_result, |
|||
ext_params, work_node.path) |
|||
else: |
|||
self.logger.error("数据库连接失败!!") |
|||
#暂存结果 |
|||
oneres = {'执行指令': instr, '结果': reslut} |
|||
results.append(oneres) |
|||
#指令都执行结束后,入节点待提交队列 |
|||
str_res = json.dumps(results, ensure_ascii=False) |
|||
# 提交llm待处理任务 --更新节点work_status |
|||
self.put_node_reslist(work_node, str_res, 1) |
|||
# 保存记录 |
|||
g_PKM.WriteData(self.attack_tree,str(self.task_id)) |
|||
except queue.Empty: |
|||
time.sleep(self.sleep_time) |
|||
else:#不是工作状态则休眠 |
|||
time.sleep(self.sleep_time) |
|||
|
|||
#llm请求提交线程 |
|||
def th_llm_worker(self,index): |
|||
''' |
|||
几个规则--TM的work线程同 |
|||
1.线程获取一个节点后,其他线程不能再获取这个节点(遇到被执行的节点,直接放弃执行)--- 加了没办法保存中间结果进行测试 |
|||
2.llm返回的指令,只可能是该节点,或者是子节点的,不符合这条规则的都不处理,避免llm处理混乱。 |
|||
:return: |
|||
''' |
|||
# 线程的dbm需要一个线程一个 |
|||
th_DBM = DBManager() |
|||
th_DBM.connect() |
|||
th_index = index |
|||
while self.brun: |
|||
if self.task_status == 1: |
|||
try: |
|||
llm_node = self.llm_node_queue.get(block=False) #获取一个待处理节点 |
|||
#开始处理 |
|||
tmp_commands = [] |
|||
# {llm_node.status} --- 暂时固化为未完成 |
|||
user_Prompt = f''' |
|||
当前分支路径:{llm_node.path} |
|||
当前节点信息: |
|||
- 节点名称:{llm_node.name} |
|||
- 节点状态:未完成 |
|||
- 漏洞类型:{llm_node.vul_type} |
|||
''' |
|||
while True: |
|||
llm_data = llm_node.get_res() |
|||
if llm_data is None: |
|||
break |
|||
llm_type = llm_data["llm_type"] |
|||
str_res = llm_data["result"] |
|||
#获取提示词 |
|||
prompt = self.get_llm_prompt(llm_type,str_res,user_Prompt) |
|||
self.doing_llm_list[th_index] = prompt |
|||
# 提交llm请求返回数据--并对返回数据进行处理,节点指令直接执行,测试指令根据工作模式执行 |
|||
node_cmds, commands,reasoning_content, content, post_time = self.LLM.get_llm_instruction(prompt,llm_node) # message要更新 |
|||
self.doing_llm_list[th_index] = "" |
|||
# LLM记录存数据库 |
|||
if th_DBM.ok: |
|||
llm_node.llm_sn += 1 |
|||
bres = th_DBM.insert_llm(self.task_id, prompt, reasoning_content, content, post_time, llm_node.llm_sn,llm_node.path) |
|||
if not bres: |
|||
self.logger.error(f"{llm_node.name}-llm入库失败!") |
|||
else: |
|||
self.logger.error("数据库连接失败!") |
|||
''' |
|||
对于LLM返回的错误处理机制 |
|||
1.验证节点是否都有测试指令返回 |
|||
2.LLM的回复开始反复时(有点难判断) |
|||
''' |
|||
# 更新tree |
|||
bok, new_commands,iadd_node = self.tree_manager(node_cmds, llm_node, commands, th_DBM) |
|||
# 分析指令入对应节点 |
|||
if bok: # 节点指令若存在错误,测试指令都不处理,需要LLM重新生成 |
|||
tmp_commands.extend(new_commands) |
|||
#测试指令入节点待处理队列 --同时修改节点的work_status |
|||
self.put_node_instrlist(tmp_commands, llm_node,iadd_node) |
|||
#一个节点完成,节点树持久化---待验证是否有局部更新持久化的方案 |
|||
g_PKM.WriteData(self.attack_tree,str(self.task_id)) |
|||
except queue.Empty: |
|||
#self.logger.debug("llm队列中暂时无新的提交任务!") |
|||
time.sleep(self.sleep_time) |
|||
else: |
|||
time.sleep(self.sleep_time) |
|||
|
|||
#自检线程 --1.输出执行状态。2.需要自检和修复 |
|||
def th_check(self): |
|||
while self.brun: |
|||
try: |
|||
cur_time = get_local_timestr() |
|||
print(f"-----------当前时间程序运行情况:{cur_time}") |
|||
# #待执行instr-node |
|||
# instr_node_list = list(self.instr_node_queue.queue) #待执行指令的node--线程不安全 |
|||
# print(f"**当前待执行指令node的数量为:{len(instr_node_list)}") |
|||
#执行中instr-node |
|||
index = 0 |
|||
for w_th in self.workth_list: |
|||
if not w_th.is_alive():#线程 |
|||
print(f"线程-{index}已处于异常状态,需要重新创建一个工作线程") |
|||
else: |
|||
print(f"线程-{index}在执行指令:{self.doing_instr_list[index]}") |
|||
index += 1 |
|||
|
|||
index = 0 |
|||
for l_th in self.llmth_list: |
|||
if not l_th.is_alive(): |
|||
print(f"LLM线程-{index}已处于异常状态,需要重新创建一个LLM线程") |
|||
else: |
|||
print(f"LLM线程-{index}在执行指令:{self.doing_llm_list[index]}") |
|||
index += 1 |
|||
#待提交llm-node |
|||
# llm_node_list = list(self.llm_node_queue.queue) #待提交llm的node--线程不安全 |
|||
# print(f"**当前待提交llm的node的数量为:{len(llm_node_list)}") |
|||
#休眠60 |
|||
time.sleep(60) |
|||
except Exception as e: |
|||
print(f"*********自检线程异常退出:{str(e)}") |
|||
break |
|||
|
|||
#------------入两个nodeMQ-禁止直接调用入队列----------- |
|||
def put_instr_mq(self,node): |
|||
#这里不做状态的判断,调用前处理 |
|||
self.instr_node_queue.put(node) |
|||
self.update_node_work_status(node,2) #在执行--1.work_status不影响整个任务的执行,错了问题不大,2--attack_tree持久化需要出去lock信息。 |
|||
|
|||
def put_llm_mq(self,node): |
|||
#同instr_mq |
|||
self.llm_node_queue.put(node) |
|||
self.update_node_work_status(node,4) #提交中 |
|||
|
|||
async def put_instr_mq_async(self,node): |
|||
#这里不做状态的判断,调用前处理 |
|||
self.instr_node_queue.put(node) |
|||
await self.update_node_work_status_async(node,2) #在执行--1.work_status不影响整个任务的执行,错了问题不大,2--attack_tree持久化需要出去lock信息。 |
|||
|
|||
async def put_llm_mq_async(self,node): |
|||
#同instr_mq |
|||
self.llm_node_queue.put(node) |
|||
await self.update_node_work_status_async(node,4) #提交中 |
|||
|
|||
async def update_node_work_status_async(self,node,work_status): |
|||
#更新状态 |
|||
bchange = node.update_work_status(work_status) |
|||
#基于websocket推送到前端 |
|||
if bchange: |
|||
#判断是否是web端最新获取数据的task |
|||
if self.taskM.web_cur_task == self.task_id: |
|||
idatatype = 1 |
|||
strdata = {"node_path":node.path,"node_workstatus":work_status} |
|||
await g_WSM.send_data(idatatype,strdata) |
|||
|
|||
#------------入Node的两个list--禁止直接调用入list------- |
|||
def put_node_reslist(self, node, str_res, llm_type): |
|||
# 推送llm提交任务到节点的待处理任务中,并根据工作模式判断是否如llm_node_quere |
|||
one_llm = {'llm_type': llm_type, 'result': str_res} |
|||
node.add_res(one_llm) # 入节点结果队列 |
|||
self.update_node_work_status(node,3) #待提交llm |
|||
# 如果是自动执行的模式则入队列交给llm线程处理 |
|||
if self.app_work_type == 1: |
|||
if self.work_type == 1 and node.bwork: |
|||
self.put_llm_mq(node) #变4 |
|||
|
|||
#递归找节点 |
|||
def find_node_by_child_node_name(self,cur_node,node_name): |
|||
find_node = None |
|||
if cur_node.children: |
|||
for child_node in cur_node.children: |
|||
if child_node.name == node_name: |
|||
find_node = child_node |
|||
break |
|||
else: |
|||
find_node = self.find_node_by_child_node_name(child_node,node_name) |
|||
if find_node: |
|||
break |
|||
return find_node |
|||
|
|||
def put_node_instrlist(self, commands, node,iadd_node): #如果当前节点没有进一般指令返回,需要修改节点执行状态 |
|||
node_list = [] #一次返回的测试指令 |
|||
for command in commands: |
|||
# 使用正则匹配方括号中的node_path(非贪婪模式) |
|||
match = re.search(r'\[(.*?)\]', command) |
|||
if match: |
|||
node_path = match.group(1) |
|||
instruction = re.sub(r'\[.*?\]', "", command, count=1, flags=re.DOTALL) |
|||
#'''强制约束,不是本节点或者是子节点的指令不处理''' |
|||
find_node = self.attack_tree.find_node_by_nodepath_parent(node_path,node,iadd_node,commands) |
|||
if not find_node:#对于没有基于节点路径找到对应节点--增加通过节点名称匹配的机制 2025-4-13日添加 |
|||
node_name = node_path.split("->")[-1] |
|||
find_node = self.find_node_by_child_node_name(node,node_name) |
|||
|
|||
if find_node: |
|||
find_node.add_instr(instruction) |
|||
#DS-llm存在返回指令还会修改节点状态为已完成的问题,需要修正 |
|||
find_node.status = "未完成" |
|||
if find_node not in node_list: |
|||
node_list.append(find_node) |
|||
self.update_node_work_status(find_node,1) #待执行 |
|||
else:#如果还没找到就暂时放弃 |
|||
self.logger.error(f"基于节点路径没有找到对应的节点{node_path},父节点都没找到!当前节点{node.path}")#丢弃该指令 |
|||
else: |
|||
self.logger.error(f"得到的指令格式不符合规范:{command}")#丢弃该指令--- |
|||
#这里对于丢弃指令,有几种方案: |
|||
# 1.直接丢弃不处理,但需要考虑会不会产生节点缺失指令的问题,需要有机制验证节点;------ 需要有个独立线程,节点要加锁--首选待改进方案 |
|||
# 2.入当前节点的res_queue,但有可能当前节点没有其他指令,不会触发提交,另外就算提交,会不会产生预设范围外的返回,不确定; |
|||
# 3.独立队列处理 |
|||
#判断当前节点是否有指令 |
|||
if node not in node_list: |
|||
#修改该节点状态为0--无待执行任务 |
|||
self.update_node_work_status(node,0) |
|||
#入instr队列 |
|||
if self.app_work_type == 1: |
|||
if self.work_type == 1: #是自动执行模式 |
|||
for node in node_list: |
|||
if node.bwork: |
|||
self.put_instr_mq(node) #2-执行中 |
|||
|
|||
def put_work_node(self): |
|||
'''遍历节点需要处理的任务,提交mq''' |
|||
nodes = self.attack_tree.traverse_bfs() |
|||
for node in nodes: |
|||
if not node.is_instr_empty(): #待执行指令有值 |
|||
if not node.is_llm_empty(): |
|||
self.logger.error(f"{node.path}即存在待执行指令,还存在待提交的llm,需要人工介入!!") |
|||
else: |
|||
if node.bwork: |
|||
self.put_instr_mq(node) #提交执行 |
|||
elif not node.is_llm_empty(): #待提交llm有值 |
|||
if not node.is_instr_empty(): |
|||
self.logger.error(f"{node.path}即存在待执行指令,还存在待提交的llm,需要人工介入!!") |
|||
else: |
|||
if node.bwork: |
|||
self.put_llm_mq(node) #提交执行 |
|||
|
|||
#web端提交单步任务--节点单步 |
|||
async def put_one_node(self,node): |
|||
#提交某个节点的代表任务 |
|||
if self.task_status ==1 and self.work_type==0 and node.bwork: |
|||
if not node.is_instr_empty(): #待执行指令有值 |
|||
if not node.is_llm_empty(): |
|||
self.logger.error(f"{node.path}即存在待执行指令,还存在待提交的llm,需要人工介入!!") |
|||
return False,"该节点的待执行任务数据不正确,请联系管理员!" |
|||
else: |
|||
if node.bwork: |
|||
await self.put_instr_mq_async(node) #提交执行 |
|||
elif not node.is_llm_empty(): #待提交llm有值 |
|||
if not node.is_instr_empty(): |
|||
self.logger.error(f"{node.path}即存在待执行指令,还存在待提交的llm,需要人工介入!!") |
|||
return False, "该节点的待执行任务数据不正确,请联系管理员!" |
|||
else: |
|||
if node.bwork: |
|||
await self.put_llm_mq_async(node) #提交执行 |
|||
else: |
|||
await self.update_node_work_status_async(node,0) #只是修补措施,保障状态的一致性 |
|||
return False,"当前节点没有待执行任务!" |
|||
return True,"已提交单步任务" |
|||
else: |
|||
return False,"当前的任务或节点状态不允许执行单步,请检查!" |
|||
|
|||
#web端提交任务单步--任务单步 |
|||
async def put_one_task(self): |
|||
if self.task_status == 1 and self.work_type == 0: |
|||
nodes = self.attack_tree.traverse_bfs() |
|||
for node in nodes: |
|||
await self.put_one_node(node) |
|||
return True,"已提交单步任务" |
|||
else: |
|||
return False,"当前的任务状态不允许执行单步,请检查!" |
|||
|
|||
#修改节点的执行状态,并需要基于websocket推送到前端显示 同步线程调用 |
|||
def update_node_work_status(self,node,work_status): |
|||
#更新状态 |
|||
bchange = node.update_work_status(work_status) |
|||
#基于websocket推送到前端 |
|||
if bchange: |
|||
#判断是否是web端最新获取数据的task |
|||
if self.taskM.web_cur_task == self.task_id: |
|||
idatatype = 1 |
|||
strdata = {"node_path":node.path,"node_workstatus":work_status} |
|||
asyncio.run(g_WSM.send_data(idatatype,strdata)) |
|||
|
|||
#获取本次的提交提示词 |
|||
def get_llm_prompt(self,llm_type,str_res,user_Prompt): |
|||
if llm_type == 0: |
|||
ext_Prompt = f''' |
|||
初始信息:{str_res} |
|||
任务:请开始对该目标的渗透测试工作。 |
|||
''' |
|||
elif llm_type == 1: # 提交指令执行结果 --- 正常提交 |
|||
# 构造本次提交的prompt |
|||
ext_Prompt = f''' |
|||
上一步结果:{str_res} |
|||
任务:生成下一步渗透测试指令或判断是否完成该节点测试。 |
|||
''' |
|||
elif llm_type == 2: # llm返回的指令存在问题,需要再次请求返回 |
|||
ext_Prompt = f''' |
|||
反馈类型:节点指令格式错误 |
|||
错误信息:{str_res} |
|||
任务:请按格式要求重新生成该节点上一次返回中生成的所有指令。 |
|||
''' |
|||
elif llm_type ==3: #对节点没有指令的,请求指令 |
|||
ext_Prompt = f''' |
|||
任务:{str_res}。 |
|||
''' |
|||
elif llm_type == 5: |
|||
ext_Prompt = f''' |
|||
反馈类型:测试指令格式错误 |
|||
错误信息:{str_res} |
|||
任务:请根据格式要求,重新生成该测试指令。 |
|||
''' |
|||
else: |
|||
self.logger.debug("意外的类型参数") |
|||
return "" |
|||
|
|||
user_Prompt = user_Prompt + ext_Prompt |
|||
return user_Prompt |
|||
|
|||
#添加子节点 |
|||
def add_children_node(self,parent_node,children_names,cur_node,status="未完成"): |
|||
for child_name in children_names: |
|||
bfind = False |
|||
for node_child in parent_node.children: |
|||
if node_child.name == child_name: |
|||
bfind = True #有重复的了 |
|||
break |
|||
if not bfind: |
|||
# 添加节点 |
|||
new_node = TreeNode(child_name, parent_node.task_id, status) |
|||
parent_node.add_child(new_node,cur_node.messages) # message的传递待验证 |
|||
|
|||
#处理节点指令 |
|||
def tree_manager(self,node_cmds,node,commands,DBM): |
|||
'''更新渗透测试树 |
|||
node_cmds是json-list |
|||
2025-03-22添加commands参数,用于处理LLM对同一个节点返回了测试指令,但还返回了no_instruction节点指令 |
|||
''' |
|||
if not node_cmds: # or len(node_cmds)==0: 正常not判断就可以有没有节点指令 |
|||
return True,commands,0 |
|||
|
|||
#对节点指令进行校验 |
|||
bok,strerror = g_CV.verify_node_cmds(node_cmds) |
|||
if not bok: #节点指令存在问题,则不进行后续处理,提交一个错误反馈任务 |
|||
# 提交llm待处理任务 |
|||
self.put_node_reslist(node, strerror, 2) |
|||
return False,commands,0 |
|||
|
|||
residue_node_cmds = [] |
|||
no_instr_nodes = [] |
|||
#如果有on_instruction,先补全指令保障信息链的完整 |
|||
for node_cmd in node_cmds: |
|||
action = node_cmd["action"] |
|||
if action == "no_instruction": |
|||
node_names = node_cmd["nodes"].split(',') |
|||
for node_name in node_names: |
|||
# 先判断是否在测试指令中,若在则不提交llm任务,只能接受在一次返回中同一节点有多条测试指令,不允许分次返回 |
|||
bcommand = False |
|||
for com in commands: |
|||
if node_name in com: |
|||
bcommand = True |
|||
break |
|||
if bcommand: # 如果存在测试指令,则不把该节点放入补充信息llm任务---尝试不对比是否有返回指令,DS会一直返回指令,还返回on_instruction |
|||
continue |
|||
no_instr_nodes.append(node_name) |
|||
else:#剩余的节点指令 |
|||
residue_node_cmds.append(node_cmd) |
|||
|
|||
if no_instr_nodes: # 阻塞式,在当前节点提交补充信息,完善节点指令 -- 优势是省token |
|||
new_commands = self.get_other_instruction(no_instr_nodes, DBM, node) |
|||
commands.extend(new_commands) |
|||
|
|||
# #对节点数据进行初步验证 |
|||
# ad_instr_nodes, no_add_nodes = g_CV.verify_node_data(node_cmds) |
|||
# if no_add_nodes:#如果有没有添加的节点,默认在当前节点下添加 -- 一般不会有,还没遇到 |
|||
# self.add_children_node(node,no_add_nodes,node) |
|||
# #ad_instr_nodes --- 还没处理 |
|||
|
|||
#先执行add_node操作---2025-4-12-调整:message取当前节点,节点允许为子节点添加子节点 |
|||
iadd_node = 0 |
|||
residue_cmd_sno_add_= [] |
|||
for node_json in residue_node_cmds: |
|||
action = node_json["action"] |
|||
if action == "add_node": # 新增节点 |
|||
parent_node_name = node_json["parent"] |
|||
status = "未完成" #2025-4-11修改MGS-节点指令格式,取消了status |
|||
node_names = node_json["nodes"].split(',') |
|||
# 新增节点原则上应该都是当前节点增加子节点 |
|||
if node.name == parent_node_name or parent_node_name.endswith(node.name): #2233ai,节点名称字段会返回整个路径 |
|||
#添加当前节点的子节点 -- 这是标准情况 |
|||
self.add_children_node(node, node_names,node) |
|||
iadd_node += len(node_names) # 添加节点的数量---当前只记录给当前节点添加的子节点的数量 |
|||
elif node.parent.name == parent_node_name or parent_node_name.endswith(node.parent.name):#添加当前节点的平级节点 |
|||
#是添加当前节点的平级节点(当前节点的父节点下添加子节点) --使用2233ai-o3时遇到的情况 |
|||
self.add_children_node(node.parent,node_names,node) |
|||
else: |
|||
badd = False |
|||
for child_node in node.children:#给子节点添加子节点 |
|||
if parent_node_name == child_node.name or parent_node_name.endswith(child_node.name): |
|||
badd = True |
|||
self.add_children_node(child_node,node_names,node) |
|||
break |
|||
if not badd: |
|||
self.logger.error(f"添加子节点时,遇到父节点名称没有找到的,需要介入!!{node_json}") # 丢弃该节点 |
|||
else:#未处理的节点指令添加到list |
|||
residue_cmd_sno_add_.append(node_json) |
|||
|
|||
if iadd_node and self.taskM.web_cur_task == self.task_id: #如果新增了节点,且该节点树是当前查看的数据,需要通知前端更新数据 |
|||
idatatype = 2 |
|||
strdata = "update accack_tree!" |
|||
asyncio.run(g_WSM.send_data(idatatype, strdata)) |
|||
#先取消当前task,已经通知前端重新获取,这样可以避免后端不必要的数据推送 |
|||
self.taskM.web_cur_task = 0 |
|||
|
|||
#执行剩余的节点指令--不分先后 |
|||
for node_json in residue_cmd_sno_add_:#2025-4-11重新调整了节点指令格式定义 |
|||
action = node_json["action"] |
|||
if action == "find_vul": |
|||
node_name = node_json["node"] |
|||
vul_node = None |
|||
if node.name == node_name or node_name.endswith(node.name): #正常应该是当前节点漏洞信息--暂时只考虑只会有一个漏洞 |
|||
vul_node = node |
|||
else: #匹配子节点 |
|||
for child in node.children: |
|||
if child.name == node_name or node_name.endswith(child.name): |
|||
vul_node = node |
|||
break |
|||
if vul_node: #找到对应了漏洞节点 |
|||
try: |
|||
vul_node.vul_type = node_json["vulnerability"]["name"] |
|||
vul_node.vul_grade = node_json["vulnerability"]["risk"] |
|||
vul_node.vul_info = node_json["vulnerability"]["info"] |
|||
#保存到数据库 |
|||
DBM.insert_taks_vul(self.task_id,vul_node.name,vul_node.path,vul_node.vul_type,vul_node.vul_grade, |
|||
vul_node.vul_info) |
|||
except: |
|||
self.logger.error("漏洞信息错误") |
|||
continue |
|||
else: |
|||
str_user = f"遇到不是修改本节点状态的,需要介入!!{node_json}" |
|||
self.logger.error(str_user) |
|||
elif action == "end_work": |
|||
node_name = node_json["node"] |
|||
if node.name == node_name or node_name.endswith(node_name): # 正常应该是当前节点 |
|||
node.status = "已完成" |
|||
else: |
|||
str_user = f"遇到不是修改本节点状态的,需要介入!!{node_json}" |
|||
self.logger.error(str_user) |
|||
elif action == "no_create": #提交人工确认 |
|||
nodes = node_json["nodes"] |
|||
if nodes: |
|||
str_add = {"未新增的节点": nodes} |
|||
self.logger.debug(str_add) |
|||
# 提交一个继续反馈任务--继续后续工作 2025-3-25不自动处理 |
|||
# self.put_one_llm_work(str_add, node, 4) |
|||
# self.logger.debug(f"未新增的节点有:{nodes}") |
|||
else: |
|||
self.logger.error("****不应该执行到这!程序逻辑存在问题!") |
|||
return True,commands,iadd_node |
|||
|
|||
#阻塞轮询补充指令 |
|||
def get_other_instruction(self,nodes,DBM,cur_node): |
|||
res_str = ','.join(nodes) |
|||
new_commands = [] |
|||
while res_str: |
|||
self.logger.debug(f"开始针对f{res_str}这些节点请求测试指令") |
|||
user_Prompt = f''' |
|||
当前分支路径:{cur_node.path} |
|||
当前节点信息: |
|||
- 节点名称:{cur_node.name} |
|||
- 节点状态:{cur_node.status} |
|||
- 漏洞类型:{cur_node.vul_type} |
|||
反馈类型:需要补充以下子节点的测试指令:{res_str} |
|||
任务: |
|||
1.请生成这些子节点的测试指令,注意不要生成重复的测试指令; |
|||
2.这些节点的父节点为当前节点,请正确生成这些节点的节点路径; |
|||
3.只有当还有节点未能生成测试指令或不完整时,才返回未生成指令的节点列表。 |
|||
''' |
|||
res_str = "" |
|||
#node_cmds, commands = self.LLM.get_llm_instruction(user_Prompt, DBM, cur_node) # message要更新 |
|||
node_cmds, commands, reasoning_content, content, post_time = self.LLM.get_llm_instruction(user_Prompt, |
|||
cur_node) # message要更新 |
|||
# LLM记录存数据库 |
|||
cur_node.llm_sn += 1 |
|||
bres = DBM.insert_llm(self.task_id, user_Prompt, reasoning_content, content, post_time, cur_node.llm_sn,cur_node.path) |
|||
if not bres: |
|||
self.logger.error(f"{cur_node.name}-llm入库失败!") |
|||
#把返回的测试指令进行追加 |
|||
new_commands.extend(commands) |
|||
#判断是否还有未添加指令的节点 |
|||
for node_json in node_cmds: #正常应该只有一条no_instruction --暂时只处理 |
|||
if "no_instruction" in node_json and "nodes" in node_json: |
|||
tmp_nodes = [] |
|||
node_names = node_json["nodes"].split(',') |
|||
for node_name in node_names: |
|||
if node_name in nodes: |
|||
tmp_nodes.append(node_name) |
|||
res_str = ','.join(tmp_nodes) |
|||
break |
|||
self.logger.debug("未添加指令的节点,都已完成指令的添加!") |
|||
return new_commands |
|||
|
|||
def start_task(self,task_id,task_status=1,attack_tree=None): |
|||
self.task_id = task_id |
|||
''' |
|||
启动该测试任务 |
|||
''' |
|||
#判断目标合法性 |
|||
# bok,target,type = self.TargetM.validate_and_extract(self.target) #是否还需要判断待定? |
|||
# if not bok: |
|||
# return False, "{target}检测目标不合规,请检查!" |
|||
#初始化节点树 |
|||
if attack_tree:#有值的情况是load |
|||
self.attack_tree =attack_tree |
|||
#加载未完成的任务 |
|||
if self.work_type ==1:#自动模式 |
|||
#提交到mq,待线程执行 |
|||
self.put_work_node() |
|||
else: #无值的情况是new_create |
|||
root_node = TreeNode(self.target, self.task_id) #根节点 |
|||
self.attack_tree = AttackTree(root_node) #创建测试树,同时更新根节点相关内容 |
|||
self.LLM.build_initial_prompt(root_node) #对根节点初始化system-msg |
|||
#插入一个user消息 |
|||
# 提交第一个llm任务,开始工作 |
|||
know_info = f"本测试主机的IP地址为:{self.local_ip}" |
|||
if self.cookie: |
|||
know_info = know_info + f",本站点的cookie值为{self.cookie}" |
|||
self.put_node_reslist(root_node,know_info,0) #入待提交list |
|||
#初始保存个attack_tree文件 |
|||
g_PKM.WriteData(self.attack_tree,str(self.task_id)) |
|||
#启动工作线程 |
|||
self.task_status = task_status |
|||
self.brun = True #线程正常启动 |
|||
#启动指令工作线程 |
|||
for i in range(self.max_thread_num): |
|||
w_th = threading.Thread(target=self.do_worker_th,args=(i,)) |
|||
w_th.start() |
|||
self.workth_list[i] = w_th |
|||
#启动llm提交线程--llm暂时单线程,多线程处理时attack_tree需要加锁 |
|||
for j in range(self.llm_max_nums): |
|||
l_th = threading.Thread(target=self.th_llm_worker,args=(j,)) |
|||
l_th.start() |
|||
self.llmth_list[j]=l_th |
|||
#启动自检线程 |
|||
self.check_th = threading.Thread(target=self.th_check) |
|||
self.check_th.start() |
|||
|
|||
def stop_task(self): #还未处理 |
|||
self.brun = False |
|||
self.InstrM.init_data() |
|||
#结束任务需要收尾处理#? |
|||
|
|||
|
|||
if __name__ == "__main__": |
|||
pass |
@ -0,0 +1,49 @@ |
|||
import json |
|||
import struct |
|||
from quart import websocket |
|||
|
|||
class WebSocketManager: |
|||
def __init__(self): |
|||
self.ws_clients={} |
|||
|
|||
async def register(self, user_id, ws_proxy): |
|||
ws = ws_proxy._get_current_object() # 获取代理背后的真实对象 |
|||
self.ws_clients[user_id] = ws |
|||
|
|||
async def unregister(self, user_id=0): |
|||
if user_id in self.ws_clients: |
|||
del self.ws_clients[user_id] |
|||
print(f"{user_id}-socket退出") |
|||
|
|||
def create_binary_error_message(self,error_text, idatatype=0): |
|||
# 将错误信息包装成 JSON |
|||
body = json.dumps({"error": error_text}).encode('utf-8') |
|||
idata_len = len(body) |
|||
# 构造数据头:固定字符串 "TFTF",后面 8 字节分别为 idatatype 和 idata_len (使用大端字节序) |
|||
header = b"TFTF" + struct.pack("!II", idatatype, idata_len) |
|||
return header + body |
|||
|
|||
async def send_data(self,idatatype,data,user_id=0): |
|||
""" |
|||
发送数据格式: |
|||
- 固定 4 字节:"TFTF"(ASCII) |
|||
- 接下来 8 字节为数据头,包含两个无符号整数(每个 4 字节),分别为 idatatype 与 idata_len |
|||
- 最后 idata_len 字节为数据体(JSON字符串),数据体中包含 node_path 和 node_workstatus |
|||
""" |
|||
if user_id not in self.ws_clients: |
|||
return |
|||
ws = self.ws_clients[user_id] |
|||
|
|||
# 将 data 转换为 JSON 字符串并转换为字节 |
|||
body = json.dumps(data).encode('utf-8') |
|||
idata_len = len(body) |
|||
# 使用 struct.pack 构造数据头(大端字节序) |
|||
header = b"TFTF" + struct.pack("!II", idatatype, idata_len) |
|||
message = header + body |
|||
try: |
|||
await ws.send(message) |
|||
except Exception as e: |
|||
print(f"发送失败: {e}") |
|||
await self.unregister(user_id) # 异常时自动注销连接 |
|||
|
|||
g_WSM = WebSocketManager() |
@ -0,0 +1,53 @@ |
|||
import pickle |
|||
import threading |
|||
import os |
|||
from myutils.ConfigManager import myCongif |
|||
|
|||
class PickleManager: |
|||
def __init__(self): |
|||
self.lock = threading.Lock() # 线程锁 |
|||
self.tree_file = myCongif.get_data("TreeFile") |
|||
|
|||
def getfile_path(self,filename=""): |
|||
filepath = self.tree_file |
|||
if filename: |
|||
filepath = "tree_data/"+filename |
|||
return filepath |
|||
|
|||
def WriteData(self,attack_tree,filename=""): |
|||
filepath = self.getfile_path(filename) |
|||
with self.lock: |
|||
with open(filepath, 'wb') as f: |
|||
pickle.dump(attack_tree, f) |
|||
|
|||
def ReadData(self,filename=""): |
|||
attack_tree = None |
|||
filepath = self.getfile_path(filename) |
|||
with self.lock: |
|||
with open(filepath, "rb") as f: |
|||
attack_tree = pickle.load(f) |
|||
return attack_tree |
|||
|
|||
def DelData(self,filename=""): |
|||
filepath = self.getfile_path(filename) |
|||
#删除文件 |
|||
try: |
|||
os.remove(filepath) |
|||
return True |
|||
except FileNotFoundError: |
|||
# 文件不存在 |
|||
return True |
|||
except PermissionError: |
|||
# 没有删除权限/文件被占用 |
|||
return False |
|||
except IsADirectoryError: |
|||
# 路径指向的是目录而非文件 |
|||
return False |
|||
except Exception as e: |
|||
# 其他未知错误(如路径非法、存储介质故障等) |
|||
print(f"删除文件时发生意外错误: {str(e)}") |
|||
return False |
|||
|
|||
|
|||
|
|||
g_PKM = PickleManager() |
@ -0,0 +1,11 @@ |
|||
from tools.ToolBase import ToolBase |
|||
|
|||
class ArpingTool(ToolBase): |
|||
def validate_instruction(self, instruction): |
|||
#指令过滤 |
|||
timeout = 0 |
|||
return instruction,timeout |
|||
|
|||
def analyze_result(self, result,instruction,stderr,stdout): |
|||
#指令结果分析 |
|||
return result |
@ -0,0 +1,13 @@ |
|||
from tools.ToolBase import ToolBase |
|||
|
|||
class DirSearchTool(ToolBase): |
|||
def validate_instruction(self, instruction): |
|||
#指令过滤 |
|||
timeout = 0 |
|||
if "-o " not in instruction or "--output=" not in instruction: |
|||
instruction += " -o ds_result.txt" |
|||
return instruction,timeout |
|||
|
|||
def analyze_result(self, result,instruction,stderr,stdout): |
|||
#指令结果分析 |
|||
return result |
@ -0,0 +1,11 @@ |
|||
from tools.ToolBase import ToolBase |
|||
|
|||
class GitdumperTool(ToolBase): |
|||
def validate_instruction(self, instruction): |
|||
#指令过滤 |
|||
timeout = 60*2 |
|||
return instruction,timeout |
|||
|
|||
def analyze_result(self, result,instruction,stderr,stdout): |
|||
#指令结果分析 |
|||
return result |
@ -0,0 +1,11 @@ |
|||
from tools.ToolBase import ToolBase |
|||
|
|||
class MedusaTool(ToolBase): |
|||
def validate_instruction(self, instruction): |
|||
#指令过滤 |
|||
timeout = 0 |
|||
return instruction,timeout |
|||
|
|||
def analyze_result(self, result,instruction,stderr,stdout): |
|||
#指令结果分析 |
|||
return result |
@ -0,0 +1,76 @@ |
|||
from tools.ToolBase import ToolBase |
|||
import os |
|||
import shlex |
|||
import subprocess |
|||
import tempfile |
|||
|
|||
class MsfvenomTool(ToolBase): |
|||
def validate_instruction(self, instruction): |
|||
#指令过滤 |
|||
timeout = 0 |
|||
return instruction,timeout |
|||
|
|||
def do_worker_script(self,str_instruction,timeout,ext_params): |
|||
# 创建临时文件保存输出 |
|||
with tempfile.NamedTemporaryFile(delete=False) as tmpfile: |
|||
output_file = tmpfile.name |
|||
|
|||
# 使用 shlex.quote 对 str_instruction 进行安全包装,确保整个命令作为一个参数传递 |
|||
safe_instr = shlex.quote(str_instruction.strip()) |
|||
# 构建 script 命令 |
|||
# 注意:此时 safe_instr 包含单引号,确保整个 -c 参数不被拆分 |
|||
script_cmd = f"script -q -c {safe_instr} {output_file}" |
|||
# 选项 -q 表示静默(quiet),减少不必要的输出 |
|||
|
|||
# # 构建并执行 script 命令 |
|||
# script_cmd = f"script -c '{str_instruction}' {output_file}" |
|||
try: |
|||
if timeout ==0: |
|||
result = subprocess.run(script_cmd, shell=True, text=True) |
|||
else: |
|||
result = subprocess.run(script_cmd, shell=True, text=True,timeout=timeout) |
|||
# 读取输出文件内容 |
|||
with open(output_file, 'r') as f: |
|||
output = f.read() |
|||
lines = output.splitlines() |
|||
# 跳过第一行(Script started)和最后一行(Script done) |
|||
ftp_output = lines[1:-1] |
|||
output = '\n'.join(ftp_output) |
|||
except subprocess.TimeoutExpired: |
|||
output = "命令超时返回" |
|||
try: |
|||
with open(output_file, 'r') as f: |
|||
partial_output = f.read() |
|||
if partial_output: |
|||
output += f"\n部分输出:\n{partial_output}" |
|||
except FileNotFoundError: |
|||
pass # 文件可能未创建 |
|||
except subprocess.CalledProcessError as e: |
|||
output = f"错误: {e}" |
|||
finally: |
|||
# 删除临时文件 |
|||
try: |
|||
os.remove(output_file) |
|||
except FileNotFoundError: |
|||
pass # 文件可能未创建 |
|||
return output |
|||
|
|||
def execute_instruction(self, instruction_old): |
|||
ext_params = self.create_extparams() |
|||
# 第一步:验证指令合法性 |
|||
instruction,time_out = self.validate_instruction(instruction_old) |
|||
if not instruction: |
|||
return False, instruction_old, "该指令暂不执行!","",ext_params |
|||
# 过滤修改后的指令是否需要判重?同样指令再执行结果一致?待定---#? |
|||
|
|||
# 第二步:执行指令---需要对ftp指令进行区分判断 |
|||
output = self.do_worker_script(instruction, time_out, ext_params) |
|||
|
|||
# 第三步:分析执行结果 |
|||
analysis = self.analyze_result(output,instruction,"","") |
|||
|
|||
return True, instruction, analysis,output,ext_params |
|||
|
|||
def analyze_result(self, result,instruction,stderr,stdout): |
|||
#指令结果分析 |
|||
return result |
@ -0,0 +1,75 @@ |
|||
from tools.ToolBase import ToolBase |
|||
import re |
|||
import os |
|||
import shlex |
|||
import subprocess |
|||
import tempfile |
|||
|
|||
|
|||
class OtherTool(ToolBase): |
|||
def validate_instruction(self, instruction): |
|||
#指令过滤 |
|||
timeout = 60*2 |
|||
return instruction,timeout |
|||
|
|||
def do_worker_script(self,str_instruction,timeout,ext_params): |
|||
# 创建临时文件保存输出 |
|||
with tempfile.NamedTemporaryFile(delete=False) as tmpfile: |
|||
output_file = tmpfile.name |
|||
|
|||
# 使用 shlex.quote 对 str_instruction 进行安全包装,确保整个命令作为一个参数传递 |
|||
safe_instr = shlex.quote(str_instruction.strip()) |
|||
# 构建 script 命令 |
|||
# 注意:此时 safe_instr 包含单引号,确保整个 -c 参数不被拆分 |
|||
script_cmd = f"script -q -c {safe_instr} {output_file}" |
|||
# 选项 -q 表示静默(quiet),减少不必要的输出 |
|||
|
|||
# # 构建并执行 script 命令 |
|||
# script_cmd = f"script -c '{str_instruction}' {output_file}" |
|||
try: |
|||
result = subprocess.run(script_cmd, shell=True, text=True,timeout=timeout) |
|||
# 读取输出文件内容 |
|||
with open(output_file, 'r') as f: |
|||
output = f.read() |
|||
lines = output.splitlines() |
|||
# 跳过第一行(Script started)和最后一行(Script done) |
|||
ftp_output = lines[1:-1] |
|||
output = '\n'.join(ftp_output) |
|||
except subprocess.TimeoutExpired: |
|||
output = "命令超时返回" |
|||
try: |
|||
with open(output_file, 'r') as f: |
|||
partial_output = f.read() |
|||
if partial_output: |
|||
output += f"\n部分输出:\n{partial_output}" |
|||
except FileNotFoundError: |
|||
pass # 文件可能未创建 |
|||
except subprocess.CalledProcessError as e: |
|||
output = f"错误: {e}" |
|||
finally: |
|||
# 删除临时文件 |
|||
try: |
|||
os.remove(output_file) |
|||
except FileNotFoundError: |
|||
pass # 文件可能未创建 |
|||
return output |
|||
|
|||
def execute_instruction(self, instruction_old): |
|||
ext_params = self.create_extparams() |
|||
# 第一步:验证指令合法性 |
|||
instruction,time_out = self.validate_instruction(instruction_old) |
|||
if not instruction: |
|||
return False, instruction_old, "该指令暂不执行!","",ext_params |
|||
# 过滤修改后的指令是否需要判重?同样指令再执行结果一致?待定---#? |
|||
|
|||
# 第二步:执行指令---需要对ftp指令进行区分判断 |
|||
output = self.do_worker_script(instruction, time_out, ext_params) |
|||
|
|||
# 第三步:分析执行结果 |
|||
analysis = self.analyze_result(output,instruction,"","") |
|||
|
|||
return True, instruction, analysis,output,ext_params |
|||
|
|||
def analyze_result(self, result,instruction,stderr,stdout): |
|||
#指令结果分析 |
|||
return result |
@ -0,0 +1,11 @@ |
|||
from tools.ToolBase import ToolBase |
|||
|
|||
class PingTool(ToolBase): |
|||
def validate_instruction(self, instruction): |
|||
#指令过滤 |
|||
timeout = 0 |
|||
return instruction,timeout |
|||
|
|||
def analyze_result(self, result,instruction,stderr,stdout): |
|||
#指令结果分析 |
|||
return result |
@ -1,153 +0,0 @@ |
|||
#python代码动态执行 |
|||
import ast |
|||
import subprocess |
|||
import json |
|||
import builtins |
|||
import re |
|||
import paramiko |
|||
import impacket |
|||
import psycopg2 |
|||
import socket |
|||
import struct |
|||
import sys |
|||
import requests |
|||
import ssl |
|||
import mysql.connector |
|||
from tools.ToolBase import ToolBase |
|||
from mycode.Result_merge import my_merge |
|||
|
|||
class PythoncodeTool(ToolBase): |
|||
|
|||
def is_safe_code(self,code): |
|||
# List of high-risk functions to block (can be adjusted based on requirements) |
|||
HIGH_RISK_FUNCTIONS = ['eval', 'exec', 'os.system', 'subprocess.call', 'subprocess.Popen'] |
|||
|
|||
"""Check if the code contains high-risk function calls.""" |
|||
try: |
|||
tree = ast.parse(code) |
|||
for node in ast.walk(tree): |
|||
if isinstance(node, ast.Call): |
|||
if isinstance(node.func, ast.Name) and node.func.id in HIGH_RISK_FUNCTIONS: |
|||
return False |
|||
elif isinstance(node.func, ast.Attribute) and node.func.attr in HIGH_RISK_FUNCTIONS: |
|||
return False |
|||
return True |
|||
except SyntaxError: |
|||
return False |
|||
|
|||
def validate_instruction(self, instruction): |
|||
#指令过滤 |
|||
timeout = 0 |
|||
instr = instruction.replace("python_code ","") |
|||
instr = instruction.replace("python-code ", "") |
|||
# Safety check |
|||
if not self.is_safe_code(instr): |
|||
return "", timeout |
|||
return instr,timeout |
|||
|
|||
def safe_import(self,name,*args,**kwargs): |
|||
ALLOWED_MODULES = ['subprocess', 'json','re'] |
|||
if name not in ALLOWED_MODULES: |
|||
raise ImportError(f"Import of '{name}' is not allowed") |
|||
return builtins.__import__(name, *args, **kwargs) |
|||
|
|||
def execute_instruction(self, instruction_old): |
|||
''' |
|||
执行指令:验证合法性 -> 执行 -> 分析结果 |
|||
:param instruction_old: |
|||
:return: |
|||
bool:true-正常返回给大模型,false-结果不返回给大模型 |
|||
str:执行的指令 |
|||
str:执行指令的结果 |
|||
''' |
|||
ext_params = self.create_extparams() |
|||
|
|||
# 定义允许的内置函数集合 --白名单 |
|||
allowed_builtins = { |
|||
'__name__': __name__, |
|||
'__import__': builtins.__import__, |
|||
"abs": abs, |
|||
"all": all, |
|||
"any": any, |
|||
"bool": bool, |
|||
"chr": chr, |
|||
"dict": dict, |
|||
"float": float, |
|||
"int": int, |
|||
"len": len, |
|||
"list": list, |
|||
"max": max, |
|||
"min": min, |
|||
"print": print, |
|||
"range": range, |
|||
"set": set, |
|||
"str": str, |
|||
"sum": sum, |
|||
"type": type, |
|||
'open':open, |
|||
'Exception':Exception, |
|||
# 根据需要可以添加其他安全的内置函数 |
|||
} |
|||
# 第一步:验证指令合法性 |
|||
instruction,time_out = self.validate_instruction(instruction_old) |
|||
if not instruction: |
|||
return False, instruction_old, "该指令暂不执行!","",ext_params |
|||
# 过滤修改后的指令是否需要判重?同样指令再执行结果一致?待定---#? |
|||
|
|||
# 第二步:执行指令 |
|||
output = "" |
|||
try: |
|||
# 构造安全的全局命名空间,只包含我们允许的 __builtins__ |
|||
# 虽然动态代码中包含了import subprocess,但是还是需要在全局命名空间中添加subprocess这些库 |
|||
# 正常情况应该是不需要的,后续再研究 |
|||
safe_globals = {"__builtins__": allowed_builtins, |
|||
'subprocess':subprocess, |
|||
'json':json, |
|||
're':re, |
|||
'paramiko':paramiko, |
|||
'impacket':impacket, |
|||
'psycopg2':psycopg2, |
|||
'socket':socket, |
|||
'mysql':mysql, |
|||
'mysql.connector':mysql.connector, |
|||
'struct':struct, |
|||
'sys':sys, |
|||
'requests':requests, |
|||
'ssl':ssl} |
|||
safe_locals = {} #不需要预设局部参数 |
|||
# 在限制环境中执行代码 |
|||
exec(instruction, safe_globals,safe_locals) |
|||
# Check if check_samba_vuln is defined |
|||
if 'dynamic_fun' not in safe_locals: |
|||
analysis = "Function dynamic_fun() is not defined" |
|||
ext_params['is_use'] = True |
|||
return True,instruction,analysis,analysis,ext_params |
|||
# Get the function and call it |
|||
dynamic_fun = safe_locals['dynamic_fun'] |
|||
status, tmpout = dynamic_fun() #LLM存在status定义错误的情况(执行成功,却返回的是False) #重点要处理 |
|||
output = f"status:{status},output:{tmpout}" |
|||
except Exception as e: |
|||
analysis = f"执行动态代码时出错: {str(e)}" |
|||
ext_params['is_use'] = True |
|||
return True,instruction,analysis,analysis,ext_params |
|||
|
|||
|
|||
# 第三步:分析执行结果 |
|||
analysis = self.analyze_result(output, instruction,"","") |
|||
# 指令和结果入数据库 |
|||
# ? |
|||
if not analysis: # analysis为“” 不提交LLM |
|||
return False, instruction, analysis,"",ext_params |
|||
return True, instruction, analysis,"",ext_params |
|||
|
|||
def analyze_result(self, result,instruction,stderr,stdout): |
|||
#指令结果分析 --- 要不要限定一个max_len? |
|||
if "enum4linux " in instruction: #存在指令包装成Python代码返回的情况 |
|||
result = my_merge("enum4linux",result) |
|||
return result |
|||
|
|||
if __name__ == "__main__": |
|||
llm_code = """ |
|||
def run_test(): |
|||
return 'Penetration test executed successfully!' |
|||
""" |
@ -0,0 +1,11 @@ |
|||
from tools.ToolBase import ToolBase |
|||
|
|||
class RpcclientTool(ToolBase): |
|||
def validate_instruction(self, instruction): |
|||
#指令过滤 |
|||
timeout = 0 |
|||
return instruction,timeout |
|||
|
|||
def analyze_result(self, result,instruction,stderr,stdout): |
|||
#指令结果分析 |
|||
return result |
@ -0,0 +1,11 @@ |
|||
from tools.ToolBase import ToolBase |
|||
|
|||
class RpcinfoTool(ToolBase): |
|||
def validate_instruction(self, instruction): |
|||
#指令过滤 |
|||
timeout = 0 |
|||
return instruction,timeout |
|||
|
|||
def analyze_result(self, result,instruction,stderr,stdout): |
|||
#指令结果分析 |
|||
return result |
@ -0,0 +1,13 @@ |
|||
from tools.ToolBase import ToolBase |
|||
|
|||
class SmbmapTool(ToolBase): |
|||
def validate_instruction(self, instruction): |
|||
#指令过滤 |
|||
timeout = 0 |
|||
if " grep " not in instruction: |
|||
instruction =instruction.strip() + " | grep -E 'READ|WRITE|Disk|path'" |
|||
return instruction,timeout |
|||
|
|||
def analyze_result(self, result,instruction,stderr,stdout): |
|||
#指令结果分析 |
|||
return result |
@ -0,0 +1,77 @@ |
|||
from tools.ToolBase import ToolBase |
|||
import os |
|||
import shlex |
|||
import subprocess |
|||
import tempfile |
|||
|
|||
|
|||
class SshpassTool(ToolBase): |
|||
def validate_instruction(self, instruction): |
|||
#指令过滤 |
|||
timeout = 0 |
|||
return instruction,timeout |
|||
|
|||
def do_worker_script(self,str_instruction,timeout,ext_params): |
|||
# 创建临时文件保存输出 |
|||
with tempfile.NamedTemporaryFile(delete=False) as tmpfile: |
|||
output_file = tmpfile.name |
|||
|
|||
# 使用 shlex.quote 对 str_instruction 进行安全包装,确保整个命令作为一个参数传递 |
|||
safe_instr = shlex.quote(str_instruction.strip()) |
|||
# 构建 script 命令 |
|||
# 注意:此时 safe_instr 包含单引号,确保整个 -c 参数不被拆分 |
|||
script_cmd = f"script -q -c {safe_instr} {output_file}" |
|||
# 选项 -q 表示静默(quiet),减少不必要的输出 |
|||
|
|||
# # 构建并执行 script 命令 |
|||
# script_cmd = f"script -c '{str_instruction}' {output_file}" |
|||
try: |
|||
if timeout ==0: |
|||
result = subprocess.run(script_cmd, shell=True, text=True) |
|||
else: |
|||
result = subprocess.run(script_cmd, shell=True, text=True,timeout=timeout) |
|||
# 读取输出文件内容 |
|||
with open(output_file, 'r') as f: |
|||
output = f.read() |
|||
lines = output.splitlines() |
|||
# 跳过第一行(Script started)和最后一行(Script done) |
|||
ftp_output = lines[1:-1] |
|||
output = '\n'.join(ftp_output) |
|||
except subprocess.TimeoutExpired: |
|||
output = "命令超时返回" |
|||
try: |
|||
with open(output_file, 'r') as f: |
|||
partial_output = f.read() |
|||
if partial_output: |
|||
output += f"\n部分输出:\n{partial_output}" |
|||
except FileNotFoundError: |
|||
pass # 文件可能未创建 |
|||
except subprocess.CalledProcessError as e: |
|||
output = f"错误: {e}" |
|||
finally: |
|||
# 删除临时文件 |
|||
try: |
|||
os.remove(output_file) |
|||
except FileNotFoundError: |
|||
pass # 文件可能未创建 |
|||
return output |
|||
|
|||
def execute_instruction(self, instruction_old): |
|||
ext_params = self.create_extparams() |
|||
# 第一步:验证指令合法性 |
|||
instruction,time_out = self.validate_instruction(instruction_old) |
|||
if not instruction: |
|||
return False, instruction_old, "该指令暂不执行!","",ext_params |
|||
# 过滤修改后的指令是否需要判重?同样指令再执行结果一致?待定---#? |
|||
|
|||
# 第二步:执行指令---需要对ftp指令进行区分判断 |
|||
output = self.do_worker_script(instruction, time_out, ext_params) |
|||
|
|||
# 第三步:分析执行结果 |
|||
analysis = self.analyze_result(output,instruction,"","") |
|||
|
|||
return True, instruction, analysis,output,ext_params |
|||
|
|||
def analyze_result(self, result,instruction,stderr,stdout): |
|||
#指令结果分析 |
|||
return result |
@ -0,0 +1,73 @@ |
|||
from tools.ToolBase import ToolBase |
|||
import os |
|||
import shlex |
|||
import subprocess |
|||
import tempfile |
|||
|
|||
class TcpdumpTool(ToolBase): |
|||
def validate_instruction(self, instruction): |
|||
#指令过滤 |
|||
timeout = 60*2 |
|||
return instruction,timeout |
|||
|
|||
def do_worker_script(self,str_instruction,timeout,ext_params): |
|||
# 创建临时文件保存输出 |
|||
with tempfile.NamedTemporaryFile(delete=False) as tmpfile: |
|||
output_file = tmpfile.name |
|||
|
|||
# 使用 shlex.quote 对 str_instruction 进行安全包装,确保整个命令作为一个参数传递 |
|||
safe_instr = shlex.quote(str_instruction.strip()) |
|||
# 构建 script 命令 |
|||
# 注意:此时 safe_instr 包含单引号,确保整个 -c 参数不被拆分 |
|||
script_cmd = f"script -q -c {safe_instr} {output_file}" |
|||
# 选项 -q 表示静默(quiet),减少不必要的输出 |
|||
|
|||
# # 构建并执行 script 命令 |
|||
# script_cmd = f"script -c '{str_instruction}' {output_file}" |
|||
try: |
|||
result = subprocess.run(script_cmd, shell=True, text=True,timeout=timeout) |
|||
# 读取输出文件内容 |
|||
with open(output_file, 'r') as f: |
|||
output = f.read() |
|||
lines = output.splitlines() |
|||
# 跳过第一行(Script started)和最后一行(Script done) |
|||
ftp_output = lines[1:-1] |
|||
output = '\n'.join(ftp_output) |
|||
except subprocess.TimeoutExpired: |
|||
output = "命令超时返回" |
|||
try: |
|||
with open(output_file, 'r') as f: |
|||
partial_output = f.read() |
|||
if partial_output: |
|||
output += f"\n部分输出:\n{partial_output}" |
|||
except FileNotFoundError: |
|||
pass # 文件可能未创建 |
|||
except subprocess.CalledProcessError as e: |
|||
output = f"错误: {e}" |
|||
finally: |
|||
# 删除临时文件 |
|||
try: |
|||
os.remove(output_file) |
|||
except FileNotFoundError: |
|||
pass # 文件可能未创建 |
|||
return output |
|||
|
|||
def execute_instruction1(self, instruction_old): |
|||
ext_params = self.create_extparams() |
|||
# 第一步:验证指令合法性 |
|||
instruction,time_out = self.validate_instruction(instruction_old) |
|||
if not instruction: |
|||
return False, instruction_old, "该指令暂不执行!","",ext_params |
|||
# 过滤修改后的指令是否需要判重?同样指令再执行结果一致?待定---#? |
|||
|
|||
# 第二步:执行指令---需要对ftp指令进行区分判断 |
|||
output = self.do_worker_script(instruction, time_out, ext_params) |
|||
|
|||
# 第三步:分析执行结果 |
|||
analysis = self.analyze_result(output,instruction,"","") |
|||
|
|||
return True, instruction, analysis,output,ext_params |
|||
|
|||
def analyze_result(self, result,instruction,stderr,stdout): |
|||
#指令结果分析 |
|||
return result |
@ -0,0 +1,11 @@ |
|||
from tools.ToolBase import ToolBase |
|||
|
|||
class XvfbrunTool(ToolBase): |
|||
def validate_instruction(self, instruction): |
|||
#指令过滤 |
|||
timeout = 0 |
|||
return instruction,timeout |
|||
|
|||
def analyze_result(self, result,instruction,stderr,stdout): |
|||
#指令结果分析 |
|||
return result |
@ -1,4 +1,4 @@ |
|||
from quart import Blueprint |
|||
#定义模块 |
|||
api = Blueprint('api',__name__) |
|||
from . import user |
|||
from . import user,task,wsm,system |
|||
|
@ -0,0 +1,23 @@ |
|||
from . import api |
|||
from mycode.DBManager import app_DBM |
|||
from myutils.ReManager import mReM |
|||
from quart import Quart, render_template, redirect, url_for, request,jsonify |
|||
|
|||
@api.route('/system/getinfo',methods=['GET']) |
|||
async def get_system_info(): |
|||
data = app_DBM.getsystem_info() |
|||
return jsonify({"local_ip":data[0],"version":data[1]}) |
|||
|
|||
@api.route('/system/updateip',methods=['POST']) |
|||
async def update_local_ip(): |
|||
data = await request.get_json() |
|||
local_ip = data.get("local_ip") |
|||
if mReM.is_valid_ip(local_ip): |
|||
bsuccess = app_DBM.update_localip(local_ip) |
|||
error = "" |
|||
if not bsuccess: |
|||
error = "修改IP地址失败!" |
|||
else: |
|||
bsuccess = False |
|||
error = "IP不合法" |
|||
return jsonify({"bsuccess":bsuccess,"error":error}) |
@ -0,0 +1,245 @@ |
|||
from . import api |
|||
from quart import Quart, render_template, redirect, url_for, request,jsonify |
|||
from mycode.TargetManager import g_TM |
|||
from mycode.DBManager import app_DBM |
|||
from mycode.TaskManager import g_TaskM |
|||
|
|||
|
|||
def is_valid_target(test_target: str) -> bool: |
|||
""" |
|||
验证 test_target 的逻辑(这里用简单示例代替已有逻辑) |
|||
例如:测试目标不能为空且长度大于3 |
|||
""" |
|||
if test_target: |
|||
return True |
|||
return False |
|||
|
|||
@api.route('/task/start',methods=['POST']) |
|||
async def start_task(): #开始任务 |
|||
data = await request.get_json() |
|||
test_target = data.get("testTarget") |
|||
cookie_info = data.get("cookieInfo") |
|||
llm_type = data.get("curmodel") # //0-腾讯云,1-DS,2-2233.ai,3-GPT 目前只有1-2,2025-4-4 |
|||
work_type = data.get("workType") #0-人工,1-自动 |
|||
#新增任务处理 |
|||
bok,_,_ = g_TM.validate_and_extract(test_target) |
|||
if not bok: |
|||
# 返回错误信息,状态码 400 表示请求错误 |
|||
return jsonify({"error": "测试目标验证失败,请检查输入内容!"}), 400 |
|||
#开始任务 |
|||
try: |
|||
b_success = g_TaskM.create_task(test_target,cookie_info,llm_type,work_type) |
|||
#再启动 |
|||
if not b_success: |
|||
return jsonify({"error": "检测任务创建失败,请联系管理员!"}), 500 |
|||
except: |
|||
return jsonify({"error": "该目标已经在测试中,请检查!"}), 400 |
|||
#跳转到任务管理页面 |
|||
return redirect(url_for('main.get_html', html='task_manager.html')) |
|||
|
|||
@api.route('/task/taskover',methods=['POST']) |
|||
async def over_task(): |
|||
data = await request.get_json() |
|||
task_id = data.get("cur_task_id") |
|||
if not task_id: |
|||
return jsonify({'error': 'Missing task_id'}), 400 |
|||
bsuccess,error = g_TaskM.over_task(task_id) |
|||
return jsonify({"bsuccess": bsuccess, "error": error}) |
|||
|
|||
@api.route('/task/deltask',methods=['POST']) |
|||
async def del_task(): |
|||
data = await request.get_json() |
|||
task_id = data.get("task_id") |
|||
if not task_id: |
|||
return jsonify({'error': 'Missing task_id'}), 400 |
|||
bsuccess,error = g_TaskM.del_task(task_id) |
|||
return jsonify({"bsuccess": bsuccess, "error": error}) |
|||
|
|||
|
|||
@api.route('/task/getlist',methods=['GET']) |
|||
async def get_task_list(): |
|||
#task_list = app_DBM.get_task_list() #从内存取--2025-4-6 |
|||
task_list = g_TaskM.get_task_list() |
|||
if task_list: |
|||
return jsonify(task_list) |
|||
else: |
|||
return jsonify({"error":"查询任务数据出错!"}),500 |
|||
|
|||
@api.route('/task/getinstr',methods=['POST']) |
|||
async def get_instr(): |
|||
data = await request.get_json() |
|||
task_id = data.get("cur_task_id") |
|||
node_name = data.get("nodeName") |
|||
if not task_id: |
|||
return jsonify({'error': 'Missing task_id'}), 400 |
|||
instrs = app_DBM.get_task_instrs(task_id,node_name) |
|||
return jsonify({"instrs":instrs}) |
|||
|
|||
@api.route('/task/getvul',methods=['POST']) |
|||
async def get_vul(): |
|||
data = await request.get_json() |
|||
task_id = data.get("cur_task_id") |
|||
node_name = data.get("nodeName") |
|||
vul_type = data.get("vulType") |
|||
vul_level = data.get("vulLevel") |
|||
if not task_id: |
|||
return jsonify({'error': 'Missing task_id'}), 400 |
|||
vuls = app_DBM.get_task_vul(task_id,node_name,vul_type,vul_level) |
|||
return jsonify({"vuls":vuls}) |
|||
|
|||
@api.route('/task/gettree',methods=['POST']) |
|||
async def get_tree(): |
|||
data = await request.get_json() |
|||
task_id = data.get("task_id") |
|||
if not task_id: |
|||
return jsonify({'error': 'Missing task_id'}), 400 |
|||
tree_dict = g_TaskM.get_node_tree(task_id) |
|||
return jsonify({"tree":tree_dict}) |
|||
|
|||
@api.route('/task/gethistree',methods=['POST']) |
|||
async def get_his_tree(): |
|||
data = await request.get_json() |
|||
task_id = data.get("task_id") |
|||
if not task_id: |
|||
return jsonify({'error': 'Missing task_id'}), 400 |
|||
tree_dict = g_TaskM.get_his_node_tree(task_id) |
|||
return jsonify({"tree":tree_dict}) |
|||
|
|||
@api.route('/task/taskcontrol',methods=['POST']) |
|||
async def task_status_control(): |
|||
'''控制任务状态 |
|||
1.对于执行时间长的指令,如何处理?强制停止的话,要有个执行中指令的缓存,强制停止该指令返回到待执行,执行完成,该指令到执行完成; |
|||
''' |
|||
data = await request.get_json() |
|||
task_id = data.get("cur_task_id") |
|||
if not task_id: |
|||
return jsonify({'error': 'Missing task_id'}), 400 |
|||
#只做暂停和继续间的切换,以服务器端的状态为准 |
|||
bsuccess,strerror,new_task_status = g_TaskM.control_taks(task_id) |
|||
if bsuccess: |
|||
return jsonify({'newstatus':new_task_status}) |
|||
return jsonify({'error': strerror}), 400 |
|||
|
|||
@api.route('/task/taskstep',methods=['POST']) |
|||
async def task_one_step(): |
|||
'''单步推进任务--也就是待处理node 返回bsuccess,error |
|||
1.执行单步的前提条件是,工作线程都要在工作; |
|||
2.遍历节点把需要处理的节点进入待处理queue,instr和llm只能一个有数据(强制约束) |
|||
''' |
|||
data = await request.get_json() |
|||
task_id = data.get("cur_task_id") |
|||
if not task_id: |
|||
return jsonify({'error': 'Missing task_id'}), 400 |
|||
bsuccess,error = await g_TaskM.task_one_step(task_id) |
|||
return jsonify({"bsuccess":bsuccess,"error":error}) |
|||
|
|||
@api.route('/task/nodestep',methods=['POST']) |
|||
async def node_one_step(): |
|||
data = await request.get_json() |
|||
task_id = data.get("task_id") |
|||
node_path = data.get("node_path") |
|||
if not task_id: |
|||
return jsonify({'error': 'Missing task_id'}), 400 |
|||
bsuccess,error = await g_TaskM.node_one_step(task_id,node_path) |
|||
return jsonify({"bsuccess":bsuccess,"error":error}) |
|||
|
|||
@api.route('/task/taskworktype',methods=['POST']) |
|||
async def task_work_type_control(): |
|||
data = await request.get_json() |
|||
task_id = data.get("cur_task_id") |
|||
newwork_type = data.get("mode") |
|||
if not task_id: |
|||
return jsonify({'error': 'Missing task_id or newwork_type'}), 400 |
|||
bsuccess = g_TaskM.update_task_work_type(task_id,newwork_type) |
|||
return jsonify({"bsuccess": bsuccess}) |
|||
|
|||
@api.route('/task/nodecontrol',methods=['POST']) |
|||
async def node_work_status_control(): |
|||
data = await request.get_json() |
|||
task_id = data.get("task_id") |
|||
nodepath = data.get("node_path") |
|||
if not task_id or not nodepath: |
|||
return jsonify({'error': 'Missing task_id or node_path'}), 400 |
|||
#修改节点的工作状态 |
|||
bsuccess,newbwork = g_TaskM.node_bwork_control(task_id,nodepath) |
|||
if not bsuccess: |
|||
return jsonify({'error': 'node_path not bfind'}), 400 |
|||
return jsonify({"newbwork":newbwork}) |
|||
|
|||
@api.route('/task/nodegetinstr',methods=['POST']) |
|||
async def node_get_instr(): |
|||
data = await request.get_json() |
|||
task_id = data.get("task_id") |
|||
nodepath = data.get("node_path") |
|||
if not task_id: |
|||
return jsonify({'error': 'Missing task_id'}), 400 |
|||
#返回 { doneInstrs: [...], todoInstrs: [...] } |
|||
doneInstrs = app_DBM.get_task_node_done_instr(task_id,nodepath) |
|||
todoInstrs = g_TaskM.get_task_node_todo_instr(task_id,nodepath) |
|||
return jsonify({"doneInstrs":doneInstrs,"todoInstrs":todoInstrs}) |
|||
|
|||
@api.route('/task/hisnodegetinstr',methods=['POST']) |
|||
async def his_node_get_instr(): |
|||
data = await request.get_json() |
|||
task_id = data.get("task_id") |
|||
nodepath = data.get("node_path") |
|||
if not task_id: |
|||
return jsonify({'error': 'Missing task_id'}), 400 |
|||
#返回 { doneInstrs: [...], todoInstrs: [...] } |
|||
doneInstrs = app_DBM.get_task_node_done_instr(task_id,nodepath) |
|||
#todoInstrs = g_TaskM.get_task_node_todo_instr(task_id,nodepath) |
|||
return jsonify({"doneInstrs":doneInstrs}) |
|||
|
|||
@api.route('/task/nodegetmsg',methods=['POST']) |
|||
async def node_get_msg(): |
|||
data = await request.get_json() |
|||
task_id = data.get("task_id") |
|||
nodepath = data.get("node_path") |
|||
if not task_id: |
|||
return jsonify({'error': 'Missing task_id'}), 400 |
|||
submitted,pending = g_TaskM.get_task_node_MSG(task_id,nodepath) |
|||
return jsonify({"submitted": submitted, "pending": pending}) |
|||
|
|||
@api.route('/task/nodeupdatemsg',methods=['POST']) |
|||
async def node_update_msg(): |
|||
data = await request.get_json() |
|||
task_id = data.get("task_id") |
|||
nodepath = data.get("node_path") |
|||
newllm_type = data.get("llmtype") |
|||
newcontent = data.get("content") |
|||
if not task_id: |
|||
return jsonify({'error': 'Missing task_id'}), 400 |
|||
bsuccess,error =g_TaskM.update_node_MSG(task_id,nodepath,newllm_type,newcontent) |
|||
return jsonify({"bsuccess":bsuccess,"error":error}) |
|||
|
|||
@api.route('/task/delnodeinstr',methods=['POST']) |
|||
async def node_del_instr(): |
|||
data = await request.get_json() |
|||
task_id = data.get("task_id") |
|||
nodepath = data.get("node_path") |
|||
instr = data.get("item") |
|||
if not task_id: |
|||
return jsonify({'error': 'Missing task_id'}), 400 |
|||
bsuccess,error = g_TaskM.del_node_instr(task_id,nodepath,instr) |
|||
return jsonify({"bsuccess": bsuccess, "error": error}) |
|||
|
|||
|
|||
@api.route('/task/histasks',methods=['POST']) |
|||
async def get_his_task(): |
|||
data = await request.get_json() |
|||
target_name = data.get("target_name") |
|||
safe_rank = data.get("safe_rank") |
|||
llm_type = data.get("llm_type") |
|||
start_time= data.get("start_time") |
|||
end_time= data.get("end_time") |
|||
his_tasks = g_TaskM.get_his_tasks(target_name,safe_rank,llm_type,start_time,end_time) |
|||
return jsonify({"his_tasks":his_tasks}) |
|||
|
|||
|
|||
|
|||
|
|||
|
|||
|
|||
|
|||
|
|||
|
@ -0,0 +1,43 @@ |
|||
import json |
|||
from . import api |
|||
from quart import Quart, websocket, jsonify |
|||
from mycode.WebSocketManager import g_WSM |
|||
|
|||
# WebSocket 路由,端口默认与 HTTP 同端口,例如 5000(开发时) |
|||
@api.websocket("/ws") |
|||
async def ws(): |
|||
""" |
|||
WebSocket 连接入口: |
|||
1. 客户端连接成功后,首先应发送登录数据包,例如 {"user_id": 1} |
|||
2. 后端解析登录数据包,将 user_id 与 websocket 绑定(注册) |
|||
3. 后续进入消息接收循环,根据数据协议(TFTF+头+体格式)处理数据 |
|||
""" |
|||
# 接收登录数据包(假设为纯 JSON 包,非二进制格式) |
|||
login_msg = await websocket.receive() |
|||
try: |
|||
login_data = json.loads(login_msg) |
|||
user_id = login_data.get("user_id") |
|||
if user_id is None: |
|||
error_msg = g_WSM.create_binary_error_message("没有提供user_id") |
|||
await websocket.send(error_msg) |
|||
return |
|||
# 注册当前连接 |
|||
await g_WSM.register(user_id, websocket) |
|||
except Exception as e: |
|||
errof_msg = g_WSM.create_binary_error_message(str(e)) |
|||
await websocket.send(errof_msg) |
|||
return |
|||
|
|||
try: |
|||
# 主循环:接收后续消息并处理;此处可以根据协议解析消息 ---暂时无web发送数据的业务 |
|||
while True: |
|||
message = await websocket.receive() |
|||
# 这里可以添加对接收到的消息解析处理,根据协议分离 TFTF 等 |
|||
print("收到消息:", message) |
|||
# 例如,直接回显消息;实际项目中应解析消息格式并调用相应的业务逻辑 |
|||
# await websocket.send(message) |
|||
except Exception as e: |
|||
print("WebSocket 出现异常:", e) |
|||
finally: |
|||
# 注销当前连接 |
|||
await g_WSM.unregister(user_id) |
@ -0,0 +1,134 @@ |
|||
/* 左侧树容器:固定高度,允许滚动,根节点居中 */ |
|||
.node-tree-area { |
|||
width: 100%; |
|||
height: 100%; /* 示例高度,根据需求调整 */ |
|||
border: 1px solid #ddd; |
|||
overflow: hidden; /* 超出时出现滚动条 */ |
|||
background-color: #f8f9fa; |
|||
text-align: center; /* 内部 inline-block 居中 */ |
|||
position: relative; |
|||
} |
|||
|
|||
/* 树节点内容区域,不包含刷新按钮 */ |
|||
.tree-content { |
|||
height: 100%; |
|||
overflow: auto; |
|||
padding-top: 5px; /* 留出顶部刷新按钮位置 */ |
|||
} |
|||
|
|||
/* 顶部刷新按钮 */ |
|||
.refresh-container { |
|||
position: absolute; |
|||
top: 5px; |
|||
left: 5px; |
|||
z-index: 100; |
|||
} |
|||
|
|||
.tree-refresh { |
|||
position: absolute; |
|||
top: 5px; |
|||
left: 5px; |
|||
z-index: 100; |
|||
cursor: pointer; |
|||
border: none; |
|||
background-color: #007bff; /* 蓝色背景 */ |
|||
color: #fff; /* 白色文字 */ |
|||
font-size: 20px; |
|||
padding: 5px 10px; |
|||
border-radius: 4px; |
|||
} |
|||
.tree-refresh:hover { |
|||
background-color: #0056b3; /* 悬停时深蓝 */ |
|||
} |
|||
|
|||
/* 让树的 ul 使用 inline-block 显示,便于根节点居中 */ |
|||
.tree-root-ul { |
|||
display: inline-block; |
|||
text-align: left; /* 子节点左对齐 */ |
|||
margin: 20px; |
|||
padding:0; |
|||
} |
|||
.tree-root-ul ul { |
|||
margin-left: 20px; /* 子节点缩进 */ |
|||
padding-left: 20px; |
|||
border-left: 1px dashed #ccc; /* 增加分隔线效果 */ |
|||
} |
|||
/* 节点容器:增加立体效果 */ |
|||
.node-container { |
|||
display: inline-flex; |
|||
align-items: center; |
|||
} |
|||
/* 切换图标 */ |
|||
.toggle-icon { |
|||
margin-right: 5px; |
|||
font-weight: bold; |
|||
cursor: pointer; |
|||
} |
|||
/* 每个节点样式 */ |
|||
.tree-node { |
|||
display: inline-block; |
|||
padding: 6px 10px; |
|||
margin: 3px 0; |
|||
border: 1px solid transparent; |
|||
border-radius: 4px; |
|||
background-color: #fff; |
|||
cursor: pointer; |
|||
transition: all 0.2s ease; |
|||
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.15); |
|||
} |
|||
.tree-node:hover { |
|||
background-color: #e6f7ff; |
|||
transform: translateY(-1px); |
|||
} |
|||
.tree-node.selected { |
|||
border-color: #1890ff; |
|||
background-color: #bae7ff; |
|||
} |
|||
/* 针对漏洞级别的样式 */ |
|||
.tree-node.vul-low { |
|||
border-color: #87d068; |
|||
background-color: #f6ffed; |
|||
} |
|||
.tree-node.vul-medium { |
|||
border-color: #faad14; |
|||
background-color: #fff7e6; |
|||
} |
|||
.tree-node.vul-high { |
|||
border-color: #ff4d4f; |
|||
background-color: #fff1f0; |
|||
} |
|||
.tree-node.no-work { |
|||
border-color: #151515; |
|||
background-color: #c4c4c4; |
|||
} |
|||
/* 收缩/展开时隐藏子节点 ul */ |
|||
.collapsed { |
|||
display: none; |
|||
} |
|||
/* 右侧信息区域 */ |
|||
.node-info-area { |
|||
border: 1px solid #ddd; |
|||
padding: 10px; |
|||
min-height: 200px; |
|||
} |
|||
/* 按钮区域 */ |
|||
.node-actions button { |
|||
margin-right: 10px; |
|||
margin-bottom: 10px; |
|||
} |
|||
|
|||
/* 限制模态框对话框的最大高度(比如距离屏幕上下各留 20px) */ |
|||
.modal-dialog { |
|||
max-height: calc(100vh - 60px); |
|||
/* 如果模态框尺寸需要自适应宽度,可以考虑设置 width: auto; */ |
|||
} |
|||
|
|||
/* 限制模态框内容区域的最大高度并启用垂直滚动 */ |
|||
.modal-content { |
|||
max-height: calc(100vh - 60px); |
|||
} |
|||
|
|||
/* 模态框主体区域启用滚动 */ |
|||
.modal-body { |
|||
overflow-y: auto; |
|||
} |
@ -1,563 +0,0 @@ |
|||
let video_list = {}; //element_id -- socket
|
|||
let run_list = {}; //element_id -- runtag
|
|||
let berror_state_list = {}; //element_id -- 错误信息显示
|
|||
let m_count = 0; |
|||
let connection_version = {}; // 保存每个 element_id 的版本号
|
|||
let channel_list = null; |
|||
const fourViewButton = document.getElementById('fourView'); |
|||
const nineViewButton = document.getElementById('nineView'); |
|||
|
|||
//页面即将卸载时执行
|
|||
window.addEventListener('beforeunload', function (event) { |
|||
// 关闭所有 WebSocket 连接或执行其他清理操作
|
|||
for(let key in video_list){ |
|||
const videoFrame = document.getElementById(`video-${key}`); |
|||
const event = new Event('closeVideo'); |
|||
videoFrame.dispatchEvent(event); |
|||
|
|||
delete video_list[key]; |
|||
berror_state_list[key] = false; |
|||
} |
|||
}); |
|||
|
|||
//页面加载时执行
|
|||
document.addEventListener('DOMContentLoaded', async function() { |
|||
console.log('DOM fully loaded and parsed'); |
|||
// 发送请求获取额外数据 --- 这个接口功能有点大了---暂时只是更新通道树2024-7-29
|
|||
try { |
|||
let response = await fetch('/api/channel/list'); |
|||
if (!response.ok) { |
|||
throw new Error('Network response was not ok'); |
|||
} |
|||
channel_list = await response.json(); |
|||
// 遍历输出每个元素的信息
|
|||
let area_name = "" |
|||
let html = '<ul class="list-group">'; |
|||
channel_list.forEach(channel => { |
|||
// console.log(`Area Name: ${channel.area_name}`);
|
|||
// console.log(`ID: ${channel.ID}`);
|
|||
// console.log(`Channel Name: ${channel.channel_name}`);
|
|||
// console.log(`URL: ${channel.url}`);
|
|||
// console.log(`Type: ${channel.type}`);
|
|||
// console.log(`Status: ${channel.status}`);
|
|||
// console.log(`Element ID: ${channel.element_id}`);
|
|||
if(area_name !== `${channel.area_name}`){ |
|||
if(area_name !== ""){ |
|||
html += '</ul>'; |
|||
html += '</li>'; |
|||
} |
|||
area_name = `${channel.area_name}`; |
|||
html += `<li class="list-group-item"><strong>${area_name}</strong>`; |
|||
html += '<ul class="list-group">'; |
|||
} |
|||
//html += `<li class="list-group-item">${channel.channel_name}</li>`;
|
|||
html += `<li class="list-group-item" draggable="true" ondragstart="drag(event)"
|
|||
data-node-id="${channel.ID}" data-node-name="${area_name}--${channel.channel_name}"> |
|||
<svg class="bi" width="16" height="16"><use xlink:href="#view"/></svg> |
|||
${channel.channel_name} |
|||
</li>`; |
|||
}); |
|||
if(area_name !== ""){ |
|||
html += '</ul>'; |
|||
html += '</li>'; |
|||
} |
|||
html += '</ul>'; |
|||
const treeView = document.getElementById('treeView'); |
|||
treeView.innerHTML = html |
|||
generateVideoNodes(4); |
|||
} catch (error) { |
|||
console.error('Failed to fetch data:', error); |
|||
} |
|||
}); |
|||
|
|||
document.addEventListener('click', function() { |
|||
console.log("第一次页面点击,开始显示视频--已注释",m_count); |
|||
// if(m_count != 0){
|
|||
// count = m_count
|
|||
// //获取视频接口
|
|||
// const url = `/api/viewlist?count=${count}`;
|
|||
// fetch(url)
|
|||
// .then(response => response.json())
|
|||
// .then(data => {
|
|||
// console.log('Success:', data);
|
|||
// clist = data.clist;
|
|||
// elist = data.elist;
|
|||
// nlist = data.nlist;
|
|||
// for(let i=0;i<clist.length;i++){
|
|||
// if(parseInt(elist[i]) < count){
|
|||
// console.log("切换窗口时进行连接",clist[i])
|
|||
// connectToStream(elist[i],clist[i],nlist[i])
|
|||
// //startFLVStream(elist[i],clist[i],nlist[i]);
|
|||
// }
|
|||
// }
|
|||
// })
|
|||
// .catch(error => {
|
|||
// console.error('Error:', error);
|
|||
// });
|
|||
// }
|
|||
}, { once: true }); |
|||
|
|||
//视频窗口
|
|||
document.getElementById('fourView').addEventListener('click', function() { |
|||
if (fourViewButton.classList.contains('btn-primary')) { |
|||
return; // 如果按钮已经是选中状态,直接返回
|
|||
} |
|||
const videoGrid = document.getElementById('videoGrid'); |
|||
videoGrid.classList.remove('nine'); |
|||
videoGrid.classList.add('four'); |
|||
generateVideoNodes(4); |
|||
//更新按钮点击状态
|
|||
fourViewButton.classList.remove('btn-secondary'); |
|||
fourViewButton.classList.add('btn-primary'); |
|||
nineViewButton.classList.remove('btn-primary'); |
|||
nineViewButton.classList.add('btn-secondary'); |
|||
}); |
|||
|
|||
document.getElementById('nineView').addEventListener('click', function() { |
|||
if (nineViewButton.classList.contains('btn-primary')) { |
|||
return; // 如果按钮已经是选中状态,直接返回
|
|||
} |
|||
const videoGrid = document.getElementById('videoGrid'); |
|||
videoGrid.classList.remove('four'); |
|||
videoGrid.classList.add('nine'); |
|||
generateVideoNodes(9); |
|||
//更新按钮点击状态
|
|||
fourViewButton.classList.remove('btn-primary'); |
|||
fourViewButton.classList.add('btn-secondary'); |
|||
nineViewButton.classList.remove('btn-secondary'); |
|||
nineViewButton.classList.add('btn-primary'); |
|||
}); |
|||
|
|||
function generateVideoNodes(count) { //在这里显示视频-初始化 ---这里使用重置逻辑
|
|||
//结束在播放的socket
|
|||
for(let key in video_list){ |
|||
//flv使用
|
|||
// const videoFrame = document.getElementById(`video-${key}`);
|
|||
// const event = new Event('closeVideo');
|
|||
// videoFrame.dispatchEvent(event);
|
|||
|
|||
//通用关闭
|
|||
run_list[key] = false; |
|||
video_list[key].close(); |
|||
berror_state_list[key] = false; |
|||
delete video_list[key]; |
|||
} |
|||
//切换窗口布局
|
|||
const videoGrid = document.getElementById('videoGrid'); |
|||
let html = ''; |
|||
for (let i = 0; i < count; i++) { |
|||
let frameWidth = count === 4 ? 'calc(50% - 10px)' : 'calc(33.33% - 10px)'; |
|||
html += ` |
|||
<div class="video-frame" data-frame-id="${i}" style="width: ${frameWidth};" |
|||
ondrop="drop(event)" ondragover="allowDrop(event)"> |
|||
<div class="video-header"> |
|||
<div class="video-title">Video Stream ${i+1}</div> |
|||
<div class="video-buttons"> |
|||
<button onclick="toggleFullScreen(${i})">🔲</button> |
|||
<button onclick="closeVideo(${i})">❌</button> |
|||
</div> |
|||
</div> |
|||
<div class="video-area"><canvas id="video-${i}"></canvas></div> |
|||
</div>`; |
|||
} |
|||
videoGrid.innerHTML = html; |
|||
|
|||
//开始还原视频,获取视频接口
|
|||
// if(m_count != 0){
|
|||
const url = `/api/viewlist?count=${count}`; |
|||
fetch(url) |
|||
.then(response => response.json()) |
|||
.then(data => { |
|||
console.log('Success:', data); |
|||
clist = data.clist; |
|||
elist = data.elist; |
|||
nlist = data.nlist; |
|||
for(let i=0;i<clist.length;i++){ |
|||
if(parseInt(elist[i]) < count){ |
|||
connectToStream(elist[i],clist[i],nlist[i]) |
|||
//startFLVStream(elist[i],clist[i],nlist[i]);
|
|||
} |
|||
} |
|||
}) |
|||
.catch(error => { |
|||
console.error('Error:', error); |
|||
}); |
|||
// }
|
|||
//m_count = count
|
|||
} |
|||
|
|||
function toggleFullScreen(id) { |
|||
console.log('toggleFullScreen'); |
|||
const videoFrame = document.querySelector(`[data-frame-id="${id}"]`); |
|||
if (!document.fullscreenElement) { |
|||
videoFrame.requestFullscreen().catch(err => { |
|||
alert(`Error attempting to enable full-screen mode: ${err.message} (${err.name})`); |
|||
}); |
|||
} else { |
|||
document.exitFullscreen(); |
|||
}; |
|||
} |
|||
|
|||
function allowDrop(event) { |
|||
event.preventDefault(); |
|||
} |
|||
|
|||
function drag(event) { |
|||
event.dataTransfer.setData("text", event.target.dataset.nodeId); |
|||
event.dataTransfer.setData("name", event.target.dataset.nodeName); |
|||
} |
|||
|
|||
function drop(event) { |
|||
event.preventDefault(); |
|||
const nodeId = event.dataTransfer.getData("text"); |
|||
const nodeName = event.dataTransfer.getData("name"); |
|||
const frameId = event.currentTarget.dataset.frameId; |
|||
|
|||
//需要判断下当前窗口是否已经在播放视频
|
|||
const imgElement = document.getElementById(`video-${frameId}`); |
|||
const titleElement = document.querySelector(`[data-frame-id="${frameId}"] .video-title`); |
|||
|
|||
if (titleElement.textContent !== `Video Stream ${Number(frameId)+1}`) { |
|||
showModal('请先关闭当前窗口视频,然后再播放新的视频。'); |
|||
return; |
|||
}; |
|||
//发送视频链接接口
|
|||
const url = '/api/start_stream'; |
|||
const data = {"channel_id":nodeId,"element_id":frameId}; |
|||
// 发送 POST 请求
|
|||
fetch(url, { |
|||
method: 'POST', // 指定请求方法为 POST
|
|||
headers: { |
|||
'Content-Type': 'application/json' // 设置请求头,告诉服务器请求体的数据类型为 JSON
|
|||
}, |
|||
body: JSON.stringify(data) // 将 JavaScript 对象转换为 JSON 字符串
|
|||
}) |
|||
.then(response => response.json()) // 将响应解析为 JSON
|
|||
.then(data => { |
|||
const istatus = data.status; |
|||
if(istatus === 0){ |
|||
showModal(data.msg); // 使用 Modal 显示消息
|
|||
return; |
|||
} |
|||
else{ |
|||
//获取视频流
|
|||
console.log("drop触发") |
|||
connectToStream(frameId,nodeId,nodeName); |
|||
//startFLVStream(frameId,nodeId,nodeName); #基于FLV的开发程度:后端直接用RTSP流转发是有画面的,但CPU占用太高,用不了。2024-8-30
|
|||
} |
|||
}) |
|||
.catch((error) => { |
|||
showModal(`Error: ${error.message}`); // 使用 Modal 显示错误信息
|
|||
return; |
|||
}); |
|||
//console.log('retrun 只是把fetch结束,这里的代码还是会执行');
|
|||
} |
|||
|
|||
function connect(channel_id,element_id,imgcanvas,ctx,offscreenCtx,offscreenCanvas,streamUrl) { |
|||
//判断是否有重复socket,进行删除
|
|||
if(element_id in video_list) { |
|||
run_list[element_id] = false; |
|||
video_list[element_id].close(); |
|||
delete video_list[element_id]; |
|||
console.log("有历史数据未删干净!!---",element_id) //要不要等待待定
|
|||
} |
|||
// 每次连接时增加版本号
|
|||
const current_version = (connection_version[element_id] || 0) + 1; |
|||
connection_version[element_id] = current_version; |
|||
const socket = new WebSocket(streamUrl); |
|||
socket.binaryType = 'arraybuffer'; // 设置为二进制数据接收
|
|||
socket.customData = { channel_id: channel_id, element_id: element_id, |
|||
imgcanvas:imgcanvas,ctx:ctx,offscreenCtx:offscreenCtx,offscreenCanvas:offscreenCanvas, |
|||
version_id: current_version,streamUrl:streamUrl}; // 自定义属性 -- JS异步事件只能等到当前同步任务(代码块)完成之后才有可能被触发。
|
|||
//新的连接
|
|||
video_list[element_id] = socket; |
|||
run_list[element_id] = true; |
|||
berror_state_list[element_id] = false; |
|||
imgcanvas.style.display = 'block'; |
|||
|
|||
// 处理连接打开事件
|
|||
socket.onopen = function(){ |
|||
console.log('WebSocket connection established--',socket.customData.channel_id); |
|||
}; |
|||
|
|||
socket.onmessage = function(event) { |
|||
let el_id = socket.customData.element_id |
|||
let cl_id = socket.customData.channel_id |
|||
let imgcanvas = socket.customData.imgcanvas |
|||
let ctx = socket.customData.ctx |
|||
let offctx = socket.customData.offscreenCtx |
|||
let offscreenCanvas = socket.customData.offscreenCanvas |
|||
|
|||
// 转换为字符串来检查前缀
|
|||
let message = new TextDecoder().decode(event.data.slice(0, 6)); // 取前6个字节
|
|||
if (message.startsWith('frame:')){ |
|||
//如有错误信息显示 -- 清除错误信息
|
|||
if(berror_state_list[el_id]){ |
|||
console.log("清除错误信息!"); |
|||
removeErrorMessage(imgcanvas); |
|||
berror_state_list[el_id] = false; |
|||
} |
|||
// 接收到 JPG 图像数据,转换为 Blob
|
|||
let img = new Image(); |
|||
let blob = new Blob([event.data.slice(6)], { type: 'image/jpeg' }); |
|||
// 将 Blob 转换为可用的图像 URL
|
|||
img.src = URL.createObjectURL(blob); |
|||
//定义图片加载函数
|
|||
img.onload = function() { |
|||
imgcanvas.width = offscreenCanvas.width = img.width; |
|||
imgcanvas.height = offscreenCanvas.height = img.height; |
|||
|
|||
// 在 OffscreenCanvas 上绘制
|
|||
offctx.clearRect(0, 0, imgcanvas.width, imgcanvas.height); |
|||
offctx.drawImage(img, 0, 0, imgcanvas.width, imgcanvas.height); |
|||
// 将 OffscreenCanvas 的内容复制到主 canvas
|
|||
ctx.drawImage(offscreenCanvas, 0, 0); |
|||
|
|||
// 用完就释放
|
|||
URL.revokeObjectURL(img.src); |
|||
// blob = null
|
|||
// img = null
|
|||
// message = null
|
|||
// event.data = null
|
|||
// event = null
|
|||
}; |
|||
}else if(message.startsWith('error:')){ |
|||
const errorText = new TextDecoder().decode(event.data.slice(6)); // 截掉前缀 'error:'
|
|||
//目前只处理一个错误信息,暂不区分
|
|||
displayErrorMessage(imgcanvas, "该视频源未获取到画面,请检查后刷新重试,默认两分钟后重连"); |
|||
berror_state_list[el_id] = true; |
|||
} |
|||
}; |
|||
|
|||
socket.onclose = function() { |
|||
let el_id = socket.customData.element_id; |
|||
let cl_id = socket.customData.channel_id; |
|||
//判断是不是要重连
|
|||
if(socket.customData.version_id === connection_version[el_id]){ |
|||
if(run_list[el_id]){ |
|||
console.log(`尝试重新连接... Channel ID: ${cl_id}`); |
|||
setTimeout(() => connect(cl_id, el_id, socket.customData.imgcanvas, |
|||
socket.customData.ctx,socket.customData.streamUrl), 1000*10); // 尝试在10秒后重新连接
|
|||
} |
|||
delete video_list[el_id]; |
|||
delete connection_version[el_id]; |
|||
delete run_list[el_id]; |
|||
} |
|||
}; |
|||
|
|||
socket.onerror = function() { |
|||
console.log(`WebSocket错误,Channel ID: ${socket.customData.channel_id}`); |
|||
socket.close(1000, "Normal Closure"); |
|||
}; |
|||
} |
|||
|
|||
|
|||
function connectToStream(element_id,channel_id,channel_name) { |
|||
console.log("开始连接视频",element_id,channel_id); |
|||
//更新控件状态--设置视频区域的标题
|
|||
const titleElement = document.querySelector(`[data-frame-id="${element_id}"] .video-title`); |
|||
titleElement.textContent = channel_name; |
|||
//视频控件
|
|||
//const imgElement = document.getElementById(`video-${element_id}`);
|
|||
//imgElement.alt = `Stream ${channel_name}`;
|
|||
const imgcanvas = document.getElementById(`video-${element_id}`); |
|||
const ctx = imgcanvas.getContext('2d') |
|||
// 创建 OffscreenCanvas
|
|||
const offscreenCanvas = new OffscreenCanvas(imgcanvas.width, imgcanvas.height); |
|||
const offscreenCtx = offscreenCanvas.getContext('2d'); |
|||
|
|||
const streamUrl = `ws://${window.location.host}/api/ws/video_feed/${channel_id}`; |
|||
|
|||
//创建websocket连接,并接收和显示图片
|
|||
connect(channel_id,element_id,imgcanvas,ctx,offscreenCtx,offscreenCanvas,streamUrl); //执行websocket连接 -- 异步的应该会直接返回
|
|||
} |
|||
|
|||
function closeVideo(id) { |
|||
if(id in video_list) { |
|||
const imgcanvas = document.getElementById(`video-${id}`); |
|||
const titleElement = document.querySelector(`[data-frame-id="${id}"] .video-title`); |
|||
//断socket
|
|||
run_list[id] = false; |
|||
video_list[id].close(); |
|||
delete video_list[id]; |
|||
//清空控件状态
|
|||
imgcanvas.style.display = 'none'; // 停止播放时隐藏元素
|
|||
titleElement.textContent = `Video Stream ${id+1}`; |
|||
removeErrorMessage(imgcanvas); |
|||
berror_state_list[id] = false; |
|||
//删记录
|
|||
const url = '/api/close_stream'; |
|||
const data = {"element_id":id}; |
|||
// 发送 POST 请求
|
|||
fetch(url, { |
|||
method: 'POST', // 指定请求方法为 POST
|
|||
headers: { |
|||
'Content-Type': 'application/json' // 设置请求头,告诉服务器请求体的数据类型为 JSON
|
|||
}, |
|||
body: JSON.stringify(data) // 将 JavaScript 对象转换为 JSON 字符串
|
|||
}) |
|||
.then(response => response.json()) // 将响应解析为 JSON
|
|||
.then(data => { |
|||
console.log('Success:', data); |
|||
const istatus = data.status; |
|||
if(istatus == 0){ |
|||
showModal(data.msg); // 使用 Modal 显示消息
|
|||
return; |
|||
} |
|||
}) |
|||
.catch((error) => { |
|||
showModal(`Error: ${error.message}`); // 使用 Modal 显示错误信息
|
|||
return; |
|||
}); |
|||
} |
|||
else{ |
|||
showModal('当前视频窗口未播放视频。'); |
|||
return; |
|||
} |
|||
} |
|||
|
|||
function startFLVStream(element_id,channel_id,channel_name) { |
|||
|
|||
// 设置视频区域的标题
|
|||
const titleElement = document.querySelector(`[data-frame-id="${element_id}"] .video-title`); |
|||
titleElement.textContent = channel_name; |
|||
//获取视频
|
|||
// const imgElement = document.getElementById(`video-${element_id}`);
|
|||
// imgElement.alt = `Stream ${channel_name}`;
|
|||
const videoElement = document.getElementById(`video-${element_id}`); |
|||
let reconnectAttempts = 0; |
|||
const maxReconnectAttempts = 3; |
|||
const flvUrl = `ws://${window.location.host}/api/ws/video_feed/${channel_id}`; |
|||
|
|||
function initFLVPlayer() { |
|||
if (flvjs.isSupported()) { |
|||
//要避免重复播放
|
|||
if(element_id in video_list) { |
|||
closeFLVStream(element_id) |
|||
}else{ |
|||
video_list[element_id] = element_id; |
|||
berror_state_list[element_id] = true; |
|||
} |
|||
|
|||
flvPlayer = flvjs.createPlayer({ |
|||
type: 'flv', |
|||
url: flvUrl, |
|||
}); |
|||
|
|||
flvPlayer.attachMediaElement(videoElement); |
|||
flvPlayer.load(); |
|||
flvPlayer.play(); |
|||
|
|||
// 设定超时时间,例如10秒
|
|||
timeoutId = setTimeout(() => { |
|||
console.error('No video data received. Closing connection.'); |
|||
flvPlayer.destroy(); // 停止视频
|
|||
// 显示错误信息或提示
|
|||
displayErrorMessage(videoElement, "该视频源获取画面超时,请检查后刷新重试,默认两分钟后重连"); |
|||
berror_state_list[element_id] = true; |
|||
}, 130000); // 130秒
|
|||
|
|||
// 错误处理
|
|||
flvPlayer.on(flvjs.Events.ERROR, (errorType, errorDetail) => { |
|||
console.error(`FLV Error: ${errorType} - ${errorDetail}`); |
|||
if (reconnectAttempts < maxReconnectAttempts) { |
|||
reconnectAttempts++; |
|||
console.log("开始重连") |
|||
setTimeout(initFLVPlayer, 30000); // 尝试重连
|
|||
} else { |
|||
displayErrorMessage(videoElement, "重连超时,请检查后重试,可联系技术支持!"); |
|||
berror_state_list[element_id] = true; |
|||
} |
|||
}); |
|||
|
|||
// 监听播放事件,如果播放成功则清除超时计时器
|
|||
flvPlayer.on(flvjs.Events.STATISTICS_INFO, () => { |
|||
clearTimeout(timeoutId); |
|||
timeoutId = null; |
|||
removeErrorMessage(videoElement); |
|||
berror_state_list[element_id] = false; |
|||
}); |
|||
|
|||
// 关闭视频流时销毁播放器
|
|||
videoElement.addEventListener('closeVideo', () => { |
|||
if(flvPlayer){ |
|||
flvPlayer.destroy(); |
|||
videoElement.removeEventListener('closeVideo', onCloseVideo); |
|||
flvPlayer.off(flvjs.Events.ERROR); |
|||
flvPlayer.off(flvjs.Events.STATISTICS_INFO); |
|||
delete flvPlayer |
|||
} |
|||
}); |
|||
|
|||
} else { |
|||
console.error('FLV is not supported in this browser.'); |
|||
} |
|||
} |
|||
|
|||
initFLVPlayer(); |
|||
} |
|||
|
|||
// 主动关闭视频的函数
|
|||
function closeFLVStream(id) { |
|||
const titleElement = document.querySelector(`[data-frame-id="${id}"] .video-title`); |
|||
if (titleElement.textContent === `Video Stream ${Number(id)+1}`) { |
|||
showModal('当前视频窗口未播放视频。'); |
|||
return; |
|||
}; |
|||
|
|||
//发送视频链接接口
|
|||
const url = '/api/close_stream'; |
|||
const data = {"element_id":id}; |
|||
// 发送 POST 请求
|
|||
fetch(url, { |
|||
method: 'POST', // 指定请求方法为 POST
|
|||
headers: { |
|||
'Content-Type': 'application/json' // 设置请求头,告诉服务器请求体的数据类型为 JSON
|
|||
}, |
|||
body: JSON.stringify(data) // 将 JavaScript 对象转换为 JSON 字符串
|
|||
}) |
|||
.then(response => response.json()) // 将响应解析为 JSON
|
|||
.then(data => { |
|||
console.log('Success:', data); |
|||
const istatus = data.status; |
|||
if(istatus == 0){ |
|||
showModal(data.msg); // 使用 Modal 显示消息
|
|||
return; |
|||
} |
|||
else{ |
|||
const videoFrame = document.getElementById(`video-${element_id}`); |
|||
const event = new Event('closeVideo'); |
|||
videoFrame.dispatchEvent(event); |
|||
|
|||
//videoFrame.style.display = 'none'; // 停止播放时隐藏 img 元素
|
|||
titleElement.textContent = `Video Stream ${id+1}`; |
|||
removeErrorMessage(videoFrame); |
|||
berror_state_list[key] = false; |
|||
delete video_list[id]; |
|||
} |
|||
}) |
|||
.catch((error) => { |
|||
showModal(`Error: ${error.message}`); // 使用 Modal 显示错误信息
|
|||
return; |
|||
}); |
|||
|
|||
} |
|||
|
|||
|
|||
function displayErrorMessage(imgElement, message) { |
|||
removeErrorMessage(imgElement) |
|||
imgElement.style.display = 'none'; // 隐藏图片
|
|||
const errorElement = document.createElement('div'); |
|||
errorElement.textContent = message; |
|||
errorElement.classList.add('error-message'); |
|||
imgElement.parentNode.appendChild(errorElement); |
|||
} |
|||
|
|||
function removeErrorMessage(imgElement) { |
|||
const errorElement = imgElement.parentNode.querySelector('.error-message'); |
|||
if (errorElement) { |
|||
imgElement.parentNode.removeChild(errorElement); |
|||
imgElement.style.display = 'block'; |
|||
} |
|||
} |
|||
|
@ -1,735 +0,0 @@ |
|||
const apiEndpoint = '/api/channel/list'; |
|||
const rowsPerPage = 10; |
|||
//算法配置窗口部分控件
|
|||
const searchEndpoint = '/api/channel/select'; |
|||
const canvas = document.getElementById('myCanvas'); |
|||
const ctx = canvas.getContext('2d'); |
|||
const backgroundCanvas = document.getElementById('backgroundCanvas'); |
|||
const backgroundCtx = backgroundCanvas.getContext('2d'); |
|||
const img = new Image(); |
|||
const tbody = document.getElementById('schedule-body');//布防计划
|
|||
|
|||
let currentPage = 1; |
|||
let channelData = []; |
|||
let channelData_bak = []; |
|||
let areaData = ["请选择"]; |
|||
let currentEditingRow = null; |
|||
let cid_schedule = "-1"; |
|||
let m_polygon = ""; |
|||
let check_area = 0; |
|||
let draw_status = false; //是否是绘制状态,处于绘制状态才能开始绘制
|
|||
let b_img = false; //有没有加载图片成功,如果没有初始化的时候就不绘制线条了。
|
|||
let points = []; //检测区域的点坐标数组
|
|||
//布防计划
|
|||
|
|||
|
|||
document.addEventListener('DOMContentLoaded', function () { |
|||
fetchChannelData(); //初始化通道管理页面元素数据
|
|||
|
|||
document.getElementById('searchButton').addEventListener('click', function () { |
|||
performSearch(); |
|||
}); |
|||
//新增通道模块--保存按钮
|
|||
document.getElementById('saveButton').addEventListener('click', function () { |
|||
addChannel(1); |
|||
}); |
|||
//修改通道模块--保存按钮
|
|||
document.getElementById('saveButton_cc').addEventListener('click', function () { |
|||
addChannel(2); |
|||
}); |
|||
//算法配置模块--取消按钮
|
|||
document.getElementById('cancelButton_mx').addEventListener('click', function () { |
|||
close_mx_model(); |
|||
}); |
|||
//保存算法配置--保存按钮
|
|||
document.getElementById('saveButton_mx').addEventListener('click', function () { |
|||
save_mx_model(); |
|||
}); |
|||
//开始绘制区域按钮
|
|||
document.getElementById('but_hzqy').addEventListener('click', function () { |
|||
startDraw(); |
|||
}); |
|||
}); |
|||
|
|||
//添加和修改通道 1--新增,2--修改
|
|||
function addChannel(itype) { |
|||
let area; |
|||
let cName; |
|||
let Rtsp; |
|||
let cid; |
|||
const spinnerOverlay = document.getElementById("spinnerOverlay"); |
|||
let saveButton = null; |
|||
let CNameInput = null; |
|||
let RTSPInput = null; |
|||
|
|||
if(itype ==1){ |
|||
saveButton = document.getElementById('saveButton'); |
|||
CNameInput = document.getElementById('CNameInput'); |
|||
RTSPInput = document.getElementById('RTSPInput'); |
|||
area = document.getElementById('areaSelect_M').value; |
|||
cid = -1 |
|||
} |
|||
else if(itype ==2){ |
|||
saveButton = document.getElementById('saveButton_cc'); |
|||
CNameInput = document.getElementById('CNameInput_cc'); |
|||
RTSPInput = document.getElementById('RTSPInput_cc'); |
|||
area = document.getElementById('areaSelect_CC').value; |
|||
cid = currentEditingRow.cells[0].innerText; |
|||
} |
|||
console.log("点击了保存按钮"); |
|||
cName = CNameInput.value.trim(); |
|||
Rtsp = RTSPInput.value.trim(); |
|||
if(area === "请选择"){ |
|||
alert('请选择所属区域'); |
|||
} |
|||
else{ |
|||
if (cName && Rtsp) { |
|||
saveButton.disabled = true; |
|||
//发送视频链接接口
|
|||
const url = '/api/channel/add'; |
|||
const data = {"area":area,"cName":cName,"Rtsp":Rtsp,"cid":cid}; |
|||
// 显示 Spinners
|
|||
spinnerOverlay.style.display = "flex"; |
|||
// 发送 POST 请求
|
|||
fetch(url, { |
|||
method: 'POST', // 指定请求方法为 POST
|
|||
headers: { |
|||
'Content-Type': 'application/json' // 设置请求头,告诉服务器请求体的数据类型为 JSON
|
|||
}, |
|||
body: JSON.stringify(data) // 将 JavaScript 对象转换为 JSON 字符串
|
|||
}) |
|||
.then(response => response.json()) // 将响应解析为 JSON
|
|||
.then(data => { |
|||
const istatus = data.status; |
|||
saveButton.disabled = false; |
|||
alert(data.msg); // 使用 Modal 显示消息
|
|||
if(istatus == 1){ |
|||
//刷新列表
|
|||
fetchChannelData(); |
|||
if(itype ==1){ |
|||
//添加通道成功
|
|||
$('#channelModal').modal('hide'); |
|||
} |
|||
else if(itype==2){ |
|||
//修改通道成功
|
|||
currentEditingRow = null; |
|||
$('#ChangeC').modal('hide'); |
|||
} |
|||
} |
|||
}) |
|||
.catch((error) => { |
|||
alert(`Error: ${error.message}`); // 使用 Modal 显示错误信息
|
|||
// 启用保存按钮
|
|||
saveButton.disabled = false; |
|||
return; |
|||
}) |
|||
.finally(()=>{ |
|||
// 隐藏 Spinners
|
|||
spinnerOverlay.style.display = "none"; |
|||
}); |
|||
} else { |
|||
alert('通道名称和RTSP地址不能为空'); |
|||
} |
|||
} |
|||
} |
|||
|
|||
//初始化通道管理页面元素数据
|
|||
async function fetchChannelData() { //获取通道相关信息(/api/channel/list),刷新通道表格控件数据
|
|||
try { |
|||
const response = await fetch(apiEndpoint); |
|||
channelData = await response.json(); |
|||
channelData_bak = channelData; |
|||
|
|||
url = "/api/channel/area/list" |
|||
area_response = await fetch(url); |
|||
areaDatas = await area_response.json(); |
|||
areaData = ["请选择"]; //清空下
|
|||
areaDatas.forEach((area) => { |
|||
areaData.push(area.area_name) |
|||
}); |
|||
|
|||
renderTable(); //刷新表格
|
|||
renderPagination(); //刷新分页元素
|
|||
renderAreaOptions(); //所属区域下来框
|
|||
} catch (error) { |
|||
console.error('Error fetching channel data:', error); |
|||
} |
|||
} |
|||
|
|||
//刷新表单页面数据
|
|||
function renderTable() { |
|||
const tableBody = document.getElementById('table-body'); |
|||
tableBody.innerHTML = ''; |
|||
|
|||
const start = (currentPage - 1) * rowsPerPage; |
|||
const end = start + rowsPerPage; |
|||
const pageData = channelData.slice(start, end); |
|||
const surplus_count = rowsPerPage - pageData.length; |
|||
|
|||
pageData.forEach((channel) => { |
|||
// if(area_name!==channel.area_name){ //这里要求区域名称一样的要在一起
|
|||
// area_name = channel.area_name;
|
|||
// areaData.push(area_name);
|
|||
// }
|
|||
const row = document.createElement('tr'); |
|||
row.innerHTML = ` |
|||
<td>${channel.ID}</td> |
|||
<td>${channel.area_name}</td> |
|||
<td>${channel.channel_name}</td> |
|||
<td>${channel.ulr}</td> |
|||
<td>${channel.model_name}</td> |
|||
<td> |
|||
<button class="btn btn-primary btn-sm modify-btn">修改</button> |
|||
<button class="btn btn-secondary btn-sm algorithm-btn">算法</button> |
|||
<button class="btn btn-danger btn-sm delete-btn">删除</button> |
|||
</td> |
|||
`;
|
|||
tableBody.appendChild(row); |
|||
row.querySelector('.modify-btn').addEventListener('click', () => modifyChannel(row)); |
|||
row.querySelector('.algorithm-btn').addEventListener('click', () => configureAlgorithm(row)); |
|||
row.querySelector('.delete-btn').addEventListener('click', () => deleteChannel(row)); |
|||
}); |
|||
} |
|||
|
|||
//关键字查询数据
|
|||
async function performSearch() { |
|||
try { |
|||
const area = document.getElementById('areaSelect').value; |
|||
const channelName = document.getElementById('channelNameInput').value; |
|||
if(area === "请选择" && channelName===""){ |
|||
channelData = channelData_bak; |
|||
} |
|||
else if(area === "请选择"){ |
|||
channelData = []; |
|||
channelData_bak.forEach((channel) => { |
|||
if(channelName === channel.channel_name){ |
|||
channelData.push(channel); |
|||
} |
|||
}); |
|||
} |
|||
else if(channelName === ""){ |
|||
channelData = []; |
|||
channelData_bak.forEach((channel) => { |
|||
if(area === channel.area_name){ |
|||
channelData.push(channel); |
|||
} |
|||
}); |
|||
} |
|||
else{ |
|||
channelData = []; |
|||
channelData_bak.forEach((channel) => { |
|||
if(area === channel.area_name && channelName === channel.channel_name){ |
|||
channelData.push(channel); |
|||
} |
|||
}); |
|||
} |
|||
// 渲染表格和分页控件
|
|||
currentPage = 1; // 重置当前页为第一页
|
|||
renderTable(); |
|||
renderPagination(); |
|||
} catch (error) { |
|||
console.error('Error performing search:', error); |
|||
} |
|||
} |
|||
|
|||
//点击修改按钮,显示修改通道信息模块 --只是显示
|
|||
function modifyChannel(row) { |
|||
// const cid = row.cells[0].innerText;
|
|||
const areaName = row.cells[1].innerText; |
|||
const channelName = row.cells[2].innerText; |
|||
const url = row.cells[3].innerText; |
|||
|
|||
const area = document.getElementById('areaSelect_CC'); |
|||
const CName = document.getElementById('CNameInput_cc'); |
|||
const RTSP = document.getElementById('RTSPInput_cc'); |
|||
|
|||
for(let i=0;i< area.options.length;i++){ |
|||
if(area.options[i].value === areaName){ |
|||
area.options[i].selected = true; |
|||
break; |
|||
} |
|||
} |
|||
CName.value = channelName; |
|||
RTSP.value = url; |
|||
|
|||
currentEditingRow = row; |
|||
$('#ChangeC').modal('show'); |
|||
} |
|||
|
|||
//点击算法按钮,显示算法配置模块 --只是显示
|
|||
function configureAlgorithm(row) { |
|||
//获取当前行信息
|
|||
currentEditingRow = row; |
|||
const cid = row.cells[0].innerText; |
|||
//清除数据,若需要的话
|
|||
ctx.clearRect(0, 0, canvas.width, canvas.height); //清除左侧绘画和画线信息
|
|||
tbody.innerHTML = ''; //清空布防控件数据
|
|||
points = []; //清空绘制检测区域
|
|||
draw_status = false; |
|||
b_img = false; |
|||
document.getElementById('but_hzqy').textContent = "绘制区域"; |
|||
//开始初始化算法管理模块
|
|||
show_channel_model_schedule(cid); //获取并显示结构化数据
|
|||
show_channel_img(cid); //获取并显示一帧图片 -- 获取不到图片就是黑画面 --并要绘制检测区域
|
|||
|
|||
//显示窗口
|
|||
$('#MX_M').modal('show'); |
|||
} |
|||
|
|||
//获取一帧图片
|
|||
function show_channel_img(cid){ |
|||
const data = {"cid":cid}; |
|||
fetch('/api/channel/img', { |
|||
method: 'POST', |
|||
headers: { |
|||
'Content-Type': 'application/json' |
|||
}, |
|||
body: JSON.stringify(data) |
|||
}) |
|||
.then(response => response.json()) |
|||
.then(data => { |
|||
if (data.image) { |
|||
b_img = true; |
|||
img.src = 'data:image/jpeg;base64,' + data.image; |
|||
} else { |
|||
console.error('Error:', data.error); |
|||
} |
|||
}) |
|||
.catch(error => console.error('Error:', error)); |
|||
} |
|||
|
|||
//图片加载事项
|
|||
img.onload = () => { //清除、画图和画线应该分开
|
|||
// 设置画布宽高
|
|||
backgroundCanvas.width = canvas.width = img.width; |
|||
backgroundCanvas.height = canvas.height = img.height; |
|||
// 将图片绘制到背景画布上
|
|||
backgroundCtx.drawImage(img, 0, 0, img.width, img.height); |
|||
drawLines(); |
|||
// 将背景画布的内容复制到前台画布上
|
|||
//ctx.drawImage(backgroundCanvas, 0, 0, canvas.width, canvas.height); //绘制画面
|
|||
}; |
|||
|
|||
//开始和重新绘制
|
|||
function startDraw(){ |
|||
if(!document.getElementById('zdjc').checked){ |
|||
alert("请先选择指定区域!"); |
|||
return; |
|||
} |
|||
let but = document.getElementById('but_hzqy'); |
|||
if(!draw_status){//开始绘制
|
|||
if(points.length >0){ |
|||
if (confirm('开始绘制将清除未提交保存的绘制数据,是否继续?')) { |
|||
draw_status = true; |
|||
points = []; |
|||
//按钮文字调整为结束绘制
|
|||
but.textContent = '结 束 绘 制'; |
|||
// 清除前台画布
|
|||
ctx.clearRect(0, 0, canvas.width, canvas.height); |
|||
// 将背景画布的内容复制到前台画布上
|
|||
ctx.drawImage(backgroundCanvas, 0, 0, canvas.width, canvas.height); |
|||
} |
|||
} |
|||
else{ |
|||
draw_status = true; |
|||
but.textContent = '结束绘制'; |
|||
} |
|||
} |
|||
else{//结束绘制
|
|||
draw_status = false; |
|||
but.textContent = '绘制区域'; |
|||
} |
|||
} |
|||
|
|||
//单选按钮点击事件处理
|
|||
function handleRadioClick(event) { |
|||
const selectedRadio = event.target; |
|||
console.log('Selected Radio:', selectedRadio.id); |
|||
// 根据选中的单选按钮执行相应操作
|
|||
if (selectedRadio.id === 'qjjc') { |
|||
if(draw_status){ |
|||
alert("请先结束绘制后,再切换检测方案!"); |
|||
document.getElementById('zdjc').checked = true; |
|||
} |
|||
else{ |
|||
// 处理全画面生效的逻辑
|
|||
if(points.length>0){ |
|||
if (!confirm('切换到全画面生效,将清除已绘制的区域信息,是否切换?')) { |
|||
document.getElementById('zdjc').checked = true; |
|||
}else{ |
|||
points = []; |
|||
} |
|||
} |
|||
} |
|||
//console.log('全画面生效');
|
|||
} else if (selectedRadio.id === 'zdjc') { |
|||
// 处理指定区域的逻辑
|
|||
console.log('指定区域'); |
|||
} |
|||
} |
|||
|
|||
// 鼠标点击事件处理--动态绘图
|
|||
canvas.addEventListener('click', (event) => { |
|||
if(draw_status){ |
|||
const rect = canvas.getBoundingClientRect(); |
|||
const scaleX = canvas.width / rect.width; |
|||
const scaleY = canvas.height / rect.height; |
|||
|
|||
// 获取鼠标相对于canvas的位置
|
|||
const x = (event.clientX - rect.left) * scaleX; |
|||
const y = (event.clientY - rect.top) * scaleY; |
|||
points.push({ x, y }); |
|||
console.log(points); |
|||
//绘制线条
|
|||
drawLines(); |
|||
} |
|||
}); |
|||
|
|||
// 绘制区域,各点连接
|
|||
function drawLines() { |
|||
if(b_img){ |
|||
// 清除前台画布
|
|||
ctx.clearRect(0, 0, canvas.width, canvas.height); |
|||
// 将背景画布的内容复制到前台画布上
|
|||
ctx.drawImage(backgroundCanvas, 0, 0, canvas.width, canvas.height); |
|||
|
|||
// 绘制点和线
|
|||
ctx.strokeStyle = 'red'; |
|||
ctx.lineWidth = 2; |
|||
|
|||
if (points.length > 0) { |
|||
ctx.beginPath(); |
|||
ctx.moveTo(points[0].x, points[0].y); |
|||
|
|||
for (let i = 1; i < points.length; i++) { |
|||
ctx.lineTo(points[i].x, points[i].y); |
|||
} |
|||
|
|||
// 连接最后一个点到起点
|
|||
ctx.lineTo(points[0].x, points[0].y); |
|||
ctx.stroke(); |
|||
} |
|||
|
|||
points.forEach(point => { |
|||
ctx.beginPath(); |
|||
ctx.arc(point.x, point.y, 5, 0, Math.PI * 2); |
|||
ctx.fillStyle = 'red'; |
|||
ctx.fill(); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
//获取并显示该通道相关算法的结构化数据 --- 这里用GET会更加贴切一些
|
|||
function show_channel_model_schedule(cid){ |
|||
//发送视频链接接口
|
|||
const url = '/api/channel/C2M'; |
|||
const data = {"cid":cid}; |
|||
// 发送 POST 请求
|
|||
fetch(url, { |
|||
method: 'POST', // 指定请求方法为 POST
|
|||
headers: { |
|||
'Content-Type': 'application/json' // 设置请求头,告诉服务器请求体的数据类型为 JSON
|
|||
}, |
|||
body: JSON.stringify(data) // 将 JavaScript 对象转换为 JSON 字符串
|
|||
}) |
|||
.then(response => response.json()) // 将响应解析为 JSON
|
|||
.then(data => { |
|||
const m_datas = data.m_datas; //算法清单
|
|||
const c2m_data = data.c2m_data; //该通道管理算法的相关数据,会有空的情况
|
|||
const schedule = data.schedule; //布防计划
|
|||
//console.log("m_datas--",m_datas);
|
|||
//console.log("c2m_data--",c2m_data);
|
|||
//console.log("schedule--",schedule);
|
|||
//配置算法下拉清单
|
|||
select_datas = ["请选择"]; |
|||
m_datas.forEach(option => { |
|||
select_datas.push(option.model_name); |
|||
}); |
|||
set_select_data("model_select",select_datas); |
|||
select_str = currentEditingRow.cells[4].innerText; |
|||
//检测区域
|
|||
if(c2m_data.length >0){ |
|||
model_id = c2m_data[0].model_id; |
|||
model_name = currentEditingRow.cells[4].innerText; |
|||
set_select_selct("model_select",model_name); |
|||
check_area = c2m_data[0].check_area |
|||
if( check_area == 0){ //全画面生效
|
|||
document.getElementById('qjjc').checked = true; |
|||
m_polygon = ""; |
|||
} |
|||
else{//指定区域
|
|||
document.getElementById('zdjc').checked = true; |
|||
m_polygon = c2m_data[0].polygon; |
|||
console.log("m_polygon--",m_polygon); |
|||
if(m_polygon !== ""){ //指定区域了,一般是会有数据的。
|
|||
const coords = parseCoordStr(m_polygon); |
|||
points = coords; |
|||
} |
|||
} |
|||
//阈值
|
|||
document.getElementById('zxyz').value = c2m_data[0].conf_thres |
|||
document.getElementById('iouyz').value = c2m_data[0].iou_thres |
|||
} |
|||
|
|||
//布防计划
|
|||
const days = ['一', '二', '三', '四', '五', '六','日']; |
|||
const num_days=['0','1','2','3','4','5','6'] |
|||
days.forEach((day, dayIndex) => { |
|||
const row = document.createElement('tr'); |
|||
const dayCell = document.createElement('th'); |
|||
dayCell.textContent = day; |
|||
row.appendChild(dayCell); |
|||
num_day = num_days[dayIndex] |
|||
for (let hour = 0; hour < 24; hour++) { |
|||
const cell = document.createElement('td'); |
|||
if(schedule.length >0){ |
|||
const status = schedule.find(item => item.day === num_day && item.hour === hour); |
|||
if (status && status.status === 1) { |
|||
cell.classList.add('blocked'); |
|||
} else { |
|||
cell.classList.add('allowed'); |
|||
} |
|||
} |
|||
else{ |
|||
cell.classList.add('blocked'); |
|||
} |
|||
row.appendChild(cell); |
|||
|
|||
cell.addEventListener('click', () => { |
|||
if (cell.classList.contains('blocked')) { |
|||
cell.classList.remove('blocked'); |
|||
cell.classList.add('allowed'); |
|||
// Update status in the database
|
|||
//updateStatus(day, hour, 0);
|
|||
} else { |
|||
cell.classList.remove('allowed'); |
|||
cell.classList.add('blocked'); |
|||
// Update status in the database
|
|||
//updateStatus(day, hour, 1);
|
|||
} |
|||
}); |
|||
} |
|||
tbody.appendChild(row); |
|||
}); |
|||
}) |
|||
.catch((error) => { |
|||
alert(`Error: ${error.message}`); // 使用 Modal 显示错误信息
|
|||
return; |
|||
}); |
|||
} |
|||
|
|||
// 将字符串转换为数组
|
|||
function parseCoordStr(str) { |
|||
return str.match(/\(([^)]+)\)/g).map(pair => { |
|||
const [x, y] = pair.replace(/[()]/g, '').split(',').map(Number); |
|||
return { x, y }; |
|||
}); |
|||
} |
|||
|
|||
//关闭算法配置窗口
|
|||
function close_mx_model(){ |
|||
if (confirm('确定退出窗口吗?未保存的修改将丢失!')) { |
|||
$('#MX_M').modal('hide'); |
|||
} |
|||
} |
|||
|
|||
//保存算法配置窗口数据
|
|||
function save_mx_model(){ |
|||
let model_name; //算法名称
|
|||
let check_area; //检测区域标识 0-全局,1-指定范围
|
|||
let polygon_str; //具体的检测区域
|
|||
let conf_thres; //置信阈值
|
|||
let iou_thres; //iou阈值
|
|||
let schedule; //布防计划
|
|||
const saveButton = document.getElementById('saveButton_mx'); |
|||
saveButton.disabled = true; //不可点击状态
|
|||
//配置算法
|
|||
model_name = document.getElementById("model_select").value; |
|||
//检测区域
|
|||
if(document.getElementById('zdjc').checked){ |
|||
check_area = 1; |
|||
console.log("points--",points); |
|||
if (points.length > 0){ |
|||
const formattedArray = points.map(point => `(${point.x},${point.y})`); |
|||
polygon_str = `[${formattedArray.join(',')}]`; |
|||
}else{ |
|||
polygon_str = ""; |
|||
} |
|||
|
|||
}else{ |
|||
check_area = 0; |
|||
polygon_str = ""; |
|||
} |
|||
//置信阈值和IOU阈值
|
|||
conf_thres = getInputValueAsFloat('zxyz'); |
|||
iou_thres = getInputValueAsFloat('iouyz'); |
|||
//验证数据
|
|||
if(model_name !== "请选择"){ |
|||
console.log(model_name); |
|||
if(conf_thres <= 0 || conf_thres>=1 || iou_thres <= 0 || iou_thres>=1){ |
|||
alert("阈值的有效范围是大于0,小于1;请输入正确的阈值(默认可0.5)。"); |
|||
saveButton.disabled = false; //不可点击状态
|
|||
return; |
|||
} |
|||
} |
|||
//布防计划
|
|||
// 定义一个对象来存储数据
|
|||
const scheduleData = { |
|||
'0': Array(24).fill(0), |
|||
'1': Array(24).fill(0), |
|||
'2': Array(24).fill(0), |
|||
'3': Array(24).fill(0), |
|||
'4': Array(24).fill(0), |
|||
'5': Array(24).fill(0), |
|||
'6': Array(24).fill(0) |
|||
}; |
|||
// 遍历 tbody 的每一行
|
|||
[...tbody.children].forEach((row, dayIndex) => { |
|||
// 获取当前行的所有单元格
|
|||
const cells = row.getElementsByTagName('td'); |
|||
|
|||
// 遍历每一个单元格
|
|||
for (let hour = 0; hour < cells.length; hour++) { |
|||
// 检查单元格的 class 是否包含 'blocked'
|
|||
if (cells[hour].classList.contains('blocked')) { |
|||
// 将对应的 scheduleData 位置设置为 1
|
|||
scheduleData[dayIndex][hour] = 1; |
|||
} else { |
|||
// 将对应的 scheduleData 位置设置为 0
|
|||
scheduleData[dayIndex][hour] = 0; |
|||
} |
|||
} |
|||
}); |
|||
// 将 scheduleData 对象转换为 JSON 字符串
|
|||
const scheduleData_json = JSON.stringify(scheduleData); |
|||
//提交到服务器
|
|||
// console.log("model_name--",model_name);
|
|||
// console.log("check_area--",check_area);
|
|||
// console.log("polygon_str--",polygon_str);
|
|||
// console.log("iou_thres--",iou_thres);
|
|||
// console.log("conf_thres--",conf_thres);
|
|||
// console.log("schedule-- ",scheduleData_json);
|
|||
cid = currentEditingRow.cells[0].innerText; |
|||
const url = '/api/channel/chanegeC2M'; |
|||
const data = {"model_name":model_name,"check_area":check_area,"polygon_str":polygon_str,"iou_thres":iou_thres, |
|||
"conf_thres":conf_thres,"schedule":scheduleData_json,"cid":cid}; |
|||
// 发送 POST 请求
|
|||
fetch(url, { |
|||
method: 'POST', // 指定请求方法为 POST
|
|||
headers: { |
|||
'Content-Type': 'application/json' // 设置请求头,告诉服务器请求体的数据类型为 JSON
|
|||
}, |
|||
body: JSON.stringify(data) // 将 JavaScript 对象转换为 JSON 字符串
|
|||
}) |
|||
.then(response => response.json()) // 将响应解析为 JSON
|
|||
.then(data => { |
|||
const istatus = data.status; |
|||
if(istatus === 0){ |
|||
alert(data.msg); // 使用 Modal 显示消息
|
|||
// 启用保存按钮
|
|||
saveButton.disabled = false; |
|||
return; |
|||
} |
|||
else{ |
|||
// 启用保存按钮
|
|||
saveButton.disabled = false; |
|||
//刷新列表
|
|||
fetchChannelData(); |
|||
//$('#MX_M').modal('hide');
|
|||
alert("修改成功!"); |
|||
} |
|||
}) |
|||
.catch((error) => { |
|||
alert(`Error: ${error.message}`); |
|||
// 启用保存按钮
|
|||
saveButton.disabled = false; |
|||
return; |
|||
}); |
|||
|
|||
} |
|||
|
|||
//删除通道
|
|||
function deleteChannel(row) { |
|||
if (confirm('确定删除此通道吗?')) { |
|||
cid = row.cells[0].innerText; |
|||
//发送视频链接接口
|
|||
const url = '/api/channel/del'; |
|||
const data = {"cid":cid}; |
|||
// 发送 POST 请求
|
|||
fetch(url, { |
|||
method: 'POST', // 指定请求方法为 POST
|
|||
headers: { |
|||
'Content-Type': 'application/json' // 设置请求头,告诉服务器请求体的数据类型为 JSON
|
|||
}, |
|||
body: JSON.stringify(data) // 将 JavaScript 对象转换为 JSON 字符串
|
|||
}) |
|||
.then(response => response.json()) // 将响应解析为 JSON
|
|||
.then(data => { |
|||
const istatus = data.status; |
|||
if(istatus === 0){ |
|||
alert(data.msg); // 使用 Modal 显示消息
|
|||
return; |
|||
} |
|||
else{ |
|||
//刷新列表
|
|||
row.remove(); |
|||
alert("删除通道成功!"); |
|||
} |
|||
}) |
|||
.catch((error) => { |
|||
alert(`Error: ${error.message}`); // 使用 Modal 显示错误信息
|
|||
return; |
|||
}); |
|||
|
|||
} |
|||
} |
|||
|
|||
//刷新分页标签
|
|||
function renderPagination() { |
|||
const pagination = document.getElementById('pagination'); |
|||
pagination.innerHTML = ''; |
|||
|
|||
const totalPages = Math.ceil(channelData.length / rowsPerPage); |
|||
for (let i = 1; i <= totalPages; i++) { |
|||
const pageItem = document.createElement('li'); |
|||
pageItem.className = 'page-item' + (i === currentPage ? ' active' : ''); |
|||
pageItem.innerHTML = `<a class="page-link" href="#">${i}</a>`; |
|||
pageItem.addEventListener('click', (event) => { |
|||
event.preventDefault(); |
|||
currentPage = i; |
|||
renderTable(); |
|||
renderPagination(); |
|||
}); |
|||
pagination.appendChild(pageItem); |
|||
} |
|||
} |
|||
|
|||
//刷新区域下拉控件
|
|||
function renderAreaOptions() { |
|||
const areaSelect = document.getElementById('areaSelect'); |
|||
const areaSelect_M = document.getElementById('areaSelect_M') |
|||
const areaSelect_CC = document.getElementById('areaSelect_CC') |
|||
//先清空
|
|||
areaSelect.innerHTML = ''; |
|||
areaSelect_M.innerHTML = ''; |
|||
areaSelect_CC.innerHTML = ''; |
|||
//再添加
|
|||
areaData.forEach(option => { |
|||
const optionElement = document.createElement('option'); |
|||
optionElement.textContent = option; |
|||
areaSelect.appendChild(optionElement); |
|||
|
|||
const optionElement_m = document.createElement('option'); |
|||
optionElement_m.textContent = option; |
|||
areaSelect_M.appendChild(optionElement_m); |
|||
|
|||
const optionElement_cc = document.createElement('option'); |
|||
optionElement_cc.textContent = option; |
|||
areaSelect_CC.appendChild(optionElement_cc); |
|||
}); |
|||
} |
|||
|
|||
|
|||
|
|||
|
|||
|
@ -1,303 +0,0 @@ |
|||
let currentPage = 1; |
|||
const rowsPerPage = 10; |
|||
let modelData = []; |
|||
let modelData_bak = []; //用于关键字检索
|
|||
let currentEditingRow = null; |
|||
|
|||
|
|||
|
|||
//页面加载初始化
|
|||
document.addEventListener('DOMContentLoaded', function () { |
|||
fetchModelData(); //初始化通道管理页面元素数据
|
|||
//新增算法模态框---保存按钮
|
|||
document.getElementById('saveButton_model').addEventListener('click',function () { |
|||
addModel(); |
|||
}); |
|||
|
|||
//配置算法模态框--保存按钮
|
|||
document.getElementById('saveButton_config_model').addEventListener('click',function () { |
|||
post_configureModel(); |
|||
}); |
|||
|
|||
//升级算法模态框--保存按钮
|
|||
document.getElementById('saveButton_upmodel').addEventListener('click',function () { |
|||
post_modifyModel(); |
|||
}); |
|||
|
|||
//查询按钮
|
|||
document.getElementById('searchMButton').addEventListener('click',function () { |
|||
searchModel(); |
|||
}); |
|||
}); |
|||
|
|||
//获取算法列表数据,并更新页面
|
|||
async function fetchModelData() { |
|||
try{ |
|||
let response = await fetch('/api/model/list'); |
|||
if (!response.ok) { |
|||
throw new Error('Network response was not ok'); |
|||
} |
|||
modelData = await response.json(); |
|||
modelData_bak = modelData; |
|||
currentPage = 1; // 重置当前页为第一页
|
|||
renderTable(); //刷新表格
|
|||
renderPagination(); |
|||
}catch (error) { |
|||
console.error('Error fetching model data:', error); |
|||
} |
|||
} |
|||
|
|||
//刷新表单页面数据
|
|||
function renderTable() { |
|||
const tableBody = document.getElementById('table-body-model'); |
|||
tableBody.innerHTML = ''; //清空
|
|||
|
|||
const start = (currentPage - 1) * rowsPerPage; |
|||
const end = start + rowsPerPage; |
|||
const pageData = modelData.slice(start, end); |
|||
const surplus_count = rowsPerPage - pageData.length; |
|||
|
|||
|
|||
pageData.forEach((model) => { |
|||
const row = document.createElement('tr'); |
|||
row.innerHTML = ` |
|||
<td>${model.ID}</td> |
|||
<td>${model.name}</td> |
|||
<td>${model.version}</td> |
|||
<td>${model.duration_time}</td> |
|||
<td>${model.proportion}</td> |
|||
<td> |
|||
<button class="btn btn-primary btn-sm modify-btn">升级</button> |
|||
<button class="btn btn-secondary btn-sm algorithm-btn">配置</button> |
|||
<button class="btn btn-danger btn-sm delete-btn">删除</button> |
|||
</td> |
|||
`;
|
|||
tableBody.appendChild(row); |
|||
row.querySelector('.modify-btn').addEventListener('click', () => modifyModel(row)); |
|||
row.querySelector('.algorithm-btn').addEventListener('click', () => configureModel(row)); |
|||
row.querySelector('.delete-btn').addEventListener('click', () => deleteModel(row)); |
|||
}); |
|||
} |
|||
|
|||
//刷新分页标签
|
|||
function renderPagination() { |
|||
const pagination = document.getElementById('pagination-model'); |
|||
pagination.innerHTML = ''; |
|||
|
|||
const totalPages = Math.ceil(modelData.length / rowsPerPage); |
|||
for (let i = 1; i <= totalPages; i++) { |
|||
const pageItem = document.createElement('li'); |
|||
pageItem.className = 'page-item' + (i === currentPage ? ' active' : ''); |
|||
pageItem.innerHTML = `<a class="page-link" href="#">${i}</a>`; |
|||
pageItem.addEventListener('click', (event) => { |
|||
event.preventDefault(); |
|||
currentPage = i; |
|||
renderTable(); |
|||
renderPagination(); |
|||
}); |
|||
pagination.appendChild(pageItem); |
|||
} |
|||
} |
|||
|
|||
//显示升级算法模态框
|
|||
function modifyModel(row){ |
|||
currentEditingRow = row; |
|||
model_name = row.cells[1].innerText; |
|||
version_name = row.cells[2].innerText; |
|||
|
|||
document.getElementById('update_mname_label').innerText = `算法名称: ${model_name}`; |
|||
document.getElementById('update_mversion_label').innerText = `当前版本: ${version_name}`; |
|||
$('#updateMM').modal('show'); |
|||
} |
|||
|
|||
//升级算法模态框--点击保存按钮
|
|||
function post_modifyModel(){ |
|||
mid = currentEditingRow.cells[0].innerText; |
|||
const btn = document.getElementById('saveButton_upmodel'); |
|||
const fileInput = document.getElementById('updateModelFile'); |
|||
const file = fileInput.files[0]; |
|||
if(file){ |
|||
btn.disabled = true; //不可点击
|
|||
const formData = new FormData(); |
|||
formData.append('file', file); |
|||
formData.append('mid', mid); |
|||
|
|||
fetch('/api/model/upgrade', { |
|||
method: 'POST', |
|||
body: formData, |
|||
}) |
|||
.then(response => response.json()) |
|||
.then(data => { |
|||
const istatus = data.status; |
|||
alert(data.msg); |
|||
btn.disabled = false; |
|||
if(istatus == 1 ){ |
|||
fetchModelData(); |
|||
$('#updateMM').modal('hide'); |
|||
} |
|||
}) |
|||
.catch(error => { |
|||
console.error('Error:', error); |
|||
alert('升级失败,请重试。'); |
|||
btn.disabled = false; |
|||
}); |
|||
} |
|||
else{ |
|||
alert('请选择升级包进行上传。'); |
|||
btn.disabled = false; |
|||
} |
|||
} |
|||
|
|||
//显示配置算法模态框
|
|||
function configureModel(row){ |
|||
currentEditingRow = row; |
|||
model_name = row.cells[1].innerText; |
|||
duration_time = row.cells[3].innerText; |
|||
proportion = row.cells[4].innerText; |
|||
//设置模态框控件遍历
|
|||
document.getElementById('config_mname_label').innerText = `算法名称: ${model_name}`; |
|||
document.getElementById('duration_timeInput').value = duration_time; |
|||
document.getElementById('proportionInput').value = proportion; |
|||
$('#configMM').modal('show'); |
|||
} |
|||
|
|||
//配置算法模态框--点击保存按钮
|
|||
function post_configureModel(){ |
|||
mid = currentEditingRow.cells[0].innerText; |
|||
duration_time = parseInt(document.getElementById('duration_timeInput').value); |
|||
proportion = parseFloat(document.getElementById('proportionInput').value); |
|||
if(isNaN(duration_time) || isNaN(proportion) ){ |
|||
alert("请输入数字!"); |
|||
return; |
|||
} |
|||
if(proportion<=0 || proportion>=1){ |
|||
alert("占比阈值需要大于0,且小于1"); |
|||
return; |
|||
} |
|||
//提交数据
|
|||
const url = '/api/model/changecnf'; |
|||
const data = {"mid":mid,"duration_time":duration_time,"proportion":proportion}; |
|||
// 发送 POST 请求
|
|||
fetch(url, { |
|||
method: 'POST', // 指定请求方法为 POST
|
|||
headers: { |
|||
'Content-Type': 'application/json' // 设置请求头,告诉服务器请求体的数据类型为 JSON
|
|||
}, |
|||
body: JSON.stringify(data) // 将 JavaScript 对象转换为 JSON 字符串
|
|||
}) |
|||
.then(response => response.json()) // 将响应解析为 JSON
|
|||
.then(data => { |
|||
const istatus = data.status; |
|||
if(istatus === 0){ |
|||
alert(data.msg); |
|||
return; |
|||
} |
|||
else{ |
|||
//刷新列表
|
|||
fetchModelData(); |
|||
alert(data.msg); |
|||
$('#configMM').modal('hide'); |
|||
} |
|||
}) |
|||
.catch((error) => { |
|||
alert(`Error: ${error.message}`); |
|||
return; |
|||
}); |
|||
} |
|||
|
|||
//删除算法记录
|
|||
function deleteModel(row){ |
|||
if (confirm('确定删除此算法吗?')) { |
|||
mid = row.cells[0].innerText; |
|||
const url = '/api/model/del'; |
|||
const data = {"mid":mid}; |
|||
// 发送 POST 请求
|
|||
fetch(url, { |
|||
method: 'POST', // 指定请求方法为 POST
|
|||
headers: { |
|||
'Content-Type': 'application/json' // 设置请求头,告诉服务器请求体的数据类型为 JSON
|
|||
}, |
|||
body: JSON.stringify(data) // 将 JavaScript 对象转换为 JSON 字符串
|
|||
}) |
|||
.then(response => response.json()) // 将响应解析为 JSON
|
|||
.then(data => { |
|||
const istatus = data.status; |
|||
if(istatus === 0){ |
|||
alert(data.msg); |
|||
return; |
|||
} |
|||
else{ |
|||
//刷新列表
|
|||
row.remove(); |
|||
alert("删除算法成功!"); |
|||
} |
|||
}) |
|||
.catch((error) => { |
|||
alert(`Error: ${error.message}`); |
|||
return; |
|||
}); |
|||
|
|||
} |
|||
} |
|||
|
|||
//新增算法--保存按钮
|
|||
function addModel(){ |
|||
const btn = document.getElementById('saveButton_model'); |
|||
const fileInput = document.getElementById('uploadModelFile'); |
|||
const file = fileInput.files[0]; |
|||
const mName = document.getElementById('MNameInput').value; |
|||
|
|||
if (file && mName) { |
|||
btn.disabled = true; //不可点击
|
|||
const formData = new FormData(); |
|||
formData.append('file', file); |
|||
formData.append('mName', mName); |
|||
|
|||
fetch('/api/model/add', { |
|||
method: 'POST', |
|||
body: formData, |
|||
}) |
|||
.then(response => response.json()) |
|||
.then(data => { |
|||
const istatus = data.status; |
|||
alert(data.msg); |
|||
btn.disabled = false; |
|||
if(istatus == 1 ){ |
|||
fetchModelData(); |
|||
$('#addMM').modal('hide'); |
|||
} |
|||
}) |
|||
.catch(error => { |
|||
console.error('Error:', error); |
|||
alert('上传失败,请重试。'); |
|||
btn.disabled = false; |
|||
}); |
|||
} else { |
|||
alert('请填写算法名称并选择一个升级包进行上传。'); |
|||
btn.disabled = false; |
|||
} |
|||
} |
|||
|
|||
//关键字检索
|
|||
function searchModel(){ |
|||
try { |
|||
const modelName = document.getElementById('modelNameInput').value; |
|||
if(modelName===""){ |
|||
modelData = modelData_bak; |
|||
} |
|||
else{ |
|||
modelData = []; |
|||
modelData_bak.forEach((model) => { |
|||
if(model.name.includes(modelName)){ |
|||
modelData.push(model); |
|||
} |
|||
}); |
|||
} |
|||
// 渲染表格和分页控件
|
|||
currentPage = 1; // 重置当前页为第一页
|
|||
renderTable(); |
|||
renderPagination(); |
|||
} catch (error) { |
|||
console.error('Error performing search:', error); |
|||
} |
|||
} |
@ -0,0 +1,82 @@ |
|||
function getWsUrl() { |
|||
const protocol = window.location.protocol === "https:" ? "wss" : "ws"; |
|||
const host = window.location.host; // 包含主机和端口(如 localhost:5000)
|
|||
const path = "/api/ws"; // 你的 WebSocket 路由
|
|||
return `${protocol}://${host}${path}`; |
|||
} |
|||
|
|||
function initWebSocket() { |
|||
// 替换成你实际的 WebSocket 地址,例如 ws://localhost:5000/ws
|
|||
wsUrl = getWsUrl(); |
|||
const ws = new WebSocket(wsUrl); |
|||
// 设置接收二进制数据为 ArrayBuffer
|
|||
ws.binaryType = "arraybuffer"; |
|||
|
|||
ws.onopen = function() { |
|||
console.log("WebSocket 已连接"); |
|||
// 发送登录数据包,假设格式为 JSON(后端可以根据需要解析此包) --暂时默认0只有一个
|
|||
ws.send(JSON.stringify({ user_id: 0 })); |
|||
}; |
|||
|
|||
ws.onmessage = function(event) { |
|||
// 解析收到的二进制数据
|
|||
const buffer = event.data; |
|||
const dataView = new DataView(buffer); |
|||
// 读取前四个字节,验证魔数 "TFTF"
|
|||
let magic = ""; |
|||
for (let i = 0; i < 4; i++) { |
|||
magic += String.fromCharCode(dataView.getUint8(i)); |
|||
} |
|||
if (magic !== "TFTF") { |
|||
console.error("收到非法格式数据:", magic); |
|||
return; |
|||
} |
|||
// 读取数据头:接下来的 8 字节分别为 idata_type 和 idata_len(均为 32 位无符号整数,假设大端)
|
|||
const idata_type = dataView.getUint32(4, false); |
|||
const idata_len = dataView.getUint32(8, false); |
|||
|
|||
// 数据体从偏移量 12 开始,长度为 idata_len
|
|||
const bodyBytes = new Uint8Array(buffer, 12, idata_len); |
|||
const decoder = new TextDecoder("utf-8"); |
|||
const bodyText = decoder.decode(bodyBytes); |
|||
// 假设数据体为 JSON 字符串
|
|||
let bodyData; |
|||
try { |
|||
bodyData = JSON.parse(bodyText); |
|||
} catch (error) { |
|||
console.error("解析数据体出错:", error); |
|||
return; |
|||
} |
|||
|
|||
// 针对 idata_type 为 1 的处理:更新节点
|
|||
if(idata_type === 0){ |
|||
console.log(bodyData); |
|||
} |
|||
else if (idata_type === 1) {//type-1为更新节点的node_workstatus
|
|||
// bodyData 应该包含 node_path 和 node_workstatus 两个字段
|
|||
updateTreeNode(bodyData.node_path, bodyData.node_workstatus);//node_tree.js
|
|||
} |
|||
else if(idata_type === 2){ |
|||
if(cur_task_id === 0){return;} |
|||
// 清空选中状态
|
|||
selectedNodeData = null; |
|||
//刷新界面内容
|
|||
update_select_node_data_show("-","-","-","-","-",false) |
|||
// 重新加载节点树数据
|
|||
loadNodeTree(cur_task_id); |
|||
} |
|||
else { |
|||
console.error("未知的数据类型"); |
|||
} |
|||
}; |
|||
|
|||
ws.onerror = function(error) { |
|||
console.error("WebSocket 错误:", error); |
|||
}; |
|||
|
|||
ws.onclose = function() { |
|||
console.log("WebSocket 连接关闭"); |
|||
}; |
|||
|
|||
return ws; |
|||
} |
@ -0,0 +1,930 @@ |
|||
// 全局变量,用于保存当前选中的节点数据
|
|||
let selectedNodeData = null; |
|||
|
|||
/** |
|||
* 根据节点数据递归生成树形结构(返回 <li> 元素) |
|||
* 假设节点数据格式: |
|||
* { |
|||
* "node_name":node.name, |
|||
* "node_path":node.path, |
|||
* "node_status":node.status, |
|||
* "node_bwork":node.bwork, |
|||
* "node_vultype":node.vul_type, |
|||
* "node_vulgrade":node.vul_grade, |
|||
* children: [ { ... }, { ... } ] |
|||
* } |
|||
*/ |
|||
function generateTreeHTML(nodeData) { |
|||
const li = document.createElement("li"); |
|||
const nodeSpan = document.createElement("span"); |
|||
nodeSpan.className = "tree-node"; |
|||
//设置data属性
|
|||
nodeSpan.setAttribute("data-node_name", nodeData.node_name); |
|||
nodeSpan.setAttribute("data-node_path", nodeData.node_path); |
|||
nodeSpan.setAttribute("data-node_status", nodeData.node_status); |
|||
nodeSpan.setAttribute("data-node_bwork", nodeData.node_bwork); |
|||
nodeSpan.setAttribute("data-node_vultype", nodeData.node_vultype); |
|||
nodeSpan.setAttribute("data-node_vulgrade", nodeData.node_vulgrade || ""); |
|||
nodeSpan.setAttribute("data-node_workstatus",nodeData.node_workstatus); |
|||
if(nodeData.node_workstatus ===0){ |
|||
nodeSpan.classList.add("no-work"); |
|||
}else { |
|||
nodeSpan.classList.remove("no-work"); |
|||
} |
|||
// 根据漏洞级别添加样式
|
|||
if (nodeData.node_vulgrade) { |
|||
nodeSpan.classList.remove("no-work"); |
|||
if (nodeData.node_vulgrade === "低危") { |
|||
nodeSpan.classList.add("vul-low"); |
|||
} else if (nodeData.node_vulgrade === "中危") { |
|||
nodeSpan.classList.add("vul-medium"); |
|||
} else if (nodeData.node_vulgrade === "高危") { |
|||
nodeSpan.classList.add("vul-high"); |
|||
} |
|||
} |
|||
// 创建容器用于存放切换图标与文本
|
|||
const container = document.createElement("div"); |
|||
container.className = "node-container"; |
|||
// 如果有子节点,则添加切换图标
|
|||
if (nodeData.children && nodeData.children.length > 0) { |
|||
const toggleIcon = document.createElement("span"); |
|||
toggleIcon.className = "toggle-icon"; |
|||
toggleIcon.textContent = "-"; // 默认展开时显示“-”
|
|||
container.appendChild(toggleIcon); |
|||
} |
|||
//节点文本
|
|||
const textSpan = document.createElement("span"); |
|||
textSpan.className = "node-text"; |
|||
textSpan.textContent = nodeData.node_name; |
|||
container.appendChild(textSpan); |
|||
nodeSpan.appendChild(container); |
|||
li.appendChild(nodeSpan); |
|||
//如果存在子节点,递归生成子节点列表
|
|||
if (nodeData.children && nodeData.children.length > 0) { |
|||
const ul = document.createElement("ul"); |
|||
nodeData.children.forEach((child) => { |
|||
ul.appendChild(generateTreeHTML(child)); |
|||
}); |
|||
li.appendChild(ul); |
|||
} |
|||
return li; |
|||
} |
|||
|
|||
// 绑定所有节点的点击事件
|
|||
function bindTreeNodeEvents() { |
|||
document.querySelectorAll(".tree-node").forEach((el) => { |
|||
el.addEventListener("click", (event) => { |
|||
// 阻止事件冒泡,避免点击时展开折叠影响
|
|||
event.stopPropagation(); |
|||
// 清除之前选中的节点样式
|
|||
document |
|||
.querySelectorAll(".tree-node.selected") |
|||
.forEach((node) => node.classList.remove("selected")); |
|||
// 当前节点标记为选中
|
|||
el.classList.add("selected"); |
|||
// 读取 data 属性更新右侧显示
|
|||
const nodeName = el.getAttribute("data-node_name"); |
|||
const status = el.getAttribute("data-node_status"); |
|||
const nodepath = el.getAttribute("data-node_path"); |
|||
const nodebwork = el.getAttribute("data-node_bwork"); |
|||
const vulType = el.getAttribute("data-node_vultype"); |
|||
const vulLevel = el.getAttribute("data-node_vulgrade"); |
|||
const workstatus = el.getAttribute("data-node_workstatus"); |
|||
//selectedNodeData = { nodeName, status, vulType, vulLevel,nodepath,nodebwork };
|
|||
// 示例中默认填充
|
|||
selectedNodeData = { |
|||
node_name: nodeName, |
|||
node_path: nodepath, |
|||
status: status, |
|||
node_bwork: nodebwork, |
|||
vul_type: vulType, |
|||
vul_grade: vulLevel || "-", |
|||
workstatus: workstatus |
|||
}; |
|||
//刷新界面内容
|
|||
update_select_node_data_show(nodeName,status,vulType,vulLevel,workstatus,nodebwork) |
|||
}); |
|||
// 双击事件:展开/收缩子节点区域
|
|||
el.addEventListener("dblclick", (event) => { |
|||
event.stopPropagation(); |
|||
// 找到该节点下的 <ul> 子节点列表
|
|||
const parentLi = el.parentElement; |
|||
const childUl = parentLi.querySelector("ul"); |
|||
if (childUl) { |
|||
// 切换 collapsed 类,控制 display
|
|||
childUl.classList.toggle("collapsed"); |
|||
// 更新切换图标
|
|||
const toggleIcon = el.querySelector(".toggle-icon"); |
|||
if (toggleIcon) { |
|||
toggleIcon.textContent = childUl.classList.contains("collapsed") |
|||
? "+" |
|||
: "-"; |
|||
} |
|||
} |
|||
}); |
|||
}); |
|||
} |
|||
|
|||
// 动态加载节点树数据
|
|||
async function loadNodeTree(task_id) { |
|||
// 清空选中状态
|
|||
selectedNodeData = null; |
|||
//刷新界面内容
|
|||
update_select_node_data_show("-","-","-","-","-",false) |
|||
try { |
|||
const res = await fetch("/api/task/gettree", { |
|||
method: "POST", |
|||
headers: { "Content-Type": "application/json" }, |
|||
body: JSON.stringify({ task_id }), //task_id:task_id
|
|||
}); |
|||
if (!res.ok) { |
|||
const errorData = await res.json(); |
|||
throw new Error(errorData.error || `HTTP错误 ${res.status}`); |
|||
} |
|||
const data = await res.json(); |
|||
const treeData = data.tree; |
|||
if (!treeData) { |
|||
document.getElementById("treeContent").innerHTML = |
|||
"<p>无节点数据</p>"; |
|||
return; |
|||
} |
|||
|
|||
// 创建一个 <ul> 作为树的根容器
|
|||
const ul = document.createElement("ul"); |
|||
ul.className = "tree-root-ul"; |
|||
ul.appendChild(generateTreeHTML(treeData)); |
|||
// 替换节点树容器的内容
|
|||
const container = document.getElementById("treeContent"); |
|||
container.innerHTML = ""; |
|||
container.appendChild(ul); |
|||
// 绑定节点点击事件
|
|||
bindTreeNodeEvents(); |
|||
} catch (error) { |
|||
console.error("加载节点树失败:", error); |
|||
document.getElementById("treeContent").innerHTML = "<p>加载节点树失败</p>"; |
|||
} |
|||
} |
|||
|
|||
function getWorkStatus_Str(workstatus){ |
|||
strworkstatus = "" |
|||
switch (workstatus){ |
|||
case 0: |
|||
strworkstatus = "无待执行任务"; |
|||
break; |
|||
case 1: |
|||
strworkstatus = "待执行指令中"; |
|||
break; |
|||
case 2: |
|||
strworkstatus = "指令执行中"; |
|||
break; |
|||
case 3: |
|||
strworkstatus = "待提交llm中"; |
|||
break; |
|||
case 4: |
|||
strworkstatus = "提交llm中"; |
|||
break; |
|||
default: |
|||
strworkstatus = "-" |
|||
} |
|||
return strworkstatus |
|||
} |
|||
|
|||
//根据web端过来的数据,更新节点的工作状态
|
|||
function updateTreeNode(node_path, node_workstatus) { |
|||
// 根据 node_path 查找对应节点(假设每个 .tree-node 上设置了 data-node_path 属性)
|
|||
const nodeEl = document.querySelector(`.tree-node[data-node_path="${node_path}"]`); |
|||
if (nodeEl) { |
|||
// 更新 DOM 属性(属性值均为字符串)
|
|||
nodeEl.setAttribute("data-node_workstatus", node_workstatus); |
|||
//判断是否需要更新界面
|
|||
if(selectedNodeData){ |
|||
if(node_path === selectedNodeData.node_path){ //只有是当前选中节点才更新数据
|
|||
selectedNodeData.workstatus = node_workstatus; |
|||
strnew = getWorkStatus_Str(node_workstatus); |
|||
document.getElementById("node_workstatus").textContent = strnew; |
|||
} |
|||
} |
|||
} else { |
|||
console.warn(`未找到节点 ${node_path}`); |
|||
} |
|||
} |
|||
|
|||
//刷新节点的数据显示
|
|||
function update_select_node_data_show(nodeName,testStatus,vulType,vulLevel,workStatus,nodebwork){ |
|||
document.getElementById("nodeName").textContent = nodeName; |
|||
document.getElementById("testStatus").textContent = testStatus; |
|||
document.getElementById("node_vulType").textContent = vulType; |
|||
document.getElementById("node_vulLevel").textContent = vulLevel; |
|||
str_workStatus = getWorkStatus_Str(Number(workStatus)); |
|||
document.getElementById("node_workstatus").textContent = str_workStatus; |
|||
if(nodebwork==="true"){ |
|||
document.getElementById("node_bwork").textContent = "执行中"; |
|||
document.getElementById("btnToggleStatus").textContent = "暂停"; |
|||
}else { |
|||
document.getElementById("node_bwork").textContent = "暂停中"; |
|||
document.getElementById("btnToggleStatus").textContent = "继续"; |
|||
} |
|||
setNodeBtnStatus(); |
|||
} |
|||
|
|||
//节点按钮的状态控制
|
|||
function setNodeBtnStatus(){ |
|||
const btn_TS = document.getElementById("btnToggleStatus"); |
|||
const btn_NodeStep = document.getElementById("btnNodeStep"); |
|||
const btn_VI = document.getElementById("btnViewInstr"); |
|||
const btn_VM = document.getElementById("btnViewMsg"); |
|||
const btn_AI = document.getElementById("btnAddInfo"); |
|||
const btn_AC = document.getElementById("btnAddChild"); |
|||
if(!selectedNodeData){ |
|||
//没有选择node,按钮全部置不可用
|
|||
btn_TS.disabled = true; |
|||
btn_TS.classList.add("disabled-btn"); |
|||
btn_NodeStep.disabled = true; |
|||
btn_NodeStep.classList.add("disabled-btn"); |
|||
btn_VI.disabled = true; |
|||
btn_VI.classList.add("disabled-btn"); |
|||
btn_VM.disabled = true; |
|||
btn_VM.classList.add("disabled-btn"); |
|||
btn_AI.disabled = true; |
|||
btn_AI.classList.add("disabled-btn"); |
|||
btn_AC.disabled = true; |
|||
btn_AC.classList.add("disabled-btn"); |
|||
} |
|||
else{ |
|||
//5个可用
|
|||
btn_TS.disabled = false; |
|||
btn_TS.classList.remove("disabled-btn"); |
|||
btn_VI.disabled = false; |
|||
btn_VI.classList.remove("disabled-btn"); |
|||
btn_VM.disabled = false; |
|||
btn_VM.classList.remove("disabled-btn"); |
|||
btn_AI.disabled = false; |
|||
btn_AI.classList.remove("disabled-btn"); |
|||
btn_AC.disabled = false; |
|||
btn_AC.classList.remove("disabled-btn"); |
|||
if(cur_task.taskStatus === 1 && cur_task.workType === 0 && selectedNodeData.node_bwork==="true"){ |
|||
btn_NodeStep.disabled = false; |
|||
btn_NodeStep.classList.remove("disabled-btn"); |
|||
} |
|||
else{ |
|||
btn_NodeStep.disabled = true; |
|||
btn_NodeStep.classList.add("disabled-btn"); |
|||
} |
|||
} |
|||
} |
|||
|
|||
// 刷新按钮事件绑定
|
|||
document.getElementById("btnRefresh").addEventListener("click", () => { |
|||
// 重新加载节点树数据
|
|||
loadNodeTree(cur_task_id); |
|||
}); |
|||
|
|||
// 按钮事件:当未选中节点时提示
|
|||
function checkSelectedNode() { |
|||
if (!selectedNodeData) { |
|||
alert("请先选择节点"); |
|||
return false; |
|||
} |
|||
return true; |
|||
} |
|||
|
|||
//节点-暂停/继续,bwork控制
|
|||
document.getElementById("btnToggleStatus").addEventListener("click", () => { |
|||
if (!checkSelectedNode()) return; |
|||
// bwork的状态已后端状态为准,只做切换
|
|||
update_node_bwork(cur_task_id,selectedNodeData.node_path); |
|||
}); |
|||
async function update_node_bwork(task_id,node_path){ |
|||
try { |
|||
const res = await fetch("/api/task/nodecontrol", { |
|||
method: "POST", |
|||
headers: { "Content-Type": "application/json" }, |
|||
body: JSON.stringify({ task_id,node_path }), //task_id:task_id
|
|||
}); |
|||
if (!res.ok) { |
|||
const errorData = await res.json(); |
|||
throw new Error(errorData.error || `HTTP错误 ${res.status}`); |
|||
} |
|||
//修改成功
|
|||
const data = await res.json(); |
|||
const newbwork = data.newbwork; |
|||
//更新数据
|
|||
const selectedEl = document.querySelector(".tree-node.selected"); |
|||
if (selectedEl) { |
|||
selectedEl.setAttribute("data-node_bwork", newbwork); |
|||
selectedNodeData.node_bwork = newbwork; |
|||
} |
|||
//刷新界面
|
|||
const btn_NodeStep = document.getElementById("btnNodeStep"); |
|||
if(newbwork){ |
|||
document.getElementById("node_bwork").textContent ="执行中"; |
|||
document.getElementById("btnToggleStatus").textContent = "暂停"; |
|||
if(cur_task.taskStatus === 1 && cur_task.workType === 0){ |
|||
btn_NodeStep.disabled = false; |
|||
btn_NodeStep.classList.remove("disabled-btn"); |
|||
} |
|||
}else { |
|||
document.getElementById("node_bwork").textContent = "暂停中"; |
|||
document.getElementById("btnToggleStatus").textContent = "继续"; |
|||
btn_NodeStep.disabled = true; |
|||
btn_NodeStep.classList.add("disabled-btn"); |
|||
} |
|||
}catch (error) { |
|||
alert("修改节点的bwork失败:", error); |
|||
} |
|||
} |
|||
|
|||
//节点-单步工作
|
|||
document.getElementById("btnNodeStep").addEventListener("click", () => { |
|||
if (!checkSelectedNode()) return; |
|||
node_one_step(cur_task_id,selectedNodeData.node_path); |
|||
}); |
|||
async function node_one_step(task_id,node_path){ |
|||
try { |
|||
const res = await fetch("/api/task/nodestep", { |
|||
method: "POST", |
|||
headers: { "Content-Type": "application/json" }, |
|||
body: JSON.stringify({ task_id,node_path }), //task_id:task_id
|
|||
}); |
|||
if (!res.ok) { |
|||
const errorData = await res.json(); |
|||
throw new Error(errorData.error || `HTTP错误 ${res.status}`); |
|||
} |
|||
//修改成功
|
|||
const data = await res.json(); |
|||
const bsuccess = data.bsuccess; |
|||
if(bsuccess){ |
|||
alert("该节点任务已提交,请稍候查看执行结果!") |
|||
} |
|||
else{ |
|||
error = data.erroe; |
|||
alert("该节点单步失败!",error) |
|||
} |
|||
|
|||
}catch (error) { |
|||
alert("该节点单步失败,请联系管理员!", error); |
|||
} |
|||
} |
|||
|
|||
|
|||
|
|||
//----------------------查看指令modal----------------------------
|
|||
let doneInstrs = []; // 已执行指令的所有数据
|
|||
let todoInstrs = []; // 待执行指令的所有数据
|
|||
let donePage = 1; // 已执行指令当前页
|
|||
let todoPage = 1; // 待执行指令当前页
|
|||
|
|||
document.getElementById("msgModal").addEventListener("hidden.bs.modal", () => { |
|||
document.activeElement.blur(); // 清除当前焦点
|
|||
}); |
|||
|
|||
document.getElementById("btnViewInstr").addEventListener("click", () => { |
|||
if (!checkSelectedNode()) return; |
|||
openInstrModal() |
|||
}); |
|||
// 打开对话框函数
|
|||
function openInstrModal() { |
|||
const modalEl = document.getElementById("instrModal"); |
|||
// 假设用 Bootstrap 5 的 Modal 组件
|
|||
const instrModal = new bootstrap.Modal(modalEl, {keyboard: false}); |
|||
// 在打开 modal 时,先更新提示内容,将 loadingMsg 显示“请稍后,数据获取中…”
|
|||
const loadingMsg = document.getElementById("loadingMsg"); |
|||
if (loadingMsg) { |
|||
loadingMsg.textContent = "请稍后,数据获取中..."; |
|||
} |
|||
// 显示对话框
|
|||
instrModal.show(); |
|||
// 加载指令数据
|
|||
loadInstrData(); |
|||
} |
|||
|
|||
// 调用后端接口,获取指令数据
|
|||
async function loadInstrData() { |
|||
task_id = cur_task_id; |
|||
node_path = selectedNodeData.node_path; |
|||
try { |
|||
const res = await fetch("/api/task/nodegetinstr", { |
|||
method: "POST", |
|||
headers: { "Content-Type": "application/json" }, |
|||
body: JSON.stringify({task_id,node_path}), |
|||
}); |
|||
if (!res.ok) { |
|||
const errorData = await res.json(); |
|||
throw new Error(errorData.error || `HTTP错误 ${res.status}`); |
|||
} |
|||
const data = await res.json(); |
|||
// 数据获取成功后,清除加载提示
|
|||
const loadingMsg = document.getElementById("loadingMsg"); |
|||
if (loadingMsg) { |
|||
loadingMsg.style.display = "none"; // 或者清空其 innerHTML
|
|||
} |
|||
doneInstrs = data.doneInstrs || []; |
|||
todoInstrs = data.todoInstrs || []; |
|||
donePage = 1; |
|||
todoPage = 1; |
|||
renderDoneInstrTable(donePage); |
|||
renderTodoInstrTable(todoPage); |
|||
} catch (error) { |
|||
console.error("加载指令数据异常:", error); |
|||
} |
|||
} |
|||
|
|||
// 渲染已执行指令表格
|
|||
function renderDoneInstrTable(page) { |
|||
const tbody = document.getElementById("doneInstrTbody"); |
|||
// 计算起始索引
|
|||
const startIndex = (page - 1) * pageSize; |
|||
const endIndex = startIndex + pageSize; |
|||
const pageData = doneInstrs.slice(startIndex, endIndex); |
|||
//select instruction,start_time,result from task_result where task_id=%s and node_path=%s;
|
|||
tbody.innerHTML = ""; |
|||
// 插入行
|
|||
pageData.forEach((item, i) => { |
|||
const tr = document.createElement("tr"); |
|||
|
|||
// 第一列:序号
|
|||
const tdIndex = document.createElement("td"); |
|||
tdIndex.textContent = startIndex + i + 1; |
|||
tr.appendChild(tdIndex); |
|||
|
|||
// 第二列:指令内容
|
|||
const tdInstr = document.createElement("td"); |
|||
tdInstr.textContent = item[0]; |
|||
tr.appendChild(tdInstr); |
|||
|
|||
// 第三列:开始时间(如果没有则显示空字符串)
|
|||
const tdStartTime = document.createElement("td"); |
|||
tdStartTime.textContent = item[1] || ""; |
|||
tr.appendChild(tdStartTime); |
|||
|
|||
// 第四列:执行结果
|
|||
const tdResult = document.createElement("td"); |
|||
tdResult.textContent = item[2] || ""; |
|||
tr.appendChild(tdResult); |
|||
|
|||
tbody.appendChild(tr); |
|||
}); |
|||
|
|||
// 若不足 10 行,补空行
|
|||
for (let i = pageData.length; i < pageSize; i++) { |
|||
const tr = document.createElement("tr"); |
|||
tr.innerHTML = ` |
|||
<td> </td> |
|||
<td> </td> |
|||
<td> </td> |
|||
<td> </td> |
|||
`;
|
|||
tbody.appendChild(tr); |
|||
} |
|||
} |
|||
|
|||
// 渲染待执行指令表格
|
|||
function renderTodoInstrTable(page) { |
|||
const tbody = document.getElementById("todoInstrTbody"); |
|||
const startIndex = (page - 1) * pageSize; |
|||
const endIndex = startIndex + pageSize; |
|||
const pageData = todoInstrs.slice(startIndex, endIndex); |
|||
|
|||
tbody.innerHTML = ""; |
|||
pageData.forEach((item, i) => { |
|||
const tr = document.createElement("tr"); |
|||
const idx = startIndex + i + 1; |
|||
// 第一列:序号
|
|||
const tdIndex = document.createElement("td"); |
|||
tdIndex.textContent = idx; |
|||
tr.appendChild(tdIndex); |
|||
|
|||
// 第二列:指令文本内容(直接使用 textContent)
|
|||
const tdItem = document.createElement("td"); |
|||
tdItem.textContent = item; // 使用 textContent 避免 HTML 解析
|
|||
tr.appendChild(tdItem); |
|||
|
|||
// 第三列:复制和删除按钮
|
|||
const tdAction = document.createElement("td"); |
|||
// const btn_cp = document.createElement("button");
|
|||
// btn_cp.className = "btn btn-primary btn-sm";
|
|||
// btn_cp.textContent = "复制";
|
|||
// btn_cp.style.marginRight = "2px"; // 设置间隔
|
|||
// btn_cp.onclick = () => confirmCopyTodoInstr(idx - 1);
|
|||
// tdAction.appendChild(btn_cp);
|
|||
const btn = document.createElement("button"); |
|||
btn.className = "btn btn-danger btn-sm"; |
|||
btn.textContent = "删除"; |
|||
btn.onclick = () => confirmDeleteTodoInstr(idx - 1); |
|||
tdAction.appendChild(btn); |
|||
tr.appendChild(tdAction); |
|||
|
|||
tbody.appendChild(tr); |
|||
}); |
|||
|
|||
// 补空行
|
|||
for (let i = pageData.length; i < pageSize; i++) { |
|||
const tr = document.createElement("tr"); |
|||
tr.innerHTML = ` |
|||
<td> </td> |
|||
<td> </td> |
|||
<td> </td> |
|||
`;
|
|||
tbody.appendChild(tr); |
|||
} |
|||
} |
|||
|
|||
function confirmCopyTodoInstr(idx) { |
|||
// 从全局数组 todoInstrs 中获取指令文本
|
|||
const instruction = todoInstrs[idx]; |
|||
|
|||
if (navigator.clipboard && navigator.clipboard.writeText) { |
|||
navigator.clipboard.writeText(instruction) |
|||
.then(() => { |
|||
alert("已复制: " + instruction); |
|||
}) |
|||
.catch((err) => { |
|||
console.error("使用 Clipboard API 复制失败:", err); |
|||
fallbackCopyTextToClipboard(instruction); |
|||
}); |
|||
} else { |
|||
// 如果 clipboard API 不可用,则回退使用 execCommand 方法
|
|||
fallbackCopyTextToClipboard(instruction); |
|||
} |
|||
} |
|||
|
|||
function fallbackCopyTextToClipboard(text) { |
|||
// 创建 textarea 元素
|
|||
const textArea = document.createElement("textarea"); |
|||
textArea.value = text; |
|||
|
|||
// 使用 CSS 样式使其不可见,同时保证能够获得焦点
|
|||
textArea.style.position = "fixed"; // 避免页面滚动
|
|||
textArea.style.top = "0"; |
|||
textArea.style.left = "0"; |
|||
textArea.style.width = "2em"; |
|||
textArea.style.height = "2em"; |
|||
textArea.style.padding = "0"; |
|||
textArea.style.border = "none"; |
|||
textArea.style.outline = "none"; |
|||
textArea.style.boxShadow = "none"; |
|||
textArea.style.background = "transparent"; |
|||
|
|||
document.body.appendChild(textArea); |
|||
textArea.focus(); |
|||
textArea.select(); |
|||
|
|||
try { |
|||
const successful = document.execCommand('copy'); |
|||
if (successful) { |
|||
alert("已复制: " + text); |
|||
} else { |
|||
alert("复制失败,请手动复制!"); |
|||
} |
|||
} catch (err) { |
|||
console.error("Fallback: 无法复制", err); |
|||
alert("复制失败,请手动复制!"); |
|||
} |
|||
document.body.removeChild(textArea); |
|||
} |
|||
|
|||
|
|||
// 删除待执行指令,先确认
|
|||
function confirmDeleteTodoInstr(arrIndex) { |
|||
if (!confirm("确认删除该条待执行指令?")) return; |
|||
// arrIndex 在当前分页中的索引
|
|||
// 先算出全局索引
|
|||
const realIndex = (todoPage - 1) * pageSize + arrIndex; |
|||
const item = todoInstrs[realIndex]; |
|||
// 调用后端删除接口
|
|||
deleteTodoInstr(item); |
|||
} |
|||
|
|||
// 调用后端接口删除
|
|||
async function deleteTodoInstr(item) { |
|||
task_id = cur_task_id; |
|||
node_path = selectedNodeData.node_path; |
|||
try { |
|||
const res = await fetch("/api/task/delnodeinstr", { |
|||
method: "POST", |
|||
headers: { "Content-Type": "application/json" }, |
|||
body: JSON.stringify({ task_id,node_path,item }) |
|||
}); |
|||
if (!res.ok) { |
|||
alert(data.error || "删除失败"); |
|||
return; |
|||
} |
|||
const data = await res.json(); |
|||
if(data.bsuccess){ |
|||
// 删除成功,更新本地数据
|
|||
const idx = todoInstrs.findIndex(x => x.id === item.id); |
|||
if (idx !== -1) { |
|||
todoInstrs.splice(idx, 1); |
|||
} |
|||
// 重新渲染
|
|||
renderTodoInstrTable(todoPage); |
|||
//0需要更新work_status为无待执行任务的状态
|
|||
if(todoInstrs.length === 0){ |
|||
updateTreeNode(node_path, 0); |
|||
} |
|||
} |
|||
else{ |
|||
alert("指令删除失败",data.error) |
|||
} |
|||
} catch (error) { |
|||
console.error("删除指令异常:", error); |
|||
alert("删除指令异常,请联系管理员!"); |
|||
} |
|||
} |
|||
|
|||
// 分页事件
|
|||
document.getElementById("doneInstrPrev").addEventListener("click", (e) => { |
|||
e.preventDefault(); |
|||
if (donePage > 1) { |
|||
donePage--; |
|||
renderDoneInstrTable(donePage); |
|||
} |
|||
}); |
|||
document.getElementById("doneInstrNext").addEventListener("click", (e) => { |
|||
e.preventDefault(); |
|||
const maxPage = Math.ceil(doneInstrs.length / pageSize); |
|||
if (donePage < maxPage) { |
|||
donePage++; |
|||
renderDoneInstrTable(donePage); |
|||
} |
|||
}); |
|||
document.getElementById("todoInstrPrev").addEventListener("click", (e) => { |
|||
e.preventDefault(); |
|||
if (todoPage > 1) { |
|||
todoPage--; |
|||
renderTodoInstrTable(todoPage); |
|||
} |
|||
}); |
|||
document.getElementById("todoInstrNext").addEventListener("click", (e) => { |
|||
e.preventDefault(); |
|||
const maxPage = Math.ceil(todoInstrs.length / pageSize); |
|||
if (todoPage < maxPage) { |
|||
todoPage++; |
|||
renderTodoInstrTable(todoPage); |
|||
} |
|||
}); |
|||
|
|||
// 导出当前页数据
|
|||
document.getElementById("btnExport").addEventListener("click", () => { |
|||
// 判断当前是哪个 Tab
|
|||
const activeTab = document.querySelector("#instrTab button.nav-link.active"); |
|||
if (activeTab.id === "doneInstrTab") { |
|||
exportCurrentPage(doneInstrs, donePage, ["序号", "执行指令", "执行时间", "执行结果"]); |
|||
} else { |
|||
exportCurrentPage(todoInstrs, todoPage, ["序号", "待执行指令"]); |
|||
} |
|||
}); |
|||
|
|||
function exportCurrentPage(dataArr, page, headerArr) { |
|||
const startIndex = (page - 1) * pageSize; |
|||
const endIndex = startIndex + pageSize; |
|||
const pageData = dataArr.slice(startIndex, endIndex); |
|||
|
|||
// 在 CSV 的开头加上 BOM,用于 Excel 识别 UTF-8 编码
|
|||
let csvContent = "\uFEFF" + headerArr.join(",") + "\n"; |
|||
pageData.forEach((item, i) => { |
|||
const rowIndex = startIndex + i + 1; |
|||
if (headerArr.length === 4) { |
|||
// 已执行:序号,执行指令,执行时间,执行结果
|
|||
csvContent += rowIndex + "," + |
|||
(item.command || "") + "," + |
|||
(item.execTime || "") + "," + |
|||
(item.result || "") + "\n"; |
|||
} else { |
|||
// 待执行:序号,待执行指令
|
|||
csvContent += rowIndex + "," + (item.command || "") + "\n"; |
|||
} |
|||
}); |
|||
// 如果不足 pageSize 行,补足空行(根据列数进行适当补全)
|
|||
for (let i = pageData.length; i < pageSize; i++) { |
|||
// 根据 headerArr.length 来设置空行的格式
|
|||
if (headerArr.length === 4) { |
|||
csvContent += ",,,\n"; |
|||
} else { |
|||
csvContent += ",\n"; |
|||
} |
|||
} |
|||
|
|||
const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" }); |
|||
const url = URL.createObjectURL(blob); |
|||
const link = document.createElement("a"); |
|||
link.href = url; |
|||
link.download = "指令导出.csv"; |
|||
link.click(); |
|||
URL.revokeObjectURL(url); |
|||
} |
|||
|
|||
//---------------------查看MSGmodal------------------------------
|
|||
document.getElementById("msgModal").addEventListener("hidden.bs.modal", () => { |
|||
document.activeElement.blur(); // 清除当前焦点
|
|||
}); |
|||
|
|||
document.getElementById("btnViewMsg").addEventListener("click", () => { |
|||
if (!checkSelectedNode()) return; |
|||
openMsgModal(); |
|||
}); |
|||
|
|||
let submittedMsgs = []; // 存储已提交的 MSG 数据数组
|
|||
let pendingMsg = {}; // 存储待提交的 MSG 数据
|
|||
let submittedPage = 1; |
|||
|
|||
function openMsgModal(){ |
|||
// 显示 Modal(使用 Bootstrap 5 Modal)
|
|||
const msgModal = new bootstrap.Modal(document.getElementById("msgModal"), { keyboard: false }); |
|||
msgModal.show(); |
|||
|
|||
// 加载数据:调用后端接口 /api/task/getnodeinstr 或其他接口获取数据
|
|||
// 这里仅作示例使用模拟数据
|
|||
loadMsgData(); |
|||
} |
|||
|
|||
async function loadMsgData(){ |
|||
task_id = cur_task_id; |
|||
node_path = selectedNodeData.node_path; |
|||
try { |
|||
const res = await fetch("/api/task/nodegetmsg", { |
|||
method: "POST", |
|||
headers: { "Content-Type": "application/json" }, |
|||
body: JSON.stringify({task_id,node_path}), |
|||
}); |
|||
if (!res.ok) { |
|||
const errorData = await res.json(); |
|||
throw new Error(errorData.error || `HTTP错误 ${res.status}`); |
|||
} |
|||
const data = await res.json(); |
|||
submittedMsgs = data.submitted || []; |
|||
pendingMsg = data.pending || {}; //one_llm = {'llm_type': llm_type, 'result': str_res}
|
|||
|
|||
submittedPage = 1; |
|||
renderSubmittedTable(submittedPage); |
|||
// 填充待提交区域 ---
|
|||
document.getElementById("llmtype").value = pendingMsg.llm_type || "0"; |
|||
document.getElementById("pendingContent").value = pendingMsg.result || ""; |
|||
} catch (error) { |
|||
console.error("加载Msg数据异常:", error); |
|||
} |
|||
} |
|||
|
|||
function renderSubmittedTable(page){ |
|||
const tbody = document.getElementById("submittedTbody"); |
|||
const start = (page - 1) * pageSize; |
|||
const end = start + pageSize; |
|||
const pageData = submittedMsgs.slice(start, end); |
|||
tbody.innerHTML = ""; |
|||
pageData.forEach((item, index) => { |
|||
const tr = document.createElement("tr"); |
|||
|
|||
// 第一列:序号
|
|||
const tdIndex = document.createElement("td"); |
|||
tdIndex.textContent = start + index + 1; |
|||
tr.appendChild(tdIndex); |
|||
|
|||
// 第二列:角色
|
|||
const tdRole = document.createElement("td"); |
|||
tdRole.textContent = item.role; |
|||
tr.appendChild(tdRole); |
|||
|
|||
// 第三列:内容
|
|||
const tdContent = document.createElement("td"); |
|||
tdContent.textContent = item.content; |
|||
tr.appendChild(tdContent); |
|||
|
|||
tbody.appendChild(tr); |
|||
}); |
|||
// 不足 10 行时,补空行
|
|||
for(let i = pageData.length; i < pageSize; i++){ |
|||
const tr = document.createElement("tr"); |
|||
tr.innerHTML = `<td> </td><td> </td><td> </td>`; |
|||
tbody.appendChild(tr); |
|||
} |
|||
} |
|||
|
|||
// 分页按钮事件
|
|||
document.getElementById("submittedPrev").addEventListener("click", function(e){ |
|||
e.preventDefault(); |
|||
if(submittedPage > 1){ |
|||
submittedPage--; |
|||
renderSubmittedTable(submittedPage); |
|||
} |
|||
}); |
|||
document.getElementById("submittedNext").addEventListener("click", function(e){ |
|||
e.preventDefault(); |
|||
const maxPage = Math.ceil(submittedMsgs.length / pageSize); |
|||
if(submittedPage < maxPage){ |
|||
submittedPage++; |
|||
renderSubmittedTable(submittedPage); |
|||
} |
|||
}); |
|||
|
|||
// 导出功能:导出当前页已提交数据到 CSV
|
|||
document.getElementById("btnExportSubmitted").addEventListener("click", function(){ |
|||
exportSubmittedCurrentPage(); |
|||
}); |
|||
function exportSubmittedCurrentPage(){ |
|||
const start = (submittedPage - 1) * pageSize; |
|||
const end = start + pageSize; |
|||
const pageData = submittedMsgs.slice(start, end); |
|||
let csv = "\uFEFF" + "序号,角色,内容\n"; // 添加 BOM 防乱码
|
|||
pageData.forEach((item, i) => { |
|||
csv += `${start + i + 1},${item.role},${item.content}\n`; |
|||
}); |
|||
// 补空行
|
|||
for(let i = pageData.length; i < pageSize; i++){ |
|||
csv += ",,\n"; |
|||
} |
|||
const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" }); |
|||
const url = URL.createObjectURL(blob); |
|||
const a = document.createElement("a"); |
|||
a.href = url; |
|||
a.download = "已提交消息.csv"; |
|||
a.click(); |
|||
URL.revokeObjectURL(url); |
|||
} |
|||
|
|||
//新增请求指令的msg
|
|||
document.getElementById("btnNeedInstr").addEventListener("click",function(){ |
|||
if(selectedNodeData.workstatus === "0"){ |
|||
if (!confirm("是否确认要为该节点新增请求指令的信息?")) return; |
|||
// 获取用户在待提交区输入的新值
|
|||
newType = 3; |
|||
newContent = "请针对该节点信息,生成下一步渗透测试指令。"; |
|||
//提交到后端更新
|
|||
bsuccess = putnewmsg(newType,newContent); |
|||
if(bsuccess){ |
|||
//更新缓存
|
|||
selectedNodeData.workstatus = "3"; |
|||
const nodeEl = document.querySelector(`.tree-node[data-node_path="${selectedNodeData.node_path}"]`); |
|||
if (nodeEl) { |
|||
// 更新 DOM 属性(属性值均为字符串)
|
|||
nodeEl.setAttribute("data-node_workstatus", "3"); |
|||
nodeEl.classList.remove("no-work"); |
|||
} |
|||
//更新界面
|
|||
strnew = getWorkStatus_Str(3); |
|||
document.getElementById("node_workstatus").textContent = strnew; |
|||
document.getElementById("llmtype").value = newType; |
|||
document.getElementById("pendingContent").value = newContent; |
|||
} |
|||
}else { |
|||
alert("只允许在-无待执行任务状态下新增请求指令的msg!") |
|||
} |
|||
}); |
|||
// 保存待提交内容修改
|
|||
document.getElementById("btnSavePending").addEventListener("click", function(){ |
|||
if(selectedNodeData.workstatus === "3"){ |
|||
if (!confirm("是否确认要保存对该节点待提交信息的修改?")) return; |
|||
// 获取用户在待提交区输入的新值
|
|||
const newType = document.getElementById("llmtype").value; |
|||
const newContent = document.getElementById("pendingContent").value; |
|||
//提交到后端更新
|
|||
putnewmsg(newType,newContent); |
|||
}else { |
|||
alert("只允许在-待提交llm状态下修改或新增msg!") |
|||
} |
|||
|
|||
|
|||
}); |
|||
async function putnewmsg(llmtype,content){ |
|||
task_id = cur_task_id; |
|||
node_path = selectedNodeData.node_path; |
|||
try { |
|||
const res = await fetch("/api/task/nodeupdatemsg", { |
|||
method: "POST", |
|||
headers: { "Content-Type": "application/json" }, |
|||
body: JSON.stringify({task_id,node_path,llmtype,content}), |
|||
}); |
|||
if (!res.ok) { |
|||
const errorData = await res.json(); |
|||
throw new Error(errorData.error || `HTTP错误 ${res.status}`); |
|||
} |
|||
const data = await res.json(); |
|||
bsuccess = data.bsuccess; |
|||
if(bsuccess){ |
|||
alert("修改成功") |
|||
pendingMsg.llmtype = llmtype; |
|||
pendingMsg.content = content; |
|||
return true; |
|||
} |
|||
else{ |
|||
alert("修改失败:",data.error) |
|||
return false; |
|||
} |
|||
} catch (error) { |
|||
console.error("加载Msg数据异常:", error); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
|
|||
//---------------------添加信息modal------------------------------
|
|||
document.getElementById("btnAddInfo").addEventListener("click", () => { |
|||
if (!checkSelectedNode()) return; |
|||
alert("该功能实现中..."); |
|||
}); |
|||
document.getElementById("btnAddChild").addEventListener("click", () => { |
|||
if (!checkSelectedNode()) return; |
|||
alert("该功能实现中..."); |
|||
}); |
|||
|
|||
// 页面加载完成后,加载节点树
|
|||
//document.addEventListener("DOMContentLoaded", loadNodeTree);
|
@ -0,0 +1,534 @@ |
|||
let task_list = [] |
|||
let cur_task = null //当前选择的task--用于修改缓存时使用
|
|||
let cur_task_id = 0 //当前选择的cur_task_id
|
|||
let ws = null |
|||
|
|||
// 页面卸载时断开连接
|
|||
window.addEventListener("beforeunload", function() { |
|||
if (ws) { |
|||
ws.close(); |
|||
ws =null; |
|||
} |
|||
task_list = [] |
|||
cur_task = null; |
|||
cur_task_id = 0; |
|||
}); |
|||
|
|||
// 页面加载完成后调用接口获取任务列表数据
|
|||
document.addEventListener("DOMContentLoaded", async () => { |
|||
//建立wbsocket
|
|||
initWebSocket() |
|||
//获取左侧任务列表
|
|||
getTasklist(); |
|||
//当前选中的数据要清空
|
|||
cur_task = null; |
|||
cur_task_id = 0; |
|||
//任务基本信息界面初始化
|
|||
document.getElementById("detailTestTarget").textContent = "-"; |
|||
document.getElementById("detailTestStatus").textContent = "-"; |
|||
document.getElementById("detailSafeStatus").textContent = '-'; |
|||
//单选按钮
|
|||
set_radio_selection('testMode', 'auto'); |
|||
setSetpBtnStatus(); |
|||
//节点树界面初始化
|
|||
update_select_node_data_show("-","-","-","-","-",false) |
|||
//单选按钮点击事件 ------ 是不是更规范的编程方式,应该把控件事件的添加都放页面加载完成后处理
|
|||
const autoRadio = document.getElementById("autoMode"); |
|||
const manualRadio = document.getElementById("manualMode"); |
|||
autoRadio.addEventListener("click", () => updateTestMode(1)); |
|||
manualRadio.addEventListener("click", () => updateTestMode(0)); |
|||
//tab页点击刷新数据
|
|||
document.getElementById("testInstructionsTab").addEventListener("click",()=>searchInstructions()); |
|||
document.getElementById("vulnerabilitiesTab").addEventListener("click",()=>searchVulnerabilities()); |
|||
//指令和漏洞数据的导出按钮点击事件
|
|||
document.getElementById("instrExportBtn").addEventListener("click",()=>ExportInstructions()); |
|||
document.getElementById("vulExportBtn").addEventListener("click",()=>ExportVuls()); |
|||
|
|||
|
|||
}); |
|||
|
|||
//----------------------左侧任务列表-----------------------
|
|||
function getstrsafeR(safeRank){ |
|||
if(safeRank === 0){ |
|||
safeR = "安全"; |
|||
} |
|||
else{ |
|||
safeR = "存在风险"; |
|||
} |
|||
return safeR; |
|||
} |
|||
function getstrTaskS(taskStatus){ |
|||
if(taskStatus === 0){ |
|||
taskS = "暂停中"; |
|||
}else if(taskStatus === 1){ |
|||
taskS = "执行中"; |
|||
}else { |
|||
taskS = "已完成"; |
|||
} |
|||
return taskS; |
|||
} |
|||
|
|||
async function getTasklist(){ |
|||
try { |
|||
const res = await fetch("/api/task/getlist"); |
|||
if (!res.ok) { |
|||
const errorData = await res.json(); |
|||
throw new Error(errorData.error || `HTTP错误 ${res.status}`); |
|||
} |
|||
const data = await res.json(); |
|||
task_list = data.tasks |
|||
const taskList = document.getElementById("taskList"); |
|||
taskList.innerHTML = ""; // 清空“加载中”提示
|
|||
// 遍历任务数组,生成任务项
|
|||
task_list.forEach((task) => { |
|||
const taskItem = document.createElement("div"); |
|||
taskItem.dataset.taskID =task.taskID //在taskItem添加数据属性,这是关联数据的第二种方法,selectedEl.dataset.taskId; 感觉比cur_task更好
|
|||
taskItem.className = "task-item"; |
|||
|
|||
// 第一行:测试目标
|
|||
const targetDiv = document.createElement("div"); |
|||
targetDiv.className = "task-target"; |
|||
targetDiv.textContent = task.testTarget; |
|||
taskItem.appendChild(targetDiv); |
|||
|
|||
// 第二行:测试状态,带缩进
|
|||
const statusDiv = document.createElement("div"); |
|||
statusDiv.className = "task-status"; |
|||
let safeR = getstrsafeR(task.safeRank); |
|||
let taskS = getstrTaskS(task.taskStatus); |
|||
statusDiv.textContent = `${taskS}-${safeR}`; |
|||
taskItem.appendChild(statusDiv); |
|||
|
|||
// 可绑定点击事件:点击任务项更新右侧详情
|
|||
taskItem.addEventListener("click", () => { |
|||
// 取消所有任务项的选中状态
|
|||
document.querySelectorAll(".task-item.selected").forEach(item => { |
|||
item.classList.remove("selected"); |
|||
}); |
|||
// 给当前点击的任务项添加选中状态
|
|||
taskItem.classList.add("selected"); |
|||
//执行业务代码 --参数当前选中task的数据
|
|||
cur_task = task; |
|||
selected_task_item() |
|||
}); |
|||
taskList.appendChild(taskItem); |
|||
}); |
|||
} catch (error) { |
|||
console.error("加载任务列表出错:", error); |
|||
document.getElementById("taskList").innerHTML = "<p>加载任务列表失败!</p>"; |
|||
} |
|||
} |
|||
|
|||
//选中tasklist--更新界面数据
|
|||
function selected_task_item(){ |
|||
if(cur_task_id === cur_task.taskID) return; |
|||
cur_task_id = cur_task.taskID; |
|||
//按钮状态更新
|
|||
actionButton = document.getElementById("actionButton"); |
|||
if(cur_task.taskStatus === 0){ |
|||
actionButton.textContent = "继续"; |
|||
}else if(cur_task.taskStatus === 1){ |
|||
actionButton.textContent = "暂停"; |
|||
}else { |
|||
} |
|||
//基本信息
|
|||
let safeR = getstrsafeR(cur_task.safeRank); |
|||
let taskS = getstrTaskS(cur_task.taskStatus); |
|||
document.getElementById("detailTestTarget").textContent = cur_task.testTarget; |
|||
document.getElementById("detailTestStatus").textContent = taskS; |
|||
document.getElementById("detailSafeStatus").textContent = safeR; |
|||
//单选按钮
|
|||
if(cur_task.workType === 0){ //人工
|
|||
set_radio_selection('testMode', 'manual'); |
|||
}else { //1-自动
|
|||
set_radio_selection('testMode', 'auto'); |
|||
} |
|||
//更新单步按钮
|
|||
setSetpBtnStatus() |
|||
//加载任务其他信息--node_tree.js
|
|||
loadNodeTree(cur_task_id); |
|||
//加载测试指令和漏洞数据
|
|||
searchInstructions(); |
|||
searchVulnerabilities(); |
|||
// renderTableRows(document.querySelector("#instrTable tbody"), []);
|
|||
// renderTableRows(document.querySelector("#vulTable tbody"), []);
|
|||
} |
|||
|
|||
//--------------------任务基本信息区域--------------------
|
|||
//单选按钮--测试模式修改
|
|||
async function updateTestMode(mode){ //0-人工,1-自动
|
|||
if(cur_task){ |
|||
if(cur_task.workType !== mode){ |
|||
if(cur_task.taskStatus === 1){ |
|||
alert("执行状态,不允许修改测试模式!"); |
|||
if( cur_task.workType === 0){ //人工
|
|||
set_radio_selection('testMode', 'manual'); |
|||
}else { //1-自动
|
|||
set_radio_selection('testMode', 'auto'); |
|||
} |
|||
} |
|||
else {//不是执行状态,可以修改测试模式
|
|||
try { |
|||
const res = await fetch("/api/task/taskworktype", { |
|||
method: "POST", |
|||
headers: { "Content-Type": "application/json" }, |
|||
body: JSON.stringify({ cur_task_id,mode }), //task_id:task_id
|
|||
}); |
|||
// 新增状态码校验
|
|||
if (!res.ok) { |
|||
const errorData = await res.json(); |
|||
throw new Error(errorData.error || `HTTP错误 ${res.status}`); |
|||
} |
|||
const data = await res.json(); |
|||
bsuccess = data.bsuccess; |
|||
if(bsuccess){ |
|||
cur_task.workType = mode; //更新前端缓存
|
|||
//更新单步按钮状态
|
|||
setSetpBtnStatus() |
|||
}else { |
|||
alert("修改测试模失败!") |
|||
if( cur_task.workType === 0){ //人工 修改失败还原单选按钮点击状态
|
|||
set_radio_selection('testMode', 'manual'); |
|||
}else { //1-自动
|
|||
set_radio_selection('testMode', 'auto'); |
|||
} |
|||
} |
|||
} catch (error) { |
|||
console.error("控制任务状态异常:", error); |
|||
alert("控制任务状态异常:",error); |
|||
if( cur_task.workType === 0){ //人工 修改失败还原单选按钮点击状态
|
|||
set_radio_selection('testMode', 'manual'); |
|||
}else { //1-自动
|
|||
set_radio_selection('testMode', 'auto'); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
|
|||
} |
|||
|
|||
//点击暂停/继续按钮
|
|||
document.getElementById("actionButton").addEventListener("click",() => { |
|||
controlTask(); |
|||
}); |
|||
async function controlTask(){ |
|||
if(cur_task_id === 0){ |
|||
alert("请先选择一个任务!") |
|||
return |
|||
} |
|||
try { |
|||
const res = await fetch("/api/task/taskcontrol", { |
|||
method: "POST", |
|||
headers: { "Content-Type": "application/json" }, |
|||
body: JSON.stringify({ cur_task_id }), //task_id:task_id
|
|||
}); |
|||
// 新增状态码校验
|
|||
if (!res.ok) { |
|||
const errorData = await res.json(); |
|||
throw new Error(errorData.error || `HTTP错误 ${res.status}`); |
|||
} |
|||
const data = await res.json(); |
|||
newstatus = data.newstatus; |
|||
//更新页面
|
|||
if(newstatus === 0){ |
|||
document.getElementById("detailTestStatus").textContent = "暂停中"; |
|||
actionButton.textContent = "继续"; |
|||
}else if(newstatus === 1){ |
|||
document.getElementById("detailTestStatus").textContent = "执行中"; |
|||
actionButton.textContent = "暂停"; |
|||
}else { |
|||
document.getElementById("detailTestStatus").textContent = "已完成"; |
|||
actionButton.textContent = "已完成"; |
|||
} |
|||
cur_task.taskStatus = newstatus; //光有个cur_taks也可以
|
|||
setSetpBtnStatus(); |
|||
//更新task_list的显示
|
|||
updateTaskList(); |
|||
} catch (error) { |
|||
console.error("控制任务状态异常:", error); |
|||
alert("控制任务状态异常:",error); |
|||
} |
|||
} |
|||
|
|||
//结束任务-brun-false
|
|||
document.getElementById("btnTaskOver").addEventListener("click",()=>{ |
|||
overTask(); |
|||
}) |
|||
async function overTask(){ |
|||
if(cur_task_id === 0){ |
|||
alert("请先选择一个任务!") |
|||
return |
|||
} |
|||
try { |
|||
if (confirm('确定要结束此任务吗?')){ |
|||
const res = await fetch("/api/task/taskover", { |
|||
method: "POST", |
|||
headers: { "Content-Type": "application/json" }, |
|||
body: JSON.stringify({ cur_task_id }), //task_id:task_id
|
|||
}); |
|||
// 新增状态码校验
|
|||
if (!res.ok) { |
|||
const errorData = await res.json(); |
|||
throw new Error(errorData.error || `HTTP错误 ${res.status}`); |
|||
} |
|||
const data = await res.json(); |
|||
bsuccess = data.bsuccess; |
|||
error = data.error; |
|||
if(bsuccess){ |
|||
//更新页面
|
|||
task_list = [] |
|||
cur_task = null //当前选择的task--用于修改缓存时使用
|
|||
cur_task_id = 0 //当前选择的cur_task_id
|
|||
//重新获取任务list
|
|||
getTasklist(); |
|||
}else { |
|||
alert("结束任务失败:",error); |
|||
} |
|||
} |
|||
} catch (error) { |
|||
alert("结束任务失败:",error); |
|||
} |
|||
} |
|||
|
|||
//修改了涉及到tasklist的展示内容,修改tasklist显示
|
|||
function updateTaskList(){ |
|||
//更新数据
|
|||
const selectedEl = document.querySelector(".task-item.selected"); |
|||
if (selectedEl) { |
|||
let safeR = getstrsafeR(cur_task.safeRank); |
|||
let taskS = getstrTaskS(cur_task.taskStatus); |
|||
const statusDiv = selectedEl.querySelector(".task-status"); |
|||
statusDiv.textContent = `${taskS}-${safeR}`; |
|||
} |
|||
} |
|||
|
|||
//设置单步按钮的可点击状态状态
|
|||
function setSetpBtnStatus(){ |
|||
if(cur_task){ |
|||
task_status = cur_task.taskStatus; |
|||
work_type = cur_task.workType; |
|||
}else { |
|||
task_status = 0; |
|||
work_type = 0; |
|||
} |
|||
btn_TaskStep = document.getElementById("one_step"); |
|||
btn_NodeStep = document.getElementById("btnNodeStep"); |
|||
if(task_status===1 && work_type===0){ //执行中且是人工模式
|
|||
btn_TaskStep.disabled= false; |
|||
btn_TaskStep.classList.remove("disabled-btn"); |
|||
//node-step
|
|||
if (selectedNodeData) { |
|||
if(selectedNodeData.node_bwork){ //有选中node,且节点为工作状态
|
|||
btn_NodeStep.disabled= false; |
|||
btn_NodeStep.classList.remove("disabled-btn"); |
|||
} |
|||
else { |
|||
btn_NodeStep.disabled = true; |
|||
btn_NodeStep.classList.add("disabled-btn"); //css会去重
|
|||
} |
|||
} |
|||
}else{ //其他情况都是不可用状态
|
|||
btn_TaskStep.disabled = true; // 添加 disabled 属性
|
|||
btn_TaskStep.classList.add("disabled-btn"); // 添加自定义样式
|
|||
if (selectedNodeData) { |
|||
btn_NodeStep.disabled = true; |
|||
btn_NodeStep.classList.add("disabled-btn"); //css会去重
|
|||
} |
|||
} |
|||
} |
|||
|
|||
//单步按钮--任务单步
|
|||
document.getElementById("one_step").addEventListener("click",() => { |
|||
if(cur_task_id===0){ |
|||
return |
|||
} |
|||
one_step_task(); |
|||
}); |
|||
async function one_step_task(){ |
|||
try { |
|||
const res = await fetch("/api/task/taskstep", { |
|||
method: "POST", |
|||
headers: { "Content-Type": "application/json" }, |
|||
body: JSON.stringify({ cur_task_id }), //task_id:task_id
|
|||
}); |
|||
if (!res.ok) { |
|||
const errorData = await res.json(); |
|||
throw new Error(errorData.error || `HTTP错误 ${res.status}`); |
|||
} |
|||
//修改成功
|
|||
const data = await res.json(); |
|||
const bsuccess = data.bsuccess; |
|||
if(bsuccess){ |
|||
alert("该任务已提交单步工作,请稍候查看执行结果!") |
|||
} |
|||
else{ |
|||
error = data.erroe; |
|||
alert("该任务单步失败!",error) |
|||
} |
|||
}catch (error) { |
|||
alert("该节点单步失败,请联系管理员!", error); |
|||
} |
|||
} |
|||
|
|||
//------------------测试数据和漏洞数据tab-------------------
|
|||
const pageSize = 10; |
|||
// 复用:根据返回的数据数组渲染表格 tbody,保证固定 10 行
|
|||
function renderTableRows(tbody, rowsData) { |
|||
tbody.innerHTML = ""; |
|||
// 遍历数据行,生成 <tr>
|
|||
rowsData.forEach((row, index) => { |
|||
const tr = document.createElement("tr"); |
|||
// 这里假设 row 为对象,包含各个字段;下标从1开始显示序号
|
|||
for (const cellData of Object.values(row)) { |
|||
const td = document.createElement("td"); |
|||
td.textContent = cellData; |
|||
tr.appendChild(td); |
|||
} |
|||
tbody.appendChild(tr); |
|||
}); |
|||
// 补足空行
|
|||
const rowCount = rowsData.length; |
|||
for (let i = rowCount; i < pageSize; i++) { |
|||
const tr = document.createElement("tr"); |
|||
for (let j = 0; j < tbody.parentElement.querySelectorAll("th").length; j++) { |
|||
const td = document.createElement("td"); |
|||
td.innerHTML = " "; |
|||
tr.appendChild(td); |
|||
} |
|||
tbody.appendChild(tr); |
|||
} |
|||
} |
|||
|
|||
//--------------------------测试指令-------------------------------
|
|||
let allInstrs = []; |
|||
let currentInstrPage = 1; |
|||
function renderInstrPage(page) { |
|||
currentInstrPage = page; |
|||
const start = (page - 1) * pageSize; |
|||
const end = start + pageSize; |
|||
const pageData = allInstrs.slice(start, end); |
|||
|
|||
const tbody = document.querySelector("#instrTable tbody"); |
|||
renderTableRows(tbody, pageData); |
|||
|
|||
// 更新分页按钮
|
|||
document.getElementById("instrPrev").dataset.page = page > 1 ? page - 1 : 1; |
|||
document.getElementById("instrNext").dataset.page = (end < allInstrs.length) ? page + 1 : page; |
|||
} |
|||
|
|||
// 查询测试指令
|
|||
async function searchInstructions(page = 1) { |
|||
if(cur_task_id === 0){ |
|||
return; |
|||
} |
|||
const nodeName = document.getElementById("instrNodeName").value.trim(); |
|||
try { |
|||
const res = await fetch("/api/task/getinstr", { |
|||
method: "POST", |
|||
headers: { |
|||
"Content-Type": "application/json", |
|||
}, |
|||
body: JSON.stringify({ |
|||
cur_task_id, |
|||
nodeName |
|||
}), |
|||
}); |
|||
if (!res.ok) { |
|||
const errorData = await res.json(); |
|||
throw new Error(errorData.error || `HTTP错误 ${res.status}`); |
|||
} |
|||
const data = await res.json(); |
|||
allInstrs = data.instrs; |
|||
renderInstrPage(1); //显示第一页数据
|
|||
} catch (error) { |
|||
console.error("获取测试指令失败:", error); |
|||
} |
|||
} |
|||
|
|||
//导出测试指令数据
|
|||
async function ExportInstructions(){ |
|||
alert("导出指令功能实现中。。。"); |
|||
} |
|||
|
|||
// 绑定测试指令查询按钮事件
|
|||
document.getElementById("instrSearchBtn").addEventListener("click", () => { |
|||
searchInstructions(); |
|||
}); |
|||
// 绑定测试指令分页点击事件
|
|||
document.getElementById("instrPrev").addEventListener("click", (e) => { |
|||
const page = parseInt(e.target.dataset.page, 10); |
|||
renderInstrPage(page); |
|||
}); |
|||
document.getElementById("instrNext").addEventListener("click", (e) => { |
|||
const page = parseInt(e.target.dataset.page, 10); |
|||
renderInstrPage(page); |
|||
}); |
|||
|
|||
//------------------漏洞数据---------------------------------
|
|||
let allVuls = []; |
|||
let currentVulPage = 1; |
|||
function renderVulPage(page) { |
|||
currentVulPage = page; |
|||
const start = (page - 1) * pageSize; |
|||
const end = start + pageSize; |
|||
const pageData = allVuls.slice(start, end); |
|||
|
|||
const tbody = document.querySelector("#vulTable tbody"); |
|||
renderTableRows(tbody, pageData); |
|||
|
|||
// 更新分页按钮
|
|||
document.getElementById("vulPrev").dataset.page = page > 1 ? page - 1 : 1; |
|||
document.getElementById("vulNext").dataset.page = (end < allVuls.length) ? page + 1 : page; |
|||
} |
|||
|
|||
// 查询漏洞数据
|
|||
async function searchVulnerabilities(page = 1) { |
|||
if(cur_task_id === 0){return;} |
|||
const nodeName = document.getElementById("vulNodeName").value.trim(); |
|||
const vulType = document.getElementById("vulType").value.trim(); |
|||
const vulLevel = document.getElementById("vulLevel").value; |
|||
try { |
|||
const res = await fetch("/api/task/getvul", { |
|||
method: "POST", |
|||
headers: { |
|||
"Content-Type": "application/json", |
|||
}, |
|||
body: JSON.stringify({ |
|||
cur_task_id, |
|||
nodeName, |
|||
vulType, |
|||
vulLevel |
|||
}), |
|||
}); |
|||
if (!res.ok) { |
|||
const errorData = await res.json(); |
|||
throw new Error(errorData.error || `HTTP错误 ${res.status}`); |
|||
} |
|||
const data = await res.json(); |
|||
allVuls = data.vuls; |
|||
renderVulPage(1) |
|||
} catch (error) { |
|||
console.error("获取漏洞数据失败:", error); |
|||
} |
|||
} |
|||
|
|||
//导出漏洞数据
|
|||
async function ExportVuls(){ |
|||
alert("导出漏洞功能实现中。。。"); |
|||
} |
|||
|
|||
// 绑定漏洞数据查询按钮事件
|
|||
document.getElementById("vulSearchBtn").addEventListener("click", () => { |
|||
searchVulnerabilities(); |
|||
}); |
|||
// 绑定漏洞数据分页点击事件
|
|||
document.getElementById("vulPrev").addEventListener("click", (e) => { |
|||
const page = parseInt(e.target.dataset.page, 10); |
|||
renderVulPage(page); |
|||
}); |
|||
document.getElementById("vulNext").addEventListener("click", (e) => { |
|||
const page = parseInt(e.target.dataset.page, 10); |
|||
renderVulPage(page); |
|||
}); |
@ -0,0 +1,659 @@ |
|||
// 全局变量,用于保存当前选中的节点数据
|
|||
let selectedNodeData = null; |
|||
|
|||
/** |
|||
* 根据节点数据递归生成树形结构(返回 <li> 元素) |
|||
* 假设节点数据格式: |
|||
* { |
|||
* "node_name":node.name, |
|||
* "node_path":node.path, |
|||
* "node_status":node.status, |
|||
* "node_bwork":node.bwork, |
|||
* "node_vultype":node.vul_type, |
|||
* "node_vulgrade":node.vul_grade, |
|||
* children: [ { ... }, { ... } ] |
|||
* } |
|||
*/ |
|||
function generateTreeHTML(nodeData) { |
|||
const li = document.createElement("li"); |
|||
const nodeSpan = document.createElement("span"); |
|||
nodeSpan.className = "tree-node"; |
|||
//设置data属性
|
|||
nodeSpan.setAttribute("data-node_name", nodeData.node_name); |
|||
nodeSpan.setAttribute("data-node_path", nodeData.node_path); |
|||
nodeSpan.setAttribute("data-node_status", nodeData.node_status); |
|||
nodeSpan.setAttribute("data-node_bwork", nodeData.node_bwork); |
|||
nodeSpan.setAttribute("data-node_vultype", nodeData.node_vultype); |
|||
nodeSpan.setAttribute("data-node_vulgrade", nodeData.node_vulgrade || ""); |
|||
nodeSpan.setAttribute("data-node_workstatus",nodeData.node_workstatus); |
|||
if(nodeData.node_workstatus ===0){ |
|||
nodeSpan.classList.add("no-work"); |
|||
}else { |
|||
nodeSpan.classList.remove("no-work"); |
|||
} |
|||
// 根据漏洞级别添加样式
|
|||
if (nodeData.node_vulgrade) { |
|||
nodeSpan.classList.remove("no-work"); |
|||
if (nodeData.node_vulgrade === "低危") { |
|||
nodeSpan.classList.add("vul-low"); |
|||
} else if (nodeData.node_vulgrade === "中危") { |
|||
nodeSpan.classList.add("vul-medium"); |
|||
} else if (nodeData.node_vulgrade === "高危") { |
|||
nodeSpan.classList.add("vul-high"); |
|||
} |
|||
} |
|||
// 创建容器用于存放切换图标与文本
|
|||
const container = document.createElement("div"); |
|||
container.className = "node-container"; |
|||
// 如果有子节点,则添加切换图标
|
|||
if (nodeData.children && nodeData.children.length > 0) { |
|||
const toggleIcon = document.createElement("span"); |
|||
toggleIcon.className = "toggle-icon"; |
|||
toggleIcon.textContent = "-"; // 默认展开时显示“-”
|
|||
container.appendChild(toggleIcon); |
|||
} |
|||
//节点文本
|
|||
const textSpan = document.createElement("span"); |
|||
textSpan.className = "node-text"; |
|||
textSpan.textContent = nodeData.node_name; |
|||
container.appendChild(textSpan); |
|||
nodeSpan.appendChild(container); |
|||
li.appendChild(nodeSpan); |
|||
//如果存在子节点,递归生成子节点列表
|
|||
if (nodeData.children && nodeData.children.length > 0) { |
|||
const ul = document.createElement("ul"); |
|||
nodeData.children.forEach((child) => { |
|||
ul.appendChild(generateTreeHTML(child)); |
|||
}); |
|||
li.appendChild(ul); |
|||
} |
|||
return li; |
|||
} |
|||
|
|||
// 绑定所有节点的点击事件
|
|||
function bindTreeNodeEvents() { |
|||
document.querySelectorAll(".tree-node").forEach((el) => { |
|||
el.addEventListener("click", (event) => { |
|||
// 阻止事件冒泡,避免点击时展开折叠影响
|
|||
event.stopPropagation(); |
|||
// 清除之前选中的节点样式
|
|||
document |
|||
.querySelectorAll(".tree-node.selected") |
|||
.forEach((node) => node.classList.remove("selected")); |
|||
// 当前节点标记为选中
|
|||
el.classList.add("selected"); |
|||
// 读取 data 属性更新右侧显示
|
|||
const nodeName = el.getAttribute("data-node_name"); |
|||
const status = el.getAttribute("data-node_status"); |
|||
const nodepath = el.getAttribute("data-node_path"); |
|||
const nodebwork = el.getAttribute("data-node_bwork"); |
|||
const vulType = el.getAttribute("data-node_vultype"); |
|||
const vulLevel = el.getAttribute("data-node_vulgrade"); |
|||
const workstatus = el.getAttribute("data-node_workstatus"); |
|||
//selectedNodeData = { nodeName, status, vulType, vulLevel,nodepath,nodebwork };
|
|||
// 示例中默认填充
|
|||
selectedNodeData = { |
|||
node_name: nodeName, |
|||
node_path: nodepath, |
|||
status: status, |
|||
node_bwork: nodebwork, |
|||
vul_type: vulType, |
|||
vul_grade: vulLevel || "-", |
|||
workstatus: workstatus |
|||
}; |
|||
//刷新界面内容
|
|||
update_select_node_data_show(nodeName,status,vulType,vulLevel,workstatus,nodebwork) |
|||
}); |
|||
// 双击事件:展开/收缩子节点区域
|
|||
el.addEventListener("dblclick", (event) => { |
|||
event.stopPropagation(); |
|||
// 找到该节点下的 <ul> 子节点列表
|
|||
const parentLi = el.parentElement; |
|||
const childUl = parentLi.querySelector("ul"); |
|||
if (childUl) { |
|||
// 切换 collapsed 类,控制 display
|
|||
childUl.classList.toggle("collapsed"); |
|||
// 更新切换图标
|
|||
const toggleIcon = el.querySelector(".toggle-icon"); |
|||
if (toggleIcon) { |
|||
toggleIcon.textContent = childUl.classList.contains("collapsed") |
|||
? "+" |
|||
: "-"; |
|||
} |
|||
} |
|||
}); |
|||
}); |
|||
} |
|||
|
|||
// 动态加载节点树数据
|
|||
async function loadNodeTree(task_id) { |
|||
// 清空选中状态
|
|||
selectedNodeData = null; |
|||
//刷新界面内容
|
|||
update_select_node_data_show("-","-","-","-","-",false) |
|||
try { |
|||
const res = await fetch("/api/task/gethistree", { |
|||
method: "POST", |
|||
headers: { "Content-Type": "application/json" }, |
|||
body: JSON.stringify({ task_id }), //task_id:task_id
|
|||
}); |
|||
if (!res.ok) { |
|||
const errorData = await res.json(); |
|||
throw new Error(errorData.error || `HTTP错误 ${res.status}`); |
|||
} |
|||
const data = await res.json(); |
|||
const treeData = data.tree; |
|||
if (!treeData) { |
|||
document.getElementById("treeContent").innerHTML = |
|||
"<p>无节点数据</p>"; |
|||
return; |
|||
} |
|||
|
|||
// 创建一个 <ul> 作为树的根容器
|
|||
const ul = document.createElement("ul"); |
|||
ul.className = "tree-root-ul"; |
|||
ul.appendChild(generateTreeHTML(treeData)); |
|||
// 替换节点树容器的内容
|
|||
const container = document.getElementById("treeContent"); |
|||
container.innerHTML = ""; |
|||
container.appendChild(ul); |
|||
// 绑定节点点击事件
|
|||
bindTreeNodeEvents(); |
|||
} catch (error) { |
|||
console.error("加载节点树失败:", error); |
|||
document.getElementById("treeContent").innerHTML = "<p>加载节点树失败</p>"; |
|||
} |
|||
} |
|||
|
|||
function getWorkStatus_Str(workstatus){ |
|||
strworkstatus = "" |
|||
switch (workstatus){ |
|||
case 0: |
|||
strworkstatus = "无待执行任务"; |
|||
break; |
|||
case 1: |
|||
strworkstatus = "待执行指令中"; |
|||
break; |
|||
case 2: |
|||
strworkstatus = "指令执行中"; |
|||
break; |
|||
case 3: |
|||
strworkstatus = "待提交llm中"; |
|||
break; |
|||
case 4: |
|||
strworkstatus = "提交llm中"; |
|||
break; |
|||
default: |
|||
strworkstatus = "-" |
|||
} |
|||
return strworkstatus |
|||
} |
|||
|
|||
//根据web端过来的数据,更新节点的工作状态
|
|||
function updateTreeNode(node_path, node_workstatus) { |
|||
// 根据 node_path 查找对应节点(假设每个 .tree-node 上设置了 data-node_path 属性)
|
|||
const nodeEl = document.querySelector(`.tree-node[data-node_path="${node_path}"]`); |
|||
if (nodeEl) { |
|||
// 更新 DOM 属性(属性值均为字符串)
|
|||
nodeEl.setAttribute("data-node_workstatus", node_workstatus); |
|||
//判断是否需要更新界面
|
|||
if(selectedNodeData){ |
|||
if(node_path === selectedNodeData.node_path){ //只有是当前选中节点才更新数据
|
|||
selectedNodeData.workstatus = node_workstatus; |
|||
strnew = getWorkStatus_Str(node_workstatus); |
|||
document.getElementById("node_workstatus").textContent = strnew; |
|||
} |
|||
} |
|||
} else { |
|||
console.warn(`未找到节点 ${node_path}`); |
|||
} |
|||
} |
|||
|
|||
//刷新节点的数据显示
|
|||
function update_select_node_data_show(nodeName,testStatus,vulType,vulLevel,workStatus,nodebwork){ |
|||
document.getElementById("nodeName").textContent = nodeName; |
|||
document.getElementById("testStatus").textContent = testStatus; |
|||
document.getElementById("node_vulType").textContent = vulType; |
|||
document.getElementById("node_vulLevel").textContent = vulLevel; |
|||
str_workStatus = getWorkStatus_Str(Number(workStatus)); |
|||
document.getElementById("node_workstatus").textContent = str_workStatus; |
|||
if(nodebwork==="true"){ |
|||
document.getElementById("node_bwork").textContent = "执行中"; |
|||
}else { |
|||
document.getElementById("node_bwork").textContent = "暂停中"; |
|||
} |
|||
setNodeBtnStatus(); |
|||
} |
|||
|
|||
//节点按钮的状态控制
|
|||
function setNodeBtnStatus(){ |
|||
const btn_VI = document.getElementById("btnViewInstr"); |
|||
if(!selectedNodeData){ |
|||
//没有选择node,按钮全部置不可用
|
|||
btn_VI.disabled = true; |
|||
btn_VI.classList.add("disabled-btn"); |
|||
} |
|||
else{ |
|||
btn_VI.disabled = false; |
|||
btn_VI.classList.remove("disabled-btn"); |
|||
} |
|||
} |
|||
|
|||
// // 刷新按钮事件绑定
|
|||
// document.getElementById("btnRefresh").addEventListener("click", () => {
|
|||
// // 重新加载节点树数据
|
|||
// loadNodeTree(cur_task_id);
|
|||
// });
|
|||
|
|||
// 按钮事件:当未选中节点时提示
|
|||
function checkSelectedNode() { |
|||
if (!selectedNodeData) { |
|||
alert("请先选择节点"); |
|||
return false; |
|||
} |
|||
return true; |
|||
} |
|||
|
|||
//----------------------查看节点--指令modal----------------------------
|
|||
let doneInstrs = []; // 已执行指令的所有数据
|
|||
let todoInstrs = []; // 待执行指令的所有数据
|
|||
let donePage = 1; // 已执行指令当前页
|
|||
let todoPage = 1; // 待执行指令当前页
|
|||
|
|||
document.getElementById("btnViewInstr").addEventListener("click", () => { |
|||
if (!checkSelectedNode()) return; |
|||
openInstrModal() |
|||
}); |
|||
// 打开对话框函数
|
|||
function openInstrModal() { |
|||
const instrCanvas = new bootstrap.Offcanvas(document.getElementById('instrCanvas')); |
|||
instrCanvas.show(); |
|||
|
|||
// const modalEl = document.getElementById("instrModal");
|
|||
// // 假设用 Bootstrap 5 的 Modal 组件
|
|||
// const instrModal = new bootstrap.Modal(modalEl, {keyboard: false});
|
|||
// 显示对话框
|
|||
//instrModal.show();
|
|||
|
|||
// 在打开 modal 时,先更新提示内容,将 loadingMsg 显示“请稍后,数据获取中…”
|
|||
const loadingMsg = document.getElementById("loadingMsg"); |
|||
if (loadingMsg) { |
|||
loadingMsg.textContent = "请稍后,数据获取中..."; |
|||
} |
|||
|
|||
// 加载指令数据
|
|||
loadInstrData(); |
|||
} |
|||
|
|||
// 调用后端接口,获取指令数据
|
|||
async function loadInstrData() { |
|||
task_id = cur_task_id; |
|||
node_path = selectedNodeData.node_path; |
|||
try { |
|||
const res = await fetch("/api/task/hisnodegetinstr", { |
|||
method: "POST", |
|||
headers: { "Content-Type": "application/json" }, |
|||
body: JSON.stringify({task_id,node_path}), |
|||
}); |
|||
if (!res.ok) { |
|||
const errorData = await res.json(); |
|||
throw new Error(errorData.error || `HTTP错误 ${res.status}`); |
|||
} |
|||
const data = await res.json(); |
|||
// 数据获取成功后,清除加载提示
|
|||
const loadingMsg = document.getElementById("loadingMsg"); |
|||
if (loadingMsg) { |
|||
loadingMsg.style.display = "none"; // 或者清空其 innerHTML
|
|||
} |
|||
doneInstrs = data.doneInstrs || []; |
|||
//todoInstrs = data.todoInstrs || [];
|
|||
donePage = 1; |
|||
todoPage = 1; |
|||
renderDoneInstrTable(donePage); |
|||
//renderTodoInstrTable(todoPage);
|
|||
} catch (error) { |
|||
console.error("加载指令数据异常:", error); |
|||
} |
|||
} |
|||
|
|||
// 渲染已执行指令表格
|
|||
function renderDoneInstrTable(page) { |
|||
const tbody = document.getElementById("doneInstrTbody"); |
|||
// 计算起始索引
|
|||
const startIndex = (page - 1) * pageSize; |
|||
const endIndex = startIndex + pageSize; |
|||
const pageData = doneInstrs.slice(startIndex, endIndex); |
|||
//select instruction,start_time,result from task_result where task_id=%s and node_path=%s;
|
|||
tbody.innerHTML = ""; |
|||
// 插入行
|
|||
pageData.forEach((item, i) => { |
|||
const tr = document.createElement("tr"); |
|||
|
|||
// 第一列:序号
|
|||
const tdIndex = document.createElement("td"); |
|||
tdIndex.textContent = startIndex + i + 1; |
|||
tr.appendChild(tdIndex); |
|||
|
|||
// 第二列:指令内容
|
|||
const tdInstr = document.createElement("td"); |
|||
tdInstr.textContent = item[0]; |
|||
tr.appendChild(tdInstr); |
|||
|
|||
// 第三列:开始时间(如果没有则显示空字符串)
|
|||
const tdStartTime = document.createElement("td"); |
|||
tdStartTime.textContent = item[1] || ""; |
|||
tr.appendChild(tdStartTime); |
|||
|
|||
// 第四列:执行结果
|
|||
const tdResult = document.createElement("td"); |
|||
tdResult.textContent = item[2] || ""; |
|||
tr.appendChild(tdResult); |
|||
|
|||
tbody.appendChild(tr); |
|||
}); |
|||
|
|||
// 若不足 10 行,补空行
|
|||
for (let i = pageData.length; i < pageSize; i++) { |
|||
const tr = document.createElement("tr"); |
|||
tr.innerHTML = ` |
|||
<td> </td> |
|||
<td> </td> |
|||
<td> </td> |
|||
<td> </td> |
|||
`;
|
|||
tbody.appendChild(tr); |
|||
} |
|||
} |
|||
|
|||
// 渲染待执行指令表格
|
|||
function renderTodoInstrTable(page) { |
|||
const tbody = document.getElementById("todoInstrTbody"); |
|||
const startIndex = (page - 1) * pageSize; |
|||
const endIndex = startIndex + pageSize; |
|||
const pageData = todoInstrs.slice(startIndex, endIndex); |
|||
|
|||
tbody.innerHTML = ""; |
|||
pageData.forEach((item, i) => { |
|||
const tr = document.createElement("tr"); |
|||
const idx = startIndex + i + 1; |
|||
// 第一列:序号
|
|||
const tdIndex = document.createElement("td"); |
|||
tdIndex.textContent = idx; |
|||
tr.appendChild(tdIndex); |
|||
|
|||
// 第二列:指令文本内容(直接使用 textContent)
|
|||
const tdItem = document.createElement("td"); |
|||
tdItem.textContent = item; // 使用 textContent 避免 HTML 解析
|
|||
tr.appendChild(tdItem); |
|||
|
|||
// 第三列:复制和删除按钮
|
|||
const tdAction = document.createElement("td"); |
|||
// const btn_cp = document.createElement("button");
|
|||
// btn_cp.className = "btn btn-primary btn-sm";
|
|||
// btn_cp.textContent = "复制";
|
|||
// btn_cp.style.marginRight = "2px"; // 设置间隔
|
|||
// btn_cp.onclick = () => confirmCopyTodoInstr(idx - 1);
|
|||
// tdAction.appendChild(btn_cp);
|
|||
const btn = document.createElement("button"); |
|||
btn.className = "btn btn-danger btn-sm"; |
|||
btn.textContent = "删除"; |
|||
btn.onclick = () => confirmDeleteTodoInstr(idx - 1); |
|||
tdAction.appendChild(btn); |
|||
tr.appendChild(tdAction); |
|||
|
|||
tbody.appendChild(tr); |
|||
}); |
|||
|
|||
// 补空行
|
|||
for (let i = pageData.length; i < pageSize; i++) { |
|||
const tr = document.createElement("tr"); |
|||
tr.innerHTML = ` |
|||
<td> </td> |
|||
<td> </td> |
|||
<td> </td> |
|||
`;
|
|||
tbody.appendChild(tr); |
|||
} |
|||
} |
|||
|
|||
// 分页事件
|
|||
document.getElementById("doneInstrPrev").addEventListener("click", (e) => { |
|||
e.preventDefault(); |
|||
if (donePage > 1) { |
|||
donePage--; |
|||
renderDoneInstrTable(donePage); |
|||
} |
|||
}); |
|||
document.getElementById("doneInstrNext").addEventListener("click", (e) => { |
|||
e.preventDefault(); |
|||
const maxPage = Math.ceil(doneInstrs.length / pageSize); |
|||
if (donePage < maxPage) { |
|||
donePage++; |
|||
renderDoneInstrTable(donePage); |
|||
} |
|||
}); |
|||
document.getElementById("todoInstrPrev").addEventListener("click", (e) => { |
|||
e.preventDefault(); |
|||
if (todoPage > 1) { |
|||
todoPage--; |
|||
renderTodoInstrTable(todoPage); |
|||
} |
|||
}); |
|||
document.getElementById("todoInstrNext").addEventListener("click", (e) => { |
|||
e.preventDefault(); |
|||
const maxPage = Math.ceil(todoInstrs.length / pageSize); |
|||
if (todoPage < maxPage) { |
|||
todoPage++; |
|||
renderTodoInstrTable(todoPage); |
|||
} |
|||
}); |
|||
|
|||
// 导出当前页数据
|
|||
document.getElementById("btnExport").addEventListener("click", () => { |
|||
// 判断当前是哪个 Tab
|
|||
const activeTab = document.querySelector("#instrTab button.nav-link.active"); |
|||
if (activeTab.id === "doneInstrTab") { |
|||
exportCurrentPage(doneInstrs, donePage, ["序号", "执行指令", "执行时间", "执行结果"]); |
|||
} else { |
|||
exportCurrentPage(todoInstrs, todoPage, ["序号", "待执行指令"]); |
|||
} |
|||
}); |
|||
|
|||
function exportCurrentPage(dataArr, page, headerArr) { |
|||
const startIndex = (page - 1) * pageSize; |
|||
const endIndex = startIndex + pageSize; |
|||
const pageData = dataArr.slice(startIndex, endIndex); |
|||
|
|||
// 在 CSV 的开头加上 BOM,用于 Excel 识别 UTF-8 编码
|
|||
let csvContent = "\uFEFF" + headerArr.join(",") + "\n"; |
|||
pageData.forEach((item, i) => { |
|||
const rowIndex = startIndex + i + 1; |
|||
if (headerArr.length === 4) { |
|||
// 已执行:序号,执行指令,执行时间,执行结果
|
|||
csvContent += rowIndex + "," + |
|||
(item.command || "") + "," + |
|||
(item.execTime || "") + "," + |
|||
(item.result || "") + "\n"; |
|||
} else { |
|||
// 待执行:序号,待执行指令
|
|||
csvContent += rowIndex + "," + (item.command || "") + "\n"; |
|||
} |
|||
}); |
|||
// 如果不足 pageSize 行,补足空行(根据列数进行适当补全)
|
|||
for (let i = pageData.length; i < pageSize; i++) { |
|||
// 根据 headerArr.length 来设置空行的格式
|
|||
if (headerArr.length === 4) { |
|||
csvContent += ",,,\n"; |
|||
} else { |
|||
csvContent += ",\n"; |
|||
} |
|||
} |
|||
|
|||
const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" }); |
|||
const url = URL.createObjectURL(blob); |
|||
const link = document.createElement("a"); |
|||
link.href = url; |
|||
link.download = "指令导出.csv"; |
|||
link.click(); |
|||
URL.revokeObjectURL(url); |
|||
} |
|||
|
|||
//------------------测试数据和漏洞数据tab-------------------
|
|||
// 复用:根据返回的数据数组渲染表格 tbody,保证固定 10 行
|
|||
function renderTableRows(tbody, rowsData) { |
|||
tbody.innerHTML = ""; |
|||
// 遍历数据行,生成 <tr>
|
|||
rowsData.forEach((row, index) => { |
|||
const tr = document.createElement("tr"); |
|||
// 这里假设 row 为对象,包含各个字段;下标从1开始显示序号
|
|||
for (const cellData of Object.values(row)) { |
|||
const td = document.createElement("td"); |
|||
td.textContent = cellData; |
|||
tr.appendChild(td); |
|||
} |
|||
tbody.appendChild(tr); |
|||
}); |
|||
// 补足空行
|
|||
const rowCount = rowsData.length; |
|||
for (let i = rowCount; i < pageSize; i++) { |
|||
const tr = document.createElement("tr"); |
|||
for (let j = 0; j < tbody.parentElement.querySelectorAll("th").length; j++) { |
|||
const td = document.createElement("td"); |
|||
td.innerHTML = " "; |
|||
tr.appendChild(td); |
|||
} |
|||
tbody.appendChild(tr); |
|||
} |
|||
} |
|||
|
|||
//--------------------------测试指令-------------------------------
|
|||
let allInstrs = []; |
|||
let currentInstrPage = 1; |
|||
function renderInstrPage(page) { |
|||
currentInstrPage = page; |
|||
const start = (page - 1) * pageSize; |
|||
const end = start + pageSize; |
|||
const pageData = allInstrs.slice(start, end); |
|||
|
|||
const tbody = document.querySelector("#instrTable tbody"); |
|||
renderTableRows(tbody, pageData); |
|||
|
|||
// 更新分页按钮
|
|||
document.getElementById("instrPrev").dataset.page = page > 1 ? page - 1 : 1; |
|||
document.getElementById("instrNext").dataset.page = (end < allInstrs.length) ? page + 1 : page; |
|||
} |
|||
|
|||
// 查询测试指令
|
|||
async function searchInstructions(page = 1) { |
|||
if(cur_task_id === 0){ |
|||
return; |
|||
} |
|||
const nodeName = document.getElementById("instrNodeName").value.trim(); |
|||
try { |
|||
const res = await fetch("/api/task/getinstr", { |
|||
method: "POST", |
|||
headers: { |
|||
"Content-Type": "application/json", |
|||
}, |
|||
body: JSON.stringify({ |
|||
cur_task_id, |
|||
nodeName |
|||
}), |
|||
}); |
|||
if (!res.ok) { |
|||
const errorData = await res.json(); |
|||
throw new Error(errorData.error || `HTTP错误 ${res.status}`); |
|||
} |
|||
const data = await res.json(); |
|||
allInstrs = data.instrs; |
|||
renderInstrPage(1); //显示第一页数据
|
|||
} catch (error) { |
|||
console.error("获取测试指令失败:", error); |
|||
} |
|||
} |
|||
|
|||
//导出测试指令数据
|
|||
async function ExportInstructions(){ |
|||
alert("导出指令功能实现中。。。") |
|||
} |
|||
|
|||
// 绑定测试指令查询按钮事件
|
|||
document.getElementById("instrSearchBtn").addEventListener("click", () => { |
|||
searchInstructions(); |
|||
}); |
|||
// 绑定测试指令分页点击事件
|
|||
document.getElementById("instrPrev").addEventListener("click", (e) => { |
|||
const page = parseInt(e.target.dataset.page, 10); |
|||
renderInstrPage(page); |
|||
}); |
|||
document.getElementById("instrNext").addEventListener("click", (e) => { |
|||
const page = parseInt(e.target.dataset.page, 10); |
|||
renderInstrPage(page);; |
|||
}); |
|||
|
|||
//------------------漏洞数据---------------------------------
|
|||
let allVuls = []; |
|||
let currentVulPage = 1; |
|||
function renderVulPage(page) { |
|||
currentVulPage = page; |
|||
const start = (page - 1) * pageSize; |
|||
const end = start + pageSize; |
|||
const pageData = allVuls.slice(start, end); |
|||
|
|||
const tbody = document.querySelector("#vulTable tbody"); |
|||
renderTableRows(tbody, pageData); |
|||
|
|||
// 更新分页按钮
|
|||
document.getElementById("vulPrev").dataset.page = page > 1 ? page - 1 : 1; |
|||
document.getElementById("vulNext").dataset.page = (end < allVuls.length) ? page + 1 : page; |
|||
} |
|||
|
|||
// 查询漏洞数据
|
|||
async function searchVulnerabilities(page = 1) { |
|||
if(cur_task_id === 0){return;} |
|||
const nodeName = document.getElementById("vulNodeName").value.trim(); |
|||
const vulType = document.getElementById("vulType").value.trim(); |
|||
const vulLevel = document.getElementById("vulLevel").value; |
|||
try { |
|||
const res = await fetch("/api/task/getvul", { |
|||
method: "POST", |
|||
headers: { |
|||
"Content-Type": "application/json", |
|||
}, |
|||
body: JSON.stringify({ |
|||
cur_task_id, |
|||
nodeName, |
|||
vulType, |
|||
vulLevel |
|||
}), |
|||
}); |
|||
if (!res.ok) { |
|||
const errorData = await res.json(); |
|||
throw new Error(errorData.error || `HTTP错误 ${res.status}`); |
|||
} |
|||
const data = await res.json(); |
|||
allVuls = data.vuls; |
|||
renderVulPage(1) |
|||
} catch (error) { |
|||
console.error("获取漏洞数据失败:", error); |
|||
} |
|||
} |
|||
|
|||
//导出漏洞数据
|
|||
async function ExportVuls(){ |
|||
alert("导出漏洞功能实现中。。。") |
|||
} |
|||
|
|||
// 绑定漏洞数据查询按钮事件
|
|||
document.getElementById("vulSearchBtn").addEventListener("click", () => { |
|||
searchVulnerabilities(); |
|||
}); |
|||
// 绑定漏洞数据分页点击事件
|
|||
document.getElementById("vulPrev").addEventListener("click", (e) => { |
|||
const page = parseInt(e.target.dataset.page, 10); |
|||
renderVulPage(page); |
|||
}); |
|||
document.getElementById("vulNext").addEventListener("click", (e) => { |
|||
const page = parseInt(e.target.dataset.page, 10); |
|||
renderVulPage(page); |
|||
}); |
@ -1,246 +0,0 @@ |
|||
let modelMap = []; //model_name model_id
|
|||
let channelMap = []; //channel_name channel_id
|
|||
let warn_data = []; //查询到的报警数据
|
|||
let page_data = []; |
|||
let currentEditingRow = null; |
|||
let currentPage = 1; |
|||
const rowsPerPage = 30; |
|||
//页面加载初始化
|
|||
document.addEventListener('DOMContentLoaded', function () { |
|||
perWarnHtml() |
|||
|
|||
document.getElementById('delPageButton').addEventListener('click', function () { |
|||
delpageWarn(); |
|||
}); |
|||
}); |
|||
|
|||
//搜索按钮点击
|
|||
document.getElementById('searchMButton').addEventListener('click', function() { |
|||
shearchWarn(); |
|||
}); |
|||
|
|||
//查询数据
|
|||
async function shearchWarn(){ |
|||
//查询告警数据
|
|||
let modelName = document.getElementById('modelSelect').value; |
|||
let channelName = document.getElementById('channelSelect').value; |
|||
const startTime = document.getElementById('startTime').value; |
|||
const endTime = document.getElementById('endTime').value; |
|||
const sCount = 0; // 起始记录数从0开始
|
|||
const eCount = 5000; // 每页显示10条记录
|
|||
|
|||
if(modelName == "请选择"){ |
|||
modelName = ""; |
|||
} |
|||
if(channelName == "请选择"){ |
|||
channelName = ""; |
|||
} |
|||
|
|||
// 构造请求体
|
|||
const requestData = { |
|||
model_name: modelName || "", // 如果为空,则传空字符串
|
|||
channel_name: channelName || "", |
|||
start_time: startTime || "", |
|||
end_time: endTime || "", |
|||
s_count: sCount, |
|||
e_count: eCount |
|||
}; |
|||
try{ |
|||
// 发送POST请求到后端
|
|||
const response = await fetch('/api/warn/search_warn', { |
|||
method: 'POST', |
|||
headers: { |
|||
'Content-Type': 'application/json' |
|||
}, |
|||
body: JSON.stringify(requestData) // 将数据转为JSON字符串
|
|||
}); |
|||
|
|||
// 检查响应是否成功
|
|||
if (response.ok) { |
|||
warn_data = await response.json(); |
|||
// 在这里处理查询结果,比如更新表格显示数据
|
|||
currentPage = 1; // 重置当前页为第一页
|
|||
renderTable(); //刷新表格
|
|||
renderPagination(); |
|||
} else { |
|||
console.error('查询失败:', response.status); |
|||
} |
|||
} catch (error) { |
|||
console.error('请求出错:', error); |
|||
} |
|||
} |
|||
|
|||
async function perWarnHtml() { |
|||
//获取算法和通道列表,在下拉框显示
|
|||
try{ |
|||
//算法名称下拉框
|
|||
let response = await fetch('/api/model/list'); |
|||
if (!response.ok) { |
|||
throw new Error('Network response was not ok'); |
|||
} |
|||
model_datas = await response.json(); |
|||
model_select_datas = ["请选择"]; |
|||
model_datas.forEach(option => { |
|||
model_select_datas.push(option.name); |
|||
modelMap[option.name] = option.ID; |
|||
}); |
|||
set_select_data("modelSelect",model_select_datas); |
|||
|
|||
//视频通道下拉框
|
|||
response = await fetch('/api/channel/tree'); |
|||
if (!response.ok) { |
|||
throw new Error('Network response was not ok'); |
|||
} |
|||
channel_datas = await response.json(); |
|||
channel_select_datas = ["请选择"]; |
|||
channel_datas.forEach(option => { |
|||
channel_select_datas.push(option.channel_name); |
|||
channelMap[option.channel_name] = option.ID; |
|||
}); |
|||
set_select_data("channelSelect",channel_select_datas); |
|||
//查询数据
|
|||
shearchWarn() |
|||
}catch (error) { |
|||
console.error('Error fetching model data:', error); |
|||
} |
|||
} |
|||
|
|||
//刷新表单页面数据
|
|||
function renderTable() { |
|||
const tableBody = document.getElementById('table-body-warn'); |
|||
tableBody.innerHTML = ''; //清空
|
|||
|
|||
const start = (currentPage - 1) * rowsPerPage; |
|||
const end = start + rowsPerPage; |
|||
pageData = warn_data.slice(start, end); |
|||
const surplus_count = rowsPerPage - pageData.length; |
|||
|
|||
|
|||
pageData.forEach((warn) => { |
|||
const row = document.createElement('tr'); |
|||
row.innerHTML = ` |
|||
<td>${warn.ID}</td> |
|||
<td>${warn.model_name}</td> |
|||
<td>${warn.channel_name}</td> |
|||
<td>${warn.creat_time}</td> |
|||
<td> |
|||
<button class="btn btn-primary btn-sm warn-show-btn">查看</button> |
|||
<button class="btn btn-secondary btn-sm warn-video-btn">视频</button> |
|||
<button class="btn btn-danger btn-sm warn-delete-btn">删除</button> |
|||
</td> |
|||
`;
|
|||
tableBody.appendChild(row); |
|||
row.querySelector('.warn-show-btn').addEventListener('click', () => showWarn(row)); |
|||
row.querySelector('.warn-video-btn').addEventListener('click', () => videoWarn(row)); |
|||
row.querySelector('.warn-delete-btn').addEventListener('click', () => deleteWarn(row)); |
|||
}); |
|||
} |
|||
|
|||
//刷新分页标签
|
|||
function renderPagination() { |
|||
const pagination = document.getElementById('pagination-warn'); |
|||
pagination.innerHTML = ''; |
|||
|
|||
const totalPages = Math.ceil(warn_data.length / rowsPerPage); |
|||
for (let i = 1; i <= totalPages; i++) { |
|||
const pageItem = document.createElement('li'); |
|||
pageItem.className = 'page-item' + (i === currentPage ? ' active' : ''); |
|||
pageItem.innerHTML = `<a class="page-link" href="#">${i}</a>`; |
|||
pageItem.addEventListener('click', (event) => { |
|||
event.preventDefault(); |
|||
currentPage = i; |
|||
renderTable(); |
|||
renderPagination(); |
|||
}); |
|||
pagination.appendChild(pageItem); |
|||
} |
|||
} |
|||
|
|||
//显示报警信息详情
|
|||
function showWarn(row){ |
|||
currentEditingRow = row; |
|||
model_name = row.cells[1].innerText; |
|||
channel_name = row.cells[2].innerText; |
|||
create_time = row.cells[3].innerText; |
|||
img_path = pageData[row.rowIndex-1].img_path; |
|||
|
|||
document.getElementById('modelName').innerText = `${model_name}`; |
|||
document.getElementById('channleName').innerText = `${channel_name}`; |
|||
document.getElementById('warnTime').innerText = `${create_time}`; |
|||
document.getElementById('warnImg').src = `/api/warn/warn_img?path=${img_path}`; // 设置图片的 src
|
|||
|
|||
$('#showWarn').modal('show'); |
|||
} |
|||
|
|||
//下载视频按钮
|
|||
function videoWarn(row){ |
|||
video_path = pageData[row.rowIndex-1].video_path; |
|||
// const videoPlayer = document.getElementById('videoPlayer');
|
|||
// videoPlayer.src = `/api/warn/warn_video?path=${encodeURIComponent(video_path)}`;
|
|||
// videoPlayer.load(); // 确保重新加载视频
|
|||
// $('#showVideo').modal('show');
|
|||
// 创建下载链接
|
|||
const downloadUrl = `/api/warn/warn_video?path=${encodeURIComponent(video_path)}`; |
|||
const a = document.createElement('a'); |
|||
a.href = downloadUrl; |
|||
document.body.appendChild(a); |
|||
a.click(); |
|||
document.body.removeChild(a); |
|||
} |
|||
|
|||
//删除一条报警数据
|
|||
function deleteWarn(row){ |
|||
if (confirm('确定删除此报警吗?')) { |
|||
let alarmIds=[]; |
|||
warn_id = row.cells[0].innerText; |
|||
alarmIds.push(warn_id); |
|||
// 发送POST请求到后端
|
|||
fetch('/api/warn/warn_del', { |
|||
method: 'POST', |
|||
headers: { |
|||
'Content-Type': 'application/json' |
|||
}, |
|||
body: JSON.stringify({ alarmIds: alarmIds }) |
|||
}) |
|||
.then(response => response.json()) |
|||
.then(data => { |
|||
if (data.success) { |
|||
alert('删除成功'); |
|||
} else { |
|||
alert('删除失败'); |
|||
} |
|||
}) |
|||
.catch(error => { |
|||
console.error('Error:', error); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
//删除当前页的报警数据
|
|||
function delpageWarn(){ |
|||
if (confirm('确定删除此页报警吗?')) { |
|||
let alarmIds=[]; |
|||
pageData.forEach((warn) => { |
|||
alarmIds.push(warn.ID) |
|||
}); |
|||
// 发送POST请求到后端
|
|||
fetch('/api/warn/warn_del', { |
|||
method: 'POST', |
|||
headers: { |
|||
'Content-Type': 'application/json' |
|||
}, |
|||
body: JSON.stringify({ alarmIds: alarmIds }) |
|||
}) |
|||
.then(response => response.json()) |
|||
.then(data => { |
|||
if (data.success) { |
|||
alert('删除成功'); |
|||
} else { |
|||
alert('删除失败'); |
|||
} |
|||
}) |
|||
.catch(error => { |
|||
console.error('Error:', error); |
|||
}); |
|||
} |
|||
} |
@ -0,0 +1,16 @@ |
|||
{% extends 'base.html' %} |
|||
|
|||
{% block title %}ZFSAFE{% endblock %} |
|||
|
|||
<!-- 页面样式块 --> |
|||
{% block style %} |
|||
{% endblock %} |
|||
|
|||
<!-- 页面内容块 --> |
|||
{% block content %} |
|||
<h3 style="text-align: center;padding: 10px"> 功能建设中,在二期实现。。。</h3> |
|||
{% endblock %} |
|||
|
|||
<!-- 页面脚本块 --> |
|||
{% block script %} |
|||
{% endblock %} |
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
@ -0,0 +1,661 @@ |
|||
{% extends 'base.html' %} |
|||
|
|||
{% block title %}ZFSAFE{% endblock %} |
|||
|
|||
<!-- 在此处可添加样式文件 --> |
|||
{% block style_link %} |
|||
<link href="{{ url_for('main.static', filename='css/node_tree.css') }}" rel="stylesheet"> |
|||
{% endblock %} |
|||
|
|||
<!-- 页面样式块 --> |
|||
{% block style %} |
|||
|
|||
/* 查询条件区域:使用 row 分布,输入框占满所在列 */ |
|||
.search-section .form-control, |
|||
.search-section .form-select { |
|||
width: 100%; |
|||
} |
|||
/* 查询条件区域,每个条件统一高度且左右间隔均等 */ |
|||
.search-section .col { |
|||
padding: 0 5px; |
|||
} |
|||
|
|||
/* 表格样式:统一垂直居中 */ |
|||
.table thead th, .table tbody td { |
|||
vertical-align: middle; |
|||
text-align: center; |
|||
} |
|||
|
|||
/* 分页区域右对齐 */ |
|||
.pagination-section { |
|||
text-align: right; |
|||
padding-right: 15px; |
|||
} |
|||
|
|||
/* 固定行高,比如 45px,每页 10 行 */ |
|||
.fixed-row-height { |
|||
height: 45px; |
|||
overflow: hidden; |
|||
} |
|||
/* 模态框内部最大高度,超出部分滚动 */ |
|||
.modal-dialog { |
|||
max-height: calc(100vh+20px); |
|||
} |
|||
.modal-content { |
|||
max-height: calc(100vh+20px); |
|||
} |
|||
.modal-body { |
|||
overflow-y: auto; |
|||
} |
|||
/* 这里设置页码按钮样式(可根据需要调整) */ |
|||
.pagination { |
|||
margin: 0; |
|||
} |
|||
.disabled-btn { |
|||
/* 禁用状态样式 */ |
|||
background-color: #cccccc; /* 灰色背景 */ |
|||
color: #666666; /* 文字颜色变浅 */ |
|||
cursor: not-allowed; /* 鼠标显示禁用图标 */ |
|||
opacity: 0.7; /* 可选:降低透明度 */ |
|||
|
|||
/* 禁用点击事件(通过 disabled 属性已实现,此样式仅增强视觉效果) */ |
|||
pointer-events: none; /* 可选:彻底阻止鼠标事件 */ |
|||
} |
|||
|
|||
.offcanvas-backdrop.show { |
|||
z-index: 1055; |
|||
} |
|||
|
|||
/* 再把 offcanvas 本身提到更高,超过 modal(modal 是 1055) */ |
|||
.offcanvas.show { |
|||
z-index: 1060; |
|||
} |
|||
/* 让所有右侧 offcanvas-end 都变成 60% 宽 */ |
|||
.offcanvas.offcanvas-end { |
|||
width: 60% !important; |
|||
max-width: none; /* 取消默认 max-width */ |
|||
} |
|||
{% endblock %} |
|||
|
|||
<!-- 页面内容块 --> |
|||
{% block content %} |
|||
|
|||
<div class="container"> |
|||
<!-- 查询条件区域 --> |
|||
<div class="search-section mb-3"> |
|||
<form class="row g-3 align-items-center"> |
|||
<!-- 每个输入框直接使用 placeholder 显示标题,水平分布 --> |
|||
<div class="col-3"> |
|||
<input type="text" class="form-control" id="testTarget" name="target" placeholder="检测目标"> |
|||
</div> |
|||
<div class="col-2"> |
|||
<select class="form-select" id="riskLevel" name="risk_level"> |
|||
<option value="">风险级别</option> |
|||
<option value="0">0</option> |
|||
<option value="1">1</option> |
|||
<option value="2">2</option> |
|||
<option value="3">3</option> |
|||
<option value="4">4</option> |
|||
<option value="5">5</option> |
|||
<option value="6">6</option> |
|||
<option value="7">7</option> |
|||
<option value="8">8</option> |
|||
<option value="9">9</option> |
|||
</select> |
|||
</div> |
|||
<div class="col-2"> |
|||
<select class="form-select" id="useModel" name="model"> |
|||
<option value="">使用模型</option> |
|||
<option value="1">DeepSeek</option> |
|||
<option value="2">GPT-O3</option> |
|||
</select> |
|||
</div> |
|||
<div class="col-2" > |
|||
<input type="date" id="startTime" class="form-control" name="start_time" placeholder="开始时间"> |
|||
</div> |
|||
<div class="col-2"> |
|||
<input type="date" id="endTime" class="form-control" name="end_time" placeholder="结束时间"> |
|||
</div> |
|||
<div class="col-auto"> |
|||
<button type="button" class="btn btn-primary" id="btnQuery">查询</button> |
|||
</div> |
|||
</form> |
|||
</div> |
|||
|
|||
<!-- 表格区域 --> |
|||
<div class="table-section mb-3"> |
|||
<table class="table table-bordered table-hover" id="histasksTable" style="width: 100%; table-layout: fixed;"> |
|||
<thead class="table-light"> |
|||
<tr> |
|||
<th style="width: 60px;">ID</th> |
|||
<th style="width: 20%;">检测目标</th> |
|||
<th style="width: 15%;">开始时间</th> |
|||
<th style="width: 15%;">结束时间</th> |
|||
<th style="width: 15%;">风险等级</th> |
|||
<th style="width: 15%;">使用模型</th> |
|||
<th style="width: 100px;">操作</th> |
|||
</tr> |
|||
</thead> |
|||
<tbody id="histasksTbody"> |
|||
<!-- 数据由JS动态填充,固定10行一页 --> |
|||
</tbody> |
|||
</table> |
|||
</div> |
|||
|
|||
<!-- 分页控件区域 --> |
|||
<div class="pagination-section mb-3"> |
|||
<nav> |
|||
<ul class="pagination pagination-sm justify-content-end" id="histasksPagination"> |
|||
<li class="page-item"> |
|||
<a class="page-link" href="#" id="prevPage">上一页</a> |
|||
</li> |
|||
<!-- 页码动态生成 --> |
|||
<li class="page-item"> |
|||
<a class="page-link" href="#" id="nextPage">下一页</a> |
|||
</li> |
|||
</ul> |
|||
</nav> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- 模态框:显示 task_manager.html 中的中间 tab 页内容 --> |
|||
<!-- 这里只显示节点树、测试指令、漏洞数据三个 tab 页 --> |
|||
<div class="modal fade" id="viewModal" tabindex="-1" aria-labelledby="viewModalLabel" aria-hidden="true"> |
|||
<div class="modal-dialog modal-xl"> |
|||
<div class="modal-content"> |
|||
<div class="modal-header"> |
|||
<h5 class="modal-title" id="viewModalLabel">任务详情</h5> |
|||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="关闭"></button> |
|||
</div> |
|||
<div class="modal-body p-0"> |
|||
<!-- 这里仅嵌入中间 tab 页部分 --> |
|||
<div class="tab-wrapper"> |
|||
<ul class="nav nav-tabs" id="myTab" role="tablist"> |
|||
<li class="nav-item" role="presentation"> |
|||
<button class="nav-link active" id="nodeTreeTab" data-bs-toggle="tab" data-bs-target="#nodeTree" type="button" role="tab" aria-controls="nodeTree" aria-selected="true"> |
|||
节点树 |
|||
</button> |
|||
</li> |
|||
<li class="nav-item" role="presentation"> |
|||
<button class="nav-link" id="testInstructionsTab" data-bs-toggle="tab" data-bs-target="#testInstructions" type="button" role="tab" aria-controls="testInstructions" aria-selected="false"> |
|||
测试指令 |
|||
</button> |
|||
</li> |
|||
<li class="nav-item" role="presentation"> |
|||
<button class="nav-link" id="vulnerabilitiesTab" data-bs-toggle="tab" data-bs-target="#vulnerabilities" type="button" role="tab" aria-controls="vulnerabilities" aria-selected="false"> |
|||
漏洞数据 |
|||
</button> |
|||
</li> |
|||
</ul> |
|||
<div class="tab-content" id="myTabContent"> |
|||
<!-- 节点树 --> |
|||
<div class="tab-pane fade show active p-3 h-100" id="nodeTree" role="tabpanel" aria-labelledby="nodeTreeTab"> |
|||
<div class="row h-100"> |
|||
<!-- 左侧:节点树区域 --> |
|||
<div class="col-8 h-100"> |
|||
<div class="node-tree-area" id="nodeTreeContainer" style="height: 100%; overflow-y: auto; position: relative; background-color: #f8f9fa;"> |
|||
<!-- 固定刷新按钮 --> |
|||
<!-- <div class="refresh-container" style="position: absolute; top: 5px; left: 5px; z-index: 100;">--> |
|||
<!-- <button class="tree-refresh btn btn-primary btn-sm" id="btnRefresh" title="刷新节点树">↻</button>--> |
|||
<!-- </div>--> |
|||
<!-- 节点树内容 --> |
|||
<div id="treeContent" class="tree-content" style="padding-top: 40px;"> |
|||
<p id="treeLoadingMsg" style="text-align:center;">加载中...</p> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<!-- 右侧:节点信息与操作 --> |
|||
<div class="col-4 h-100"> |
|||
<div class="node-info-area mb-3" style="padding: 10px;"> |
|||
<h5>节点信息</h5> |
|||
<p><strong>节点名称:</strong> <span id="nodeName">-</span></p> |
|||
<p><strong>测试状态:</strong> <span id="testStatus">-</span></p> |
|||
<p><strong>漏洞类型:</strong> <span id="node_vulType">-</span></p> |
|||
<p><strong>漏洞级别:</strong> <span id="node_vulLevel">-</span></p> |
|||
<p><strong>工作状态:</strong> <span id="node_bwork">-</span></p> |
|||
<p><strong>执行状态:</strong> <span id="node_workstatus">-</span></p> |
|||
</div> |
|||
<div class="node-actions" style="padding: 0 10px 10px;"> |
|||
<div class="row mb-2"> |
|||
<div class="col-12"> |
|||
<button class="btn btn-primary w-100" id="btnViewInstr">查看指令</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<!-- 测试指令 --> |
|||
<div class="tab-pane fade p-3" id="testInstructions" role="tabpanel" aria-labelledby="testInstructionsTab"> |
|||
<div class="row search-area mb-2"> |
|||
<div class="col-4"> |
|||
<input type="text" class="form-control" id="instrNodeName" placeholder="节点名称"> |
|||
</div> |
|||
<div class="col-2"> |
|||
<button class="btn btn-primary" id="instrSearchBtn">查询</button> |
|||
<button class="btn btn-primary" id="instrExportBtn">导出</button> |
|||
</div> |
|||
</div> |
|||
<table class="table table-bordered table-hover" id="instrTable" style="width: 100%; table-layout: fixed;"> |
|||
<colgroup> |
|||
<col style="width: 5%;"> |
|||
<col style="width: 15%;"> |
|||
<col style="width: 5%;"> |
|||
<col style="width: 30%;" class="wrap-cell"> |
|||
<col style="width: auto;"> |
|||
</colgroup> |
|||
<thead> |
|||
<tr> |
|||
<th>序号</th> |
|||
<th>节点路径</th> |
|||
<th>指序</th> |
|||
<th>执行指令</th> |
|||
<th>执行结果</th> |
|||
</tr> |
|||
</thead> |
|||
<tbody> |
|||
<!-- 默认显示10行 --> |
|||
</tbody> |
|||
</table> |
|||
<!-- 分页控件 --> |
|||
<nav> |
|||
<ul class="pagination" id="instrPagination"> |
|||
<li class="page-item"> |
|||
<a class="page-link" href="#" id="instrPrev">上一页</a> |
|||
</li> |
|||
<li class="page-item"> |
|||
<a class="page-link" href="#" id="instrNext">下一页</a> |
|||
</li> |
|||
</ul> |
|||
</nav> |
|||
</div> |
|||
<!-- 漏洞数据 --> |
|||
<div class="tab-pane fade p-3" id="vulnerabilities" role="tabpanel" aria-labelledby="vulnerabilitiesTab"> |
|||
<div class="row search-area mb-2"> |
|||
<div class="col-3"> |
|||
<input type="text" class="form-control" id="vulNodeName" placeholder="节点名称"> |
|||
</div> |
|||
<div class="col-3"> |
|||
<input type="text" class="form-control" id="vulType" placeholder="漏洞类型"> |
|||
</div> |
|||
<div class="col-3"> |
|||
<select class="form-select" id="vulLevel"> |
|||
<option value="">漏洞级别</option> |
|||
<option value="低危">低危</option> |
|||
<option value="中危">中危</option> |
|||
<option value="高危">高危</option> |
|||
</select> |
|||
</div> |
|||
<div class="col-2"> |
|||
<button class="btn btn-primary" id="vulSearchBtn">查询</button> |
|||
<button class="btn btn-primary" id="vulExportBtn">导出</button> |
|||
</div> |
|||
</div> |
|||
<table class="table table-bordered table-hover" id="vulTable"> |
|||
<thead> |
|||
<tr> |
|||
<th class="seq-col">序号</th> |
|||
<th>节点路径</th> |
|||
<th>漏洞类型</th> |
|||
<th>漏洞级别</th> |
|||
<th>漏洞说明</th> |
|||
</tr> |
|||
</thead> |
|||
<tbody> |
|||
<!-- 默认显示10行 --> |
|||
</tbody> |
|||
</table> |
|||
<!-- 分页控件 --> |
|||
<nav> |
|||
<ul class="pagination" id="vulPagination"> |
|||
<li class="page-item"> |
|||
<a class="page-link" href="#" id="vulPrev">上一页</a> |
|||
</li> |
|||
<li class="page-item"> |
|||
<a class="page-link" href="#" id="vulNext">下一页</a> |
|||
</li> |
|||
</ul> |
|||
</nav> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<!-- 模态框 footer --> |
|||
<div class="modal-footer"> |
|||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- 查看节点执行指令offcanvas --modal 改---> |
|||
<div class="offcanvas offcanvas-end" tabindex="-1" id="instrCanvas" aria-labelledby="instrCanvasLabel"> |
|||
<div class="offcanvas-header"> |
|||
<!-- 返回按钮 --> |
|||
<!-- <button type="button" class="btn btn-outline-secondary btn-sm" data-bs-dismiss="offcanvas">--> |
|||
<!-- ←--> |
|||
<!-- </button>--> |
|||
<h5 class="offcanvas-title" id="instrOffcanvasLabel">测试指令</h5> |
|||
<button type="button" class="btn-close text-reset" data-bs-dismiss="offcanvas" aria-label="Close"></button> |
|||
</div> |
|||
<div class="offcanvas-body"> |
|||
<!-- 返回按钮 --> |
|||
<div class="mb-3"> |
|||
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-dismiss="offcanvas"> |
|||
← 返回 |
|||
</button> |
|||
</div> |
|||
<!-- 新增一个提示容器 --> |
|||
<!-- <div id="loadingMsg" style="text-align: center; padding: 10px;">请稍后,数据获取中...</div>--> |
|||
<div id="loadingMsg" class="text-center mb-3">请稍后,数据获取中...</div> |
|||
<!-- 页签(已执行、待执行) --> |
|||
<ul class="nav nav-tabs" id="instrTab" role="tablist"> |
|||
<li class="nav-item" role="presentation"> |
|||
<button |
|||
class="nav-link active" |
|||
id="doneInstrTab" |
|||
data-bs-toggle="tab" |
|||
data-bs-target="#doneInstr" |
|||
type="button" |
|||
role="tab" |
|||
aria-controls="doneInstr" |
|||
aria-selected="true" |
|||
> |
|||
已执行 |
|||
</button> |
|||
</li> |
|||
<li class="nav-item" role="presentation"> |
|||
<button |
|||
class="nav-link" |
|||
id="todoInstrTab" |
|||
data-bs-toggle="tab" |
|||
data-bs-target="#todoInstr" |
|||
type="button" |
|||
role="tab" |
|||
aria-controls="todoInstr" |
|||
aria-selected="false" |
|||
> |
|||
待执行 |
|||
</button> |
|||
</li> |
|||
</ul> |
|||
<div class="tab-content pt-3" id="instrTabContent"> |
|||
<!-- 已执行指令表格 --> |
|||
<div |
|||
class="tab-pane fade show active" |
|||
id="doneInstr" |
|||
role="tabpanel" |
|||
aria-labelledby="doneInstrTab" |
|||
> |
|||
<table class="table table-bordered table-hover"> |
|||
<thead> |
|||
<tr> |
|||
<th style="width: 50px;">序号</th> |
|||
<th>执行指令</th> |
|||
<th>执行时间</th> |
|||
<th>执行结果</th> |
|||
</tr> |
|||
</thead> |
|||
<tbody id="doneInstrTbody"> |
|||
<!-- 动态生成,固定 10 行 --> |
|||
</tbody> |
|||
</table> |
|||
<!-- 分页控件 --> |
|||
<nav> |
|||
<ul class="pagination justify-content-end" id="doneInstrPagination"> |
|||
<li class="page-item"> |
|||
<a class="page-link" href="#" id="doneInstrPrev">上一页</a> |
|||
</li> |
|||
<li class="page-item"> |
|||
<a class="page-link" href="#" id="doneInstrNext">下一页</a> |
|||
</li> |
|||
</ul> |
|||
</nav> |
|||
</div> |
|||
<!-- 待执行指令表格 --> |
|||
<div |
|||
class="tab-pane fade" |
|||
id="todoInstr" |
|||
role="tabpanel" |
|||
aria-labelledby="todoInstrTab" |
|||
> |
|||
<table class="table table-bordered table-hover"> |
|||
<thead> |
|||
<tr> |
|||
<th style="width: 50px;">序号</th> |
|||
<th>待执行指令</th> |
|||
<th style="width: 80px;">操作</th> |
|||
</tr> |
|||
</thead> |
|||
<tbody id="todoInstrTbody"> |
|||
<!-- 动态生成,固定 10 行 --> |
|||
</tbody> |
|||
</table> |
|||
<!-- 分页控件 --> |
|||
<nav> |
|||
<ul class="pagination justify-content-end" id="todoInstrPagination"> |
|||
<li class="page-item"> |
|||
<a class="page-link" href="#" id="todoInstrPrev">上一页</a> |
|||
</li> |
|||
<li class="page-item"> |
|||
<a class="page-link" href="#" id="todoInstrNext">下一页</a> |
|||
</li> |
|||
</ul> |
|||
</nav> |
|||
</div> |
|||
</div> |
|||
<!-- 操作按钮 --> |
|||
<div class="mt-4 d-flex justify-content-end"> |
|||
<button type="button" class="btn btn-primary" id="btnExport">导出</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
{% endblock %} |
|||
|
|||
<!-- 页面脚本块 --> |
|||
{% block script %} |
|||
<script src="{{ url_for('main.static', filename='scripts/task_modal.js') }}"></script> |
|||
<script> |
|||
// 全局变量 |
|||
let cur_task_id = 0; |
|||
let allHistasks = []; |
|||
let currentPage = 1; |
|||
const pageSize = 10; |
|||
|
|||
// 分页渲染函数 |
|||
function renderHistasksTable(page) { |
|||
currentPage = page; |
|||
const tbody = document.getElementById("histasksTbody"); |
|||
const startIndex = (page - 1) * pageSize; |
|||
const endIndex = startIndex + pageSize; |
|||
const pageData = allHistasks.slice(startIndex, endIndex); |
|||
//select ID,task_target,safe_rank,llm_type,start_time,end_time from task |
|||
tbody.innerHTML = ""; |
|||
pageData.forEach((task, i) => { |
|||
const tr = document.createElement("tr"); |
|||
// 每个单元格创建时使用 textContent,确保固定行高,如有需要也可增加 class "fixed-row-height" |
|||
const tdId = document.createElement("td"); |
|||
tdId.textContent = task[0]; |
|||
tr.appendChild(tdId); |
|||
|
|||
const tdTarget = document.createElement("td"); |
|||
tdTarget.textContent = task[1]; |
|||
tr.appendChild(tdTarget); |
|||
|
|||
const tdStart = document.createElement("td"); |
|||
tdStart.textContent = task[4] || ""; |
|||
tr.appendChild(tdStart); |
|||
|
|||
const tdEnd = document.createElement("td"); |
|||
tdEnd.textContent = task[5] || ""; |
|||
tr.appendChild(tdEnd); |
|||
|
|||
const tdRisk = document.createElement("td"); |
|||
tdRisk.textContent = (task[2] === 0) ? "安全" : "存在风险"; |
|||
tr.appendChild(tdRisk); |
|||
|
|||
const tdModel = document.createElement("td"); |
|||
model_test = "" |
|||
if(task[3]===1){ |
|||
model_test="DeepSeek"; |
|||
} |
|||
else if(task[3]===2){ |
|||
model_test="GPT-O3"; |
|||
} |
|||
else{ |
|||
model_test="其他模型"; |
|||
} |
|||
tdModel.textContent = model_test; |
|||
tr.appendChild(tdModel); |
|||
|
|||
const tdAction = document.createElement("td"); |
|||
// 查看按钮(点击后弹出 modal) |
|||
const btnView = document.createElement("button"); |
|||
btnView.className = "btn btn-outline-info btn-sm"; |
|||
btnView.textContent = "查看"; |
|||
btnView.onclick = () => openViewModal(task[0]); |
|||
tdAction.appendChild(btnView); |
|||
// 删除按钮(示例) |
|||
const btnDel = document.createElement("button"); |
|||
btnDel.className = "btn btn-outline-danger btn-sm ms-1"; |
|||
btnDel.textContent = "删除"; |
|||
btnDel.onclick = () => confirmDeleteTask(task[0]); |
|||
tdAction.appendChild(btnDel); |
|||
|
|||
tr.appendChild(tdAction); |
|||
tbody.appendChild(tr); |
|||
}); |
|||
|
|||
// 补空行 |
|||
for (let i = pageData.length; i < pageSize; i++) { |
|||
const tr = document.createElement("tr"); |
|||
for (let j = 0; j < 7; j++) { |
|||
const td = document.createElement("td"); |
|||
td.textContent = "\u00A0"; |
|||
tr.appendChild(td); |
|||
} |
|||
tbody.appendChild(tr); |
|||
} |
|||
updatePagination(); |
|||
} |
|||
|
|||
// 更新分页按钮 |
|||
function updatePagination() { |
|||
const totalPages = Math.ceil(allHistasks.length / pageSize); |
|||
document.getElementById("prevPage").dataset.page = currentPage > 1 ? currentPage - 1 : 1; |
|||
document.getElementById("nextPage").dataset.page = currentPage < totalPages ? currentPage + 1 : totalPages; |
|||
} |
|||
|
|||
// 分页按钮点击事件 |
|||
document.getElementById("prevPage").addEventListener("click", function(e) { |
|||
e.preventDefault(); |
|||
const page = parseInt(this.dataset.page, 10); |
|||
renderHistasksTable(page); |
|||
}); |
|||
document.getElementById("nextPage").addEventListener("click", function(e) { |
|||
e.preventDefault(); |
|||
const page = parseInt(this.dataset.page, 10); |
|||
renderHistasksTable(page); |
|||
}); |
|||
|
|||
// 查询按钮事件,调用 /api/task/histasks 接口获取数据(示例) |
|||
document.getElementById("btnQuery").addEventListener("click", async function() { |
|||
// 此处可拼接获取表单数据条件,示例直接调用接口 |
|||
/* |
|||
target_name = data.get("target_name") |
|||
safe_rank = data.get("safe_rank") |
|||
llm_type = data.get("llm_type") |
|||
start_time= data.get("start_time") |
|||
end_time= data.get("end_time") |
|||
* */ |
|||
const target_name = document.getElementById("testTarget").value.trim(); |
|||
const safe_rank = document.getElementById("riskLevel").value; |
|||
const llm_type = document.getElementById("useModel").value; |
|||
const start_time = document.getElementById("startTime").value; |
|||
const end_time = document.getElementById("endTime").value; |
|||
|
|||
try { |
|||
const res = await fetch("/api/task/histasks", { |
|||
method: "POST", |
|||
headers: { "Content-Type": "application/json" }, |
|||
body: JSON.stringify({target_name,safe_rank,llm_type,start_time,end_time}) |
|||
}); |
|||
if (!res.ok) { |
|||
const errorData = await res.json(); |
|||
throw new Error(errorData.error || `HTTP错误 ${res.status}`); |
|||
} |
|||
const data = await res.json(); |
|||
allHistasks = data.his_tasks || []; |
|||
renderHistasksTable(1); |
|||
} catch (error) { |
|||
console.error("查询任务记录出错:", error); |
|||
alert("查询失败!"); |
|||
} |
|||
}); |
|||
|
|||
// “查看详情”按钮事件(统一使用模态框显示 task_manager.html) |
|||
async function openViewModal(task_id) { |
|||
cur_task_id = task_id; |
|||
const viewModal = new bootstrap.Modal(document.getElementById("viewModal"), { keyboard: false }); |
|||
viewModal.show(); |
|||
//查询节点树数据 |
|||
loadNodeTree(task_id); |
|||
//查询指令数据 |
|||
searchInstructions(1); |
|||
//查询漏洞数据 |
|||
searchVulnerabilities(1); |
|||
} |
|||
|
|||
// 删除任务的示例函数 |
|||
async function confirmDeleteTask(task_id) { |
|||
if (confirm("确认删除任务 " + task_id + " 吗?")) { |
|||
// 发送删除请求... |
|||
try { |
|||
const res = await fetch("/api/task/deltask", { |
|||
method: "POST", |
|||
headers: { "Content-Type": "application/json" }, |
|||
body: JSON.stringify({task_id}), |
|||
}); |
|||
if (!res.ok) { |
|||
const errorData = await res.json(); |
|||
throw new Error(errorData.error || `HTTP错误 ${res.status}`); |
|||
} |
|||
const data = await res.json(); |
|||
bsuccess = data.bsuccess; |
|||
if(bsuccess){ |
|||
// 1. 从前端缓存里删除这条任务 |
|||
allHistasks = allHistasks.filter(t => t[0] !== task_id); |
|||
|
|||
// 2. 重新渲染当前页 |
|||
// 注意:如果删除后当前页已经没有任何数据了,可以让 currentPage-- |
|||
const totalPages = Math.ceil(allHistasks.length / pageSize) || 1; |
|||
if (currentPage > totalPages) { |
|||
currentPage = totalPages; |
|||
} |
|||
renderHistasksTable(currentPage); |
|||
|
|||
// (可选)如果你想做局部删除,而不重画整表,也可以直接: |
|||
// btnEl.closest("tr").remove(); |
|||
alert("删除成功") |
|||
} |
|||
else{ |
|||
alert("删除失败:",data.error) |
|||
return false; |
|||
} |
|||
} catch (error) { |
|||
console.error("删除任务数据异常:", error); |
|||
return false; |
|||
} |
|||
} |
|||
} |
|||
|
|||
// 页面加载时可以自动调用查询接口加载数据 |
|||
document.addEventListener("DOMContentLoaded", () => { |
|||
// 可自动加载数据,或者等待用户点击查询 |
|||
document.getElementById("btnQuery").click(); |
|||
//renderHistasksTable(1); |
|||
}); |
|||
</script> |
|||
|
|||
{% endblock %} |
@ -0,0 +1,172 @@ |
|||
{% extends 'base.html' %} |
|||
|
|||
{% block title %}ZFSAFE{% endblock %} |
|||
|
|||
<!-- 页面样式块 --> |
|||
{% block style %} |
|||
#cookieInfo { |
|||
font-size: 0.9rem; |
|||
} |
|||
{% endblock %} |
|||
|
|||
<!-- 页面内容块 --> |
|||
{% block content %} |
|||
<div class="container mt-4"> |
|||
<!-- 测试目标输入框 --> |
|||
<div class="mb-3"> |
|||
<label for="testTarget" class="form-label"> |
|||
测试目标: <span class="text-danger">*</span> |
|||
</label> |
|||
<input |
|||
type="text" |
|||
class="form-control" |
|||
id="testTarget" |
|||
placeholder="输入测试目标" |
|||
required |
|||
/> |
|||
</div> |
|||
|
|||
<!-- cookie 信息输入框,左缩进,非必填 --> |
|||
<div class="mb-3"> |
|||
<div style="margin-left: 20px;margin-bottom: 10px"> |
|||
<label class="fw-bold" style="font-size:0.9rem">cookie信息 (非必填):</label> |
|||
<input |
|||
type="text" |
|||
class="form-control" |
|||
id="cookieInfo" |
|||
placeholder="输入cookie信息" |
|||
/> |
|||
</div> |
|||
<!-- 模型选择 --> |
|||
<div style="margin-left: 20px; margin-bottom: 10px"> |
|||
<label class="fw-bold" style="font-size:0.9rem">模型选择:</label> |
|||
<select class="form-select" id="modelSelect" style="font-size:0.9rem"> |
|||
<option value="DeepSeek">DeepSeek</option> |
|||
<option value="GPT-O3">GPT-O3</option> |
|||
</select> |
|||
</div> |
|||
|
|||
<!-- 测试模式:全自动,半自动 --> |
|||
<div style="margin-left: 20px"> |
|||
<label class="fw-bold" style="font-size:0.9rem">测试模式: </label> |
|||
<div class="form-check form-check-inline"> |
|||
<input |
|||
class="form-check-input" |
|||
type="radio" |
|||
name="testMode" |
|||
id="autoMode" |
|||
value="auto" |
|||
/> |
|||
<label class="form-check-label" for="autoMode" |
|||
>自动执行</label |
|||
> |
|||
</div> |
|||
<div class="form-check form-check-inline"> |
|||
<input |
|||
class="form-check-input" |
|||
type="radio" |
|||
name="testMode" |
|||
id="manualMode" |
|||
value="manual" |
|||
checked |
|||
/> |
|||
<label class="form-check-label" for="manualMode" |
|||
>人工确认</label |
|||
> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- 开始按钮,右对齐 --> |
|||
<div class="mb-3 text-end"> |
|||
<button id="startButton" class="btn btn-primary">开始</button> |
|||
</div> |
|||
|
|||
<!-- 使用说明 --> |
|||
<div class="mt-4"> |
|||
<label for="usage" class="form-label">使用说明:</label> |
|||
<textarea class="form-control" id="usage" rows="10"> |
|||
1.测试模式分为两种:自动执行和人工确认(单步模式),模式的切换只允许在暂停情况下调整; |
|||
2.暂停不停止正在执行指令,指令执行后会根据当前参数的设定执行下一步工作; |
|||
3.单步的作用是将节点中:待执行的指令进行执行,待提交LLM的数据提交LLM; |
|||
4.顶部的单步是针对整个任务的单步执行,若节点执行状态不一致,会存在某些节点执行测试指令,某些节点提交llm任务的情况,节点树区域的控制是针对该节点的控制; |
|||
5.由于LLM的不一致性,会存在无执行任务,但没有标记完成的任务节点,可作为已完成论; |
|||
6.在单步模式下,若某指令执行的结果错误,可以在查看MSG功能里,修改待提交的执行结果,来保障测试的顺利推进; |
|||
7.对于已经验证漏洞存在的节点,若LLM返回了测试指令,但没有必要继续验证的话,可以点击该节点的暂停按钮,暂停该节点的测试推进; |
|||
8.本工具仅限于在得到授权的前提下使用,若目标重要性很高,请使用单步模式,确认测试指令的影响后执行。 |
|||
</textarea> |
|||
</div> |
|||
</div> |
|||
{% endblock %} |
|||
|
|||
<!-- 页面脚本块 --> |
|||
{% block script %} |
|||
<script> |
|||
let curmodel = 1 //0-腾讯云,1-DS,2-2233.ai,3-GPT |
|||
// 为模型选择下拉框绑定选择事件 |
|||
document.getElementById("modelSelect").addEventListener("change", function() { |
|||
const selectedModel = this.value; |
|||
console.log("选择的模型为:" + selectedModel); |
|||
// 可根据需要进一步处理选中模型 |
|||
if(selectedModel === "DeepSeek"){ |
|||
curmodel = 1 |
|||
}else if(selectedModel === "GPT-O3"){ |
|||
curmodel = 2 //暂时用2233.ai接口代替o3 |
|||
} |
|||
else { |
|||
alert("模型参数存在问题,请联系管理员!!"); |
|||
} |
|||
}); |
|||
|
|||
document.getElementById("startButton").addEventListener("click", async () => { |
|||
//取值 |
|||
const testTarget = document.getElementById("testTarget").value; |
|||
const cookieInfo = document.getElementById("cookieInfo").value; |
|||
let workType = 0; //0-人工,1-自动 |
|||
const selected = document.getElementById('manualMode').checked; |
|||
if(selected){ |
|||
workType = 0; |
|||
}else { |
|||
workType = 1; |
|||
} |
|||
// 测试目标不能为空 |
|||
if (!testTarget) { |
|||
alert("测试目标不能为空!"); |
|||
return; |
|||
} |
|||
try { |
|||
const response = await fetch("/api/task/start", { |
|||
method: "POST", |
|||
headers: { |
|||
"Content-Type": "application/json", |
|||
}, |
|||
body: JSON.stringify({ |
|||
testTarget, |
|||
cookieInfo, |
|||
workType, |
|||
curmodel, |
|||
}), |
|||
}); |
|||
|
|||
// // 状态码校验 |
|||
// if (!response.ok) { |
|||
// const errorData = await res.json(); |
|||
// throw new Error(errorData.error || `HTTP错误 ${res.status}`); |
|||
// } |
|||
|
|||
// 如果后端返回了重定向,则前端自动跳转 |
|||
if (response.redirected) { |
|||
window.location.href = response.url; |
|||
} else { //除了跳转,都是返回的错误信息 |
|||
const data = await response.json(); |
|||
if (data.error) { |
|||
alert(data.error); |
|||
} |
|||
} |
|||
} catch (error) { |
|||
console.error("Error:", error); |
|||
alert("请求出错,请稍后再试!"); |
|||
} |
|||
}); |
|||
</script> |
|||
{% endblock %} |
@ -1,176 +0,0 @@ |
|||
<!DOCTYPE html> |
|||
<html lang="en"> |
|||
<head> |
|||
<meta charset="UTF-8"> |
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|||
<title>ZFBOX</title> |
|||
<link rel="stylesheet" href="{{ url_for('main.static', filename='css/bootstrap.min.css') }}"> |
|||
<link href="../static/resources/css/bootstrap.min.css" rel="stylesheet"> |
|||
<style> |
|||
body { |
|||
display: flex; |
|||
flex-direction: column; |
|||
height: 100vh; |
|||
} |
|||
|
|||
header { |
|||
background-color: #007bff; |
|||
color: white; |
|||
padding: 10px 0; |
|||
} |
|||
|
|||
.navbar-nav .nav-link { |
|||
color: white; |
|||
} |
|||
|
|||
main { |
|||
display: flex; |
|||
flex: 1; |
|||
overflow: hidden; |
|||
} |
|||
|
|||
.tree-view { |
|||
border-right: 1px solid #ddd; |
|||
padding: 10px; |
|||
overflow-y: auto; |
|||
} |
|||
|
|||
.video-content { |
|||
padding: 10px; |
|||
overflow-y: auto; |
|||
flex: 1; |
|||
} |
|||
|
|||
footer { |
|||
background-color: #f8f9fa; |
|||
text-align: center; |
|||
padding: 10px 0; |
|||
} |
|||
|
|||
.video-frame { |
|||
background-color: #f8f9fa; |
|||
border: 1px solid #ddd; |
|||
margin-bottom: 10px; |
|||
} |
|||
|
|||
.video-frame img { |
|||
width: 100%; |
|||
height: auto; |
|||
} |
|||
|
|||
.video-grid { |
|||
display: grid; |
|||
grid-template-columns: repeat(2, 1fr); |
|||
gap: 10px; |
|||
} |
|||
|
|||
.video-grid.eight { |
|||
grid-template-columns: repeat(4, 1fr); |
|||
} |
|||
|
|||
.toggle-buttons { |
|||
margin-bottom: 10px; |
|||
} |
|||
</style> |
|||
</head> |
|||
<body> |
|||
<header> |
|||
<nav class="navbar navbar-expand-lg navbar-light"> |
|||
<div class="container-fluid"> |
|||
<a class="navbar-brand" href="#">智凡BOX</a> |
|||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation"> |
|||
<span class="navbar-toggler-icon"></span> |
|||
</button> |
|||
<div class="collapse navbar-collapse" id="navbarNav"> |
|||
<ul class="navbar-nav"> |
|||
<li class="nav-item"> |
|||
<a class="nav-link" href="#">实时预览</a> |
|||
</li> |
|||
<li class="nav-item"> |
|||
<a class="nav-link" href="#">通道管理</a> |
|||
</li> |
|||
<li class="nav-item"> |
|||
<a class="nav-link" href="#">算法管理</a> |
|||
</li> |
|||
<li class="nav-item"> |
|||
<a class="nav-link" href="#">系统管理</a> |
|||
</li> |
|||
<li class="nav-item"> |
|||
<a class="nav-link" href="#">用户管理</a> |
|||
</li> |
|||
</ul> |
|||
<ul class="navbar-nav ms-auto"> |
|||
<li class="nav-item"> |
|||
<a class="nav-link" href="#">张三 退出</a> |
|||
</li> |
|||
</ul> |
|||
</div> |
|||
</div> |
|||
</nav> |
|||
</header> |
|||
<main> |
|||
<div class="tree-view col-md-3"> |
|||
<ul class="list-group"> |
|||
<li class="list-group-item">一区 |
|||
<ul class="list-group"> |
|||
<li class="list-group-item">北门通道一</li> |
|||
<li class="list-group-item">南门通道二</li> |
|||
<li class="list-group-item">通道三</li> |
|||
</ul> |
|||
</li> |
|||
<li class="list-group-item">二区域 |
|||
<ul class="list-group"> |
|||
<li class="list-group-item">通道一</li> |
|||
</ul> |
|||
</li> |
|||
</ul> |
|||
</div> |
|||
<div class="video-content col-md-9"> |
|||
<div class="toggle-buttons"> |
|||
<button id="fourView" class="btn btn-primary">四画面</button> |
|||
<button id="eightView" class="btn btn-secondary">八画面</button> |
|||
</div> |
|||
<div id="videoGrid" class="video-grid"> |
|||
<div class="video-frame"><img src="../static/resources/images/video_placeholder.png" alt="Video Stream"></div> |
|||
<div class="video-frame"><img src="../static/resources/images/video_placeholder.png" alt="Video Stream"></div> |
|||
<div class="video-frame"><img src="../static/resources/images/video_placeholder.png" alt="Video Stream"></div> |
|||
<div class="video-frame"><img src="../static/resources/images/video_placeholder.png" alt="Video Stream"></div> |
|||
</div> |
|||
</div> |
|||
</main> |
|||
<footer> |
|||
© 2024 ZFKJ All Rights Reserved |
|||
</footer> |
|||
|
|||
<script src="{{ url_for('main.static', filename='js/bootstrap.bundle.min.js') }}"></script> |
|||
<script> |
|||
document.getElementById('fourView').addEventListener('click', function() { |
|||
const videoGrid = document.getElementById('videoGrid'); |
|||
videoGrid.classList.remove('eight'); |
|||
videoGrid.classList.add('four'); |
|||
videoGrid.innerHTML = ` |
|||
<div class="video-frame"><img src="../static/resources/images/video_placeholder.png" alt="Video Stream"></div> |
|||
<div class="video-frame"><img src="../static/resources/images/video_placeholder.png" alt="Video Stream"></div> |
|||
<div class="video-frame"><img src="../static/resources/images/video_placeholder.png" alt="Video Stream"></div> |
|||
<div class="video-frame"><img src="../static/resources/images/video_placeholder.png" alt="Video Stream"></div> |
|||
`; |
|||
}); |
|||
|
|||
document.getElementById('eightView').addEventListener('click', function() { |
|||
const videoGrid = document.getElementById('videoGrid'); |
|||
videoGrid.classList.remove('four'); |
|||
videoGrid.classList.add('eight'); |
|||
videoGrid.innerHTML = ` |
|||
<div class="video-frame"><img src="../static/resources/images/video_placeholder.png" alt="Video Stream"></div> |
|||
<div class="video-frame"><img src="../static/resources/images/video_placeholder.png" alt="Video Stream"></div> |
|||
<div class="video-frame"><img src="../static/resources/images/video_placeholder.png" alt="Video Stream"></div> |
|||
<div class="video-frame"><img src="../static/resources/images/video_placeholder.png" alt="Video Stream"></div> |
|||
<div class="video-frame"><img src="../static/resources/images/video_placeholder.png" alt="Video Stream"></div> |
|||
<div class="video-frame"><img src="../static/resources/images/video_placeholder.png" alt="Video Stream"></div> |
|||
<div class="video-frame"><img src="../static/resources/images/video_placeholder.png" alt="Video Stream"></div> |
|||
<div class="video-frame"><img src="../static/resources/images/video_placeholder.png" alt="Video Stream"></div> |
|||
`; |
|||
}); |
|||
</script> |
|||
</body> |
|||
</html> |
@ -0,0 +1,444 @@ |
|||
{% extends 'base.html' %} |
|||
|
|||
{% block title %}ZFSAFE{% endblock %} |
|||
|
|||
<!-- 在此处可添加样式文件 --> |
|||
{% block style_link %} |
|||
<link href="{{ url_for('main.static', filename='css/node_tree.css') }}" rel="stylesheet"> |
|||
{% endblock %} |
|||
|
|||
<!-- 页面样式块 --> |
|||
{% block style %} |
|||
/* 搜索条件区域居中 */ |
|||
.search-area { |
|||
text-align: center; |
|||
margin-bottom: 15px; |
|||
} |
|||
/* 表格样式 */ |
|||
.table thead th { |
|||
text-align: center; /* 表头居中 */ |
|||
vertical-align: middle; |
|||
} |
|||
.table tbody td { |
|||
vertical-align: middle; |
|||
padding: 0.3rem 0.5rem; /* 缩小单元格上下内边距 */ |
|||
line-height: 1.5; |
|||
} |
|||
/* 序号列宽度较小 */ |
|||
.table thead th.seq-col, |
|||
.table tbody td.seq-col { |
|||
width: 50px; |
|||
text-align: center; |
|||
} |
|||
/* 分页控件右对齐 */ |
|||
.pagination { |
|||
justify-content: end; |
|||
} |
|||
/* 左侧任务列表整体样式 */ |
|||
.task-list { |
|||
height: calc(100vh - 210px); |
|||
overflow-y: auto; |
|||
background-color: #fff; |
|||
padding: 5px; |
|||
border-right: 1px solid #ddd; |
|||
} |
|||
/* 每个任务项样式 */ |
|||
.task-item { |
|||
padding: 5px; |
|||
margin-bottom: 5px; |
|||
background-color: #f9f9f9; |
|||
border: 1px solid #eee; |
|||
border-radius: 4px; |
|||
cursor: pointer; |
|||
transition: background-color 0.3s ease, box-shadow 0.3s ease; |
|||
} |
|||
/* 任务项选中状态样式 */ |
|||
.task-item.selected { |
|||
background-color: #e6f7ff; |
|||
border: 1px solid #1890ff; |
|||
box-shadow: 2px 2px 6px rgba(0, 0, 0, 0.2); |
|||
} |
|||
.task-item:hover { |
|||
background-color: #e6f7ff; |
|||
} |
|||
.task-item.selected { |
|||
border-color: #1890ff; |
|||
background-color: #bae7ff; |
|||
} |
|||
/* 任务目标样式,第一行 */ |
|||
.task-target { |
|||
font-weight: bold; |
|||
} |
|||
/* 任务状态,第二行,有缩进 */ |
|||
.task-status { |
|||
margin-left: 10px; |
|||
color: #666; |
|||
font-size: 0.8rem; |
|||
} |
|||
|
|||
/* 定义一个全高容器,与左侧任务列表高度保持一致 */ |
|||
.full-height { |
|||
height: calc(100vh - 210px); |
|||
} |
|||
|
|||
/* 右侧区域采用 flex 布局,垂直排列 */ |
|||
.right-container { |
|||
display: flex; |
|||
flex-direction: column; |
|||
} |
|||
|
|||
/* 基本信息区域,保持固定高度 */ |
|||
.basic-info { |
|||
/* 根据需要设定高度,或保持内容自适应 */ |
|||
flex: 0 0 auto; |
|||
} |
|||
|
|||
/* Tab 内容区域占满剩余空间 */ |
|||
.tab-wrapper { |
|||
flex: 1 1 auto; |
|||
display: flex; |
|||
flex-direction: column; |
|||
overflow: hidden; /* 防止内部溢出 */ |
|||
} |
|||
|
|||
/* 导航部分根据内容高度 */ |
|||
#myTab { |
|||
flex: 0 0 auto; |
|||
} |
|||
|
|||
.tab-content{ |
|||
flex: 1 1 auto; |
|||
overflow-y: auto; |
|||
} |
|||
|
|||
.disabled-btn { |
|||
/* 禁用状态样式 */ |
|||
background-color: #cccccc; /* 灰色背景 */ |
|||
color: #666666; /* 文字颜色变浅 */ |
|||
cursor: not-allowed; /* 鼠标显示禁用图标 */ |
|||
opacity: 0.7; /* 可选:降低透明度 */ |
|||
|
|||
/* 禁用点击事件(通过 disabled 属性已实现,此样式仅增强视觉效果) */ |
|||
pointer-events: none; /* 可选:彻底阻止鼠标事件 */ |
|||
} |
|||
|
|||
{% endblock %} |
|||
{% block modal %} |
|||
{% include 'task_manager_modal.html' %} |
|||
{% endblock %} |
|||
<!-- 页面内容块 --> |
|||
{% block content %} |
|||
<div class="container mt-4"> |
|||
<div class="container-fluid"> |
|||
<div class="row"> |
|||
<!-- 左侧:任务列表 --> |
|||
<div class="col-2 task-list" id="taskList"> |
|||
<!-- 动态生成的任务项将插入此处 --> |
|||
<p>加载中...</p> |
|||
</div> |
|||
|
|||
<!-- 右侧:任务详情 --> |
|||
<div class="col-10 full-height right-container"> |
|||
<!-- 上方:基本信息 --> |
|||
<div class="row basic-info"> |
|||
<div class="col-9"> |
|||
<div class="mb-2"> |
|||
<label class="fw-bold">测试目标: </label> |
|||
<span id="detailTestTarget">192.168.1.110</span> |
|||
</div> |
|||
<div class="row"> |
|||
<div class="col-3"> |
|||
<label class="fw-bold">工作状态: </label> |
|||
<span id="detailTestStatus">执行中</span> |
|||
</div> |
|||
<div class="col-3"> |
|||
<label class="fw-bold">安全情况: </label> |
|||
<span id="detailSafeStatus">安全</span> |
|||
</div> |
|||
<div class="col-6"> |
|||
<label class="fw-bold">测试模式: </label> |
|||
<div class="form-check form-check-inline"> |
|||
<input |
|||
class="form-check-input" |
|||
type="radio" |
|||
name="testMode" |
|||
id="autoMode" |
|||
value="auto" |
|||
checked |
|||
/> |
|||
<label class="form-check-label" for="autoMode" |
|||
>自动执行</label |
|||
> |
|||
</div> |
|||
<div class="form-check form-check-inline"> |
|||
<input |
|||
class="form-check-input" |
|||
type="radio" |
|||
name="testMode" |
|||
id="manualMode" |
|||
value="manual" |
|||
/> |
|||
<label class="form-check-label" for="manualMode" |
|||
>人工确认</label |
|||
> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<!-- <div class="col-2" style="display: flex; justify-content: center; align-items: center"> --> |
|||
<div class="col-3 d-flex justify-content-center align-items-center"> |
|||
<!-- 按钮 (联动测试状态示例: 执行中->暂停, 暂停中->继续, 已结束->重启) --> |
|||
<button class="btn btn-primary btn-block" id="actionButton">暂停</button> |
|||
<button class="btn btn-primary btn-block m-2" id="one_step">单步</button> |
|||
<button class="btn btn-danger btn-block m-2" id="btnTaskOver">结束</button> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- 下方:Tab 页 --> |
|||
<div class="tab-wrapper"> |
|||
<ul class="nav nav-tabs" id="myTab" role="tablist"> |
|||
<li class="nav-item" role="presentation"> |
|||
<button |
|||
class="nav-link active" |
|||
id="nodeTreeTab" |
|||
data-bs-toggle="tab" |
|||
data-bs-target="#nodeTree" |
|||
type="button" |
|||
role="tab" |
|||
aria-controls="nodeTree" |
|||
aria-selected="true" |
|||
> |
|||
节点树 |
|||
</button> |
|||
</li> |
|||
<li class="nav-item" role="presentation"> |
|||
<button |
|||
class="nav-link" |
|||
id="testInstructionsTab" |
|||
data-bs-toggle="tab" |
|||
data-bs-target="#testInstructions" |
|||
type="button" |
|||
role="tab" |
|||
aria-controls="testInstructions" |
|||
aria-selected="false" |
|||
> |
|||
测试指令 |
|||
</button> |
|||
</li> |
|||
<li class="nav-item" role="presentation"> |
|||
<button |
|||
class="nav-link" |
|||
id="vulnerabilitiesTab" |
|||
data-bs-toggle="tab" |
|||
data-bs-target="#vulnerabilities" |
|||
type="button" |
|||
role="tab" |
|||
aria-controls="vulnerabilities" |
|||
aria-selected="false" |
|||
> |
|||
漏洞数据 |
|||
</button> |
|||
</li> |
|||
</ul> |
|||
<div class="tab-content " id="myTabContent"> |
|||
<!-- 节点树 --> |
|||
<div |
|||
class="tab-pane fade show active p-3 h-100" |
|||
id="nodeTree" |
|||
role="tabpanel" |
|||
aria-labelledby="nodeTreeTab" |
|||
> |
|||
<div class="row h-100"> |
|||
<!-- 左侧:节点树区域 --> |
|||
<div class="col-8 h-100"> |
|||
<div class="node-tree-area" id="nodeTreeContainer"> |
|||
<!-- 顶部刷新按钮 --> |
|||
<div class="refresh-container"> |
|||
<button class="tree-refresh" id="btnRefresh" title="刷新节点树"> |
|||
↻ |
|||
</button> |
|||
</div> |
|||
<!-- 节点树内容区域 --> |
|||
<div id="treeContent" class="tree-content"> |
|||
<p id="treeLoadingMsg" style="text-align:center;">加载中...</p> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<!-- 右侧:节点信息与操作 --> |
|||
<div class="col-4 h-100"> |
|||
<div class="node-info-area mb-3"> |
|||
<h5>节点信息</h5> |
|||
<p><strong>节点名称:</strong> <span id="nodeName">-</span></p> |
|||
<p><strong>测试状态:</strong> <span id="testStatus">-</span></p> |
|||
<p><strong>漏洞类型:</strong> <span id="node_vulType">-</span></p> |
|||
<p><strong>漏洞级别:</strong> <span id="node_vulLevel">-</span></p> |
|||
<p><strong>工作状态:</strong> <span id="node_bwork">-</span></p> |
|||
<p><strong>执行状态:</strong> <span id="node_workstatus">-</span></p> |
|||
</div> |
|||
<div class="node-actions"> |
|||
<div class="row"> |
|||
<div class="col-6"><button class="btn btn-primary w-100" id="btnToggleStatus">暂停</button></div> |
|||
<div class="col-6"><button class="btn btn-primary w-100" id="btnNodeStep">单步</button></div> |
|||
</div> |
|||
<div class="row"> |
|||
<div class="col-6"><button class="btn btn-primary w-100" id="btnViewInstr">查看指令</button></div> |
|||
<div class="col-6"><button class="btn btn-primary w-100" id="btnViewMsg">查看MSG</button></div> |
|||
</div> |
|||
<div class="row"> |
|||
<div class="col-6"><button class="btn btn-primary w-100" id="btnAddInfo">添加信息</button></div> |
|||
<div class="col-6"><button class="btn btn-primary w-100" id="btnAddChild">添加子节点</button></div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- 测试指令 --> |
|||
<div |
|||
class="tab-pane fade p-3" |
|||
id="testInstructions" |
|||
role="tabpanel" |
|||
aria-labelledby="testInstructionsTab" |
|||
> |
|||
<!-- 搜索区域 --> |
|||
<div class="row search-area"> |
|||
<div class="col-4"> |
|||
<input |
|||
type="text" |
|||
class="form-control" |
|||
id="instrNodeName" |
|||
placeholder="节点名称" |
|||
/> |
|||
</div> |
|||
<div class="col-2"> |
|||
<button class="btn btn-primary" id="instrSearchBtn"> |
|||
查询 |
|||
</button> |
|||
<button class="btn btn-primary" id="instrExportBtn"> |
|||
导出 |
|||
</button> |
|||
</div> |
|||
</div> |
|||
<table class="table table-bordered table-hover" id="instrTable" style="width: 100%; table-layout: fixed;"> |
|||
<colgroup> |
|||
<!-- 第一列:序号,固定宽度或百分比 --> |
|||
<col style="width: 5%;"> |
|||
<!-- 第二列:节点路径,例如 25% --> |
|||
<col style="width: 15%;"> |
|||
<!-- 第三列:指令序号,固定宽度 --> |
|||
<col style="width: 5%;"> |
|||
<!-- 第四列:执行指令,设置为固定宽度或者百分比 --> |
|||
<col style="width: 30%;" class="wrap-cell"> |
|||
<!-- 第五列:执行结果,占用剩余所有宽度 --> |
|||
<col style="width: auto;"> |
|||
</colgroup> |
|||
<thead> |
|||
<tr> |
|||
<th>序号</th> |
|||
<th>节点路径</th> |
|||
<th>指序</th> |
|||
<th>执行指令</th> |
|||
<th>执行结果</th> |
|||
</tr> |
|||
</thead> |
|||
<tbody> |
|||
<!-- 默认显示 10 行(后续用 JS 渲染数据,补空行) --> |
|||
</tbody> |
|||
</table> |
|||
<!-- 分页控件 --> |
|||
<nav> |
|||
<ul class="pagination" id="instrPagination"> |
|||
<li class="page-item"> |
|||
<a class="page-link" href="#" id="instrPrev">上一页</a> |
|||
</li> |
|||
<li class="page-item"> |
|||
<a class="page-link" href="#" id="instrNext">下一页</a> |
|||
</li> |
|||
</ul> |
|||
</nav> |
|||
</div> |
|||
|
|||
<!-- 漏洞数据 --> |
|||
<div |
|||
class="tab-pane fade p-3" |
|||
id="vulnerabilities" |
|||
role="tabpanel" |
|||
aria-labelledby="vulnerabilitiesTab" |
|||
> |
|||
<!-- 搜索区域 --> |
|||
<div class="row search-area"> |
|||
<div class="col-3"> |
|||
<input |
|||
type="text" |
|||
class="form-control" |
|||
id="vulNodeName" |
|||
placeholder="节点名称" |
|||
/> |
|||
</div> |
|||
<div class="col-3"> |
|||
<input |
|||
type="text" |
|||
class="form-control" |
|||
id="vulType" |
|||
placeholder="漏洞类型" |
|||
/> |
|||
</div> |
|||
<div class="col-3"> |
|||
<select class="form-select" id="vulLevel"> |
|||
<option value="">漏洞级别</option> |
|||
<option value="低危">低</option> |
|||
<option value="中危">中</option> |
|||
<option value="高危">高</option> |
|||
</select> |
|||
</div> |
|||
<div class="col-2"> |
|||
<button class="btn btn-primary" id="vulSearchBtn"> |
|||
查询 |
|||
</button> |
|||
<button class="btn btn-primary" id="vulExportBtn"> |
|||
导出 |
|||
</button> |
|||
</div> |
|||
</div> |
|||
<table class="table table-bordered table-hover" id="vulTable"> |
|||
<thead> |
|||
<tr> |
|||
<th class="seq-col">序号</th> |
|||
<th>节点路径</th> |
|||
<th>漏洞类型</th> |
|||
<th>漏洞级别</th> |
|||
<th>漏洞说明</th> |
|||
</tr> |
|||
</thead> |
|||
<tbody> |
|||
<!-- 默认显示 10 行 --> |
|||
</tbody> |
|||
</table> |
|||
<!-- 分页控件 --> |
|||
<nav> |
|||
<ul class="pagination" id="vulPagination"> |
|||
<li class="page-item"> |
|||
<a class="page-link" href="#" id="vulPrev">上一页</a> |
|||
</li> |
|||
<li class="page-item"> |
|||
<a class="page-link" href="#" id="vulNext">下一页</a> |
|||
</li> |
|||
</ul> |
|||
</nav> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
{% endblock %} |
|||
|
|||
<!-- 页面脚本块 --> |
|||
{% block script %} |
|||
<!-- 在此处可添加与后端交互的脚本 --> |
|||
<script src="{{ url_for('main.static', filename='scripts/my_web_socket.js') }}"></script> |
|||
<script src="{{ url_for('main.static', filename='scripts/task_manager.js') }}"></script> |
|||
<script src="{{ url_for('main.static', filename='scripts/node_tree.js') }}"></script> |
|||
<script src="{{ url_for('main.static', filename='scripts/jquery-3.2.1.slim.min.js') }}"></script> |
|||
{% endblock %} |
@ -0,0 +1,214 @@ |
|||
<!-- 指令对话框:Bootstrap Modal --> |
|||
<div |
|||
class="modal fade" |
|||
id="instrModal" |
|||
tabindex="-1" |
|||
aria-labelledby="instrModalLabel" |
|||
aria-hidden="true" |
|||
> |
|||
<div class="modal-dialog modal-xl"><!-- 宽一些,可根据需求调整 --> |
|||
<div class="modal-content"> |
|||
<div class="modal-header"> |
|||
<h5 class="modal-title" id="instrModalLabel">测试指令</h5> |
|||
<button |
|||
type="button" |
|||
class="btn-close" |
|||
data-bs-dismiss="modal" |
|||
aria-label="Close" |
|||
></button> |
|||
</div> |
|||
<div class="modal-body"> |
|||
<!-- 新增一个提示容器 --> |
|||
<div id="loadingMsg" style="text-align: center; padding: 10px;">请稍后,数据获取中...</div> |
|||
<!-- 页签(已执行、待执行) --> |
|||
<ul class="nav nav-tabs" id="instrTab" role="tablist"> |
|||
<li class="nav-item" role="presentation"> |
|||
<button |
|||
class="nav-link active" |
|||
id="doneInstrTab" |
|||
data-bs-toggle="tab" |
|||
data-bs-target="#doneInstr" |
|||
type="button" |
|||
role="tab" |
|||
aria-controls="doneInstr" |
|||
aria-selected="true" |
|||
> |
|||
已执行 |
|||
</button> |
|||
</li> |
|||
<li class="nav-item" role="presentation"> |
|||
<button |
|||
class="nav-link" |
|||
id="todoInstrTab" |
|||
data-bs-toggle="tab" |
|||
data-bs-target="#todoInstr" |
|||
type="button" |
|||
role="tab" |
|||
aria-controls="todoInstr" |
|||
aria-selected="false" |
|||
> |
|||
待执行 |
|||
</button> |
|||
</li> |
|||
</ul> |
|||
<div class="tab-content pt-3" id="instrTabContent"> |
|||
<!-- 已执行指令表格 --> |
|||
<div |
|||
class="tab-pane fade show active" |
|||
id="doneInstr" |
|||
role="tabpanel" |
|||
aria-labelledby="doneInstrTab" |
|||
> |
|||
<table class="table table-bordered table-hover"> |
|||
<thead> |
|||
<tr> |
|||
<th style="width: 50px;">序号</th> |
|||
<th>执行指令</th> |
|||
<th>执行时间</th> |
|||
<th>执行结果</th> |
|||
</tr> |
|||
</thead> |
|||
<tbody id="doneInstrTbody"> |
|||
<!-- 动态生成,固定 10 行 --> |
|||
</tbody> |
|||
</table> |
|||
<!-- 分页控件 --> |
|||
<nav> |
|||
<ul class="pagination justify-content-end" id="doneInstrPagination"> |
|||
<li class="page-item"> |
|||
<a class="page-link" href="#" id="doneInstrPrev">上一页</a> |
|||
</li> |
|||
<li class="page-item"> |
|||
<a class="page-link" href="#" id="doneInstrNext">下一页</a> |
|||
</li> |
|||
</ul> |
|||
</nav> |
|||
</div> |
|||
<!-- 待执行指令表格 --> |
|||
<div |
|||
class="tab-pane fade" |
|||
id="todoInstr" |
|||
role="tabpanel" |
|||
aria-labelledby="todoInstrTab" |
|||
> |
|||
<table class="table table-bordered table-hover"> |
|||
<thead> |
|||
<tr> |
|||
<th style="width: 50px;">序号</th> |
|||
<th>待执行指令</th> |
|||
<th style="width: 80px;">操作</th> |
|||
</tr> |
|||
</thead> |
|||
<tbody id="todoInstrTbody"> |
|||
<!-- 动态生成,固定 10 行 --> |
|||
</tbody> |
|||
</table> |
|||
<!-- 分页控件 --> |
|||
<nav> |
|||
<ul class="pagination justify-content-end" id="todoInstrPagination"> |
|||
<li class="page-item"> |
|||
<a class="page-link" href="#" id="todoInstrPrev">上一页</a> |
|||
</li> |
|||
<li class="page-item"> |
|||
<a class="page-link" href="#" id="todoInstrNext">下一页</a> |
|||
</li> |
|||
</ul> |
|||
</nav> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<!-- 对话框底部:导出按钮居右 --> |
|||
<div class="modal-footer"> |
|||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"> |
|||
关闭 |
|||
</button> |
|||
<button type="button" class="btn btn-primary" id="btnExport"> |
|||
导出 |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- 查看MSG对话框:Bootstrap Modal --> |
|||
<div class="modal fade" id="msgModal" tabindex="-1" aria-labelledby="msgModalLabel" aria-hidden="true"> |
|||
<div class="modal-dialog modal-xl"> |
|||
<div class="modal-content"> |
|||
<div class="modal-header"> |
|||
<h5 class="modal-title" id="msgModalLabel">查看MSG</h5> |
|||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="关闭"></button> |
|||
</div> |
|||
<div class="modal-body"> |
|||
<!-- Tab 导航 --> |
|||
<ul class="nav nav-tabs" id="msgTab" role="tablist"> |
|||
<li class="nav-item" role="presentation"> |
|||
<button class="nav-link active" id="submittedTab" data-bs-toggle="tab" data-bs-target="#submitted" type="button" role="tab" aria-controls="submitted" aria-selected="true"> |
|||
已提交 |
|||
</button> |
|||
</li> |
|||
<li class="nav-item" role="presentation"> |
|||
<button class="nav-link" id="pendingTab" data-bs-toggle="tab" data-bs-target="#pending" type="button" role="tab" aria-controls="pending" aria-selected="false"> |
|||
待提交 |
|||
</button> |
|||
</li> |
|||
</ul> |
|||
<div class="tab-content pt-3" id="msgTabContent"> |
|||
<!-- 已提交 Tab --> |
|||
<div class="tab-pane fade show active" id="submitted" role="tabpanel" aria-labelledby="submittedTab"> |
|||
<table class="table table-bordered table-hover"> |
|||
<thead> |
|||
<tr> |
|||
<th style="width:50px;">序号</th> |
|||
<th>角色</th> |
|||
<th>内容</th> |
|||
</tr> |
|||
</thead> |
|||
<tbody id="submittedTbody"> |
|||
<!-- 动态生成 10 行;数据不足时补空行 --> |
|||
</tbody> |
|||
</table> |
|||
<!-- 分页控件 --> |
|||
<nav> |
|||
<ul class="pagination justify-content-end" id="submittedPagination"> |
|||
<li class="page-item"> |
|||
<a class="page-link" href="#" id="submittedPrev">上一页</a> |
|||
</li> |
|||
<li class="page-item"> |
|||
<a class="page-link" href="#" id="submittedNext">下一页</a> |
|||
</li> |
|||
</ul> |
|||
</nav> |
|||
<!-- 导出按钮 --> |
|||
<div class="text-end"> |
|||
<button class="btn btn-primary" id="btnExportSubmitted">导出</button> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- 待提交 Tab --> |
|||
<div class="tab-pane fade" id="pending" role="tabpanel" aria-labelledby="pendingTab"> |
|||
<form id="pendingForm"> |
|||
<div class="mb-3"> |
|||
<label for="llmtype" class="form-label fw-bold" style="font-size:0.9rem">llmtype:</label> |
|||
<input type="text" class="form-control" id="llmtype" placeholder="请输入 llmtype" disabled> |
|||
</div> |
|||
<div class="mb-3"> |
|||
<label for="pendingContent" class="form-label fw-bold" style="font-size:0.9rem">内容:</label> |
|||
<textarea class="form-control" id="pendingContent" rows="5" placeholder="请输入内容"></textarea> |
|||
</div> |
|||
<!-- 你可以在此处增加一个保存按钮,由用户提交待提交的内容修改 --> |
|||
<div class="text-end"> |
|||
<button type="button" class="btn btn-primary" id="btnSavePending">保存</button> |
|||
<button type="button" class="btn btn-primary" id="btnNeedInstr">请求指令</button> |
|||
<!-- <button type="button" class="btn btn-primary" id="btnRedoInstr">重新执行</button>--> |
|||
</div> |
|||
</form> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="modal-footer"> |
|||
<!-- 关闭按钮 --> |
|||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
@ -1,143 +0,0 @@ |
|||
{% extends 'base.html' %} |
|||
|
|||
{% block title %}ZFBOX{% endblock %} |
|||
|
|||
{% block style %} |
|||
.nav-scroller { |
|||
position: relative; |
|||
z-index: 2; |
|||
height: 2.75rem; |
|||
overflow-y: hidden; |
|||
} |
|||
|
|||
.nav-scroller .nav { |
|||
display: flex; |
|||
flex-wrap: nowrap; |
|||
padding-bottom: 1rem; |
|||
margin-top: -1px; |
|||
overflow-x: auto; |
|||
text-align: center; |
|||
white-space: nowrap; |
|||
-webkit-overflow-scrolling: touch; |
|||
} |
|||
|
|||
.blue-svg { |
|||
color: blue; /* 这将影响所有使用currentColor的fill属性 */ |
|||
} |
|||
|
|||
main { |
|||
display: flex; |
|||
flex: 1; |
|||
overflow: hidden; |
|||
} |
|||
|
|||
.tree-view { |
|||
border-right: 1px solid #ddd; |
|||
padding: 10px; |
|||
overflow-y: auto; |
|||
} |
|||
|
|||
.video-frame { |
|||
position: relative; |
|||
width: calc(50% - 10px); /* 默认4画面布局,每行2个视频框架 */ |
|||
margin: 5px; |
|||
float: left; |
|||
background-color: #ccc; /* 默认灰色填充 */ |
|||
border: 1px solid #000; /* 边框 */ |
|||
} |
|||
.video-header { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
background-color: #2c3e50; |
|||
color: #fff; |
|||
padding: 5px; |
|||
} |
|||
.video-area { |
|||
width: 100%; |
|||
padding-bottom: 75%; /* 4:3 比例 */ |
|||
background-color: #000; /* 默认灰色填充 */ |
|||
position: relative; |
|||
border: 1px solid #ddd; /* 视频区域边框 */ |
|||
} |
|||
|
|||
.video-area canvas { |
|||
display: none; |
|||
position: absolute; |
|||
top: 0; |
|||
left: 0; |
|||
width: 100%; |
|||
height: 100%; |
|||
object-fit: contain; |
|||
} |
|||
|
|||
.video-buttons { |
|||
display: flex; |
|||
gap: 10px; |
|||
} |
|||
|
|||
.video-buttons button { |
|||
background: none; |
|||
border: none; |
|||
color: white; |
|||
cursor: pointer; |
|||
} |
|||
|
|||
.video-buttons button:hover { |
|||
color: #f39c12; |
|||
} |
|||
|
|||
.dropdown-menu { |
|||
min-width: 100px; |
|||
} |
|||
|
|||
|
|||
#videoGrid.four .video-frame { |
|||
width: calc(50% - 10px); /* 每行4个视频框架 */ |
|||
} |
|||
#videoGrid.eight .video-frame { |
|||
width: calc(12.5% - 10px); /* 每行8个视频框架 */ |
|||
} |
|||
#videoGrid.nine .video-frame { |
|||
width: calc(33.33% - 10px); /* 每行3个视频框架,9画面布局 */ |
|||
} |
|||
.toggle-buttons { |
|||
margin-bottom: 5px; |
|||
} |
|||
.btn-small { |
|||
font-size: 0.75rem; |
|||
padding: 0.25rem 0.5rem; |
|||
} |
|||
.error-message { |
|||
position: absolute; |
|||
top: 50%; |
|||
left: 50%; |
|||
transform: translate(-50%, -50%); |
|||
color: red; |
|||
background-color: rgba(255, 255, 255, 0.8); /* 半透明背景 */ |
|||
padding: 10px; |
|||
border-radius: 5px; |
|||
} |
|||
{% endblock %} |
|||
|
|||
{% block content %} |
|||
<div class="container d-flex flex-wrap" > |
|||
<div id="treeView" class="tree-view col-md-3 "> |
|||
<!-- 动态树视图 --> |
|||
</div> |
|||
|
|||
<div class="video-content col-md-9"> |
|||
<div id="videoGrid" class="row four"> |
|||
<!-- 动态视频节点 --> |
|||
</div> |
|||
<div class="toggle-buttons"> |
|||
<button id="fourView" class="btn btn-primary btn-small">四画面</button> |
|||
<button id="nineView" class="btn btn-secondary btn-small">九画面</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
{% endblock %} |
|||
|
|||
{% block script %} |
|||
<script src="{{ url_for('main.static', filename='scripts/aiortc-client-new.js') }}"></script> |
|||
{% endblock %} |
@ -0,0 +1,16 @@ |
|||
{% extends 'base.html' %} |
|||
|
|||
{% block title %}ZFSAFE{% endblock %} |
|||
|
|||
<!-- 页面样式块 --> |
|||
{% block style %} |
|||
{% endblock %} |
|||
|
|||
<!-- 页面内容块 --> |
|||
{% block content %} |
|||
<h3 style="text-align: center;padding: 10px"> 功能建设中,在二期实现。。。</h3> |
|||
{% endblock %} |
|||
|
|||
<!-- 页面脚本块 --> |
|||
{% block script %} |
|||
{% endblock %} |
Loading…
Reference in new issue