Browse Source

V0.5.5.3

1.dnoe polling task;
2.need auto do task;---bak
master
张龙 6 days ago
parent
commit
2d4a358ba8
  1. 1
      config.yaml
  2. 77
      mycode/AssetsManager.py
  3. 35
      mycode/DBManager.py
  4. 139
      mycode/PollingManager.py
  5. 112
      mycode/TargetManager.py
  6. 3
      mycode/TaskManager.py
  7. 6
      run.py
  8. 51
      web/API/assets.py
  9. 13
      web/main/static/resources/css/flatpickr.min.css
  10. 49
      web/main/static/resources/scripts/assets_manager.js
  11. 64
      web/main/static/resources/scripts/base.js
  12. 2
      web/main/static/resources/scripts/flatpickr
  13. 1
      web/main/templates/base.html
  14. 496
      web/main/templates/polling_target.html

1
config.yaml

@ -1,6 +1,7 @@
#工作模式
App_Work_type: 0 #0-开发模式,只允许单步模式 1-生产模式 包裹下的逻辑可以正常执行
max_run_task: 3 #允许同时运行的任务数
max_polling_task: 3 #运行同时运行的巡检任务
#线程休眠的时间
sleep_time: 20

77
mycode/AssetsManager.py

@ -1,4 +1,5 @@
from mycode.DBManager import app_DBM
from mycode.TargetManager import g_TM
class AssetsManager:
def __init__(self):
@ -142,11 +143,11 @@ class AssetsManager:
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,)
strsql = "select ID from assets_user where ID_num = %s or uname = %s;"
params = (IDno,user)
data = app_DBM.safe_do_select(strsql, params, 1)
if data:
return False, "证件号码已经存在,请重新修改", []
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)
@ -174,5 +175,75 @@ class AssetsManager:
bsuccess,error = app_DBM.del_owner_db(id)
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()

35
mycode/DBManager.py

@ -867,6 +867,41 @@ LEFT JOIN (
bok, _ = self.safe_do_sql(strsql, params)
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 test(self):
# 建立数据库连接
conn = pymysql.connect(

139
mycode/PollingManager.py

@ -0,0 +1,139 @@
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()

112
mycode/TargetManager.py

@ -51,16 +51,6 @@ class TargetManager:
except ValueError:
continue
# 辅助函数:验证IPv4地址的有效性
def _is_valid_ipv4(self,ip):
parts = ip.split('.')
if len(parts) != 4:
return False
for part in parts:
if not part.isdigit():
return False
return True
#验证目标格式的合法性,并提取域名或IP
def validate_and_extract(self,input_str):
'''
@ -104,33 +94,72 @@ class TargetManager:
pass
# Check if target is a valid URL
# 检查是否为有效的 URL
try:
# 解析 URL
result = urlparse(target)
# Only allow http or https schemes
if not result.scheme:
result = urlparse('http://'+target)
# 确保 URL 具有协议(http 或 https)
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
if not netloc:
return None,None
# Handle IPv6 addresses in URLs (enclosed in brackets)
# 处理 URL 中的 IPv6 地址(用方括号括起来)
if netloc.startswith('[') and netloc.endswith(']'):
ip_str = netloc[1:-1]
try:
ipaddress.IPv6Address(ip_str)
return 'IPv6',ipaddress
return 'IPv6', ip_str
except ValueError:
return None,None
# Handle potential IPv4 addresses
return None, None
# 处理可能的 IPv4 地址
elif self._is_valid_ipv4(netloc):
try:
ipaddress.IPv4Address(netloc)
return 'IPv4',ipaddress
return 'IPv4', netloc
except ValueError:
return None,None
# If not an IP-like string, assume it's a domain name and accept
return 'URL',netloc
return None, None
# 检查 netloc 是否为有效的域名
elif self._is_valid_domain(netloc):
return 'URL', netloc
else:
return None, None
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):
info = {}
@ -203,26 +232,29 @@ 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",
"www.crnn.cn",
"oa.crnn.cn",
"ftp://invalid.com", # 不合规
"http://300.400.500.600" # 不合规
]
# 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 = [
"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")
for case in test_cases:

3
mycode/TaskManager.py

@ -101,6 +101,9 @@ class TaskManager:
result = ",".join(fail_list)
return result
def create_polling_task(self,target):
#创建巡检任务
#开启task任务--正常只应该有web触发调用
def start_task_TM(self,task_id):
task = self.tasks[task_id]

6
run.py

@ -4,6 +4,7 @@ from mycode.TaskManager import g_TaskM
from web import create_app
from hypercorn.asyncio import serve
from hypercorn.config import Config
from mycode.PollingManager import PollingManager
async def run_quart_app():
app = create_app()
@ -30,6 +31,7 @@ if __name__ == '__main__':
g_TaskM.load_tasks()
#启动web项目--hypercorn
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")
bsuccess,error = g_AssetsM.del_owner(id)
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;
});
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----------------------------
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() {
// 构造 CSV 文本
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 nodeNameEl = document.getElementById("vulNodeName");

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

@ -84,9 +84,34 @@ function isValidIP(ip) {
return ipRegex.test(ip);
}
//post提交JSON数据
function postJson(url,data){
//post数据
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
@ -114,3 +139,38 @@ function postFrom(url,data){
//get请求数据
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">
<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/flatpickr.min.css') }}">
<link href="{{ url_for('main.static', filename='css/headers.css') }}" rel="stylesheet">
{% block style_link %}{% endblock %}
<style>

496
web/main/templates/polling_target.html

@ -31,6 +31,10 @@
height: 45px;
overflow: hidden;
}
.offcanvas {
z-index: 1060 !important;
}
{% endblock %}
<!-- 页面内容块 -->
@ -38,7 +42,7 @@
<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-2 mb-2"><button class="btn btn-primary me-3" onclick="importModal()">导入</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>
@ -62,8 +66,8 @@
</select>
</div>
<div class="col-2 text-end">
<button class="btn btn-primary" onclick="fetchData()">查询</button>
<button class="btn btn-primary" onclick="exportOwnerData()">导出</button>
<button class="btn btn-primary" onclick="loadPollingT(1)">查询</button>
<button class="btn btn-primary" onclick="exportPTData()">导出</button>
</div>
</div>
<!-- 表格 -->
@ -95,14 +99,500 @@
</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 -->
<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---------- -->
<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---------- -->
<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 %}
<!-- 页面脚本块 -->
{% 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 %}
Loading…
Cancel
Save