#自动化测试逻辑规则控制 #统一控制规则 和 渗透测试树的维护 import json import re import queue import time import threading import pickle from mycode.AttackMap import AttackTree from mycode.AttackMap import TreeNode from mycode.LLMManager import LLMManager from myutils.ConfigManager import myCongif #单一实例 from myutils.MyLogger_logger import LogHandler from mycode.DBManager import DBManager class ControlCenter: def __init__(self,DBM,TM): self.logger = LogHandler().get_logger("ControlCenter") self.task_id = None self.target = None self.attack_tree = None self.DBM = DBM self.TM = TM #LLM对象 self.LLM = LLMManager(myCongif.get_data("LLM_type")) self.llm_quere = queue.Queue() #提交LLM指令的队列---- self.max_thread_num = 1 # 控制最大并发指令数量 self.llmth_list = [] #llm线程list self.brun = False def __del__(self): self.brun =False self.task_id = None self.target = None self.attack_tree = None self.DBM = None def init_cc_data(self): #一次任务一次数据 pass def get_user_init_info(self): '''开始任务初,获取用户设定的基础信息,初始信息可以分为两块: 1.提交llm的补充信息 和 保留在本地的信息(如工具补充参数等,cookie) 2.用户可以设置全局,和指定节点(端口) 3.补充测试节点 ''' # ?包括是否对目标进行初始化的信息收集 return {"已知信息":"无"} def start_do(self,target,task_id): '''一个新任务的开始''' self.task_id = task_id self.target = target #创建/初始化测试树 if self.attack_tree: self.attack_tree = None #释放 root_node = TreeNode(target,task_id) self.attack_tree = AttackTree(root_node)#创建测试树,同时更新根节点相关内容 #初始化启动提示信息 know_info = self.get_user_init_info() self.LLM.build_initial_prompt(root_node) #提交到待处理队列 self.put_one_llm_work(know_info,root_node,1) # 启动LLM请求提交线程 self.brun = True #启动线程 ineed_create_num = self.max_thread_num - len(self.llmth_list) #正常应该就是0或者是max_num for i in range(ineed_create_num): l_th = threading.Thread(target=self.th_llm_worker) l_th.start() self.llmth_list.append(l_th) def put_one_llm_work(self,str_res,node,llm_type): '''提交任务到llm_quere''' if llm_type == 0: self.logger.debug("llm_type不能设置为0") return node.llm_type = llm_type #目前处理逻辑中一个node应该只会在一个queue中,且只会有一次记录。所以llm_type复用 node.res_quere.append(str_res) #提交到待处理队列 self.llm_quere.put(node) def get_one_llm_work(self,node): '''获取该节点的llm提交数据,会清空type和res_quere''' llm_type = node.llm_type node.llm_type = 0 #回归0 res_list = node.res_quere[:] #浅拷贝,复制第一层 node.res_quere.clear() #清空待处理数据,相当于把原应用关系接触 return llm_type,res_list def restore_one_llm_work(self,node,llm_type,res_list): node.llm_type = llm_type node.res_quere = res_list #llm请求提交线程 def th_llm_worker(self):#LLM没有修改的全局变量,应该可以共用一个client ''' 几个规则--TM的work线程同 1.线程获取一个节点后,其他线程不能再获取这个节点(遇到被执行的节点,直接放弃执行)--- 加了没办法保存中间结果进行测试 2.llm返回的指令,只可能是该节点,或者是子节点的,不符合这条规则的都不处理,避免llm处理混乱。 :return: ''' # 线程的dbm需要一个线程一个 th_DBM = DBManager() th_DBM.connect() while self.brun: try: #节点锁 node = self.llm_quere.get(block=False) self.get_llm_instruction(node,th_DBM) #释放锁 # 暂存状态--测试时使用--限制条件llm工作线程只能未1个 with open("attack_tree", 'wb') as f: pickle.dump(self.attack_tree, f) except queue.Empty: self.logger.debug("llm队列中暂时无新的提交任务!") time.sleep(30) #约束1:一个节点只能同时提交一次,未测试的节点不要重复 def get_llm_instruction(self,node,DBM): user_Prompt = "" ext_Prompt = "" llm_type, res_list = self.get_one_llm_work(node) try: res_str = json.dumps(res_list, ensure_ascii=False) except TypeError as e: self.logger.error(f"{res_list}序列化失败:{e},需要自查程序添加代码!") return # 直接返回 if llm_type == 0: self.logger.error("这里type不应该为0,请自查程序逻辑!") return #2025-3-20增加了llm_type user_Prompt = f''' 当前分支路径:{node.path} 当前节点信息: - 节点名称:{node.name} - 节点状态:{node.status} - 漏洞类型:{node.vul_type} ''' if llm_type == 1: #提交指令执行结果 --- 正常提交 # 构造本次提交的prompt ext_Prompt = f''' 上一步结果:{res_str} 任务:生成下一步渗透测试指令或判断是否完成该节点测试。 ''' elif llm_type ==2: #llm返回的指令存在问题,需要再次请求返回 ext_Prompt = f''' 反馈类型:节点指令格式错误 错误信息:{res_str} 任务:请按格式要求重新生成该节点上一次返回中生成的所有指令。 ''' # ''' # elif llm_type ==4: #未生成节点列表 # ext_Prompt = f''' # 反馈类型:需要继续补充信息 # 缺失信息:{res_str} # 任务: # 1.请生成这些节点的新增节点指令,并生成对应的测试指令; # 2.这些节点的父节点为当前节点,请正确生成这些节点的节点路径; # 3.若节点未能全部新增,必须返回未新增的节点列表 # 4.若有未生成指令的节点,必须返回未生成指令的节点列表。 # ''' elif llm_type ==5: ext_Prompt = f''' 反馈类型:测试指令格式错误 错误信息:{res_str} 任务:请根据格式要求,重新生成该测试指令。 ''' else: self.logger.debug("意外的类型参数") return if not ext_Prompt: self.logger.error("未成功获取本次提交的user_prompt") return #提交LLM user_Prompt = user_Prompt + ext_Prompt node_cmds, commands = self.LLM.get_llm_instruction(user_Prompt,DBM, node) # message要更新 ''' 对于LLM返回的错误处理机制 1.验证节点是否都有测试指令返回 2.LLM的回复开始反复时(有点难判断) ''' # 更新tree bok,new_commands = self.tree_manager(node_cmds, node,commands,DBM) # 分析指令入对应节点 if bok: #节点指令若存在错误,测试指令都不处理,需要LLM重新生成 node_list = self.instr_in_node(new_commands, node) # 插入TM的node_queue中,交TM线程处理---除了LLM在不同的请求返回针对同一节点的测试指令,正常业务不会产生两次进队列 for node in node_list: self.TM.node_queue.put(node) def verify_node_cmds(self,node_cmds,node): ''' 验证节点指令的合规性,持续维护 :param node_cmds: :param node: :return: Flase 存在问题, True 合规 ''' strerror = "" for node_json in node_cmds: if "action" not in node_json: self.logger.error(f"缺少action节点:{node_json}") strerror = {"节点指令错误":f"{node_json}缺少action节点,不符合格式要求!"} break action = node_json["action"] if action == "add_node": if "parent" not in node_json or "status" not in node_json or "nodes" not in node_json: strerror = {"节点指令错误": f"{node_json}不符合格式要求,缺少节点!"} break elif action == "update_status": if "status" not in node_json or "node" not in node_json: strerror = {"节点指令错误": f"{node_json}不符合格式要求,缺少节点!"} break elif action =="no_instruction" or action=="no_create": if "nodes" not in node_json: strerror = {"节点指令错误": f"{node_json}不符合格式要求,缺少节点!"} break else: strerror = {"节点指令错误": f"{node_json}不可识别的action值!"} break if not strerror: return True #提交一个错误反馈任务 self.put_one_llm_work(strerror,node,2) return False 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 #对节点指令进行校验 if not self.verify_node_cmds(node_cmds,node): return False,commands #节点指令存在问题,终止执行 #执行节点操作---先执行add_node,怕返回顺序不一直 residue_node_cmds = [] for node_json in node_cmds: action = node_json["action"] if action == "add_node": # 新增节点 parent_node_name = node_json["parent"] # 新增节点原则上应该都是当前节点增加子节点 if node.name == parent_node_name or parent_node_name.endswith(node.name): status = node_json["status"] node_names = node_json["nodes"].split(',') for node_name in node_names: # 判重---遇到过补充未生成指令的节点时,返回了新增这些节点的指令 bfind = False for node_child in node.children: if node_child.name == node_name: bfind = True break if not bfind: # 添加节点 new_node = TreeNode(node_name, node.task_id, status) node.add_child(new_node) # message的传递待验证 else: self.logger.error(f"添加子节点时,遇到父节点名称不一致的,需要介入!!{node_json}") # 丢弃该节点 else:#其他指令添加到list residue_node_cmds.append(node_json) #执行剩余的节点指令--不分先后 for node_json in residue_node_cmds: action = node_json["action"] if action == "update_status": node_name = node_json["node"] status = node_json["status"] vul_type = "未发现" if node.name == node_name: node.status = status if "vulnerability" in node_json: #{\"name\":\"漏洞名称\",\"risk\":\"风险等级(低危/中危/高危)\",\"info\":\"补充信息(没有可为空)\"}}; vul_type = json.dumps(node_json["vulnerability"],ensure_ascii=False) #json转字符串 try: node.name = node_json["vulnerability"]["name"] node.vul_grade = node_json["vulnerability"]["risk"] node.vul_info = node_json["vulnerability"]["info"] except: self.logger.error("漏洞信息错误") node.vul_type = vul_type else: str_user = f"遇到不是修改本节点状态的,需要介入!!{node_json}" self.logger.error(str_user) self.need_user_know(str_user,node) elif action == "no_instruction": #返回的未生成指令的数据进行校验:1.要有数据;2.节点不是当前节点就是子节点 nodes = [] node_names = node_json["nodes"].split(',') for node_name in node_names: #先判断是否在测试指令中,若在则不提交llm任务,只能接受在一次返回中同一节点有多条测试指令,不允许分次返回 bcommand = False for com in commands: if node_name in com: bcommand = True break if bcommand: #如果存在测试指令,则不把该节点放入补充信息llm任务 continue #验证对应节点是否已经创建---本节点或子节点,其他节点不处理(更狠一点就是本节点都不行) if node_name == node.name: nodes.append(node_name) # str_add = "请生成测试指令" # self.put_one_llm_work(str_add,node,1) else: for child_node in node.children: if child_node.name == node_name: nodes.append(node_name) # str_add = "无" # self.put_one_llm_work(str_add, child_node, 1) break if nodes: #阻塞式,在当前节点提交补充信息,完善节点指令 -- 优势是省token new_commands = self.get_other_instruction(nodes,DBM,node) commands.extend(new_commands) elif action == "no_create": #提交人工确认 nodes = node_json["nodes"] if nodes: str_add = {"未新增的节点": nodes} self.logger.debug(str_add) # 提交一个继续反馈任务--继续后续工作 2025-3-25不自动处理 # self.put_one_llm_work(str_add, node, 4) # self.logger.debug(f"未新增的节点有:{nodes}") else: self.logger.error("****不应该执行到这!程序逻辑存在问题!") return True,commands #阻塞轮询补充指令 def get_other_instruction(self,nodes,DBM,cur_node): res_str = ','.join(nodes) new_commands = [] 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.这些节点的父节点为当前节点,请正确生成这些节点的节点路径; 3.若还有节点未能生成测试指令,必须返回未生成指令的节点列表。 ''' res_str = "" node_cmds, commands = self.LLM.get_llm_instruction(user_Prompt, DBM, cur_node) # message要更新 #把返回的测试指令进行追加 new_commands.extend(commands) #判断是否还有未添加指令的节点 for node_json in node_cmds: #正常应该只有一条no_instruction if "no_instruction" in node_json and "nodes" in node_json: tmp_nodes = [] node_names = node_json["nodes"].split(',') for node_name in node_names: if node_name in nodes: tmp_nodes.append(node_name) res_str = ','.join(tmp_nodes) break self.logger.debug("为添加指令的节点,都已完成指令的添加!") return new_commands def instr_in_node(self,commands,node): node_list = [] #一次返回的测试指令 for command in commands: # 使用正则匹配方括号中的node_path(非贪婪模式) match = re.search(r'\[(.*?)\]', command) if match: node_path = match.group(1) #'''强制约束,不是本节点或者是子节点的指令不处理''' find_node = self.attack_tree.find_node_by_nodepath_parent(node_path,node) if find_node: instruction = re.sub(r'\[.*?\]', "", command,count=1,flags=re.DOTALL) find_node.instr_queue.append(instruction) #入输出队列 if find_node not in node_list: node_list.append(find_node) else: self.logger.error(f"基于节点路径没有找到对应的节点{node_path},父节点都没找到!")#丢弃该指令 else: self.logger.error(f"得到的指令格式不符合规范:{command}")#丢弃该指令--- #这里对于丢弃指令,有几种方案: # 1.直接丢弃不处理,但需要考虑会不会产生节点缺失指令的问题,需要有机制验证节点;------ 需要有个独立线程,节点要加锁--首选待改进方案 # 2.入当前节点的res_queue,但有可能当前节点没有其他指令,不会触发提交,另外就算提交,会不会产生预设范围外的返回,不确定; # 3.独立队列处理 return node_list #需要用户确认的信息--待完善 def need_user_know(self,strinfo,node): pass #待修改 def is_user_instr(self,instr): ''' 过滤需要人工确认或手动执行的指令 ---- 待完善 :param instr: :return: ''' #if instr.startswith("curl") or instr.startswith("http") or instr.startswith("wget"): if instr.startswith("http") or instr.startswith("wget") or instr.startswith("ssh"): return True #指令入队列,待修改 def instr_in_quere(self,instr_list): ''' 对于运行需要较长时间的不强求同一批次返回给LLM :param instr_list: :return: ''' for instr in instr_list: if self.is_user_instr(instr): self.user_instr.put(instr) print(f"需要人工确认的指令{instr}") else: matched =False for prefix in self.long_time_instr: if instr.startswith(prefix): matched =True if not matched: with self.lock: self.batch_num += 1 #非耗时指令+1 print(f"&&&&&&当前batch_num:{self.batch_num}") else: with self.lock: self.long_instr_num +=1 #耗时指令数量+1 # 指令入队列 self.instr_queue.append(instr) def stop_do(self): #清空数据 self.task_id = None self.target = None self.attack_tree = None #停止llm处理线程 self.brun =False for th in self.llmth_list: th.jion()