Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7639f5f69d | |||
| 79647d8962 |
@@ -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) ⏸️
|
||||
Goal: allow MSPs to manage multiple client tenants from a single deployment.
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<title>Admin Operations Center</title>
|
||||
<link rel="stylesheet" href="/style.css?v=15" />
|
||||
<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>
|
||||
</head>
|
||||
<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"):
|
||||
response.headers["Content-Security-Policy"] = (
|
||||
"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'; "
|
||||
"connect-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):
|
||||
end = datetime.utcnow()
|
||||
earliest_allowed = end - timedelta(days=_API_MAX_LOOKBACK_DAYS)
|
||||
max_window_start = end - timedelta(hours=_API_MAX_WINDOW_HOURS)
|
||||
|
||||
if since:
|
||||
# Office 365 API expects format without Z
|
||||
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:
|
||||
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")
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user