155 lines
7.4 KiB
Python
155 lines
7.4 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
浏览器端破坏性操作真实 UI 审计脚本
|
|
以 Admin 登录共享试用环境 (192.168.139.230:8082),逐页审查 15 个实体实际操作按钮
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import time
|
|
import subprocess
|
|
import json
|
|
from playwright.sync_api import sync_playwright
|
|
|
|
BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))
|
|
SCREENSHOT_DIR = os.path.join(BASE_DIR, "docs/reports/destructive-action-alignment/screenshots")
|
|
TARGET_URL = "http://192.168.139.230:8082"
|
|
|
|
def get_captcha_code(uuid):
|
|
cmd = ["redis-cli", "-h", "192.168.139.230", "-p", "6379", "-a", "sict123456", "get", f"captcha_codes:{uuid}"]
|
|
res = subprocess.check_output(cmd).decode("utf-8").strip()
|
|
# 过滤 warning 提示
|
|
lines = [l for l in res.split("\n") if "Warning:" not in l]
|
|
return lines[-1].strip().strip('"') if lines else ""
|
|
|
|
def audit_pages():
|
|
results = []
|
|
|
|
with sync_playwright() as p:
|
|
browser = p.chromium.launch(headless=True)
|
|
context = browser.new_context(viewport={"width": 1440, "height": 900})
|
|
page = context.new_page()
|
|
|
|
last_uuid = [None]
|
|
def handle_response(response):
|
|
if "captchaImage" in response.url:
|
|
try:
|
|
data = response.json()
|
|
last_uuid[0] = data.get("uuid")
|
|
except Exception:
|
|
pass
|
|
page.on("response", handle_response)
|
|
|
|
print(f"1. 导航至登录页面 {TARGET_URL}...")
|
|
page.goto(f"{TARGET_URL}/login", wait_until="networkidle")
|
|
time.sleep(1)
|
|
|
|
uuid = last_uuid[0]
|
|
if not uuid:
|
|
print("未能获取验证码 UUID")
|
|
return
|
|
|
|
code = get_captcha_code(uuid)
|
|
print(f"获取验证码成功: {code} (uuid: {uuid})")
|
|
|
|
page.fill('input[placeholder="账号"]', "admin")
|
|
page.fill('input[placeholder="密码"]', "admin123")
|
|
page.fill('input[placeholder="验证码"]', code)
|
|
page.click('button:has-text("登 录")')
|
|
|
|
page.wait_for_url("**/index", timeout=10000)
|
|
print("Admin 登录成功,进入首页仪表盘")
|
|
page.screenshot(path=os.path.join(SCREENSHOT_DIR, "00_admin_dashboard.png"))
|
|
|
|
# 巡检路由列表
|
|
pages_to_audit = [
|
|
{"entity": "教师 (Teacher)", "url": f"{TARGET_URL}/person/teacher", "shot": "01_teacher.png"},
|
|
{"entity": "学员 (Student)", "url": f"{TARGET_URL}/person/student", "shot": "02_student.png"},
|
|
{"entity": "课程 (Course)", "url": f"{TARGET_URL}/coursemgr/course", "shot": "03_course.png"},
|
|
{"entity": "教室/场地 (Classroom)", "url": f"{TARGET_URL}/equip/classroom", "shot": "04_classroom.png"},
|
|
{"entity": "宿舍 (Dormitory)", "url": f"{TARGET_URL}/equip/dormitory", "shot": "05_dormitory.png"},
|
|
{"entity": "培训计划 (Plan)", "url": f"{TARGET_URL}/coursemgr/plan", "shot": "06_plan.png"},
|
|
{"entity": "报到 (Report)", "url": f"{TARGET_URL}/analysis/checkin", "shot": "10_report.png"},
|
|
{"entity": "考勤 (Attendance)", "url": f"{TARGET_URL}/analysis/course", "shot": "11_attendance.png"},
|
|
{"entity": "自主学习 (Duration)", "url": f"{TARGET_URL}/analysis/duration", "shot": "12_duration.png"},
|
|
{"entity": "住宿 (Accommodation)", "url": f"{TARGET_URL}/train/accommodation", "shot": "13_accommodation.png"},
|
|
{"entity": "门禁授权 (Auth)", "url": f"{TARGET_URL}/train/auth", "shot": "14_door_auth.png"},
|
|
{"entity": "成绩 (Score)", "url": f"{TARGET_URL}/train/score", "shot": "15_score.png"}
|
|
]
|
|
|
|
for item in pages_to_audit:
|
|
print(f"正在巡检: {item['entity']} -> {item['url']}...")
|
|
try:
|
|
page.goto(item["url"], wait_until="networkidle")
|
|
time.sleep(1.5)
|
|
# 抓取表格操作列中的按钮文本
|
|
buttons = page.locator(".el-table__row .el-button").all_text_contents()
|
|
# 去重并清洗文本
|
|
btn_set = list(dict.fromkeys([b.strip() for b in buttons if b.strip()]))
|
|
print(f" 实际可见操作按钮: {btn_set}")
|
|
|
|
shot_path = os.path.join(SCREENSHOT_DIR, item["shot"])
|
|
page.screenshot(path=shot_path)
|
|
|
|
results.append({
|
|
"entity": item["entity"],
|
|
"url": item["url"],
|
|
"visible_buttons": btn_set,
|
|
"screenshot": item["shot"]
|
|
})
|
|
except Exception as e:
|
|
print(f" 访问异常: {e}")
|
|
results.append({
|
|
"entity": item["entity"],
|
|
"url": item["url"],
|
|
"error": str(e)
|
|
})
|
|
|
|
# 针对培训计划内的弹窗操作(计划课程、课次日程、参训人员)专门巡检
|
|
print("正在巡检培训计划内嵌子组件: 计划课程、课次、参训人员...")
|
|
try:
|
|
page.goto(f"{TARGET_URL}/coursemgr/plan", wait_until="networkidle")
|
|
time.sleep(1.5)
|
|
# 点击第一行的“课程配置”
|
|
course_cfg_btn = page.locator('button:has-text("课程配置")').first
|
|
if course_cfg_btn.is_visible():
|
|
course_cfg_btn.click()
|
|
time.sleep(1)
|
|
dialog_btns = page.locator('.el-dialog:visible .el-table__row .el-button').all_text_contents()
|
|
print(f" [计划课程 Plan Course] 弹窗内可见操作按钮: {list(dict.fromkeys([b.strip() for b in dialog_btns if b.strip()]))}")
|
|
page.screenshot(path=os.path.join(SCREENSHOT_DIR, "07_plan_course_config.png"))
|
|
# 关闭弹窗
|
|
page.locator('.el-dialog:visible button:has-text("关 闭")').click()
|
|
time.sleep(0.5)
|
|
|
|
# 点击“计划详情”
|
|
detail_btn = page.locator('button:has-text("计划详情")').first
|
|
if detail_btn.is_visible():
|
|
detail_btn.click()
|
|
time.sleep(1)
|
|
# 切换到“参训人员”
|
|
page.locator('.el-dialog:visible .el-tabs__item:has-text("参训人员")').click()
|
|
time.sleep(1)
|
|
enroll_btns = page.locator('.el-dialog:visible .el-table__row .el-button').all_text_contents()
|
|
print(f" [参训人员 Enrollment] 面板可见操作按钮: {list(dict.fromkeys([b.strip() for b in enroll_btns if b.strip()]))}")
|
|
page.screenshot(path=os.path.join(SCREENSHOT_DIR, "08_plan_enrollment_panel.png"))
|
|
|
|
# 切换到“课程表”
|
|
page.locator('.el-dialog:visible .el-tabs__item:has-text("课程表")').click()
|
|
time.sleep(1)
|
|
tt_btns = page.locator('.el-dialog:visible .el-table__row .el-button').all_text_contents()
|
|
print(f" [课次日程 Timetable] 面板可见操作按钮: {list(dict.fromkeys([b.strip() for b in tt_btns if b.strip()]))}")
|
|
page.screenshot(path=os.path.join(SCREENSHOT_DIR, "09_plan_timetable_panel.png"))
|
|
except Exception as e:
|
|
print(f" 计划子组件巡检异常: {e}")
|
|
|
|
browser.close()
|
|
|
|
print("\n--- 浏览器巡检汇总完成 ---")
|
|
with open(os.path.join(BASE_DIR, "docs/reports/destructive-action-alignment/browser_audit_result.json"), "w", encoding="utf-8") as f:
|
|
json.dump(results, f, ensure_ascii=False, indent=2)
|
|
|
|
if __name__ == "__main__":
|
|
audit_pages()
|