Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d100388c7d | |||
| 11fd87411d | |||
| 6a80bf4eb9 | |||
| 5e02f5a402 | |||
| 0c3e5ec57b |
@@ -16,7 +16,7 @@ jobs:
|
|||||||
run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.cqre.net -u ${{ github.actor }} --password-stdin 2>&1 | grep -v "WARNING! Your credentials are stored unencrypted"
|
run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.cqre.net -u ${{ github.actor }} --password-stdin 2>&1 | grep -v "WARNING! Your credentials are stored unencrypted"
|
||||||
|
|
||||||
- name: Build Docker image
|
- name: Build Docker image
|
||||||
run: docker build ./backend --tag git.cqre.net/cqrenet/aoc-backend:${{ gitea.ref_name }}
|
run: docker build ./backend --build-arg VERSION=${{ gitea.ref_name }} --tag git.cqre.net/cqrenet/aoc-backend:${{ gitea.ref_name }}
|
||||||
|
|
||||||
- name: Push Docker image
|
- name: Push Docker image
|
||||||
run: docker push git.cqre.net/cqrenet/aoc-backend:${{ gitea.ref_name }}
|
run: docker push git.cqre.net/cqrenet/aoc-backend:${{ gitea.ref_name }}
|
||||||
|
|||||||
78
RELEASE_NOTES_v1.2.5.md
Normal file
78
RELEASE_NOTES_v1.2.5.md
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
# AOC v1.2.5 Release Notes
|
||||||
|
|
||||||
|
**Release date:** 2026-04-20
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What's new
|
||||||
|
|
||||||
|
### Natural language query (`/api/ask`)
|
||||||
|
Ask questions in plain English and get AI-generated answers backed by your audit logs.
|
||||||
|
|
||||||
|
- **Regex-based parsing** extracts time ranges (`last 3 days`, `yesterday`, `today`) and entities (`device ABC123`, `user bob@example.com`) without calling an LLM.
|
||||||
|
- **AI narrative summarisation** via any OpenAI-compatible API (OpenAI, Azure OpenAI, MS Foundry, Ollama).
|
||||||
|
- **Graceful fallback** when no LLM is configured — returns a structured bullet list with a clear error banner.
|
||||||
|
- **Cited evidence** — every answer includes the raw events that back it up.
|
||||||
|
|
||||||
|
### Filter-aware queries
|
||||||
|
The ask endpoint now respects the filter panel. When you set **Service = Exchange**, **Result = failure** and ask *"What happened to device X?"*, the LLM only sees failed Exchange events for that device.
|
||||||
|
|
||||||
|
### Scales to thousands of events
|
||||||
|
For large result sets (>50 events), the LLM receives an **aggregated overview** instead of a raw dump:
|
||||||
|
- Counts by service, action, result, and actor
|
||||||
|
- Failure highlights
|
||||||
|
- The 50 most recent raw events as samples
|
||||||
|
|
||||||
|
This keeps token usage low while preserving accuracy.
|
||||||
|
|
||||||
|
### Azure OpenAI / MS Foundry support
|
||||||
|
- Automatic `api-key` header detection for Azure endpoints.
|
||||||
|
- `LLM_API_VERSION` config for Azure `api-version` query parameters.
|
||||||
|
- `max_completion_tokens` support for newer model deployments.
|
||||||
|
|
||||||
|
### Version display
|
||||||
|
- `GET /api/version` endpoint reads the `VERSION` file.
|
||||||
|
- Frontend shows a version badge in the header (e.g., **1.2.5**).
|
||||||
|
|
||||||
|
### Production hardening (from v1.1.0)
|
||||||
|
- Dockerfile runs as non-root user with Gunicorn + Uvicorn workers.
|
||||||
|
- `docker-compose.prod.yml` with internal-only MongoDB, health checks, and nginx reverse proxy.
|
||||||
|
- Security headers (`X-Frame-Options`, `X-Content-Type-Options`, etc.).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Add to your `.env`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Required for AI narrative summarisation
|
||||||
|
LLM_API_KEY=your-key
|
||||||
|
LLM_BASE_URL=https://api.openai.com/v1
|
||||||
|
LLM_MODEL=gpt-4o-mini
|
||||||
|
LLM_MAX_EVENTS=200
|
||||||
|
LLM_TIMEOUT_SECONDS=30
|
||||||
|
LLM_API_VERSION= # set for Azure OpenAI, e.g. 2024-12-01-preview
|
||||||
|
```
|
||||||
|
|
||||||
|
For Azure OpenAI / MS Foundry:
|
||||||
|
```bash
|
||||||
|
LLM_BASE_URL=https://your-resource.openai.azure.com/openai/deployments/your-deployment
|
||||||
|
LLM_API_KEY=your-azure-key
|
||||||
|
LLM_API_VERSION=2024-12-01-preview
|
||||||
|
LLM_MODEL=your-deployment-name
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Upgrade notes
|
||||||
|
|
||||||
|
No breaking changes. Existing `/api/events`, filters, pagination, tags, and comments work unchanged.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Docker image
|
||||||
|
|
||||||
|
```
|
||||||
|
git.cqre.net/cqrenet/aoc-backend:v1.2.5
|
||||||
|
```
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
FROM python:3.11-slim
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
# Bake the version into the image at build time
|
||||||
|
ARG VERSION=unknown
|
||||||
|
ENV VERSION=${VERSION}
|
||||||
|
|
||||||
# Security: run as non-root
|
# Security: run as non-root
|
||||||
RUN groupadd -r aoc && useradd -r -g aoc aoc
|
RUN groupadd -r aoc && useradd -r -g aoc aoc
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
<div class="page" x-data="aocApp()" x-init="initApp()">
|
<div class="page" x-data="aocApp()" x-init="initApp()">
|
||||||
<header class="hero">
|
<header class="hero">
|
||||||
<div>
|
<div>
|
||||||
<p class="eyebrow">Admin Operations Center</p>
|
<p class="eyebrow">Admin Operations Center <span class="version-badge" x-text="appVersion"></span></p>
|
||||||
<h1>Directory Audit Explorer</h1>
|
<h1>Directory Audit Explorer</h1>
|
||||||
<p class="lede">Filter Microsoft Entra audit events by user, app, time, action, and action type.</p>
|
<p class="lede">Filter Microsoft Entra audit events by user, app, time, action, and action type.</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -243,6 +243,7 @@
|
|||||||
actor: '', selectedServices: [], search: '', operation: '', result: '', start: '', end: '', limit: 100, includeTags: '', excludeTags: '',
|
actor: '', selectedServices: [], search: '', operation: '', result: '', start: '', end: '', limit: 100, includeTags: '', excludeTags: '',
|
||||||
},
|
},
|
||||||
options: { actors: [], services: [], operations: [], results: [] },
|
options: { actors: [], services: [], operations: [], results: [] },
|
||||||
|
appVersion: '',
|
||||||
askQuestionText: '',
|
askQuestionText: '',
|
||||||
askLoading: false,
|
askLoading: false,
|
||||||
askAnswer: '',
|
askAnswer: '',
|
||||||
@@ -252,6 +253,7 @@
|
|||||||
askLlmError: '',
|
askLlmError: '',
|
||||||
|
|
||||||
async initApp() {
|
async initApp() {
|
||||||
|
await this.loadVersion();
|
||||||
await this.initAuth();
|
await this.initAuth();
|
||||||
if (!this.authConfig?.auth_enabled || this.accessToken) {
|
if (!this.authConfig?.auth_enabled || this.accessToken) {
|
||||||
await this.loadFilterOptions();
|
await this.loadFilterOptions();
|
||||||
@@ -260,6 +262,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async loadVersion() {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/version');
|
||||||
|
if (res.ok) {
|
||||||
|
const body = await res.json();
|
||||||
|
this.appVersion = body.version || '';
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
},
|
||||||
|
|
||||||
authHeader() {
|
authHeader() {
|
||||||
return this.accessToken ? { Authorization: `Bearer ${this.accessToken}` } : {};
|
return this.accessToken ? { Authorization: `Bearer ${this.accessToken}` } : {};
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -433,6 +433,20 @@ input {
|
|||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.version-badge {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 8px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(125, 211, 252, 0.15);
|
||||||
|
border: 1px solid rgba(125, 211, 252, 0.3);
|
||||||
|
color: var(--accent-strong);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
.ask-events {
|
.ask-events {
|
||||||
margin-bottom: 14px;
|
margin-bottom: 14px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -134,6 +134,13 @@ async def metrics():
|
|||||||
return Response(content=prometheus_metrics(), media_type="text/plain")
|
return Response(content=prometheus_metrics(), media_type="text/plain")
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/version")
|
||||||
|
async def version():
|
||||||
|
import os
|
||||||
|
|
||||||
|
return {"version": os.environ.get("VERSION", "unknown")}
|
||||||
|
|
||||||
|
|
||||||
frontend_dir = Path(__file__).parent / "frontend"
|
frontend_dir = Path(__file__).parent / "frontend"
|
||||||
app.mount("/", StaticFiles(directory=frontend_dir, html=True), name="frontend")
|
app.mount("/", StaticFiles(directory=frontend_dir, html=True), name="frontend")
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ services:
|
|||||||
backend:
|
backend:
|
||||||
build: ./backend
|
build: ./backend
|
||||||
# For production, use the pre-built image instead:
|
# For production, use the pre-built image instead:
|
||||||
# image: git.cqre.net/cqrenet/aoc-backend:v1.1.0
|
# image: git.cqre.net/cqrenet/aoc-backend:v1.2.5
|
||||||
container_name: aoc-backend
|
container_name: aoc-backend
|
||||||
restart: always
|
restart: always
|
||||||
env_file:
|
env_file:
|
||||||
|
|||||||
Reference in New Issue
Block a user