Skip to content
Merged
54 changes: 51 additions & 3 deletions app/dto/schemas.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from enum import Enum
from pydantic import BaseModel, Field
from typing import Optional
from app.core.config import settings

# Spring Boot Gateway에서 Python FastAPI로 검사를 요청할 때의 바디 규격
class URLScanRequest(BaseModel):
Expand All @@ -17,8 +17,7 @@ class URLScanResponse(BaseModel):
error_message: Optional[str] = Field(None, description="에러 발생 시 메시지 기록용")

class Config:

json_schema_extra = {
json_schema_extra = {
"example": {
"has_url": True,
"original_url": "https://bit.ly/suspect-link",
Expand All @@ -29,3 +28,52 @@ class Config:
"error_message": None
}
}

# 3중 가중치 시스템 스펙 정의
class RiskGrade(str, Enum):
HIGH = "HIGH"
MEDIUM = "MEDIUM"
LOW = "LOW"

class ContributionBreakdown(BaseModel):
llm: int = Field(..., description="LLM 문맥 분석 기여 점수 (0~50)", ge=0, le=50)
hybrid_url: int = Field(..., description="하이브리드 URL 보안 엔진 기여 점수 (0~30)", ge=0, le=30)
rules: int = Field(..., description="로컬 가드 규칙 기반 기여 점수 (0~20)", ge=0, le=20)

class SmishingAnalysisResponse(BaseModel):
status: str = Field(..., description="응답 상태 (SUCCESS / ERROR)")
message: str = Field(..., description="응답 메시지 설명")

# 핵심 합성 스코어 필드
final_score: int = Field(..., description="3중 가중치 합성 최종 위험 점수 (0~100)", ge=0, le=100)
risk_grade: RiskGrade = Field(..., description="최종 점수 기반 위험 등급 분류 (HIGH/MEDIUM/LOW)")
contribution_breakdown: ContributionBreakdown = Field(..., description="3개 레이어별 점수 기여도 명세")

# 세부 분석 트랙 데이터
text_analysis: Optional[dict] = Field(None, description="LLM 실시간 문맥 분석 상세 결과")
url_analysis: Optional[dict] = Field(None, description="하이브리드 URL 보안 엔진 상세 분석 결과")

class Config:
use_enum_values = True
json_schema_extra = {
"example": {
"status": "SUCCESS",
"message": "통합 스미싱 분석이 완료되었습니다.",
"final_score": 95,
"risk_grade": "HIGH",
"contribution_breakdown": {
"llm": 45,
"hybrid_url": 30,
"rules": 20
},
"text_analysis": {
"risk_score": 90,
"reason": "지인을 사칭한 금전 요구 문맥 감지"
},
"url_analysis": {
"has_url": True,
"is_url_malicious": True,
"url_risk_score": 0.95
}
}
}
4 changes: 3 additions & 1 deletion app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
version=settings.VERSION
)

# Spring Boot 및 프론트엔드 연동을 위한 CORS 미들웨어 설정

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Insecure CORS: wildcard origin combined with credentials.

The CORS middleware configured below (per the static-analysis hint) pairs allow_origins=["*"] with allow_credentials=True and wildcard methods/headers. With credentials enabled, Starlette/FastAPI reflects the request Origin, effectively allowing any site to make authenticated cross-origin requests (CWE-942 / OWASP A05:2021). Replace the wildcard with an explicit origin allowlist (your frontend and Spring Boot hosts) and enumerate allowed methods/headers when credentials are enabled.

🔒️ Suggested direction
-    allow_origins=["*"],
+    allow_origins=["https://your-frontend.example", "https://your-springboot.example"],
     allow_credentials=True,
-    allow_methods=["*"],
-    allow_headers=["*"],
+    allow_methods=["GET", "POST", "OPTIONS"],
+    allow_headers=["Content-Type", "Authorization"],
🧰 Tools
🪛 ast-grep (0.44.1)

[warning] 11-17: CORSMiddleware allows credentials together with a wildcard origin, methods, or headers, which lets any site issue authenticated cross-origin requests. Use an explicit origin allowlist and enumerate the allowed methods/headers when allow_credentials=True.
Context: app.add_middleware(
CORSMiddleware,
allow_origins=[""],
allow_credentials=True,
allow_methods=["
"],
allow_headers=["*"],
)
Note: [CWE-942] Permissive Cross-domain Policy with Untrusted Domains. OWASP A05:2021 Security Misconfiguration. Starlette/FastAPI CORSMiddleware reflects the request Origin when credentials are enabled, so pairing allow_credentials=True with a wildcard origin/methods/headers exposes authenticated endpoints to any site.

(starlette-cors-credentials-wildcard-python)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/main.py` at line 11, Update the CORS middleware configuration below the
Spring Boot/frontend integration comment to replace allow_origins=["*"] with an
explicit allowlist containing the frontend and Spring Boot hosts, and replace
wildcard allow_methods and allow_headers with the required enumerated values
while retaining credentials only for trusted origins.

Source: Linters/SAST tools

app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
Expand All @@ -16,7 +17,8 @@
allow_headers=["*"],
)

app.include_router(analyze.router, prefix=settings.API_V1_STR)
# 라우터 통합 등록
app.include_router(analyze.router, prefix="/api")

@app.get("/", tags=["Root"])
def root_check():
Expand Down
58 changes: 21 additions & 37 deletions app/router/analyze.py
Original file line number Diff line number Diff line change
@@ -1,49 +1,33 @@
from fastapi import APIRouter, Depends, HTTPException, status
from app.dto.response import ApiResponse
from app.dto.request import AnalyzeRequest
from app.dto.schemas import URLScanResponse
from app.service.security.gemini_text_analyzer import analyze_text_with_gemini
import logging
from fastapi import APIRouter, Depends, status, HTTPException
from app.dto.schemas import URLScanRequest, SmishingAnalysisResponse
from app.service.scan_service import ScanService

# 문자 분석 전용 라우터 생성
logger = logging.getLogger(__name__)

router = APIRouter(prefix="/analyze", tags=["Analyze"])

# 서비스 인스턴스 생성 유틸
def get_scan_service() -> ScanService:
return ScanService()

# 통합 스미싱 탐지 API
@router.post("", response_model=ApiResponse[dict], status_code=status.HTTP_200_OK)
@router.post(
"",
response_model=SmishingAnalysisResponse,
status_code=status.HTTP_200_OK,
summary="[메인 통합 엔진] 문자 본문 기반 3중 스미싱 통합 분석"
)
async def analyze_smishing(
payload: AnalyzeRequest,
payload: URLScanRequest,
scan_service: ScanService = Depends(get_scan_service)
):

try:
text_analysis = await analyze_text_with_gemini(payload.message)

url_scan_result: URLScanResponse = await scan_service.scan_message_text(payload.message)

real_url_analysis = {
"has_url": url_scan_result.has_url,
"is_shortened": url_scan_result.original_url != url_scan_result.traced_url if url_scan_result.has_url else False,
"origin_url": url_scan_result.traced_url, # 최종 목적지 URL
"original_url": url_scan_result.original_url, # 최초 추출 URL
"is_url_malicious": url_scan_result.is_url_malicious,
"url_risk_score": url_scan_result.url_risk_score,
"engine_source": url_scan_result.engine_source,
"error_message": url_scan_result.error_message
}
) -> SmishingAnalysisResponse:

is_smishing_detected = (text_analysis.get("result", {}).get("grade") != "SAFE") or url_scan_result.is_url_malicious

result = {
"smishing_detected": is_smishing_detected,
"text_analysis": text_analysis,
"url_analysis": real_url_analysis
}

return ApiResponse.success(data=result, message="문자 분석이 완료되었습니다.")
logger.info(f"[Router] 통합 스미싱 분석 마스터 파이프라인 진입: {payload.text[:15]}...")

try:
return await scan_service.analyze_pipeline(payload.text)
except Exception as e:
return ApiResponse.error(message=f"통합 스미싱 탐지 중 서버 에러가 발생했습니다: {str(e)}")
logger.error(f"[Router] 스캔 처리 중 장애 발생: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"서버 내부 스캔 파이프라인 연산 중 오류: {str(e)}"
)
Loading