On this page0
Unfamiliar word? See Glossary: words used in these docs. Looking for the tasks in order? Begin at Start Here.
Reference
Included in: All plans. Which Plan Includes What
Local AI & Model Governance
The AI features across the product (candidate scoring, report review, plain-English search of logs and the cloud map, finding explanations, workflow and compliance drafting) run against a local AI engine (Ollama) on your own instance — the prompt and the data in it are not sent to an outside AI service.
The Three Controls
An administrator has three controls over it: which model is active (limited to a fixed list of approved models, so the setting cannot be used to reach an outside server), a switch that turns AI off for the whole instance, and an optional rule that only people who have been given an explicit AI permission may use it. All three are under Security Settings → AI Model.
Why The Model Is A Locked List, Not A Text Field
“The AI runs locally, nothing leaves your server” is only true if the server cannot be told to fetch a model from somewhere else. A model name can point at an outside server — so a free-text model box would let an administrator (or anyone who took over an administrator account) name a model on an attacker-controlled host, and the moment the instance downloaded it, it would have opened an outbound channel to that host. Zonitix closes that door by only accepting a model from a short pre-approved list. Anything else is refused before it reaches the AI engine, and the refusal is written to the audit log.
Runs On Your Instance
The Model Is Not Free Text
qwen3:0.6b (the default) and qwen2.5:0.5b (the lightest). A value that isn’t on the list — or that carries a host, path, or scheme — is rejected outright and never downloaded.AI Can Be Switched Off
AI Can Be Restricted By Permission
If something here is not working as you expect, start with When something goes wrong.
The plain explanation is above. This part is for IT and developers: the exact settings, permission names, API addresses and data fields behind it. Closed: select the heading above to open it. While it is closed, your browser’s find-on-page (Ctrl+F or Cmd+F) does not search this part.
Setting The Active Model
The active model is a security setting, changed through the same guarded settings endpoint as MFA and access-control policy. Writing it needs the manage_security permission and a fresh step-up. A step-up counts for 15 minutes. The server reads the window from a stored setting, sudoTimeoutMinutes, and holds any value between 1 and 60 minutes, but no screen and no API route writes that setting, so every instance uses 15 minutes. A missing or expired step-up is refused with "error": "SUDO_REQUIRED" in the response body. The status beside it is 400 on most routes, because the gateway turns a 401 from a service behind it into 400. It stays 401 on the routes the gateway passes through unchanged: removing a passkey, the diagnostics bundle, and in Security Lab starting an aggressive scan, changing a scan scope and archiving findings. Key on the error string, not the status. On this route the status is 400. Reading the setting back is broader: manage_security, view_users, or view_dashboard. Each AI feature is gated separately, on the permission of the feature it belongs to (table below).
| Method | Path | Purpose | Auth |
|---|---|---|---|
| POST | /api/v1/auth/settings/security | Set activeAiModel, aiModelRouting, aiEnabled or aiRequireUsePermission (among other security settings). Model values are allowlist-checked before any write; a model change background-pulls the model. | manage_security + step-up (sudo) |
| GET | /api/v1/auth/settings/security | Read current settings incl. activeAiModel (default qwen3:0.6b), aiEnabled and aiRequireUsePermission. | manage_security / view_users / view_dashboard |
| POST | /api/v1/ai/analyze-report | One of the AI feature routes (full list below): stream an AI review of a report. Runs against local Ollama; 400 if reportData is missing. | view_reporting (+ internal M2M token) |
Which Permission Each AI Feature Needs
There is no single “use AI” route. Every AI feature has its own route under /api/v1/ai/, and each checks the permission of the screen it serves, so AI never shows a person data that screen would not. Where two permissions are listed, either one is enough. With Require explicit AI permission on, use_ai is needed as well for every feature route, except for people with the administrator role; anyone else is refused with 403. The two AI Model settings routes (/model-status and /model-capability) are not subject to that rule or to the AI off switch, so they still answer when AI is off.
| AI feature | Route | Permission |
|---|---|---|
| Candidate scoring, résumé field extraction, competencies, interview questions, scorecard calibration, pipeline insights | /score-candidate, /score-candidate-stream, /extract-resume-fields, /generate-competencies, /generate-interview-questions, /calibrate-scorecards, /pipeline-insights | view_recruitment or manage_recruitment |
| Drafting a job description or offer template, the recruiter assistant, rewriting text | /generate-job-description, /generate-offer-template, /chat, /chat-stream, /refine-text | view_recruitment or manage_recruitment |
| Drafting a message to a candidate | /draft-candidate-message | manage_recruitment |
| AI review of a report | /analyze-report | view_reporting |
| Plain-English search of System Logs | /parse-log-query | view_logs |
| Cloud map: plain-English filter, ranked priorities, suggested toxic combinations | /parse-cloud-query, /prioritize-findings, /detect-toxic-combos | view_cloud_infrastructure |
| Explaining a Security Lab or cloud finding | /explain-finding | view_security_findings or view_cloud_infrastructure |
| Drafting a workflow from a description | /draft-workflow | manage_workflows |
| Drafting a justification note for a compliance control | /draft-justification | manage_compliance |
| Model download status and capability check (AI Model settings) | /model-status, /model-capability | manage_security |
The Allowlist Guard
The allowlist is built once from AI_MODELS (a whitespace-separated env list) plus DEFAULT_AI_MODEL. A submitted activeAiModel is accepted only if it is a non-empty string, carries no slash, whitespace, or scheme (so it is a bare name:tag, never host/path:tag or https://…), and is a member of that set. The check runs before any setting is written, so a bad value never touches the settings store or the AI engine. The same guard covers per-task routing (aiModelRouting): the scoring, calibration and justification tasks can each be pointed at a model, and every routed value must be on the same allowlist.
// identity-service — the allowlist and the ref check (verbatim shape):
const ALLOWED_AI_MODELS = new Set(
(process.env.AI_MODELS || 'qwen3:0.6b qwen2.5:0.5b')
.split(/\s+/).map(s => s.trim()).filter(Boolean)
.concat(process.env.DEFAULT_AI_MODEL || 'qwen3:0.6b'));
const isAllowedAiModel = (model) => {
if (typeof model !== 'string' || !model) return false;
// reject any ref with a path, whitespace, or scheme — bare name:tag only
if (/[\/\s]|:\/\//.test(model)) return false;
return ALLOWED_AI_MODELS.has(model);
};
// On failure: audit + reject BEFORE writing anything.
if (activeAiModel !== undefined && !isAllowedAiModel(activeAiModel)) {
await createLog(req, 'AI_MODEL_REJECTED', ...);
return res.status(400).json({ error: 'Unsupported AI model. ...' });
}
// On an accepted CHANGE: fire-and-forget pull into the LOCAL Ollama.
// OLLAMA_URL defaults to the on-network engine — no public egress.
fetch(`${OLLAMA_URL}/api/pull`, { // e.g. http://ollama:11434
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: model, stream: false }),
}); // never blocks the save; only the outcome is logged
How An Inspector Verifies It
- Send a hostile model ref and confirm it is refused. A
POSTwith{ "activeAiModel": "evil.example.com/x/y:tag" }must return 400 with an “Unsupported AI model” error, and the storedactiveAiModel(viaGET) must be unchanged. A scheme (https://…) or a slash in the ref is rejected on shape alone, before the set lookup. - Check the audit trail. Every rejection writes an
AI_MODEL_REJECTEDlog atwarninglevel naming the attempted value — so an attempt to point the model off-box is visible after the fact, not silent. - Confirm the engine is local.
OLLAMA_URLresolves to the compose-internal engine (defaulthttp://ollama:11434). Pulls and inference go to that address; there is no hosted-AI credential and no third-party API host in the AI path. - Confirm the write is step-up gated. A
POSTto/settings/securitywithout a recent step-up returns 400 with"error": "SUDO_REQUIRED", regardless of the model value. - Confirm the off switch is enforced server-side. With
aiEnabledfalse, an AI request returns 503 with{ aiDisabled: true }. WithaiRequireUsePermissiontrue, a non-administrator withoutuse_aigets 403. Both are checked in the AI service itself, not only hidden in the interface. Only an explicit stored “off” disables AI: if the settings lookup itself fails, AI stays available.
Defense In Depth — And What It Is Not
The AI service applies the same allowlist a second time on its own pull-on-miss path, so a model that somehow bypassed the settings check still cannot be auto-fetched from an arbitrary host. What the allowlist does not do: it is not a content filter on the prompt or the model’s output, and it does not encrypt the local Ollama traffic (plain HTTP on the private compose network is by design — TLS terminates at the edge). Its single job is to keep the “which model” choice from becoming an outbound exfiltration channel.