Compare commits

..

2 Commits

Author SHA1 Message Date
tomas.kracmar 7639f5f69d Release v1.7.18: fix Alpine.js SRI + CSP, add frontend modernization roadmap
Release / build-and-push (push) Successful in 1m23s
CI / lint-and-test (push) Successful in 1m22s
- Revert @alpinejs/csp (CSP build has no support for template literals,
  optional chaining, or x-html — all used in the app template); switch
  back to the regular alpinejs build
- Pin Alpine.js to 3.15.12 with a verified SRI hash (replaces the
  floating @3.x.x tag that caused the integrity check failure)
- Restore 'unsafe-eval' to script-src (required by Alpine.js's
  new Function() expression evaluator; inline script was already
  eliminated in v1.7.17 so 'unsafe-inline' stays removed)
- Add Phase 7.5 Frontend Modernization to ROADMAP: Vue 3 + Vite
  migration plan that will allow a clean CSP without unsafe-eval

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 08:01:57 +02:00
tomas.kracmar 79647d8962 Release v1.7.17: Alpine.js CSP build, O365 API window clamping
Release / build-and-push (push) Successful in 1m49s
CI / lint-and-test (push) Successful in 2m6s
2026-05-29 06:44:36 +02:00
6 changed files with 84 additions and 4 deletions
+38
View File
@@ -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
View File
@@ -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: ~23 hours
- Template migration (HTML directives): ~46 hours
- `app.js` → Vue component: ~23 hours
- MSAL integration via npm: ~1 hour
- Testing + polish: ~24 hours
**Total: ~12 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.
+1 -1
View File
@@ -1 +1 @@
1.7.16 1.7.18
+1 -1
View File
@@ -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
View File
@@ -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; "
+13 -1
View File
@@ -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")