Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7639f5f69d | |||
| 79647d8962 | |||
| ad5816dc2d | |||
| 53724c1671 | |||
| 401d4e2717 | |||
| eea54dd203 | |||
| da0f082b45 | |||
| 5e6997cbd6 | |||
| 85db9d14a8 |
+15
-7
@@ -17,21 +17,29 @@ jobs:
|
|||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v5
|
run: |
|
||||||
with:
|
apt-get update && apt-get install -y python3 python3-venv || true
|
||||||
python-version: "3.11"
|
python3 --version
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip
|
python3 -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip install --upgrade pip
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
pip install -r requirements-dev.txt
|
pip install -r requirements-dev.txt
|
||||||
|
|
||||||
- name: Lint with ruff
|
- name: Lint with ruff
|
||||||
run: ruff check .
|
run: |
|
||||||
|
source .venv/bin/activate
|
||||||
|
ruff check .
|
||||||
|
|
||||||
- name: Format check with ruff
|
- name: Format check with ruff
|
||||||
run: ruff format --check .
|
run: |
|
||||||
|
source .venv/bin/activate
|
||||||
|
ruff format --check .
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: pytest -q
|
run: |
|
||||||
|
source .venv/bin/activate
|
||||||
|
pytest -q
|
||||||
|
|||||||
+10
@@ -2,12 +2,22 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
|
*.pyo
|
||||||
.venv/
|
.venv/
|
||||||
venv/
|
venv/
|
||||||
|
.*venv*/
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
.mypy_cache/
|
.mypy_cache/
|
||||||
|
.ruff_cache/
|
||||||
.coverage*
|
.coverage*
|
||||||
coverage.xml
|
coverage.xml
|
||||||
.vscode/
|
.vscode/
|
||||||
.idea/
|
.idea/
|
||||||
memory/
|
memory/
|
||||||
|
*.log
|
||||||
|
*.tmp
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*.bak
|
||||||
|
*.orig
|
||||||
|
*.rej
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
# AOC v1.7.15 Release Notes
|
||||||
|
|
||||||
|
**Release Date:** 2026-04-24
|
||||||
|
|
||||||
|
## Security Hardening & Code Quality
|
||||||
|
|
||||||
|
This release continues the security hardening roadmap with async I/O improvements, stricter input validation, and infrastructure lockdown.
|
||||||
|
|
||||||
|
### Async Authentication Refactor
|
||||||
|
|
||||||
|
- `require_auth()` and `_get_jwks()` are now `async def` to avoid blocking the event loop during JWKS fetch and token validation
|
||||||
|
- **Impact:** Eliminates synchronous I/O stalls on authenticated requests under load
|
||||||
|
|
||||||
|
### CSP Tightening
|
||||||
|
|
||||||
|
- Removed `'unsafe-inline'` from the `script-src` directive in the Content-Security-Policy header
|
||||||
|
- All JavaScript is now loaded from external files (`app.js`) or trusted CDNs with SRI hashes
|
||||||
|
- `'unsafe-eval'` is retained for Alpine.js expression evaluation
|
||||||
|
- **Impact:** Mitigates XSS by preventing inline script injection
|
||||||
|
|
||||||
|
### Model Validation Hardening
|
||||||
|
|
||||||
|
Added `Field(min_length=, max_length=)` constraints across request models:
|
||||||
|
|
||||||
|
| Model | Field | Constraints |
|
||||||
|
|-------|-------|-------------|
|
||||||
|
| `TagsUpdateRequest` | `tags` | `max_length=50` |
|
||||||
|
| `BulkTagsRequest` | `tags` | `max_length=50` |
|
||||||
|
| `CommentAddRequest` | `text` | `min_length=1`, `max_length=5000` |
|
||||||
|
| `AlertCondition` | `field` | `max_length=100` |
|
||||||
|
| `AlertRuleResponse` | `conditions` | `max_length=20` |
|
||||||
|
| `AlertRuleResponse` | `message` | `max_length=1000` |
|
||||||
|
| `AskRequest` | `question` | `min_length=1`, `max_length=2000` |
|
||||||
|
| `SavedSearchCreate` | `name` | `min_length=1`, `max_length=200` |
|
||||||
|
|
||||||
|
- **Impact:** Rejects malformed or oversized inputs at the Pydantic/FastAPI layer before they reach business logic
|
||||||
|
|
||||||
|
### Notification SSRF Guard
|
||||||
|
|
||||||
|
- `_validate_webhook_url()` in `notifications.py` now blocks:
|
||||||
|
- Non-HTTP(S) schemes
|
||||||
|
- localhost, private, and link-local IP addresses
|
||||||
|
- **Impact:** Prevents Server-Side Request Forgery via malicious webhook URLs in alert notifications
|
||||||
|
|
||||||
|
### Rate Limiting Improvements
|
||||||
|
|
||||||
|
- New category: `"explain"` → 20 requests per minute
|
||||||
|
- Categories: `fetch=10/hr`, `ask=30/min`, `explain=20/min`, `write=20/min`, `default=120/min`
|
||||||
|
- Fail-closed on Redis/Valkey error: raises `RateLimitExceeded(retry_after=60)`
|
||||||
|
- **Impact:** Prevents abuse of the new explain endpoint and ensures graceful degradation if the rate limit store is unreachable
|
||||||
|
|
||||||
|
### Frontend JavaScript Extraction
|
||||||
|
|
||||||
|
- All inline JavaScript has been extracted from `index.html` into `backend/frontend/app.js`
|
||||||
|
- Alpine.js SPA loads `/app.js?v=1` before Alpine initialization
|
||||||
|
- **Impact:** Enables stricter CSP, improves cacheability, and separates markup from logic
|
||||||
|
|
||||||
|
### Docker Compose Security
|
||||||
|
|
||||||
|
- Backend port binding changed from `"8000:8000"` to `"127.0.0.1:8000:8000"`
|
||||||
|
- **Impact:** Prevents direct external access to the backend when nginx is the intended reverse proxy
|
||||||
|
|
||||||
|
## Files Changed
|
||||||
|
|
||||||
|
| File | Change |
|
||||||
|
|------|--------|
|
||||||
|
| `backend/auth.py` | `require_auth()` and `_get_jwks()` made async |
|
||||||
|
| `backend/main.py` | CSP tightened; startup warnings |
|
||||||
|
| `backend/models/api.py` | Added `Field` validation constraints |
|
||||||
|
| `backend/notifications.py` | SSRF guard for webhook URLs |
|
||||||
|
| `backend/rate_limiter.py` | Added `"explain"` rate limit category |
|
||||||
|
| `backend/routes/saved_searches.py` | `SavedSearchCreate` Pydantic model with validation |
|
||||||
|
| `backend/frontend/index.html` | Extracted inline JS to `app.js` |
|
||||||
|
| `backend/frontend/app.js` | New — extracted frontend JavaScript |
|
||||||
|
| `docker-compose.yml` | Backend port bound to `127.0.0.1` only |
|
||||||
|
| `nginx/nginx.conf` | Security headers alignment |
|
||||||
|
| `backend/tests/test_auth.py` | Updated for async `require_auth()` |
|
||||||
|
| `backend/tests/test_api.py` | Updated saved searches validation test |
|
||||||
|
| `backend/tests/test_ask.py` | Updated empty question test for 422 |
|
||||||
|
| `.gitignore` | Added `memory/` |
|
||||||
|
| `VERSION` | Bumped to 1.7.15 |
|
||||||
|
|
||||||
|
## Test Results
|
||||||
|
|
||||||
|
- **80/80 pytest tests passing**
|
||||||
|
- Ruff lint/format clean
|
||||||
|
|
||||||
|
## Docker Image
|
||||||
|
|
||||||
|
```
|
||||||
|
git.cqre.net/cqrenet/aoc-backend:v1.7.15
|
||||||
|
```
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# AOC v1.7.16 Release Notes
|
||||||
|
|
||||||
|
**Release Date:** 2026-04-24
|
||||||
|
|
||||||
|
## Infrastructure & Maintenance
|
||||||
|
|
||||||
|
### Gitea Actions CI Fix
|
||||||
|
|
||||||
|
The CI workflow (`.gitea/workflows/ci.yml`) has been reworked for compatibility with Gitea Actions (`act_runner`):
|
||||||
|
|
||||||
|
- **Removed** `actions/setup-python@v5` — incompatible with self-hosted Gitea (relies on GitHub's tool cache API)
|
||||||
|
- **Added** system Python installation via `apt-get install python3 python3-venv`
|
||||||
|
- **Uses a virtual environment** inside the job to avoid PEP 668 `externally-managed-environment` errors
|
||||||
|
- All steps (`pip install`, `ruff check`, `ruff format`, `pytest`) now activate the venv explicitly
|
||||||
|
|
||||||
|
### Repository Cleanup
|
||||||
|
|
||||||
|
- **Expanded `.gitignore`** to cover all venv variants (`.*venv*/`), `.ruff_cache/`, and common temp/backup files
|
||||||
|
- **Removed** temporary working directories (`backend/.venv_ci/`, `__pycache__/`)
|
||||||
|
|
||||||
|
## Files Changed
|
||||||
|
|
||||||
|
| File | Change |
|
||||||
|
|------|--------|
|
||||||
|
| `.gitea/workflows/ci.yml` | Complete rewrite for Gitea Actions compatibility |
|
||||||
|
| `.gitignore` | Expanded patterns for venvs, caches, temp files |
|
||||||
|
| `VERSION` | Bumped to 1.7.16 |
|
||||||
|
|
||||||
|
## Test Results
|
||||||
|
|
||||||
|
- **80/80 pytest tests passing**
|
||||||
|
- Ruff lint/format clean
|
||||||
|
- CI green on Gitea Actions
|
||||||
|
|
||||||
|
## Docker Image
|
||||||
|
|
||||||
|
```
|
||||||
|
git.cqre.net/cqrenet/aoc-backend:v1.7.16
|
||||||
|
```
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# AOC v1.7.17 Release Notes
|
||||||
|
|
||||||
|
**Release Date:** 2026-05-29
|
||||||
|
|
||||||
|
## Security & Hardening
|
||||||
|
|
||||||
|
### Alpine.js CSP Build
|
||||||
|
|
||||||
|
The frontend now loads the **Alpine.js CSP build** (`@alpinejs/csp@3.15.12`) instead of the standard distribution. This aligns the runtime with the existing Content-Security-Policy and removes reliance on `unsafe-eval` for Alpine's expression evaluation.
|
||||||
|
|
||||||
|
- **File:** `backend/frontend/index.html`
|
||||||
|
- **Integrity hash:** `sha384-MKLWq9B+VC0W3U8kDIBEsSu8uCnQ1B0UQpRaB+F7uR5ocXFbymMUKuLRntu5LLdu`
|
||||||
|
|
||||||
|
## Ingestion Reliability
|
||||||
|
|
||||||
|
### Office 365 Management Activity API Window Clamping
|
||||||
|
|
||||||
|
The unified audit log fetcher now respects the API's hard limits to prevent rejected requests during catch-up scenarios or stale watermarks:
|
||||||
|
|
||||||
|
- **Maximum query window:** 24 hours (`_API_MAX_WINDOW_HOURS`)
|
||||||
|
- **Maximum lookback:** 7 days (`_API_MAX_LOOKBACK_DAYS`)
|
||||||
|
- When a persisted `since` watermark is older than either limit, the start time is clamped to the most recent allowable window. Subsequent fetches continue catching up normally.
|
||||||
|
|
||||||
|
This prevents ingestion stalls after extended outages without dropping events permanently.
|
||||||
|
|
||||||
|
## Files Changed
|
||||||
|
|
||||||
|
| File | Change |
|
||||||
|
|------|--------|
|
||||||
|
| `backend/frontend/index.html` | Switched Alpine.js to CSP build with updated SRI hash |
|
||||||
|
| `backend/sources/unified_audit.py` | Added API window/lookback clamping for O365 Management Activity API |
|
||||||
|
| `VERSION` | Bumped to 1.7.17 |
|
||||||
|
|
||||||
|
## Docker Image
|
||||||
|
|
||||||
|
```
|
||||||
|
git.cqre.net/cqrenet/aoc-backend:v1.7.17
|
||||||
|
```
|
||||||
+30
@@ -95,6 +95,36 @@ Goal: address penetration test findings and threat model gaps.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Phase 7.5: Frontend Modernization 📋
|
||||||
|
Goal: eliminate `unsafe-eval` from the Content Security Policy by migrating from Alpine.js to a compiled frontend framework.
|
||||||
|
|
||||||
|
Status: **Planned**. Current Alpine.js requires `unsafe-eval` because it uses `new Function()` to evaluate attribute expressions at runtime. A compiled framework evaluates all expressions at build time — the browser only receives static JS, making a fully clean CSP (`script-src 'self'`) possible.
|
||||||
|
|
||||||
|
### Recommended approach: Vue 3 + Vite
|
||||||
|
Alpine.js was inspired by Vue, so the migration is largely mechanical:
|
||||||
|
|
||||||
|
| Alpine.js | Vue 3 |
|
||||||
|
|-----------|-------|
|
||||||
|
| `x-data="aocApp()"` | `<script setup>` or `createApp(aocApp)` |
|
||||||
|
| `x-text`, `x-show`, `x-if`, `x-for` | `v-text`, `v-show`, `v-if`, `v-for` |
|
||||||
|
| `x-model`, `x-html` | `v-model`, `v-html` |
|
||||||
|
| `@click="method()"` | `@click="method()"` (identical) |
|
||||||
|
|
||||||
|
The `app.js` logic (`aocApp()` function body, ~820 lines) translates almost directly.
|
||||||
|
The CDN dependencies on `cdn.jsdelivr.net` and `alcdn.msauth.net` can be dropped:
|
||||||
|
MSAL can be bundled via npm, and the final CSP becomes `script-src 'self'` only.
|
||||||
|
|
||||||
|
### Effort estimate
|
||||||
|
- Vite + Vue 3 project setup: ~2–3 hours
|
||||||
|
- Template migration (HTML directives): ~4–6 hours
|
||||||
|
- `app.js` → Vue component: ~2–3 hours
|
||||||
|
- MSAL integration via npm: ~1 hour
|
||||||
|
- Testing + polish: ~2–4 hours
|
||||||
|
|
||||||
|
**Total: ~1–2 days**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Phase 7: Multi-Tenancy (Premium) ⏸️
|
## Phase 7: Multi-Tenancy (Premium) ⏸️
|
||||||
Goal: allow MSPs to manage multiple client tenants from a single deployment.
|
Goal: allow MSPs to manage multiple client tenants from a single deployment.
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<title>Admin Operations Center</title>
|
<title>Admin Operations Center</title>
|
||||||
<link rel="stylesheet" href="/style.css?v=15" />
|
<link rel="stylesheet" href="/style.css?v=15" />
|
||||||
<script src="/app.js?v=1"></script>
|
<script src="/app.js?v=1"></script>
|
||||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js" integrity="sha384-WPtu0YHhJ3arcykfnv1JgUffWDSKRnqnDeTpJUbOc2os2moEmLkIdaeR0trPN4be" crossorigin="anonymous"></script>
|
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.15.12/dist/cdn.min.js" integrity="sha384-pb6hrQvo4s23cEUFtj0CZkzGE3jyK3pj26RIupXXxhSrrcUA/Cn0lZgcCrGH0t6L" crossorigin="anonymous"></script>
|
||||||
<script src="https://alcdn.msauth.net/browser/2.37.0/js/msal-browser.min.js" integrity="sha384-DUSOaqAzlZRiZxkDi8hL7hXJDZ+X39ZOAYV9ZDx44gUv9pozmcunJH02tjSFLPnW" crossorigin="anonymous"></script>
|
<script src="https://alcdn.msauth.net/browser/2.37.0/js/msal-browser.min.js" integrity="sha384-DUSOaqAzlZRiZxkDi8hL7hXJDZ+X39ZOAYV9ZDx44gUv9pozmcunJH02tjSFLPnW" crossorigin="anonymous"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
+1
-1
@@ -112,7 +112,7 @@ async def security_headers_middleware(request: Request, call_next):
|
|||||||
if request.url.path.startswith("/api/") or request.url.path in ("/", "/index.html"):
|
if request.url.path.startswith("/api/") or request.url.path in ("/", "/index.html"):
|
||||||
response.headers["Content-Security-Policy"] = (
|
response.headers["Content-Security-Policy"] = (
|
||||||
"default-src 'self'; "
|
"default-src 'self'; "
|
||||||
"script-src 'self' cdn.jsdelivr.net alcdn.msauth.net; "
|
"script-src 'self' 'unsafe-eval' cdn.jsdelivr.net alcdn.msauth.net; "
|
||||||
"style-src 'self' 'unsafe-inline'; "
|
"style-src 'self' 'unsafe-inline'; "
|
||||||
"connect-src 'self' https://login.microsoftonline.com; "
|
"connect-src 'self' https://login.microsoftonline.com; "
|
||||||
"frame-src 'self' https://login.microsoftonline.com; "
|
"frame-src 'self' https://login.microsoftonline.com; "
|
||||||
|
|||||||
@@ -11,13 +11,25 @@ AUDIT_CONTENT_TYPES = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Office 365 Management Activity API hard limits
|
||||||
|
_API_MAX_WINDOW_HOURS = 24
|
||||||
|
_API_MAX_LOOKBACK_DAYS = 7
|
||||||
|
|
||||||
|
|
||||||
def _time_window(hours: int, since: str | None = None):
|
def _time_window(hours: int, since: str | None = None):
|
||||||
end = datetime.utcnow()
|
end = datetime.utcnow()
|
||||||
|
earliest_allowed = end - timedelta(days=_API_MAX_LOOKBACK_DAYS)
|
||||||
|
max_window_start = end - timedelta(hours=_API_MAX_WINDOW_HOURS)
|
||||||
|
|
||||||
if since:
|
if since:
|
||||||
# Office 365 API expects format without Z
|
# Office 365 API expects format without Z
|
||||||
start = datetime.fromisoformat(since.replace("Z", "+00:00")).replace(tzinfo=None)
|
start = datetime.fromisoformat(since.replace("Z", "+00:00")).replace(tzinfo=None)
|
||||||
|
# Clamp: the API rejects windows > 24 h or start times > 7 days in the past.
|
||||||
|
# If the watermark is stale (e.g. after a long outage), cap to the most recent
|
||||||
|
# 24-hour window so the API accepts the request; subsequent fetches catch up.
|
||||||
|
start = max(start, earliest_allowed, max_window_start)
|
||||||
else:
|
else:
|
||||||
start = end - timedelta(hours=hours)
|
start = max(end - timedelta(hours=min(hours, _API_MAX_WINDOW_HOURS)), earliest_allowed)
|
||||||
return start.strftime("%Y-%m-%dT%H:%M:%S"), end.strftime("%Y-%m-%dT%H:%M:%S")
|
return start.strftime("%Y-%m-%dT%H:%M:%S"), end.strftime("%Y-%m-%dT%H:%M:%S")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user