Compare commits

...

2 Commits

Author SHA1 Message Date
张龙 26ef8dc086 V0.5.5.4 3 weeks ago
张龙 2d4a358ba8 V0.5.5.3 3 weeks ago
  1. 2
      config.yaml
  2. 77
      mycode/AssetsManager.py
  3. 51
      mycode/DBManager.py
  4. 140
      mycode/PollingManager.py
  5. 125
      mycode/TargetManager.py
  6. 76
      mycode/TaskManager.py
  7. 21
      mycode/TaskObject.py
  8. 6
      run.py
  9. 51
      web/API/assets.py
  10. 13
      web/main/static/resources/css/flatpickr.min.css
  11. 49
      web/main/static/resources/scripts/assets_manager.js
  12. 64
      web/main/static/resources/scripts/base.js
  13. 2
      web/main/static/resources/scripts/flatpickr
  14. 1
      web/main/templates/base.html
  15. 496
      web/main/templates/polling_target.html

2
config.yaml

@ -1,6 +1,7 @@
#工作模式 #工作模式
App_Work_type: 0 #0-开发模式,只允许单步模式 1-生产模式 包裹下的逻辑可以正常执行 App_Work_type: 0 #0-开发模式,只允许单步模式 1-生产模式 包裹下的逻辑可以正常执行
max_run_task: 3 #允许同时运行的任务数 max_run_task: 3 #允许同时运行的任务数
max_polling_task: 3 #运行同时运行的巡检任务
#线程休眠的时间 #线程休眠的时间
sleep_time: 20 sleep_time: 20
@ -20,6 +21,7 @@ mysql:
database: zfsafe database: zfsafe
#LLM #LLM
polling_llm_type: 1 #0-腾讯云,1-DS,2-2233.ai,3-GPT
LLM_max_chain_count: 10 #为了避免推理链过长,造成推理效果变差,应该控制一个推理链的长度上限 LLM_max_chain_count: 10 #为了避免推理链过长,造成推理效果变差,应该控制一个推理链的长度上限
#Node #Node

77
mycode/AssetsManager.py

@ -1,4 +1,5 @@
from mycode.DBManager import app_DBM from mycode.DBManager import app_DBM
from mycode.TargetManager import g_TM
class AssetsManager: class AssetsManager:
def __init__(self): def __init__(self):
@ -142,11 +143,11 @@ class AssetsManager:
if not user or not type or not contact or not phone or not IDno: if not user or not type or not contact or not phone or not IDno:
return False, "有信息没有填写,请补充完整!", [] return False, "有信息没有填写,请补充完整!", []
if do_mode =="add": if do_mode =="add":
strsql = "select ID from assets_user where ID_num = %s" strsql = "select ID from assets_user where ID_num = %s or uname = %s;"
params = (IDno,) params = (IDno,user)
data = app_DBM.safe_do_select(strsql, params, 1) data = app_DBM.safe_do_select(strsql, params, 1)
if data: if data:
return False, "证件号码已经存在,请重新修改", [] return False, "证件号码或用户名称已经存在,请重新修改", []
strsql = "insert into assets_user (itype,uname,tellnum,tell_username,ID_num) values (%s,%s,%s,%s,%s);" strsql = "insert into assets_user (itype,uname,tellnum,tell_username,ID_num) values (%s,%s,%s,%s,%s);"
params = (type,user,phone,contact,IDno) params = (type,user,phone,contact,IDno)
@ -174,5 +175,75 @@ class AssetsManager:
bsuccess,error = app_DBM.del_owner_db(id) bsuccess,error = app_DBM.del_owner_db(id)
return bsuccess,error return bsuccess,error
#---------巡检目标------------
def add_polling_target(self,pollind_targets, owner_name,owner_id):
suc_list = []
fail_list = []
pTlist = pollind_targets.split(',')
#对目标的合法性进行初步判断
for pT in pTlist:
target_type,check_target = g_TM.is_valid_target(pT)
if not target_type: # 非法目标
fail_list.append(pT)
else: #合法目标
#判断巡检目标是否已存在
strsql = "select ID from target where scr_target = %s;"
params = (pT,)
data = app_DBM.safe_do_select(strsql,params,1)
if data:
fail_list.append(pT)
continue
#入库---是否调整为批量插入
if owner_name:
strsql = "insert into target (scr_target,check_target,owner_id,target_type) values (%s,%s,%s,%s);"
params = (pT,check_target,owner_id,target_type)
else:
strsql = "insert into target (scr_target,check_target,target_type) values (%s,%s,%s);"
params = (pT, check_target, target_type)
bok,_ = app_DBM.safe_do_sql(strsql,params)
if bok:
suc_list.append(pT)
else:
fail_list.append(pT)
return suc_list,fail_list
def get_polling_target(self,PT,owner,PP,safe_rank):
pTargets = app_DBM.get_polling_target_db(PT,owner,PP,safe_rank)
return pTargets
def update_pt_owner(self,PT,owner_id):
strsql = "update target set owner_id = %s where scr_target=%s;"
params = (owner_id,PT)
bok,_ = app_DBM.safe_do_sql(strsql,params)
if bok:
error = ""
else:
error = "修改目标所属用户失败,请联系技术支持!"
return bok,error
def update_pt_period(self,PT,polling_type,polling_period,selectedTime):
strsql = "update target set polling_type=%s,polling_period=%s,polling_start_time=%s where scr_target=%s;"
params = (polling_type,polling_period,selectedTime,PT)
bok, _ = app_DBM.safe_do_sql(strsql, params)
if bok:
error = ""
#? 需要更新该目标的巡检计划
else:
error = "修改目标巡检策略失败,请联系技术支持!"
return bok, error
def del_pt(self,PT):
strsql = "delete from target where scr_target=%s;"
params = (PT,)
bok, _ = app_DBM.safe_do_sql(strsql, params)
if bok:
error = ""
else:
error = "删除巡检目标失败,请联系技术支持!"
return bok, error
g_AssetsM = AssetsManager() g_AssetsM = AssetsManager()

51
mycode/DBManager.py

@ -219,11 +219,11 @@ SELECT COLUMN_NAME, CHARACTER_MAXIMUM_LENGTH
return data return data
def get_run_tasks(self): def get_run_tasks(self):
strsql = "select ID,task_target,task_status,work_type,cookie_info,llm_type,safe_rank,fake_target from task where task_status <> 2 order by ID;" strsql = "select ID,task_target,task_status,work_type,cookie_info,llm_type,safe_rank,fake_target,target_id from task where task_status <> 2 order by ID;"
datas = self.do_select(strsql) datas = self.do_select(strsql)
return datas return datas
def start_task(self,test_target,cookie_info,work_type,llm_type,fake_target) -> int: def start_task(self,test_target,cookie_info,work_type,llm_type,fake_target,target_id=0) -> int:
''' '''
数据库添加检测任务 数据库添加检测任务
:param task_name: :param task_name:
@ -232,9 +232,9 @@ SELECT COLUMN_NAME, CHARACTER_MAXIMUM_LENGTH
''' '''
task_id =0 task_id =0
start_time = get_local_timestr() start_time = get_local_timestr()
sql = "INSERT INTO task (task_name,task_target,start_time,task_status,safe_rank,work_type,cookie_info,llm_type,fake_target) " \ sql = "INSERT INTO task (task_name,task_target,start_time,task_status,safe_rank,work_type,cookie_info,llm_type,fake_target,target_id) " \
"VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s)" "VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)"
params = (test_target,test_target,start_time,1,0,work_type,cookie_info,llm_type,fake_target) params = (test_target,test_target,start_time,1,0,work_type,cookie_info,llm_type,fake_target,target_id)
bok,task_id = self.safe_do_sql(sql,params,1) bok,task_id = self.safe_do_sql(sql,params,1)
return task_id return task_id
@ -867,6 +867,47 @@ LEFT JOIN (
bok, _ = self.safe_do_sql(strsql, params) bok, _ = self.safe_do_sql(strsql, params)
return True,"" return True,""
def get_polling_target_db(self,PT,owner,PP,safe_rank):
strsql = '''
select t.scr_target,o.uname,t.polling_period,t.polling_last_time,t.risk_rank,t.polling_type,t.polling_start_time from target t
left join assets_user o on o.ID = t.owner_id
'''
conditions = []
params = []
# 按需添加其他条件
if PT and PT.strip():
conditions.append("t.scr_target like %s ")
params.append(f"%{PT}%")
if owner and owner.strip():
conditions.append("o.uname like %s ")
params.append(f"%{owner}%")
if PP and PP.strip():
conditions.append("t.polling_period=%s ")
params.append(PP)
if safe_rank and safe_rank.strip():
conditions.append("t.risk_rank = %s ")
params.append(safe_rank)
# 组合完整的WHERE子句
if len(conditions) > 0:
strsql += " WHERE " + " AND ".join(conditions)
# 执行查询(将参数转为元组)
datas = self.safe_do_select(strsql, tuple(params))
return datas
def update_polling_last_time(self,target_ID):
str_time = get_local_timestr()
strsql = "update target set polling_last_time = %s where ID=%s;"
params = (str_time,target_ID)
bok,_ = self.safe_do_sql(strsql,params)
return bok
def test(self): def test(self):
# 建立数据库连接 # 建立数据库连接
conn = pymysql.connect( conn = pymysql.connect(

140
mycode/PollingManager.py

@ -0,0 +1,140 @@
import threading
from time import sleep
from typing import List, Dict
from mycode.DBManager import DBManager
from datetime import datetime, timedelta
from mycode.TaskManager import g_TaskM
class PollingManager:
def __init__(self):
self.DBM = DBManager()
self.brun = True
self.bUpdate = False
self.p_th = None
def parse_time(self,time_str: str) -> datetime:
"""将时间字符串转为 datetime 对象"""
if not time_str:
return None
try:
return datetime.datetime.strptime(time_str, '%Y-%m-%d %H:%M:%S')
except ValueError:
return None
def need_check(self,target: Dict) -> bool:
now = datetime.datetime.now()
period = target[5]
start_time_str = target[6]
last_time = self.parse_time(target[7])
# 不巡检的目标
if target[4] == 0 or period == 0:
return False
# 当前时间是否达到设定开始时间
if start_time_str:
try:
# 只取时间部分用于每天/每周的对比
st_hour, st_minute = map(int, start_time_str.strip().split(':')[:2])
except Exception:
return False
else:
st_hour, st_minute = 0, 0
# 当天设定执行时间点
target_time = now.replace(hour=st_hour, minute=st_minute, second=0, microsecond=0)
# 如果 last_time 已经检查过今天了,就跳过
if last_time and last_time >= target_time:
return False
if period == 1: # 每天
return now >= target_time
elif period == 2: # 每周
return now.weekday() == 0 and now >= target_time # 每周一执行
elif period == 3: # 每月
return now.day == 1 and now >= target_time # 每月1号执行
else:
return False
def should_poll(self,polling_period: int, start_time_str: str, last_time_str: str = None) -> bool:
now = datetime.now()
# 无周期,直接返回 False
if polling_period == 0:
return False
try:
# 获取比较基准时间
if last_time_str:
base_time = datetime.strptime(last_time_str, "%Y-%m-%d %H:%M:%S")
elif start_time_str:
base_time = datetime.strptime(start_time_str, "%Y-%m-%d %H:%M:%S")
if now > base_time:
return True
else:
return False
except Exception as e:
print(f"时间格式错误: {e}")
return False
if polling_period == 1:
# 每天:超过 24 小时
return (now - base_time) >= timedelta(days=1)
elif polling_period == 2:
# 每周:要到下一个“周一的那个时间点”
weekday = base_time.weekday() # 0=周一, ..., 6=周日
days_to_next_monday = (7 - weekday) % 7 or 7 # 至少是下一个周一
next_monday = base_time + timedelta(days=days_to_next_monday)
next_monday = next_monday.replace(hour=base_time.hour, minute=base_time.minute, second=base_time.second)
return now >= next_monday
elif polling_period == 3:
# 每月:下个月的 1 号,时间点与原始时间相同
year = base_time.year + (base_time.month // 12)
month = (base_time.month % 12) + 1
next_month_first = datetime(year, month, 1, base_time.hour, base_time.minute, base_time.second)
return now >= next_month_first
return False
def do_check(self,target):
print(f"巡检中: {target[2]} (类型: {target[9]})")
#创建task---并初始化待办
g_TaskM.create_polling_task(target)
def update_last_time(self, target_id):
now_str = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
strsql = "UPDATE target SET polling_last_time=%s WHERE ID=%s;"
params = (now_str,target_id)
bok,_ = self.DBM.safe_do_sql(strsql,params)
def needUpdateTargets(self):
self.bUpdate = True
def polling_th(self):
strsql = "select * from target;"
targets = self.DBM.do_select(strsql)
while self.brun:
if self.bUpdate:
targets = self.DBM.do_select(strsql)
self.bUpdate =False
for target in targets:
if self.should_poll(target[5],target[6],target[7]):
self.do_check(target)
self.update_last_time(target[0])
sleep(60 * 60) #一个小时
def run_polling(self):
self.p_th = threading.Thread(target=self.polling_th(), name=f"p_th")
self.p_th.start()
if __name__ == "__main__":
PM = PollingManager()
PM.polling_th()

125
mycode/TargetManager.py

@ -10,6 +10,7 @@ import requests
import whois import whois
import dns.resolver import dns.resolver
import ssl import ssl
import random
from urllib.parse import urlparse from urllib.parse import urlparse
from datetime import datetime from datetime import datetime
@ -51,15 +52,13 @@ class TargetManager:
except ValueError: except ValueError:
continue continue
# 辅助函数:验证IPv4地址的有效性 def get_fake_target(self,type):
def _is_valid_ipv4(self,ip): if type ==1:
parts = ip.split('.') fake_target = "192.168.3." + str(random.randint(2, 254))
if len(parts) != 4: else:
return False fake_target = "czzfkj" + chr(random.randint(97, 122)) + chr(random.randint(97, 122))
for part in parts: return fake_target
if not part.isdigit():
return False
return True
#验证目标格式的合法性,并提取域名或IP #验证目标格式的合法性,并提取域名或IP
def validate_and_extract(self,input_str): def validate_and_extract(self,input_str):
@ -76,14 +75,12 @@ class TargetManager:
if target_type =="IPv4" or target_type=="IPv6": if target_type =="IPv4" or target_type=="IPv6":
type = 1 #IP type = 1 #IP
real_target = target
fake_target = "192.168.3.107"
elif target_type == "URL": elif target_type == "URL":
type = 2 #domain type = 2 #domain
real_target = target
fake_target = "czzfkjxx"
else: #目标不合法 else: #目标不合法
return False,real_target,type,fake_target return False,real_target,type,fake_target
real_target = target
fake_target = self.get_fake_target(type)
return True,real_target,type,fake_target return True,real_target,type,fake_target
#验证目标是否合法 #验证目标是否合法
@ -104,33 +101,72 @@ class TargetManager:
pass pass
# Check if target is a valid URL # Check if target is a valid URL
# 检查是否为有效的 URL
try: try:
# 解析 URL
result = urlparse(target) result = urlparse(target)
# Only allow http or https schemes
if not result.scheme: # 确保 URL 具有协议(http 或 https)
result = urlparse('http://'+target) if not result.scheme or result.scheme not in ['http', 'https']:
# 如果没有协议,尝试添加 'http://' 并重新解析
result = urlparse('http://' + target)
if not result.netloc:
return None, None
else:
# 如果有协议,确保 netloc 不为空
if not result.netloc:
return None, None
netloc = result.netloc netloc = result.netloc
if not netloc:
return None,None # 处理 URL 中的 IPv6 地址(用方括号括起来)
# Handle IPv6 addresses in URLs (enclosed in brackets)
if netloc.startswith('[') and netloc.endswith(']'): if netloc.startswith('[') and netloc.endswith(']'):
ip_str = netloc[1:-1] ip_str = netloc[1:-1]
try: try:
ipaddress.IPv6Address(ip_str) ipaddress.IPv6Address(ip_str)
return 'IPv6',ipaddress return 'IPv6', ip_str
except ValueError: except ValueError:
return None,None return None, None
# Handle potential IPv4 addresses # 处理可能的 IPv4 地址
elif self._is_valid_ipv4(netloc): elif self._is_valid_ipv4(netloc):
try: try:
ipaddress.IPv4Address(netloc) ipaddress.IPv4Address(netloc)
return 'IPv4',ipaddress return 'IPv4', netloc
except ValueError: except ValueError:
return None,None return None, None
# If not an IP-like string, assume it's a domain name and accept # 检查 netloc 是否为有效的域名
return 'URL',netloc elif self._is_valid_domain(netloc):
return 'URL', netloc
else:
return None, None
except ValueError: except ValueError:
return None,None return None, None
def _is_valid_ipv4(self, ip):
'''
检查字符串是否为有效的 IPv4 地址格式
:param ip: 输入的字符串
:return: True 如果是有效的 IPv4 地址否则 False
'''
parts = ip.split('.')
if len(parts) != 4:
return False
for part in parts:
if not part.isdigit() or not 0 <= int(part) <= 255:
return False
return True
def _is_valid_domain(self, domain):
'''
检查字符串是否为有效的域名格式
:param domain: 输入的字符串
:return: True 如果是有效的域名否则 False
'''
# 使用正则表达式验证域名格式
domain_regex = re.compile(
r'^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$'
)
return bool(domain_regex.match(domain))
def collect_ip_info(self,ip): def collect_ip_info(self,ip):
info = {} info = {}
@ -203,26 +239,29 @@ g_TM = TargetManager()
if __name__ == "__main__": if __name__ == "__main__":
#tm = TargetManager() #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",
"www.crnn.cn",
"oa.crnn.cn",
"ftp://invalid.com", # 不合规
"http://300.400.500.600" # 不合规
]
# test_cases = [ # test_cases = [
# "256.254.1111.23",
# "8.8.8.8",
# "2001:db8::1",
# "http://www.crnn.cc/", # "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 = [
"4545.234",
"http://www.crnn.cc/product_category/network-security-services",
"not a url",
"192.15.2.3",
"12314.5123.45123"
]
#tm.test("https://www.crnn.cn") #tm.test("https://www.crnn.cn")
for case in test_cases: for case in test_cases:

76
mycode/TaskManager.py

@ -11,22 +11,37 @@ class TaskManager:
def __init__(self): def __init__(self):
self.logger = LogHandler().get_logger("TaskManager") self.logger = LogHandler().get_logger("TaskManager")
self.TargetM = TargetManager() self.TargetM = TargetManager()
self.tasks = {} # 执行中的任务,test_id为key
self.num_threads = myCongif.get_data("Task_max_threads")
self.max_run_task = myCongif.get_data("max_run_task")
self.cur_task_run_num = 0
#获取系统信息 -- 用户可修改的都放在DB中,不修改的放config #获取系统信息 -- 用户可修改的都放在DB中,不修改的放config
self.num_threads = myCongif.get_data("Task_max_threads")
data = app_DBM.get_system_info() data = app_DBM.get_system_info()
self.local_ip = data[0] self.local_ip = data[0]
self.version = data[1] self.version = data[1]
self.tasks_lock = threading.Lock() self.tasks_lock = threading.Lock()
self.web_cur_task = 0 #web端当前显示的 self.brun = True
self.tm_th = None #任务控制线程
#----临时任务相关-------
self.web_cur_task = 0 #web端当前显示的 -- web端可以操作的都为临时任务
self.tasks = {} # 执行中的任务,test_id为key
self.max_run_task = myCongif.get_data("max_run_task")
self.cur_task_run_num = 0
#-----巡检任务相关-------
self.polling_llmtype = myCongif.get_data("polling_llm_type")
self.polling_tasks = {} #独立一个队列
self.max_polling_task = myCongif.get_data("max_polling_task")
self.cur_polling_task = 0
self.work_type = 1 #巡检工作模式就是自动了
#判断目标是不是在当前执行任务中,---没加锁,最多跟删除有冲突,问题应该不大 #判断目标是不是在当前执行任务中,---没加锁,最多跟删除有冲突,问题应该不大
def is_target_in_tasks(self,task_target): def is_target_in_tasks(self,task_target,itype = 1):
for task in self.tasks.values(): '''itype:1--临时任务,2--巡检任务'''
if task_target == task.target: if itype ==1:
return True for task in self.tasks.values():
if task_target == task.target:
return True
else:
for p_task in self.polling_tasks.values():
if task_target == p_task.target:
return True
return False return False
#程序启动后,加载未完成的测试任务 #程序启动后,加载未完成的测试任务
@ -34,7 +49,7 @@ class TaskManager:
'''程序启动时,加载未执行完成的,未点击结束的任务 -- task_status<>2 '''程序启动时,加载未执行完成的,未点击结束的任务 -- task_status<>2
#若不是异常停止,正常情况下,任务都应该是没有待办MQ的 #若不是异常停止,正常情况下,任务都应该是没有待办MQ的
''' '''
datas = app_DBM.get_run_tasks() datas = app_DBM.get_run_tasks() #
for data in datas: for data in datas:
task_id = data[0] task_id = data[0]
task_target = data[1] task_target = data[1]
@ -44,8 +59,12 @@ class TaskManager:
llm_type = data[5] llm_type = data[5]
safe_rank = data[6] safe_rank = data[6]
fake_target = data[7] fake_target = data[7]
target_id = data[8]
# 创建任务对象 # 创建任务对象
task = TaskObject(task_target, cookie_info, work_type, llm_type, self.num_threads, self.local_ip,fake_target,self,safe_rank) task = TaskObject(task_target, cookie_info, work_type, llm_type, self.num_threads, self.local_ip,fake_target,target_id,self,safe_rank)
#这里要做区分.临时任务和巡检任务
#?
#读取attact_tree---load的任务应该都要有attact_tree #读取attact_tree---load的任务应该都要有attact_tree
attack_tree = g_PKM.ReadData(str(task_id)) attack_tree = g_PKM.ReadData(str(task_id))
if attack_tree: if attack_tree:
@ -96,12 +115,43 @@ class TaskManager:
task.init_task(task_id) task.init_task(task_id)
#保留task对象 #保留task对象
self.tasks[task_id] = task self.tasks[task_id] = task
#尝试启动task
self.start_task_TM(task_id)
else: else:
fail_list.append(target) fail_list.append(target)
result = ",".join(fail_list) result = ",".join(fail_list)
return result return result
#开启task任务--正常只应该有web触发调用 def create_polling_task(self,target):
#创建巡检任务 -- 使用check_target
task_target = target[2]
if self.is_target_in_tasks(task_target,2): #巡检任务一个周期还没有执行完。
#更新检查时间,本次巡检轮次不执行工作
bok = app_DBM.update_polling_last_time(target[0])
else:#创建任务
if target[9] == "IPv4" or target[9] == "IPv6":
fake_target = self.TargetM.get_fake_target(1)
else:
fake_terget = self.TargetM.get_fake_target(2)
#创建任务实例
polling_task = TaskObject(target, "", self.work_type, self.polling_llmtype, self.num_threads, self.local_ip, fake_target, self)
# 获取task_id -- test_target,cookie_info,work_type,llm_type 入数据库
task_id = app_DBM.start_task(target, "", self.work_type, self.polling_llmtype, fake_target,target[0]) #增加一个target_id
if task_id >0:
polling_task.init_task(task_id)#初始化任务信息,并提交到待处理节点队列
#保留task对象--巡检任务
self.polling_tasks[task_id] = polling_task
def th_control_takes(self):
while self.brun:
#遍历临时任务--启停任务
pass
#遍历巡检任务-启停
pass
#休眠
time.sleep(60*5)
#开启task任务
def start_task_TM(self,task_id): def start_task_TM(self,task_id):
task = self.tasks[task_id] task = self.tasks[task_id]
if task: if task:
@ -118,7 +168,7 @@ class TaskManager:
return False,f"已到达最大的启动数--{self.max_run_task}" return False,f"已到达最大的启动数--{self.max_run_task}"
return False,"该任务不存在,程序逻辑存在问题!" return False,"该任务不存在,程序逻辑存在问题!"
#停止task任务 #停止task任务--暂停
def stop_task_TM(self,task_id): def stop_task_TM(self,task_id):
task = self.tasks[task_id] task = self.tasks[task_id]
if task: if task:

21
mycode/TaskObject.py

@ -27,7 +27,7 @@ import textwrap
class TaskObject: class TaskObject:
def __init__(self,test_target,cookie_info,work_type,llm_type,num_threads,local_ip,fake_target,taskM,safe_rank=0): def __init__(self,test_target,cookie_info,work_type,llm_type,num_threads,local_ip,fake_target,taskM,target_id=0,safe_rank=0):
#功能类相关 #功能类相关
self.taskM = taskM self.taskM = taskM
self.logger = LogHandler().get_logger("TaskObject") self.logger = LogHandler().get_logger("TaskObject")
@ -42,6 +42,7 @@ class TaskObject:
self.max_layer = myCongif.get_data("max_node_layer") self.max_layer = myCongif.get_data("max_node_layer")
self.sleep_time = myCongif.get_data("sleep_time") self.sleep_time = myCongif.get_data("sleep_time")
self.target = test_target self.target = test_target
self.target_id = target_id #巡检任务有值--对应巡检目标
self.cookie = cookie_info self.cookie = cookie_info
self.work_type = work_type #工作模式 0-人工,1-自动 self.work_type = work_type #工作模式 0-人工,1-自动
self.task_id = None self.task_id = None
@ -542,15 +543,15 @@ class TaskObject:
self.put_instr_mq(node) #2-执行中 self.put_instr_mq(node) #2-执行中
def put_work_node(self,work_type): def put_work_node(self,work_type):
'''遍历节点需要处理的任务,提交mq,load_task-在自动模式下-触发--线程安全 '''遍历节点需要处理的任务,提交mq,load_task-触发--线程安全
work_type 0-人工1-自动 work_type 0-人工1-自动
''' '''
instr_status = None instr_status = None
llm_status = None llm_status = None
if work_type == 0: if work_type == 0: #人工
instr_status = (2,) instr_status = (2,)
llm_status = (4,) llm_status = (4,)
else: else: #自动
instr_status = (1,2) instr_status = (1,2)
llm_status = (3,4) llm_status = (3,4)
@ -559,12 +560,12 @@ class TaskObject:
if not node.bwork: if not node.bwork:
continue continue
node_work_status = node.get_work_status() node_work_status = node.get_work_status()
if node_work_status in instr_status: #待执行指令 if node_work_status in instr_status: #待执行指令
if node.is_instr_empty():#说明数据有问题了,放弃掉 if node.is_instr_empty():#
node.update_work_status(-1) #置0 -1作为额外的条件参数 node.update_work_status(-1) #置0 -1作为额外的条件参数
else: else:
self.put_instr_node(node) #1,2都提交执行 self.put_instr_node(node) #instr_status都提交执行
node.update_work_status(-2)# 置2 node.update_work_status(-2)# 置2 --执行中
#llm-list不处理,正常应该为空 #llm-list不处理,正常应该为空
elif node_work_status in llm_status: elif node_work_status in llm_status:
if node.is_llm_empty():#数据有问题,放弃掉 if node.is_llm_empty():#数据有问题,放弃掉
@ -983,9 +984,7 @@ class TaskObject:
# 初始化节点树 # 初始化节点树
if attack_tree: # 有值的情况是load if attack_tree: # 有值的情况是load
self.attack_tree = attack_tree self.attack_tree = attack_tree
# 加载未完成的任务 # 加载未完成的任务-并恢复工作
# if self.work_type == 1: # 自动模式
# # 提交到mq,待线程执行
self.put_work_node(self.work_type) self.put_work_node(self.work_type)
else: # 无值的情况是new_create else: # 无值的情况是new_create

6
run.py

@ -4,6 +4,7 @@ from mycode.TaskManager import g_TaskM
from web import create_app from web import create_app
from hypercorn.asyncio import serve from hypercorn.asyncio import serve
from hypercorn.config import Config from hypercorn.config import Config
from mycode.PollingManager import PollingManager
async def run_quart_app(): async def run_quart_app():
app = create_app() app = create_app()
@ -30,6 +31,7 @@ if __name__ == '__main__':
g_TaskM.load_tasks() g_TaskM.load_tasks()
#启动web项目--hypercorn #启动web项目--hypercorn
asyncio.run(run_quart_app()) asyncio.run(run_quart_app())
#Uvicom启动 #启动巡检线程
#uvicorn.run("run:app", host="0.0.0.0", port=5001, workers=4, reload=True) PM = PollingManager()
PM.run_polling()

51
web/API/assets.py

@ -168,3 +168,54 @@ async def del_Owner():
id = data.get("id") id = data.get("id")
bsuccess,error = g_AssetsM.del_owner(id) bsuccess,error = g_AssetsM.del_owner(id)
return jsonify({"bsuccess": bsuccess, "error": error}) return jsonify({"bsuccess": bsuccess, "error": error})
#-------------巡检目标相关------------
@api.route('/assets/addpollingtarget', methods=['POST'])
@login_required
async def add_polling_target():
data = await request.get_json()
pollind_targets = data.get("pollind_targets")
owner_name = data.get("selectedOwnerName")
owner_id = data.get("selectedOwnerId")
success_list,fail_list = g_AssetsM.add_polling_target(pollind_targets,owner_name,owner_id)
return jsonify({"success_list":success_list,"fail_list":fail_list})
@api.route('/assets/getpollingtarget', methods=['POST'])
@login_required
async def get_polling_target():
data = await request.get_json()
# PT,owner,PP,safe_rank
PT = data.get("PT")
owner = data.get("owner")
PP = data.get("PP")
safe_rank = data.get("safe_rank")
pTargets = g_AssetsM.get_polling_target(PT,owner,PP,safe_rank)
return jsonify({"pTargets": pTargets})
@api.route('/assets/updatePTOwner', methods=['POST'])
@login_required
async def update_pt_owner():
data = await request.get_json()
PT = data.get("PT")
owner_id = data.get("owner_id")
bsuccess,error = g_AssetsM.update_pt_owner(PT,owner_id)
return jsonify({"bsuccess": bsuccess,"error":error})
@api.route('/assets/updatePTPeriod', methods=['POST'])
@login_required
async def update_pt_period():
data = await request.get_json()
PT = data.get("PT")
polling_type = int(data.get("polling_type"))
polling_period = int(data.get("polling_period"))
selectedTime = data.get("localTimeStr")
bsuccess,error = g_AssetsM.update_pt_period(PT,polling_type,polling_period,selectedTime)
return jsonify({"bsuccess": bsuccess, "error": error})
@api.route('/assets/delPT', methods=['POST'])
@login_required
async def del_pt():
data = await request.get_json()
PT = data.get("PT")
bsuccess,error = g_AssetsM.del_pt(PT)
return jsonify({"bsuccess": bsuccess, "error": error})

13
web/main/static/resources/css/flatpickr.min.css

File diff suppressed because one or more lines are too long

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

@ -14,34 +14,7 @@ window.addEventListener("beforeunload", function() {
currentIpPage = 1; currentIpPage = 1;
}); });
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();
}
/* ---------- 简易 Toast ---------- */
function showToast(msg, type='info') {
const toastEl = document.createElement('div');
toastEl.className = `toast align-items-center text-white bg-${type} border-0 position-fixed bottom-0 end-0 m-3`;
toastEl.role = 'alert';
toastEl.innerHTML = `
<div class="d-flex">
<div class="toast-body">${msg}</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
</div>`;
document.body.appendChild(toastEl);
const t = new bootstrap.Toast(toastEl, { delay: 3000 });
t.show();
toastEl.addEventListener('hidden.bs.toast', () => toastEl.remove());
}
//---------------------------IP-Assets Tab---------------------------- //---------------------------IP-Assets Tab----------------------------
async function exportIpAssets(){ async function exportIpAssets(){
@ -401,19 +374,6 @@ async function showPortData(IP){
} }
}; };
// 工具:格式化单元格内容,遇到“纯数字-数字”形式时自动做公式化处理
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;
}
function exportLatest() { function exportLatest() {
// 构造 CSV 文本 // 构造 CSV 文本
let rows = [ let rows = [
@ -456,15 +416,6 @@ async function showPortData(IP){
} }
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);
}
//***********漏洞数据************** //***********漏洞数据**************
const vulModalEl = document.getElementById('vulDataModal'); const vulModalEl = document.getElementById('vulDataModal');
const nodeNameEl = document.getElementById("vulNodeName"); const nodeNameEl = document.getElementById("vulNodeName");

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

@ -84,9 +84,34 @@ function isValidIP(ip) {
return ipRegex.test(ip); return ipRegex.test(ip);
} }
//post提交JSON数据 //post数据
function postJson(url,data){ 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();
}
/* ---------- 简易 Toast ---------- */
function showToast(msg, type='info') {
const toastEl = document.createElement('div');
toastEl.className = `toast align-items-center text-white bg-${type} border-0 position-fixed bottom-0 end-0 m-3`;
toastEl.role = 'alert';
toastEl.innerHTML = `
<div class="d-flex">
<div class="toast-body">${msg}</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
</div>`;
document.body.appendChild(toastEl);
const t = new bootstrap.Toast(toastEl, { delay: 3000 });
t.show();
toastEl.addEventListener('hidden.bs.toast', () => toastEl.remove());
} }
//post提交From数据 -- 返回数据是JSON //post提交From数据 -- 返回数据是JSON
@ -114,3 +139,38 @@ function postFrom(url,data){
//get请求数据 //get请求数据
function getDATA(url){ function getDATA(url){
} }
//导出csv表格
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;
}
function formatDateToLocalString(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0'); // 月份从 0 开始
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}

2
web/main/static/resources/scripts/flatpickr

File diff suppressed because one or more lines are too long

1
web/main/templates/base.html

@ -5,6 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}My Website{% endblock %}</title> <title>{% block title %}My Website{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('main.static', filename='css/bootstrap.min.css') }}"> <link rel="stylesheet" href="{{ url_for('main.static', filename='css/bootstrap.min.css') }}">
<link rel="stylesheet" href="{{ url_for('main.static', filename='css/flatpickr.min.css') }}">
<link href="{{ url_for('main.static', filename='css/headers.css') }}" rel="stylesheet"> <link href="{{ url_for('main.static', filename='css/headers.css') }}" rel="stylesheet">
{% block style_link %}{% endblock %} {% block style_link %}{% endblock %}
<style> <style>

496
web/main/templates/polling_target.html

@ -31,6 +31,10 @@
height: 45px; height: 45px;
overflow: hidden; overflow: hidden;
} }
.offcanvas {
z-index: 1060 !important;
}
{% endblock %} {% endblock %}
<!-- 页面内容块 --> <!-- 页面内容块 -->
@ -38,7 +42,7 @@
<div class="container-xxl mt-2"> <div class="container-xxl mt-2">
<!-- 查询区 --> <!-- 查询区 -->
<div class="row mb-3"> <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-2 mb-2"><button class="btn btn-primary me-3" onclick="importModal()">导入</button></div>
<div class="col-10"></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 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-3"><input type="text" class="form-control" id="owner" placeholder="所属用户"></div>
@ -62,8 +66,8 @@
</select> </select>
</div> </div>
<div class="col-2 text-end"> <div class="col-2 text-end">
<button class="btn btn-primary" onclick="fetchData()">查询</button> <button class="btn btn-primary" onclick="loadPollingT(1)">查询</button>
<button class="btn btn-primary" onclick="exportOwnerData()">导出</button> <button class="btn btn-primary" onclick="exportPTData()">导出</button>
</div> </div>
</div> </div>
<!-- 表格 --> <!-- 表格 -->
@ -95,14 +99,500 @@
</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---------- --> <!-- --------导入modal---------- -->
<!-- 导入目标 Modal -->
<div class="modal fade" id="importTargetModal" tabindex="-1" aria-labelledby="importTargetModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content">
<div class="modal-header border-0">
<h4 class="modal-title fw-bold" id="importTargetModalLabel">导入目标</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="关闭"></button>
</div>
<div class="modal-body">
<div class="mb-3 d-flex justify-content-between align-items-center">
<label class="form-label fw-bold mb-0">巡检目标:</label>
<button class="btn btn-primary text-white" onclick="importTargetTxt()">导入文件</button>
</div>
<textarea class="form-control" rows="8" style="resize: none;" id="pollingTarget" placeholder="输入巡检目标。多目标以,(英文逗号)隔开,或导入txt文件(一行一个目标)"></textarea>
<input type="file" id="fileInput" accept=".txt" style="display:none;"/>
<div class="row mt-3 align-items-center">
<div class="col-2"><label class="form-label fw-bold">所属用户:</label></div>
<div class="col-8"><input type="text" class="form-control" id="addtargetowner" placeholder="所属用户"></div>
<div class="col-2 d-flex justify-content-end"><button type="button" class="btn btn-primary" onclick="addOwner(1)">所属用户</button></div>
</div>
</div>
<div class="modal-footer border-0 d-flex justify-content-end">
<button type="button" class="btn btn-primary me-3" onclick="addPollingTarget()">确定</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
</div>
</div>
</div>
</div>
<!-- --------所属用户modal---------- --> <!-- --------所属用户modal---------- -->
<div class="modal fade" id="ownerModal" tabindex="-1" aria-labelledby="ownerModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content">
<div class="modal-header border-0">
<h4 class="modal-title fw-bold" id="ownerModalLabel">所属用户</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="关闭"></button>
</div>
<div class="modal-body">
<div class="mb-3 d-flex justify-content-between align-items-center">
<label class="form-label fw-bold mb-0">所属用户:</label>
<button class="btn btn-primary text-white" onclick="showOwner()">修改</button>
</div>
<input type="text" class="form-control" id="now_owner" placeholder="所属用户">
</div>
<div class="modal-footer border-0 d-flex justify-content-end">
<button type="button" class="btn btn-primary me-3" onclick="upOwner()">确定</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
</div>
</div>
</div>
</div>
<!-- --------巡检策略modal---------- --> <!-- --------巡检策略modal---------- -->
<div class="modal fade" id="strategyModal" tabindex="-1" aria-labelledby="strategyModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content">
<div class="modal-header border-0">
<h4 class="modal-title fw-bold" id="strategyModalLabel">巡检策略</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="关闭"></button>
</div>
<div class="modal-body">
<div class="row align-items-center mb-3">
<div class="col-2 text-end fw-bold">
检测方案:
</div>
<div class="col-10">
<select class="form-select" id = "polling_type_Select">
<option selected value=0>请选择</option>
<option value=1>安全性检测</option>
<option value=2>可用性检测</option>
</select>
</div>
</div>
<div class="row align-items-center mb-3">
<div class="col-2 text-end fw-bold">
巡检周期:
</div>
<div class="col-10">
<select class="form-select" id = "pollint_period_Select">
<option selected value=0>请选择</option>
<option value=1>每日</option>
<option value=2>每周</option>
<option value=3>每月</option>
</select>
</div>
</div>
<div class="row align-items-center mb-3">
<div class="col-2 text-end fw-bold">
开始时间:
</div>
<div class="col-10">
<input type="text" class="form-control" id="startTimeInput" placeholder="请选择开始时间">
</div>
</div>
</div>
<div class="modal-footer border-0 justify-content-end">
<button type="button" class="btn btn-primary me-2" id="ipdatePTP" onclick="savePTP()">保存</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
</div>
</div>
</div>
</div>
{% endblock %} {% endblock %}
<!-- 页面脚本块 --> <!-- 页面脚本块 -->
{% block script %} {% block script %}
<script src="{{ url_for('main.static', filename='scripts/flatpickr') }}"></script>
<script>
//------------刷新页面表格-----------
const pollingT_tbody = document.querySelector('#pollingTable tbody');
const pT_prev = document.getElementById("pollingPrev");
const pT_next = document.getElementById("pollingNext");
let polling_targets = [],currentPTPage = 1,pageSize = 10;
let curTPlist_index = 0;
document.addEventListener("DOMContentLoaded", async () => {
// 初始加载
loadPollingT();
});
// 页面卸载时断开连接
window.addEventListener("beforeunload", function() {
polling_targets = [];
currentPTPage = 1;
});
// 分页按钮
pT_prev.addEventListener('click', e => { e.preventDefault(); randerPTTable(+e.target.dataset.page); });
pT_next.addEventListener('click', e => { e.preventDefault(); randerPTTable(+e.target.dataset.page); });
async function loadPollingT(page=1){
const PT = document.getElementById("polltarget").value.trim();
const owner = document.getElementById("owner").value.trim();
const PP = document.getElementById("polling_period").value;
const safe_rank = document.getElementById("risk_rank").value;
try {
const data = await postJSON("/api/assets/getpollingtarget",{PT,owner,PP,safe_rank})
polling_targets = data.pTargets || [];
randerPTTable(page); //刷新表格
} catch (error) {
console.error("查询巡检目标出错:", error);
alert("查询失败!"+error);
}
}
function getstrPollingType(itype){
strType = "";
if(itype === 1){
strType = "每天";
}else if(itype === 2){
strType = "每周";
}else if(itype === 3){
strType = "每月";
}
return strType;
}
function randerPTTable(page){
currentPTPage = page;
const start = (currentPTPage-1)*pageSize;
const slice = polling_targets.slice(start, start+pageSize);
pollingT_tbody.innerHTML = '';
for (let i=0; i<slice.length; i++) {
const item = slice[i];
const tr = document.createElement('tr');
const strType = getstrPollingType(item[2])
tr.innerHTML = `
<td>${start+i+1}</td>
<td>${item[0]}</td>
<td>${item[1]}</td>
<td>${strType}</td>
<td>${item[3]}</td>
<td>${item[4]}</td>
<td>
<button class="btn btn-sm btn-info asset-op-btn" onclick="updateOwner('${start+i}')">所属用户</button>
<button class="btn btn-sm btn-info asset-op-btn" onclick="PTConfig('${start+i}')">巡检策略</button>
<button class="btn btn-sm btn-danger asset-op-btn" onclick="confirmDeletePT('${start+i}')">删除</button>
</td>
`;
pollingT_tbody.appendChild(tr);
}
// 补足空行
for (let i=slice.length; i<pageSize; i++) {
const tr = document.createElement('tr');
tr.innerHTML = '<td colspan="9">&nbsp;</td>';
pollingT_tbody.appendChild(tr);
}
// 更新分页
pT_prev.dataset.page = currentPTPage>1?currentPTPage-1:1;
pT_next.dataset.page = pollingT_tbody.length>currentPTPage*pageSize?currentPTPage+1:currentPTPage;
}
//导出
function exportPTData(){
// 构造 CSV 文本
let rows = [
['序号', '检测目标', '所属用户', '检测周期', '最新检测时间','风险等级'].map(fmtCell).join(',')
];
polling_targets.forEach((row, i) => {
rows.push([
(i + 1).toString(),
row[0] || '',
row[1] || '',
row[2] || '',
row[3] || '',
row[4].toString(),
].map(fmtCell).join(','));
});
const csv = '\uFEFF' + rows.join('\r\n'); // 加 BOM
downloadCSV(csv, 'Polling_Target.csv');
}
//所属用户修改
function updateOwner(index){
select_data = polling_targets[index];
curTPlist_index = index;
addOwner(2); //类型为2
}
//删除目标
async function confirmDeletePT(index){
if (!confirm('确认删除?')) return;
select_data = polling_targets[index];
PT = select_data[0]
//提交接口
try {
const data = await postJSON("/api/assets/delPT",{PT});
bsuccess = data.bsuccess;
error = data.error;
if(bsuccess){
//更新数据
polling_targets.splice(index,1);
//刷新表格
randerPTTable(currentPTPage);
}
else {
alert("删除巡检目标失败:"+error);
}
} catch (error) {
console.error("删除巡检目标失败:", error);
alert("删除巡检目标失败!");
}
}
//-----------导入modal-------------
const importTargetModalEl = document.getElementById("importTargetModal");
const importTargetModal = new bootstrap.Modal(importTargetModalEl);
const addTargetEl = document.getElementById("pollingTarget"); //目标输入框
const fileInput = document.getElementById('fileInput');
const ownerEl = document.getElementById("addtargetowner")
//显示导入modal
function importModal(){
selectedOwnerId = null;
addTargetEl.value = "";
importTargetModal.show();
}
//导入txt文件
function importTargetTxt(){
fileInput.value = null; // 允许重复选择同一个文件
fileInput.click();
}
// 文件选中后读取内容、替换换行并填入输入框
fileInput.addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;
try {
// 现代浏览器支持直接用 File.text()
const text = await file.text();
// 按行拆分、去空行、再用英文逗号拼起来
const targets = text
.split(/\r?\n/) // 按 Unix/Windows 换行拆分
.map(line => line.trim()) // 去掉每行首尾空白
.filter(line => line) // 丢掉空行
.join(',');
// 填入测试目标输入框
addTargetEl.value = targets;
} catch (err) {
console.error('读取文件失败', err);
alert('读取文件失败,请检查文件格式');
}
});
//导入巡检目标,提交接口
async function addPollingTarget(){
pollind_targets = addTargetEl.value;
selectedOwnerName = ownerEl.value;
//提交接口
try {
const data = await postJSON("/api/assets/addpollingtarget",{pollind_targets,selectedOwnerName,selectedOwnerId});
success_list = data.success_list || [];
fail_list = data.fail_list || [];
let strsuc = success_list.join(',');
let strfail = fail_list.join(',');
let strout = "导入成功的有:\n" + strsuc + "\n导入失败的有:"+ strfail;
alert(strout);
importTargetModal.hide();
loadPollingT();
} catch (error) {
console.error("导入巡检目标出错:", error);
alert("导入巡检目标出错!");
}
}
//------------所属用户侧边栏---------------
const ownerDrawerEl = document.getElementById('ownerDrawer');
const ownerSearchEl = document.getElementById('ownerSearchKeyword');
const ownerTableBody = document.getElementById('ownerTableBody');
const btnSearchOwner = document.getElementById('btnSearchOwner');
const ownerDrawer = new bootstrap.Offcanvas(ownerDrawerEl);
let selectedOwnerId = null; // 当前选定的 user_id
let selectedOwnerName = null;
let owner_list = null; // 资产所属单位列表
let add_owner_type = 0; //修改所属用的来源 1-导入,2-列表
//显示所属用户侧边栏
function addOwner(itype){
add_owner_type = itype
ownerDrawer.show();
loadOwners(ownerSearchEl.value.trim());
}
//选择一个所属用户
ownerTableBody.addEventListener('click', e => {
const btn = e.target.closest('button[data-id]');
if (!btn) return;
selectedOwnerId = btn.dataset.id;
selectedOwnerName = btn.dataset.name;
if(add_owner_type === 1){
ownerEl.value = btn.dataset.name;
}
else {
//提交后台更新所属用户----需要删除用户没实现 --updatePTOwner PT owner_id
PT = polling_targets[curTPlist_index][0]
postupdateOwner(PT,selectedOwnerId)
}
ownerDrawer.hide();
});
//5.1--搜索按钮
btnSearchOwner.addEventListener('click', () => loadOwners(ownerSearchEl.value.trim()));
//调用接口获取所属用户列表
async function loadOwners(keyword='') {
try {
const res = await fetch('/api/assets/getassetsuser', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ keyword })
});
owner_list = (await res.json()).user_list;
ownerTableBody.innerHTML = '';
//ID,uname,tellnum,tell_username
owner_list.forEach((u, idx) => {
const tr = document.createElement('tr');
tr.innerHTML = `
<td>${idx + 1}</td>
<td>${u[1]}</td>
<td><button class="btn btn-link p-0" data-id="${u[0]}" data-name="${u[1]}"
data-tellnum="${u[2]}" data-telluname="${u[3]}">选择</button></td>`;
ownerTableBody.appendChild(tr);
});
} catch (e) { showToast('加载用户列表失败', 'danger'); }
}
//----------所属用户-----------
async function postupdateOwner(PT,owner_id){
//提交接口
try {
const data = await postJSON("/api/assets/updatePTOwner",{PT,owner_id});
bsuccess = data.bsuccess;
error = data.error;
if(bsuccess){
//更新数据
polling_targets[curTPlist_index][1] = selectedOwnerName
//刷新表格
randerPTTable(currentPTPage);
}
else {
alert("修改所属用户失败:"+error);
}
} catch (error) {
console.error("修改所属用户失败:", error);
alert("修改所属用户失败!");
}
}
//-----------巡检策略---------
const ptpModalEl = document.getElementById("strategyModal")
const ptpModal = new bootstrap.Modal(ptpModalEl);
const ptEl = document.getElementById("polling_type_Select");
const ppEl = document.getElementById("pollint_period_Select");
const pstEl = document.getElementById("startTimeInput")
// 初始化时间控件
const fp = flatpickr("#startTimeInput", {
enableTime: true,
dateFormat: "Y-m-d H:i",
time_24hr: true,
});
//巡检策略配置--显示modal窗口
function PTConfig(index){
curTPlist_index = index;
select_data = polling_targets[index]; //t.scr_target,o.uname,t.polling_period,k.start_time,t.risk_rank,t.polling_type,t.polling_start_time
polling_type = select_data[5];
polling_period = select_data[2];
polling_start_time= select_data[6];
if(polling_type){
ptEl.value = polling_type;
}
if(polling_period){
ppEl.value = polling_period;
}
fp.setDate(polling_start_time)
ptpModal.show();
}
//保存巡检策略
async function savePTP(){
PT = polling_targets[curTPlist_index][0];
polling_type = parseInt(ptEl.value); //巡检类型
polling_period = parseInt(ppEl.value); //巡检周期
polling_start_time= pstEl.value; //巡检开始时间
// 将字符串转为时间对象
const selectedTime = new Date(polling_start_time);
const currentTime = new Date();
const localTimeStr = formatDateToLocalString(selectedTime);
if(polling_type === 0 || polling_period === 0 || isNaN(selectedTime.getTime())){
alert("巡检策略参数不能为空!");
return;
}
if (selectedTime <= currentTime) {
alert("开始时间必须大于当前时间!");
return;
}
//提交接口
try {
const data = await postJSON("/api/assets/updatePTPeriod",{PT,polling_type,polling_period,localTimeStr});
bsuccess = data.bsuccess;
error = data.error;
if(bsuccess){
//更新数据
polling_targets[curTPlist_index][2] = polling_period;
polling_targets[curTPlist_index][5] = polling_type;
polling_targets[curTPlist_index][6] = localTimeStr;
//刷新表格
randerPTTable(currentPTPage);
ptpModal.hide();
}else {
alert("修改巡检策略失败:"+error);
}
} catch (error) {
console.error("修改巡检策略失败:", error);
alert("修改巡检策略失败!"+error);
}
}
</script>
{% endblock %} {% endblock %}
Loading…
Cancel
Save