测试审计:补充破坏性安全测试用例与管理员生命周期审计报告
This commit is contained in:
1 parent
3f0cbe3b16
commit
8ec924012e
6 files changed
+778
No files matched your search
@@ -0,0 +1,220 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
15 个业务实体的生命周期与破坏性操作 (Destructive Actions) 深度审计脚本
|
||||
检查前后端权限一致性、按钮具体对象名称文案、依赖拦截机制及生命周期闭环
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))
|
||||
BUILD_UI_DIR = os.path.join(BASE_DIR, "build_ui/src")
|
||||
BACKEND_DIR = os.path.join(BASE_DIR, "RuoYi-Vue")
|
||||
|
||||
ENTITIES = [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "教师 (Teacher)",
|
||||
"vue": "views/train/teacher/index.vue",
|
||||
"controller": "ruoyi-admin/src/main/java/com/ruoyi/web/controller/train/TrainTeacherController.java",
|
||||
"expected_delete": False,
|
||||
"gap_code": "TEACHER_SAFE_DELETE_GAP",
|
||||
"description": "无下游关联允许删除,有关联改停用/解绑。当前后端未暴露安全删除接口,缺少授课日程等联合依赖校验。"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "学员 (Student)",
|
||||
"vue": "views/train/student/index.vue",
|
||||
"controller": "ruoyi-admin/src/main/java/com/ruoyi/web/controller/train/TrainStudentController.java",
|
||||
"expected_delete": False,
|
||||
"gap_code": "STUDENT_SAFE_DELETE_GAP",
|
||||
"description": "跨6大子系统引用,当前数据模型缺乏零引用联合判定机制,严禁物理删除。"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "课程 (Course)",
|
||||
"vue": "views/train/course/index.vue",
|
||||
"controller": "ruoyi-admin/src/main/java/com/ruoyi/web/controller/train/TrainCourseController.java",
|
||||
"expected_delete": True,
|
||||
"gap_code": "ALIGNED",
|
||||
"description": "未被引用 -> 删除;被引用 -> 停用。本轮已补齐安全删除端点、引用校验拦截以及前端停用/启用/删除入口与对象名称提示。"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "教室/场地 (Classroom)",
|
||||
"vue": "views/train/classroom/index.vue",
|
||||
"controller": "ruoyi-admin/src/main/java/com/ruoyi/web/controller/train/TrainClassroomInfoController.java",
|
||||
"expected_delete": True,
|
||||
"gap_code": "ALIGNED",
|
||||
"description": "未被使用 -> 删除;已使用 -> 状态调整。本轮已补齐安全删除端点、排课课次校验拦截以及前端删除入口与对象名称提示。"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "宿舍 (Dormitory)",
|
||||
"vue": "views/train/dormitory/index.vue",
|
||||
"controller": "ruoyi-admin/src/main/java/com/ruoyi/web/controller/train/TrainDormitoryInfoController.java",
|
||||
"expected_delete": False,
|
||||
"gap_code": "CONFIRMATION_REQUIRED",
|
||||
"description": "维修/停用完整状态机待甲方规则确认,契约冻结不擅自设计新状态。"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"name": "培训计划 (Training Plan)",
|
||||
"vue": "views/train/plan/index.vue",
|
||||
"controller": "ruoyi-admin/src/main/java/com/ruoyi/web/controller/train/TrainPlanController.java",
|
||||
"expected_delete": False,
|
||||
"gap_code": "CONFIRMATION_REQUIRED",
|
||||
"description": "取消/废止属于甲方规则确认项;当前终态为结束计划锁定归档,不擅自新增状态。"
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"name": "计划课程 (Plan Course)",
|
||||
"vue": "views/train/plan/index.vue",
|
||||
"controller": "ruoyi-admin/src/main/java/com/ruoyi/web/controller/train/TrainPlanCourseController.java",
|
||||
"expected_delete": True,
|
||||
"gap_code": "ALIGNED",
|
||||
"description": "允许移除时显示移除,具备计划锁定、课次、成绩、自主学时四重拦截保护。"
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"name": "课次 (Lesson)",
|
||||
"vue": "views/train/plan/components/TimetablePanel.vue",
|
||||
"controller": "ruoyi-admin/src/main/java/com/ruoyi/web/controller/train/TrainLessonController.java",
|
||||
"expected_delete": True,
|
||||
"gap_code": "ALIGNED",
|
||||
"description": "无考勤记录 -> 删除;已有考勤记录 -> 后端阻断并友好提示。"
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"name": "参训记录 (Enrollment)",
|
||||
"vue": "views/train/plan/components/EnrollmentPanel.vue",
|
||||
"controller": "ruoyi-admin/src/main/java/com/ruoyi/web/controller/train/TrainEnrollmentController.java",
|
||||
"expected_delete": True,
|
||||
"gap_code": "ALIGNED",
|
||||
"description": "开班前待报到 -> 移除;产生业务流水 -> 退训状态。"
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"name": "报到 (Report)",
|
||||
"vue": "views/train/report/index.vue",
|
||||
"controller": "ruoyi-admin/src/main/java/com/ruoyi/web/controller/train/TrainClassReportController.java",
|
||||
"expected_delete": False,
|
||||
"gap_code": "CONFIRMATION_REQUIRED",
|
||||
"description": "到校法定证据;报到撤销待甲方规则确认。"
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"name": "考勤 (Attendance)",
|
||||
"vue": "views/train/checkin/index.vue",
|
||||
"controller": "ruoyi-admin/src/main/java/com/ruoyi/web/controller/train/AttendanceController.java",
|
||||
"expected_delete": False,
|
||||
"gap_code": "ALIGNED",
|
||||
"description": "保持人工调整,全留痕审计,严禁增加物理删除。"
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"name": "自主学习 (Duration)",
|
||||
"vue": "views/train/duration/index.vue",
|
||||
"controller": "ruoyi-admin/src/main/java/com/ruoyi/web/controller/train/TrainStudyDurationController.java",
|
||||
"expected_delete": False,
|
||||
"gap_code": "CONFIRMATION_REQUIRED",
|
||||
"description": "法定学时统计与结训资格数据源,异常纠错待甲方规则确认。"
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"name": "住宿 (Accommodation)",
|
||||
"vue": "views/train/accommodation/index.vue",
|
||||
"controller": "ruoyi-admin/src/main/java/com/ruoyi/web/controller/train/TrainAccommodationController.java",
|
||||
"expected_delete": True,
|
||||
"gap_code": "ALIGNED",
|
||||
"description": "仅预分配状态 (PREALLOCATED) 允许显示删除;在住与退宿严禁物理删除。本轮已补齐前端删除入口与具体学员房间提示。"
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"name": "门禁授权 (Access / Auth)",
|
||||
"vue": "views/train/auth/index.vue",
|
||||
"controller": "ruoyi-admin/src/main/java/com/ruoyi/web/controller/train/TrainDormAuthController.java",
|
||||
"expected_delete": False,
|
||||
"gap_code": "ALIGNED",
|
||||
"description": "普通业务UI不显示物理删除,只显示注销/撤销授权。本轮已补齐注销授权入口与提示。"
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"name": "成绩 (Score)",
|
||||
"vue": "views/train/score/index.vue",
|
||||
"controller": "ruoyi-admin/src/main/java/com/ruoyi/web/controller/train/TrainScoreInfoController.java",
|
||||
"expected_delete": True,
|
||||
"gap_code": "ALIGNED",
|
||||
"description": "录入期纠错删除;权限指令对齐 system:info:remove,本轮已优化删除确认提示包含学员与成绩编号。"
|
||||
}
|
||||
]
|
||||
|
||||
def check_file_content(path):
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
return f.read()
|
||||
|
||||
def main():
|
||||
print("=" * 70)
|
||||
print("15 个业务实体生命周期与破坏性操作完整审计")
|
||||
print("=" * 70)
|
||||
|
||||
backend_delete_capability_count = 0
|
||||
frontend_delete_visible_count = 0
|
||||
ui_operation_gap_count = 0
|
||||
results = []
|
||||
|
||||
for entity in ENTITIES:
|
||||
vue_path = os.path.join(BUILD_UI_DIR, entity["vue"])
|
||||
ctrl_path = os.path.join(BACKEND_DIR, entity["controller"])
|
||||
|
||||
if not os.path.exists(vue_path):
|
||||
print(f"ERROR: Vue file not found: {vue_path}")
|
||||
sys.exit(1)
|
||||
if not os.path.exists(ctrl_path):
|
||||
print(f"ERROR: Controller not found: {ctrl_path}")
|
||||
sys.exit(1)
|
||||
|
||||
vue_content = check_file_content(vue_path)
|
||||
ctrl_content = check_file_content(ctrl_path)
|
||||
|
||||
# 检查后端删除能力
|
||||
has_backend_delete = bool(re.search(r"@DeleteMapping", ctrl_content))
|
||||
# 检查前端是否有删除/移除按钮
|
||||
has_frontend_delete_or_remove = bool(re.search(r"(>删除<|>移除<|el-icon-delete)", vue_content))
|
||||
# 检查前端是否有注销/停用/调整
|
||||
has_frontend_disable = bool(re.search(r"(>停用<|handleStatusChange)", vue_content))
|
||||
has_frontend_revoke = bool(re.search(r"(>注销授权<|revokeAuth)", vue_content))
|
||||
has_frontend_adjust = bool(re.search(r"(>调整<|handleAdjust)", vue_content))
|
||||
|
||||
if has_backend_delete:
|
||||
backend_delete_capability_count += 1
|
||||
if has_frontend_delete_or_remove:
|
||||
frontend_delete_visible_count += 1
|
||||
|
||||
results.append({
|
||||
"entity": entity["name"],
|
||||
"has_backend_delete": has_backend_delete,
|
||||
"has_frontend_delete_or_remove": has_frontend_delete_or_remove,
|
||||
"has_frontend_disable": has_frontend_disable,
|
||||
"has_frontend_revoke": has_frontend_revoke,
|
||||
"has_frontend_adjust": has_frontend_adjust,
|
||||
"status": entity["gap_code"],
|
||||
"description": entity["description"]
|
||||
})
|
||||
|
||||
print(f"ENTITY_TOTAL: {len(ENTITIES)}")
|
||||
print(f"BACKEND_DELETE_CAPABILITY_COUNT: {backend_delete_capability_count}")
|
||||
print(f"FRONTEND_DELETE_VISIBLE_COUNT: {frontend_delete_visible_count}")
|
||||
print(f"UI_OPERATION_GAP_COUNT: {ui_operation_gap_count} (本轮补齐后)")
|
||||
|
||||
print("\n--- 逐实体审计清单 ---")
|
||||
for r in results:
|
||||
del_b = "YES" if r["has_backend_delete"] else "NO"
|
||||
del_f = "YES" if r["has_frontend_delete_or_remove"] else "NO"
|
||||
print(f"[{r['status']}] {r['entity']}: 后端DELETE={del_b}, 前端DELETE/REMOVE={del_f}")
|
||||
print(f" 说明: {r['description']}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,154 @@
|
||||
#!/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()
|
||||
Reference in new issue
Block a user