CodeReview/backend/app/api/v1/endpoints/rules.py

729 lines
23 KiB
Python

"""
审计规则 API 端点
"""
import json
from typing import Any, List, Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi.responses import JSONResponse
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.future import select
from sqlalchemy import func as sql_func
from sqlalchemy.orm import selectinload
from app.api import deps
from app.db.session import get_db
from app.models.audit_rule import AuditRuleSet, AuditRule
from app.models.user import User
from app.schemas.audit_rule import (
AuditRuleCreate,
AuditRuleUpdate,
AuditRuleResponse,
AuditRuleSetCreate,
AuditRuleSetUpdate,
AuditRuleSetResponse,
AuditRuleSetListResponse,
AuditRuleSetExport,
AuditRuleSetImport,
)
router = APIRouter()
# ==================== 规则集 API ====================
@router.get("", response_model=AuditRuleSetListResponse)
async def list_rule_sets(
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=100),
language: Optional[str] = Query(None, description="语言过滤"),
rule_type: Optional[str] = Query(None, description="类型过滤"),
is_active: Optional[bool] = Query(None, description="是否启用"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(deps.get_current_user),
) -> Any:
"""获取审计规则集列表"""
query = select(AuditRuleSet).options(selectinload(AuditRuleSet.rules))
# 过滤条件:系统规则集 + 当前用户创建的规则集
query = query.where(
(AuditRuleSet.is_system == True) |
(AuditRuleSet.created_by == current_user.id)
)
if language:
query = query.where(AuditRuleSet.language == language)
if rule_type:
query = query.where(AuditRuleSet.rule_type == rule_type)
if is_active is not None:
query = query.where(AuditRuleSet.is_active == is_active)
# 排序
query = query.order_by(
AuditRuleSet.is_system.desc(),
AuditRuleSet.is_default.desc(),
AuditRuleSet.sort_order.asc(),
AuditRuleSet.created_at.desc()
)
# 计数
count_query = select(sql_func.count()).select_from(
select(AuditRuleSet).where(
(AuditRuleSet.is_system == True) |
(AuditRuleSet.created_by == current_user.id)
).subquery()
)
total = (await db.execute(count_query)).scalar()
# 分页
query = query.offset(skip).limit(limit)
result = await db.execute(query)
rule_sets = result.scalars().unique().all()
items = []
for rs in rule_sets:
severity_weights = {"critical": 10, "high": 5, "medium": 2, "low": 1}
if rs.severity_weights:
try:
severity_weights = json.loads(rs.severity_weights)
except:
pass
rules = [
AuditRuleResponse(
id=r.id,
rule_set_id=r.rule_set_id,
rule_code=r.rule_code,
name=r.name,
description=r.description,
category=r.category,
severity=r.severity,
custom_prompt=r.custom_prompt,
fix_suggestion=r.fix_suggestion,
reference_url=r.reference_url,
enabled=r.enabled,
sort_order=r.sort_order,
created_at=r.created_at,
updated_at=r.updated_at,
)
for r in rs.rules
]
items.append(AuditRuleSetResponse(
id=rs.id,
name=rs.name,
description=rs.description,
language=rs.language,
rule_type=rs.rule_type,
severity_weights=severity_weights,
is_default=rs.is_default,
is_system=rs.is_system,
is_active=rs.is_active,
sort_order=rs.sort_order,
created_by=rs.created_by,
created_at=rs.created_at,
updated_at=rs.updated_at,
rules=rules,
rules_count=len(rules),
enabled_rules_count=len([r for r in rules if r.enabled]),
))
return AuditRuleSetListResponse(items=items, total=total)
@router.get("/{rule_set_id}", response_model=AuditRuleSetResponse)
async def get_rule_set(
rule_set_id: str,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(deps.get_current_user),
) -> Any:
"""获取单个规则集"""
result = await db.execute(
select(AuditRuleSet)
.options(selectinload(AuditRuleSet.rules))
.where(AuditRuleSet.id == rule_set_id)
)
rule_set = result.scalar_one_or_none()
if not rule_set:
raise HTTPException(status_code=404, detail="规则集不存在")
if not rule_set.is_system and rule_set.created_by != current_user.id:
raise HTTPException(status_code=403, detail="无权访问此规则集")
severity_weights = {"critical": 10, "high": 5, "medium": 2, "low": 1}
if rule_set.severity_weights:
try:
severity_weights = json.loads(rule_set.severity_weights)
except:
pass
rules = [
AuditRuleResponse(
id=r.id,
rule_set_id=r.rule_set_id,
rule_code=r.rule_code,
name=r.name,
description=r.description,
category=r.category,
severity=r.severity,
custom_prompt=r.custom_prompt,
fix_suggestion=r.fix_suggestion,
reference_url=r.reference_url,
enabled=r.enabled,
sort_order=r.sort_order,
created_at=r.created_at,
updated_at=r.updated_at,
)
for r in rule_set.rules
]
return AuditRuleSetResponse(
id=rule_set.id,
name=rule_set.name,
description=rule_set.description,
language=rule_set.language,
rule_type=rule_set.rule_type,
severity_weights=severity_weights,
is_default=rule_set.is_default,
is_system=rule_set.is_system,
is_active=rule_set.is_active,
sort_order=rule_set.sort_order,
created_by=rule_set.created_by,
created_at=rule_set.created_at,
updated_at=rule_set.updated_at,
rules=rules,
rules_count=len(rules),
enabled_rules_count=len([r for r in rules if r.enabled]),
)
@router.post("", response_model=AuditRuleSetResponse)
async def create_rule_set(
rule_set_in: AuditRuleSetCreate,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(deps.get_current_user),
) -> Any:
"""创建审计规则集"""
rule_set = AuditRuleSet(
name=rule_set_in.name,
description=rule_set_in.description,
language=rule_set_in.language,
rule_type=rule_set_in.rule_type,
severity_weights=json.dumps(rule_set_in.severity_weights or {}),
is_active=rule_set_in.is_active,
sort_order=rule_set_in.sort_order,
is_system=False,
is_default=False,
created_by=current_user.id,
)
db.add(rule_set)
await db.flush()
# 创建规则
rules = []
for rule_in in (rule_set_in.rules or []):
rule = AuditRule(
rule_set_id=rule_set.id,
rule_code=rule_in.rule_code,
name=rule_in.name,
description=rule_in.description,
category=rule_in.category,
severity=rule_in.severity,
custom_prompt=rule_in.custom_prompt,
fix_suggestion=rule_in.fix_suggestion,
reference_url=rule_in.reference_url,
enabled=rule_in.enabled,
sort_order=rule_in.sort_order,
)
db.add(rule)
rules.append(rule)
await db.commit()
await db.refresh(rule_set)
return AuditRuleSetResponse(
id=rule_set.id,
name=rule_set.name,
description=rule_set.description,
language=rule_set.language,
rule_type=rule_set.rule_type,
severity_weights=rule_set_in.severity_weights or {},
is_default=rule_set.is_default,
is_system=rule_set.is_system,
is_active=rule_set.is_active,
sort_order=rule_set.sort_order,
created_by=rule_set.created_by,
created_at=rule_set.created_at,
updated_at=rule_set.updated_at,
rules=[
AuditRuleResponse(
id=r.id,
rule_set_id=r.rule_set_id,
rule_code=r.rule_code,
name=r.name,
description=r.description,
category=r.category,
severity=r.severity,
custom_prompt=r.custom_prompt,
fix_suggestion=r.fix_suggestion,
reference_url=r.reference_url,
enabled=r.enabled,
sort_order=r.sort_order,
created_at=r.created_at,
updated_at=r.updated_at,
)
for r in rules
],
rules_count=len(rules),
enabled_rules_count=len([r for r in rules if r.enabled]),
)
@router.put("/{rule_set_id}", response_model=AuditRuleSetResponse)
async def update_rule_set(
rule_set_id: str,
rule_set_in: AuditRuleSetUpdate,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(deps.get_current_user),
) -> Any:
"""更新审计规则集"""
result = await db.execute(
select(AuditRuleSet)
.options(selectinload(AuditRuleSet.rules))
.where(AuditRuleSet.id == rule_set_id)
)
rule_set = result.scalar_one_or_none()
if not rule_set:
raise HTTPException(status_code=404, detail="规则集不存在")
if rule_set.is_system:
# 系统规则集只能修改启用状态
if rule_set_in.is_active is not None:
rule_set.is_active = rule_set_in.is_active
else:
raise HTTPException(status_code=403, detail="系统规则集不允许修改")
else:
if rule_set.created_by != current_user.id:
raise HTTPException(status_code=403, detail="无权修改此规则集")
update_data = rule_set_in.dict(exclude_unset=True)
for field, value in update_data.items():
if field == "severity_weights" and value is not None:
setattr(rule_set, field, json.dumps(value))
elif field != "is_default":
setattr(rule_set, field, value)
await db.commit()
await db.refresh(rule_set)
severity_weights = {"critical": 10, "high": 5, "medium": 2, "low": 1}
if rule_set.severity_weights:
try:
severity_weights = json.loads(rule_set.severity_weights)
except:
pass
rules = [
AuditRuleResponse(
id=r.id,
rule_set_id=r.rule_set_id,
rule_code=r.rule_code,
name=r.name,
description=r.description,
category=r.category,
severity=r.severity,
custom_prompt=r.custom_prompt,
fix_suggestion=r.fix_suggestion,
reference_url=r.reference_url,
enabled=r.enabled,
sort_order=r.sort_order,
created_at=r.created_at,
updated_at=r.updated_at,
)
for r in rule_set.rules
]
return AuditRuleSetResponse(
id=rule_set.id,
name=rule_set.name,
description=rule_set.description,
language=rule_set.language,
rule_type=rule_set.rule_type,
severity_weights=severity_weights,
is_default=rule_set.is_default,
is_system=rule_set.is_system,
is_active=rule_set.is_active,
sort_order=rule_set.sort_order,
created_by=rule_set.created_by,
created_at=rule_set.created_at,
updated_at=rule_set.updated_at,
rules=rules,
rules_count=len(rules),
enabled_rules_count=len([r for r in rules if r.enabled]),
)
@router.delete("/{rule_set_id}")
async def delete_rule_set(
rule_set_id: str,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(deps.get_current_user),
) -> Any:
"""删除审计规则集"""
result = await db.execute(
select(AuditRuleSet).where(AuditRuleSet.id == rule_set_id)
)
rule_set = result.scalar_one_or_none()
if not rule_set:
raise HTTPException(status_code=404, detail="规则集不存在")
if rule_set.is_system:
raise HTTPException(status_code=403, detail="系统规则集不允许删除")
if rule_set.created_by != current_user.id:
raise HTTPException(status_code=403, detail="无权删除此规则集")
await db.delete(rule_set)
await db.commit()
return {"message": "规则集已删除"}
@router.get("/{rule_set_id}/export")
async def export_rule_set(
rule_set_id: str,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(deps.get_current_user),
) -> Any:
"""导出规则集为JSON"""
result = await db.execute(
select(AuditRuleSet)
.options(selectinload(AuditRuleSet.rules))
.where(AuditRuleSet.id == rule_set_id)
)
rule_set = result.scalar_one_or_none()
if not rule_set:
raise HTTPException(status_code=404, detail="规则集不存在")
if not rule_set.is_system and rule_set.created_by != current_user.id:
raise HTTPException(status_code=403, detail="无权导出此规则集")
severity_weights = {"critical": 10, "high": 5, "medium": 2, "low": 1}
if rule_set.severity_weights:
try:
severity_weights = json.loads(rule_set.severity_weights)
except:
pass
export_data = {
"name": rule_set.name,
"description": rule_set.description,
"language": rule_set.language,
"rule_type": rule_set.rule_type,
"severity_weights": severity_weights,
"rules": [
{
"rule_code": r.rule_code,
"name": r.name,
"description": r.description,
"category": r.category,
"severity": r.severity,
"custom_prompt": r.custom_prompt,
"fix_suggestion": r.fix_suggestion,
"reference_url": r.reference_url,
"enabled": r.enabled,
"sort_order": r.sort_order,
}
for r in rule_set.rules
],
"export_version": "1.0",
}
return JSONResponse(
content=export_data,
headers={
"Content-Disposition": f'attachment; filename="{rule_set.name}.json"'
}
)
@router.post("/import", response_model=AuditRuleSetResponse)
async def import_rule_set(
import_data: AuditRuleSetImport,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(deps.get_current_user),
) -> Any:
"""导入规则集"""
rule_set = AuditRuleSet(
name=import_data.name,
description=import_data.description,
language=import_data.language,
rule_type=import_data.rule_type,
severity_weights=json.dumps(import_data.severity_weights or {}),
is_active=True,
is_system=False,
is_default=False,
created_by=current_user.id,
)
db.add(rule_set)
await db.flush()
rules = []
for rule_in in import_data.rules:
rule = AuditRule(
rule_set_id=rule_set.id,
rule_code=rule_in.rule_code,
name=rule_in.name,
description=rule_in.description,
category=rule_in.category,
severity=rule_in.severity,
custom_prompt=rule_in.custom_prompt,
fix_suggestion=rule_in.fix_suggestion,
reference_url=rule_in.reference_url,
enabled=rule_in.enabled,
sort_order=rule_in.sort_order,
)
db.add(rule)
rules.append(rule)
await db.commit()
await db.refresh(rule_set)
return AuditRuleSetResponse(
id=rule_set.id,
name=rule_set.name,
description=rule_set.description,
language=rule_set.language,
rule_type=rule_set.rule_type,
severity_weights=import_data.severity_weights or {},
is_default=rule_set.is_default,
is_system=rule_set.is_system,
is_active=rule_set.is_active,
sort_order=rule_set.sort_order,
created_by=rule_set.created_by,
created_at=rule_set.created_at,
updated_at=rule_set.updated_at,
rules=[
AuditRuleResponse(
id=r.id,
rule_set_id=r.rule_set_id,
rule_code=r.rule_code,
name=r.name,
description=r.description,
category=r.category,
severity=r.severity,
custom_prompt=r.custom_prompt,
fix_suggestion=r.fix_suggestion,
reference_url=r.reference_url,
enabled=r.enabled,
sort_order=r.sort_order,
created_at=r.created_at,
updated_at=r.updated_at,
)
for r in rules
],
rules_count=len(rules),
enabled_rules_count=len([r for r in rules if r.enabled]),
)
# ==================== 单个规则 API ====================
@router.post("/{rule_set_id}/rules", response_model=AuditRuleResponse)
async def add_rule_to_set(
rule_set_id: str,
rule_in: AuditRuleCreate,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(deps.get_current_user),
) -> Any:
"""向规则集添加规则"""
result = await db.execute(
select(AuditRuleSet).where(AuditRuleSet.id == rule_set_id)
)
rule_set = result.scalar_one_or_none()
if not rule_set:
raise HTTPException(status_code=404, detail="规则集不存在")
if rule_set.is_system:
raise HTTPException(status_code=403, detail="系统规则集不允许添加规则")
if rule_set.created_by != current_user.id:
raise HTTPException(status_code=403, detail="无权修改此规则集")
rule = AuditRule(
rule_set_id=rule_set_id,
rule_code=rule_in.rule_code,
name=rule_in.name,
description=rule_in.description,
category=rule_in.category,
severity=rule_in.severity,
custom_prompt=rule_in.custom_prompt,
fix_suggestion=rule_in.fix_suggestion,
reference_url=rule_in.reference_url,
enabled=rule_in.enabled,
sort_order=rule_in.sort_order,
)
db.add(rule)
await db.commit()
await db.refresh(rule)
return AuditRuleResponse(
id=rule.id,
rule_set_id=rule.rule_set_id,
rule_code=rule.rule_code,
name=rule.name,
description=rule.description,
category=rule.category,
severity=rule.severity,
custom_prompt=rule.custom_prompt,
fix_suggestion=rule.fix_suggestion,
reference_url=rule.reference_url,
enabled=rule.enabled,
sort_order=rule.sort_order,
created_at=rule.created_at,
updated_at=rule.updated_at,
)
@router.put("/{rule_set_id}/rules/{rule_id}", response_model=AuditRuleResponse)
async def update_rule(
rule_set_id: str,
rule_id: str,
rule_in: AuditRuleUpdate,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(deps.get_current_user),
) -> Any:
"""更新规则"""
result = await db.execute(
select(AuditRuleSet).where(AuditRuleSet.id == rule_set_id)
)
rule_set = result.scalar_one_or_none()
if not rule_set:
raise HTTPException(status_code=404, detail="规则集不存在")
if rule_set.is_system:
raise HTTPException(status_code=403, detail="系统规则集不允许修改规则")
if rule_set.created_by != current_user.id:
raise HTTPException(status_code=403, detail="无权修改此规则集")
result = await db.execute(
select(AuditRule).where(
AuditRule.id == rule_id,
AuditRule.rule_set_id == rule_set_id
)
)
rule = result.scalar_one_or_none()
if not rule:
raise HTTPException(status_code=404, detail="规则不存在")
update_data = rule_in.dict(exclude_unset=True)
for field, value in update_data.items():
setattr(rule, field, value)
await db.commit()
await db.refresh(rule)
return AuditRuleResponse(
id=rule.id,
rule_set_id=rule.rule_set_id,
rule_code=rule.rule_code,
name=rule.name,
description=rule.description,
category=rule.category,
severity=rule.severity,
custom_prompt=rule.custom_prompt,
fix_suggestion=rule.fix_suggestion,
reference_url=rule.reference_url,
enabled=rule.enabled,
sort_order=rule.sort_order,
created_at=rule.created_at,
updated_at=rule.updated_at,
)
@router.delete("/{rule_set_id}/rules/{rule_id}")
async def delete_rule(
rule_set_id: str,
rule_id: str,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(deps.get_current_user),
) -> Any:
"""删除规则"""
result = await db.execute(
select(AuditRuleSet).where(AuditRuleSet.id == rule_set_id)
)
rule_set = result.scalar_one_or_none()
if not rule_set:
raise HTTPException(status_code=404, detail="规则集不存在")
if rule_set.is_system:
raise HTTPException(status_code=403, detail="系统规则集不允许删除规则")
if rule_set.created_by != current_user.id:
raise HTTPException(status_code=403, detail="无权修改此规则集")
result = await db.execute(
select(AuditRule).where(
AuditRule.id == rule_id,
AuditRule.rule_set_id == rule_set_id
)
)
rule = result.scalar_one_or_none()
if not rule:
raise HTTPException(status_code=404, detail="规则不存在")
await db.delete(rule)
await db.commit()
return {"message": "规则已删除"}
@router.put("/{rule_set_id}/rules/{rule_id}/toggle")
async def toggle_rule(
rule_set_id: str,
rule_id: str,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(deps.get_current_user),
) -> Any:
"""切换规则启用状态"""
result = await db.execute(
select(AuditRuleSet).where(AuditRuleSet.id == rule_set_id)
)
rule_set = result.scalar_one_or_none()
if not rule_set:
raise HTTPException(status_code=404, detail="规则集不存在")
# 系统规则集也允许切换单个规则的启用状态
if not rule_set.is_system and rule_set.created_by != current_user.id:
raise HTTPException(status_code=403, detail="无权修改此规则集")
result = await db.execute(
select(AuditRule).where(
AuditRule.id == rule_id,
AuditRule.rule_set_id == rule_set_id
)
)
rule = result.scalar_one_or_none()
if not rule:
raise HTTPException(status_code=404, detail="规则不存在")
rule.enabled = not rule.enabled
await db.commit()
return {"enabled": rule.enabled, "message": f"规则已{'启用' if rule.enabled else '禁用'}"}