A project-aware VEX push connector for Dependency-Track. It pulls each
KernelScan product's config-aware VEX and imports it into the matching DT
project, so DT's findings carry KernelScan's not_affected / exploitable
verdicts — the config-reachable ones actionable, the config-gated ones
auto-marked Not Affected with the config-mapping justification.
Open-source client for the commercial KernelScan service. It runs beside your Dependency-Track instance and is the companion to
kernelscan-dt-plugin— see SECURITY.md for its exact data flow.
KernelScan's DT integration has two halves. They are separate on purpose: a DT v5
analyzer produces findings only — analysis decisions (not_affected +
justification) come exclusively from VEX imports or CEL policies, never from an
analyzer. So the plugin can surface the right findings but cannot triage them.
| Channel | Component | Runs | Delivers |
|---|---|---|---|
| Findings | kernelscan-dt-plugin |
in-process in DT | which kernel CVEs are config-reachable (incl. ones NVD is behind on) |
| Verdicts | this connector | beside DT, scheduled | the config-aware not_affected / exploitable analysis, per project |
Together: a project's kernel findings are fully triaged. You can run the verdict
channel with either half — but paired with the plugin you get coverage and
triage under one CVE-* / NVD identity.
Each cycle, for every DT project:
- Resolve the product. Read the project's CycloneDX BOM and find the Linux kernel component; resolve the KernelScan product exactly the way the plugin does (see below), so findings and verdicts always agree on the same product.
- Pull the VEX.
GET /api/products/{id}/vexfrom KernelScan. The endpoint is ETag-aware, so an unchanged VEX is skipped (304) — no needless re-imports churning DT's audit trail. - Apply it.
PUT /api/v1/vexinto the project. DT's importer binds each verdict to its finding by CVE id.
Per-project failures are logged and skipped; one bad project never aborts the cycle.
API keys are read only from the environment, never from the command line —
argv is world-readable (ps, docker inspect, shell history), so a security
tool must not take secrets as flags:
pip install kernelscan-dt-vex-sync # or: pip install .
export DT_API_KEY=odt_… # or DT_API_KEY_FILE=/run/secrets/dt
export KSCAN_API_KEY=ks_live_… # or KSCAN_API_KEY_FILE=/run/secrets/kscan
# preflight: verify both endpoints + keys
dt-vex-sync --check --dt-url https://dtrack.example.com
# one cycle (CI / cron)
dt-vex-sync --once # add --dry-run to resolve + count without pushing
# scheduled daemon (default every hour)
dt-vex-sync --interval 3600Secrets — env / file only (no flag):
| Env | File variant | Notes |
|---|---|---|
DT_API_KEY |
DT_API_KEY_FILE |
DT team key (VIEW_PORTFOLIO + VULNERABILITY_ANALYSIS) |
KSCAN_API_KEY |
KSCAN_API_KEY_FILE |
KernelScan account key ks_live_… (or KERNELSCAN_API_KEY) |
The *_FILE variant reads the secret from a file path — the Docker/systemd/k8s
secrets convention — so the value never appears in the environment table either.
Non-secret flags (each with an env var, handy for Docker):
| Flag | Env | Default |
|---|---|---|
--dt-url |
DT_URL |
— |
--kscan-url |
KSCAN_URL |
https://kernelscan.io |
--state-file |
DT_SYNC_STATE_FILE |
/data/dt-vex-sync-state.json |
--interval |
DT_SYNC_INTERVAL |
3600 |
Other flags: --once, --dry-run, --force (ignore change-detection),
--project <uuid> (one project), --check, --quiet.
A ready-to-edit docker-compose.yml runs it as a
restart: unless-stopped service beside your DT, with the state file on a
named volume.
Create a DT team key scoped to:
VIEW_PORTFOLIO— list projects and export their BOMs, andVULNERABILITY_ANALYSIS— import the VEX.
No admin login is used.
Resolution mirrors the analyzer plugin's selectProductId — deliberately, so the
two channels never disagree:
-
By explicit binding — a kernel component's
kernelscan:productproperty pins a product by name or id. Required when two products share a kernel version but have different.config(e.g. a left/right appliance pair):{ "type": "operating-system", "name": "linux", "version": "6.6.69", "cpe": "cpe:2.3:o:linux:linux_kernel:6.6.69:*:*:*:*:*:*:*", "properties": [{ "name": "kernelscan:product", "value": "Router-A [6.6.69]" }] } -
By kernel version — otherwise the version (from the component's purl or CPE, falling back to the DT project's
versionfield) selects the product, if exactly one product has that version.
If several products share that version — a multi-arch account (same version
imported for x86_64 + arm64), or a left/right appliance pair with different
.config — the connector skips and logs a warning rather than guess. There
is no architecture default: guessing an arch would risk applying the wrong
.config's verdicts (and would diverge from the plugin, which dropped its arch
config in v0.2.0). Pin the right product with a kernelscan:product binding.
The kernels you sync must exist as products in your KernelScan account —
that's where the .config and its verdicts live.
pip install -e '.[test]'
pytestLayout:
dt_vex_sync/
cli.py argparse + --once / polling loop
runner.py one sync cycle (pure orchestration) + change-detection
resolver.py project -> product (Python port of the plugin's selectProductId)
dt_client.py Dependency-Track REST (list projects, export BOM, PUT /vex)
kscan_client.py KernelScan REST (products, product VEX with ETag)
state.py per-project last-pushed ETag/hash (JSON, atomic writes)
resolver.pyis a hand-maintained port ofKernelScanVulnAnalyzer.{selectProductId,kernelVersionOf,productBindingOf}inkernelscan-dt-plugin. Keep the two in lock-step — if they diverge, a project could show one product's findings but be triaged with another's verdicts.
Apache License 2.0 — see LICENSE and NOTICE.
Copyright © 2026 KernelScan UG (haftungsbeschränkt). The KernelScan service
itself is commercial; this connector is the open-source client.