Skip to content

Feat(#17): 문자 분석 통합 엔드포인트 및 3중 가중치 스코어링 시스템 구현 - #18

Merged
pearseona merged 8 commits into
developfrom
feat/17-scoring-pipeline
Jul 23, 2026
Merged

Feat(#17): 문자 분석 통합 엔드포인트 및 3중 가중치 스코어링 시스템 구현#18
pearseona merged 8 commits into
developfrom
feat/17-scoring-pipeline

Conversation

@pearseona

@pearseona pearseona commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

📝 개요

텍스트 컨텍스트 분석(Gemini)과 URL 하이브리드 검사 파이프라인(VT+GSB)의 결과물을 단일 진입점으로 결합하는 통합 문자 분석 엔드포인트를 구축합니다.
또한, 탐지 결과의 신뢰도를 극대화하고 프론트엔드 원형 게이지 UI에 대응하기 위해 [LLM 50% / VirusTotal 30% / 규칙 기반 20%] 가중치를 합성하는 3중 스코어링 시스템 엔진을 설계하고 백엔드 응답 스키마를 확장합니다.

🔗 관련 이슈

🎯 주요 변경 사항

  • 라우팅 경로 최적화 및 통합 (app/main.py, app/router/analyze.py)
    • 기존 v1 접두사를 제거하고 메인 백엔드가 직관적으로 호출할 수 있도록 **/api/analyze**로 최종 엔드포인트 경로를 확정했습니다.
  • Spring Boot 통신 규격 일치화 (app/dto/schemas.py)
    • Spring Boot API Gateway 및 프론트엔드 통신 사양에 맞춰 URLScanRequestSmishingAnalysisResponse 구조로 DTO 명칭과 필드 구조를 완벽하게 동기화했습니다.
    • 요청 바디의 Key를 text로 단일화하여 데이터 변환 오버헤드를 제거했습니다.
  • 비동기 마스터 파이프라인 버그 수정 및 안정화 (app/service/scan_service.py)
    • 런타임 차단 원인이 되던 파이썬 내 자자한 들여쓰기(IndentationError) 및 모듈 임포트 구조(ImportError) 지뢰를 전면 제거했습니다.
    • asyncio.gather를 통한 텍스트 트랙(Gemini)과 URL 트랙(Hybrid Engine)의 비동기 병렬 처리 공정을 정교화했습니다.
  • 스코어링 엔진 오타 및 로직 교정 (app/utils/scoring_engine.py)
    • ScoringEngine.calculate_score 메서드 호출 시 런타임 에러를 유발하던 파라미터 오타(is_url_maliciout -> is_url_malicious)를 수정했습니다.
  • 실시간 AI Context 분석 활성화 (app/service/security/gemini_text_analyzer.py)
    • 로컬 테스트 환경의 MOCK_SECURITY_API (Sandbox Mode) 설정을 해제하여 실제 Gemini AI가 문자 메시지 문맥을 실시간으로 독해하고 위험도 점수 및 한글 분석 사유를 생성하도록 연동 완료했습니다.

📸 사진

✅ PR 체크리스트

  • 관련 이슈를 연결했습니다.
  • 구현 범위와 변경 이유를 설명했습니다.
  • 로컬 테스트(uvicorn 구동 또는 테스트 코드)를 통과했습니다.
  • API 변경 사항이 있다면 Swagger / API 명세에 반영했습니다.
  • 민감 정보(API Key, 시크릿 키 등)가 코드·로그·테스트 데이터에 포함되지 않았습니다.
  • 프론트엔드 또는 메인 백엔드(Spring)에 영향을 주는 응답 스키마 또는 Enum 변경이 있다면 팀에 공유했습니다.
  • 병합(Merge) 전 작업 브랜치를 삭제하지 않았습니다.

Summary by CodeRabbit

  • New Features

    • Added integrated smishing analysis with text and URL risk assessment.
    • Introduced final risk scores from 0–100 with Low, Medium, and High grades.
    • Added contribution breakdowns for text analysis, URL risk, and rules.
    • Analysis results can include detailed text and URL findings.
    • Added hybrid URL scanning with fallback handling and risk metadata.
  • Improvements

    • Updated the analysis endpoint to use the new response format.
    • Standardized the analysis route under /api.
    • Improved error reporting for failed analyses.

@pearseona pearseona self-assigned this Jul 20, 2026
@pearseona pearseona added the feat New feature or functional additions to the application label Jul 20, 2026
@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The /analyze endpoint now delegates to a centralized smishing analysis pipeline. The pipeline combines Gemini text analysis, hybrid URL scanning, local rules, and weighted scoring, returning a structured risk response with score, grade, and contribution details.

Changes

Smishing analysis

Layer / File(s) Summary
Response contract and weighted scoring
app/dto/schemas.py, app/utils/scoring_engine.py
Adds risk grades, bounded contribution fields, the structured analysis response, and weighted score calculation.
Hybrid URL scanning engine
app/service/security/hybrid_url_engine.py
Adds mock and production URL scanning with Google Safe Browsing, VirusTotal fallback, risk scoring, and error reporting.
Centralized analysis pipeline
app/service/scan_service.py
Combines text analysis and URL scanning, applies local URL rules, calculates final risk, and simplifies URL-only scanning.
API response and router integration
app/router/analyze.py, app/main.py
Updates the endpoint request and response models, delegates processing to ScanService, raises HTTP errors, and mounts the router at /api.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant AnalyzeRouter
  participant ScanService
  participant GeminiTextAnalyzer
  participant HybridUrlEngine
  participant ScoringEngine

  Client->>AnalyzeRouter: POST /api/analyze
  AnalyzeRouter->>ScanService: analyze_pipeline(text)
  ScanService->>GeminiTextAnalyzer: analyze text
  ScanService->>HybridUrlEngine: scan traced URL
  ScanService->>ScoringEngine: calculate score
  ScoringEngine-->>ScanService: score, grade, breakdown
  ScanService-->>AnalyzeRouter: SmishingAnalysisResponse
  AnalyzeRouter-->>Client: analysis response
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main changes: a unified message analysis endpoint and triple-weight scoring.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/17-scoring-pipeline

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
app/router/analyze.py (1)

26-33: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Router error path is largely unreachable.

analyze_pipeline already wraps its body in try/except Exception and returns a SmishingAnalysisResponse(status="ERROR", ...) (see app/service/scan_service.py Lines 82-92), so this except/HTTPException(500) rarely fires and pipeline failures surface as HTTP 200 with status="ERROR" rather than a 5xx. Confirm this is the intended contract for the frontend/Spring Boot client; if a 5xx is expected on failure, the pipeline should re-raise instead of swallowing. Minor: add from e to the raise to satisfy B904.

🤖 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/router/analyze.py` around lines 26 - 33, Align the failure contract
between the router and analyze_pipeline: update analyze_pipeline so pipeline
exceptions are re-raised when failures must produce HTTP 5xx responses, allowing
the analyze router’s HTTPException path to execute. Preserve the existing error
response only if HTTP 200 with status="ERROR" is the intended client contract;
otherwise remove that swallowing behavior and chain the router’s HTTPException
with “from e”.
🤖 Prompt for all review comments with 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.

Inline comments:
In `@app/main.py`:
- 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.

In `@app/utils/scoring_engine.py`:
- Line 26: Clamp the value assigned to llm_contrib in the scoring calculation to
the declared 0–50 range, matching the existing bound enforced by
ContributionBreakdown.llm. Preserve the current scaling for valid llm_score
values while preventing out-of-range scores from producing a contribution above
50.

---

Nitpick comments:
In `@app/router/analyze.py`:
- Around line 26-33: Align the failure contract between the router and
analyze_pipeline: update analyze_pipeline so pipeline exceptions are re-raised
when failures must produce HTTP 5xx responses, allowing the analyze router’s
HTTPException path to execute. Preserve the existing error response only if HTTP
200 with status="ERROR" is the intended client contract; otherwise remove that
swallowing behavior and chain the router’s HTTPException with “from e”.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: fa8760b3-4415-4e2c-b12d-70c421919ccc

📥 Commits

Reviewing files that changed from the base of the PR and between 9e61733 and 3cdfbf2.

📒 Files selected for processing (6)
  • app/dto/schemas.py
  • app/main.py
  • app/router/analyze.py
  • app/service/scan_service.py
  • app/service/security/hybrid_url_engine.py
  • app/utils/scoring_engine.py

Comment thread app/main.py
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

# 텍스트 문맥 점수와 하이브리드 URL 엔진의 결과값을 결합하여 최종 위험도를 산출

# 1. LLM 문맥 분석 기여 점수 (최대 50점)
llm_contrib = round(llm_score * 0.5)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Clamp llm_contrib to its declared bound (0–50).

Unlike url_contrib (clamped to 30), llm_contrib is not bounded. If llm_score ever exceeds 100 (e.g. an out-of-range Gemini risk_score), llm_contrib > 50 will fail the ContributionBreakdown.llm le=50 constraint at Line 54, raising a ValidationError that collapses the whole pipeline into the ERROR fallback and discards all analysis.

🛡️ Proposed fix
-        llm_contrib = round(llm_score * 0.5)
+        llm_contrib = min(max(round(llm_score * 0.5), 0), 50)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
llm_contrib = round(llm_score * 0.5)
llm_contrib = min(max(round(llm_score * 0.5), 0), 50)
🤖 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/utils/scoring_engine.py` at line 26, Clamp the value assigned to
llm_contrib in the scoring calculation to the declared 0–50 range, matching the
existing bound enforced by ContributionBreakdown.llm. Preserve the current
scaling for valid llm_score values while preventing out-of-range scores from
producing a contribution above 50.

@pearseona
pearseona merged commit 6d1fa67 into develop Jul 23, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feat New feature or functional additions to the application

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant