JAVV API¶
This page describes the HTTP API of JAVV. It is for people who write scripts for JAVV or monitor it. The tasks come first. Then the reference tables give each endpoint, error and metric.
The backend also publishes a description of its API, with each parameter and its limits. It is at
/docs and /openapi.json on the backend port, 8000. The frontend does not send these two paths
to the backend.
To push scan results from your own tools, read Push from your own script. To connect the JAVV scanners, read Connect the scanners.
For AI agents
To read or triage the findings for a user, use the MCP server, not these routes: Use the MCP server. Its tools take the parameters of the routes, with the same names and limits.
To write a script, obey these rules:
- Use only the parameter names in
/openapi.json. The backend ignores a query parameter that it does not know, and then the answer is not filtered. - Each read of findings, images, trends, scanners or the audit log needs a
cluster_id. Get it fromGET /api/v1/clusters. - The results of Trivy and Grype stay apart. Do not add them together, and do not merge them.
Sign in from a script¶
A script uses the same address as a browser. The frontend sends /api, /auth and /readyz to
the backend.
Prerequisites:
- bash,
curlandjq. - A JAVV username and password.
Steps:
- Set the address of JAVV:
JAVV=https://javv.example.com - Sign in. The command keeps the session cookie in the file
jar:The reply showsread -rs -p 'Password: ' pw && echo jq -n --arg u admin --arg p "$pw" '{username: $u, password: $p}' \ | curl -sf -c jar -b jar -H 'Content-Type: application/json' --data @- "$JAVV/auth/login"must_change. If it istrue, do step 3. If it isfalse, go to step 4. - Change the password. The new password must have 12 characters or more:
JAVV signs the user out of each session, then writes a new session cookie to
read -rs -p 'New password: ' new && echo jq -n --arg c "$pw" --arg n "$new" '{current_password: $c, new_password: $n}' \ | curl -sf -c jar -b jar -H 'Content-Type: application/json' --data @- "$JAVV/auth/password" unset newjar. - Delete the password from the shell:
unset pw - Send your requests with the cookie. For example, show your user and permissions:
curl -sf -b jar "$JAVV/auth/me" - At the end of the script, sign out. JAVV then ends the session:
curl -sf -b jar -X POST "$JAVV/auth/logout"
Keep the file jar private. Until the session ends, the cookie in it gives access as your user.
Read a list page by page¶
A list endpoint gives one page of rows. List responses shows the three forms of a list. This procedure reads all the findings of one cluster with the cursor.
Prerequisites:
- A session cookie in the file
jar(Sign in from a script).
Steps:
- Find the id of the cluster:
curl -sf -b jar "$JAVV/api/v1/clusters" | jq -r '.clusters[] | "\(.cluster_id) \(.cluster_name)"' - Set the id:
cluster=<cluster_id> - Read each page, 500 rows at a time. Send
next_cursorback ascursoruntil it isnull:cursor= while :; do page=$(curl -sfG -b jar "$JAVV/api/v1/findings" --data-urlencode "cluster_id=$cluster" \ --data-urlencode size=500 ${cursor:+--data-urlencode "cursor=$cursor"}) || break jq -c '.data[] | {cve_id, scanner, severity, state}' <<<"$page" cursor=$(jq -r '.next_cursor // empty' <<<"$page") [ -n "$cursor" ] || break done > findings.jsonl - Compare the number of lines in
findings.jsonlwithtotal.valueof a page. If they are not equal, a page failed. Read the list again.
A cursor stays valid for a short time only. If a page answers 410, start again at step 3.
To stop before the last page, close the cursor. Until then, it keeps one of your open-cursor slots (Exports and reports):
jq -n --arg c "$cursor" '{cursor: $c}' |
curl -sf -b jar -X POST "$JAVV/api/v1/cursors/close" -H 'Content-Type: application/json' -d @-
Scrape the metrics¶
The backend gives its metrics at /metrics, in the Prometheus format.
- This path needs no sign-in.
- The frontend does not send it to the backend.
- Metrics lists each metric.
Prerequisites:
- A Prometheus that can connect to the backend port, 8000.
Steps:
- Let your Prometheus connect to the backend port, 8000:
- with docker compose: publish the port. Use the
portsblock in comments underbackendincompose.yaml(Ports and access). - with Helm: use the backend Service. For a release with the name
javv, it isjavv-backend.
- with docker compose: publish the port. Use the
- Add a scrape of
http://<backend>:8000/metricsto your Prometheus. - Look at the metrics from a machine that can connect to the port:
curl -sf http://<backend>:8000/metrics | grep '^# TYPE javv_'
JAVV gives no alert rules. Make alerts from the metrics that apply to your setup.
Authentication¶
Each endpoint uses one of four kinds of authentication.
| Kind | What the request sends | Endpoints |
|---|---|---|
| None | Nothing | /healthz, /readyz and /metrics. Control the access to these with your network. Also POST /auth/login and POST /auth/logout. |
| Scanner token | Authorization: Bearer <token>. Each token is for one cluster and one scanner. JAVV keeps only a hash of the token. |
The scanner endpoints: ingest, scan scope, scan runs and inventory runs. A token can send data only for its own cluster and scanner. |
| MCP token | Authorization: Bearer <token>. An admin makes the token for one AI agent. JAVV keeps only a hash of the token. |
The MCP endpoint only (The MCP endpoint). An MCP token never passes the check for scanner tokens, and a scanner token never passes the MCP check. |
| Session | The javv_session cookie from POST /auth/login |
All other endpoints |
The session cookie has the HttpOnly and SameSite=Lax flags. It also has the Secure flag,
unless JAVV_SESSION_COOKIE_SECURE is false (http or https).
- Session time: a session ends
JAVV_SESSION_TTL_HOURSafter the sign-in (default 24), or at sign-out. The server decides, not the cookie. - Failed sign-ins: after
JAVV_LOGIN_MAX_ATTEMPTSfailures (default 5) for one username inJAVV_LOGIN_LOCKOUT_MINUTES(default 15), the sign-in answers 429. All cases of a name are one count:Aliceandalicecount together. A successful sign-in sets the count to zero. - One answer for each failure: a wrong username, a wrong password, a disabled user and an expired session all get the same 401. Thus the answer does not show if a username exists.
- The login body:
POST /auth/loginaccepts onlyContent-Type: application/json. A body that is not JSON gets 422. A different JSON type, for exampleapplication/merge-patch+json, gets 415.
A temporary password¶
The first admin, a new user and a user after a password reset have a temporary password. The user
must change it (Sign in from a script, step 3). Until then, the session
can use only /auth/*: show the user, change the password, and sign out. Each other session
endpoint answers a well-formed request with 403 password change required, reads included. A
body that is not valid JSON gets 422 first.
Each refusal logs a password change required warning with the username. It also increments
javv_auth_failures_total{reason="must_change"}.
A password change¶
POST /auth/password changes the password of the user of the session.
- The new password has 12 to 256 characters. Each character counts as one, also a character outside ASCII.
- 401: the current password is wrong, or the session ended. The two get the same answer.
- 403: an identity provider manages the password of the user.
- 422: the new password has fewer than 12 or more than 256 characters, or the body is not valid.
- The audit row comes first. JAVV writes a
pwd_changerow to the audit log before it changes the password. If JAVV cannot write the row, the password does not change, and the answer is an error. - 200 with no cookie: the password changed, but JAVV could not make the new session. Each old
session ended, so sign in again with the new password. JAVV logs a
password changed, no new sessionwarning and incrementsjavv_auth_failures_total{reason="session_not_minted"}.
Roles and permissions¶
Some endpoints need a permission. The tables in Endpoints name it. A user gets the
permissions of its role. The web app uses the capabilities from GET /auth/me to show or hide
its controls. The backend checks the permission again on each request.
At its first start, JAVV creates four roles:
| Role | Permissions |
|---|---|
viewer |
None. The user can read data only. |
triager |
can_triage, can_export, can_edit_dashboards |
security_lead |
can_triage, can_accept_audit_final, can_export, can_edit_dashboards |
admin |
All the permissions, also the permissions of later releases |
| Permission | What it allows |
|---|---|
can_triage |
Triage findings. Make, change and revoke not_affected decisions. |
can_accept_audit_final |
Accept a risk, in a decision or in triage. Make, change and revoke risk_accepted and ignore_rule decisions. Change the state of a risk-accepted finding, also in a bulk triage. Read and export the approvals queue. |
can_export |
Download the export files: the findings CSV and VEX, the audit log CSV, the contributors CSV and the approvals CSV. Make an export report and download it. |
can_edit_dashboards |
Make dashboards. Change, share and delete your own dashboards. |
can_manage_tokens |
Make, revoke and rotate scanner tokens and MCP tokens |
can_manage_users |
Make users. Change their role, their status and their password. Read the account events in the audit log. |
can_manage_settings |
Change the SLA, staleness, retirement and scan scope settings, and the cluster names. Retire a cluster, and bring it back. Read the OpenSearch facts. Start the staleness sweep. Change or delete the saved views of other users. See, change and delete the dashboards of other users, with can_edit_dashboards. Read the dashboard rows in the audit log. |
can_manage_retention |
Read and change the data settings: retention, rollover, export lifetime and findings cleanup. Read and take snapshots. Delete a retired cluster. |
can_restore_snapshot |
Restore a snapshot |
can_inspect_store |
Use the data inspector. Read the status of the background jobs. |
can_rebuild_state |
Start the job that builds the current state again |
can_drop_index |
Start the lifecycle sweep, which deletes old indices |
The Roles card in the web app shows a name and a short text for each permission. It gets them from
GET /api/v1/admin/roles. The backend keeps one list of the permissions, in this order.
can_export controls the export files, not the reads. A user without it can still read each
finding through GET /api/v1/findings, 500 rows a page.
Each read and export applies the cluster that the request names. Each user can read each
cluster. A cluster_id has 8 to 64 lowercase letters, digits or hyphens, and does not start with
a hyphen.
Errors¶
Each answer that is not a success has the same body, with the type application/problem+json:
{"type": "about:blank", "title": "invalid credentials", "status": 401, "detail": null, "request_id": "b169be4a30444fdc"}
type:about:blank, or one of these. Match ontype, not ontitle: the title can change.store-read-onlyon a 503 for a write that OpenSearch refused because its disk is full.cluster-delete-unfinishedon a 409 for a cluster whose delete did not finish: a bring back (POST /api/v1/clusters/{cluster_id}/unretire) or a new token (POST /api/v1/admin/tokens). Delete the cluster again to finish it.expiry-passedon a 422 for a decision whoseexpiryis not after today in UTC, or not after now (POST /api/v1/decisions,PATCH /api/v1/decisions/{decision_id}).
title: a short text that names the error.detail: on a 422 from a field check, the list of the fields that are not valid, with the reason for each. It does not hold the value that you sent. Else it isnull.request_id: the id of the request in the backend log. TheX-Request-IDheader of the answer also has it, on each answer. To set the id yourself, sendX-Request-IDwith 1 to 64 letters, digits or hyphens. If you send no id, or an id that is not valid, the backend makes one.
| Status | Meaning |
|---|---|
| 400 | The ingest body is not valid gzip or JSON. On the other endpoints, a body that is not valid JSON gets 422. |
| 401 | The request has no valid session or token. |
| 403 | The user does not have the permission, or must change the password first. A scanner token sent data for a different cluster or scanner. |
| 404 | The item does not exist. For an item of a different user, the answer is also 404. |
| 409 | A different request changed the item first, or the item is not in the necessary state. |
| 410 | The cursor or the export expired. Do the request again from the start. |
| 413 | The request asks for too many rows, or its body is too large. Make the request smaller. |
| 415 | The sign-in body has a JSON type that is not application/json, or a dashboard import body is not application/x-ndjson. |
| 422 | A field is not valid. detail names the field. |
| 501 | The CSV or VEX export of findings got a past as_of. Make a report with as_of_t instead (A past time: as_of). |
| 429 | Too many requests. Some answers have a Retry-After header with the seconds to wait. The title "The search backend is busy" tells that OpenSearch itself has too much work (a full queue or a circuit breaker). That answer has Retry-After: 5. |
| 503 | OpenSearch is not available. Do the request again later. The type store-read-only, with the title The store is read-only: its disk is full, tells that the disk of OpenSearch is past its flood-stage watermark. Reads work. Each change that OpenSearch refuses, also a sign-in, gets this 503 until OpenSearch has free disk space. An ingest push gets the ingest 503 instead (Ingest a scan). |
List responses¶
A list has one of three forms. The form comes from how the endpoint pages its rows.
| Paging | Form | Endpoints |
|---|---|---|
| Cursor | {"data": [...], "next_cursor": "...", "total": {"value": N, "relation": "eq"}} |
/findings, /audit, /scanners/ingest-failures |
Offset: size and offset |
{"<name>": [...], "total": N} |
/decisions (decisions), /decisions/approvals (approvals), /admin/users (users), /admin/tokens (tokens), /admin/mcp-tokens (tokens) |
| None | {"<name>": [...]} |
/contributors (leaderboard), /images (images), /images/timeline (events), /findings/top-components (components), /findings/top-images (images), /clusters (clusters), /views (views), /dashboards (dashboards), /dashboards/roles (roles), /notifications (items), /admin/jobs (jobs), /admin/roles (roles), /admin/snapshots (snapshots), /scanners/provenance (scanners), /scanners/freshness (scanners) |
/findings/groupsuses a cursor, but it has nototal./notificationsputs its rows initems, not in a key with the name of the resource.- Some answers have more keys next to the list. For example,
/imagesalso hasinventory. - In a cursor list,
totalis an object. In an offset list,totalis a number.
Read a key by its name, and check that it exists. In jq, a key that does not exist gives null,
and the length of null is 0. That looks the same as an empty list.
Endpoints¶
In the tables, the Access column gives the authentication, or the permission that the endpoint needs:
- none: no authentication.
- token: a scanner token.
- session: any signed-in user.
- a permission name: a signed-in user with that permission (Roles and permissions).
The audit log keeps each sign-in and sign-out, and each change to triage, decisions, settings, the scan scope, users, tokens, saved views, dashboards, clusters, snapshots and jobs. It also keeps each query of the data inspector.
Each signed-in user reads the audit log. Only a user with can_manage_users reads the account
events: the rows with entity_type user (sign-ins and sign-outs, password, user and role
changes), token (the scanner tokens) and mcp_tool (the tool calls of the MCP tokens). For
the other users, the three audit endpoints leave these rows out of the list, the counts and the
CSV.
Only a user with can_manage_settings reads the rows with entity_type dashboard. A dashboard
row holds the whole dashboard, also a private one, and these users can see each dashboard. For
the other users, the three audit endpoints leave these rows out in the same way.
System¶
| Method | Path | Access | What it does |
|---|---|---|---|
| GET | /healthz |
none | Liveness. It answers 200 while the process runs. It does not use OpenSearch. |
| GET | /readyz |
none | Readiness. It answers 200 {"status": "ready"} when OpenSearch answers, else 503 {"status": "degraded"}. OpenSearch can answer but refuse writes (its disk is past the flood-stage watermark), or be red (part of its data is not available). Then the answer is 200 with "checks": {"store": "read-only"} or "red". Reads still work, so the backend stays in service. |
| GET | /metrics |
none | The metrics, in the Prometheus format (Metrics) |
| GET | /api/v1/meta |
session | The versions that the backend runs: version (the JAVV release), mapping_version, envelope_versions (the ingest schema versions that it accepts), opensearch_version and python_version. If OpenSearch does not answer, opensearch_version is null. mcp_enabled is true when JAVV_MCP_ENABLED turns on the MCP endpoint. |
The backend also writes the release and the mapping version in its bootstrap complete log line.
Scanner endpoints¶
The scanners use these endpoints. Each one applies to the cluster and the scanner of the token, never to values in the body. Push from your own script gives the order of the calls in a scan cycle.
| Method | Path | Access | What it does |
|---|---|---|---|
| POST | /api/v1/ingest/scan |
token | Accepts the results of one scan (Ingest a scan) |
| GET | /api/v1/scan-scope |
token | Gives the scan scope of the cluster of the token |
| POST | /api/v1/scan-runs |
token | Gives the next scan_order for the cluster and scanner of the token. Each value is larger than the one before. |
| POST | /api/v1/inventory-runs |
token | Closes the inventory of a scan cycle. Body: {scan_run_id, expected_count, started_at}. The backend counts the images that it received. The run is committed only if that count is complete. A repeated call gives the first result again. |
Sign-in and sessions¶
| Method | Path | Access | What it does |
|---|---|---|---|
| POST | /auth/login |
none | Signs in and sets the session cookie. The username can be in any case: the backend looks for the name as typed, then for the name in lower case. A wrong username or password gets 401. Too many failures get 429 (Authentication). |
| POST | /auth/logout |
none | Signs out. It ends the session of the cookie on the server, if the request has a cookie, and deletes the cookie. The answer is 204, also with no session. |
| POST | /auth/password |
session | Changes your password. Body: {current_password, new_password}. It signs you out of each session and sets a new cookie. The rules and the refusals are in A password change. |
| GET | /auth/me |
session | Your user: username, role, capabilities and must_change |
Admin¶
| Method | Path | Access | What it does |
|---|---|---|---|
| GET/POST | /api/v1/admin/tokens |
can_manage_tokens |
GET lists the scanner tokens, newest first, with size (1 to 1000, default 100), offset (0 to 9000) and an optional cluster_id. POST makes a token for {cluster_id, scanner, expiry}. The answer shows the token one time only. A cluster with a delete that did not finish gets 409. A token for a deleted cluster's ID onboards that cluster again: JAVV then keeps its new data. |
| POST | /api/v1/admin/tokens/{token_id}/revoke |
can_manage_tokens |
Disables a token |
| POST | /api/v1/admin/tokens/{token_id}/rotate |
can_manage_tokens |
Makes a new token, with a new id, for the same cluster, scanner and expiry. Then it disables the old token. A revoked token and an expired token get 409, and nothing changes. Make a new token for them. |
| GET/POST | /api/v1/admin/mcp-tokens |
can_manage_tokens |
The tokens of AI agents for the MCP endpoint. GET lists them, newest first, with size (1 to 1000, default 100) and offset (0 to 9000). Each token has name, level, created_by, created_at, expiry, disabled and last_used_at. POST makes a token for {name, level, expiry}. The name has 3 to 64 lower-case letters, digits, dots, underscores or hyphens. The level is read (the default), write or write_risk_accept. The expiry is necessary, in the future and at most 90 days from now. It must have a time zone, for example 2026-12-01T00:00:00Z: an expiry with no time zone gets 422. The answer shows the token one time only. A name that a token has, also a revoked token, gets 409. |
| POST | /api/v1/admin/mcp-tokens/{name}/revoke |
can_manage_tokens |
Disables a token. A revoked token gets 409. An expired token can be revoked. A revoke also stops a rotate that comes at the same time. |
| POST | /api/v1/admin/mcp-tokens/{name}/rotate |
can_manage_tokens |
Gives the token a new secret and the new expiry of {expiry}, with the same rule as a make. The name and the level stay. The old secret stops at once. A revoked token and an expired token get 409, and nothing changes. If a revoke changes the token during the rotate, the rotate gets 409 and the token stays revoked. |
| GET/POST | /api/v1/admin/users |
can_manage_users |
GET lists the users, with size (1 to 1000, default 100) and offset (0 to 9000). POST makes a user with {username, temp_password, role}. A username has 3 to 64 lower-case letters, digits, dots, underscores or hyphens. A username with a capital letter gets 422, and so do system and fleet. A username that exists gets 409. |
| PATCH | /api/v1/admin/users/{username}/role |
can_manage_users |
Changes the role of a user, and signs that user out of each session |
| PATCH | /api/v1/admin/users/{username}/disabled |
can_manage_users |
Disables or enables a user. A disabled user loses each session. The last enabled admin gets 409. |
| POST | /api/v1/admin/users/{username}/password-reset |
can_manage_users |
Sets a temporary password, which the user must change. It signs that user out of each session. |
| GET | /api/v1/admin/roles |
can_manage_users |
The roles and their permissions in roles. Also permissions: each permission with its id, a name and a description of what it allows, in the order of the table in Roles and permissions. |
| GET | /api/v1/admin/snapshots |
can_manage_retention |
The snapshots in the configured repository, newest first. With no repository, configured is false. |
| POST | /api/v1/admin/snapshots |
can_manage_retention |
Starts a snapshot of findings, javv-images-* and system-* except system-sessions, and answers 202 at once. The scan history is not in the snapshot (Snapshots). With no repository, it answers 409. |
| POST | /api/v1/admin/snapshots/{snapshot_name}/restore |
can_restore_snapshot |
Restores a snapshot into copies with the prefix restored-. It never writes to the live indices. To use a copy, move it in by hand. |
| GET | /api/v1/admin/opensearch-runtime |
can_manage_settings |
Facts about OpenSearch: version, health, nodes, roles, heap, discovery.type, path.repo and the security plugin. It gives only these fields. |
| POST | /api/v1/admin/opensearch/inspect |
can_inspect_store |
The data inspector. The data inspector gives its rules. |
| GET | /api/v1/admin/jobs |
can_inspect_store |
The status of each of the eight background jobs, and of the scheduler (The jobs list) |
| POST | /api/v1/admin/jobs/{kind}/run |
the permission of the job | Starts a job, and answers 202 {attempt_id} (Start a background job) |
The data inspector¶
POST /api/v1/admin/opensearch/inspect takes {method, path, body} and sends only allowed reads
to OpenSearch:
GETorPOSTon<index>/_searchand<index>/_countGETon<index>/_mappingGETon_cat/indices,_cat/shards,_cluster/healthand_nodes/stats. The_catreads show only the indices of JAVV.
These requests get 422, with the reason:
- a request on
system-users,system-sessionsorsystem-tokens - a body with
script,script_fields,runtime_mappings,pitorscroll - a
sizelarger thanJAVV_INSPECT_MAX_HITS.
An answer larger than JAVV_INSPECT_MAX_RESPONSE_BYTES gets 413. A good answer is
{took_ms, bytes, cap_bytes, body}. The audit log keeps each query, with the user, the method and
path, and a hash of the query.
The jobs list¶
GET /api/v1/admin/jobs gives {jobs, scheduler}. jobs has one entry for each job kind:
- the last run:
status,requested_by,started_at,finished_at,resultanderror. A job that never ran hasstatus: idle. stale:truewhen a run isrunning, but it stopped its heartbeat.runnableandcapability: if you can start the job withPOST .../run, and the permission for it.capabilityisnullfor a job that you cannot start.schedule: the cron expression fromJAVV_JOB_<KIND>_CRON, ornull.next_run_at: the next run, in UTC. It isnullwhen the job or the scheduler is off.health, one of these values. The first value that applies is the result:failed: the last run failed.off: the job has no schedule, or the scheduler is off.never_ran: the job has a schedule, but no run.overdue: two scheduled times passed after the last start.ok: all other cases.
scheduler is {enabled, timezone}: if this backend runs the schedules, and the time zone of the
cron expressions.
Start a background job¶
POST /api/v1/admin/jobs/{kind}/run starts one of three jobs:
| Kind | Permission |
|---|---|
rebuild_state |
can_rebuild_state |
staleness_sweep |
can_manage_settings |
lifecycle_sweep |
can_drop_index |
- The other five kinds, and a kind that does not exist, get 404.
- If a run of the job is in progress, the request gets 409. The scheduler applies the same rule. Thus one run of a kind at a time occurs.
?dry_run=trueapplies tolifecycle_sweeponly. It answers 200{dry_run, result: {rolled, dropped, errors}}with the counts that a run would give. It changes nothing. On the other kinds,dry_rungets 422.
Findings: read¶
These endpoints need a session only. Each one needs cluster_id, except
POST /api/v1/cursors/close.
| Method | Path | Access | What it does |
|---|---|---|---|
| GET | /api/v1/findings |
session | One page of findings, with a cursor (Read a list page by page). size is 1 to 500, default 50. sort is severity_rank, first_seen_at, last_scan_at, cvss or epss. The last page closes the search of the cursor. With keep_open=true, a page read with a cursor keeps it open after the last page, so you can read an earlier page again. Then close it with POST /api/v1/cursors/close. |
| GET | /api/v1/findings/facets |
session | The count of findings for each value of a field, for each scanner. fields is one or more of severity, state, scanner, fixable, kev, disagree, present, ptype, namespaces, assignee, overdue and unassigned. For namespaces and assignee, it gives the 32 largest values. The namespaces, ptype and assignee facets count without the filter of their own field. Thus a value that you pick does not take the other values out of the list. The unassigned facet counts without the assignee values, and it keeps exclude_assignee. On the Findings screen, a click on Unassigned clears those values, so the count is the result of the click. All other facets count with all filters. With all_filters=true, each facet counts with all filters, also the filter of its own field. A dashboard panel uses this, so that "Namespace is not kube-system" takes kube-system out of its list. contains is a value search, with 2 to 128 characters. It needs exactly one of namespaces, ptype and assignee in fields. It gives the values of that field that contain the text, in any case. It searches all values, not only the 32 largest. The counts use the same filters as the list. It gives at most 32 values, and more is true when more values contain the text. A contains with another field, more than one field or no field gives 422. At a past time, only ptype takes contains, because the scan history has no namespaces or assignee facet. |
| GET | /api/v1/findings/groups |
session | Findings in groups, page by page with a cursor. by is image_repo, image_digest, namespaces, cve_id, assignee, app or ptype. |
| GET | /api/v1/findings/top-components |
session | The packages with the most findings, and the count of different CVEs for each scanner. A past as_of gets 422. |
| GET | /api/v1/findings/top-images |
session | The running images with the most critical findings, then high, medium and low findings, with the filters of GET /api/v1/findings. A dashboard panel of Riskiest images with filters uses it. It gives at most 100 images. An image with none of these four severities is not in the list. Each row has image_digest, image_repo, tag and namespaces, as on GET /api/v1/images, and the count of each severity for each scanner in severity_by_scanner. The rank adds the counts of the scanners. Use scanner to rank by one scanner. inventory is null when JAVV has no committed inventory. A past as_of reads the scan history, and the filters that the history does not keep get 422. The images go in one terms query, so the read works for at most 65,536 running images (index.max_terms_count). OpenSearch reads each shard in one pass, so the 100 images are the 100 with the highest rank. JAVV_EXACT_RANKING sets this. |
| GET | /api/v1/trends/scans · /api/v1/trends/findings |
session | Counts for each day over days (1 to 365, default 30), for each scanner. Trends gives the details. |
| GET | /api/v1/trends/ingest-failures |
session | The pushes that the ingest refused after the token check, for each time interval and scanner (Trends) |
| GET | /api/v1/contributors |
session | The triage work of each user over days (1 to 365, default 30): actions, time to fix and SLA results. totals gives the same values for the team. |
| GET | /api/v1/contributors/export.csv |
can_export |
The data of /contributors as CSV. It has one action_<name> column for each triage action. A file larger than JAVV_EXPORT_MAX_ROWS rows gets 413. |
| GET | /api/v1/scanners/freshness |
session | For each scanner of the cluster: last_ingest_at and silent_for_seconds. With no push yet, both are null. |
| GET | /api/v1/scanners/ingest-failures |
session | The pushes that the ingest refused after the token check, newest first. scanner is necessary. days is 1 to 365 (default 30), and size is 1 to 100 (default 25). Failed pushes gives the fields. |
| GET | /api/v1/scanners/provenance |
session | For each scanner: the versions and effective_config of the last committed run, and the last runs runs (1 to 50, default 10). findings_total of a run counts findings. A run that JAVV stored before release 0.9.5 counts the entries of the scanner reports. |
| GET | /api/v1/audit |
session | The audit log, page by page with a cursor. Filters: entity_type, action, actor and finding_key, each with an exclude_ form except finding_key. entity_type, action and actor take several values, for example actor=a&actor=b, at most 32 for each filter. A row matches when it has one of the values. An exclude_ filter with several values leaves out the rows with any of them. finding_key also returns each bulk change that included the finding (action bulk_triage). Such a row lists the changed findings in target_ids. order is desc (default) or asc. keep_open is as in /findings. Each row also shows the CVE, image and scanner of the finding or decision that it changed. |
| POST | /api/v1/cursors/close |
session | Closes a cursor of /findings or /audit before its last page. Body: {cursor}. It closes the search of the cursor and frees your slot (Exports and reports), and answers 204. A cursor of a different user, of a list that ended, or that expired also gets 204, and nothing changes. A cursor that is not valid gets 422. |
| GET | /api/v1/audit/facets |
session | The counts of entity_type, action and actor, with the same filters. The actor counts leave out the actor and exclude_actor filters, so the other users keep their counts. With interval (day or hour) and window_days, it also gives activity: the events in each interval. contains is a value search on the actor values, with 2 to 128 characters. It gives the users that contain the text, in any case, from all users and not only the 32 with the most events. The counts use the same filters as the actor counts. It gives at most 32 values, and more is true when more users contain the text. It gives no entity_type, action or activity. contains with interval gives 422. |
| GET | /api/v1/audit/export.csv |
can_export |
The audit log as CSV, with the same filters. A file larger than JAVV_EXPORT_MAX_ROWS rows gets 413. |
| GET | /api/v1/images |
session | The running images: the images of the last committed inventory. It also shows images with no findings. If JAVV has no committed inventory, inventory is null. The severity counts of an image count findings, as the counts of a scan in /images/timeline do. An image that JAVV scanned before release 0.9.5 counts the entries of the scanner report until its next scan. |
| GET | /api/v1/images/timeline |
session | The scan history of one image tag. image_repo and tag are necessary. total of a scan counts findings. A scan that JAVV stored before release 0.9.5 counts the entries of the scanner report. |
| GET | /api/v1/clusters |
session | The clusters (The clusters list) |
Filters¶
/findings, /findings/facets, /findings/groups, the findings exports and the saved views take
the same filters:
| Filter | What it selects |
|---|---|
severity |
critical, high, medium, low, negligible or unknown. The facet uses the same words. A row also shows the word of its scanner in severity. |
state |
open, acknowledged, not_affected, risk_accepted, resolved or stale |
scanner, image_digest |
The rows with that value |
namespace, ptype, assignee, image_repo, cve_id, package_name |
The rows with one of the values. Give the parameter one time for each value, for example namespace=a&namespace=b. Each filter takes at most 32 values. A row in more than one namespace matches each of its namespaces. |
kev, fixable, disagree |
true or false |
present |
true (default): the findings in the last scan. false: the findings that are gone. |
new_within_days |
The findings first seen in the last 1 to 365 days |
overdue |
true or false, from the SLA policy at the time of the request. A change of the policy changes the result at once. |
unassigned |
true: the rows with no owner. An empty assignee counts as no owner. |
q |
2 to 128 characters. It finds the rows where cve_id, image_repo, namespaces, assignee or package_name contains the text, in any case. |
- Each filter except
kev,fixable,disagree,present,new_within_days,overdue,unassigned,image_digestandqhas anexclude_form, for exampleexclude_severity. Anexclude_filter leaves out the rows with that value. A row with no value for the field stays. Thusexclude_assignee=bobkeeps the rows with no owner. Anexclude_filter with more than one value leaves out the rows with any of the values. package_nameis not a facet.fields=package_namegets 422.- In a saved view (
preset) and in a report (params), the filters that take more than one value (namespace,ptype,assignee,image_repo,cve_id,package_nameand theirexclude_forms) also take one string. JAVV keeps it as a list with that one value. - One field takes the filter or its
exclude_form, not both.assigneeandunassignedalso do not go together. Each of these pairs gets 422, also in a saved view and in a report.
A past time: as_of¶
Each read takes as_of. With no value, now or a time in the future, the read shows the current
state. With a time in the past, JAVV builds the state at that time from the history:
kev,epss,disagree,image_repo,tagandappare not in the history. They arenullin the rows.- The SLA deadline (
overdueanddue_at) uses the KEV value that each scan recorded, as a read now does. A scan that JAVV received before release 0.9.1 has no KEV value in the history. A finding from such a scan gets the deadline of its severity. - A filter, sort or group on one of these fields gets 422.
image_repoandexclude_image_repoget 422 too. - A facet on one of these fields gives no values.
ptype,package_nameand theexclude_filters on recorded fields apply as usual.- The CSV and VEX exports of findings get 501. A report (
POST /api/v1/reports) can use a pastas_of_t. - The audit log and the failed pushes end their window at that time.
Trends¶
/trends/scans: the committed scans.intervalisday(default) orhour.houris possible only for 31 days or less, else 422. At a pastas_of, the intervals are days./trends/findings: the new findings and the resolved findings.resolvedcounts only the findings that a scan no longer showed (resolved_semantics: "scan_resolved"). A triage toresolveddoes not count.splitisscanner(default) orseverity.split=severitygets 422 at a pastas_of.scanner(trivyorgrype) limits the series to one scanner./trends/ingest-failures: the samedaysandintervalas/trends/scans, so the two series align. It gives{series: {<scanner>: [{date, count}]}, days, interval}. With no refused push,seriesis{}.
Failed pushes¶
GET /api/v1/scanners/ingest-failures gives {data, total, next_cursor}. Each row has
@timestamp, failure_id, scanner, stage, reason, status, error and image_ref. The
ingest rejected log line has the same failure_id. The read uses no point in time. Thus a push
that fails during your read can show on a later page.
The clusters list¶
GET /api/v1/clusters gives each cluster that has a token, a name or a retirement record.
cluster_name is a name to show. JAVV never uses it in a query. Without a name, it is the
cluster_id. Each row has:
retired. A retired cluster is in the list only with?include_retired=true. A cluster that sent a scan after its retirement is not retired.last_scan_at: the newest accepted scan, ornull.silent_since: the start of the retirement countdown. This is the newest accepted scan, else the first token, or the return from retirement if that is later.warns_atandretires_at.nullmeans that the cluster never retires.delete_started:truewhen a delete of the cluster started and did not finish. Delete it again.retirement_mode:manualorautofor a retired cluster, elsenull. A manual retirement revoked the tokens. To bring the cluster back, make a new token.
Findings: triage and decisions¶
| Method | Path | Access | What it does |
|---|---|---|---|
| PATCH | /api/v1/findings/{finding_key}/triage |
can_triage |
Changes the state of one finding: open, acknowledged, not_affected, risk_accepted or resolved. not_affected needs a vex_justification. risk_accepted also needs can_accept_audit_final. A change of state of a risk-accepted finding also needs can_accept_audit_final. This includes a stale finding that goes back to risk_accepted when the scanner reports it again. A change of notes or assignee only does not need it. |
| POST | /api/v1/findings/bulk-triage |
can_triage |
Changes the state of all the findings that a selector finds, now. More than JAVV_BULK_INLINE_LIMIT (5000) findings get 413. Use a report for more. A selector that finds more than JAVV_BULK_MAX_TARGETS (10000) gets 413. An empty selector gets 422. A change of state of a set that holds risk-accepted findings needs can_accept_audit_final. Without it, the request gets 403 with the count, and JAVV changes no finding. |
| POST | /api/v1/decisions |
can_triage |
Makes a decision. A risk_accepted or ignore_rule decision also needs can_accept_audit_final. expiry is a date (YYYY-MM-DD), or a date and time with a time zone. JAVV keeps a date and time in UTC. A date stops at 00:00 UTC on that day. Thus the date must be after today in UTC, and a date and time must be after now. Else the answer is 422. |
| GET | /api/v1/decisions |
session | Lists the decisions of the cluster, with cve_id, include_revoked (default false), size (1 to 500, default 50) and offset (0 to 10000) |
| PATCH | /api/v1/decisions/{decision_id} |
can_triage |
Changes a decision. JAVV never changes a decision. It revokes the old one and makes a new one, at the same time. A change from or to risk_accepted or ignore_rule also needs can_accept_audit_final. The new decision keeps the old expiry when the change does not set one. When that expiry is not in the future, the answer is 422, as for POST. |
| POST | /api/v1/decisions/{decision_id}/revoke |
can_triage |
Revokes a decision. Its effect on the findings stops. A risk_accepted or ignore_rule decision also needs can_accept_audit_final. |
| GET | /api/v1/decisions/approvals |
can_accept_audit_final |
The approvals queue: the active risk acceptances, the nearest expiry first (The approvals queue) |
| GET | /api/v1/decisions/approvals/export.csv |
can_accept_audit_final and can_export |
The approvals queue as CSV, with the same filters (The approvals queue) |
The approvals queue¶
GET /api/v1/decisions/approvals takes size and offset, and these filters:
q: a part of the CVE id.status:active,expiring,expiredoropen-ended. JAVV calculates it fromexpiryat the time of the request.warn_days(default 7) sets whenexpiringstarts.created_by.scanner:both,trivyorgrype.
status and created_by take several values, for example status=expired&status=expiring, at
most 32 for each filter. An acceptance matches when it has one of the values. q and scanner
take one value. Each filter except q has an exclude_ form. An exclude_ filter with several
values leaves out the acceptances with any of them. The answer also has facets: the counts of
status, created_by and scanner with the same filters. The created_by counts leave out the
created_by and exclude_created_by filters, so the other approvers keep their counts. A revoked acceptance is never in the
queue.
contains is a value search on the created_by values, with 2 to 128 characters. It gives the
approvers that contain the text, in any case, from all approvers and not only the 32 with the most
acceptances. The counts use the same filters and warn_days as the created_by counts. The answer
has only facets.created_by, with at most 32 values, and more. more is true when more
approvers contain the text. The answer has no rows and no total.
The CSV export has the same rows. A file larger than JAVV_EXPORT_MAX_ROWS rows gets 413.
Two columns are different from the screen:
- JAVV calculates
statusfor each row, with the same rules as thestatusfilter. scope_namespacesandscope_imagesgive the full lists. If both are empty, the acceptance applies to the full cluster.
Settings¶
| Method | Path | Access | What it does |
|---|---|---|---|
| GET | /api/v1/settings/sla |
session | The SLA policy: critical_days, high_days, medium_days, low_days and kev_days |
| PUT | /api/v1/settings/sla |
can_manage_settings |
Replaces the SLA policy |
| GET | /api/v1/settings/staleness |
session | The staleness timers for ?cluster_id: the value for the cluster, else the value for all clusters. per_cluster_override shows which one applies. |
| PUT | /api/v1/settings/staleness |
can_manage_settings |
Replaces the staleness timers. With cluster_id in the body, for that cluster. Without it, for all clusters. |
| GET | /api/v1/settings/retirement |
session | The retirement window for ?cluster_id: retire_after_days (null means never) and warn_days, with per_cluster_override |
| PUT | /api/v1/settings/retirement |
can_manage_settings |
Sets retire_after_days and warn_days. Both are necessary. cluster_id in the body applies as for staleness. A window that is not longer than the scanner-down timer gets 422. A warn_days that is not shorter than the window gets 422. |
| GET | /api/v1/settings/scan-scope |
session | The scan scope of ?cluster_id |
| PUT | /api/v1/scan-scope |
can_manage_settings |
Replaces the scan scope of a cluster: include_namespaces, ignore_namespaces, exclude_images and ignore_kinds. An empty include list means all namespaces. An ignore entry wins over an include entry. |
| GET | /api/v1/settings/data |
can_manage_retention |
The data settings for ?cluster_id: retention, rollover, export lifetime, findings cleanup and the snapshot repository |
| PUT | /api/v1/settings/retention |
can_manage_retention |
Sets retention_days. cluster_id in the body applies as for staleness. |
| PUT | /api/v1/settings/rollover |
can_manage_retention |
Sets max_age_days, max_docs and max_size_gb. cluster_id in the body applies as for staleness. |
| PUT | /api/v1/settings/report-ttl |
can_manage_retention |
Sets the export lifetime in hours, for all clusters |
| PUT | /api/v1/settings/findings-cleanup |
can_manage_retention |
Sets cleanup_days: how long a finding that is gone stays. cluster_id in the body applies as for staleness. |
Configuring JAVV gives the defaults and the limits of each runtime setting.
Clusters¶
| Method | Path | Access | What it does |
|---|---|---|---|
| PUT | /api/v1/clusters/{cluster_id}/name |
can_manage_settings |
Changes the name that JAVV shows for a cluster. The cluster_id never changes. |
| POST | /api/v1/clusters/{cluster_id}/retire |
can_manage_settings |
Retires a cluster. It leaves the cluster list, and JAVV revokes its tokens. JAVV keeps its data. A cluster that does not exist gets 404. A retired cluster gets 409. |
| POST | /api/v1/clusters/{cluster_id}/unretire |
can_manage_settings |
Brings a retired cluster back to the list. Its tokens stay revoked. The retirement countdown starts again. A cluster that is not retired gets 409. A cluster with a delete that did not finish gets 409. |
| DELETE | /api/v1/clusters/{cluster_id} |
can_manage_retention |
Deletes a retired cluster (Delete a cluster) |
Delete a cluster¶
DELETE /api/v1/clusters/{cluster_id} deletes all the data of a retired cluster, except its rows
in the audit log. Snapshots from before keep the data.
- It writes a marker for the delete.
- It deletes the tokens of the cluster. A scanner that still pushes gets 401.
- It deletes the history indices of the cluster.
- It deletes the rows of the cluster in
findings,javv-scan-watermarks,javv-scan-orders,system-decisions,system-notificationsandsystem-reports. - It deletes the settings and the name of the cluster, then its retirement record.
-
It marks the delete as finished.
-
A cluster that is not retired gets 409. A cluster that does not exist gets 404.
- If an admin brings the cluster back while the delete starts, only one of the two goes ahead. When the delete loses, it gets 409 and deletes nothing. When the bring back loses, it gets 409, and the delete continues.
- If a step cannot finish, or OpenSearch is not available, the request gets 503. Send the request again. The delete continues from the step that stopped.
- The answer is
{cluster_id, deleted: {<what>: count}}. - The marker stays. Each night, the retirement sweep deletes the rows that a push wrote after the delete. Before it deletes them, it writes an audit row with their counts. After a second pass with no rows, or if the cluster comes back, the sweep deletes the marker. A new token for the cluster's ID also deletes the marker: the cluster is back, and JAVV keeps its new data.
- A second delete of the same ID, after the cluster came back, writes its own marker.
Saved views¶
| Method | Path | Access | What it does |
|---|---|---|---|
| GET | /api/v1/views |
session | Lists the saved views. Each user can see each view. |
| POST | /api/v1/views |
session | Saves a view. You are its owner. preset takes the filters. A value that is not valid gets 422, and so does a pair that Findings refuses. workbench keeps the table setup: columns, dense, sort, order and window_days. |
| PATCH | /api/v1/views/{view_id} |
session, the owner or can_manage_settings |
Changes the name, the description, preset or workbench. A preset gets the same checks as on POST. A different user gets 403. A change by a different request first gets 409. |
| DELETE | /api/v1/views/{view_id} |
session, the owner or can_manage_settings |
Deletes a view (204). The audit log keeps a copy of it. |
Dashboards¶
A dashboard is a layout of panels. It holds no data: each panel gets its data with the permissions of the user who opens the dashboard.
| Method | Path | Access | What it does |
|---|---|---|---|
| GET | /api/v1/dashboards |
session | Lists the dashboards that you can see, at most 1,000, by name |
| GET | /api/v1/dashboards/{dashboard_id} |
session | One dashboard. A dashboard that you cannot see gets 404, as a dashboard that does not exist. |
| POST | /api/v1/dashboards |
can_edit_dashboards |
Saves a new dashboard (201). You are its owner. It is private if the body does not set sharing. A name in roles that is not the name of a role gets 422 (unknown_role). |
| PATCH | /api/v1/dashboards/{dashboard_id} |
can_edit_dashboards, the owner or can_manage_settings |
Changes the fields in the body. The body must have the revision that you read. A different revision gets 409. A user who can see the dashboard, but is not its owner, gets 403. To stop the sharing with roles, send the new sharing and "roles": [] together. A change that sends no panels keeps the stored panels. A name in roles that is not the name of a role gets 422 (unknown_role). A change that sends no roles keeps the stored roles, and does not examine them. |
| DELETE | /api/v1/dashboards/{dashboard_id} |
can_edit_dashboards, the owner or can_manage_settings |
Deletes the dashboard (204). The query parameter revision is the revision that you read. A different revision gets 409. The audit log keeps a copy of it. |
| GET | /api/v1/dashboards/roles |
can_edit_dashboards |
The names of the roles that you can share a dashboard with, sorted: {"roles": ["admin", ...]}. It gives no capabilities. /api/v1/admin/roles gives them and needs can_manage_users. |
| GET | /api/v1/dashboards/export.ndjson |
session | A file of the dashboards that you can see, at most 1,000. Give one dashboard_id for each dashboard, or no dashboard_id for all of them. See "Dashboard files". |
| POST | /api/v1/dashboards/import |
can_edit_dashboards |
Makes a dashboard from each line of a file (200, with a result for each line). With copy=true, each line gets a new ID. See "Dashboard files". |
Who can see a dashboard:
sharing: private: the owner onlysharing: everyone: each usersharing: roles: the users with one of the roles inroles- a user with
can_manage_settings: each dashboard
The fields that you set:
| Field | Values |
|---|---|
name |
1 to 128 characters |
description |
At most 1,024 characters |
scanner |
all (default), trivy or grype: the scanner that the dashboard opens with |
sharing |
private (default), everyone or roles |
roles |
The roles that can see the dashboard, with sharing: roles only. At least one. |
panels |
At most 24 panels |
Each panel:
| Field | Values |
|---|---|
panel |
The panel ID, for example findings-summary. openapi.json lists the IDs. |
x |
The column: 0 (left) or 1 (right) |
y |
The row of the top of the panel: 0 to 1,000 |
w |
The width: 1 (one column) or 2 (both columns). x + w is at most 2. |
h |
The height: short or tall. Each panel has the heights that its catalog entry gives. |
scanner |
dashboard (default), trivy or grype, only on a panel with scanner data. dashboard follows the scanner switch of the dashboard. |
filters |
The filters of GET /api/v1/findings, with the fields and checks of the preset of a saved view. Only on the panels that read findings counts: findings-summary, package-type, findings-by-namespace, riskiest-images, top-components and triage-progress. |
audit_filters |
The Action and User filters of GET /api/v1/audit: action, actor, exclude_action and exclude_actor, each a list of at most 32 values. An action is one of the ten triage actions, in lower case: acknowledge, not_affected, risk_accept, resolve, reopen, assign, note, bulk_triage, decision_create and decision_revoke. A user has at most 128 characters. Only on triage-activity. The panel sets the Entity filter itself. |
The backend refuses with 422:
- a field that is not in these lists, or a panel ID that it does not know
- a height that the panel does not have, or a
scanneron a panel with no scanner data filterson a panel that does not read findings countsscannerorexclude_scannerinfilters. Thescannerfield of the panel does this.stateorexclude_statein thefiltersoftriage-progress. This panel shows each state.audit_filterson a panel other thantriage-activity, orentity_typeinaudit_filters- a field and its
exclude_form inaudit_filters, as the Audit log refuses them - an action in
audit_filtersthat is not a triage action. The panel counts only the triage rows. - a panel outside the two columns, or two panels that start on the same cell. A panel holds only the cells of its top row: one cell for a width of 1, and both cells for a width of 2. The height takes no part in this check, because the page makes each panel as tall as its content.
roleswithoutsharing: roles, orsharing: roleswithout a role
JAVV writes owner, created_by, created_at, updated_by, updated_at and revision. Each
change adds 1 to revision. The audit log has a row for each change (dashboard_create,
dashboard_update and dashboard_delete).
Dashboard files¶
A dashboard file is NDJSON: one dashboard on each line, then one summary line. A line has
dashboard_id, schema_version, name, description, scanner, panels, sharing and
roles. A line has no owner, user names or times. The summary line is
{"exported_count": N, "missing_ids": [...]}. A missing ID is an ID that does not exist or a
dashboard that you cannot see. The export does not need can_export.
The export sorts the dashboards by name. Without dashboard_id, it gives the first 1,000, and
the summary line does not tell if there are more. The IDs are in the URL: each ID adds
approximately 45 characters. A URL longer than 16 KB (approximately 350 IDs) gets 431 from the
frontend server.
The import reads the file as the body of the request, with Content-Type: application/x-ndjson.
A different type gets 415.
- You are the owner of each dashboard that the import makes. The audit log gets a
dashboard_createrow for each one. - The import ignores
owner,created_by,created_at,updated_by,updated_atandrevisionin a line. JAVV sets them. - Each line gets the checks of
POST /api/v1/dashboards. Adashboard_idhas 32 lower-case hexadecimal characters. A line must be UTF-8. The import ignores a byte order mark at the start of a line. An object in a line has at most 14 fields. A field that is in an object two times counts two times. - A line can have a role from a different JAVV. If this JAVV has no role with that name, the
line gets 422 (
unknown_role), and the line does not make a dashboard. - A line ends at a line feed (
\n). The import ignores a\rbefore it. It skips empty lines and the summary line of an export. - The result of a line is
201(made),409(the ID exists) or422(the line is not valid). A422gives the place and the reason of each error, not the value of the file. - With
copy=true, each line gets a new ID, so no line gets409. - A file larger than
JAVV_DASHBOARD_IMPORT_MAX_BYTESgets 413. A file with more than 1,000 lines that are not empty also gets 413, also when some of those lines are not valid. The summary line of an export is not in this count. After a 413, the import makes no dashboard. - A line that makes a dashboard is 3 requests to OpenSearch. 100 such lines take approximately 5 seconds.
- If OpenSearch refuses a write, the import stops and the answer is the error (for example 429
or 503), without the results. The lines before that line have their dashboards. To complete an
import without
copy, send the same file again: the dashboards that exist get409. To complete an import withcopy=true, count the dashboards that the import made: you are the owner, and theircreated_atis after the start of the import. The import writes the lines in the order of the file. Thus, delete that number of valid lines from the start of the file, and send the other lines withcopy=true.
The answer has results and the counts created, conflicts and invalid. Each result has
line and status. A 201 or 409 result also has dashboard_id, and a 422 result has
errors.
Exports and reports¶
| Method | Path | Access | What it does |
|---|---|---|---|
| GET | /api/v1/findings/export.csv |
can_export |
The findings of the filters as CSV. More than JAVV_EXPORT_MAX_ROWS (50000) rows get 413. Make the filter narrower, or use a report. |
| GET | /api/v1/findings/export.vex |
can_export |
The findings as an OpenVEX or CycloneDX document, for one scanner. scanner is necessary. The row limit is the same. |
| POST | /api/v1/reports |
can_export for export, can_triage for bulk_triage |
Puts a job in the queue and answers 201. Its params take the filters of GET /api/v1/findings and refuse the same pairs with 422: a filter with its exclude_ side, and assignee with unassigned. kind: bulk_triage needs can_triage, and can_accept_audit_final for a risk acceptance or for a change of state of a set that holds risk-accepted findings. A bulk triage keeps the list of findings at this time. If the user does not have can_accept_audit_final, the job checks the set again before it changes a finding. When a finding became risk-accepted after the request, the job fails and changes no finding. Only the JAVV_BULK_MAX_TARGETS limit applies. Its patch takes the fields of POST /api/v1/findings/bulk-triage (state, vex_justification, assignee and notes), with the same limits. A different field gets 422. JAVV does not change a field whose value is null. |
| GET | /api/v1/reports/{report_id} |
session, the owner | The status of your job. A job of a different user gets 404. When the job is done and not expired, the answer has a download_token for 15 minutes. |
| GET | /api/v1/reports/{report_id}/download |
can_export, the owner, and token |
Downloads the result. After expires_at, the request gets 410. With no result yet, it gets 404. A wrong or expired token gets 403. |
In each CSV file, JAVV puts an apostrophe before a cell that starts with =, +, -, @, a tab
or a carriage return. Thus a spreadsheet does not run the cell as a formula.
Each user can have JAVV_MAX_CONCURRENT_PITS_PER_PRINCIPAL (default 10) open cursors and exports
at the same time. One more gets 429 with Retry-After. A cursor is open from its first page until
its last page, until you close it with POST /api/v1/cursors/close, or until
JAVV_SEARCH_PIT_KEEP_ALIVE plus 30 seconds after the last page that you read. JAVV keeps the result of a report in
OpenSearch for JAVV_EXPORT_TTL_HOURS (default 24). The report_drain job runs the queue, on
JAVV_JOB_REPORT_DRAIN_CRON (default every 5 minutes). A result larger than
JAVV_EXPORT_MAX_BYTES makes the job fail. Before a bulk triage job writes, it checks its patch
again. A patch that is not valid makes the job fail, and the error of the job names the field,
not its value. At the end of a job, a report_ready notification
goes to the user.
Notifications¶
| Method | Path | Access | What it does |
|---|---|---|---|
| GET | /api/v1/notifications |
session | Your 50 newest notifications, and unread, the count of the unread ones. Each item has cluster_name: the name of its cluster now, the cluster_id if the cluster has no name, or null if the item has no cluster. |
| PATCH | /api/v1/notifications/{notification_id}/read |
session | Marks one of your notifications as read. A notification of a different user gets 404. |
| DELETE | /api/v1/notifications/{notification_id} |
session | Deletes one of your notifications (204). A notification of a different user gets 404. |
The types are report_ready, sla_breach, assignment and cluster_retiring:
- The retirement sweep sends
cluster_retiringto each user withcan_manage_settingswhen a cluster starts its warning period. It sends it one time for each silence.refis theretires_atof the cluster. - The sweep deletes these notifications when the cluster scans again, or when a return from retirement starts a new silence. A change of the settings keeps them.
Ingest a scan¶
POST /api/v1/ingest/scan accepts the results of one scan: an envelope with schema_version 3 or
4, as JSON. The body can have Content-Encoding: gzip. The findings of a version 3 envelope have
ptype: null. Push from your own script gives the schema, the order of the calls
and an example.
The backend does these checks in this order:
- The
Authorizationheader starts withBearer. - The token is not over its rate limit. The limit applies to each token value, before the backend looks for the token.
- The token exists, is not disabled, and is not expired.
- The compressed body is not larger than its limit. The backend counts the bytes as they arrive,
and does not trust
Content-Length. - The body after gzip is not larger than its limit.
- The body is JSON.
- The envelope is valid. A field that the schema does not have makes it not valid.
- The
cluster_idandscannerof the envelope are the ones of the token. - The
scan_orderis not larger than the largest one thatPOST /api/v1/scan-runsgave to this cluster and scanner. - The
last_seen_atis not more thanJAVV_INGEST_FUTURE_TOLERANCE_MINUTESahead of the backend's clock, and not older than the staleness period of the cluster.
Then the backend writes the findings. Each finding has a fixed id. Thus when a scanner sends the
same envelope again, JAVV keeps one copy. Two entries of one envelope with the same vuln_id,
package_name and package_version are one finding. The backend keeps the values of the last
entry, and it counts the finding one time.
| Status | When |
|---|---|
| 202 | The backend accepted the envelope: {accepted, findings, commit}. findings is the number of findings that the backend wrote. |
| 400 | The body is not valid gzip, or not valid JSON. |
| 401 | The token is missing, not valid, disabled or expired. Each of these gets the same answer. |
| 403 | The cluster_id or the scanner of the envelope is not the one of the token. |
| 413 | The compressed body or the body after gzip is larger than its limit. |
| 422 | The envelope is not valid. Examples: a field that the schema does not have, a cluster_id that is not valid, or counts that do not agree. A time with no time zone and a schema_version that is not 3 or 4 also get 422. A scan_order that POST /api/v1/scan-runs did not give gets 422 with reason unallocated_order. A last_seen_at too far ahead or too old gets clock_skew. A value that OpenSearch refuses to store also gets 422. The scanner does not try again. |
| 429 | The token is over its rate limit. |
| 503 | OpenSearch could not keep the data, or OpenSearch is not reachable. The scanner tries again. |
Configuring JAVV gives the limits and their defaults.
Logs of a refused push¶
Each refused push increments javv_ingest_rejected_total{reason}. A push that the backend refused
after the token check (400, 403, 413, 422 or 503) also writes one ingest rejected warning. The
warning has these fields:
- always:
reason,status, thecluster_idandscannerof the token, andfailure_id - on a 413:
limit_bytes - on a 422 for an envelope that is not valid:
errors, the count of errors - on a 422 for
unallocated_order:scan_order, andmax_allocated, the largest order given - on a 422 for
clock_skew:last_seen_at, andfreshness_dayswhen the time is too old - on a 503 because OpenSearch is not reachable:
error, the kind of the connection error - on a 403:
payload_cluster_idandpayload_scanner.
The backend never logs the token. A 429 writes one warning for each token in each minute, at most. A 401 writes no warning, so that a sender with no token cannot fill the log.
Records of a refused push¶
The backend also keeps each push that it refused after the token check. It writes one document to
javv-ingest-failures-<cluster_id>, with the cluster and scanner of the token. The Failed
ingests table on the Scanner status page and GET /api/v1/scanners/ingest-failures show
these documents. A 401 and a 429 make no
record.
The answer to the scanner does not depend on the record. If the backend cannot write the record,
it logs ingest failure not recorded with the failure_id. It also increments
javv_ingest_failures_unrecorded_total{reason}.
The MCP endpoint¶
An AI agent, for example in Cursor, VS Code or Codex, reads and changes JAVV data through the MCP endpoint at POST /api/v1/mcp. The endpoint exists only when JAVV_MCP_ENABLED is true (MCP endpoint). An admin makes the token in POST /api/v1/admin/mcp-tokens (Admin).
The endpoint takes the MCP protocol versions 2026-07-28, 2025-11-25, 2025-06-18, 2025-03-26 and 2024-11-05. A client of an older version starts with initialize. The endpoint keeps no session: it never sends Mcp-Session-Id, and each request carries its token.
Before the MCP server reads a request, the endpoint checks it in this order:
| Check | Answer |
|---|---|
The request has an Origin header |
403. A browser always sends this header, and an agent client does not. Thus a web page that a browser opens cannot use the endpoint. |
| The method is not POST | 405, with Allow: POST |
The Authorization header has no live MCP token |
401 invalid token, with WWW-Authenticate: Bearer. A missing, unknown, revoked or expired token and a scanner token get the same answer. |
The token sent more than JAVV_MCP_RATE_LIMIT_PER_MINUTE requests in one minute |
429, with Retry-After: 60 |
The 401 and the 403 only count in javv_auth_failures_total, with no log line: a sender can repeat them with no limit. A 429 also logs mcp rate limited with the token.
A request that passes the checks is the principal mcp:<name> of the token. The token writes last_used_at at most one time a minute.
The level of the token sets the tools that the token can see and call:
| Level | Tools | Permissions of the principal |
|---|---|---|
read |
The read tools | None |
write |
The read tools, set_triage, bulk_triage and not_affected_decision |
can_triage |
write_risk_accept |
All the tools, also risk_accept_decision |
can_triage, can_accept_audit_final |
The tool list of a token (tools/list) holds only the tools of its level. A call to another tool is a tool error with 403, and it also writes its audit row. Read tools have the hint readOnlyHint. Write tools have the hint destructiveHint. A client can use this hint to ask the user before a write.
Each tool call writes one audit row after the call, and one mcp tool call log line:
| Field | Value |
|---|---|
actor |
mcp:<name> |
action |
mcp_tool_call |
entity_type |
mcp_tool |
entity_id |
The tool name and the first 12 characters of a SHA-256 hash of the arguments, for example list_clusters sha256:1a2b3c4d5e6f. The row never holds the arguments. An unknown tool name shows as unknown_tool. |
field, new_value |
result. The value is ok or error <status>. A tool that the level does not allow gets 403. An argument that is not valid gets 422. Another error gets the status of the route. |
cluster_id |
The cluster_id argument of a read tool, when the call has one. For bulk_triage and a decision of kind make, the cluster_id of the body or the payload. The other write calls have no cluster on this row, but the audit rows of their route have one. |
If JAVV cannot write the row, the call is a tool error and the agent gets no data. After a write, the error tells the agent that the tool made the change, so that the agent does not make it again.
The answer of a tool is one text with compact JSON.
- A tool error gives the status and the text of the route. For an argument that is not valid, it gives the name of the argument and the reason. It never holds the value of an argument.
- An answer longer than
JAVV_MCP_MAX_ANSWER_CHARSloses rows from the end of its longest top-level list. Itscutfield then gives the list, the rows that it kept and the rows that it had. - A page with a next page (
next_cursor) is not cut, because the next page starts after the last row of the full page. If the page is longer than the cap, the call is a tool error that tells the agent to ask for a smallersize. - An answer with no top-level list that is longer than the cap is a tool error. For example, a trend over many days in
trendscan be longer than the cap. Then the agent must ask for fewerdays. After a write, the error tells the agent that the tool made the change.
The read tools. Each read tool calls the function of one route. It takes the parameters of that route, with the same names, types and limits. It gives the answer that the route gives to an admin, for each level of token. Thus the agent also sees the account rows of the audit log and each dashboard.
A tool with more than one kind takes the parameter kind. A parameter that the kind does not take is an error, and a missing parameter that the kind needs is an error. A missing parameter that the kind does not need takes the default of the route.
| Tool | Route |
|---|---|
list_clusters |
GET /api/v1/clusters |
search_findings |
GET /api/v1/findings. size is 20 by default and 25 at most. The tool has no keep_open. A first page, with no cursor, closes the open search of the last first page of the same token and tool. |
count_findings |
GET /api/v1/findings/facets |
group_findings |
GET /api/v1/findings/groups |
top_images |
GET /api/v1/findings/top-images |
top_components |
GET /api/v1/findings/top-components |
list_images |
GET /api/v1/images |
image_timeline |
GET /api/v1/images/timeline |
trends |
Kind findings, scans or ingest_failures: GET /api/v1/trends/findings, /scans or /ingest-failures |
scanner_status |
Kind freshness or provenance: GET /api/v1/scanners/freshness or /provenance |
scanner_ingest_failures |
GET /api/v1/scanners/ingest-failures |
audit_log |
GET /api/v1/audit. The tool has no keep_open, and a first page closes a search as search_findings does. |
count_audit_log |
GET /api/v1/audit/facets |
contributors |
GET /api/v1/contributors |
list_decisions |
GET /api/v1/decisions |
list_approvals |
GET /api/v1/decisions/approvals |
list_dashboards |
GET /api/v1/dashboards |
get_dashboard |
GET /api/v1/dashboards/{dashboard_id} |
read_settings |
Kind about (GET /api/v1/meta), scan_scope (GET /api/v1/settings/scan-scope), sla, staleness, retirement, data (GET /api/v1/settings/<kind>), snapshots (GET /api/v1/admin/snapshots), opensearch (GET /api/v1/admin/opensearch-runtime) or jobs (GET /api/v1/admin/jobs) |
list_accounts |
Kind users, roles, scanner_tokens or mcp_tokens: GET /api/v1/admin/users, /roles, /tokens or /mcp-tokens. As the routes, the answer holds no password, token or hash. |
These reads have no tool. test_mcp_read_coverage.py fails when a new GET route has no tool and is not in this list, NO_TOOL in backend/src/backend/mcp/read_tools.py.
| Route | Why it has no tool |
|---|---|
/healthz, /readyz, /metrics |
They tell the state of the platform, not JAVV data. |
/auth/me |
It tells the user of a session. A token has no session. |
/api/v1/notifications, /api/v1/views |
They hold the bell and the saved views of one user. |
/api/v1/scan-scope |
It is the route of the scanner token. read_settings with kind scan_scope reads the same scope. |
/api/v1/dashboards/roles |
It gives the role list of the share dialog of a dashboard. |
/api/v1/reports/{report_id}, /api/v1/reports/{report_id}/download |
They give the state and the file of an export report. |
/api/v1/findings/export.csv, /api/v1/findings/export.vex, /api/v1/audit/export.csv, /api/v1/contributors/export.csv, /api/v1/decisions/approvals/export.csv, /api/v1/dashboards/export.ndjson |
They give an export file. The tool of the same list reads the same rows. |
The write tools. Each write tool calls the function of one route, with the principal mcp:<name> and only the permissions of the token level. Before the call, it runs the permission check of the route. The route then does all its own checks, and it writes its own audit rows with the actor mcp:<name>. For example, a write token cannot set the state risk_accepted. With set_triage and bulk_triage, it cannot change the state of a finding with an accepted risk. But a not_affected decision for a smaller scope has precedence over a risk accept for a larger scope, as on the screens. For example, a not_affected decision for one image has precedence over a risk accept for the cluster.
| Tool | Level | Route |
|---|---|---|
set_triage |
write |
PATCH /api/v1/findings/{finding_key}/triage |
bulk_triage |
write |
POST /api/v1/findings/bulk-triage, with its limits JAVV_BULK_MAX_TARGETS and JAVV_BULK_INLINE_LIMIT |
not_affected_decision |
write |
Kind make (POST /api/v1/decisions), change (PATCH /api/v1/decisions/{decision_id}) or revoke (POST /api/v1/decisions/{decision_id}/revoke). The type of a new or changed decision can only be not_affected. Kinds change and revoke take only a not_affected decision. |
risk_accept_decision |
write_risk_accept |
The same three routes. The type of a new or changed decision can only be risk_accepted or ignore_rule. Kinds change and revoke take only a decision of these types. |
A write route that is not in this table has no tool. test_mcp_read_coverage.py fails when a write tool uses another route.
Metrics¶
GET /metrics gives these metrics, in the Prometheus format (Scrape the metrics).
| Metric | Type | Labels | What it counts |
|---|---|---|---|
javv_ingest_accepted_total |
counter | scanner |
Envelopes that the backend accepted and wrote |
javv_ingest_rejected_total |
counter | reason |
Envelopes that the backend refused. reason is bad_token, rate_limited, too_large, bad_gzip, bad_json, invalid_envelope, unallocated_order, clock_skew, scope_mismatch or storage_error. |
javv_ingest_failures_unrecorded_total |
counter | reason |
Refused pushes with no record (Records of a refused push). A value larger than 0 means that the Failed ingests table does not show all the refused pushes. |
javv_ingest_findings_written_total |
counter | scanner |
Findings that the backend wrote. Two entries of an envelope for one finding count one time. |
javv_http_request_duration_seconds |
histogram | method, route, status |
The time of each request. route is the route pattern, not the full path. A path with no route counts as unmatched. /metrics and the probes are not counted. |
javv_opensearch_request_errors_total |
counter | kind |
Failed requests to OpenSearch: conn and timeout on reads, 429 and 503 on writes. 429 also counts each request that got 429 "The search backend is busy". The backend logs store busy for these at most once in 10 seconds, with held_back. |
javv_opensearch_backoff_retries_total |
counter | none | Writes that OpenSearch refused with 429 or 503, and that the backend sent again. A high rate means that OpenSearch is too busy. |
javv_sla_clock_missing_total |
counter | none | Pages of findings with a row that has no sla_clock_at. The backend calculates the value for these rows, and logs a warning. A steady rate means that the store needs the rebuild_state job. |
javv_cas_conflicts_total |
counter | site |
Writes that lost a race to a different writer: watermarks, scan_orders, inventory_orders, reconcile, reproject, merge, report_claim and retirement. A high rate means many writers on the same data. |
javv_limit_rejections_total |
counter | limit |
Requests that a limit refused: pit_cap, export_rows, bulk_targets, bulk_inline, client_events, inspect_bytes, dashboard_import and mcp_rate |
javv_pits_open |
gauge | none | The cursors and exports that are open now. A cursor that a client stopped using counts until JAVV_SEARCH_PIT_KEEP_ALIVE plus 30 seconds after the last page that the client read, and then stops counting. |
javv_export_rows_total / javv_export_bytes_total |
counter | format |
The rows and bytes that the exports sent. If a client stops the download, the count shows what it received. |
javv_auth_failures_total |
counter | reason |
bad_credentials, locked_out, expired_session, missing_capability, must_change (a session with a temporary password) or session_not_minted (a password change with no new session). The MCP endpoint adds mcp_origin (a request with an Origin header) and mcp_token (a request with no live MCP token). The labels never show a username. |
javv_config_warnings_total |
counter | setting |
Starts with a weak connection to OpenSearch: JAVV_OPENSEARCH_VERIFY_CERTS (the certificate is not checked on https) or JAVV_OPENSEARCH_URL (a password over plain http). Each one also logs a warning at the start. |
javv_stored_setting_unknown_fields_total |
counter | setting |
Reads of a runtime setting that had fields this release does not know. The backend ignores these fields. After a rollback, a value larger than 0 means that a newer release saved that setting. setting is sla, scan_scope, snapshot_repo, report_ttl, lifecycle, findings_cleanup, staleness, retirement or cluster-retirement. |
javv_job_runs_total |
counter | kind, outcome |
Background jobs that the scheduler started. outcome is done, failed or skipped. skipped means that a different run held the job. A steady failed rate is a job that fails each time. No done over the schedule of a job is a job that does not run. |
javv_job_last_success_timestamp_seconds |
gauge | kind |
The Unix time of the last successful scheduled run in this process. It is 0 after a restart, until a run succeeds. Alert on time() - value, against the schedule of the job. |
javv_scheduler_tick_errors_total |
counter | none | Scheduler ticks that failed before a job started, usually because OpenSearch was not available. A steady rate means that no job runs. |
javv_cluster_retirement_held_total |
counter | none | Retirement sweeps that retired nothing, because no cluster sent an accepted scan in its scanner-down time. This points at JAVV, an outage or a refused scanner version, not at the clusters. Each one also logs a warning. |
javv_store_unavailable_total |
counter | none | Requests that got 503 because OpenSearch was not reachable or still starting. The counter counts each one. The backend logs store unavailable at most once in 10 seconds, with held_back: the count since the line before. Any sender can repeat a request while OpenSearch is away. |
javv_store_read_only_total |
counter | none | Writes that got 503 store-read-only because the disk of OpenSearch is past its flood-stage watermark. The counter counts each one. The backend logs store read-only at most once in 10 seconds, with held_back. |
javv_cluster_delete_incomplete_total |
counter | none | Cluster deletes that stopped and answered 503. Send the delete again to finish it. Each one also logs a warning. |
javv_cluster_delete_leftovers_total |
counter | none | Deleted clusters with rows that a push or a job wrote after the delete. The retirement sweep deleted those rows, after it wrote an audit row with their counts (action cluster_delete_recheck). Each one also logs a warning. |
javv_cluster_delete_recheck_failures_total |
counter | none | Deleted clusters that the retirement sweep could not check, or sweeps that could not check any. The sweep goes on to the next deleted cluster, and the next run checks them again. Each one also logs a warning. |
javv_cluster_retirement_notify_failures_total |
counter | none | Retirement sweeps that could not write or delete their notifications. The retirements stay, and the next run tries again. Each one also logs a warning. |
The backend also gives the default process and garbage collection metrics of prometheus_client.
A scrape does not use OpenSearch. Thus the metrics stay available during an OpenSearch outage.
The backend runs one process, and the metrics are for that process.
Client events¶
| Method | Path | Access | What it does |
|---|---|---|---|
| POST | /api/v1/client-events |
session | Writes the warnings and errors of the browser to the backend log, and answers 204. It keeps nothing in OpenSearch and writes no audit row. |
The body is {events: [{level, event, fields}]}, with 1 to 20 events:
leveliswarnorerror. Other values get 422.eventmatches^[a-z0-9][a-z0-9 ._-]{0,63}$.fieldshas at most 25 keys in each object, at most 3 levels, keys of at most 64 characters, values of at most 512 characters, and lists of at most 20 items.
A body that does not agree with these rules gets 422.
The backend writes each event as client.<event>, with client_event=true and the username.
Thus an event from a browser cannot look like an event of the backend. The fields of the event
stay under one fields key. Thus a browser cannot set the username of the log line. The log
redaction also applies inside fields.
Each user can send JAVV_CLIENT_EVENTS_RATE_LIMIT_PER_MINUTE requests in each minute. One more
gets 429 with Retry-After. It also increments javv_limit_rejections_total{limit="client_events"}
and logs a warning. The backend checks the body before the rate limit. Thus a refused body writes
nothing to the log.
A session with a temporary password gets 403. A request with no session gets 401.
Logs¶
The backend writes JSON log lines to its standard output.
- Request id: each line of a request has its
request_id. Errors tells how to set it. Each line of a push also has thecluster_idandscanner. - Unexpected errors: an error with no handler logs one
unhandled errorline at theerrorlevel, with the stack,method,pathandrequest_id. The answer is a 500 with the samerequest_id. - Redaction: the backend replaces the value of each key that contains
token,secret,password,authorization,pepper,sessionorcookie. It also replaces eachBearer ...text. No log line has a token. - OpenSearch bodies: the backend never logs the body of a request to OpenSearch, or of its answer, at any log level.