Dependency vulnerabilities
Project → Vulnerabilities lists the known vulnerabilities in the dependencies of your tracked branches and versions, and lets you decide on each one once for the whole project. The server finds them by matching each target’s dependency list with its own copy of the Trivy database (The vulnerability database). A scan does not have to run.
The tab sits between Issues and Dependencies. Everything on this page is in every edition except three sections, which need Business or Enterprise: exploited, alerts and export.
When the tab shows
The tab shows when both switches are on:
| Switch | Where |
|---|---|
| Vulnerability monitoring | Settings → Vulnerability database, for the instance |
| Track vulnerabilities in this project | Project → Settings → Vulnerability monitoring |
With either one off, the tab and the badge are hidden. Decisions still work and are kept, and decisions made meanwhile apply when you switch monitoring on again.
The header names the date of the data and the time of the last check, such as “Vulnerability data from Oct 29, 2026 · checked Oct 30, 2026, 9:00 AM”.
Some states replace the table:
| The tab says | When |
|---|---|
| “No vulnerability data yet.” | the server has no database. Instance admins get a link to Settings → Vulnerability database. Everyone else is told to ask an instance admin |
| “Vulnerability monitoring is off for this project.” | a link to the tab is opened while a switch is off. Organization and project admins get a link to the project settings |
| “Not checked yet.” | the server has not checked the tracked targets yet. It checks them after the next database update or analysis |
A project with no released versions shows the CI snippets of Record a version on each tag.
By version
By version is the default view. It has one row per tracked target:
- the main branch,
- the branches that match the project’s branch patterns,
- the tracked versions (Which versions are tracked).
| Column | Meaning |
|---|---|
| Branch or version | the name, with a main tag on the main branch, Pinned for a pinned version and “No dependency list” for a target without one |
| Critical, High, Medium | the open vulnerabilities of that severity. Low and Unknown show in the list only |
| New | “N new”: see New vulnerabilities |
A count leaves out the vulnerabilities that a decision closed (Not affected, Won’t fix) and the
ones that .trivyignore ignores (The .trivyignore file).
Select a row to see that target’s vulnerabilities. Its counts show beside its name.
Versions that are not tracked fold into one line, such as “2.1.9 · 2.0.5 · 14 more — not tracked”. Show lists them with “Not tracked” and the counts they kept. Hide folds them again.
The menu of a version row changes how the version is tracked. It needs the permission to change the project’s settings (Organization admin and Project admin, Roles).
| Menu entry | Effect |
|---|---|
| Pin | the version is tracked, whatever the rule says |
| Unpin | removes the pin: the rule decides |
| Mark as unsupported | the version is never tracked |
| Track automatically | removes the mark: the rule decides |
The marks are the ones of Releases and versions.
By vulnerability
By vulnerability shows one row per vulnerability across all tracked targets. Select a row of By version to see the rows of one target.
| On a row | Meaning |
|---|---|
| severity, id | the severity (Critical, High, Medium, Low or Unknown), also the colour of the row’s left edge, and the vulnerability’s id |
| package | the package, its installed versions, and “Fix x.y.z” with the version that fixes it, or “No fix yet” |
| status | Open, Not affected or Won’t fix |
| title | the advisory’s title, on at most two lines |
| In / Not in | the targets that have the vulnerability, and the tracked ones that do not |
| “Found |
when Qualor first saw it. “ without a scan” means the server found it after a new database, with no new analysis |
| “Ignored in .trivyignore” | the row is dimmed and does not count |
| the decision | a triaged row is dimmed. Its reason, or “Reason not given”, its comment, who decided and when show under the title |
Two filters narrow the list:
| Filter | Options |
|---|---|
| Status | All, Open, Not affected, Won’t fix |
| Severity | All, Critical, High, Medium, Low, Unknown |
Exploited and exploit probability (Business and Enterprise)
With the vuln.priority feature the tab shows which vulnerabilities attackers use already. The
data comes from the CISA Known Exploited Vulnerabilities catalog (KEV) and from the FIRST Exploit
Prediction Scoring System (EPSS). Without the feature the tab shows none of it.
| Where | What it shows |
|---|---|
| Exploited column of By version | the open vulnerabilities of the target that are in KEV. A count leaves out decided and ignored ones, like the other counts. It shows 0 when there are none |
| Exploited (KEV) badge on a row | the vulnerability is in KEV. The tooltip gives the date CISA added it, and says “Known to be used in ransomware” when KEV marks it so |
| EPSS on a row | the probability that the vulnerability is exploited in the next 30 days, such as “EPSS 42.0%”. Below 0.1% it reads “<0.1%”. The tooltip adds the percentile |
| Exploited only filter | lists only the rows with the badge. It is kept in the URL as exploited=true |
| order of the rows | open rows first, then the others. Within each group, exploited rows come first, and both parts run by severity |
KEV and EPSS cover CVE ids only. A vulnerability with another id, such as a GHSA id without a CVE alias, has no badge and no EPSS. The vulnerability database lists the sources and how the server keeps them up to date.
New vulnerabilities
A target shows “N new” for the vulnerabilities that need a decision. A vulnerability counts as new when it is all of these:
- open, with no decision that closes it;
- not ignored by
.trivyignore; - found by the server after its first re-check that followed the target’s latest analysis.
| Target | When “new” clears |
|---|---|
| A branch | with its next analysis, or when the vulnerability is triaged |
| A version | when the vulnerability is triaged, or the version stops being tracked. A rescan of the same tag also resets it |
Right after you upgrade to 0.15, nothing is new: what the server finds first is the baseline. A branch or version tracked for the first time shows nothing new from its first re-check either. A version that is tracked again keeps its earlier baseline, so the vulnerabilities found while it was untracked show as new.
The badge “New vulnerabilities in main and 2 versions” shows in the projects list and on the project page. It names the main branch, the number of branches and the number of versions with new vulnerabilities, such as “New vulnerabilities in 3 versions”. It links to the tab.
The projects list
The Vulnerabilities column of Projects shows each project at a glance.
| It shows | Meaning |
|---|---|
| “7 Critical”, “23 High” | the open Critical and High vulnerabilities of the main branch. “No critical or high” when there are none |
| “1 exploited” | the open vulnerabilities in KEV, on the main branch and the tracked versions, each id once. Business and Enterprise only |
| “5 releases” | the tracked versions with an open vulnerability. It links to the tab |
| the badge | see New vulnerabilities |
| “–” | monitoring is off for the project, or the main branch has no dependency list yet |
Open means without a decision that closes it, and not ignored by .trivyignore.
Alerts (Business and Enterprise)
With the vuln.alerts feature Qualor tells you about new vulnerabilities in the branches and
versions it tracks, without you opening the tab.
| Where | What it does |
|---|---|
| New vulnerabilities in tracked versions in the project’s Watch menu, and a column in Settings → Notifications | turns the two emails on or off for that watch. A new watch has it on. Without the feature the box is disabled and shows a Business pill |
| Digest email | once a day at 06:00 UTC: the new High and Critical vulnerabilities that are open and not ignored |
| Exploited email | at once, when a vulnerability is in the CISA KEV catalog |
| Slack and Teams | the exploited vulnerabilities at once, and a section in the weekly digest (Slack and Microsoft Teams notifications) |
vulnerabilities.found webhook event |
the same list as the digest, for generic webhooks (Webhooks) |
Only vulnerabilities found after the alert baseline count. The emails, their limits and the baseline in detail are in Vulnerability emails. Every email has a link that stops them for all your watches.
Statuses and reasons
Triage sets a status for a vulnerability in a package:
| Status | Meaning | Reason | Comment |
|---|---|---|---|
| Open | needs a decision | no | optional |
| Not affected (false positive) | the project is not exposed | required | required |
| Affected, won’t fix | the project is exposed and accepts it | no | required |
A comment has at most 2000 characters. The decision records who made it and when.
The reason says why the project is not affected. The reasons are the five VEX justifications, in plain words:
| Reason in the UI | OpenVEX justification |
CycloneDX justification (in the export) |
|---|---|---|
| The package is not in the build | component_not_present |
code_not_present |
| The vulnerable code is not in the build | vulnerable_code_not_present |
code_not_present |
| The vulnerable code is never called | vulnerable_code_not_in_execute_path |
code_not_reachable |
| An attacker cannot control the input | vulnerable_code_cannot_be_controlled_by_adversary |
requires_environment |
| Another control already protects it | inline_mitigations_already_exist |
protected_by_mitigating_control |
Change status on a row opens the dialog. It needs the permission to triage issues (Maintainer and above). A Viewer reads the tab and sees no Change status button.
One decision, a wide scope
A decision belongs to a package name and a vulnerability id. It covers the whole project: the main branch, all branches and all versions. The ecosystem is not part of the key.
Applies to narrows a decision to one branch or version. That decision overrides the project’s decision on that target, and nowhere else. When you save a project-wide decision from a target’s view, the dialog can remove that target’s override in the same step.
Decisions are written whether monitoring is on or off.
Trivy issues on the Issues tab
A Trivy vulnerability is also an issue of the Trivy engine. Both places use the same decisions.
| You do this | What happens |
|---|---|
Set a Trivy issue on main or on a tracked branch to Won’t fix or False positive |
the project’s decision changes. If that branch has a decision of its own, that decision changes instead |
| Reopen such an issue | the decision becomes Open |
| Resolve such an issue | nothing is written. The next dependency list shows whether the package changed |
| Triage a Trivy issue on another branch or in a merge request | the issue changes and no decision is written |
The other direction:
- A project decision sets the matching Trivy issues on
mainand on tracked branches to its status. An Open decision reopens the ones that are False positive or Won’t fix. - On other branches and in merge requests, a decision that is not Open sets the open Trivy issues to that status.
- A new Trivy issue takes the decision at ingestion when it is not Open.
- A decision never changes resolved and closed issues. When an analysis finds such an issue again
on
mainor a tracked branch, it reopens with the decision’s status, Open included. - A branch with its own decision is not changed by a project-wide decision.
The status dialog of the Issues tab asks for a Reason when you set False positive on such an issue. It says “Select a reason.” until you do. A Trivy issue without a package name that Qualor can read writes no decision and asks for no reason.
The decisions also change the gate: Won’t fix and false positive issues do not count (Issue statuses).
Migrated decisions
On upgrade, each False positive and Won’t fix Trivy issue on main became a project decision with
no reason. When several issues share a package and vulnerability, the latest one counts, and its
comment becomes the decision’s comment. The tab shows “Reason not given” and the Issues tab asks for
a reason at the next edit.
Export SBOM and VEX (Business and Enterprise)
With the sbom feature the tab downloads a software bill of materials (SBOM) and a vulnerability
exploitability exchange (VEX) document for one branch or version. Export SBOM and Export VEX
sit in the header of the tab. Each opens a menu: choose the Target, then a format.
| Button | Formats | Target must have |
|---|---|---|
| Export SBOM | CycloneDX 1.6 (JSON), SPDX 2.3 (JSON) | a dependency list |
| Export VEX | CycloneDX VEX, OpenVEX | a finished vulnerability check, with monitoring on |
A menu lists only the targets that can be exported: the tracked ones, then the untracked versions. A button with no such target is not shown. Without the feature the tab shows neither button.
What an SBOM holds
An SBOM lists every package of the target’s dependency list, sorted by name, version and ecosystem.
- A package is one entry. Packages that share a purl, such as one package in an npm and a yarn lockfile, are one entry.
- A package without a purl is still listed.
- The document names the project and the target, and carries the time it was made and Qualor as its tool.
Licences come from the dependency list. A package with several licences has all of them.
| Format | Licences |
|---|---|
| CycloneDX | one SPDX expression joined with AND when every licence is a valid SPDX id or expression, else one name per licence. No licence, no licenses entry |
| SPDX | licenseDeclared is the licences joined with AND. Free text becomes a LicenseRef-… whose text is in hasExtractedLicensingInfos. No licence is NOASSERTION. licenseConcluded is always NOASSERTION |
What a VEX holds
A VEX has one entry for each vulnerability in each package of the target, with the target’s
decision. A decision that applies to the target alone wins over the
project-wide one. Vulnerabilities that .trivyignore ignores are left out. KEV and EPSS are not in
the file.
A checked target with no findings gives a VEX with no entries. For OpenVEX that is an empty
statements list, which the OpenVEX schema does not accept, because it asks for at least one
statement. CycloneDX VEX has no such limit.
| Status | CycloneDX analysis |
OpenVEX status |
|---|---|---|
| Open | state: in_triage |
under_investigation |
| Not affected (false positive) | state: not_affected and a justification |
not_affected and a justification |
| Affected, won’t fix | state: exploitable and response: [will_not_fix] |
affected and an action_statement |
The reason of a not-affected decision becomes the justification of each format, as the
reasons table shows.
The comment and the author of a decision travel with it.
| Part | CycloneDX | OpenVEX |
|---|---|---|
| Comment | analysis.detail |
impact_statement for not affected, action_statement for won’t fix |
| Who decided | the property qualor:decided-by |
status_notes: “Decided by name.” |
| When | analysis.lastUpdated |
the statement’s timestamp |
A migrated decision has no reason, so it has no justification, and its
comment says why. When OpenVEX needs a statement and the decision has no comment, the file says “No
reason was recorded.” for not affected and “No action is planned.” for won’t fix. An entry with no
decision has no author and no date. An Open decision, made by a reopen or as an override for one
target, carries its author, date and (CycloneDX) comment like any other.
Which file a tool reads
| Tool | Reads |
|---|---|
| Trivy | the OpenVEX file, with either SBOM: trivy sbom <sbom> --vex <file> |
| Dependency-Track | the CycloneDX SBOM, imported first, then the CycloneDX VEX, imported into the same project |
Dependency-Track applies a VEX decision only to a finding it holds under the same source and id. The VEX names a vulnerability by its CVE when it has one, with the source NVD. A finding that Dependency-Track takes from OSV or GitHub Advisories has a GHSA id instead, so its decision is not applied.
Download from CI
The same files come from the API. A project analysis token downloads the SBOM and VEX of its own project, so a release job needs no personal token. The parameters and an example are in SBOM and VEX export.
The .trivyignore file
The scanner sends the ids that the repository’s .trivyignore ignores, with their exp: dates
(The vulnerability database). A vulnerability ignored
there shows dimmed with “Ignored in .trivyignore”. It is not counted and is never new.
The rate limit
A decision counts as 1, plus 1 for each issue it changes, against the per-user budget of issue
changes. The Issues tab uses the same budget: 120 changes a minute, with a burst of 500. Beyond it
the API answers 429 RATE_LIMITED with a Retry-After header, and a decision is not saved.
The API
| Route | Needs | Answers |
|---|---|---|
GET /api/v0/projects/:id/vulnerabilities/targets |
read access to the project | the tracked targets with their counts and new, the untracked versions, the data date and the last check |
GET /api/v0/projects/:id/vulnerabilities |
read access | the vulnerabilities, one per package and id. Query: target (branch:<id> or version:<id>), status (all, open, false_positive, wont_fix), severity, exploited=true (needs vuln.priority, else 403), page, pageSize (50 by default, at most 100). Empty while monitoring is off |
PUT /api/v0/projects/:id/vulnerability-decisions |
issue.triage |
the decision and appliedIssues, the number of issues it changed |
A decision is a body like this:
q -X PUT "$QUALOR_URL/api/v0/projects/<project id>/vulnerability-decisions" -d '{
"packageName":"lodash","vulnerabilityId":"CVE-2021-23337","scope":{"kind":"project"},
"status":"false_positive","reason":"vulnerable_code_not_in_execute_path",
"comment":"Only used in a build script"}'
scopeis{"kind":"project"},{"kind":"branch","id":"…"}or{"kind":"version","id":"…"}. A merge request has no decisions.reasonis one of the five OpenVEX values above. It is required forfalse_positiveand refused for any other status.replaceOverrideOfremoves a target’s decision in the same step. It needs a project-wide scope.- The pair must exist in the project, or the answer is 404.
POST /issues/{id}/transition and POST /issues/bulk-transition accept the same reason.
| Code | When |
|---|---|
REASON_REQUIRED (422, body.reason) |
a false positive that writes a project decision has no reason |
COMMENT_REQUIRED (422, body.comment) |
Not affected or Won’t fix has no comment |
The MCP tool qualor_transition_issue accepts an optional reason too.
A decision is an audit event, vulnerability.decision_changed
(Roles and the audit log, Enterprise).