Compare commits

...

3 Commits

Author SHA1 Message Date
张龙 51d3497cea V0.5.5.2 3 weeks ago
张龙 f5c40f6fda V0.5.5.1 1 month ago
张龙 d93cf60341 V0.5.5 1 month ago
  1. 9
      .idea/deployment.xml
  2. 2
      .idea/misc.xml
  3. 2
      .idea/zf_safe.iml
  4. 3
      config.yaml
  5. 178
      mycode/AssetsManager.py
  6. 223
      mycode/AttackMap.py
  7. 91
      mycode/CommandVerify.py
  8. 489
      mycode/DBManager.py
  9. 30
      mycode/DataFilterManager.py
  10. 220
      mycode/LLMManager.py
  11. 5
      mycode/PythonTManager.py
  12. 39
      mycode/PythoncodeTool.py
  13. 209
      mycode/TargetManager.py
  14. 36
      mycode/TaskManager.py
  15. 784
      mycode/TaskObject.py
  16. 64
      myutils/ContentManager.py
  17. 7
      myutils/PickleManager.py
  18. 36
      myutils/ReadWriteLock.py
  19. 12
      pipfile
  20. 55
      test.py
  21. 2
      tools/ArpingTool.py
  22. 27
      tools/CurlTool.py
  23. 2
      tools/DigTool.py
  24. 2
      tools/DirSearchTool.py
  25. 2
      tools/DirbTool.py
  26. 2
      tools/Enum4linuxTool.py
  27. 2
      tools/Hping3Tool.py
  28. 2
      tools/HydraTool.py
  29. 2
      tools/KubehunterTool.py
  30. 2
      tools/MedusaTool.py
  31. 2
      tools/MkdirTool.py
  32. 2
      tools/MsfconsoleTool.py
  33. 2
      tools/MsfvenomTool.py
  34. 2
      tools/NslookupTool.py
  35. 2
      tools/PingTool.py
  36. 2
      tools/PrintfTool.py
  37. 2
      tools/RpcclientTool.py
  38. 2
      tools/RpcinfoTool.py
  39. 2
      tools/SearchsploitTool.py
  40. 2
      tools/ShowmountTool.py
  41. 2
      tools/SmbclientTool.py
  42. 2
      tools/SmbmapTool.py
  43. 2
      tools/SmtpuserenumTool.py
  44. 2
      tools/SmugglerTool.py
  45. 2
      tools/SqlmapTool.py
  46. 2
      tools/SshpassTool.py
  47. 2
      tools/SslscanTool.py
  48. 2
      tools/Sublist3rTool.py
  49. 2
      tools/SwaksTool.py
  50. 2
      tools/TouchTool.py
  51. 2
      tools/WgetTool.py
  52. 2
      tools/WhatwebTool.py
  53. 2
      tools/WhoisTool.py
  54. 2
      tools/XvfbrunTool.py
  55. 2
      web/API/__init__.py
  56. 170
      web/API/assets.py
  57. 11
      web/API/task.py
  58. 1023
      web/main/static/resources/scripts/assets_manager.js
  59. 11
      web/main/static/resources/scripts/task_manager.js
  60. 593
      web/main/templates/assets_manager.html
  61. 10
      web/main/templates/assets_manager_modal.html
  62. 331
      web/main/templates/assets_user_manager.html
  63. 4
      web/main/templates/header.html
  64. 8
      web/main/templates/his_task.html
  65. 4
      web/main/templates/index.html
  66. 108
      web/main/templates/polling_target.html
  67. 16
      web/main/templates/safe_status.html
  68. 4
      web/main/templates/task_manager.html
  69. 8
      web/main/templates/task_manager_modal.html

9
.idea/deployment.xml

@ -1,7 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PublishConfigData" autoUpload="Always" serverName="root@192.168.3.151:22" remoteFilesAllowedToDisappearOnAutoupload="false">
<component name="PublishConfigData" autoUpload="Always" serverName="root@192.168.204.135:22 password" remoteFilesAllowedToDisappearOnAutoupload="false">
<serverData>
<paths name="root@192.168.204.135:22 password">
<serverdata>
<mappings>
<mapping deploy="/mnt/zfsafe" local="$PROJECT_DIR$" />
</mappings>
</serverdata>
</paths>
<paths name="root@192.168.204.136:22 password">
<serverdata>
<mappings>

2
.idea/misc.xml

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Remote Python 3.8.20 (sftp://root@192.168.3.151:22/root/miniconda3/envs/py38/bin/python)" project-jdk-type="Python SDK" />
<component name="ProjectRootManager" version="2" project-jdk-name="Remote Python 3.8.20 (sftp://root@192.168.204.135:22/root/ENTER/envs/py38/bin/python)" project-jdk-type="Python SDK" />
</project>

2
.idea/zf_safe.iml

@ -2,7 +2,7 @@
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="jdk" jdkName="Remote Python 3.8.20 (sftp://root@192.168.3.151:22/root/miniconda3/envs/py38/bin/python)" jdkType="Python SDK" />
<orderEntry type="jdk" jdkName="Remote Python 3.8.20 (sftp://root@192.168.204.135:22/root/ENTER/envs/py38/bin/python)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

3
config.yaml

@ -24,7 +24,8 @@ LLM_max_chain_count: 10 #为了避免推理链过长,造成推理效果变差
#Node
max_do_sn: 15 #同一节点最多执行5次指令
max_llm_sn: 5 #同一节点最多llm的提交次数
max_node_layer: 5 #根节点是0层,最多新增的层级数
#用户初始密码
pw: zfkj_123!@#

178
mycode/AssetsManager.py

@ -0,0 +1,178 @@
from mycode.DBManager import app_DBM
class AssetsManager:
def __init__(self):
pass
def __del__(self):
pass
def get_IP_assets(self,IP,user,safe_rank):
ip_assets = []
ip_assets = app_DBM.get_ip_assets_db(IP,user,safe_rank)
if not ip_assets:
ip_assets = []
return ip_assets
def get_IP_info(self,IP):
ip_info = app_DBM.get_ip_info_db(IP)
if not ip_info:
ip_info = []
return ip_info
def get_assets_users(self,uname):
assets_users = app_DBM.get_assets_users_db(uname)
if not assets_users:
assets_users = []
return assets_users
def update_assets_users(self,IP,owner_id,itype = 1):
return app_DBM.update_assets_users_db(IP,owner_id,itype)
def get_port_latest(self,ip):
return app_DBM.get_port_latest_db(ip)
def get_port_history(self,ip):
#拿ip_id
strsql = "select id,scan_count from ip_assets where ip_address = %s"
params = (ip,)
ip_data = app_DBM.safe_do_select(strsql,params,1)
ip_id = ip_data[0]
scan_count = ip_data[1]
# 1) 拿批次
sql_batches = '''
SELECT DISTINCT scan_count, scan_time
FROM port_assets
WHERE ip_id=%s
ORDER BY scan_count ASC;
'''
batches = app_DBM.safe_do_select(sql_batches, (ip_id,))
times = [row[1] for row in batches]
counts = [row[0] for row in batches]
if len(times) != scan_count:
print(f"*****数据批次有问题")
# 2) 拿所有端口数据
sql_ports = '''
SELECT port, service, version, status, scan_count
FROM port_assets
WHERE ip_id=%s
ORDER BY port ASC, scan_count ASC;
'''
rows = app_DBM.safe_do_select(sql_ports, (ip_id,))
# 3) 组织成: { port: { service, version, statuses: [...], changed: [...] } }
from collections import OrderedDict
port_dict = OrderedDict()
for port, service, version, status, sc in rows:
entry = port_dict.setdefault(port, {
'service': [],
'version': [],
'statuses': [],
'scancount': [],
'changed': []
})
entry['service'].append(service)
entry['version'].append(version)
entry['statuses'].append(status)
entry['scancount'].append(sc)
# 4) 计算 changed 数组:与前一批次对比
for port, info in port_dict.items():
service = info['service']
version = info['version']
statuses = info['statuses']
#scancount = info['scancount'] #执行次序不用对比
changed = []
for i in range(len(service)):
if i == 0:
changed.append(0) # 第一个批次,默认无变化
else:
if service[i] != service[i-1] or version[i] != version[i-1] or statuses[i] != statuses[i-1]:
changed.append(1)
else:
changed.append(0)
info['changed'] = changed
return times,port_dict
def get_ip_url_latest(self,ip):
return app_DBM.get_ip_url_latest_db(ip)
def get_ip_url_history(self,ip):
return app_DBM.get_ip_url_history_db(ip)
def get_vul_data(self,ip,nodeName,vulType,vulLevel):
# 先获取该IP最新的task_id
task_id = app_DBM.get_last_task_by_ip(ip)
if not task_id:
return []
vuls = app_DBM.get_task_vul(task_id, nodeName, vulType, vulLevel)
return vuls
def del_ip_assets(self,ip):
bsuccess,error = app_DBM.del_ip_assets(ip)
return bsuccess,error
def get_url_assets(self,url,owner,email):
url_assets = app_DBM.get_url_assets_db(url,owner,email)
return url_assets
def get_url_to_ip(self,url_id):
last_to_ips,his_to_ip = app_DBM.get_url_to_ip_db(url_id)
return last_to_ips,his_to_ip
def del_url_assets(self,url_id):
bsuccess, error = app_DBM.del_url_assets_db(url_id)
return bsuccess, error
def get_owners(self,owner, owner_type, contact, tellnum):
owner_list = []
owner_list = app_DBM.get_owner_db(owner, owner_type, contact, tellnum)
return owner_list
def add_update_owner(self,owner_data, do_mode):
'''
:param owner_data:
:param do_mode:
:return:
'''
id = owner_data["id"]
user = owner_data["user"]
type = owner_data["type"]
contact = owner_data["contact"]
phone = owner_data["phone"]
IDno = owner_data["IOno"]
if not user or not type or not contact or not phone or not IDno:
return False, "有信息没有填写,请补充完整!", []
if do_mode =="add":
strsql = "select ID from assets_user where ID_num = %s"
params = (IDno,)
data = app_DBM.safe_do_select(strsql, params, 1)
if data:
return False, "证件号码已经存在,请重新修改", []
strsql = "insert into assets_user (itype,uname,tellnum,tell_username,ID_num) values (%s,%s,%s,%s,%s);"
params = (type,user,phone,contact,IDno)
bok,new_id = app_DBM.safe_do_sql(strsql,params,1)
elif do_mode == "edit":
strsql = "select ID from assets_user where ID_num = %s and ID <> %s"
params = (IDno,id)
data = app_DBM.safe_do_select(strsql, params, 1)
if data:
return False, "证件号码已经存在,请重新修改", []
strsql = "update assets_user set itype=%s,uname=%s,tellnum=%s,tell_username=%s,ID_num=%s where ID=%s;"
params = (type, user, phone, contact, IDno,id)
bok,_ = app_DBM.safe_do_sql(strsql,params)
else:
return False,"操作模式超出预期",[]
if bok:
owner_list = app_DBM.get_owner_db("", "", "", "")
return True,"",owner_list
else:
return False, "数据库操作失败", []
def del_owner(self,id):
bsuccess,error = app_DBM.del_owner_db(id)
return bsuccess,error
g_AssetsM = AssetsManager()

223
mycode/AttackMap.py

@ -92,7 +92,7 @@ class AttackTree:
for child_node in cur_node.children:
if child_node.name == node_name:
return child_node
# #走到这说明没有匹配到-则新建一个节点-
# #走到这说明没有匹配到-则新建一个节点- 少个layer
# newNode = TreeNode(node_name,cur_node.task_id)
# cur_node.add_child(newNode,cur_node.messages)
return None
@ -170,10 +170,10 @@ class AttackTree:
class TreeNode:
def __init__(self, name,task_id,status="未完成", vul_type="未发现"):
def __init__(self, name,task_id,node_layer,status="未完成", vul_type="未发现"):
self.task_id = task_id #任务id
self.name = name # 节点名称
#self.node_lock = threading.Lock() #线程锁
self.cur_layer = node_layer # 节点当前层数
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
@ -202,12 +202,34 @@ class TreeNode:
#用户补充信息
self.cookie = ""
self.ext_info = ""
#线程锁-- 2025-5-9 两个list 合并在一起管理,只会有一个List 有值
self.work_status_lock = threading.Lock()
def __getstate__(self):
state = self.__dict__.copy()
for attr in ('work_status_lock',):
state.pop(attr, None)
return state
def __setstate__(self, state):
# 恢复其余字段
self.__dict__.update(state)
# 重建运行时用的锁
self.work_status_lock = threading.Lock()
#设置用户信息
def set_user_info(self,cookie,ext_info):
self.cookie = cookie
self.ext_info = ext_info
#添加子节点
def add_child(self, child_node):
child_node.parent = self
child_node.path = self.path + f"->{child_node.name}" #子节点的路径赋值
child_node.step_num = self.step_num
self.children.append(child_node)
#---------------------messages处理--------------------
def copy_messages(self,p_msg,c_msg): #2025-4-23修改用做给本节点加msg
'''
当前节点添加mesg,约束p_msg除system只取两层c_msg:只取最后两个
@ -216,7 +238,7 @@ class TreeNode:
:return:
'''
if not p_msg or not c_msg:
print("Messages存储存在问题!需要立即检查逻辑!")
print("Messages存储存在问题!需要检查逻辑!")
return
tmp_pmsg = copy.deepcopy(p_msg)
tmp_cmsg = copy.deepcopy(c_msg)
@ -248,91 +270,158 @@ class TreeNode:
for msg in tmp_cmsg[isart:]:
self.parent_messages.append(msg)
#添加子节点
def add_child(self, child_node):
child_node.parent = self
child_node.path = self.path + f"->{child_node.name}" #子节点的路径赋值
self.children.append(child_node)
def updatemsg(self,newtype,newcontent,p_msg,c_msg,index=0): #index待处理,目前待提交状态时,只应该有一条待提交数据
with self.work_status_lock:
if self._work_status == 0: #新增指令
if not self._llm_quere:
#判断是否要copy-父节点msg
if not self.parent_messages:
self.copy_messages(p_msg,c_msg)
newmsg = {"llm_type": int(newtype), "result": newcontent}
self._llm_quere.append(newmsg)
# 更新节点状态
self._work_status = 3 # 待提交
else:
return False,"新增指令,待提交数据不应该有数据"
elif self._work_status == 3: #只允许待提交状态修改msg
if self._llm_quere:
oldmsg = self._llm_quere[0]
oldmsg_llm_type = oldmsg["llm_type"] # llm_type不允许修改
newmsg = {"llm_type": int(oldmsg_llm_type), "result": newcontent}
self._llm_quere[0] = newmsg
else:
return False,"状态是待提交,不应该没有待提交数据"
else:
return False,"该状态,不运行修改待提交数据"
return True,""
def is_instr_empty(self):#待改 --根据work_status判断
with self.work_status_lock:
if self._instr_queue:
return False
return True
def is_llm_empty(self):#待改 --根据work_status判断
with self.work_status_lock:
if self._llm_quere:
return False
return True
#修改节点的执行状态--return bchange
#修改节点的执行状态--return bchange 只能是2-4
def update_work_status(self,work_status):
bsuccess = False
if self._work_status != work_status:
self._work_status = work_status
bsuccess = True
bsuccess = True
with self.work_status_lock:
if self._work_status == 0: #初始状态
self._work_status = work_status
else:
if self._work_status == 1 and work_status == 2: #只允许从1-》2
self._work_status = 2
elif self._work_status == 3 and work_status == 4:#只允许从3-》4
self._work_status = 4
elif self._work_status ==4 and work_status == 0: #4->0
self._work_status = 0
elif work_status == -1:
self._work_status = 0
elif work_status == -2:
self._work_status = 2
elif work_status == -3:
self._work_status = 4
elif work_status == -4: #测试调用
self._work_status = 1
else:
bsuccess = False
return bsuccess
def get_work_status(self):
#加锁有没有意义---web端和本身的工作线程会有同步问题,但与持久化相比,暂时忽略
#加锁有没有意义---web端和本身的工作线程会有同步问题
work_status = self._work_status
return work_status
#-------后期扩充逻辑,目前wokr_status的修改交给上层类对象-------
def add_instr(self,instr,p_msg,c_msg):
if not self.parent_messages: #为空时赋值
self.copy_messages(p_msg,c_msg)
self._instr_queue.append(instr)
def add_instr(self,instr,p_msg,c_msg): #所有指令一次提交
if instr:
with self.work_status_lock:
if not self.parent_messages: #为空时赋值
self.copy_messages(p_msg,c_msg)
if self._work_status in (0,1,4):
self._instr_queue.append(instr)
self._work_status = 1 #待执行
return True
else:
print("插入指令时,状态不为-1,1,4!")
return False,"节点的工作状态不是0或4,请检查程序逻辑"
else:
return False,"指令数据为空"
def test_add_instr(self,instr):
def test_add_instr(self, instr):
self._instr_queue.append(instr)
self._llm_quere = []
def get_instr(self):
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,"该指令不在队列中!"
with self.work_status_lock:
if self._work_status == 2: #执行中
return self._instr_queue.pop(0) if self._instr_queue else None
else:
print("不是执行中,不应该来取指令!")
return None
def add_res(self,str_res): #结构化结果字串
self._llm_quere.append(str_res)
def del_instr(self,instr): #web端,手动删除指令
with self.work_status_lock:
if self._work_status == 1:
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,"该指令不在队列中!"
else:
return False,"只有待执行时,允许删除指令"
def add_res(self,str_res,itype =0): #llm_queue入库的情况比较多,2,0,4
if str_res:
with self.work_status_lock:
if self._work_status in (2,0,4):
if itype == 1: #要插入到第一个
tmplist = []
tmplist.append(str_res)
tmplist.extend(self._llm_quere)
self._llm_quere = tmplist
else:
self._llm_quere.append(str_res)
if self._work_status in (2,0): #提交中,不要改变执行状态
self._work_status =3
else:
print("添加llm数据时,状态不是-1,0,2,4中的一种情况")
return False,"添加llm数据时,状态不是-1,0,2,4中的一种情况"
else:
return False,"待提交llm的数据为空"
def get_res(self):
return self._llm_quere.pop(0) if self._llm_quere else None
def get_res_user(self):
return self._llm_quere
with self.work_status_lock:
if self._work_status ==4: #提交中
return self._llm_quere.pop(0) if self._llm_quere else None
else:
print("不是提交中,不应该来取待提交数据!")
return None
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 clear_res(self):
with self.work_status_lock:
self._llm_quere.clear()
def is_instr_empty(self):
if self._instr_queue:
return False
return True
#-----------web查看数据-----------
def get_instr_user(self): #读不用锁了 -有错误问题不大
with self.work_status_lock:
instr_que = self._instr_queue.copy()
return instr_que
def is_llm_empty(self):
if self._llm_quere:
return False
return True
def get_res_user(self): #读不用锁了 -- 有错误问题不大
with self.work_status_lock:
llm_que = self._llm_quere.copy()
return llm_que
def __repr__(self):
return f"TreeNode({self.name}, {self.status}, {self.vul_type})"
if __name__ == "__main__":
pass

91
mycode/CommandVerify.py

@ -5,7 +5,7 @@ class CommandVerify:
pass
#验证节点指令的结构完整性--主要是判断JSON元素是否完整
def verify_node_cmds(self,node_cmds):
def verify_node_cmds(self,json_datas):
'''
- 新增节点{\"action\":\"add_node\", \"parent\": \"父节点\", \"nodes\": \"节点1,节点2\"};
- 未生成指令节点列表{\"action\": \"no_instruction\", \"nodes\": \"节点1,节点2\"};
@ -13,36 +13,85 @@ class CommandVerify:
- 完成测试{\"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节点,不符合格式要求!"}
for json_data in json_datas:
if "action" not in json_data:
strerror = {"节点指令错误":f"{json_data}缺少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
action = json_data["action"]
if action == "dash" or action == "python":
required_keys = {"path", "content"}
elif action == "add_node":
required_keys = {"parent", "nodes"}
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
required_keys = {"node"}
elif action =="find_vul":
if "node" not in node_json or "vulnerability" not in node_json:
strerror = {"节点指令错误": f"{node_json}不符合格式要求,缺少节点!"}
required_keys = {"node","vulnerability","name","risk","info"}
elif action == "asset":
if "IPS" not in json_data or "URL" not in json_data:
strerror = {"JSON结果格式错误": f"{json_data}不符合格式要求,缺少节点!"}
break
ips_json = json_data["IPS"]
url_json = json_data["URL"]
#URL信息检查
if url_json:
required_keys = {"Domain", "Subdomains", "Registrant", "Email","Registrar","Creation_date", "Expiration_date"}
is_valid, missing = self.validate_json_keys(url_json, required_keys)
if not is_valid:
strerror = {"JSON结果格式错误": f"{url_json}缺少主键{missing}"}
break
#IP信息检查
#[\"IP\":\"192.168.1.100\",\"IPtype\":\"IPv4/IPv6\",\"Ports\":[端口信息json]]}
#{\"Port\":\"端口号\",\"Service\":\"服务名称\",\"Version\":\"版本号\",\"Protocol\":\"TCP/UDP\",\"Status\":\"open/closed/filtered\"};
for ip_json in ips_json:
required_keys = {"IP","IPtype","Ports"}
is_valid, missing = self.validate_json_keys(ip_json, required_keys)
if not is_valid:
strerror = {"JSON结果格式错误": f"{ip_json}缺少主键{missing}"}
break
#端口数据检查
ports_json = ip_json["Ports"]
for port in ports_json:
required_keys = {"Port","Service","Version","Protocol","Status"}
is_valid,missing = self.validate_json_keys(port,required_keys)
if not is_valid:
strerror = {"JSON结果格式错误": f"{port}缺少主键{missing}"}
break
continue
else:
strerror = {"节点指令错误": f"{node_json}不可识别的action值!"}
strerror = {"节点指令错误": f"{json_data}含有不可识别的action值!"}
break
is_valid, missing = self.validate_json_keys(json_data, required_keys)
if not is_valid:
strerror = {"JSON结果格式错误": f"{json_data}缺少主键{missing}"}
break
if not strerror:
return True,strerror
return False,strerror
#验证JSON关键字是否缺失
def validate_json_keys(self,json_obj, required_keys):
"""
校验JSON对象是否包含所有必需的键
Args:
json_obj (dict): 要校验的JSON对象
required_keys (set): 必需键的集合
Returns:
tuple: (bool, list) - 表示是否所有键都存在的布尔值以及缺失键的列表如果都存在则为空
"""
if not isinstance(json_obj, dict):
return False, ["JSON对象必须是字典类型"]
json_keys = set(json_obj.keys())
missing_keys = required_keys - json_keys
if missing_keys:
return False, list(missing_keys)
else:
return True, []
# 验证节点数据的合规性
def verify_node_data(self,node_cmds):
add_nodes = []

489
mycode/DBManager.py

@ -16,6 +16,7 @@ class DBManager:
self.logger = LogHandler().get_logger("DBManager")
self.lock = threading.Lock()
self.itype = myCongif.get_data("DBType")
self.COLUMN_LIMITS = {} #各个表字段长度信息
self.ok = False
if self.itype ==0:
self.host = myCongif.get_data('mysql.host')
@ -39,6 +40,49 @@ class DBManager:
self.connection = None
self.logger.debug("DBManager销毁")
def get_column_limits(self):
'''连接成功后,统一读取数据库字段列长度设定'''
tables = ['assets_user','ip_assets','ip_to_url','port_assets','task','task_llm','task_result','task_vul','url_assets','user','zf_system']
for table in tables:
table_limit = {}
strsql = f'''
SELECT COLUMN_NAME, CHARACTER_MAXIMUM_LENGTH
FROM information_schema.columns
WHERE table_schema = 'your_db'
AND table_name = '{table}';
'''
datas = self.do_select(strsql)
for data in datas:
if data[1]:
table_limit[f"{str(data[0])}"] = data[1]
self.COLUMN_LIMITS[f"{table}"] = table_limit
def trim_fields(self,table: str, data: dict, logger) -> dict:
"""
COLUMN_LIMITS 截断过长的 varchar/text 字段
:param table: 表名
:param data : 待插入/更新的字段 dict
:return : dict已截断
"""
limits = self.COLUMN_LIMITS.get(table, {})
trimmed = {}
for col, val in data.items():
if val is None or col not in limits:
trimmed[col] = val
continue
max_len = limits[col]
# 只处理 str 类型
if isinstance(val, str) and len(val) > max_len:
self.logger.debug(
f"{table}.{col} 超长:{len(val)} > {max_len},已截断"
)
trimmed[col] = val[:max_len]
else:
trimmed[col] = val
return trimmed
def connect(self):
try:
if self.itype ==0:
@ -48,6 +92,7 @@ class DBManager:
self.connection = sqlite3.connect(self.dbfile)
self.ok = True
self.logger.debug("服务器端数据库连接成功")
#self.get_column_limits()
return True
except:
self.logger.error("服务器端数据库连接失败")
@ -103,23 +148,36 @@ class DBManager:
self.lock.release()
return bok
def safe_do_sql(self,strsql,params,itype=0):
def safe_do_sql(self,strsql,params,itype=0,table=None,field_names=None):
"""
table : 目标表名若要自动截断必须填
field_names : params 顺序对应的列名列表
"""
if table and field_names:
data_map = dict(zip(field_names, params))
data_map = self.trim_fields(table, data_map, self.logger)
params = tuple(data_map[col] for col in field_names)
bok = False
task_id = 0
do_id = 0
self.lock.acquire()
if self.Retest_conn():
try:
with self.connection.cursor() as cursor:
cursor.execute(strsql, params)
self.connection.commit()
if itype ==1: #只有插入task任务数据的时候是1
task_id = cursor.lastrowid
if itype ==1: #取insert 的自增id
do_id = cursor.lastrowid
elif itype ==2: #取删除的id,需要添加RETURNING id;
row = cursor.fetchone()
if row:
do_id = row[0]
bok = True
except Exception as e:
self.logger.error("执行数据库语句%s出错:%s" % (strsql, str(e)))
self.connection.rollback()
self.lock.release()
return bok,task_id
return bok,do_id
def safe_do_select(self,strsql,params,itype=0):
results = []
@ -134,7 +192,7 @@ class DBManager:
elif itype ==1:
results = cursor.fetchone() #获得一条记录
except Exception as e:
print(f"查询出错: {e}")
print(f"查询出错: {e}--\n{strsql}")
self.lock.release()
return results
@ -181,8 +239,9 @@ class DBManager:
return task_id
def over_task(self,task_id):
strsql = "update task set task_status=2 where ID=%s;"
params = (task_id)
over_time = get_local_timestr()
strsql = "update task set task_status=2,end_time=%s where ID=%s;"
params = (over_time,task_id)
bok,_ = self.safe_do_sql(strsql, params)
return bok
@ -372,7 +431,7 @@ class DBManager:
params.append(start_time_str)
if end_time and end_time.strip(): # 检查vullevel是否非空
conditions.append("end_time < %s")
conditions.append("start_time < %s")
# 将输入字符串转为日期对象
end_date = datetime.strptime(end_time, "%Y-%m-%d")
# 生成结束时间字符串(次日 00:00:00)
@ -382,6 +441,7 @@ class DBManager:
# 组合完整的WHERE子句
if len(conditions) > 0:
strsql += " WHERE " + " AND ".join(conditions)
strsql += " order by start_time DESC"
# 执行查询(将参数转为元组)
datas = self.safe_do_select(strsql, tuple(params))
@ -404,6 +464,408 @@ class DBManager:
data = self.safe_do_select(strsql,params,1)
return data[0]
#------------------资产表相关----------------------
def add_or_update_URL_asset(self,Domain,Subdomains,Registrant,Email,Creation_date,Expiration_date,Registrar):
strsql = "select ID from url_assets where URL= %s"
params = (Domain)
data = self.safe_do_select(strsql,params,1)
do_time = get_local_timestr()
if Subdomains:
strSubdomains = ','.join(Subdomains)
else:
strSubdomains = ""
if not data:#没有数据则新增
strsql = "insert into url_assets (registrar,creation_date,expiration_date,emails,create_time,update_time,URL,subdomains,Registrant) " \
"values (%s,%s,%s,%s,%s,%s,%s,%s,%s)"
params =(Registrar,Creation_date,Expiration_date,Email,do_time,do_time,Domain,strSubdomains,Registrant)
bok,url_id = self.safe_do_sql(strsql,params,1)
else:#有值则修改
url_id = data[0]
strsql = "update url_assets set registrar=%s,Registrant=%s,creation_date=%s,expiration_date=%s,emails=%s,update_time=%s," \
"subdomains=%s where ID = %s;"
params = (Registrar,Registrant,Creation_date,Expiration_date,Email,do_time,strSubdomains,url_id)
bok,_ = self.safe_do_sql(strsql,params)
#维护历史记录 --由数据库触发器维护
return url_id
def add_or_update_IP_asset(self,IP,ip_type):
#return ip_id scan_count
strsql = "select id,scan_count from ip_assets where ip_address = %s;"
params = (IP)
data = self.safe_do_select(strsql,params,1)
if data:
return data[0],data[1]
#IP没有则新建入库
ip_id = 0
start_time = get_local_timestr()
sql = "INSERT INTO ip_assets (ip_address,ip_version,created_time,scan_count) VALUES (%s,%s,%s,%s);"
params = (IP, ip_type, start_time,0)
bok, ip_id = self.safe_do_sql(sql, params, 1)
return ip_id,0
#将task_id 和ip资产进行关联
def add_task_to_ip(self,task_id,ip_id):
strsql = "INSERT INTO task_to_ip (task_id,ip_id) VALUES (%s,%s);"
params = (task_id,ip_id)
bok,_ = self.safe_do_sql(strsql,params)
return bok
def update_port(self,ip_id,scan_count,Prots):
update_time = get_local_timestr()
scan_count += 1
strsql = "update ip_assets set update_time = %s,scan_count = %s where id=%s"
params = (update_time,scan_count,ip_id)
bok,_ = self.safe_do_sql(strsql,params)
if bok:
#最新的port数据入库 {\"Protocol\":\"TCP/UDP\",\"Status\":\"open/closed/filtered\"};
for port in Prots:
p_num = port["Port"]
service = port["Service"]
version = port["Version"]
protocol = port["Protocol"]
status = port["Status"]
strsql = "insert into port_assets (port,service,version,status,ip_id,scan_count,scan_time,Protocol) " \
"values (%s,%s,%s,%s,%s,%s,%s,%s)"
params = (p_num,service,version,status,ip_id,scan_count,update_time,protocol)
bok,_ = self.safe_do_sql(strsql,params)
return bok
def update_url_to_ip(self,url_id,ips):
strsql = "select ip_id from ip_to_url where url_id = %s"
params = (url_id)
datas = self.safe_do_select(strsql,params)
old_ips = []
for data in datas:
old_ips.append(data[0])
only_in_old = list(set(old_ips) - set(ips)) #适合不重复,不关心顺序的情况
only_in_new = list(set(ips) - set(old_ips))
do_time = get_local_timestr()
if only_in_old:#新的关联中没有,老的有,就是删除了
placeholders = ",".join(["(%s,%s,%s)"] * len(only_in_old))
sql = f"""
INSERT INTO ip_to_url_his (ip_id, url_id, del_time)
VALUES {placeholders}
"""
# 扁平化参数列表
params: list = []
for ip in only_in_old:
params += [ip, url_id, do_time]
# 一次性执行
bok, _ = self.safe_do_sql(sql, tuple(params))
if not bok:
raise RuntimeError("批量插入 ip_to_url_his 失败")
#老表中删除记录---待验证
strsql = '''
delete from ip_to_url where url_id=%s and ip_id in (%s)
'''
del_ips = ','.join([str(x) for x in only_in_old])
params = (url_id,del_ips)
bok,_ = self.safe_do_sql(strsql,params)
if not bok:
raise RuntimeError("批量删除 ip_to_url 失败")
if only_in_new:#新的有,老的没有就是新增。
placeholders = ",".join(["(%s,%s,%s)"] * len(only_in_new))
sql = f"""
INSERT INTO ip_to_url (ip_id, url_id, create_time)
VALUES {placeholders}
"""
# 扁平化参数列表
params: list = []
for ip in only_in_new:
params += [ip, url_id, do_time]
# 一次性执行
bok, _ = self.safe_do_sql(sql, tuple(params))
if not bok:
raise RuntimeError("批量插入 ip_to_url 失败")
def get_ip_assets_db(self,IP,user,risk_rank):
strsql = '''
SELECT
ia.ip_address,
au.uname,
ia.risk_rank,
ia.update_time,
COALESCE(p.port_cnt ,0) AS port_total,
COALESCE(u.url_cnt ,0) AS url_total
FROM ip_assets AS ia
LEFT JOIN assets_user AS au ON ia.owner_id = au.ID
/* 端口数量 */
LEFT JOIN (
SELECT ip_id,scan_count,COUNT(*) AS port_cnt
FROM port_assets
GROUP BY ip_id,scan_count
) AS p ON p.ip_id = ia.id and p.scan_count=ia.scan_count
/* URL 数量 */
LEFT JOIN (
SELECT ip_id, COUNT(*) AS url_cnt
FROM ip_to_url
GROUP BY ip_id
) AS u ON u.ip_id = ia.id
WHERE
(%s IS NULL OR ia.ip_address LIKE %s)
AND (%s IS NULL OR au.uname LIKE %s)
AND (%s IS NULL OR ia.risk_rank = %s);
'''
# 构造参数
ip_like = f"%{IP}%" if IP else None
user_like = f"%{user}%" if user else None
rk = risk_rank
params = (
ip_like, ip_like,
user_like, user_like,
rk, rk,
)
# cursor.execute(sql, params)
# rows = cursor.fetchall()
datas = self.safe_do_select(strsql,params)
return datas
def get_ip_info_db(self,IP):
strsql = '''
select au.ID,au.tellnum,au.tell_username from assets_user as au
left join ip_assets as ai on ai.owner_id = au.ID
where ai.ip_address = %s;
'''
params = (IP)
data = self.safe_do_select(strsql,params,1)
return data
def get_assets_users_db(self,uname):
if uname:
strsql = "select ID,uname,tellnum,tell_username from assets_user where uname like %s;"
params = (f'%{uname}%')
datas = self.safe_do_select(strsql,params)
else:
strsql = "select ID,uname,tellnum,tell_username from assets_user;"
datas = self.do_select(strsql)
return datas
def update_assets_users_db(self,IP,owner_id,itype):
if itype ==1:
strsql = '''
update ip_assets set owner_id = %s where ip_address = %s;
'''
else:
strsql = '''
update url_assets set owner_id = %s where ID = %s;
'''
params = (owner_id,IP)
bok,_ = self.safe_do_sql(strsql,params)
error = ""
if not bok:
error = "修改资产所属用户失败"
return bok,error
def get_port_latest_db(self,ip):
strsql = '''
select po.port,po.service,po.version,po.status from port_assets as po
left join ip_assets as ip
on ip.id = po.ip_id and ip.scan_count = po.scan_count
where ip.ip_address = %s
'''
params = (ip)
datas = self.safe_do_select(strsql,params)
return datas
def get_ip_url_latest_db(self,ip):
strsql = '''
select url.URL,url.subdomains,url.registrar,url.emails,url.creation_date,url.expiration_date from url_assets as url
left join ip_to_url as i2u on i2u.url_id = url.ID
left join ip_assets as ip on ip.id = i2u.ip_id
where ip_address = %s;
'''
params = (ip)
datas = self.safe_do_select(strsql,params)
return datas
def get_ip_url_history_db(self,ip):
#先把ip_id获取到
strsql = "select id from ip_assets where ip_address=%s;"
params = (ip,)
data = self.safe_do_select(strsql,params,1)
if not data:
return None
ip_id = data[0]
strsql = '''
select url.URL,i2u.create_time as time,'add' as type from ip_to_url as i2u
left join url_assets as url on url.ID = i2u.url_id
where ip_id=%s
union all
select url.URL,i2u_his.del_time as time,'del' as type from ip_to_url_his as i2u_his
left join url_assets as url on url.ID = i2u_his.url_id
where ip_id=%s
order by time DESC;
'''
params =(ip_id,ip_id)
datas = self.safe_do_select(strsql,params)
return datas
def del_url_assets_db(self,url_id):
#url assets
strsql ="delete from url_assets where ID=%s;"
params = (url_id,)
bok,_ = self.safe_do_sql(strsql,params)
#url_assets_his
strsql = "delete from url_assets_his where url_id=%s;"
bok, _ = self.safe_do_sql(strsql, params)
#ip_to_url
strsql = "delete from ip_to_url where url_id = %s;"
bok, _ = self.safe_do_sql(strsql, params)
#ip_to_url_his
strsql = "delete from ip_to_url_his where url_id = %s;"
bok, _ = self.safe_do_sql(strsql, params)
return True,"删除URL资产成功"
def get_last_task_by_ip(self,ip):
#寻找该IP最新完成的任务
strsql = '''
SELECT t.ID AS latest_task_id,t.start_time
FROM ip_assets AS ia
JOIN task_to_ip AS ti ON ia.id = ti.ip_id
JOIN task AS t ON ti.task_id = t.ID
WHERE ia.ip_address = %s
ORDER BY t.start_time DESC
LIMIT 1;
'''
params = (ip,)
data = self.safe_do_select(strsql ,params,1)
if data:
return data[0]
else:
return None
def del_ip_assets(self,ip):
#删除IP资产 -- 资产库中跟该资产相关的数据都需要删除,return bok,error
bok = False
# ip_assets
sql = """
DELETE FROM ip_assets
WHERE ip_address = %s
RETURNING id;
"""
params = (ip,)
bok,ip_id= self.safe_do_sql(sql,params,2)
if ip_id:
#task_to_ip
strsql = "delete from task_to_ip where ip_id = %s;"
bok,_ = self.safe_do_sql(strsql,params)
#ip_to_url
strsql = "delete from ip_to_url where ip_id = %s;"
bok, _ = self.safe_do_sql(strsql, params)
#ip_to_url_his
strsql = "delete from ip_to_url_his where ip_id = %s;"
bok, _ = self.safe_do_sql(strsql, params)
#port_assets
strsql = "delete from port_assets where ip_id = %s;"
bok, _ = self.safe_do_sql(strsql, params)
return True,"删除成功"
def get_url_assets_db(self,url,owner,email):
url_assets = []
strsql = '''
select url.ID,url.URL,au.uname,url.emails,url.update_time,url.expiration_date,ip.ip_count,url.Registrant,au.tellnum,au.tell_username,au.ID from url_assets as url
left join assets_user as au on au.ID = url.owner_id
left join (select url_id,count(*) as ip_count from ip_to_url group by url_id) as ip on url.ID = ip.url_id
'''
conditions = []
params = []
if url and url.strip():
conditions.append("url.URL like %s")
params.append(f"%{url}%")
if owner and owner.strip():
conditions.append("au.uname like %s")
params.append(f"%{owner}%")
if email and email.strip():
conditions.append("url.emails like %s")
params.append(f"%{email}%")
if len(conditions) > 0:
strsql += " WHERE " + " AND ".join(conditions)
# 执行查询(将参数转为元组)
url_assets = self.safe_do_select(strsql, tuple(params))
return url_assets
def get_url_to_ip_db(self,url_id):
strsql = '''
select ia.ip_address,itu.create_time from ip_assets ia
left join ip_to_url itu on itu.ip_id = ia.id
where itu.url_id = %s;
'''
params = (url_id,)
last_to_ips = self.safe_do_select(strsql,params)
strsql = '''
select ia.ip_address,itu.del_time from ip_assets ia
left join ip_to_url_his itu on itu.ip_id = ia.id
where itu.url_id = %s;
'''
params = (url_id,)
his_to_ips = self.safe_do_select(strsql, params)
return last_to_ips,his_to_ips
def get_owner_db(self,owner, owner_type, contact, tellnum):
strsql = '''
SELECT
u.ID,
u.itype,
u.uname,
u.tellnum,
u.tell_username,
u.ID_num,
IFNULL(ip.ip_count, 0) AS ip_count,
IFNULL(url.url_count, 0) AS url_count,
IFNULL(ip.ip_count, 0) + IFNULL(url.url_count, 0) AS total_assets
FROM assets_user u
LEFT JOIN (
SELECT owner_id, COUNT(*) AS ip_count
FROM ip_assets
GROUP BY owner_id
) ip ON u.ID = ip.owner_id
LEFT JOIN (
SELECT owner_id, COUNT(*) AS url_count
FROM url_assets
GROUP BY owner_id
) url ON u.ID = url.owner_id
'''
conditions = []
params = []
# 按需添加其他条件
if owner and owner.strip():
conditions.append("uname like %s")
params.append(f"%{owner}%")
if owner_type and owner_type.strip():
conditions.append("itype=%s")
params.append(owner_type)
if contact and contact.strip():
conditions.append("tell_username like %s")
params.append(f"%{contact}%")
if tellnum and tellnum.strip():
conditions.append("tellnum like %s")
params.append(f"%{tellnum}%")
# 组合完整的WHERE子句
if len(conditions) > 0:
strsql += " WHERE " + " AND ".join(conditions)
# 执行查询(将参数转为元组)
datas = self.safe_do_select(strsql, tuple(params))
return datas
def del_owner_db(self,id):
strsql = "delete from assets_user where ID=%s;"
params = (id,)
bok,_ = self.safe_do_sql(strsql,params)
#删除IP和URL与owner的关系
strsql = "update ip_assets set owner_id = 0 where owner_id = %s;"
bok,_ = self.safe_do_sql(strsql,params)
strsql = "update url_assets set owner_id = 0 where owner_id = %s;"
bok, _ = self.safe_do_sql(strsql, params)
return True,""
def test(self):
# 建立数据库连接
@ -438,6 +900,9 @@ app_DBM = DBManager()
app_DBM.connect()
if __name__ == "__main__":
mDBM = DBManager()
mDBM.connect()
print(mDBM.start_task("11","22"))
# mDBM = DBManager()
# mDBM.connect()
# print(mDBM.start_task("11","22"))
list_a = ['1','2','3','4','5','6']
str_a = ','.join(list_a)
print(str_a)

30
mycode/DataFilterManager.py

@ -1,19 +1,41 @@
import tldextract
class DataFilterManager:
def __init__(self,target,fake_target):
self.real_target = target
self.real_domain = ""
self.fake_target = fake_target
self.init_DataFilter()
def init_DataFilter(self):
if self.fake_target == "czzfkjxx":
try:
extracted = tldextract.extract(self.real_target)
self.real_domain = extracted.domain
except Exception as e:
print(f"{self.real_target}不合法!")
def filter_prompt(self,prompt):
fake_prompt = prompt.replace(self.real_target,self.fake_target)
if self.real_domain:
fake_prompt = prompt.replace(self.real_domain, self.fake_target)
else:
fake_prompt = prompt.replace(self.real_target,self.fake_target)
return fake_prompt
def filter_instruction(self,instruction):
real_instruction = instruction.replace(self.fake_target,self.real_target)
if self.real_domain:
real_instruction = instruction.replace(self.fake_target,self.real_domain)
else:
real_instruction = instruction.replace(self.fake_target, self.real_target)
return real_instruction
def filter_result(self,instr,result):
fake_instr = instr.replace(self.real_target,self.fake_target)
fake_result = result.replace(self.real_target,self.fake_target)
if self.real_domain:
fake_instr = instr.replace(self.real_domain, self.fake_target)
fake_result = result.replace(self.real_domain, self.fake_target)
else:
fake_instr = instr.replace(self.real_target,self.fake_target)
fake_result = result.replace(self.real_target,self.fake_target)
return fake_instr,fake_result

220
mycode/LLMManager.py

@ -12,12 +12,15 @@ from openai import OpenAIError, APIConnectionError, APITimeoutError
from myutils.ConfigManager import myCongif
from myutils.MyTime import get_local_timestr
from myutils.MyLogger_logger import LogHandler
from myutils.ContentManager import ContentManager
from mycode.CommandVerify import g_CV
class LLMManager:
def __init__(self,illm_type):
self.logger = LogHandler().get_logger("LLMManager")
self.api_key = None
self.api_url = None
self.ContM = ContentManager()
#temperature设置
if illm_type == 0: #腾讯云
@ -57,38 +60,37 @@ class LLMManager:
- 仅在发现新信息或漏洞时新增子节点
- 确保每个新增节点匹配测试指令
'''
# 初始化messages
def build_initial_prompt(self,node):
def build_initial_prompt(self,node,itype):
if not node:
return
#根节点初始化message----后续有可能需要为每个LLM生成不同的system msg
node.parent_messages = [{"role": "system",
"content":'''
你是一位渗透测试专家来指导本地程序进行渗透测试由你负责动态控制整个渗透测试过程根据当前测试状态和返回结果决定下一步测试指令推动测试前进直至完成渗透测试
你是一位资深的渗透测试专家现在由你来指导针对一个目标的渗透测试工作需要生成具体的指令交给本地程序执行再根据本地程序提交的执行结果规划下一步指令直至全面完成渗透测试
**总体要求**
1.以测试目标为根节点结合信息收集和测试反馈的结果以新的测试点作为子节点逐步规划和推进下一步测试形成树型结构测试树测试点需尽量全面
2.只有当收到当前节点的所有测试指令的结果且没有新的测试指令需要执行时再判断是否需要新增子节点进一步进行验证测试若没有则结束该路径的验证
3.若一次性新增的节点过多无法为每个节点都匹配测试指令请优先保障新增测试节点的完整性若有新增的节点未能匹配测试指令必须返回未匹配指令的节点列表
4.生成的指令有两类节点指令和测试指令指令之间必须以空行间隔不能包含注释和说明
5.本地程序会执行生成的指令但不具备分析判断和保持会话能力只会把执行结果返回提交
6.只有当漏洞验证成功后才添加该节点的漏洞信息
7.若无需要处理的节点数据节点指令可以不生成
8.若节点已完成测试测试指令可以不生成
**测试指令生成准则**
1.可以是dash指令也可以是python指令必须按格式要求生成
2.必须对应已有节点或同时生成新增节点指令
3.优先使用覆盖面广成功率高的指令不要生成重复的指令
4.若需要多条指令配合测试请生成对应的python指令完成闭环返回
5.避免用户交互必须要能返回
1.以测试目标为根节点以测试点作为子节点的形式来规划整个渗透测试方案
2.测试点的规划需要基于执行结果是测试目标涉及的且是完整的具体为a.完成信息收集根据信息收集到的内容所有可能存在中高风险的测试点b.漏洞验证成功还能进一步利用的测试点
3.新增测试点的约束只有当当前节点提交了所有测试指令的执行结果且没有新的测试指令需要验证时再统一判断是否需要新增子节点进一步进行验证测试若没有则结束该路径的验证
4.若一次性新增的子节点过多无法为每个节点都匹配测试指令请优先保障新增测试节点的全面
5.生成的指令有两类节点指令和测试指令指令之间必须以空行间隔不能包含注释和说明
6.若无节点操作节点指令可以不生成若当前节点已完成测试测试指令可以不生成
7.只有当漏洞验证成功后才能生成漏洞验证成功的指令避免误报
**节点指令格式**
- 新增节点{\"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\": \"节点\"};
**测试指令格式**
- dash指令```dash-[节点路径]指令内容```包裹若涉及到多步指令请生成python指令
- python指令```python-[节点路径]指令内容```包裹主函数名为dynamic_fun需包含错误处理必须返回一个tuple(status, output)
- [节点路径]为从根节点到目标节点的完整层级路径
**测试指令生成准则**
1.可以是dash指令或python指令必须按格式要求生成
2.必须对应已有节点或同时生成对应新增节点指令
3.优先使用覆盖面广成功率高的指令不能同时生成重复或作用覆盖的指令
4.若需要多条指令配合测试请生成对应的python指令完成闭环返回
5.避免用户交互必须要能返回返回的结果需要能利于你规划下一步指令
**核心要求**
- 指令之间必须要有一个空行
- 需确保测试指令的节点路径和指令的目标节点一致,例如针对子节点的测试指令节点路径不能指向当前节点
@ -98,22 +100,85 @@ class LLMManager:
```dash-[目标系统->192.168.1.100->3306端口]
mysql -u root -p 192.168.1.100
```
'''}] # 一个messages
#---2025-5-15分阶段方案---
def build_init_info_prompt(self,node):
if not node:
return
# 根节点初始化message----后续有可能需要为每个LLM生成不同的system msg
node.parent_messages = [{"role": "system","content": '''
你是一位资深的渗透测试专家现在由你来指导针对一个目标的渗透测试工作需要生成具体的指令交给本地程序执行再根据本地程序提交的执行结果规划下一步指令直至全面完成渗透测试当前为信息收集阶段
**总体要求**
1.请针对测试目标生成信息收集的测试指令不能包含注释和说明严格遵守格式要求不要有无关的前缀后缀等内容
2.每条指令必须有独特且明确的目的避免功能重复或范围重叠可以是dash指令或python指令
3.收集的信息包括域名的相关信息IP地址信息对应端口的相关信息等参考**JSON结果格式**中的内容不需要额外采集其它信息
4.若目标是URL除获取域名相关信息外还需要获取域名指向的IP再进一步解析获取这些IP的Por信息
5.若目标是IP则无需尝试获取对应的URL信息只需解析获取该IP的Port信息
6.完成URL和IP端口信息的收集后请按照格式要求封装结果信息并检查无遗漏无重复后再返回
7.在信息收集阶段节点路径都为当前节点包括测试目标是域名需要对测试目标指向的IP进行信息收集时IP节点由本地程序在渗透测试阶段统一创建
**响应示例**
- [{dash指令},{python指令}]
- [{JSON结果}]
**测试指令格式**
- dash指令{\"action\":\"dash\",\"path\":\"节点路径\",\"content\":\"指令内容\"}
- python指令{\"action\":\"python\",\"path\":\"节点路径\",\"content\":\"指令内容\"}
* python主函数名为dynamic_fun需包含错误处理必须返回一个tuple(status, output)
- [节点路径]为从根节点到目标节点的完整层级路径,"->"关联目标系统->192.168.1.100
**JSON结果格式**
- JSON结果{\"action\":\"asset\",\"URL\":{URL信息json},\"IPS\":[\"IP\":\"192.168.1.100\",\"IPtype\":\"IPv4/IPv6\",\"Ports\":[端口信息json]]}
* 端口信息json{\"Port\":\"端口号\",\"Service\":\"服务名称\",\"Version\":\"版本号\",\"Protocol\":\"TCP/UDP\",\"Status\":\"open/closed/filtered\"}
* URL信息json{\"Domain\":\"域名\",\"Subdomains\":\"[子域名1,子域名2]\",\"Registrant\":\"注册人\",\"Email\":\"注册邮箱\",\"Registrar\":\"注册商\",\"Creation_date\":\"创建日期\",\"Expiration_date\":\"到期日期\"}
- 若URL无效JSON中URL字段置空若无子域名Subdomains置空数组若无端口信息Ports字段返回空数组
**核心要求**
- 返回内容必须严格遵守响应示例不允许有其他内容或说明
- 测试指令和JSON结果必须严格遵守对应格式要求
'''}] # 一个messages
def build_init_attact_prompt(self,node):
if not node:
return
# 根节点初始化message----后续有可能需要为每个LLM生成不同的system msg
node.parent_messages = [{"role": "system","content": '''
你是一位资深的渗透测试专家现在由你来指导针对一个目标的渗透测试工作需要生成具体的指令交给本地程序执行再根据本地程序提交的执行结果规划下一步指令直至全面完成渗透测试
**总体要求**
1.以测试目标为根节点以测试点作为子节点的形式来规划整个渗透测试方案
2.测试点的规划需要基于执行结果是测试目标涉及的且是完整的具体为a.完成信息收集根据信息收集到的内容所有可能存在中高风险的测试点b.漏洞验证成功还能进一步利用的测试点
3.新增测试点的约束只有当当前节点提交了所有测试指令的执行结果且没有新的测试指令需要验证时再统一判断是否需要新增子节点进一步进行验证测试若没有则结束该路径的验证
4.若一次性新增的子节点过多无法为每个节点都匹配测试指令请优先保障新增测试节点的完整
5.生成的指令有两类节点指令和测试指令不能包含注释和说明严格遵守格式要求不要有无关的前缀后缀等内容
6.若无节点操作节点指令可以不生成若当前节点已完成测试测试指令可以不生成
7.只有当漏洞验证成功后才能生成漏洞验证成功的指令避免误报
**响应示例**
- [{节点指令},{测试指令}]
**节点指令格式**
- 新增节点{\"action\":\"add_node\", \"parent\": \"父节点\", \"nodes\": \"节点1,节点2\"};
- 漏洞验证成功{\"action\": \"find_vul\", \"node\": \"节点\",\"vulnerability\": {\"name\":\"漏洞名称\",\"risk\":\"风险等级(低危/中危/高危)\",\"info\":\"补充信息(没有可为空)\"}};
- 节点完成测试{\"action\": \"end_work\", \"node\": \"节点\"};
**测试指令格式**
- dash指令{\"action\":\"dash\",\"path\":\"节点路径\",\"content\":\"指令内容\"}
- python指令{\"action\":\"python\",\"path\":\"节点路径\",\"content\":\"指令内容\"}
* python主函数名为dynamic_fun需包含错误处理必须返回一个tuple(bool, output)
- [节点路径]为从根节点到目标节点的完整层级路径"->"关联目标系统->192.168.1.100
**测试指令生成准则**
1.可以是dash指令或python指令必须按格式要求生成
2.必须对应已有节点或同时生成对应新增节点指令
3.必须有独特且明确的目的避免功能重复或范围重叠优先使用覆盖面广成功率高的指令
4.若需要多条指令配合测试请生成对应的python指令完成闭环返回
5.避免用户交互必须要能返回返回的结果需要能利于你规划下一步指令
**核心要求**
- 返回内容必须严格遵守响应示例不允许有其他内容或说明
- 节点指令和测试指令必须严格遵守对应的格式要求
- 需确保测试指令的节点路径和指令的目标节点一致,例如针对子节点的测试指令节点路径不能指向当前节点
'''}] # 一个messages
# 调用LLM生成指令
def get_llm_instruction(self,prompt,node,DataFilter):
def get_llm_instruction(self,sendmessage):
'''
1.由于大模型API不记录用户请求的上下文一个任务的LLM不能并发
:param prompt:用户本次输入的内容
:return: instr_list
:sendmessage :发送的message
:return: iresult,reasoning_content,content,error
'''
#添加本次输入入该节点的message队列
message = {"role":"user","content":prompt}
node.cur_messages.append(message) #更新节点message
sendmessage = []
sendmessage.extend(node.parent_messages)
sendmessage.extend(node.cur_messages)
#提交LLM
#准备请求参数
params = {
@ -134,18 +199,18 @@ mysql -u root -p 192.168.1.100
response = self.client.chat.completions.create(**params)
except APITimeoutError:
self.logger.error("LLM API 请求超时")
return False, "","","", f"调用超时(model={self.model})"
return -1,"","", f"调用超时(model={self.model})"
except APIConnectionError as e:
self.logger.error(f"网络连接错误: {e}")
return False, "","", "", "网络连接错误"
return -1,"", "", "网络连接错误"
except OpenAIError as e:
# 包括 400/401/403/500 等各种 API 错误
self.logger.error(f"LLM API 错误: {e}")
return False, "","", "", f"API错误: {e}"
return -1,"", "", f"API错误: {e}"
except Exception as e:
# 兜底,防止意外
self.logger.exception("调用 LLM 时出现未预期异常")
return False, "","", "", f"未知错误: {e}"
return -1,"", "", f"未知错误: {e}"
reasoning_content = ""
content = ""
@ -174,14 +239,21 @@ mysql -u root -p 192.168.1.100
content = choice.content
else:
self.logger.error("处理到未预设的模型!")
return False,"","","","处理到未预设的模型!"
# 记录llm历史信息
node.cur_messages.append({'role': 'assistant', 'content': content})
print(content)
real_con = DataFilter.filter_instruction(content)
#按格式规定对指令进行提取
node_cmds,commands = self.fetch_instruction(real_con)
return True,node_cmds,commands,reasoning_content, content
content = choice.content
return 1,reasoning_content,content,""
def node_cmd_repair(self,part):
'''
对节点指令的合法性修复
:param part:
:return:
'''
#遇到漏洞赋值的节点指令缺少一个大括号,目前策略自动补全
# {"action":"find_vul", "node": "8180端口-Tomcat","vulnerability": {"name":"Tomcat弱口令漏洞","risk":"高危","info":"默认凭证tomcat:tomcat可访问管理控制台"}
whole = self.ContM.extract_json(part)
if not whole: #加一次就应该很少见了,不补充多次,也暂时只针对}
part += "}"
return part
def fetch_instruction(self,response_text):
'''
@ -195,51 +267,27 @@ mysql -u root -p 192.168.1.100
:param text: 输入文本
:return: node_cmds,python_blocks,shell_blocks
'''
#针对llm的回复,提取节点操作数据和执行的指令----
# 正则匹配 Python 代码块
python_blocks = re.findall(r"```python-(.*?)```", response_text, flags=re.DOTALL)
# 处理 Python 代码块,去除空行并格式化
python_blocks = [block.strip() for block in python_blocks]
#正则匹配shell指令
shell_blocks = re.findall(f"```dash-(.*?)```", response_text, flags=re.DOTALL)
shell_blocks = [block.strip() for block in shell_blocks]
# 按连续的空行拆分
# 移除 Python和dash 代码块
text_no_python = re.sub(r"```python.*?```", "PYTHON_BLOCK", response_text, flags=re.DOTALL)
text = re.sub(r"```dash.*?```", "SHELL_BLOCK", text_no_python, flags=re.DOTALL)
# 这里用 \n\s*\n 匹配一个或多个空白行
parts = re.split(r'\n\s*\n', text)
node_cmds = []
commands = []
python_index = 0
shell_index = 0
for part in parts:
part = part.strip()
if not part:
continue
if "PYTHON_BLOCK" in part:
# 还原 Python 代码块
commands.append(f"python-code {python_blocks[python_index]}")
python_index += 1
elif "SHELL_BLOCK" in part:
commands.append(shell_blocks[shell_index])
shell_index +=1
else:#其他的认为是节点操作指令--指令格式还存在不确定性,需要正则匹配,要求是JSON
pattern = re.compile(r'\{(?:[^{}]|\{[^{}]*\})*\}')
# 遍历所有匹配到的 JSON 结构
# strlines = part.strip('\n') #按行拆分,避免贪婪模式下,匹配到多行的最后一个}
# for strline in strlines:
for match in pattern.findall(part): #正常只能有一个
try:
node_cmds.append(json.loads(match)) # 解析 JSON 并添加到列表
except json.JSONDecodeError as e:#解析不了的不入队列
self.logger.error(f"LLM-{part}-JSON 解析错误: {e}") #这是需不需要人为介入?
return node_cmds,commands
# 序列化为JSON
try:
json_datas = json.loads(response_text)
except json.JSONDecodeError as e:
self.logger.debug(f"返回的内容转化成JSON失败,不符合json格式--{response_text}")
#反馈给LLM,重新返回内容
return False,node_cmds,commands,f"返回的内容json.loads失败,{str(e)}"
#对返回内容的json关键字验证
bok, strerror = g_CV.verify_node_cmds(json_datas)
if not bok:
return False, node_cmds, commands, strerror
#通过合法性校验--进行后续处理--拆分node_cmd 和 commands
for part in json_datas:
action = part["action"]
if action == "dash" or action == "python":
commands.append(part)
else:
node_cmds.append(part)
return True,node_cmds,commands,""
def test_llm(self):
messages = [
@ -255,5 +303,11 @@ mysql -u root -p 192.168.1.100
if __name__ == "__main__":
llm = LLMManager(3)
strcontent = '''
```dash-[目标系统->192.168.3.107]nmap -sV -Pn -p- 192.168.3.107```
```dash-[目标系统->192.168.3.107]dig +short -x 192.168.3.107```
'''
node_cmds, commands = llm.fetch_instruction(strcontent)
print(node_cmds)
print(commands)

5
mycode/PythonTManager.py

@ -20,7 +20,10 @@ class PythonTManager:
return self.python_tool.start_pool()
def shutdown_pool(self):
self.python_tool.shutdown_pool()
try:
self.python_tool.shutdown_pool()
except Exception as e:
print(f"关闭进程池异常{str(e)}")
def is_pool_active(self):
return self.python_tool.pool_active

39
mycode/PythoncodeTool.py

@ -27,7 +27,13 @@ import smb
import pexpect
import smbclient
import binascii
import ftplib
import threading
import whois
import sublist3r
from mysql.connector import Error
from Crypto.Cipher import DES
from packaging import version
from impacket.smbconnection import SMBConnection
from itertools import product
from socket import create_connection
@ -63,7 +69,8 @@ def _execute_dynamic(instruction_str):
'set': set, 'str': str, 'sum': sum, 'type': type,
'open': open, 'Exception': Exception, 'locals': locals,
'ConnectionResetError':ConnectionResetError,'BrokenPipeError':BrokenPipeError,
'bytes':bytes,'tuple':tuple,'format':format
'bytes':bytes,'tuple':tuple,'format':format,'next':next,'StopIteration':StopIteration,
'bytearray':bytearray,'getattr':getattr,'hasattr':hasattr,'isinstance':isinstance,'dir':dir,
}
# 构造安全的 globals
safe_globals = {
@ -106,7 +113,12 @@ def _execute_dynamic(instruction_str):
'smbclient':smbclient,
'binascii':binascii,
'Error':Error,
'SMBConnection':SMBConnection
'SMBConnection':SMBConnection,
'version':version,
'DES':DES,
'ftplib':ftplib,
'whois':whois,
'sublist3r':sublist3r,
}
safe_locals = {}
try:
@ -149,10 +161,25 @@ class PythoncodeTool():
def shutdown_pool(self):
if self.proc_pool is not None and self.pool_active:
print("关闭进程池...")
self.proc_pool.shutdown(wait=False) #wait=True 是阻塞执行,False立即返回
self.pool_active = False
self.proc_pool = None
try:
print("关闭进程池...")
pool = self.proc_pool
self.pool_active = False
self.proc_pool = None
def _shutdown_background():
try:
# 这里是真正阻塞等待队列管理和子进程退出
pool.shutdown(wait=True)
print("进程池已完全关闭。")
except Exception as e:
print(f"后台关闭进程池时出错: {e}")
# 启动一个守护线程来做真正的 shutdown(wait=True)
t = threading.Thread(target=_shutdown_background, daemon=True)
t.start()
except Exception as e:
print(f"子进程关闭异常。。{e}")
else:
print("进程池已经是关闭状态")

209
mycode/TargetManager.py

@ -2,6 +2,16 @@
对目标资产的管理包括信息的更新维护等
'''
import re
import socket
import ipaddress
import geoip2.database
import ipwhois
import requests
import whois
import dns.resolver
import ssl
from urllib.parse import urlparse
from datetime import datetime
#pattern = r'^(https?://)?((?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}|(?:\d{1,3}\.){3}\d{1,3})(:\d+)?(/.*)?$'
pattern = r'^(https?://)?((?:[0-9]{1,3}\.){3}[0-9]{1,3}|(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,})(:\d+)?(/.*)?$'
@ -10,13 +20,44 @@ class TargetManager:
def __init__(self):
pass
def extract_and_store_ips(self,str_target: str):
# 正则匹配IP地址(包含IPv4、IPv6及带端口的情况)
ip_pattern = r'''
(?P<ipv6>\[?([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\]?| # 完整IPv6
::([0-9a-fA-F]{1,4}:){0,6}[0-9a-fA-F]{1,4}| # 缩写IPv6
(?P<ipv4>(\d{1,3}\.){3}\d{1,3})(?::\d+)? # IPv4及端口
'''
candidates = re.finditer(ip_pattern, str_target, re.VERBOSE)
valid_ips = []
for match in candidates:
raw_ip = match.group().lstrip('[').rstrip(']') # 处理IPv6方括号
# 分离IP和端口(如192.168.1.1:8080)
if ':' in raw_ip and not raw_ip.count(':') > 1: # 排除IPv6的冒号
ip_part = raw_ip.split(':')[0]
else:
ip_part = raw_ip
# 验证IP有效性并分类
try:
ip_obj = ipaddress.ip_address(ip_part)
ip_type = 'v6' if ip_obj.version == 6 else 'v4'
valid_ips.append({
'binary_ip': ip_obj.packed,
'ip_type': ip_type,
'original': ip_part
})
except ValueError:
continue
# 辅助函数:验证IPv4地址的有效性
def _is_valid_ipv4(self,ip):
parts = ip.split('.')
if len(parts) != 4:
return False
for part in parts:
if not part.isdigit() or not 0 <= int(part) <= 255:
if not part.isdigit():
return False
return True
@ -24,41 +65,165 @@ class TargetManager:
def validate_and_extract(self,input_str):
'''
:param input_str:
:return: bool,str,int(1-IP,2-domain)
:return: bool,real_target,int(1-IP,2-domain),fake_target
'''
regex_match = re.fullmatch(pattern, input_str)
type = None
fake_target = ""
if regex_match:
domain_or_ip = regex_match.group(2)
# 仅对 IPv4 格式的字符串进行有效性验证
if re.fullmatch(r'\d{1,3}(\.\d{1,3}){3}', domain_or_ip):
if not self._is_valid_ipv4(domain_or_ip):
return False, None,type,fake_target
else:
type = 1 #IP
fake_target = "192.168.3.107"
real_target = ""
target_type,target = self.is_valid_target(input_str)
if not target_type: #非法目标
return False,input_str,type,fake_target
if target_type =="IPv4" or target_type=="IPv6":
type = 1 #IP
real_target = target
fake_target = "192.168.3.107"
elif target_type == "URL":
type = 2 #domain
real_target = target
fake_target = "czzfkjxx"
else: #目标不合法
return False,real_target,type,fake_target
return True,real_target,type,fake_target
#验证目标是否合法
def is_valid_target(self,target):
'''
检查目标的合法性并对于URL地址提取域名部分若是ip的URL提取IP
:param target:
:return: target_type new_target
'''
# Check if target is a valid IP address (IPv4 or IPv6)
try:
ip = ipaddress.ip_address(target)
if ip.version == 4:
return 'IPv4',target
elif ip.version == 6:
return 'IPv6',target
except ValueError:
pass
# Check if target is a valid URL
try:
result = urlparse(target)
# Only allow http or https schemes
if not result.scheme:
result = urlparse('http://'+target)
netloc = result.netloc
if not netloc:
return None,None
# Handle IPv6 addresses in URLs (enclosed in brackets)
if netloc.startswith('[') and netloc.endswith(']'):
ip_str = netloc[1:-1]
try:
ipaddress.IPv6Address(ip_str)
return 'IPv6',ipaddress
except ValueError:
return None,None
# Handle potential IPv4 addresses
elif self._is_valid_ipv4(netloc):
try:
ipaddress.IPv4Address(netloc)
return 'IPv4',ipaddress
except ValueError:
return None,None
# If not an IP-like string, assume it's a domain name and accept
return 'URL',netloc
except ValueError:
return None,None
def collect_ip_info(self,ip):
info = {}
try:
# 首先尝试 RDAP 查询
obj = ipwhois.IPWhois(ip)
whois_info = obj.lookup_rdap()
info['asn'] = whois_info.get('asn') # 获取 ASN
info['isp'] = whois_info.get('network', {}).get('name') # 获取 ISP
except (ipwhois.exceptions.IPDefinedError, ipwhois.exceptions.ASNRegistryError,
requests.exceptions.RequestException) as e:
# 如果 RDAP 失败,回退到 WHOIS 查询
try:
whois_info = obj.lookup_whois()
info['asn'] = whois_info.get('asn') # 获取 ASN
if whois_info.get('nets'):
# 从 WHOIS 的 'nets' 中提取 ISP(通常在 description 字段)
info['isp'] = whois_info['nets'][0].get('description')
except Exception as e:
info['whois_error'] = str(e) # 记录错误信息
return info
def collect_domain_info(self,domain):
info = {}
try:
w = whois.whois(domain)
info['registrar'] = w.registrar
# 处理 creation_date
if isinstance(w.creation_date, list):
info['creation_date'] = [dt.strftime('%Y-%m-%d %H:%M:%S') if isinstance(dt, datetime) else str(dt) for
dt in w.creation_date]
elif isinstance(w.creation_date, datetime):
info['creation_date'] = w.creation_date.strftime('%Y-%m-%d %H:%M:%S')
else:
info['creation_date'] = str(w.creation_date)
# 处理 expiration_date
if isinstance(w.expiration_date, list):
info['expiration_date'] = [dt.strftime('%Y-%m-%d %H:%M:%S') if isinstance(dt, datetime) else str(dt) for
dt in w.expiration_date]
elif isinstance(w.expiration_date, datetime):
info['expiration_date'] = w.expiration_date.strftime('%Y-%m-%d %H:%M:%S')
else:
type = 2 #domain
fake_target = "www.czzfxxkj.com"
return True, domain_or_ip,type,fake_target
info['expiration_date'] = str(w.expiration_date)
info['user_name'] = str(w.name)
info['emails'] = str(w.emails)
info['status'] = str(w.status)
except Exception as e:
info['whois_error'] = str(e)
try:
answers = dns.resolver.resolve(domain, 'A')
info['A_records'] = [r.to_text() for r in answers]
except Exception as e:
info['dns_error'] = str(e)
return info
def test(self,str_target):
bok, target, type, fake_target = self.validate_and_extract(str_target)
if not bok:
print(f"{str_target}目标不合法{target}")
else:
return False, None,type,fake_target
print(f"{str_target}目标合法{target} ---- {fake_target}")
g_TM = TargetManager()
if __name__ == "__main__":
tm = TargetManager()
# 示例测试
#tm = TargetManager()
#示例测试
test_cases = [
"http://192.168.1.1:8080/path",
"https://example.com",
"256.254.1111.23",
"8.8.8.8",
"2001:db8::1",
"http://www.crnn.cc/",
"https://www.crnn.cn",
"http://www.crnn.cc/product_category/network-security-services",
"192.168.1.1:80",
"example.com/path/to/resource",
"www.crnn.cn",
"oa.crnn.cn",
"ftp://invalid.com", # 不合规
"http://300.400.500.600" # 不合规
]
# test_cases = [
# "http://www.crnn.cc/",
# "http://www.crnn.cc/product_category/network-security-services"
# ]
#tm.test("https://www.crnn.cn")
for case in test_cases:
is_valid, result = tm.validate_and_extract(case)
print(f"输入: '{case}' → 合规: {is_valid}, 提取结果: {result}")
g_TM.test(case)

36
mycode/TaskManager.py

@ -31,7 +31,9 @@ class TaskManager:
#程序启动后,加载未完成的测试任务
def load_tasks(self):
'''程序启动时,加载未执行完成的,未点击结束的任务 -- task_status<>2'''
'''程序启动时,加载未执行完成的,未点击结束的任务 -- task_status<>2
#若不是异常停止,正常情况下,任务都应该是没有待办MQ的
'''
datas = app_DBM.get_run_tasks()
for data in datas:
task_id = data[0]
@ -51,15 +53,15 @@ class TaskManager:
task.init_task(task_id,attack_tree)
#开始任务 ---根据task_status来判断是否需要启动工作线程
if task_status == 1:
if self.cur_task_run_num < self.max_run_task: #load 是程序刚起,只有主线程,不加锁
bsuc,strout = task.start_task()
if bsuc:
self.cur_task_run_num +=1
if self.cur_task_run_num < self.max_run_task: #load 是程序刚起,只有主线程,不加锁
bsuc,strout = task.start_task()
if bsuc:
self.cur_task_run_num +=1
else:
task.update_task_status(0)
else:
task.update_task_status(0)
else:
self.logger.error("重载未结束任务,不应该超过最大运行数量的task_status为启动状态")
task.update_task_status(0)#尝试硬恢复
self.logger.error("重载未结束任务,不应该超过最大运行数量的task_status为启动状态")
task.update_task_status(0)#尝试硬恢复
# 内存保留task对象
self.tasks[task_id] = task
else:
@ -76,7 +78,7 @@ class TaskManager:
target_list = test_targets.split(",")
for target in target_list:
#这里判断目标的合法性
bok,target,type,fake_target = self.TargetM.validate_and_extract(target) #是否还需要判断待定?
bok,target,type,fake_target = self.TargetM.validate_and_extract(target) #若是url,target是域名部分
if not bok:#目标不合法
fail_list.append(target)
continue
@ -90,7 +92,7 @@ class TaskManager:
#获取task_id -- test_target,cookie_info,work_type,llm_type 入数据库
task_id = app_DBM.start_task(target,"",work_type,llm_type,fake_target)
if task_id >0:
#2025-4-28调整批量添加任务,默认不启动
#2025-4-28调整批量添加任务,默认不启动线程start_task
task.init_task(task_id)
#保留task对象
self.tasks[task_id] = task
@ -256,15 +258,8 @@ class TaskManager:
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:
if work_status == 0:
if not node.parent_messages: #如果messages为空--且不会是根节点
node.copy_messages(node.parent.parent_messages,node.parent.cur_messages)
bsuccess,error = node.updatemsg(newtype,newcontent,0) #取的第一条,也就修改第一条
return bsuccess,error
else:
return False,"当前节点的工作状态不允许修改MSG!"
bsuccess,error = node.updatemsg(newtype,newcontent,node.parent.parent_messages,node.parent.cur_messages,0)
return bsuccess,error
return False,"找不到对应节点!"
def del_node_instr(self,task_id,nodepath,instr):
@ -279,4 +274,5 @@ class TaskManager:
tasks = app_DBM.get_his_tasks(target_name,safe_rank,llm_type,start_time,end_time)
return tasks
g_TaskM = TaskManager() #单一实例

784
mycode/TaskObject.py

File diff suppressed because it is too large

64
myutils/ContentManager.py

@ -0,0 +1,64 @@
import json
class ContentManager:
def extract_json(self,s: str):
start = s.find('{')
if start < 0:
return None
depth = 0
for i, ch in enumerate(s[start:], start):
if ch == '{':
depth += 1
elif ch == '}':
depth -= 1
if depth == 0:
return s[start:i + 1]
return None # 没有闭合
def auto_complete_json(self,s: str) -> str:
"""
在字符串 s 自动检测未匹配的 { 并在末尾补全相应数量的 }
返回补全后的新字符串
"""
depth = 0
in_string = False
escape = False
for ch in s:
if escape:
escape = False
continue
if ch == '\\':
escape = True
continue
if ch == '"' and not escape:
in_string = not in_string
continue
if not in_string:
if ch == '{':
depth += 1
elif ch == '}':
if depth > 0:
depth -= 1
# depth 此时就是多余的 “{” 数量
if depth > 0:
s = s + '}' * depth
return s
def extract_and_fix_json(self,text: str):
"""
1. 找到首个 '{'然后尝试用 extract_json 的方式截取到末尾
2. 如果 extract_json 返回 None就用 auto_complete_json 补全后再试一次 json.loads
"""
# 找到第一个 {
start = text.find('{')
if start < 0:
raise ValueError("字符串中没有发现 '{'")
fragment = text[start:]
# 先自动补全一次
fixed = self.auto_complete_json(fragment)
# 再尝试解析
try:
return json.loads(fixed)
except json.JSONDecodeError as e:
raise ValueError(f"补全后仍解析失败: {e}")

7
myutils/PickleManager.py

@ -23,9 +23,10 @@ class PickleManager:
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)
if os.path.exists(filepath):
with self.lock:
with open(filepath, "rb") as f:
attack_tree = pickle.load(f)
return attack_tree
def DelData(self,filename=""):

36
myutils/ReadWriteLock.py

@ -0,0 +1,36 @@
import threading
class ReadWriteLock:
def __init__(self):
self._rw_lock = threading.Lock()
self._readers = 0
self._writers_waiting = 0
self._writer = False
self._cond = threading.Condition(self._rw_lock)
def acquire_read(self):
with self._cond:
# 如果已有写锁,或有写者在等待,都要等
while self._writer or self._writers_waiting > 0:
self._cond.wait()
self._readers += 1
def release_read(self):
with self._cond:
self._readers -= 1
if self._readers == 0:
self._cond.notify_all()
def acquire_write(self):
with self._cond:
self._writers_waiting += 1
# 等到没人读、没人写
while self._writer or self._readers > 0:
self._cond.wait()
self._writers_waiting -= 1
self._writer = True
def release_write(self):
with self._cond:
self._writer = False
self._cond.notify_all()

12
pipfile

@ -16,6 +16,12 @@ pip install dirsearch -i https://pypi.tuna.tsinghua.edu.cn/simple/
pip install pexpect -i https://pypi.tuna.tsinghua.edu.cn/simple/
pip install smbprotocol -i https://pypi.tuna.tsinghua.edu.cn/simple/
pip install ipwhois -i https://pypi.tuna.tsinghua.edu.cn/simple
pip install geoip2 -i https://pypi.tuna.tsinghua.edu.cn/simple
pip install python-whois -i https://pypi.tuna.tsinghua.edu.cn/simple
pip install tldextract -i https://pypi.tuna.tsinghua.edu.cn/simple
pip install sublist3r -i https://pypi.tuna.tsinghua.edu.cn/simple
apt install sublist3r
apt install gobuster
@ -55,6 +61,10 @@ 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
pip install packaging -i https://pypi.tuna.tsinghua.edu.cn/simple
pip install pycryptodome -i https://pypi.tuna.tsinghua.edu.cn/simple
systemctl start redis-server
systemctl enable redis-server
@ -67,4 +77,6 @@ fc-match Arial #验证安装
----python2------
----网络配置------
nmcli connection modify "zhang_wifi" ipv4.addresses 192.168.3.151/24 ipv4.gateway 192.168.3.1 ipv4.dns "8.8.8.8 114.114.114.114" ipv4.method manual

55
test.py

@ -73,7 +73,7 @@ if __name__ == "__main__":
current_path = os.path.dirname(os.path.realpath(__file__))
print(current_path)
test_type = 1
task_id = 49
task_id = 67
task_Object = TaskObject("test_target","cookie_info",1,1,1,"local_ip","",None)
if test_type == 0:
@ -81,32 +81,13 @@ if __name__ == "__main__":
elif test_type == 1:
# # 获取所有自定义函数详情 HIGH_RISK_FUNCTIONS = ['eval', 'exec', 'os.system', 'subprocess.call', 'subprocess.Popen']
instruction = '''python-code
import requests
def dynamic_fun():
import socket
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(20) # 设置超时时间为20秒
s.connect(("192.168.3.105", 11200))
# 发送畸形RTSP请求探测边界条件
payload = "DESCRIBE rtsp://192.168.3.105/../../../../etc/passwd RTSP/1.0\\\\r\\\\n"
payload += "CSeq: 6\\\\r\\\\n\\\\r\\\\n"
s.send(payload.encode())
response = s.recv(4096).decode()
s.close()
if "404" in response:
return (False, "存在输入过滤机制")
elif "root:" in response:
return (True, "成功读取敏感文件")
else:
return (False, f"未知响应:{response}")
r = requests.get('https://58.216.217.67/server-status', verify=False, timeout=5)
return (1, f'HTTP:{r.status_code} Headers:{r.headers}') if r.status_code==200 else (0, '')
except Exception as e:
return (False, f"连接异常:{str(e)}")
return (0, str(e))
'''
task_Object.PythonM.start_pool() #开个子进程池就行
start_time, end_time, bsuccess, instr, reslut, source_result, ext_params = task_Object.do_instruction(instruction)
@ -115,8 +96,8 @@ def dynamic_fun():
print("----执行结果----")
print(reslut)
elif test_type == 2: #给节点添加指令
node_path = "目标系统->192.168.3.105->52989端口"
instr_id = 3233
node_path = "目标系统->192.168.3.108->80端口->PHP版本漏洞检测"
instr_id = 3478
g_TaskM.load_tasks()
task = g_TaskM.tasks[task_id]
nodes = task.attack_tree.traverse_dfs()
@ -130,7 +111,7 @@ def dynamic_fun():
if "import" in str_instr:
str_instr = "python-code " + str_instr
cur_node.test_add_instr(str_instr)
cur_node.update_work_status(1)
cur_node.update_work_status(-4)
#保存数据
g_PKM.WriteData(task.attack_tree,str(task.task_id))
else:
@ -145,22 +126,32 @@ def dynamic_fun():
for node_name in unique_names:
print(node_name)
elif test_type == 4: # 修改Messages
attact_tree = g_PKM.ReadData("27")
attact_tree = g_PKM.ReadData("88")
# 创建一个新的节点
from mycode.AttackMap import TreeNode
testnode = TreeNode("test", 0)
LLM.build_initial_prompt(testnode) # 新的Message
testnode = TreeNode("test", 0,0)
LLM.build_init_attact_prompt(testnode) # 新的Message
systems = testnode.parent_messages[0]["content"]
# print(systems)
# 遍历node,查看有instr的ndoe
nodes = attact_tree.traverse_bfs()
for node in nodes:
node.parent_messages[0]["content"] = systems
g_PKM.WriteData(attact_tree, "27")
g_PKM.WriteData(attact_tree, "88")
print("完成Messgae更新")
elif test_type ==5:
mytest.dynamic_fun()
elif test_type == 6:
mytest.tmp_test()
import json
strIPS = '''
[ {"action":"asset","URL":{"Domain":"www.czzfkjxx.cn","Subdomains":[],"Registrant":"","Email":"","Registrar":"","Creation_date":"","Expiration_date":""},"IPS":[{"IP":"58.216.217.67","IPtype":"IPv4","Ports":[ {"Port":"25","Service":"smtp?","Version":"","Protocol":"tcp","Status":"open"}, {"Port":"80","Service":"http","Version":"Apache httpd","Protocol":"tcp","Status":"open"}, {"Port":"110","Service":"pop3?","Version":"","Protocol":"tcp","Status":"open"}, {"Port":"443","Service":"ssl/http","Version":"Apache httpd","Protocol":"tcp","Status":"open"} ]}]} ]
'''
node_json = json.loads(strIPS)
IPS = node_json[0]["IPS"]
URL = node_json[0]["URL"]
#task_Object.add_update_assets(URL,IPS,app_DBM)
task_Object.update_attack_tree(URL, IPS, None)
elif test_type == 7:
task_Object.test(50)
else:
pass

2
tools/ArpingTool.py

@ -3,7 +3,7 @@ from tools.ToolBase import ToolBase
class ArpingTool(ToolBase):
def validate_instruction(self, instruction):
#指令过滤
timeout = 0
timeout = 60*15
return instruction,timeout
def analyze_result(self, result,instruction,stderr,stdout):

27
tools/CurlTool.py

@ -13,16 +13,19 @@ class CurlTool(ToolBase):
# self.url = None
# self.verify_ssl = True
def get_time_out(self):
def get_time_out(self,instruction=""):
if "&&" in instruction or "||" in instruction:
return 60*15
return 61*2
def validate_instruction(self, instruction_old):
#instruction = instruction_old
#指令过滤
timeout = self.get_time_out() #curl指令遇到不返回的情况 curl --path-as-is -i http://192.168.204.137:8180/webapps/../conf/tomcat-users.xml
timeout = self.get_time_out(instruction_old) #curl指令遇到不返回的情况 curl --path-as-is -i http://192.168.204.137:8180/webapps/../conf/tomcat-users.xml
#添加-i 返回信息头
if 'base64 -d' in instruction_old:
return instruction_old
return instruction_old,timeout
# 分割成单词列表便于处理参数
curl_parts = instruction_old.split()
@ -38,10 +41,10 @@ class CurlTool(ToolBase):
else:
curl_parts.append('-i')
# 判断是否已经有 --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())) #添加超时时间
# 判断是否已经有 --max-time 参数 --curl经常组合其他指令使用,不加--max-time参数了,考usbprocess的time_out控制超时
# 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())) #添加超时时间
final_instruction = ' '.join(curl_parts)
return final_instruction, timeout
@ -76,6 +79,8 @@ class CurlTool(ToolBase):
def do_worker_subprocess(self,str_instruction,timeout,ext_params):
try:
# 执行命令,捕获输出为字节形式
if not timeout:
timeout = 60*15
if timeout:
result = subprocess.run(str_instruction, shell=True,timeout=timeout, capture_output=True)
else:
@ -298,7 +303,7 @@ class CurlTool(ToolBase):
return result
def analyze_result(self, result,instruction,stderr,stdout):
if len(result) < 2000:
if len(result) < 3000:
return result
if "curl: (28) Operation timed out after" in result or "Dload Upload Total Spent Left Speed" in result:
return "执行超时"
@ -329,12 +334,8 @@ class CurlTool(ToolBase):
result="该漏洞无法利用"
elif("-kv https://" in instruction or "-vk https://" in instruction):
result = self.get_ssl_info(stderr,stdout)
elif("grep " in instruction or " -T " in instruction or "Date:" in instruction):
elif("grep " in instruction or " -T " in instruction or "Date:" in instruction or "dirb" in instruction):
return result
# elif("-X POST " in instruction):
# 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:
result = self.get_info_curl(instruction,stdout,stderr)
return result

2
tools/DigTool.py

@ -3,7 +3,7 @@ from tools.ToolBase import ToolBase
class DigTool(ToolBase):
def validate_instruction(self, instruction):
#指令过滤
timeout = 0
timeout = 60*5
return instruction,timeout
def analyze_result(self, result,instruction,stderr,stdout):

2
tools/DirSearchTool.py

@ -3,7 +3,7 @@ from tools.ToolBase import ToolBase
class DirSearchTool(ToolBase):
def validate_instruction(self, instruction):
#指令过滤
timeout = 0
timeout = 60*15
if "-o " not in instruction or "--output=" not in instruction:
instruction += " -o ds_result.txt"
return instruction,timeout

2
tools/DirbTool.py

@ -6,7 +6,7 @@ from tools.ToolBase import ToolBase
class DirbTool(ToolBase):
def validate_instruction(self, instruction):
#指令过滤
timeout = 0
timeout = 60*15
instruction = instruction.strip()
if " -o" not in instruction:
instruction += " -o dirout.txt"

2
tools/Enum4linuxTool.py

@ -4,7 +4,7 @@ from mycode.Result_merge import my_merge
class Enum4linuxTool(ToolBase):
def validate_instruction(self, instruction):
#指令过滤
timeout = 0
timeout = 60*15
return instruction,timeout
def analyze_result(self, result,instruction,stderr,stdout):

2
tools/Hping3Tool.py

@ -10,7 +10,7 @@ class Hping3Tool(ToolBase):
:param instruction:
:return:
'''
timeout = 0
timeout = 60*15
# 拆分原始指令为列表
cmd_parts = instruction.split()

2
tools/HydraTool.py

@ -6,7 +6,7 @@ from tools.ToolBase import ToolBase
class HydraTool(ToolBase):
def validate_instruction(self, instruction):
timeout = 0
timeout = 60*15
current_path = os.path.dirname(os.path.realpath(__file__))
#hydra过滤 需要判断指令中添加字典文件存不存在
match_p = re.search(r'-P\s+([^\s]+)', instruction)

2
tools/KubehunterTool.py

@ -7,7 +7,7 @@ from tools.ToolBase import ToolBase
class KubehunterTool(ToolBase):
def validate_instruction(self, instruction):
#指令过滤
timeout = 0
timeout = 60*15
return instruction,timeout
def analyze_result(self, result,instruction,stderr,stdout):

2
tools/MedusaTool.py

@ -3,7 +3,7 @@ from tools.ToolBase import ToolBase
class MedusaTool(ToolBase):
def validate_instruction(self, instruction):
#指令过滤
timeout = 0
timeout = 60*15
return instruction,timeout
def analyze_result(self, result,instruction,stderr,stdout):

2
tools/MkdirTool.py

@ -3,7 +3,7 @@ from tools.ToolBase import ToolBase
class MkdirTool(ToolBase):
def validate_instruction(self, instruction):
#指令过滤
timeout = 0
timeout = 60*15
return instruction,timeout
def analyze_result(self, result,instruction,stderr,stdout):

2
tools/MsfconsoleTool.py

@ -20,7 +20,7 @@ class MsfconsoleTool(ToolBase):
print("Metasploit Exit!")
def validate_instruction(self, instruction):
timeout = 0
timeout = 60*15
modified_code = ""
#针对有分号的指令情况,一般是一行
if ";" in instruction: #举例:msfconsole -q -x "use exploit/unix/ftp/vsftpd_234_backdoor; set RHOST 192.168.204.137; exploit"

2
tools/MsfvenomTool.py

@ -7,7 +7,7 @@ import tempfile
class MsfvenomTool(ToolBase):
def validate_instruction(self, instruction):
#指令过滤
timeout = 0
timeout = 60*15
return instruction,timeout
def do_worker_script(self,str_instruction,timeout,ext_params):

2
tools/NslookupTool.py

@ -3,7 +3,7 @@ from tools.ToolBase import ToolBase
class NslookupTool(ToolBase):
def validate_instruction(self, instruction):
#指令过滤
timeout = 0
timeout = 60*15
return instruction,timeout
def analyze_result(self, result,instruction,stderr,stdout):

2
tools/PingTool.py

@ -3,7 +3,7 @@ from tools.ToolBase import ToolBase
class PingTool(ToolBase):
def validate_instruction(self, instruction):
#指令过滤
timeout = 0
timeout = 60*2
return instruction,timeout
def analyze_result(self, result,instruction,stderr,stdout):

2
tools/PrintfTool.py

@ -3,7 +3,7 @@ from tools.ToolBase import ToolBase
class PrintfTool(ToolBase):
def validate_instruction(self, instruction):
#指令过滤
timeout = 0
timeout = 60*5
return instruction,timeout
def analyze_result(self, result,instruction,stderr,stdout):

2
tools/RpcclientTool.py

@ -3,7 +3,7 @@ from tools.ToolBase import ToolBase
class RpcclientTool(ToolBase):
def validate_instruction(self, instruction):
#指令过滤
timeout = 0
timeout = 60*15
return instruction,timeout
def analyze_result(self, result,instruction,stderr,stdout):

2
tools/RpcinfoTool.py

@ -3,7 +3,7 @@ from tools.ToolBase import ToolBase
class RpcinfoTool(ToolBase):
def validate_instruction(self, instruction):
#指令过滤
timeout = 0
timeout = 60*15
return instruction,timeout
def analyze_result(self, result,instruction,stderr,stdout):

2
tools/SearchsploitTool.py

@ -11,7 +11,7 @@ class SearchsploitTool(ToolBase):
cur_path = Path(__file__).resolve().parent
payload_dir = cur_path / "../payload"
#指令过滤
timeout = 0
timeout = 60*15
parts = instruction.split("&&")
if len(parts) ==2:
searchsploit_cmd = parts[0].strip()

2
tools/ShowmountTool.py

@ -3,7 +3,7 @@ from tools.ToolBase import ToolBase
class ShowmountTool(ToolBase):
def validate_instruction(self, instruction):
#指令过滤
timeout = 0
timeout = 60*15
return instruction,timeout
def analyze_result(self, result,instruction,stderr,stdout):

2
tools/SmbclientTool.py

@ -4,7 +4,7 @@ import shlex
class SmbclientTool(ToolBase):
def validate_instruction(self, instruction):
#指令过滤
timeout = 0
timeout = 60*15
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

2
tools/SmbmapTool.py

@ -3,7 +3,7 @@ from tools.ToolBase import ToolBase
class SmbmapTool(ToolBase):
def validate_instruction(self, instruction):
#指令过滤
timeout = 0
timeout = 60*15
if " grep " not in instruction:
instruction =instruction.strip() + " | grep -E 'READ|WRITE|Disk|path'"
return instruction,timeout

2
tools/SmtpuserenumTool.py

@ -5,7 +5,7 @@ from tools.ToolBase import ToolBase
class SmtpuserenumTool(ToolBase):
def validate_instruction(self, instruction):
timeout = 0
timeout = 60*15
# 获取当前程序所在目录
current_path = os.path.dirname(os.path.realpath(__file__))
new_user_path = os.path.join(current_path, "../payload", "users")

2
tools/SmugglerTool.py

@ -4,7 +4,7 @@ from tools.ToolBase import ToolBase
class SmugglerTool(ToolBase):
def validate_instruction(self, instruction_old):
timeout = 0
timeout = 60*15
#指令过滤
# 获取当前程序所在目录
current_path = os.path.dirname(os.path.realpath(__file__))

2
tools/SqlmapTool.py

@ -4,7 +4,7 @@ from tools.ToolBase import ToolBase
class SqlmapTool(ToolBase):
def validate_instruction(self, instruction):
timeout = 0
timeout = 60*15
# 检查sqlmap高风险参数
high_risk_params = [
"--os-shell",

2
tools/SshpassTool.py

@ -8,7 +8,7 @@ import tempfile
class SshpassTool(ToolBase):
def validate_instruction(self, instruction):
#指令过滤
timeout = 0
timeout = 60*15
return instruction,timeout
def do_worker_script(self,str_instruction,timeout,ext_params):

2
tools/SslscanTool.py

@ -3,7 +3,7 @@ from tools.ToolBase import ToolBase
class SslscanTool(ToolBase):
def validate_instruction(self, instruction):
#指令过滤
timeout = 0
timeout = 60*15
return instruction,timeout
def analyze_result(self, result,instruction,stderr,stdout):

2
tools/Sublist3rTool.py

@ -5,7 +5,7 @@ apt install sublist3r
'''
class Sublist3rTool(ToolBase):
def validate_instruction(self, instruction):
timeout = 0
timeout = 60*15
return instruction,timeout
def analyze_result(self, result,instruction,stderr,stdout):

2
tools/SwaksTool.py

@ -3,7 +3,7 @@ from tools.ToolBase import ToolBase
class SwaksTool(ToolBase):
def validate_instruction(self, instruction):
#指令过滤
timeout = 0
timeout = 60*15
return instruction,timeout
def analyze_result(self, result,instruction,stderr,stdout):

2
tools/TouchTool.py

@ -3,7 +3,7 @@ from tools.ToolBase import ToolBase
class TouchTool(ToolBase):
def validate_instruction(self, instruction):
#指令过滤
timeout = 0
timeout = 60*15
return instruction,timeout
def analyze_result(self, result,instruction,stderr,stdout):

2
tools/WgetTool.py

@ -3,7 +3,7 @@ from tools.ToolBase import ToolBase
class WgetTool(ToolBase):
def validate_instruction(self, instruction):
#指令过滤
timeout = 0
timeout = 60*15
return instruction,timeout
def analyze_result(self, result,instruction,stderr,stdout):

2
tools/WhatwebTool.py

@ -3,7 +3,7 @@ from tools.ToolBase import ToolBase
class WhatwebTool(ToolBase):
def validate_instruction(self, instruction):
#指令过滤
timeout = 0
timeout = 60*15
return instruction,timeout
def analyze_result(self, result,instruction,stderr,stdout):

2
tools/WhoisTool.py

@ -4,7 +4,7 @@ from tools.ToolBase import ToolBase
class WhoisTool(ToolBase):
def validate_instruction(self, instruction):
#过滤
timeout = 0
timeout = 60*15
return instruction,timeout
def analyze_result(self, result,instruction,stderr,stdout):

2
tools/XvfbrunTool.py

@ -3,7 +3,7 @@ from tools.ToolBase import ToolBase
class XvfbrunTool(ToolBase):
def validate_instruction(self, instruction):
#指令过滤
timeout = 0
timeout = 60*15
return instruction,timeout
def analyze_result(self, result,instruction,stderr,stdout):

2
web/API/__init__.py

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

170
web/API/assets.py

@ -0,0 +1,170 @@
from . import api
from quart import Quart, render_template, redirect, url_for, request,jsonify
from mycode.AssetsManager import g_AssetsM
from web.common.utils import login_required
@api.route('/assets/getipassets',methods=['POST'])
@login_required
async def get_IP_assets(): #查询IP资产数据
data = await request.get_json()
IP = data.get("IP")
user = data.get("user")
safe_rank = data.get("safe_rank")
ip_assets = g_AssetsM.get_IP_assets(IP,user,safe_rank)
return jsonify({"ip_assets":ip_assets})
@api.route('/assets/getipinfo',methods=['POST'])
@login_required
async def get_IP_info(): #获取该IP资产的基本信息
data = await request.get_json()
IP = data.get("IP")
ip_info = g_AssetsM.get_IP_info(IP)
return jsonify({"ip_info":ip_info})
@api.route('/assets/getassetsuser',methods=['POST'])
@login_required
async def get_assets_user():
data = await request.get_json()
uname = data.get("keyword")
user_list = g_AssetsM.get_assets_users(uname)
return jsonify({"user_list": user_list})
@api.route('/assets/updateipinfo',methods=['POST'])
@login_required
async def update_IP_info(): #获取该IP资产的基本信息
data = await request.get_json()
IP = data.get("cur_ip")
owner_id = data.get("owner_id")
bsuccess,error = g_AssetsM.update_assets_users(IP,owner_id)
return jsonify({"bsuccess":bsuccess})
@api.route('/assets/getportlatest',methods=['POST'])
@login_required
async def get_port_latest(): #获取端口的最新数据
data = await request.get_json()
ip = data.get("ip")
if not ip:
return jsonify({'error': '缺少 ip 参数'}), 400
port_data = g_AssetsM.get_port_latest(ip)
return jsonify({"port_data":port_data})
@api.route('/assets/getporthistory',methods=['POST'])
@login_required
async def get_port_history(): #获取端口的历史数据
data = await request.get_json()
ip = data.get("ip")
if not ip:
return jsonify({'error': '缺少 ip 参数'}), 400
times,port_dict = g_AssetsM.get_port_history(ip)
return jsonify({"times":times,"entries":port_dict})
@api.route('/assets/geturllatest',methods=['POST'])
@login_required
async def get_url_latest():
data = await request.get_json()
ip = data.get("ip")
if not ip:
return jsonify({'error': '缺少 ip 参数'}), 400
ip_url_data = g_AssetsM.get_ip_url_latest(ip)
if not ip_url_data:
ip_url_data=[]
return jsonify({"ip_url_data": ip_url_data})
@api.route('/assets/geturlhistory',methods=['POST'])
@login_required
async def get_url_history():
data = await request.get_json()
ip = data.get("ip")
if not ip:
return jsonify({'error': '缺少 ip 参数'}), 400
ip_url_data = g_AssetsM.get_ip_url_history(ip)
if not ip_url_data:
ip_url_data=[]
return jsonify({"ip_url_data": ip_url_data})
@api.route('/assets/getvuldata',methods=['POST'])
@login_required
async def get_vul_data():
data = await request.get_json() #ip,nodeName,vulType,vulLevel
ip = data.get("ip")
if not ip:
return jsonify({'error': '缺少 ip 参数'}), 400
nodeName = data.get("nodeName")
vulType = data.get("vulType")
vulLevel = data.get("vulLevel")
vuls = g_AssetsM.get_vul_data(ip,nodeName,vulType,vulLevel)
return jsonify({"vuls": vuls})
@api.route('/assets/delipassets',methods=['POST'])
@login_required
async def del_ip_assets():
data = await request.get_json() # ip,nodeName,vulType,vulLevel
ip = data.get("IP")
if not ip:
return jsonify({'error': '缺少 ip 参数'}), 400
bsuccess,error = g_AssetsM.del_ip_assets(ip)
return jsonify({"bsuccess": bsuccess,"error":error})
@api.route('/assets/geturlassets', methods=['POST'])
@login_required
async def get_url_assets(): #url_filter,user_filter,email_filter
data = await request.get_json()
url = data.get("url_filter")
owner = data.get("user_filter")
email = data.get("email_filter")
url_assets = g_AssetsM.get_url_assets(url,owner,email)
return jsonify({"url_assets": url_assets})
@api.route('/assets/updateurlinfo', methods=['POST'])
@login_required
async def update_url_info():
data = await request.get_json()
url_id = data.get("cur_url_id")
owner_id = data.get("owner_id")
bsuccess, error = g_AssetsM.update_assets_users(url_id, owner_id,2)
return jsonify({"bsuccess": bsuccess,"error":error})
@api.route('/assets/geturltoIP', methods=['POST'])
@login_required
async def get_url_to_ip():
data = await request.get_json()
url_id = data.get("cur_url_id")
last_to_ips,his_to_ips = g_AssetsM.get_url_to_ip(url_id)
return jsonify({"last_to_ips":last_to_ips,"his_to_ips":his_to_ips})
@api.route('/assets/delurlassets', methods=['POST'])
@login_required
async def del_url_assets():
data = await request.get_json()
url_id = data.get("url")
bsuccess,error = g_AssetsM.del_url_assets(url_id)
return jsonify({"bsuccess": bsuccess, "error": error})
@api.route('/assets/getOwners', methods=['POST'])
@login_required
async def getOwners():
data = await request.get_json()
owner = data.get("owner")
owner_type = data.get("owner_type")
contact = data.get("contact")
tellnum = data.get("tellnum")
owner_list = g_AssetsM.get_owners(owner,owner_type,contact,tellnum)
return jsonify({"owner_list": owner_list})
@api.route('/assets/addUpdateOwners', methods=['POST'])
@login_required
async def add_update_Owner():
data = await request.get_json() #{data,currentMode}
owner_data = data.get("data")
do_mode = data.get("currentMode")
bsuccess,error,owner_list = g_AssetsM.add_update_owner(owner_data, do_mode)
return jsonify({"owner_list": owner_list,"bsuccess":bsuccess,"error":error})
@api.route('/assets/delOwners', methods=['POST'])
@login_required
async def del_Owner():
data = await request.get_json()
id = data.get("id")
bsuccess,error = g_AssetsM.del_owner(id)
return jsonify({"bsuccess": bsuccess, "error": error})

11
web/API/task.py

@ -24,19 +24,12 @@ async def start_task(): #开始任务
work_type = 0 #data.get("workType") #0-人工,1-自动
if llm_type == 2:
return jsonify({"error": "O3余额不足,请更换模型!"}), 400
# #新增任务处理
# bok,_,_ = g_TM.validate_and_extract(test_target)
# if not bok:
# # 返回错误信息,状态码 400 表示请求错误
# return jsonify({"error": "测试目标验证失败,请检查输入内容!"}), 400
#开始任务
try:
fail_list = g_TaskM.create_task(test_target,llm_type,work_type)
return jsonify({"fail_list":fail_list})
except:
return jsonify({"error": "创建任务异常,前反馈给技术人员!"}), 400
#跳转到任务管理页面
# return redirect(url_for('main.get_html', html='task_manager.html'))
@api.route('/task/taskover',methods=['POST'])
@login_required
@ -137,11 +130,11 @@ async def task_one_step():
'''
data = await request.get_json()
task_id = data.get("cur_task_id")
step_num = data.get("step_num")
step_num = int(data.get("step_num"))
if not task_id:
return jsonify({'error': 'Missing task_id'}), 400
if not step_num:
step_num = 1 #默认一
step_num = 1 #默认一
bsuccess,error = await g_TaskM.task_one_step(task_id,step_num)
return jsonify({"bsuccess":bsuccess,"error":error})

1023
web/main/static/resources/scripts/assets_manager.js

File diff suppressed because it is too large

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

@ -9,7 +9,7 @@ window.addEventListener("beforeunload", function() {
ws.close();
ws =null;
}
task_list = []
task_list = [];
cur_task = null;
cur_task_id = 0;
});
@ -294,6 +294,8 @@ async function overTask(){
task_list = []
cur_task = null //当前选择的task--用于修改缓存时使用
cur_task_id = 0 //当前选择的cur_task_id
//清空节点树
taskList.innerHTML = "加载中"; // 清空“加载中”提示
//重新获取任务list
getTasklist();
}else {
@ -366,10 +368,12 @@ document.getElementById("one_step").addEventListener("click",() => {
});
async function one_step_task(){
try {
const stepSElement= document.getElementById("stepNumSelect")
const step_num = stepSElement.value;
const res = await fetch("/api/task/taskstep", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ cur_task_id }), //task_id:task_id
body: JSON.stringify({ cur_task_id,step_num }), //task_id:task_id
});
if (!res.ok) {
const errorData = await res.json();
@ -437,7 +441,7 @@ function renderInstrPage(page) {
const tbody = document.querySelector("#instrTable tbody");
renderTableRows(tbody, pageData);
document.getElementById("instrTable").scrollIntoView({ behavior: "smooth" });
// 更新分页按钮
document.getElementById("instrPrev").dataset.page = page > 1 ? page - 1 : 1;
document.getElementById("instrNext").dataset.page = (end < allInstrs.length) ? page + 1 : page;
@ -532,6 +536,7 @@ function renderVulPage(page) {
const tbody = document.querySelector("#vulTable tbody");
renderTableRows(tbody, pageData);
document.getElementById("vulTable").scrollIntoView({ behavior: "smooth" });
// 更新分页按钮
document.getElementById("vulPrev").dataset.page = page > 1 ? page - 1 : 1;

593
web/main/templates/assets_manager.html

@ -4,13 +4,604 @@
<!-- 页面样式块 -->
{% block style %}
/* 表格固定行高与居中 */
.table-fixed tbody tr { height: 40px; }
.table-fixed td, .table-fixed th { vertical-align: middle; text-align: center; }
/* 操作按钮间距 */
.asset-op-btn { margin: 0 2px; }
.offcanvas {
z-index: 1060 !important;
}
#historyPortTable {
table-layout: fixed;
width: 100%; /* 或者你想要的整体宽度 */
}
#historyPortTable th {
/* 你在 JS 动态插入 style="width:120px" */
}
#historyPortTable td {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
{% endblock %}
<!-- 页面内容块 -->
{% block content %}
<h3 style="text-align: center;padding: 10px"> 功能规划中,在二期实现。。。</h3>
<div class="container-xxl">
<ul class="nav nav-tabs" id="assetTab" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="ipTab" data-bs-toggle="tab" data-bs-target="#ipAssets" type="button" role="tab">IP 资产</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="domainTab" data-bs-toggle="tab" data-bs-target="#domainAssets" type="button" role="tab">域名资产</button>
</li>
</ul>
<div class="tab-content p-1 border border-top-0" id="assetTabContent">
<!-- IP 资产 -->
<div class="tab-pane fade show active" id="ipAssets" role="tabpanel">
<!-- 查询区 -->
<div class="row mb-3">
<div class="col-3"><input type="text" class="form-control" id="ipFilter" placeholder="资产 IP"></div>
<div class="col-3"><input type="text" class="form-control" id="userFilter" placeholder="所属用户"></div>
<div class="col-3">
<select class="form-select" id="riskFilter">
<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-3 text-end">
<button class="btn btn-primary" id="ipSearchBtn">查询</button>
<button class="btn btn-primary" id="ipExportBtn">导出</button>
</div>
</div>
<!-- 表格 -->
<div class="table-responsive">
<table class="table table-bordered table-fixed" id="ipTable">
<thead>
<tr>
<th style="width:50px;">序号</th>
<th>资产IP</th>
<th>所属用户</th>
<th style="width:50px;">风险</th>
<th style="width:10%;">最新检测时间</th>
<th style="width:50px;">端口</th>
<th style="width:100px">关联域名</th>
<th style="width:35%;">操作</th>
</tr>
</thead>
<tbody>
<!-- JS 动态插入 15 行 -->
</tbody>
</table>
</div>
<!-- 分页 -->
<nav class="mt-0">
<ul class="pagination justify-content-end" id="ipPagination">
<li class="page-item"><a class="page-link" href="#" id="ipPrev">上一页</a></li>
<li class="page-item"><a class="page-link" href="#" id="ipNext">下一页</a></li>
</ul>
</nav>
</div>
<!-- 域名资产 -->
<div class="tab-pane fade" id="domainAssets" role="tabpanel">
<!-- 筛选区域 -->
<div class="row mb-3">
<div class="col-3"><input type="text" class="form-control" id="urlFilter" placeholder="域名"></div>
<div class="col-3"><input type="text" class="form-control" id="ownerFilter" placeholder="所属用户"></div>
<div class="col-3"><input type="text" class="form-control" id="emailFilter" placeholder="注册邮箱"></div>
<div class="col-3 text-end">
<button class="btn btn-primary" id="urlSearchBtn">查询</button>
<button class="btn btn-primary" id="urlExportBtn">导出</button>
</div>
</div>
<!-- 表格区域 -->
<div class="table-responsive">
<table class="table table-bordered table-fixed" id="urlTable">
<thead>
<tr>
<th style="width:5%;">序号</th>
<th style="width:15%;">域名</th>
<th style="width:15%;">所属用户</th>
<th style="width:10%;">注册邮箱</th>
<th style="width:10%;">最新检测时间</th>
<th style="width:10%;">过期日期</th>
<th style="width:5%;">IP</th>
<th style="width:30%;">操作</th>
</tr>
</thead>
<tbody>
<!-- JS 动态插入 10 行 -->
</tbody>
</table>
</div>
<!-- 分页控件 -->
<nav class="mt-0">
<ul class="pagination justify-content-end" id="urlPagination">
<li class="page-item"><a class="page-link" href="#" id="urlPrev">上一页</a></li>
<li class="page-item"><a class="page-link" href="#" id="urlNext">下一页</a></li>
</ul>
</nav>
</div>
</div>
</div>
<!-- IP资产信息------------------------- -->
<!-- 基本信息 Modal -->
<div class="modal fade" id="ipBasicInfoModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title fw-bold">基本信息</h3>
<button class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="ipInfoForm">
<div class="row gy-3">
<div class="col-md-6">
<label class="fw-bold">IP 地址:</label>
<span id="info_ip"></span>
</div>
<div class="col-md-6">
<label class="fw-bold">风险等级:</label>
<span id="info_risk"></span>
</div>
<div class="col-md-12">
<label class="fw-bold">最新检测时间:</label>
<span id="info_scanTime"></span>
</div>
<div class="col-md-6">
<label class="fw-bold">所属用户:</label>
<span id="info_owner"></span>
</div>
<div class="col-md-6">
<button type="button" id="btnChooseOwner" class="btn btn-primary btn-sm ms-3">
修改
</button>
<button type="button" id="btnDelOwner" class="btn btn-danger btn-sm ms-3">
删除
</button>
</div>
<div class="col-md-6">
<label class="fw-bold mb-1" for="contactName">联系人:</label>
<span id="contactName"></span>
<!-- <input id="contactName" name="contactName" class="form-control">-->
</div>
<div class="col-md-6">
<label class="fw-bold mb-1" for="contactPhone">联系电话:</label>
<span id="contactPhone"></span>
<!-- <input id="contactPhone" name="contactPhone" class="form-control">-->
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button id="saveIpInfo" class="btn btn-primary">保存</button>
<button class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
</div>
</div>
</div>
</div>
<!-- 侧边 Drawer:选择所属用户 -->
<div class="offcanvas offcanvas-start" tabindex="-1" id="ownerDrawer">
<div class="offcanvas-header">
<h5 class="offcanvas-title">选择所属用户</h5>
<button class="btn-close" data-bs-dismiss="offcanvas"></button>
</div>
<div class="offcanvas-body p-3">
<div class="input-group mb-3">
<input type="text" id="ownerSearchKeyword" class="form-control" placeholder="搜索用户名…">
<button class="btn btn-outline-secondary" id="btnSearchOwner">搜索</button>
</div>
<table class="table table-sm align-middle">
<thead>
<tr><th style="width:60px;">序号</th><th>用户名</th><th style="width:80px;">操作</th></tr>
</thead>
<tbody id="ownerTableBody"></tbody>
</table>
</div>
</div>
<!-- 端口数据 Modal -->
<div class="modal fade" id="portDataModal" tabindex="-1" aria-labelledby="portDataModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl modal-dialog-centered" style="max-width:90vw;">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title fw-bold" id="portDataModalLabel">端口数据</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="关闭"></button>
</div>
<div class="modal-body">
<!-- Nav tabs -->
<ul class="nav nav-tabs" id="portTab" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="latestTab" data-bs-toggle="tab" data-bs-target="#latestPane" type="button" role="tab">
最新数据
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="historyTab" data-bs-toggle="tab" data-bs-target="#historyPane" type="button" role="tab">
历史数据
</button>
</li>
</ul>
<!-- Tab panes -->
<div class="tab-content pt-0" id="portTabContent">
<!-- 最新数据 -->
<div class="tab-pane fade show active" id="latestPane" role="tabpanel">
<div class="table-responsive" style="max-height:480px; overflow:auto;">
<table class="table table-bordered table-hover text-center" id="latestPortTable">
<thead>
<tr>
<th style="width:60px">序号</th>
<th>端口号</th>
<th>服务</th>
<th>版本号</th>
<th>端口状态</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<nav>
<ul class="pagination justify-content-end" id="latestPortPagination">
<li class="page-item"><a class="page-link" href="#" id="latestPrev">上一页</a></li>
<li class="page-item"><a class="page-link" href="#" id="latestNext">下一页</a></li>
</ul>
</nav>
</div>
<!-- 历史数据 -->
<div class="tab-pane fade" id="historyPane" role="tabpanel">
<div class="table-responsive" style="height:515px; overflow:auto;">
<table class="table table-bordered text-center" id="historyPortTable" style="table-layout:fixed;min-width:600px;">
<thead></thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-primary" id="exportPortData">导出</button>
<button class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
</div>
</div>
</div>
</div>
<!-- 漏洞数据 Modal -->
<div class="modal fade" id="vulDataModal" tabindex="-1" aria-labelledby="vulDataModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl modal-dialog-centered" style="max-width:90vw;">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title fw-bold" id="vulDataModalLabel">漏洞数据</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="关闭"></button>
</div>
<div class="modal-body">
<!-- 搜索区域 -->
<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">
<colgroup>
<col style="width: 5%;">
<col style="width: 35%;">
<col style="width: 15%;">
<col style="width: 10%;" 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 justify-content-end" 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 class="modal-footer">
<button class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
</div>
</div>
</div>
</div>
<!-- 域名数据 Modal -->
<div class="modal fade" id="urlDataModal" tabindex="-1" aria-labelledby="urlDataModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl modal-dialog-centered" style="max-width:90vw;">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title fw-bold" id="urlDataModalLabel">关联域名</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="关闭"></button>
</div>
<div class="modal-body">
<!-- Nav tabs -->
<ul class="nav nav-tabs" id="urlTab" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="urlLatestTab" data-bs-toggle="tab" data-bs-target="#urlLatestPane" type="button" role="tab">
最新数据
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="urlHistoryTab" data-bs-toggle="tab" data-bs-target="#urlHistoryPane" type="button" role="tab">
历史数据
</button>
</li>
</ul>
<!-- Tab panes -->
<div class="tab-content pt-3" id="urlTabContent">
<!-- 最新数据 -->
<div class="tab-pane fade show active" id="urlLatestPane" role="tabpanel">
<div class="table-responsive mb-2" style="max-height:480px; overflow:auto;">
<table class="table table-bordered table-hover text-center" id="latestUrlTable">
<thead>
<tr>
<th style="width:50px">序号</th>
<th>域名</th>
<th>子域名</th>
<th>注册人</th>
<th>注册邮箱</th>
<th>创建时间</th>
<th>过期时间</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<nav>
<ul class="pagination justify-content-end" id="latestUrlPagination">
<li class="page-item"><a class="page-link" href="#" id="latestUrlPrev">上一页</a></li>
<li class="page-item"><a class="page-link" href="#" id="latestUrlNext">下一页</a></li>
</ul>
</nav>
</div>
<!-- 历史数据 -->
<div class="tab-pane fade" id="urlHistoryPane" role="tabpanel">
<div class="table-responsive mb-2" style="max-height:480px; overflow:auto;">
<table class="table table-bordered table-hover text-center" id="historyUrlTable">
<thead>
<tr>
<th style="width:50px">序号</th>
<th>时间</th>
<th>变化类型</th>
<th>关联域名</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<nav>
<ul class="pagination justify-content-end" id="historyUrlPagination">
<li class="page-item"><a class="page-link" href="#" id="historyUrlPrev">上一页</a></li>
<li class="page-item"><a class="page-link" href="#" id="historyUrlNext">下一页</a></li>
</ul>
</nav>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-primary" id="exportUrlData">导出</button>
<button class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
</div>
</div>
</div>
</div>
<!-- url资产信息-------------------------- -->
<!-- 基本信息 Modal -->
<div class="modal fade" id="urlBasicInfoModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title fw-bold">基本信息</h3>
<button class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="urlInfoForm">
<div class="row gy-3">
<div class="col-md-12">
<label class="fw-bold">URL:</label>
<span id="info_url"></span>
</div>
<div class="col-md-12">
<label class="fw-bold">最新检测时间:</label>
<span id="url_scanTime"></span>
</div>
<div class="col-md-6">
<label class="fw-bold">注册人:</label>
<span id="url_register"></span>
</div>
<div class="col-md-6">
<label class="fw-bold">注册邮箱:</label>
<span id="url_email"></span>
</div>
<div class="col-md-6">
<label class="fw-bold">所属用户:</label>
<span id="url_owner"></span>
</div>
<div class="col-md-6">
<button type="button" id="urlbtnChooseOwner" class="btn btn-primary btn-sm ms-3">
修改
</button>
<button type="button" id="urlbtnDelOwner" class="btn btn-danger btn-sm ms-3">
删除
</button>
</div>
<div class="col-md-6">
<label class="fw-bold mb-1" for="contactName">联系人:</label>
<span id="urlcontactName"></span>
</div>
<div class="col-md-6">
<label class="fw-bold mb-1" for="contactPhone">联系电话:</label>
<span id="urlcontactPhone"></span>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button id="saveUrlInfo" class="btn btn-primary">保存</button>
<button class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
</div>
</div>
</div>
</div>
<!-- 指向IP Modal -->
<div class="modal fade" id="toIpModal" tabindex="-1" aria-labelledby="urlDataModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" style="max-width:70vw;">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title fw-bold">指向IP</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="关闭"></button>
</div>
<div class="modal-body">
<!-- Nav tabs -->
<ul class="nav nav-tabs" id="toipTab" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="toipLatestTab" data-bs-toggle="tab" data-bs-target="#toipLatestPane" type="button" role="tab">
最新数据
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="toipHistoryTab" data-bs-toggle="tab" data-bs-target="#toipHistoryPane" type="button" role="tab">
历史数据
</button>
</li>
</ul>
<!-- Tab panes -->
<div class="tab-content pt-3" id="toipTabContent">
<!-- 最新数据 -->
<div class="tab-pane fade show active" id="toipLatestPane" role="tabpanel">
<div class="table-responsive mb-2" style="max-height:480px; overflow:auto;">
<table class="table table-bordered table-hover text-center" id="latesttoipTable">
<thead>
<tr>
<th style="width:50px">序号</th>
<th>IP地址</th>
<th>关联时间</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<nav>
<ul class="pagination justify-content-end" id="latesttoipPagination">
<li class="page-item"><a class="page-link" href="#" id="latesttoipPrev">上一页</a></li>
<li class="page-item"><a class="page-link" href="#" id="latesttoipNext">下一页</a></li>
</ul>
</nav>
</div>
<!-- 历史数据 -->
<div class="tab-pane fade" id="toipHistoryPane" role="tabpanel">
<div class="table-responsive mb-2" style="max-height:480px; overflow:auto;">
<table class="table table-bordered table-hover text-center" id="historytoipTable">
<thead>
<tr>
<th style="width:50px">序号</th>
<th>IP地址</th>
<th>取关时间</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<nav>
<ul class="pagination justify-content-end" id="historytoipPagination">
<li class="page-item"><a class="page-link" href="#" id="historytoipPrev">上一页</a></li>
<li class="page-item"><a class="page-link" href="#" id="historytoipNext">下一页</a></li>
</ul>
</nav>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-primary" id="exporttoipData">导出</button>
<button class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
</div>
</div>
</div>
</div>
{% endblock %}
<!-- 页面脚本块 -->
{% block script %}
<script src="{{ url_for('main.static', filename='scripts/assets_manager.js') }}"></script>
<script src="{{ url_for('main.static', filename='scripts/jquery-3.2.1.slim.min.js') }}"></script>
{% endblock %}

10
web/main/templates/assets_manager_modal.html

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
</body>
</html>

331
web/main/templates/assets_user_manager.html

@ -0,0 +1,331 @@
{% extends 'base.html' %}
{% block title %}ZFSAFE{% 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;
}
{% endblock %}
<!-- 页面内容块 -->
{% block content %}
<div class="container-xxl mt-2">
<!-- 查询区 -->
<div class="row mb-3">
<div class="col-2 mb-2"><button class="btn btn-primary me-3" onclick="openModal('add')">新增</button></div>
<div class="col-10"></div>
<div class="col-3 mb-2"><input type="text" class="form-control" id="searchUser" placeholder="资产用户"></div>
<div class="col-2">
<select class="form-select" id="ownerType">
<option value="">用户类型</option>
<option value="个人">个人</option>
<option value="私营企业">私营企业</option>
<option value="国有企业">国有企业</option>
<option value="事业单位">事业单位</option>
<option value="政府机构">政府机构</option>
<option value="团体协会">团体协会</option>
</select>
</div>
<div class="col-2"><input type="text" class="form-control" id="searchContact" placeholder="联系人"></div>
<div class="col-3"><input type="text" class="form-control" id="searchPhone" placeholder="联系电话"></div>
<div class="col-2 text-end">
<button class="btn btn-primary" onclick="fetchData()">查询</button>
<button class="btn btn-primary" onclick="exportOwnerData()">导出</button>
</div>
</div>
<!-- 表格 -->
<div class="table-responsive">
<table class="table table-bordered table-hover" id="ownerTable" style="width: 100%; table-layout: fixed;">
<thead>
<tr>
<th style="width:5%;">序号</th>
<th style="width:30%;">资产用户</th>
<th style="width:10%;">用户类型</th>
<th style="width:10%;">联系人</th>
<th style="width:15%;">联系电话</th>
<th style="width:10%;">关联资产</th>
<th style="width:20%;">操作</th>
</tr>
</thead>
<tbody>
<!-- JS 动态插入 10 行 -->
</tbody>
</table>
</div>
<!-- 分页 -->
<nav class="mt-2">
<ul class="pagination justify-content-end" id="userPagination">
<li class="page-item"><a class="page-link" href="#" id="userPrev">上一页</a></li>
<li class="page-item"><a class="page-link" href="#" id="userNext">下一页</a></li>
</ul>
</nav>
</div>
<!-- Modal -->
<div class="modal fade" id="ownerModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="modalTitle">新增/修改用户</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="ownerForm">
<input type="hidden" id="ownerId">
<div class="mb-3">
<label class="form-label me-2">资产用户:</label>
<input type="text" class="form-control" id="formUser">
</div>
<div class="mb-3">
<label class="form-label me-2">用户类型:</label>
<select class="form-select" id="formType">
<option value="">用户类型</option>
<option value="私营企业">私营企业</option>
<option value="国有企业">国有企业</option>
<option value="事业单位">事业单位</option>
<option value="政府机构">政府机构</option>
<option value="团体协会">团体协会</option>
<option value="个人">个人</option>
</select>
</div>
<div class="mb-3">
<label class="form-label me-2">证件号码:</label>
<input type="text" class="form-control" id="IDno">
</div>
<div class="mb-1 row gx-3">
<div class="col-6">
<label class="form-label me-2">联系人:</label>
<input type="text" class="form-control" id="formContact">
</div>
<div class="col-6">
<label class="form-label me-2">联系电话:</label>
<input type="text" class="form-control" id="formPhone">
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button class="btn btn-primary" onclick="saveOwner()">保存</button>
<button class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
</div>
</div>
</div>
</div>
{% endblock %}
<!-- 页面脚本块 -->
{% block script %}
<script>
let currentMode = 'add';
let ownersData = [];
let currentPage = 1;
const pageSize = 10;
async function postJSON(url, payload) {
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type':'application/json' },
body: JSON.stringify(payload)
});
if (!res.ok) {
const errorData = await res.json();
throw new Error(errorData.error || `HTTP错误 ${res.status}`);
}
return res.json();
}
function downloadCSV(text, filename) {
const blob = new Blob([text], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = filename;
link.click();
URL.revokeObjectURL(link.href);
}
function fmtCell(val) {
// 如果是数字-数字,比如 "2-4"、"10-12" 等
if (/^\d+-\d+$/.test(val)) {
return '="' + val + '"';
}
// 如果里面有中文或逗号,就双引号包裹
if (/[,\u4e00-\u9fa5]/.test(val)) {
return `"${val.replace(/"/g, '""')}"`;
}
return val;
}
async function exportOwnerData(){
//导出数据
let rows = [
['序号', '资产用户', '用户类型', '联系人', '联系电话','证件号码','关联IP','关联域名'].map(fmtCell).join(',')
];
ownersData.forEach((row, i) => {
rows.push([
(i + 1).toString(),
row[2].toString(),
row[1] || '',
row[4] || '',
row[3] || '',
row[5] || '',
row[6].toString(),
row[7].toString(),
].map(fmtCell).join(','));
});
const csv = '\uFEFF' + rows.join('\r\n'); // 加 BOM
downloadCSV(csv, `assets_owner.csv`);
}
function openModal(mode, index = null) {
currentMode = mode;
document.getElementById('ownerForm').reset();
document.getElementById('ownerId').value = '';
if (mode === 'edit' && index !== null) {
const data = ownersData[index];
document.getElementById('modalTitle').innerText = '修改用户';
document.getElementById('ownerId').value = data[0] || '';
document.getElementById('formUser').value = data[2] || '';
document.getElementById('formType').value = data[1] || '';
document.getElementById('formContact').value = data[4] || '';
document.getElementById('formPhone').value = data[3] || '';
document.getElementById('IDno').value = data[5] || '';
} else {
document.getElementById('modalTitle').innerText = '新增用户';
}
new bootstrap.Modal(document.getElementById('ownerModal')).show();
}
async function saveOwner() {
const data = {
id: document.getElementById('ownerId').value,
user: document.getElementById('formUser').value,
type: document.getElementById('formType').value,
contact: document.getElementById('formContact').value,
phone: document.getElementById('formPhone').value,
IOno: document.getElementById('IDno').value,
};
try{
const jsondata = await postJSON('/api/assets/addUpdateOwners', {data,currentMode});
bsuccess = jsondata.bsuccess;
error = jsondata.error;
if(bsuccess){
ownersData = jsondata.owner_list || [];
currentPage = 1;
document.querySelector('#ownerModal .btn-close').click();
renderTable();
}else {
alert("操作失败"+error)
}
} catch (error) {
console.error("操作失败:",error)
alert("操作失败:"+error)
}
}
function renderTable() {
const tableBody = document.querySelector('#ownerTable tbody');
//const tableBody = document.getElementById('tableBody');
tableBody.innerHTML = '';
const start = (currentPage - 1) * pageSize;
const pageData = ownersData.slice(start, start + pageSize);
pageData.forEach((item, i) => { //IP,itype,uname,tellnum,tell_username,ID_num
const tr = document.createElement('tr');
tr.innerHTML = `
<td>${start + i + 1}</td>
<td>${item[2] || ''}</td>
<td>${item[1] || ''}</td>
<td>${item[4] || ''}</td>
<td>${item[3] || ''}</td>
<td>${item[8] || ''}</td>
<td>
<button class="btn btn-info btn-sm me-1" onclick="openModal('edit', ${start + i})">修改</button>
<!-- <button class="btn btn-info btn-sm me-1" onclick="showassets(${start + i})">查看资产</button> -->
<button class="btn btn-danger btn-sm" onclick="delowner(${start + i})">删除</button>
</td>
`;
tableBody.appendChild(tr);
});
for (let i = pageData.length; i < pageSize; i++) {
const tr = document.createElement('tr');
tr.innerHTML = '<td colspan="7">&nbsp;</td>';
tableBody.appendChild(tr);
}
}
async function fetchData() {
const ownerEl = document.getElementById("searchUser");
const ownerTypeEl = document.getElementById("ownerType");
const contactEl = document.getElementById("searchContact");
const tellNumEl = document.getElementById("searchPhone");
const owner = ownerEl.value;
const owner_type = ownerTypeEl.value;
const contact = contactEl.value;
const tellnum = tellNumEl.value;
try {
const data = await postJSON("/api/assets/getOwners",{owner,owner_type,contact,tellnum})
ownersData = data.owner_list || [];
currentPage = 1;
renderTable();
} catch (error) {
console.error("查询资产用户记录出错:", error);
alert("查询失败!");
}
}
async function showassets(index){
}
async function delowner(index){
const data = ownersData[index];
const id = data[0]
if (!confirm('确认删除?')) return;
try{
const redata = await postJSON('/api/assets/delOwners', {id});
bsuccess = redata.bsuccess;
error = redata.error;
if(bsuccess){
alert("删除成功!");
await fetchData(); //刷新数据
}
else{
alert("删除失败!",error)
}
} catch (error) {
console.error("删除失败!",error);
alert("删除失败!",error)
}
}
window.onload = fetchData;
</script>
{% endblock %}

4
web/main/templates/header.html

@ -12,10 +12,12 @@
<li class="nav-item"><a href="/index.html" class="nav-link active" aria-current="page">首页</a></li>
<li class="nav-item"><a href="/task_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="/polling_target.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="/assets_user_manager.html" class="nav-link">资产用户</a></li>
<li class="nav-item"><a href="/safe_status.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="/user_manager.html" class="nav-link">用户管理</a></li>
</ul>
<div class="dropdown text-end">

8
web/main/templates/his_task.html

@ -112,7 +112,7 @@
<!-- 页面内容块 -->
{% block content %}
<div class="container">
<div class="container-xxl">
<!-- 查询条件区域 -->
<div class="search-section mb-3">
<form class="row g-3 align-items-center">
@ -545,19 +545,19 @@
const tdAction = document.createElement("td");
//报告按钮
const btnReport = document.createElement("button");
btnReport.className = "btn btn-outline-info btn-sm ms-2";
btnReport.className = "btn btn-primary btn-sm ms-2";
btnReport.textContent = "报告";
btnReport.onclick = () => createReport(task[0]);
tdAction.appendChild(btnReport);
// 查看按钮(点击后弹出 modal)
const btnView = document.createElement("button");
btnView.className = "btn btn-outline-info btn-sm ms-2";
btnView.className = "btn btn-info btn-sm ms-2";
btnView.textContent = "查看";
btnView.onclick = () => openViewModal(task[0]);
tdAction.appendChild(btnView);
// 删除按钮
const btnDel = document.createElement("button");
btnDel.className = "btn btn-outline-danger btn-sm ms-2";
btnDel.className = "btn btn-danger btn-sm ms-2";
btnDel.textContent = "删除";
btnDel.onclick = () => confirmDeleteTask(task[0]);
tdAction.appendChild(btnDel);

4
web/main/templates/index.html

@ -83,7 +83,7 @@
<textarea class="form-control" id="usage" rows="9">
1.测试模式分为两种:自动执行和人工确认(单步模式),模式的切换只允许在暂停情况下调整;
2.暂停不停止正在执行指令,指令执行后会根据当前参数的设定执行下一步工作;
3.单步的作用是将节点中:待执行的指令进行执行,待提交LLM的数据提交LLM;
3.单步的作用是将节点中:待执行的指令进行执行,待提交LLM的数据提交LLM,执行指令和提交LLM都各算一步
4.顶部的单步是针对整个任务的单步执行,若节点执行状态不一致,会存在某些节点执行测试指令,某些节点提交llm任务的情况,节点树区域的控制是针对该节点的控制;
5.由于LLM的不一致性,会存在无执行任务,但没有标记完成的任务节点,可作为已完成论;
6.在单步模式下,若某指令执行的结果错误,可以在查看MSG功能里,修改待提交的执行结果,来保障测试的顺利推进;
@ -154,7 +154,7 @@
const data = await response.json();
fail_list = data.fail_list;
if(fail_list.trim() !== ""){
alert("创建任务成功,失败的有:"+fail_list);
alert("创建任务失败的有:"+fail_list);
}
window.location.href = "/task_manager.html";
} catch (error) {

108
web/main/templates/polling_target.html

@ -0,0 +1,108 @@
{% extends 'base.html' %}
{% block title %}ZFSAFE{% 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;
}
{% endblock %}
<!-- 页面内容块 -->
{% block content %}
<div class="container-xxl mt-2">
<!-- 查询区 -->
<div class="row mb-3">
<div class="col-2 mb-2"><button class="btn btn-primary me-3" onclick="openModal('add')">导入</button></div>
<div class="col-10"></div>
<div class="col-3 mb-2"><input type="text" class="form-control" id="polltarget" placeholder="检测目标"></div>
<div class="col-3"><input type="text" class="form-control" id="owner" placeholder="所属用户"></div>
<div class="col-2">
<select class="form-select" id="polling_period">
<option value="">巡检周期</option>
<option value="1">每日</option>
<option value="2">每周</option>
<option value="3">每月</option>
</select>
</div>
<div class="col-2">
<select class="form-select" id="risk_rank">
<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>
</select>
</div>
<div class="col-2 text-end">
<button class="btn btn-primary" onclick="fetchData()">查询</button>
<button class="btn btn-primary" onclick="exportOwnerData()">导出</button>
</div>
</div>
<!-- 表格 -->
<div class="table-responsive">
<table class="table table-bordered table-hover" id="pollingTable" style="width: 100%; table-layout: fixed;">
<thead>
<tr>
<th style="width:5%;">序号</th>
<th style="width:20%;">检测目标</th>
<th style="width:20%;">所属用户</th>
<th style="width:10%;">检测周期</th>
<th style="width:15%;">最新检测时间</th>
<th style="width:10%;">风险等级</th>
<th style="width:20%;">操作</th>
</tr>
</thead>
<tbody>
<!-- JS 动态插入 10 行 -->
</tbody>
</table>
</div>
<!-- 分页 -->
<nav class="mt-2">
<ul class="pagination justify-content-end" id="pollingPagination">
<li class="page-item"><a class="page-link" href="#" id="pollingPrev">上一页</a></li>
<li class="page-item"><a class="page-link" href="#" id="pollingNext">下一页</a></li>
</ul>
</nav>
</div>
<!-- --------导入modal---------- -->
<!-- --------所属用户modal---------- -->
<!-- --------巡检策略modal---------- -->
{% endblock %}
<!-- 页面脚本块 -->
{% block script %}
{% endblock %}

16
web/main/templates/safe_status.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 %}

4
web/main/templates/task_manager.html

@ -213,8 +213,8 @@
<!-- 按钮 (联动测试状态示例: 执行中->暂停, 暂停中->启动,) -->
<button class="btn btn-primary btn-block m-2" id="actionButton">启动</button>
<div class="m-2" style="margin-bottom: 5px">
<label class="fw-bold" style="font-size:0.9rem">单步次:</label>
<select class="form-select" id="modelSelect" style="font-size:0.9rem">
<label class="fw-bold" style="font-size:0.9rem">单步次:</label>
<select class="form-select" id="stepNumSelect" style="font-size:0.9rem">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>

8
web/main/templates/task_manager_modal.html

@ -159,9 +159,9 @@
<table class="table table-bordered table-hover">
<thead>
<tr>
<th style="width:50px;">序号</th>
<th>角色</th>
<th>内容</th>
<th style="width:5%;">序号</th>
<th style="width:10%;">角色</th>
<th style="width:85%;">内容</th>
</tr>
</thead>
<tbody id="submittedTbody">
@ -190,7 +190,7 @@
</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>
<textarea class="form-control" id="pendingContent" rows="10" placeholder="请输入内容"></textarea>
</div>
<!-- 你可以在此处增加一个保存按钮,由用户提交待提交的内容修改 -->
<div class="text-end">

Loading…
Cancel
Save