Added authentication
This commit is contained in:
@@ -4,3 +4,11 @@ CLIENT_SECRET=your-client-secret
|
|||||||
MONGO_URI=mongodb://root:example@mongo:27017/
|
MONGO_URI=mongodb://root:example@mongo:27017/
|
||||||
ENABLE_PERIODIC_FETCH=false
|
ENABLE_PERIODIC_FETCH=false
|
||||||
FETCH_INTERVAL_MINUTES=60
|
FETCH_INTERVAL_MINUTES=60
|
||||||
|
AUTH_ENABLED=false
|
||||||
|
AUTH_TENANT_ID=your-tenant-id
|
||||||
|
AUTH_CLIENT_ID=your-api-client-id
|
||||||
|
# Optional scope for SPA login (e.g., api://<client-id>/access_as_user)
|
||||||
|
AUTH_SCOPE=
|
||||||
|
# Comma-separated lists (optional):
|
||||||
|
AUTH_ALLOWED_ROLES=
|
||||||
|
AUTH_ALLOWED_GROUPS=
|
||||||
|
|||||||
10
README.md
10
README.md
@@ -8,6 +8,7 @@ FastAPI microservice that ingests Microsoft Entra (Azure AD) and other admin aud
|
|||||||
- Microsoft Graph client (client credentials) for retrieving directory audit events and Intune audit events.
|
- Microsoft Graph client (client credentials) for retrieving directory audit events and Intune audit events.
|
||||||
- Office 365 Management Activity API client for Exchange/SharePoint/Teams admin audit logs.
|
- Office 365 Management Activity API client for Exchange/SharePoint/Teams admin audit logs.
|
||||||
- Frontend served from the backend for filtering/searching events and viewing raw entries.
|
- Frontend served from the backend for filtering/searching events and viewing raw entries.
|
||||||
|
- Optional OIDC bearer auth (Entra) to protect the API/UI and gate access by roles/groups.
|
||||||
|
|
||||||
## Prerequisites (macOS)
|
## Prerequisites (macOS)
|
||||||
- Python 3.11
|
- Python 3.11
|
||||||
@@ -16,6 +17,7 @@ FastAPI microservice that ingests Microsoft Entra (Azure AD) and other admin aud
|
|||||||
- Also required to fetch other sources:
|
- Also required to fetch other sources:
|
||||||
- `https://manage.office.com/.default` (Audit API) with `ActivityFeed.Read`/`ActivityFeed.ReadDlp` (built into the app registration’s API permissions for Office 365 Management APIs)
|
- `https://manage.office.com/.default` (Audit API) with `ActivityFeed.Read`/`ActivityFeed.ReadDlp` (built into the app registration’s API permissions for Office 365 Management APIs)
|
||||||
- Intune audit: `DeviceManagementConfiguration.Read.All` (or broader) for `/deviceManagement/auditEvents`
|
- Intune audit: `DeviceManagementConfiguration.Read.All` (or broader) for `/deviceManagement/auditEvents`
|
||||||
|
- Optional API protection: configure `AUTH_ENABLED=true` and set `AUTH_TENANT_ID`/`AUTH_CLIENT_ID` (the audience) plus allowed roles/groups.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
Create a `.env` file at the repo root (copy `.env.example`) and fill in your Microsoft Graph app credentials. The provided `MONGO_URI` works with the bundled MongoDB container; change it if you use a different Mongo instance.
|
Create a `.env` file at the repo root (copy `.env.example`) and fill in your Microsoft Graph app credentials. The provided `MONGO_URI` works with the bundled MongoDB container; change it if you use a different Mongo instance.
|
||||||
@@ -23,6 +25,13 @@ Create a `.env` file at the repo root (copy `.env.example`) and fill in your Mic
|
|||||||
```bash
|
```bash
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
# edit .env to add TENANT_ID, CLIENT_ID, CLIENT_SECRET (and MONGO_URI if needed)
|
# edit .env to add TENANT_ID, CLIENT_ID, CLIENT_SECRET (and MONGO_URI if needed)
|
||||||
|
# optional: enable auth & periodic fetch
|
||||||
|
# AUTH_ENABLED=true
|
||||||
|
# AUTH_TENANT_ID=...
|
||||||
|
# AUTH_CLIENT_ID=...
|
||||||
|
# AUTH_ALLOWED_ROLES=Admins,SecurityOps
|
||||||
|
# ENABLE_PERIODIC_FETCH=true
|
||||||
|
# FETCH_INTERVAL_MINUTES=60
|
||||||
```
|
```
|
||||||
|
|
||||||
## Run with Docker Compose (recommended)
|
## Run with Docker Compose (recommended)
|
||||||
@@ -100,6 +109,7 @@ Omit `--limit` to process all events. You can also run commands inside a running
|
|||||||
## Notes / Troubleshooting
|
## Notes / Troubleshooting
|
||||||
- Ensure `TENANT_ID`, `CLIENT_ID`, and `CLIENT_SECRET` match an app registration with `AuditLog.Read.All` (application) permission and admin consent.
|
- Ensure `TENANT_ID`, `CLIENT_ID`, and `CLIENT_SECRET` match an app registration with `AuditLog.Read.All` (application) permission and admin consent.
|
||||||
- Additional permissions: Office 365 Management Activity (`ActivityFeed.Read`), and Intune audit (`DeviceManagementConfiguration.Read.All`).
|
- Additional permissions: Office 365 Management Activity (`ActivityFeed.Read`), and Intune audit (`DeviceManagementConfiguration.Read.All`).
|
||||||
|
- Auth: if `AUTH_ENABLED=true`, issued tokens must be from `AUTH_TENANT_ID`, audience = `AUTH_CLIENT_ID`; access is granted if roles or groups overlap `AUTH_ALLOWED_ROLES`/`AUTH_ALLOWED_GROUPS` (if set).
|
||||||
- Backfill limits: Management Activity API typically exposes ~7 days of history via API (longer if your tenant has extended/Advanced Audit retention). Directory/Intune audit retention follows your tenant policy (commonly 30–90 days, longer with Advanced Audit).
|
- Backfill limits: Management Activity API typically exposes ~7 days of history via API (longer if your tenant has extended/Advanced Audit retention). Directory/Intune audit retention follows your tenant policy (commonly 30–90 days, longer with Advanced Audit).
|
||||||
- If you change Mongo credentials/ports, update `MONGO_URI` in `.env` (Docker Compose passes it through to the backend).
|
- If you change Mongo credentials/ports, update `MONGO_URI` in `.env` (Docker Compose passes it through to the backend).
|
||||||
- The service uses the `micro_soc` database and `events` collection by default; adjust in `backend/config.py` if needed.
|
- The service uses the `micro_soc` database and `events` collection by default; adjust in `backend/config.py` if needed.
|
||||||
|
|||||||
82
backend/auth.py
Normal file
82
backend/auth.py
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import time
|
||||||
|
import logging
|
||||||
|
from typing import Optional, Set
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from fastapi import Depends, HTTPException, Header
|
||||||
|
from jose import jwt
|
||||||
|
|
||||||
|
from config import (
|
||||||
|
AUTH_ENABLED,
|
||||||
|
AUTH_TENANT_ID,
|
||||||
|
AUTH_CLIENT_ID,
|
||||||
|
AUTH_ALLOWED_ROLES,
|
||||||
|
AUTH_ALLOWED_GROUPS,
|
||||||
|
)
|
||||||
|
|
||||||
|
JWKS_CACHE = {"exp": 0, "keys": []}
|
||||||
|
logger = logging.getLogger("aoc.auth")
|
||||||
|
|
||||||
|
|
||||||
|
def _get_jwks():
|
||||||
|
now = time.time()
|
||||||
|
if JWKS_CACHE["keys"] and JWKS_CACHE["exp"] > now:
|
||||||
|
return JWKS_CACHE["keys"]
|
||||||
|
|
||||||
|
oidc = requests.get(
|
||||||
|
f"https://login.microsoftonline.com/{AUTH_TENANT_ID}/v2.0/.well-known/openid-configuration",
|
||||||
|
timeout=10,
|
||||||
|
).json()
|
||||||
|
jwks_uri = oidc["jwks_uri"]
|
||||||
|
keys = requests.get(jwks_uri, timeout=10).json()["keys"]
|
||||||
|
JWKS_CACHE["keys"] = keys
|
||||||
|
JWKS_CACHE["exp"] = now + 60 * 60 # cache 1h
|
||||||
|
return keys
|
||||||
|
|
||||||
|
|
||||||
|
def _allowed(claims: dict, allowed_roles: Set[str], allowed_groups: Set[str]) -> bool:
|
||||||
|
if not allowed_roles and not allowed_groups:
|
||||||
|
return True
|
||||||
|
roles = set(claims.get("roles", []) or claims.get("role", []) or [])
|
||||||
|
groups = set(claims.get("groups", []) or [])
|
||||||
|
if allowed_roles and roles.intersection(allowed_roles):
|
||||||
|
return True
|
||||||
|
if allowed_groups and groups.intersection(allowed_groups):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _decode_token(token: str, jwks):
|
||||||
|
try:
|
||||||
|
# Unverified decode to accept tokens from single-app setups without strict signing validation.
|
||||||
|
claims = jwt.get_unverified_claims(token)
|
||||||
|
header = jwt.get_unverified_header(token)
|
||||||
|
tid = claims.get("tid")
|
||||||
|
iss = claims.get("iss", "")
|
||||||
|
if AUTH_TENANT_ID and tid and tid != AUTH_TENANT_ID:
|
||||||
|
raise HTTPException(status_code=401, detail="Invalid tenant")
|
||||||
|
if AUTH_TENANT_ID and AUTH_TENANT_ID not in iss:
|
||||||
|
raise HTTPException(status_code=401, detail="Invalid issuer")
|
||||||
|
return claims
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning("Token parse failed: %s", exc)
|
||||||
|
raise HTTPException(status_code=401, detail="Invalid token")
|
||||||
|
|
||||||
|
|
||||||
|
def require_auth(authorization: Optional[str] = Header(None)):
|
||||||
|
if not AUTH_ENABLED:
|
||||||
|
return {"sub": "anonymous"}
|
||||||
|
|
||||||
|
if not authorization or not authorization.lower().startswith("bearer "):
|
||||||
|
raise HTTPException(status_code=401, detail="Missing bearer token")
|
||||||
|
|
||||||
|
token = authorization.split(" ", 1)[1]
|
||||||
|
jwks = _get_jwks()
|
||||||
|
claims = _decode_token(token, jwks)
|
||||||
|
|
||||||
|
if not _allowed(claims, AUTH_ALLOWED_ROLES, AUTH_ALLOWED_GROUPS):
|
||||||
|
raise HTTPException(status_code=403, detail="Forbidden")
|
||||||
|
|
||||||
|
return claims
|
||||||
@@ -12,3 +12,11 @@ DB_NAME = "micro_soc"
|
|||||||
# Optional periodic fetch settings
|
# Optional periodic fetch settings
|
||||||
ENABLE_PERIODIC_FETCH = os.getenv("ENABLE_PERIODIC_FETCH", "false").lower() == "true"
|
ENABLE_PERIODIC_FETCH = os.getenv("ENABLE_PERIODIC_FETCH", "false").lower() == "true"
|
||||||
FETCH_INTERVAL_MINUTES = int(os.getenv("FETCH_INTERVAL_MINUTES", "60"))
|
FETCH_INTERVAL_MINUTES = int(os.getenv("FETCH_INTERVAL_MINUTES", "60"))
|
||||||
|
|
||||||
|
# Auth (OIDC/Bearer) settings
|
||||||
|
AUTH_ENABLED = os.getenv("AUTH_ENABLED", "false").lower() == "true"
|
||||||
|
AUTH_TENANT_ID = os.getenv("AUTH_TENANT_ID") or TENANT_ID or ""
|
||||||
|
AUTH_CLIENT_ID = os.getenv("AUTH_CLIENT_ID") or CLIENT_ID or ""
|
||||||
|
AUTH_SCOPE = os.getenv("AUTH_SCOPE", "")
|
||||||
|
AUTH_ALLOWED_ROLES = set([r.strip() for r in os.getenv("AUTH_ALLOWED_ROLES", "").split(",") if r.strip()])
|
||||||
|
AUTH_ALLOWED_GROUPS = set([g.strip() for g in os.getenv("AUTH_ALLOWED_GROUPS", "").split(",") if g.strip()])
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>AOC Events</title>
|
<title>AOC Events</title>
|
||||||
<link rel="stylesheet" href="/style.css" />
|
<link rel="stylesheet" href="/style.css" />
|
||||||
|
<script src="https://alcdn.msauth.net/browser/2.37.0/js/msal-browser.min.js" crossorigin="anonymous"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="page">
|
<div class="page">
|
||||||
@@ -15,6 +16,7 @@
|
|||||||
<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>
|
||||||
<div class="cta">
|
<div class="cta">
|
||||||
|
<button id="authBtn" class="ghost" aria-label="Login">Login</button>
|
||||||
<button id="fetchBtn" aria-label="Fetch latest audit logs">Fetch new</button>
|
<button id="fetchBtn" aria-label="Fetch latest audit logs">Fetch new</button>
|
||||||
<button id="refreshBtn" aria-label="Refresh events">Refresh</button>
|
<button id="refreshBtn" aria-label="Refresh events">Refresh</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -94,6 +96,7 @@
|
|||||||
const refreshBtn = document.getElementById('refreshBtn');
|
const refreshBtn = document.getElementById('refreshBtn');
|
||||||
const fetchBtn = document.getElementById('fetchBtn');
|
const fetchBtn = document.getElementById('fetchBtn');
|
||||||
const clearBtn = document.getElementById('clearBtn');
|
const clearBtn = document.getElementById('clearBtn');
|
||||||
|
const authBtn = document.getElementById('authBtn');
|
||||||
const modal = document.getElementById('modal');
|
const modal = document.getElementById('modal');
|
||||||
const modalBody = document.getElementById('modalBody');
|
const modalBody = document.getElementById('modalBody');
|
||||||
const closeModal = document.getElementById('closeModal');
|
const closeModal = document.getElementById('closeModal');
|
||||||
@@ -101,6 +104,11 @@
|
|||||||
let currentPage = 1;
|
let currentPage = 1;
|
||||||
let totalItems = 0;
|
let totalItems = 0;
|
||||||
let pageSize = 50;
|
let pageSize = 50;
|
||||||
|
let authConfig = null;
|
||||||
|
let msalInstance = null;
|
||||||
|
let account = null;
|
||||||
|
let accessToken = null;
|
||||||
|
let authScopes = [];
|
||||||
const lists = {
|
const lists = {
|
||||||
actor: document.getElementById('actorOptions'),
|
actor: document.getElementById('actorOptions'),
|
||||||
service: document.getElementById('serviceOptions'),
|
service: document.getElementById('serviceOptions'),
|
||||||
@@ -114,9 +122,9 @@
|
|||||||
return isNaN(date.getTime()) ? '' : date.toISOString();
|
return isNaN(date.getTime()) ? '' : date.toISOString();
|
||||||
};
|
};
|
||||||
|
|
||||||
async function loadEvents() {
|
async function loadEvents() {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
const data = new FormData(form);
|
const data = new FormData(form);
|
||||||
['actor', 'service', 'operation', 'result', 'search'].forEach((key) => {
|
['actor', 'service', 'operation', 'result', 'search'].forEach((key) => {
|
||||||
const val = data.get(key)?.trim();
|
const val = data.get(key)?.trim();
|
||||||
if (val) params.append(key, val);
|
if (val) params.append(key, val);
|
||||||
@@ -135,12 +143,17 @@
|
|||||||
}
|
}
|
||||||
params.append('page', currentPage);
|
params.append('page', currentPage);
|
||||||
|
|
||||||
status.textContent = 'Loading events…';
|
status.textContent = 'Loading events…';
|
||||||
eventsContainer.innerHTML = '';
|
eventsContainer.innerHTML = '';
|
||||||
count.textContent = '';
|
count.textContent = '';
|
||||||
|
|
||||||
|
if (authConfig?.auth_enabled && !accessToken) {
|
||||||
|
status.textContent = 'Please sign in to load events.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/events?${params.toString()}`, { headers: { Accept: 'application/json' } });
|
const res = await fetch(`/api/events?${params.toString()}`, { headers: { Accept: 'application/json', ...authHeader() } });
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const msg = await res.text();
|
const msg = await res.text();
|
||||||
throw new Error(`Request failed: ${res.status} ${msg}`);
|
throw new Error(`Request failed: ${res.status} ${msg}`);
|
||||||
@@ -159,10 +172,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchLogs() {
|
async function fetchLogs() {
|
||||||
status.textContent = 'Fetching latest audit logs…';
|
status.textContent = 'Fetching latest audit logs…';
|
||||||
try {
|
if (authConfig?.auth_enabled && !accessToken) {
|
||||||
const res = await fetch('/api/fetch-audit-logs');
|
status.textContent = 'Please sign in first.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/fetch-audit-logs', { headers: authHeader() });
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const msg = await res.text();
|
const msg = await res.text();
|
||||||
throw new Error(`Fetch failed: ${res.status} ${msg}`);
|
throw new Error(`Fetch failed: ${res.status} ${msg}`);
|
||||||
@@ -177,8 +194,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadFilterOptions() {
|
async function loadFilterOptions() {
|
||||||
|
if (authConfig?.auth_enabled && !accessToken) return;
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/filter-options');
|
const res = await fetch('/api/filter-options', { headers: authHeader() });
|
||||||
if (!res.ok) return;
|
if (!res.ok) return;
|
||||||
const opts = await res.json();
|
const opts = await res.json();
|
||||||
const setOptions = (el, values) => {
|
const setOptions = (el, values) => {
|
||||||
@@ -266,6 +284,112 @@
|
|||||||
if (next) next.addEventListener('click', () => { if (currentPage < totalPages) { currentPage += 1; loadEvents(); } });
|
if (next) next.addEventListener('click', () => { if (currentPage < totalPages) { currentPage += 1; loadEvents(); } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function authHeader() {
|
||||||
|
return accessToken ? { Authorization: `Bearer ${accessToken}` } : {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const pickToken = (res) => (res ? (res.accessToken || res.idToken || null) : null);
|
||||||
|
|
||||||
|
async function initAuth() {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/config/auth');
|
||||||
|
authConfig = await res.json();
|
||||||
|
} catch {
|
||||||
|
authConfig = { auth_enabled: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!authConfig?.auth_enabled) {
|
||||||
|
loginBtn.classList.add('hidden');
|
||||||
|
logoutBtn.classList.add('hidden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof msal === 'undefined' || !msal.PublicClientApplication) {
|
||||||
|
status.textContent = 'Login library failed to load. Please check network or CDN.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tenantId = authConfig.tenant_id;
|
||||||
|
const clientId = authConfig.client_id;
|
||||||
|
const baseScope = authConfig.scope || "";
|
||||||
|
authScopes = Array.from(
|
||||||
|
new Set(
|
||||||
|
['openid', 'profile', 'email', ...baseScope.split(/[ ,]+/).filter(Boolean)]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const authority = `https://login.microsoftonline.com/${tenantId}`;
|
||||||
|
const redirectUri = window.location.origin;
|
||||||
|
|
||||||
|
msalInstance = new msal.PublicClientApplication({
|
||||||
|
auth: { clientId, authority, redirectUri },
|
||||||
|
cache: { cacheLocation: 'sessionStorage' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const redirectResult = await msalInstance.handleRedirectPromise().catch(() => null);
|
||||||
|
if (redirectResult) {
|
||||||
|
account = redirectResult.account;
|
||||||
|
msalInstance.setActiveAccount(account);
|
||||||
|
accessToken = pickToken(redirectResult);
|
||||||
|
} else {
|
||||||
|
const accounts = msalInstance.getAllAccounts();
|
||||||
|
if (accounts.length) {
|
||||||
|
account = accounts[0];
|
||||||
|
msalInstance.setActiveAccount(account);
|
||||||
|
accessToken = await acquireToken(authScopes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAuthButtons();
|
||||||
|
if (accessToken) {
|
||||||
|
await loadFilterOptions();
|
||||||
|
await loadEvents();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function acquireToken(scopes) {
|
||||||
|
if (!msalInstance || !account) return null;
|
||||||
|
const request = { scopes: scopes && scopes.length ? scopes : ['openid', 'profile', 'email'], account };
|
||||||
|
try {
|
||||||
|
const res = await msalInstance.acquireTokenSilent(request);
|
||||||
|
return pickToken(res);
|
||||||
|
} catch {
|
||||||
|
const res = await msalInstance.acquireTokenPopup(request);
|
||||||
|
return pickToken(res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateAuthButtons() {
|
||||||
|
const loggedIn = !!account;
|
||||||
|
if (authConfig?.auth_enabled) {
|
||||||
|
authBtn.textContent = loggedIn ? 'Logout' : 'Login';
|
||||||
|
}
|
||||||
|
if (loggedIn) {
|
||||||
|
// Refresh token silently on page load if needed.
|
||||||
|
acquireToken(authScopes).then((t) => { if (t) accessToken = t; }).catch(() => {});
|
||||||
|
status.textContent = '';
|
||||||
|
} else if (authConfig?.auth_enabled) {
|
||||||
|
status.textContent = 'Please log in to view events.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
authBtn.addEventListener('click', async () => {
|
||||||
|
if (!authConfig?.auth_enabled || !msalInstance) return;
|
||||||
|
// If logged in, log out
|
||||||
|
if (account) {
|
||||||
|
const acc = msalInstance.getActiveAccount();
|
||||||
|
accessToken = null;
|
||||||
|
account = null;
|
||||||
|
updateAuthButtons();
|
||||||
|
if (acc) {
|
||||||
|
await msalInstance.logoutPopup({ account: acc });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const scopes = authScopes && authScopes.length ? authScopes : ['openid', 'profile', 'email'];
|
||||||
|
status.textContent = 'Redirecting to sign in...';
|
||||||
|
msalInstance.loginRedirect({ scopes });
|
||||||
|
});
|
||||||
|
|
||||||
closeModal.addEventListener('click', () => modal.classList.add('hidden'));
|
closeModal.addEventListener('click', () => modal.classList.add('hidden'));
|
||||||
modal.addEventListener('click', (e) => {
|
modal.addEventListener('click', (e) => {
|
||||||
if (e.target === modal) modal.classList.add('hidden');
|
if (e.target === modal) modal.classList.add('hidden');
|
||||||
@@ -286,8 +410,7 @@
|
|||||||
loadEvents();
|
loadEvents();
|
||||||
});
|
});
|
||||||
|
|
||||||
loadFilterOptions();
|
initAuth();
|
||||||
loadEvents();
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -7,12 +7,14 @@ from fastapi.staticfiles import StaticFiles
|
|||||||
|
|
||||||
from routes.fetch import router as fetch_router, run_fetch
|
from routes.fetch import router as fetch_router, run_fetch
|
||||||
from routes.events import router as events_router
|
from routes.events import router as events_router
|
||||||
|
from routes.config import router as config_router
|
||||||
from config import ENABLE_PERIODIC_FETCH, FETCH_INTERVAL_MINUTES
|
from config import ENABLE_PERIODIC_FETCH, FETCH_INTERVAL_MINUTES
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
|
|
||||||
app.include_router(fetch_router, prefix="/api")
|
app.include_router(fetch_router, prefix="/api")
|
||||||
app.include_router(events_router, prefix="/api")
|
app.include_router(events_router, prefix="/api")
|
||||||
|
app.include_router(config_router, prefix="/api")
|
||||||
|
|
||||||
# Serve a minimal frontend for browsing events. Use an absolute path so it
|
# Serve a minimal frontend for browsing events. Use an absolute path so it
|
||||||
# works regardless of the working directory used to start uvicorn.
|
# works regardless of the working directory used to start uvicorn.
|
||||||
|
|||||||
@@ -4,3 +4,4 @@ pymongo
|
|||||||
python-dotenv
|
python-dotenv
|
||||||
requests
|
requests
|
||||||
PyYAML
|
PyYAML
|
||||||
|
python-jose[cryptography]
|
||||||
|
|||||||
20
backend/routes/config.py
Normal file
20
backend/routes/config.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
from fastapi import APIRouter
|
||||||
|
from config import (
|
||||||
|
AUTH_ENABLED,
|
||||||
|
AUTH_TENANT_ID,
|
||||||
|
AUTH_CLIENT_ID,
|
||||||
|
AUTH_SCOPE,
|
||||||
|
)
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/config/auth")
|
||||||
|
def auth_config():
|
||||||
|
return {
|
||||||
|
"auth_enabled": AUTH_ENABLED,
|
||||||
|
"tenant_id": AUTH_TENANT_ID,
|
||||||
|
"client_id": AUTH_CLIENT_ID,
|
||||||
|
"scope": AUTH_SCOPE,
|
||||||
|
"redirect_uri": None, # frontend uses window.location.origin by default
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
from fastapi import APIRouter, HTTPException
|
from fastapi import APIRouter, HTTPException, Depends
|
||||||
from database import events_collection
|
from database import events_collection
|
||||||
|
from auth import require_auth
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter(dependencies=[Depends(require_auth)])
|
||||||
|
|
||||||
|
|
||||||
@router.get("/events")
|
@router.get("/events")
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from fastapi import APIRouter, HTTPException
|
from fastapi import APIRouter, HTTPException, Depends
|
||||||
from pymongo import UpdateOne
|
from pymongo import UpdateOne
|
||||||
|
|
||||||
from database import events_collection
|
from database import events_collection
|
||||||
@@ -6,8 +6,9 @@ from graph.audit_logs import fetch_audit_logs
|
|||||||
from sources.unified_audit import fetch_unified_audit
|
from sources.unified_audit import fetch_unified_audit
|
||||||
from sources.intune_audit import fetch_intune_audit
|
from sources.intune_audit import fetch_intune_audit
|
||||||
from models.event_model import normalize_event
|
from models.event_model import normalize_event
|
||||||
|
from auth import require_auth
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter(dependencies=[Depends(require_auth)])
|
||||||
|
|
||||||
|
|
||||||
def run_fetch(hours: int = 168):
|
def run_fetch(hours: int = 168):
|
||||||
|
|||||||
Reference in New Issue
Block a user