使用wxbot对接华三防火墙开通vpn账号和开通JumpServer堡垒机账号,让运维如此轻松
效果
管理员激活
申请vpn和堡垒机账号指令 /申请vpn+公司邮箱地址+访问使用说明
/申请vpn 362@qq.com test
激活指令 /激活vpn+任务id+权限id
/激活vpn e432abeb-e5b5-47f9-a4c7-a7e86b79f5f4 b6c715c5-d997-4017-a265-56d37e4a3acf
密码通过邮件发送
上代码
请配置好防火墙管理员账号和密码 、JumpServer管理员账号的ak sk
# -*- coding:utf-8 -*-
import http.client
import ssl # 导入证书模块
import json
import sys
import uuid
import datetime
import websocket
import time
import random
import json
import requests
import paramiko
import os
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.header import Header
import re
import uuid
import mysql.connector
import string
import logging
from httpsig.requests_auth import HTTPSignatureAuth
from bs4 import BeautifulSoup
from mysql.connector import pooling
from datetime import datetime
from httpsig.requests_auth import HTTPSignatureAuth
from datetime import datetime
from email.utils import formatdate
# 设置日志记录到文件和控制台
logging.basicConfig(level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler("app.log"),
logging.StreamHandler(sys.stdout) # 输出到控制台
])
#################################申请vpn和堡垒机 和激活功能
def send_email(to_email, vpn_account, vpn_password):
smtp_server = 'smtp.exmail.qq.com'
smtp_port = 465
from_email = 'f******_admin@fzda********ke.com'
from_password = '***************************oq' # 请确保在生产环境中妥善保管密码
# 构建邮件内容
subject = "root - Your VPN Account Details"
body = f"Dear user,\n\n您的VPN和堡垒机帐户 '{vpn_account}' has been created successfully. Your password is: {vpn_password}\n\n请及时修改密码。\n\n致以最诚挚的问候,\nroot-交付运维中心\n【腾讯文档】互联网区域堡垒机使用方法v20210713https://docs.qq.com/doc/DWm*************ek1W"
# 设置邮件的 MIME 类型和内容
message = MIMEMultipart()
message['From'] = Header("root-IT Support", 'utf-8')
message['To'] = Header(to_email, 'utf-8')
message['Subject'] = Header(subject, 'utf-8')
message.attach(MIMEText(body, 'plain', 'utf-8'))
try:
# 使用 SSL 连接到 SMTP 服务器
with smtplib.SMTP_SSL(smtp_server, smtp_port) as server:
server.login(from_email, from_password)
server.sendmail(from_email, to_email, message.as_string())
logging.info(f"Email sent successfully to {to_email}")
except smtplib.SMTPException as e:
logging.error(f"Failed to send email to {to_email}: {str(e)}")
def generate_strong_password(length=12):
# 确保密码包含至少一个大写字母、小写字母、数字和特殊字符
password_characters = string.ascii_letters + string.digits + string.punctuation
# 随机选择一个大写字母、小写字母、数字和特殊字符
password = [
random.choice(string.ascii_uppercase),
random.choice(string.ascii_lowercase),
random.choice(string.digits),
random.choice(string.punctuation),
]
# 填充剩余的密码长度
password += [random.choice(password_characters) for _ in range(length - 4)]
# 打乱密码字符的顺序
random.shuffle(password)
return ''.join(password)
def apply_vpn_account(applicant_wxid, email, purpose):
vpn_account = email.split('@')[0]
vpn_password = generate_strong_password()
application_uuid = str(uuid.uuid4())
logging.info(f"Info: apply_vpn_account {vpn_account} with vpn_password {vpn_password} application_uuid {application_uuid}")
# 连接MySQL数据库并保存申请记录
connection = mysql.connector.connect(
host="172.2.2.50",
user="root",
password="root",
database="root"
)
cursor = connection.cursor()
# 检查邮箱是否已经存在
check_query = "SELECT COUNT(*) FROM fa_vpn_fw_blj WHERE email = %s"
cursor.execute(check_query, (email,))
(count,) = cursor.fetchone()
if count > 0:
logging.warning(f"Warning: Email {email} 申请过.")
cursor.close()
connection.close()
return None, None, None
# 插入新记录
query = ("INSERT INTO fa_vpn_fw_blj (uuid, applicant_wxid, vpn_account, vpn_password, email, purpose, status) "
"VALUES (%s, %s, %s, %s, %s, %s, %s)")
cursor.execute(query, (application_uuid, applicant_wxid, vpn_account, vpn_password, email, purpose, '待激活'))
connection.commit()
cursor.close()
connection.close()
return application_uuid, vpn_account, vpn_password
def get_auth(KeyID, SecretID):
signature_headers = ['(request-target)', 'accept', 'date']
auth = HTTPSignatureAuth(key_id=KeyID, secret=SecretID, algorithm='hmac-sha256', headers=signature_headers)
return auth
def get_users_groups():
try:
# 调用堡垒机API接口查询权限组
url = "https://172.20.40.27/api/v1/users/groups/"
# 获取当前的日期时间
date_header = formatdate(timeval=None, localtime=False, usegmt=True)
headers = {
"Content-Type": "application/json",
"Authorization": "Bearer your_token",
"date": date_header
}
auth = get_auth("361e490f-***************-99482dc6113f", "029b1e59-aa10-*************-131a3b390ef5")
response = requests.get(url, headers=headers, auth=auth, verify=False)
logging.info(f"Response status code: {response.status_code}")
if response.status_code == 200:
groups = response.json()
logging.info(f"查询堡垒机权限组成功: {groups}")
return groups
else:
logging.error(f"查询堡垒机权限组失败: {response.text}")
return None
except Exception as e:
logging.error(f"Failed to query user groups from the bastion host: {str(e)}")
raise
def combine_id_name_to_string(groups):
# 将每个 id 和 name 组合成一个小字符串
id_name_strings = [f"ID: {group['id']}, Name: {group['name']}" for group in groups]
# 将所有小字符串组合成一个长字符串,使用换行符分隔
combined_string = "\n".join(id_name_strings)
return combined_string
def activate_vpn_account(application_uuid, users_groupsID , activator_wxid):
try:
# 连接MySQL数据库并获取申请记录
connection = mysql.connector.connect(
host="172.2.2.50",
user="root",
password="root",
database="root"
)
cursor = connection.cursor()
query = ("SELECT vpn_account, vpn_password, email FROM fa_vpn_fw_blj WHERE uuid=%s AND status='待激活'")
cursor.execute(query, (application_uuid,))
result = cursor.fetchone()
if result:
vpn_account, vpn_password, email = result
logging.info(f"Activating VPN account for {vpn_account} with email {email} by {activator_wxid}")
try:
# 连接防火墙,执行VPN账号添加功能
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect("172.50.0.1", port=22, username="*****************", password="****************")
cmd = f"""
sys
user-group psy_system_auto_add
identity-member user {vpn_account}
exit
local-user {vpn_account} class network
password simple {vpn_password}
access-limit 1
service-type sslvpn
group psy_system_auto_add
authorization-attribute user-role network-operator
authorization-attribute sslvpn-policy-group SSLVPNZIYUA
identity-group psy_system_auto_add
description {vpn_account}-{email}
exit
save
y
"""
stdin, stdout, stderr = ssh.exec_command(cmd)
stdout.channel.recv_exit_status() # 确保命令执行完成
ssh.close()
logging.info(f"VPN account {vpn_account} added to the firewall successfully.")
except Exception as e:
logging.error(f"Failed to add VPN account to the firewall: {str(e)}")
return(f"Failed to add VPN account to the firewall: {str(e)}")
raise # 重新抛出异常,以便进一步处理
logging.info(f"堡垒机账号 {vpn_account} 密码{vpn_password}。")
try:
# 调用堡垒机API接口添加账号
url = "https://172.20.40.27/api/v1/users/users/"
# 获取当前的日期时间
date_header = formatdate(timeval=None, localtime=False, usegmt=True)
headers = {
"Content-Type": "application/json",
"Authorization": "Bearer your_token",
"date": date_header
}
data = {
"id": application_uuid,
"name": vpn_account,
"username": vpn_account,
"password": vpn_password,
"email": email,
"role": "User",
"groups": [f"{users_groupsID}"],
"password_strategy": "custom"
}
auth = get_auth("361e490f-************-99482dc6113f", "029b1e59-aa10-********************1a3b390ef5")
response = requests.post(url, headers=headers, json=data, auth=auth ,verify=False)
if response.status_code == 201:
# 更新数据库状态为已激活,记录激活操作人wxid和激活时间
activation_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
update_query = """
UPDATE fa_vpn_fw_blj
SET status=%s, activator_wxid=%s, activation_time=%s
WHERE uuid=%s
"""
cursor.execute(update_query, ('已激活', activator_wxid, activation_time, application_uuid))
connection.commit()
logging.info(f"堡垒机账号 {vpn_account} 添加成功,并已激活。")
send_email(email, vpn_account, vpn_password)
return(f"堡垒机账号 {vpn_account} 添加成功,并已激活。账号和密码信息请查收由fzdatalake_admin@fzdatalake.com发送的邮件信息")
else:
logging.error(f"堡垒机账号添加失败: {response.text}")
return(f"堡垒机账号添加失败: {response.text}")
except Exception as e:
logging.error(f"Failed to add account to the bastion host: {str(e)}")
raise # 重新抛出异常,以便进一步处理
else:
logging.warning(f"没有待定的VPN激活 for UUID: {application_uuid}")
return(f"没有待定的VPN激活/或已经激活过 for UUID: {application_uuid}")
except mysql.connector.Error as err:
logging.error(f"Database error: {str(err)}")
except Exception as e:
logging.error(f"An error occurred: {str(e)}")
finally:
if cursor:
cursor.close()
if connection:
connection.close()
#################################申请vpn和堡垒机 和激活功能
def output(msg):
now = time.strftime("%Y-%m-%d %X")
logging.info(f'[{now}]: {msg} - {script_name}')
# 获取当前脚本的名称
script_name = os.path.basename(__file__)
output(f"当前脚本的名称是: {script_name}")
ip = '172.2.2.50'
port = 5555
SERVER = f'ws://{ip}:{port}'
HEART_BEAT = 5005
RECV_TXT_MSG = 1
RECV_TXT_CITE_MSG = 49
RECV_PIC_MSG = 3
USER_LIST = 5000
GET_USER_LIST_SUCCSESS = 5001
GET_USER_LIST_FAIL = 5002
TXT_MSG = 555
PIC_MSG = 500
AT_MSG = 550
CHATROOM_MEMBER = 5010
CHATROOM_MEMBER_NICK = 5020
PERSONAL_INFO = 6500
DEBUG_SWITCH = 6000
PERSONAL_DETAIL = 6550
DESTROY_ALL = 9999
JOIN_ROOM = 10000
########################################################################################
def match_keyword(keywords, text):
for keyword in keywords.keys():
if keyword in text:
output(keyword)
return keyword
return None
def getid():
return time.strftime("%Y%m%d%H%M%S")
################################### HTTP ################################################
def send(uri, data):
base_data = {
'id': getid(),
'type': 'null',
'roomid': 'null',
'wxid': 'null',
'content': 'null',
'nickname': 'null',
'ext': 'null',
}
base_data.update(data)
url = f'http://{ip}:{port}/{uri}'
try:
res = requests.post(url, json={'para': base_data}, timeout=5)
res.raise_for_status()
return res.json()
except requests.exceptions.RequestException as e:
output(f"HTTP Request failed: {e}")
return None
def get_member_nick(roomid, wxid):
uri = 'api/getmembernick'
data = {
'type': CHATROOM_MEMBER_NICK,
'wxid': wxid,
'roomid': roomid or 'null'
}
respJson = send(uri, data)
if respJson:
return json.loads(respJson['content'])['nick']
return None
def get_personal_info():
uri = '/api/get_personal_info'
data = {
'id': getid(),
'type': PERSONAL_INFO,
'content': 'op:personal info',
'wxid': 'null',
}
respJson = send(uri, data)
output(respJson)
################################### websocket ################################################
def get_chat_nick_p(roomid):
qs = {
'id': getid(),
'type': CHATROOM_MEMBER_NICK,
'content': roomid,
'wxid': 'ROOT',
}
return json.dumps(qs)
def debug_switch():
qs = {
'id': getid(),
'type': DEBUG_SWITCH,
'content': 'off',
'wxid': 'ROOT',
}
return json.dumps(qs)
def handle_nick(j):
data = j.content
for d in data:
output(f'nickname: {d.nickname}')
def hanle_memberlist(j):
data = j.content
for d in data:
output(f'roomid: {d.roomid}')
def get_chatroom_memberlist():
qs = {
'id': getid(),
'type': CHATROOM_MEMBER,
'wxid': 'null',
'content': 'op:list member',
}
return json.dumps(qs)
def get_personal_detail(wxid):
qs = {
'id': getid(),
'type': PERSONAL_DETAIL,
'content': 'op:personal detail',
'wxid': wxid,
}
return json.dumps(qs)
def send_wxuser_list():
qs = {
'id': getid(),
'type': USER_LIST,
'content': 'user list',
'wxid': 'null',
}
return json.dumps(qs)
def handle_wxuser_list(j):
output('启动完成')
###################################################################################
def heartbeat(msgJson):
output(msgJson['content'])
def on_open(ws):
ws.send(send_wxuser_list())
def on_error(ws, error):
output(f'on_error: {error}')
def on_close(ws):
output("closed")
###################################################################################
def destroy_all():
qs = {
'id': getid(),
'type': DESTROY_ALL,
'content': 'none',
'wxid': 'node',
}
return json.dumps(qs)
def send_msg(msg, wxid='null', roomid=None, nickname='null'):
if msg.endswith('png'):
msg_type = PIC_MSG
if roomid:
wxid = roomid
roomid = None
nickname = 'null'
elif roomid:
msg_type = AT_MSG
else:
msg_type = TXT_MSG
if roomid is None:
roomid = 'null'
qs = {
'id': getid(),
'type': msg_type,
'roomid': roomid,
'wxid': wxid,
'content': msg,
'nickname': nickname,
'ext': 'null'
}
output(f'发送消息: {qs}')
return json.dumps(qs)
###################################################################################
def welcome_join(msgJson):
output(f'收到消息: {msgJson}')
if '邀请' in msgJson['content']['content']:
roomid = msgJson['content']['id1']
nickname = msgJson['content']['content'].split('"')[-2]
#ws.send(send_msg(f'欢迎新进群', roomid=roomid, wxid='null', nickname=nickname))
def handleMsg_cite(msgJson):
msgXml = msgJson['content']['content'].replace('&', '&').replace('<', '<').replace('>', '>')
soup = BeautifulSoup(msgXml, 'lxml')
msgJson = {
'content': soup.select_one('title').text,
'id': msgJson['id'],
'id1': msgJson['content']['id2'],
'id2': 'wxid_fys2fico9put22',
'id3': '',
'srvid': msgJson['srvid'],
'time': msgJson['time'],
'type': msgJson['type'],
'wxid': msgJson['content']['id1']
}
handle_recv_msg(msgJson)
def get_type_ollama(operation_content):
url = "http://192.168.250.249:11434/api/generate"
payload = json.dumps({
"model": "qwen2:7b",
"stream": False,
"system": "你是一个运维事件记录员,你的名字叫小湖,你只要识别出事件的类型。服务器、应用程序、交换机、防火墙策略、其他这5种类型,选择其中一个简短明了回答是什么类型",
"prompt": f"{operation_content} ,这个操作事件内容是‘服务器、应用程序、交换机、防火墙策略、其他’中是哪种操作?直接回答类型"
})
headers = {
'Content-Type': 'application/json'
}
retries = 3
for attempt in range(retries):
try:
response = requests.post(url, headers=headers, data=payload)
response.raise_for_status()
result = response.json()
output(f"识别的类型: {result['response']}")
return result['response']
except requests.exceptions.RequestException as e:
output(f"AI请求失败 (尝试 {attempt + 1}/{retries}): {e}")
time.sleep(2)
output("AI服务不可用,使用默认类型")
return "其他" # 默认类型或错误处理
def handle_recv_msg(msgJson):
logging.info(f'Received message: {msgJson}')
# 获取消息内容
keyword = msgJson['content'].replace('\u2005', '').strip()
# 匹配申请VPN指令
apply_vpn_match = re.match(r'/申请vpn\s+(\S+)\s+(.+)', keyword)
# 匹配激活VPN指令
activate_vpn_match = re.match(r'/激活vpn\s+(\S+)\s+(\S+)', keyword)
# 获取房间ID和发送者ID
roomid = msgJson['wxid'] if '@chatroom' in msgJson['wxid'] else None
senderid = msgJson['id1'] if roomid else msgJson['wxid']
# 获取发送者昵称
nickname = get_member_nick(roomid, senderid) if roomid else 'null'
applicant = nickname
if apply_vpn_match:
email = apply_vpn_match.group(1)
purpose = apply_vpn_match.group(2)
logging.info(f"申请VPN: Email={email}, Purpose={purpose}")
uuid, vpn_account, vpn_password = apply_vpn_account(senderid, email, purpose)
if uuid is None:
logging.warning(f"Warning: Email {email} 申请过.")
ws.send(send_msg(f"Warning: Email {email} 该邮箱已经申请过", roomid=roomid, wxid=senderid, nickname=nickname))
else:
# 处理正常情况
ws.send(send_msg(f"申请成功: VPN 账号 {vpn_account}-激活任务{uuid}", roomid=roomid, wxid=senderid, nickname=nickname))
groups = get_users_groups()
if groups:
combined_string = combine_id_name_to_string(groups)
logging.info(f"Combined Group String: {combined_string}")
ws.send(send_msg(f"\n|权限组明细,请管理员分配|\n\n|管理员激活指令 /激活vpn 任务id 权限id|\n{combined_string}", roomid=roomid, wxid=senderid, nickname=nickname))
elif activate_vpn_match:
if msgJson['id1']=="p****yuan" or msgJson['id1']=="wxid_y*****omg222" or msgJson['id1']=="ho******05232": ##这里设置管理员的wxid,只有有权限的wxid才可以做激活操作
uuid = activate_vpn_match.group(1)
group_id = activate_vpn_match.group(2)
logging.info(f"激活VPN: UUID={uuid}, Group ID={group_id}")
record_msg = activate_vpn_account(uuid, group_id,senderid)
ws.send(send_msg(record_msg, roomid=roomid, wxid=senderid, nickname=nickname))
else:
ws.send(send_msg("无权限", roomid=roomid, wxid=senderid, nickname=nickname))
else:
logging.warning("未识别的指令")
###################################################################################
def on_message(ws, message):
j = json.loads(message)
resp_type = j['type']
if resp_type == CHATROOM_MEMBER_NICK:
handle_nick(j)
elif resp_type == PERSONAL_DETAIL:
handle_recv_msg(j)
elif resp_type == AT_MSG:
handle_recv_msg(j)
elif resp_type == DEBUG_SWITCH:
handle_recv_msg(j)
elif resp_type == PERSONAL_INFO:
handle_recv_msg(j)
elif resp_type == TXT_MSG:
handle_recv_msg(j)
elif resp_type == PIC_MSG:
handle_recv_msg(j)
elif resp_type == CHATROOM_MEMBER:
hanle_memberlist(j)
elif resp_type == RECV_PIC_MSG:
handle_recv_msg(j)
elif resp_type == RECV_TXT_MSG:
handle_recv_msg(j)
elif resp_type == RECV_TXT_CITE_MSG:
handleMsg_cite(j)
elif resp_type == HEART_BEAT:
heartbeat(j)
elif resp_type == USER_LIST:
handle_wxuser_list(j)
elif resp_type == GET_USER_LIST_SUCCSESS:
handle_wxuser_list(j)
elif resp_type == GET_USER_LIST_FAIL:
handle_wxuser_list(j)
elif resp_type == JOIN_ROOM:
welcome_join(j)
else:
output(f"Unknown message type: {resp_type}")
while True:
try:
ws = websocket.WebSocketApp(SERVER, on_open=on_open, on_message=on_message, on_error=on_error, on_close=on_close)
ws.run_forever()
except Exception as e:
output(f"WebSocket connection failed: {e}. Reconnecting in 5 seconds...")
time.sleep(5)
wxbot 如果搭建请github找工具
hook监听到的内容
Received message: {'content': '***', 'id': '20240826191727', 'id1': 'to***598', 'id2': '', 'id3': '', 'srvid': 1, 'time': '2024-08-26 19:17:27', 'type': 1, 'wxid': '715****56@chatroom'}
数据库字段表结构
# Host: localhost (Version: 5.7.26)
# Date: 2024-08-26 19:35:07
# Generator: MySQL-Front 5.3 (Build 4.234)
/*!40101 SET NAMES utf8 */;
#
# Structure for table "fa_vpn_fw_blj"
#
DROP TABLE IF EXISTS `fa_vpn_fw_blj`;
CREATE TABLE `fa_vpn_fw_blj` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`uuid` varchar(255) NOT NULL,
`applicant_wxid` varchar(255) NOT NULL,
`vpn_account` varchar(255) NOT NULL,
`vpn_password` varchar(255) NOT NULL,
`email` varchar(255) NOT NULL,
`purpose` varchar(255) NOT NULL,
`application_time` datetime DEFAULT CURRENT_TIMESTAMP,
`status` varchar(50) DEFAULT '待激活',
`activator_wxid` varchar(255) DEFAULT NULL,
`activation_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
结合fastadmin 管理后台