Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c0ea4a2d00 | |||
| 86ead8e5a0 | |||
| 425805ab23 | |||
| 98252e06c3 | |||
| 20bde939eb | |||
| 8be7965ed1 | |||
| 4a125590b9 | |||
| 76d473d731 | |||
| 7653c2f7ad | |||
| 90844fe9ad | |||
| 37c27c0213 | |||
| 0cf8108ff0 | |||
| cc561277c9 | |||
| 4b87868122 | |||
| 948d118926 | |||
| 4b7b9fd5b6 | |||
| 65bd6d71d0 | |||
| 00ec6888c5 |
+14
-14
@@ -27,9 +27,9 @@ jobs:
|
||||
name: Ruff Lint & Format
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4.2.2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5.4.0
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
cache: 'pip'
|
||||
@@ -48,9 +48,9 @@ jobs:
|
||||
name: Alembic Migration Chain Check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4.2.2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5.4.0
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
- name: Validate migration chain
|
||||
@@ -60,9 +60,9 @@ jobs:
|
||||
name: HTML Accessibility Lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4.2.2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5.4.0
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
cache: 'pip'
|
||||
@@ -78,9 +78,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [lint]
|
||||
steps:
|
||||
- uses: actions/checkout@v4.2.2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5.4.0
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
cache: 'pip'
|
||||
@@ -93,9 +93,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [lint]
|
||||
steps:
|
||||
- uses: actions/checkout@v4.2.2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5.4.0
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
cache: 'pip'
|
||||
@@ -116,10 +116,10 @@ jobs:
|
||||
ports: ["5672:5672", "15672:15672"]
|
||||
options: --health-cmd "rabbitmq-diagnostics -q ping" --health-interval 10s --health-timeout 5s --health-retries 5
|
||||
steps:
|
||||
- uses: actions/checkout@v4.2.2
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5.4.0
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
cache: 'pip'
|
||||
@@ -154,7 +154,7 @@ jobs:
|
||||
if: github.event_name == 'push'
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v4
|
||||
- name: Generate Build Metadata
|
||||
run: |
|
||||
chmod +x scripts/generate_build_metadata.sh
|
||||
@@ -212,7 +212,7 @@ jobs:
|
||||
echo "tag=main-${SHORT_SHA}" >> "$GITHUB_OUTPUT"
|
||||
echo "image=ghcr.io/${{ github.repository_owner }}/docuelevate:main-${SHORT_SHA}" >> "$GITHUB_OUTPUT"
|
||||
- name: Checkout k8s-cluster-state
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: christianlouis/k8s-cluster-state
|
||||
token: ${{ secrets.GH_PAT }}
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
build-mode: none
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v4
|
||||
|
||||
@@ -23,13 +23,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5.4.0
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
cache: 'pip'
|
||||
|
||||
@@ -28,13 +28,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout PR branch
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.head_ref }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5.4.0
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
|
||||
+4
-34
@@ -1,34 +1,4 @@
|
||||
## 2024-05-24 - SSRF in WebDAV connection test
|
||||
**Vulnerability:** The `_test_webdav_connection` function had a custom SSRF check that failed to resolve DNS names, allowing attackers to bypass the check by providing a domain that resolves to an internal IP (e.g., `127.0.0.1`).
|
||||
**Learning:** DNS resolution is required for robust SSRF protection when validating URLs provided by users.
|
||||
**Prevention:** Use a centralized `is_private_ip` function (now in `app/utils/network.py`) that resolves the hostname to its IPs and checks if any are private.
|
||||
## 2026-03-22 - B310: urllib.request.urlopen replaced with httpx
|
||||
**Vulnerability:** The `_test_webdav_connection` function used `urllib.request.urlopen`, which natively supports dangerous schemes like `file://` or `ftp://` and follows redirects by default, potentially allowing SSRF bypasses or Local File Inclusion.
|
||||
**Learning:** `urllib.request` should be avoided for user-supplied URLs. Even when URL schemes are manually validated, `urllib`'s default redirect following behavior can bypass SSRF protections (e.g. redirecting to `127.0.0.1`).
|
||||
**Prevention:** Use a modern, safer HTTP client like `httpx` with `follow_redirects=False` when testing user-provided URLs.
|
||||
|
||||
## 2026-03-20 - Safe Path Traversal Prevention in Low-Level Utilities
|
||||
**Vulnerability:** The generic file utility `hash_file` in `app/utils/file_operations.py` accepted any file path and was vulnerable to reading arbitrary files via path traversal (e.g., `../../../etc/passwd`) or absolute paths if an attacker could control the `filepath` argument.
|
||||
**Learning:** Naively checking for `".." in path` breaks legitimate relative paths used internally by the application. Blocking absolute paths entirely also breaks functionality. Input validation should occur at the API boundary, but for defense-in-depth, low-level utilities must enforce expected boundaries (e.g., the application's `workdir`).
|
||||
**Prevention:** Use `pathlib.Path.resolve()` on both the target path and the allowed base directory (`settings.workdir`). Ensure the resolved target path is strictly within the allowed boundary using `filepath_obj.relative_to(workdir_obj)`, catching the `ValueError` that is raised when the path is out of bounds. This safely blocks both relative traversal attacks and arbitrary absolute paths.
|
||||
## 2025-05-18 - [SSRF Bypass via DNS Resolution Failure]
|
||||
**Vulnerability:** The `is_private_ip` function in `app/utils/network.py` failed open (returned `False`) when a hostname could not be resolved (`socket.gaierror`).
|
||||
**Learning:** This fail-open pattern was originally added to allow external domains in tests, but in production, it created a severe SSRF risk. An attacker could bypass SSRF protections by providing a URL that fails to resolve during the security check but resolves later (DNS rebinding), or by exploiting internal routing behaviors via unresolvable addresses.
|
||||
**Prevention:** Always fail securely in network authorization functions. If a domain cannot be resolved to verify its safety, the request must be blocked (`return True` / default-deny). Tests should mock DNS resolution correctly instead of compromising production security logic.
|
||||
## 2026-03-26 - SSRF in Integration Connection Tests
|
||||
**Vulnerability:** The `_test_imap_connection` and `_test_s3_connection` functions in `app/api/integrations.py` did not validate user-provided `host` and `endpoint_url` variables against `is_private_ip()`. This allowed an attacker to test the presence of internal IMAP servers or direct S3 SDK API calls to internal infrastructure via SSRF.
|
||||
**Learning:** Any time a new generic connection or integration test is added, SSRF validation may be forgotten if the core network utility (`is_private_ip`) is not systematically applied to all outbound network operations, regardless of the protocol (e.g., IMAP, S3).
|
||||
**Prevention:** Establish a pattern where any user-configurable host or endpoint URL is immediately passed through the centralized `is_private_ip` validation function before any network call or third-party client initialization.
|
||||
|
||||
## 2024-05-27 - SSRF Bypass via HTTP Redirects
|
||||
**Vulnerability:** In `app/api/url_upload.py`, the `validate_url_safety` function was correctly verifying the initially requested URL to prevent fetching internal IPs or cloud metadata endpoints. However, the subsequent `httpx.AsyncClient` was configured with `follow_redirects=True` without validating the destination of those redirects. An attacker could bypass SSRF protections by providing a URL to an attacker-controlled server that responds with a 301/302 redirect pointing to an internal target (e.g., `http://127.0.0.1` or `http://169.254.169.254`).
|
||||
**Learning:** Checking the URL before sending the request is insufficient if the HTTP client automatically follows redirects. The target of every single redirect must be subject to the same strict validation as the initial request.
|
||||
**Prevention:** Avoid `follow_redirects=True` for user-provided URLs when possible. If redirects must be followed, attach an event hook (e.g., `event_hooks={"response": [hook_function]}`) to the `httpx` client to intercept the response, calculate the redirect destination from the `Location` header, and run the URL safety validation logic before the redirect is actually followed.
|
||||
## 2026-03-27 - SSRF Bypass via HTTP Redirects in httpx
|
||||
**Vulnerability:** The `/process-url` endpoint used `httpx.AsyncClient(follow_redirects=True)` after validating the initial user-provided URL against SSRF protections. However, it did not validate the target URLs of any subsequent HTTP redirects, allowing an attacker to provide a safe URL that redirects to an internal/private IP, bypassing the security check.
|
||||
**Learning:** Initial URL validation is insufficient when the HTTP client is configured to follow redirects automatically. The client must be explicitly configured to validate every redirect target.
|
||||
**Prevention:** When using `httpx.AsyncClient(follow_redirects=True)` for user-provided URLs, always implement a redirect validator hook function (e.g., using `event_hooks={'response': [validate_redirect]}`) that resolves the `Location` header and passes it through the same SSRF validation logic before the redirect is followed.
|
||||
## 2026-05-27 - DOM-based XSS in upload.js
|
||||
**Vulnerability:** The `upload.js` frontend file directly injected the user-controlled `file.name` into a string literal passed to `row.innerHTML`. This allowed a malicious filename (e.g., `<img src=x onerror=alert(1)>`) to execute arbitrary JavaScript in the victim's browser context (DOM-based XSS).
|
||||
**Learning:** Even internal or local files uploaded by a user can contain malicious filenames. Any data dynamically interpolated into an HTML string that is subsequently rendered via `innerHTML` must be strictly sanitized.
|
||||
**Prevention:** Implement and apply an HTML escaping function (e.g., `_escapeHtml`) to replace dangerous characters (`&`, `<`, `>`, `"`, `'`) with their corresponding HTML entities before injecting user-controlled data into the DOM.
|
||||
## 2026-06-01 - [Fix XSS in status_dashboard.html]
|
||||
**Vulnerability:** A Cross-Site Scripting (XSS) vulnerability existed in `frontend/templates/status_dashboard.html` where untrusted configuration settings (`value`), external service messages (`data.message`), and token expirations (`data.token_info.expires_in_human`) were injected directly into the DOM via `.innerHTML` without sanitization.
|
||||
**Learning:** Even internal or admin-focused dashboards can be vulnerable if they display external or user-configurable data without escaping. Constructing HTML strings dynamically from unvalidated sources is a common vector for DOM-based XSS.
|
||||
**Prevention:** Always use a sanitization function like `escapeHtml` to escape dangerous characters (`<`, `>`, `&`, `"`, `'`) before assigning dynamic content to `.innerHTML`, or prefer `.textContent` when only plaintext is intended.
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
2026-05-23T00:38:15Z
|
||||
2026-06-01T03:41:15Z
|
||||
|
||||
@@ -10,6 +10,100 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
<!-- version list -->
|
||||
|
||||
## v0.173.4 (2026-06-01)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Address status dashboard xss review nits
|
||||
([`425805a`](https://github.com/christianlouis/DocuElevate/commit/425805ab23882944aee0cb02b5e497bc536549c0))
|
||||
|
||||
### Build System
|
||||
|
||||
- **deps**: Update redis requirement from >=4.5.0 to >=8.0.0
|
||||
([#904](https://github.com/christianlouis/DocuElevate/pull/904),
|
||||
[`20bde93`](https://github.com/christianlouis/DocuElevate/commit/20bde939eb96ec8e6700b206e046388b6b891e38))
|
||||
|
||||
- **deps-dev**: Update pytest-asyncio requirement
|
||||
([#903](https://github.com/christianlouis/DocuElevate/pull/903),
|
||||
[`4a12559`](https://github.com/christianlouis/DocuElevate/commit/4a125590b9b6cb2f07bae187a9bff708dd92a5c3))
|
||||
|
||||
### Documentation
|
||||
|
||||
- **changelog**: Update changelog [skip ci]
|
||||
([`98252e0`](https://github.com/christianlouis/DocuElevate/commit/98252e06c30bba78520a8460d55f508dbf2bdd47))
|
||||
|
||||
- **changelog**: Update changelog [skip ci]
|
||||
([`8be7965`](https://github.com/christianlouis/DocuElevate/commit/8be7965ed1892978092c3f5e02e6252918c237fd))
|
||||
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Build System
|
||||
|
||||
- **deps**: Update redis requirement from >=4.5.0 to >=8.0.0
|
||||
([#904](https://github.com/christianlouis/DocuElevate/pull/904),
|
||||
[`20bde93`](https://github.com/christianlouis/DocuElevate/commit/20bde939eb96ec8e6700b206e046388b6b891e38))
|
||||
|
||||
- **deps-dev**: Update pytest-asyncio requirement
|
||||
([#903](https://github.com/christianlouis/DocuElevate/pull/903),
|
||||
[`4a12559`](https://github.com/christianlouis/DocuElevate/commit/4a125590b9b6cb2f07bae187a9bff708dd92a5c3))
|
||||
|
||||
### Documentation
|
||||
|
||||
- **changelog**: Update changelog [skip ci]
|
||||
([`8be7965`](https://github.com/christianlouis/DocuElevate/commit/8be7965ed1892978092c3f5e02e6252918c237fd))
|
||||
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Build System
|
||||
|
||||
- **deps-dev**: Update pytest-asyncio requirement
|
||||
([#903](https://github.com/christianlouis/DocuElevate/pull/903),
|
||||
[`4a12559`](https://github.com/christianlouis/DocuElevate/commit/4a125590b9b6cb2f07bae187a9bff708dd92a5c3))
|
||||
|
||||
|
||||
## v0.173.3 (2026-05-31)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Preserve falsy values in escapeHtml
|
||||
([`37c27c0`](https://github.com/christianlouis/DocuElevate/commit/37c27c02139ae4a462e1705bda9360b23eb835b3))
|
||||
|
||||
### Documentation
|
||||
|
||||
- **changelog**: Update changelog [skip ci]
|
||||
([`cc56127`](https://github.com/christianlouis/DocuElevate/commit/cc561277c9d7d0177b4ac63921637659abb66fba))
|
||||
|
||||
- **changelog**: Update changelog [skip ci]
|
||||
([`948d118`](https://github.com/christianlouis/DocuElevate/commit/948d118926be042cc3c2a68f58241cc2fcfa23ef))
|
||||
|
||||
- **changelog**: Update changelog [skip ci]
|
||||
([`65bd6d7`](https://github.com/christianlouis/DocuElevate/commit/65bd6d71d00ff7078a05ce12b00f228405e2ec1e))
|
||||
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Documentation
|
||||
|
||||
- **changelog**: Update changelog [skip ci]
|
||||
([`948d118`](https://github.com/christianlouis/DocuElevate/commit/948d118926be042cc3c2a68f58241cc2fcfa23ef))
|
||||
|
||||
- **changelog**: Update changelog [skip ci]
|
||||
([`65bd6d7`](https://github.com/christianlouis/DocuElevate/commit/65bd6d71d00ff7078a05ce12b00f228405e2ec1e))
|
||||
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Documentation
|
||||
|
||||
- **changelog**: Update changelog [skip ci]
|
||||
([`65bd6d7`](https://github.com/christianlouis/DocuElevate/commit/65bd6d71d00ff7078a05ce12b00f228405e2ec1e))
|
||||
|
||||
|
||||
## Unreleased
|
||||
|
||||
|
||||
## v0.173.2 (2026-05-23)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
+6
-6
@@ -1,10 +1,10 @@
|
||||
DocuElevate Build Information
|
||||
==============================
|
||||
Version: 0.173.2
|
||||
Build Date: 2026-05-23T00:38:15Z
|
||||
Git Commit: 6fc00b8de10b50b4b2f92f6fadbcf7ebbee7136f
|
||||
Git Short SHA: 6fc00b8
|
||||
Version: 0.173.4
|
||||
Build Date: 2026-06-01T03:41:15Z
|
||||
Git Commit: 425805ab23882944aee0cb02b5e497bc536549c0
|
||||
Git Short SHA: 425805a
|
||||
Git Branch: main
|
||||
Commit Date: 2026-05-23T02:37:54+02:00
|
||||
Build Timestamp: 2026-05-23T00:38:15Z
|
||||
Commit Date: 2026-06-01T05:40:53+02:00
|
||||
Build Timestamp: 2026-06-01T03:41:16Z
|
||||
==============================
|
||||
|
||||
@@ -9,7 +9,7 @@ Public endpoints:
|
||||
"""
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime, time, timedelta, timezone
|
||||
from typing import Annotated, Any
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||
@@ -207,19 +207,29 @@ def platform_stats(request: Request, db: DbSession, _admin: AdminUser) -> dict[s
|
||||
from app.models import FileRecord, UserProfile
|
||||
|
||||
today = datetime.now(timezone.utc).date()
|
||||
day_start = datetime.combine(today, time.min, tzinfo=timezone.utc)
|
||||
day_end = day_start + timedelta(days=1)
|
||||
month_start = day_start.replace(day=1)
|
||||
if month_start.month == 12:
|
||||
month_end = month_start.replace(year=month_start.year + 1, month=1)
|
||||
else:
|
||||
month_end = month_start.replace(month=month_start.month + 1)
|
||||
|
||||
# Total files
|
||||
total_files: int = db.query(func.count(FileRecord.id)).scalar() or 0
|
||||
|
||||
# Files today
|
||||
files_today: int = (
|
||||
db.query(func.count(FileRecord.id)).filter(func.date(FileRecord.created_at) == today).scalar() or 0
|
||||
db.query(func.count(FileRecord.id))
|
||||
.filter(FileRecord.created_at >= day_start, FileRecord.created_at < day_end)
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
|
||||
# Files this month
|
||||
files_this_month: int = (
|
||||
db.query(func.count(FileRecord.id))
|
||||
.filter(func.strftime("%Y-%m", FileRecord.created_at) == today.strftime("%Y-%m"))
|
||||
.filter(FileRecord.created_at >= month_start, FileRecord.created_at < month_end)
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
|
||||
@@ -29,7 +29,7 @@ At average usage (~40 % of quota) margins improve to 55-65 % after tax.
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import date, datetime, timezone
|
||||
from datetime import date, datetime, time, timedelta, timezone
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import func
|
||||
@@ -317,6 +317,20 @@ def _today_utc() -> date:
|
||||
return datetime.now(timezone.utc).date()
|
||||
|
||||
|
||||
def _day_bounds_utc(day: date) -> tuple[datetime, datetime]:
|
||||
start = datetime.combine(day, time.min, tzinfo=timezone.utc)
|
||||
return start, start + timedelta(days=1)
|
||||
|
||||
|
||||
def _month_bounds_utc(day: date) -> tuple[datetime, datetime]:
|
||||
start = datetime.combine(day.replace(day=1), time.min, tzinfo=timezone.utc)
|
||||
if start.month == 12:
|
||||
end = start.replace(year=start.year + 1, month=1)
|
||||
else:
|
||||
end = start.replace(month=start.month + 1)
|
||||
return start, end
|
||||
|
||||
|
||||
def _scalar_count(query: Any) -> int:
|
||||
"""Execute a count query and return an int, defaulting to 0 for NULL."""
|
||||
return query.scalar() or 0
|
||||
@@ -335,12 +349,13 @@ def get_today_file_count(db: Session, owner_id: str) -> int:
|
||||
"""Files processed by this user today (UTC, not counting duplicates)."""
|
||||
from app.models import FileRecord
|
||||
|
||||
today = _today_utc()
|
||||
day_start, day_end = _day_bounds_utc(_today_utc())
|
||||
return _scalar_count(
|
||||
db.query(func.count(FileRecord.id)).filter(
|
||||
FileRecord.owner_id == owner_id,
|
||||
FileRecord.is_duplicate.is_(False),
|
||||
func.date(FileRecord.created_at) == today,
|
||||
FileRecord.created_at >= day_start,
|
||||
FileRecord.created_at < day_end,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -349,12 +364,13 @@ def get_month_file_count(db: Session, owner_id: str) -> int:
|
||||
"""Files processed by this user this calendar month (UTC, not counting duplicates)."""
|
||||
from app.models import FileRecord
|
||||
|
||||
today = _today_utc()
|
||||
month_start, month_end = _month_bounds_utc(_today_utc())
|
||||
return _scalar_count(
|
||||
db.query(func.count(FileRecord.id)).filter(
|
||||
FileRecord.owner_id == owner_id,
|
||||
FileRecord.is_duplicate.is_(False),
|
||||
func.strftime("%Y-%m", FileRecord.created_at) == today.strftime("%Y-%m"),
|
||||
FileRecord.created_at >= month_start,
|
||||
FileRecord.created_at < month_end,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
+13
-3
@@ -2,7 +2,7 @@
|
||||
General routes for the application homepage and basic pages.
|
||||
"""
|
||||
|
||||
from datetime import date, datetime, timezone
|
||||
from datetime import date, datetime, time, timedelta, timezone
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import Depends, HTTPException, Request
|
||||
@@ -64,17 +64,27 @@ async def serve_index(request: Request, db: Session = Depends(get_db)):
|
||||
|
||||
user = request.session.get("user") or {}
|
||||
is_admin = user.get("is_admin", False)
|
||||
day_start = datetime.combine(today, time.min, tzinfo=timezone.utc)
|
||||
day_end = day_start + timedelta(days=1)
|
||||
month_start = day_start.replace(day=1)
|
||||
if month_start.month == 12:
|
||||
month_end = month_start.replace(year=month_start.year + 1, month=1)
|
||||
else:
|
||||
month_end = month_start.replace(month=month_start.month + 1)
|
||||
|
||||
try:
|
||||
total_files: int = db.query(func.count(FileRecord.id)).scalar() or 0
|
||||
|
||||
files_today: int = (
|
||||
db.query(func.count(FileRecord.id)).filter(func.date(FileRecord.created_at) == today).scalar() or 0
|
||||
db.query(func.count(FileRecord.id))
|
||||
.filter(FileRecord.created_at >= day_start, FileRecord.created_at < day_end)
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
|
||||
files_month: int = (
|
||||
db.query(func.count(FileRecord.id))
|
||||
.filter(func.strftime("%Y-%m", FileRecord.created_at) == today.strftime("%Y-%m"))
|
||||
.filter(FileRecord.created_at >= month_start, FileRecord.created_at < month_end)
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
|
||||
@@ -293,14 +293,14 @@ function processFiles(files, progressContainer, statusMessage) {
|
||||
updateStatus();
|
||||
}
|
||||
|
||||
function _escapeHtml(unsafe) {
|
||||
if (!unsafe) return '';
|
||||
return String(unsafe)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
function _escapeHtml(str) {
|
||||
if (!str) return '';
|
||||
return String(str)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
// Pre-create one progress row per file.
|
||||
@@ -308,7 +308,6 @@ function processFiles(files, progressContainer, statusMessage) {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'flex flex-col mb-2';
|
||||
const safeFileName = _escapeHtml(file.name);
|
||||
|
||||
row.innerHTML = `
|
||||
<div class="flex justify-between">
|
||||
<span class="text-sm truncate" title="${safeFileName}">${safeFileName}</span>
|
||||
@@ -558,4 +557,3 @@ function initDragAndDrop(element, progressContainer, statusMessage, options = {}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -298,9 +298,13 @@
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
const d = document.createElement('div');
|
||||
d.textContent = str;
|
||||
return d.innerHTML;
|
||||
if (str === null || str === undefined) return '';
|
||||
return String(str)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -393,6 +393,16 @@ const i18nStrings = {
|
||||
configureNow: {{ _("status.configure_now") | tojson }},
|
||||
};
|
||||
|
||||
function escapeHtml(str) {
|
||||
if (!str) return '';
|
||||
return String(str)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Modal elements
|
||||
const resultModal = document.getElementById('resultModal');
|
||||
@@ -469,9 +479,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
const isSensitive = sensitiveKeys.some(sKey => key.toLowerCase().includes(sKey));
|
||||
|
||||
if (isSensitive && value !== 'Not set' && value !== '') {
|
||||
valueCell.textContent = value.slice(4) + '********' + value.slice(-4);
|
||||
// For better readability, we can also use HTML to mask the middle part of the string
|
||||
valueCell.innerHTML = value.slice(0, 4) + '<span class="text-gray-400">********</span>' + value.slice(-4);
|
||||
valueCell.innerHTML = escapeHtml(value.slice(0, 4)) + '<span class="text-gray-400">********</span>' + escapeHtml(value.slice(-4));
|
||||
} else {
|
||||
valueCell.textContent = value;
|
||||
}
|
||||
@@ -586,9 +595,9 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
if (data.status === 'success') {
|
||||
// If there's token info, we need to handle it specially
|
||||
if (data.token_info && data.token_info.expires_in_human) {
|
||||
let message = data.message || 'Connection successful';
|
||||
let message = escapeHtml(data.message || 'Connection successful');
|
||||
message += `<br><br><div class="bg-blue-50 p-3 rounded mt-2">
|
||||
<span class="font-medium">${i18nStrings.tokenValidFor}</span> ${data.token_info.expires_in_human}
|
||||
<span class="font-medium">${i18nStrings.tokenValidFor}</span> ${escapeHtml(data.token_info.expires_in_human)}
|
||||
</div>`;
|
||||
|
||||
modalTitle.textContent = i18nStrings.testSuccessful;
|
||||
@@ -647,12 +656,12 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
// Create successful message
|
||||
let message = data.message || 'Connection successful';
|
||||
let message = escapeHtml(data.message || 'Connection successful');
|
||||
|
||||
// Add token expiration info if available (especially for Google Drive)
|
||||
if (data.token_info && data.token_info.expires_in_human) {
|
||||
message += `<br><br><div class="bg-blue-50 p-3 rounded mt-2">
|
||||
<span class="font-medium">${i18nStrings.tokenValidFor}</span> ${data.token_info.expires_in_human}
|
||||
<span class="font-medium">${i18nStrings.tokenValidFor}</span> ${escapeHtml(data.token_info.expires_in_human)}
|
||||
</div>`;
|
||||
|
||||
// Show the message with HTML
|
||||
|
||||
@@ -4,14 +4,14 @@
|
||||
# Testing
|
||||
pytest>=8.0.0
|
||||
pytest-cov>=4.1.0
|
||||
pytest-asyncio>=0.23.0
|
||||
pytest-asyncio>=1.4.0
|
||||
pytest-mock>=3.12.0
|
||||
pytest-timeout>=2.3.0 # Per-test timeout enforcement to prevent CI hangs
|
||||
httpx>=0.26.0 # For async test client
|
||||
testcontainers>=3.7.1 # For integration tests with real containers
|
||||
fpdf2>=2.8.0 # For generating test PDF documents in integration tests
|
||||
minio>=7.1.0 # For MinIO/S3 integration tests
|
||||
redis>=4.5.0 # For Redis integration tests
|
||||
redis>=8.0.0 # For Redis integration tests
|
||||
boto3>=1.26.0 # For S3 integration tests
|
||||
|
||||
# Code quality
|
||||
|
||||
+65
-64
@@ -1,66 +1,67 @@
|
||||
fastapi[all]!=0.136.3 # Web framework with all extras
|
||||
uvicorn # ASGI server
|
||||
celery # Task queue
|
||||
redis # Message broker for Celery
|
||||
fastapi[all] # Web framework with all extras
|
||||
uvicorn # ASGI server
|
||||
celery # Task queue
|
||||
redis # Message broker for Celery
|
||||
sqlalchemy # Database ORM
|
||||
psycopg[binary]>=3.2,<4.0 # PostgreSQL driver for HA database deployments
|
||||
pydantic # Data validation
|
||||
cryptography>=41.0.0 # Encryption for sensitive settings in database
|
||||
openai # GPT integration for metadata extraction
|
||||
pypdf>=3.9.0 # PDF processing for text extraction, metadata editing and rotation (upgraded from PyPDF2 to fix CVE-2023-36464)
|
||||
requests # HTTP client
|
||||
click>=8.0.0 # CLI framework for docuelevate command
|
||||
puremagic>=1.25,<2.0 # File type detection (pure Python)
|
||||
filetype>=1.2.0,<2.0 # File type detection fallback (pure Python)
|
||||
dropbox>=11.36.0 # Dropbox integration
|
||||
azure-ai-documentintelligence # Azure OCR service
|
||||
authlib>=1.6.5 # Authentication - fixed security vulnerabilities (GHSA-xxx)
|
||||
python-dotenv # Environment variables
|
||||
starlette>=0.49.1 # ASGI toolkit (used by FastAPI) - fixed DoS vulnerability
|
||||
alembic # Database migrations
|
||||
slowapi>=0.1.9 # Rate limiting middleware for FastAPI
|
||||
|
||||
# Google Drive API
|
||||
google-api-python-client>=2.79.0
|
||||
google-auth>=2.22.0
|
||||
google-auth-oauthlib>=1.0.0
|
||||
|
||||
# OneDrive/Microsoft Graph API
|
||||
msgraph-core>=1.0.0
|
||||
msal>=1.20.0
|
||||
|
||||
# AWS S3
|
||||
boto3>=1.28.0
|
||||
|
||||
# SFTP
|
||||
paramiko>=3.4.0 # SSH/SFTP implementation for Python (LGPL license)
|
||||
|
||||
# iCloud Drive
|
||||
pyicloud>=2.4.0 # Unofficial Apple iCloud API client (MIT license)
|
||||
|
||||
# Evernote
|
||||
evernote3>=1.25.14 # Evernote Cloud API SDK for Python 3 (BSD license)
|
||||
|
||||
# Safe XML parsing (protection against XML bomb / XXE attacks)
|
||||
defusedxml>=0.7.1
|
||||
|
||||
# Notification service
|
||||
apprise>=1.4.0
|
||||
|
||||
# AI provider aggregator - enables Anthropic, Gemini, Ollama, and 100+ LLM providers
|
||||
litellm>=1.0.0,<2.0.0
|
||||
|
||||
# Self-hosted OCR engines (optional – only required when the provider is enabled)
|
||||
pytesseract>=0.3.10 # Python wrapper for Tesseract OCR
|
||||
pdf2image>=1.17.0 # Convert PDF pages to images (used by Tesseract and EasyOCR providers)
|
||||
ocrmypdf>=16.0.0,<18.0.0 # Post-processing: embeds searchable text layers into PDFs via Tesseract
|
||||
meilisearch>=0.31.0 # Full-text search engine client
|
||||
stripe>=7.0.0,<16.0.0 # Stripe billing SDK (MIT license)
|
||||
|
||||
# Error and performance monitoring
|
||||
sentry-sdk[fastapi,celery,sqlalchemy]>=2.20.0,<3.0.0
|
||||
|
||||
# GraphQL API
|
||||
strawberry-graphql[fastapi]>=0.243.0,<1.0.0
|
||||
|
||||
aiofiles>=24.1.0 # Asynchronous file I/O support
|
||||
segno>=1.6.0 # Pure-Python QR code generator (server-side rendering, no Pillow dependency)
|
||||
cryptography>=41.0.0 # Encryption for sensitive settings in database
|
||||
openai # GPT integration for metadata extraction
|
||||
pypdf>=3.9.0 # PDF processing for text extraction, metadata editing and rotation (upgraded from PyPDF2 to fix CVE-2023-36464)
|
||||
requests # HTTP client
|
||||
click>=8.0.0 # CLI framework for docuelevate command
|
||||
puremagic>=1.25,<2.0 # File type detection (pure Python)
|
||||
filetype>=1.2.0,<2.0 # File type detection fallback (pure Python)
|
||||
dropbox>=11.36.0 # Dropbox integration
|
||||
azure-ai-documentintelligence # Azure OCR service
|
||||
authlib>=1.6.5 # Authentication - fixed security vulnerabilities (GHSA-xxx)
|
||||
python-dotenv # Environment variables
|
||||
starlette>=0.49.1 # ASGI toolkit (used by FastAPI) - fixed DoS vulnerability
|
||||
alembic # Database migrations
|
||||
slowapi>=0.1.9 # Rate limiting middleware for FastAPI
|
||||
|
||||
# Google Drive API
|
||||
google-api-python-client>=2.79.0
|
||||
google-auth>=2.22.0
|
||||
google-auth-oauthlib>=1.0.0
|
||||
|
||||
# OneDrive/Microsoft Graph API
|
||||
msgraph-core>=1.0.0
|
||||
msal>=1.20.0
|
||||
|
||||
# AWS S3
|
||||
boto3>=1.28.0
|
||||
|
||||
# SFTP
|
||||
paramiko>=3.4.0 # SSH/SFTP implementation for Python (LGPL license)
|
||||
|
||||
# iCloud Drive
|
||||
pyicloud>=2.4.0 # Unofficial Apple iCloud API client (MIT license)
|
||||
|
||||
# Evernote
|
||||
evernote3>=1.25.14 # Evernote Cloud API SDK for Python 3 (BSD license)
|
||||
|
||||
# Safe XML parsing (protection against XML bomb / XXE attacks)
|
||||
defusedxml>=0.7.1
|
||||
|
||||
# Notification service
|
||||
apprise>=1.4.0
|
||||
|
||||
# AI provider aggregator - enables Anthropic, Gemini, Ollama, and 100+ LLM providers
|
||||
litellm>=1.0.0,<2.0.0
|
||||
|
||||
# Self-hosted OCR engines (optional – only required when the provider is enabled)
|
||||
pytesseract>=0.3.10 # Python wrapper for Tesseract OCR
|
||||
pdf2image>=1.17.0 # Convert PDF pages to images (used by Tesseract and EasyOCR providers)
|
||||
ocrmypdf>=16.0.0,<18.0.0 # Post-processing: embeds searchable text layers into PDFs via Tesseract
|
||||
meilisearch>=0.31.0 # Full-text search engine client
|
||||
stripe>=7.0.0,<16.0.0 # Stripe billing SDK (MIT license)
|
||||
|
||||
# Error and performance monitoring
|
||||
sentry-sdk[fastapi,celery,sqlalchemy]>=2.20.0,<3.0.0
|
||||
|
||||
# GraphQL API
|
||||
strawberry-graphql[fastapi]>=0.243.0,<1.0.0
|
||||
|
||||
aiofiles>=24.1.0 # Asynchronous file I/O support
|
||||
segno>=1.6.0 # Pure-Python QR code generator (server-side rendering, no Pillow dependency)
|
||||
|
||||
Reference in New Issue
Block a user