Skip to content
| Marketplace
Sign in
Visual Studio Code>Linters>UBEL — Supply-Chain & Secrets ScannerNew to Visual Studio Code? Get it now.
UBEL — Supply-Chain & Secrets Scanner

UBEL — Supply-Chain & Secrets Scanner

Arcane-Spark

|
39 installs
| (0) | Free
Multi-ecosystem software composition analysis (SCA), secrets detector, and license/compliance scanner. Scans Node.js, Python, PHP, Rust, Go, C#/.NET, Java/Kotlin, Ruby, Swift, and Flutter/Dart projects — plus your VS Code/Cursor extensions, exposed secrets, and the host developer machine — for known
Installation
Launch VS Code Quick Open (Ctrl+P), paste the following command, and press enter.
Copied to clipboard
More Info

UBEL — Supply-Chain & Secrets Scanner for VS Code

Multi-ecosystem dependency and secrets scanner for the developer's machine and tools.
Covers source repos, developer machines, and exposed secrets — runs entirely locally. The only network calls are to osv.dev and the NVD API for vulnerability data (both mirror-configurable), plus best-effort lookups to the CISA KEV catalog and the FIRST EPSS API for exploit intelligence.

Publisher VS Code GitHub


What is UBEL?

UBEL is a software composition analysis (SCA) tool, secrets detector, and install-blocking firewall built for teams who care about what enters their supply chain at every layer. Unlike report-only scanners, the full UBEL toolset enforces policy — if a scan fails, it blocks the operation and tells you exactly why.

As a project, UBEL spans the entire delivery chain: from the moment a developer adds a dependency, through CI validation, to what is running on a deployment server or inside an AI agent's runtime environment.

This specific extension covers the editor-side slice of that: dependency vulnerability scanning (SCA) enriched with exploit intelligence (CISA KEV + EPSS), secrets detection, license compliance, and host/editor-extension auditing, all in health (report-only) mode. It is built directly on the SCA engine of the @arcane-spark/ubel-node package, so findings, policy, and reports are identical to what the CLI's health mode produces (see Extension vs. CLI). It does not include the install-time firewall (the scan-before-you-install gate that blocks a malicious package before it ever reaches node_modules), AI-powered SAST/malicious-code scanning, or CI/CD wiring — those live in the @arcane-spark/ubel-node CLI package (npm, docs) and the official GitHub Action, which this extension is a companion to rather than a replacement for.


Extension's features

  • Full dependency resolution with PURL generation
  • Querying authoritative vulnerability sources in real time, allowing newly published advisories to be detected immediately without waiting for scheduled database refreshes unlike the competitors.
  • Vulnerability scanning via batched API queries to OSV.dev and NVD's APIs
  • Concurrent vulnerability enrichment (CVSS, fix recommendations, references)
  • Exploit intelligence — every vulnerability is checked against the CISA Known Exploited Vulnerabilities catalog and scored with FIRST EPSS; policy blocks KEV entries and anything at or above an EPSS threshold, and a feed outage never aborts the scan (see Exploit Intelligence)
  • Policy engine — block/allow by severity threshold, unknown-severity packages, CISA KEV membership, EPSS score, and license risk
  • Malicious package (infection) detection — always blocked regardless of policy
  • Secrets detection — Trivy's ported, Apache-2.0-attributed ruleset, extended with UBEL's own rules for vendors Trivy's current upstream doesn't cover (HashiCorp Vault, GCP API keys/OAuth tokens, Anthropic, OpenRouter, live Stripe restricted keys, URL-embedded git credentials, database connection strings with embedded passwords, and more). Findings can be suppressed with a .ubelignore file or inline ubel:ignore markers (see Suppressing findings). Included by default in every project scan, or standalone via its own command. Match previews in every report are redacted.
  • License compliance — every package's declared license is normalized (SPDX expressions, free text, npm's UNLICENSED proprietary marker vs. the SPDX Unlicense public-domain license, missing/unknown values) and checked against the OSI-approved license list, with a derived risk rating. Included by default in every project scan, or standalone via its own command (no vulnerability lookups, no secrets scan).
  • Dependency graph with introduced-by and parent tracking (Swift and Flutter/Dart lockfiles don't record a dependency graph, so those packages have no edges)
  • Automatic report generation: JSON + HTML per scan, plus a SBOM (CycloneDX) and SARIF where the scan type supports them, named <file_name>.<tag>.<ext> — the tag (sca, os_sca, secrets or licenses) says which command produced the file (e.g. latest.sca.json, latest.os_sca.html, latest.secrets.html), plus latest.<tag>.* convenience copies. Project and extension scans (sca) write all four formats; the host-platform scan on Windows/Linux (os_sca) writes JSON, HTML and SBOM but no SARIF; the secrets and license scans write JSON and HTML only (see Reports) For historic tracking, a zipped snapshot of each scan's reports is saved too, and the newest zip of each tag is also kept at a fixed path in a separate latest_reports folder. Every report file is written atomically, so a script or CI step reading a latest.* file mid-scan never sees a half-written one
  • Zero external runtime dependencies (Node.js stdlib only)
  • Complete compliant, and enriched SBOM Cyclonedx V1.6 files with full dependencies and vulnerabilities data in VEX
  • Complete compliant, and enriched SARIF v2.1.0 files
  • Reachability analysis — each vulnerability is annotated with a reachability verdict (a reachable flag plus a critical / high / medium / low level) derived from package type, scope, dependency depth, attack vector, and import-scan confirmation across all supported ecosystems
  • Executive summary — every JSON and HTML report opens with a plain-language overview for non-technical readers: overall risk rating, policy verdict, key numbers, key findings (including weaknesses already exploited in real attacks), the components to fix first, and prioritized recommended actions (see Executive Summary)
  • Recommended package-level fixes — for every package, UBEL works out which versions to upgrade to, grouped per version range (stay on your current minor line, or move to a newer one/major), picking the fewest and highest versions that clear the most vulnerabilities, and lists whatever has no fix at all (see Recommended Package Fixes)
  • OS package release matching — advisories for the host's OS packages (deb / rpm / apk) are verified against the distro release the package is installed on, so an AlmaLinux 8 package is not flagged for AlmaLinux 9 or 10 errata; every OS-package vulnerability and suggested fix carries a high / medium / low release-confidence level (see OS Package Release Matching)
  • Compliance framework mapping — every vulnerability and secrets finding is mapped onto OWASP Top 10, PCI DSS, HIPAA, SOC 2, ISO/IEC 27001, NIST SP 800-53, GDPR, and CIS Controls v8, with a report-level per-framework/per-control finding-count summary. Included by default in every scan, across the JSON, HTML, and SARIF reports (see Compliance Framework Mapping)
  • Diffs between scans — the HTML and JSON reports compare dependencies, vulnerabilities, secrets and per-compliance-framework findings with the previous report of the same asset and scan type: what is new, what is unchanged and what was removed or fixed (see Diffs Against the Previous Report)

Commands

Command Shortcut (Win/Linux) Shortcut (Mac) What it scans
UBEL: Scan Project Ctrl+Alt+U Cmd+Alt+U All ecosystems inside the open workspace folder (includes secrets by default)
UBEL: Scan Code Editor's Extensions Ctrl+Alt+X Cmd+Alt+X Packages inside ~/.vscode/extensions or ~/.vscode-oss/extensions or ~/.cursor/extensions (full-stack walk of the extensions directory, plus the secrets pass)
UBEL: Scan Host Platform Ctrl+Alt+P Cmd+Alt+P System software installed on this machine
UBEL: Scan project for Exposed Secrets Ctrl+Alt+S Cmd+Alt+S Secrets-only pass over the open workspace folder — no dependency resolution
UBEL: Scan project for License Compliance Ctrl+Alt+L Cmd+Alt+L License-only pass over the open workspace folder — full dependency resolution, no vulnerability lookups, no secrets scan

All five commands are also accessible via the Command Palette (Ctrl+Shift+P / Cmd+Shift+P) — search UBEL.

What each command runs

Command Dependency resolution Host OS / dev tools Vulnerability lookups (OSV / NVD) KEV / EPSS enrichment Secrets scan License classification
Scan Project ✅ every ecosystem ❌ ✅ ✅ ✅ ✅
Scan Code Editor's Extensions ✅ every ecosystem found in the extensions directory (mostly npm) ❌ ✅ ✅ ✅ ✅
Scan Host Platform ❌ ✅ ✅ (CPE / NVD) ✅ ❌ ✅
Scan project for Exposed Secrets ❌ ❌ ❌ ❌ ✅ ❌
Scan project for License Compliance ✅ every ecosystem ❌ ❌ ❌ ❌ ✅

Only one scan runs at a time — starting a second while one is in progress shows a warning and does nothing.


Installation

From the Marketplace

Search for UBEL in the VS Code Extensions panel

From VSIX

  1. Download ubel-vscode-extension.vsix from the releases page.
  2. Open the Command Palette → Extensions: Install from VSIX…
  3. Select the downloaded file.

Scan Project (Ctrl+Alt+U)

Scans every ecosystem present anywhere inside the currently open workspace folder. Monorepos with mixed stacks are fully covered in a single pass — no configuration needed. Each discovered package is deduplicated by PURL, so packages shared across sub-projects are scanned exactly once. See Supported Ecosystems for Swift and Flutter/Dart specifics.

What gets scanned

Ecosystem Resolved From
Node.js (npm, pnpm, yarn, bun) node_modules/ on-disk walk
Python .venv/, venv/, virtual environment directories
PHP vendor/, composer.lock
Rust Cargo.lock (next to a Cargo.toml; workspace members are followed)
Go go.mod (module-root marker and selected versions), cross-referenced with go.sum when present
C#/.NET packages.lock.json, falling back to obj/project.assets.json; project root = a *.csproj / *.fsproj / *.vbproj
Java pom.xml declarations (parent poms on disk, <dependencyManagement>, ${…} properties and <modules> followed); the full transitive tree when .ubel/maven-deps.txt exists. Gradle is not read
Ruby Gemfile.lock (requires a Gemfile alongside)
Swift Package.resolved (SwiftPM v1/v2/v3, incl. Xcode workspaces), .build/workspace-state.json fallback, Cartfile.resolved (Carthage)
Flutter / Dart pubspec.lock, or .dart_tool/package_config.json when no lockfile is committed

Go, Java, Ruby and .NET read manifests/lockfiles rather than an installed tree, so no build is needed — except that a .NET project with neither packages.lock.json nor obj/project.assets.json reports nothing (run dotnet restore first), and a Maven project only gets transitive dependencies if you generate .ubel/maven-deps.txt with mvn dependency:tree -DoutputFile=.ubel/maven-deps.txt. Versions a Maven parent pom outside the workspace would manage stay unresolved (reported undetermined, not sent to OSV). None of these four records license data, so their packages show license unknown.

Report location

<project-root>/.ubel/reports/latest.sca.*

Scan VS Code Extensions (Ctrl+Alt+X)

Scans the npm packages bundled inside your installed VS Code / Cursor / VS Codium extensions (~/.vscode/extensions or ~/.vscode-oss/extensions or ~/.cursor/extensions). Extensions are a meaningful supply-chain surface — they run with full Node.js access in the editor host process and are updated silently. The extension detects which editor is hosting it (VS Code, Cursor, or VSCodium) and scans that editor's directory. As with a project scan, the secrets pass and license classification are included, and the report header is tagged with the scanned editor and its version.

Report location

~/.vscode/extensions/.ubel/reports/latest.vscode_sca.*

or

~/.vscode-oss/extensions/.ubel/reports/latest.vscodium_sca.*

or

~/.cursor/extensions/.ubel/reports/latest.cursor_sca.*

Scan Host Platform (Ctrl+Alt+P)

Audits the system-level software installed on the developer's machine itself — a distinct attack surface from project dependencies. Vulnerabilities are matched using CPE 2.3 identifiers against the CVE/NVD database.

This catches what dependency scanners miss: a vulnerable version of Git, an unpatched Python interpreter, an outdated Docker Desktop install, or an end-of-life .NET runtime.

Every detected component carries its actual license or vendor EULA — proprietary Microsoft/vendor components (Windows itself, Defender, Edge, Chrome, Docker Desktop, Visual Studio, Cursor, Claude Code, …) resolve to a LicenseRef-* identifier rather than unknown, while open-source runtimes and tools resolve to their real SPDX id (e.g. MIT for Node.js/.NET, PSF-2.0 for Python). Findings are also enriched with KEV/EPSS data. The host-platform scan does not run the secrets pass.

Windows — detected via registry probes and PowerShell, no elevated privileges required:

Category Components
Operating system Windows 10 / 11 (build-accurate CPE version)
Security Windows Defender
Runtimes Node.js, Python, PHP, Go, Rust, Ruby, JRE, JDK
.NET All installed .NET Core / Desktop / ASP.NET runtimes (multi-version)
Browsers Chrome, Firefox, Microsoft Edge
Developer tools Git, Docker Desktop, Visual Studio Code / Visual Studio IDE, Cursor, VSCodium, Claude Code
Shell PowerShell

Linux — reads the system package database directly, works as a standard user on most distributions:

Distribution Package manager Source PURL type
Ubuntu dpkg /var/lib/dpkg/status pkg:deb/ubuntu/
Debian dpkg /var/lib/dpkg/status pkg:deb/debian/
Alpine / Alpaquita apk /lib/apk/db/installed pkg:apk/alpine/
Red Hat / RHEL rpm rpm database (rpmdb.sqlite or Packages) pkg:rpm/redhat/
AlmaLinux rpm rpm database (rpmdb.sqlite or Packages) pkg:rpm/almalinux/
Rocky Linux rpm rpm database (rpmdb.sqlite or Packages) pkg:rpm/rocky-linux/
CentOS / Fedora rpm rpm database (rpmdb.sqlite or Packages) pkg:rpm/redhat/

The distribution is read from /etc/os-release (/usr/lib/os-release as a fallback): ID is tried first, then each ID_LIKE token, so derivatives such as Amazon Linux (ID_LIKE="centos rhel fedora") resolve to their parent family. A distribution that matches none of the above (e.g. SUSE, Arch) stops the scan with an "Unsupported OS ecosystem" error rather than guessing.

Each package entry includes its binary install paths and direct dependency edges as reported by the package database.

No package-manager binaries are executed to read installed packages (dpkg, apk and rpm are not required): the rpm database is parsed in-process and its format is detected from the file's magic bytes — Berkeley DB Packages (RHEL/CentOS/Alma/Rocky ≤ 8, Amazon Linux 2) and SQLite rpmdb.sqlite (RHEL/Alma/Rocky ≥ 9, Fedora, Amazon Linux 2023). NDB (Packages.db, SUSE) is not supported and stops the scan with a clear error.

Each package is also tagged with the distro release it was read from (e.g. almalinux-8.10), which UBEL uses to match advisories to that release — see OS Package Release Matching.

Report location

The report is always written under ~/.ubel/reports/, independent of any open workspace. On Windows and Linux the host-OS scan has its own tag, so the files are latest.os_sca.* (JSON, HTML and SBOM — no SARIF); on any other platform the tag stays sca.

~/.ubel/reports/latest.os_sca.*

Because the home directory is not a project, this scan does not add .ubel/ entries to any .gitignore / .dockerignore.


Scan for Exposed Secrets (Ctrl+Alt+S)

Runs a secrets-only pass over the open workspace folder — no dependency resolution, no package-manager calls. Built on Trivy's ported secret-scanning ruleset (Apache-2.0, see sca/vendor/trivy/NOTICE), extended with UBEL's own rules for vendors Trivy's current upstream doesn't cover:

  • HashiCorp Vault tokens (hvs. prefix)

  • Google Cloud API keys (AIza…) and OAuth access tokens (ya29.)

  • Anthropic and OpenRouter API keys

  • Firebase server tokens

  • Amazon MWS auth tokens

  • Square OAuth secrets and access tokens, Braintree access tokens. A Square EAAA… token must be a standalone string with enough entropy: it is not reported when it sits inside a longer base64/base64url run (inlined wasm, data URIs)

  • Stripe live restricted keys (rk_live_) — Trivy covers publishable/secret keys but not this format. Test-mode keys (rk_test_) are deliberately skipped, since they can only reach test-mode data

  • Credentials embedded in a git remote URL (https://user:token@host/...) — covers GitHub, GitLab, Bitbucket, Azure DevOps, Codeberg, Gitea, Gitee and SourceHut, self-hosted instances whose hostname starts with git, gitlab, bitbucket, gitea, gogs or forgejo, and any other host when the URL is a .git remote. Placeholder passwords (password, token, xxxx, …) and ${VAR} references are ignored

  • Database and broker connection strings with an embedded password — three forms, all reporting the password only:

    • URL (rule database-url-credentials): postgres://, postgresql://, mysql://, mariadb://, mongodb:// / mongodb+srv://, redis:// / rediss://, amqp:// / amqps://, mssql://, sqlserver://, cockroachdb://, clickhouse://, neo4j://, bolt://, and SQLAlchemy-style postgresql+psycopg2://
    • Key/value (rule database-connection-string-password): ADO.NET, ODBC and libpq strings such as Server=…;Database=…;Password=… or host=… password=…; a host/database key must appear earlier on the same line, so a bare password= is left to the generic rule
    • JDBC (rule jdbc-url-password): jdbc:mysql://host/db?user=u&password=…, jdbc:sqlserver://host;…;password=…

    Not reported: placeholders and templates (${DB_PASS}, <password>, changeme, YOUR_PASSWORD, xxxx, …), user:user pairs such as guest:guest, default passwords (postgres, root, admin, test, …) against localhost, 127.0.0.1, host.docker.internal or a bare docker-compose service name, and hosts containing example. The same default password against a real hostname is reported.

  • Generic high-entropy fallback for unknown vendors — a vendor-style prefix (sk-, pk_, xox*, api_, key_, token_) followed by 32+ characters, where the part after the prefix must also clear a Shannon-entropy check, so repeated runs and plain snake_case identifiers aren't reported

  • Generic key-value fallback — password/secret/token/api_key/private_key assigned a 32+ character value; there is no entropy check here, the key name is the signal

Twilio Account/App SIDs are intentionally not detected: the AC…/AP… + 32-character pattern has no checksum or distinguishing context and produced far too much noise.

The ruleset is a generated snapshot of Trivy's, regenerated from upstream when it changes — not a live mirror.

Match previews shown in every report are redacted — the raw secret value is never written to disk, in this report or any other.

In the other report formats of a regular project/extension scan (sca), secrets are exposed as a ubel:secrets entry in the SBOM's root properties, and as their own run in the SARIF file (separate tool driver and rule set from the dependency-vulnerability run). The standalone secrets command (secrets tag) writes JSON and HTML only — no SBOM or SARIF.

This scan also runs automatically as part of UBEL: Scan Project (Ctrl+Alt+U) — this command exists for when you want a fast, dependency-resolution-free pass, e.g. before a commit.

Report location

<project-root>/.ubel/reports/latest.secrets.*

UBEL: Scan Project (which also runs the secrets pass) writes latest.sca.*, while this command writes its own latest.secrets.*, so the two never overwrite each other. Each run's timestamped zip goes to ~/.ubel/history/<asset_id>/, and the newest one is also kept in ~/.ubel/latest_reports/<asset_id>/ (see Reports).

What gets scanned

  • Every match on every line is reported, not just the first per rule; overlapping matches collapse to one finding (a specific rule beats a generic one).
  • Private keys: PEM blocks are matched across lines (the finding points at the BEGIN line and carries end_line), including encrypted/legacy PEM and PGP blocks. Extensionless key files are opened too: id_rsa/id_dsa/id_ecdsa/id_ed25519 (and id_rsa.bak-style variants) are always scanned, and files with no extension or a backup-style one (.bak, .old, .orig, .p8, .ppk, .asc …) are scanned if they start with a PEM private-key header.
  • .env* files are skipped (they aren't meant to be committed). The global allow-rules still apply, so a file such as .env.example is skipped either way.
  • Skipped directories: dependency trees, build output and caches are skipped (node_modules, vendor, target, dist, build, out, virtualenvs, …). packages, bin, obj and env are ambiguous names, so they are skipped only when the layout says they hold generated content — a NuGet packages/ (next to a .sln, or containing repositories.config), a .NET bin//obj/ (next to a .sln/.csproj), a Python venv (pyvenv.cfg). A monorepo's packages/* and a Node bin/ are scanned. Any default-skipped directory can be re-included with include-dir: in .ubelignore (below); VCS metadata (.git, .svn, .hg) and UBEL's own .ubel/ directory are never walked and cannot be re-included.

Suppressing findings

Put a .ubelignore file at the workspace root. One entry per line; # starts a comment.

docs/generated/                 # skip a directory
*.snap                          # skip files by glob, at any depth
/fixtures/keys/*.pem            # leading "/" anchors to the scan root
src/seed.js rule:generic-key-value-credential   # suppress one rule (comma list ok) on matching paths
rule:generic-fallback           # disable a rule everywhere
fingerprint:3f9a1c0be27d4a55    # accept one specific finding
include-dir:dist                # scan a directory that is skipped by default
exclude-dir:generated           # skip a directory name wherever it appears
unallow:tests                   # switch off a builtin allow-rule (e.g. scan test paths)

Or mark the line in the source, in any comment syntax:

const k = "..."; // ubel:ignore
const k = "..."; // ubel:ignore[aws-access-key-id]
// ubel:ignore-next-line

Every finding carries a fingerprint in the JSON report — a hash of rule id, path and the secret, stable across line moves and never containing the secret itself — so a finding can be accepted by adding its fingerprint: line. The scan result also reports suppressed (how many findings the ignore rules dropped). The CLI's ubel-secrets --write-baseline, which writes these lines for you, is not available in the extension.

The builtin allow-list still skips paths containing test/example and secrets whose text contains "example"; unallow:tests / unallow:examples turn those off. The per-rule allow-lists of the database-connection-string rules are part of the rule, so unallow: does not affect them; switch one off with rule:<id>.


Scan for License Compliance (Ctrl+Alt+L)

Runs a license-only pass over the open workspace folder: full dependency resolution across every ecosystem present, license normalization and OSI-approval/risk classification — but no vulnerability lookups (no OSV.dev/NVD calls) and no secrets scan. Use this when you only need a license inventory (e.g. for legal/compliance review) without the time or network cost of a full vulnerability scan.

See License Compliance below for how licenses are normalized and classified — the classification logic is identical whether it runs standalone here or as part of UBEL: Scan Project.

This scan is also included automatically as part of UBEL: Scan Project (Ctrl+Alt+U) — this command exists for a faster, vulnerability-lookup-free pass when license data is all you need.

Report location

<project-root>/.ubel/reports/latest.licenses.*

UBEL: Scan Project writes latest.sca.*, UBEL: Scan project for Exposed Secrets writes latest.secrets.*, and this command writes latest.licenses.* — the three never overwrite each other. Each run's timestamped zip goes to ~/.ubel/history/<asset_id>/, and the newest one is also kept in ~/.ubel/latest_reports/<asset_id>/ (see Reports).


Files UBEL Writes to Your Workspace

Besides the reports under .ubel/ (see Reports), the Scan Project, Scan Code Editor's Extensions, Scan project for Exposed Secrets and Scan project for License Compliance commands make sure the scanned directory's .gitignore and .dockerignore ignore both .ubel/ and .ubelignore, creating either file if it doesn't exist. Scan Host Platform does not touch them.

  • Idempotent. An entry counts as covered if any equivalent pattern is already present (.ubel, /.ubel/, .ubel/*, .ubel*, …), so a hand-written entry is never duplicated.
  • Append-only. Existing content, ordering and line endings (LF/CRLF) are preserved; new entries go under a # ubel: comment.
  • Opt-out respected. A negation such as !.ubelignore means you want that entry tracked, so it is not re-added — this is how you commit a shared .ubelignore while everything else stays ignored.
  • Never fails a scan. A read-only checkout or a permissions problem is silently skipped.
  • Kill switch: set UBEL_NO_IGNORE_FILES=1 in the environment the editor was launched from to turn this off.

Scan history is kept outside the workspace, under your home directory — timestamped zips in ~/.ubel/history/ and the newest zip of each scan type in ~/.ubel/latest_reports/ (see Reports).


Scan Results

Every scan ends with a VS Code notification:

Result Notification Meaning
✅ Scan complete — no policy violations All packages passed
⚠️ Policy violation A malicious package, a vulnerability at or above the severity threshold, a known-exploited (CISA KEV) or high-EPSS vulnerability, an exposed secret, or a configured license-risk gate was hit — see Policy
❌ Scan error Unexpected failure — message contains details. This includes an OSV.dev/NVD lookup that couldn't be completed: an incomplete lookup is a failed scan, never a clean one

KEV/EPSS lookups are the exception: they only add risk signal, so if either feed is unreachable the scan still completes, the affected fields are null (unknown), a warning is shown in the report, and the matching policy rule is not enforced for that run — see Exploit Intelligence.

Every notification includes an Open Report button that opens the full interactive HTML report in your browser.


The HTML Report

Each scan produces a self-contained HTML file that works fully offline. It contains ten tabs:

Tab Contents
Dashboard Vulnerability counts by severity, policy decision summary (including threat-intel feed warnings), license-risk stats card, scan metadata
Executive Summary Plain-language risk rating, policy verdict, key findings, components to fix first, and suggested actions for non-technical readers, printable as a PDF — see Executive Summary
Secrets Exposed secrets by category, severity, file/line, and redacted match preview
Vulnerabilities Full list of matched CVEs with CVSS score, EPSS score/percentile, KEV badge, severity, fix version, reachability level, and policy decision. Click any row for a detail modal (CVSS vector, fix recommendations, OSV/NVD references, compliance frameworks, and — for OS packages — the release match)
Inventory Every scanned package with version, PURL, CPE, ecosystem, state (safe / vulnerable / infected / undetermined), license risk (OSI-approved status, risk level), and vulnerability count. Click a package for its detail modal, which includes Suggested Fixes — see Recommended Package Fixes
Dependency Sequences Interactive force-directed dependency graph — colour-coded by vulnerability status, with search, filter, drag, and pin
Detailed Stats Severity distribution charts, top vulnerable packages, ecosystem breakdown
Compliance One card per framework (OWASP Top 10, PCI DSS, HIPAA, SOC 2, ISO/IEC 27001, NIST SP 800-53, GDPR, CIS Controls v8) with control breakdown and finding counts — see Compliance Framework Mapping
Diffs What changed since the previous report of this asset and scan type: new / unchanged / removed dependencies, new / still existing / fixed vulnerabilities, new / still existing / removed secrets, and — per compliance framework — new / existing / resolved findings. Click any count for the list — see Diffs Against the Previous Report
System Info OS metadata, git info, Node.js version, engine/tool versions (no IP addresses or ports are recorded)

Executive Summary

Every report (JSON and HTML) carries an executive_summary written for readers who don't work with CVEs, CVSS scores, or package URLs — management, risk and compliance teams, product owners. It is derived entirely from data already in the report (no extra scanning or network calls), so the JSON field and the HTML tab always show the same content. It is built after the policy decision is made and never fails a scan: if it can't be built, only the summary is omitted.

In the HTML report it is the Executive Summary tab, placed right after the Dashboard.

What it contains

Section What it tells the reader
Overall risk rating One of Critical / High / Medium / Low / Minimal / Not assessed, with a rationale and general business-impact text
Headline & verdict A one-sentence overview and whether the scan meets or fails the security policy, in plain language (with the technical reason alongside)
At a glance Components reviewed and with issues, vulnerabilities by severity, how many have a fix available, how many are likely in use by your code vs. not, how many block policy, exposed credentials, how many are known to be exploited (KEV) or have a high exploit likelihood (EPSS)
Key findings The handful of things that matter most, each with a severity
Components to fix first Up to five components, ranked by malicious status, then known-exploited (and not judged unused), then whether they're likely in use, then worst severity, then forecast exploit likelihood, then issue count — each with a suggested upgrade action and the possible upgrade paths (see Recommended Package Fixes)
Suggested actions Prioritized actions with a timeframe and the reason for each
Compliance overview Which frameworks the findings touch and which are most affected (omitted when nothing maps to a framework) — see Compliance Framework Mapping
Scope, methodology & glossary What was scanned, how this specific report was produced, and plain-language definitions of the terms used

Layout and printing

The HTML tab is laid out so the first screen can be read on its own and fits one printed page:

  1. Cover — report title, what was scanned, report ID, date, and tool (cover).
  2. Bottom line — the overall risk rating next to the policy verdict, a one-paragraph summary, the Top risks, Do this first (each action with its timeframe and a suggested owner), and four key figures (bottom_line).
  3. Details — why this rating, all key findings, at-a-glance cards (glance_cards), components to fix first, all suggested actions, and compliance exposure.
  4. Appendix — methodology, "About this report" (scope and notes), and the plain-language glossary. The appendix is collapsed on screen and expanded automatically when printing.

A Print / save as PDF button at the top of the tab prints the summary only (on a white background, other tabs hidden), so it can be handed to someone who never opens the interactive report. In the Components to fix first table, the identifiers under each component are the advisory references, for tickets and audit trails. For multi-system scans, the tab also shows Configuration issues by area and Systems to review first tables when that data is present.

Overall risk rating

Rating Assigned when
Critical any malicious (MAL-*) component was found
High any Critical/High-severity vulnerability that isn't confirmed unreachable, any CISA KEV vulnerability that isn't confirmed unreachable (whatever its severity), or any exposed High/Critical credential
Medium Medium/unrated issues, vulnerabilities at or above the EPSS threshold, lower-severity secrets, or Critical/High/KEV issues that reachability analysis confirmed are not used by production code
Low only Low-severity issues (none at or above the EPSS threshold), or Medium/unrated issues that reachability analysis confirmed are all unused
Minimal nothing found
Not assessed the scan skipped vulnerability lookups (e.g. UBEL: Scan project for License Compliance) and nothing else raised the rating — shown instead of "Minimal" so an unchecked scan is never read as a clean one

The rating and the policy verdict are independent: the rating discounts findings that reachability analysis confirmed are unused, while the verdict counts every finding at or above the blocking thresholds (and any exposed secret), so a report can be rated Medium yet still be blocked. Reachability is a heuristic; a finding with no reachability result is treated as potentially reachable.

Exploit intelligence in the summary. The summary uses the same two signals the policy blocks on, so a report is never rated "Low" while the policy blocks it for an actively exploited vulnerability:

  • Known-exploited (is_kev: true) — gets its own key finding (with CISA's earliest remediation due date), an Immediately suggested action, the Known to be exploited card, and raises the rating to High unless reachability analysis confirmed the code unused (then Medium). Components with a known-exploited issue rank first among the non-malicious ones and carry an Exploited badge.
  • High EPSS — vulnerabilities at or above epss_threshold that aren't already KEV get a key finding and, when they are lower-severity, a Within days action; they set a floor of Medium. If the EPSS rule is turned off in the policy, 10% is still used for this informational reporting, and the text says the policy doesn't block on it.
  • Unknown stays unknown — if a feed was unreachable, the matching figures are null (shown as "n/a", never 0), a key finding says so, the methodology shows the lookup as incomplete, and a Low/Medium rating notes that it could be understated.

Checks that didn't run or didn't finish. When vulnerability lookups were skipped, the vulnerability-derived figures (including the exploit-intelligence ones) are null (shown as "n/a" in the HTML tab), not 0. When the secrets pass failed, exposed_credentials is null and a key finding says the result is unavailable instead of reporting zero. Components with no determinable version can't be matched against vulnerability databases, so they are called out in the methodology, limitations, and notes.

Good to know

  • The rating scale is UBEL's own — not CVSS or a regulatory standard — and the impact text is general guidance per rating level.
  • Action timeframes are built-in defaults, not your organization's remediation SLAs. The HTML tab labels the section "Suggested actions" for this reason.
  • Suggested upgrades are indicative, not a guarantee.
  • Malicious-component advisories are counted separately, so "Known weaknesses" can be lower than the Vulnerabilities tab total; a note says so when it applies.
  • scope.subject identifies what was scanned: name, repository, branch, commit (first 8 characters), scanned_at and tool, taken from the report's git, runtime, and tool metadata. Fields that aren't available are omitted from the header.
  • overall_risk.basis and recommended_actions_basis state that the rating scale is UBEL's own (not CVSS or a regulatory standard) and that action timeframes are built-in defaults rather than your organization's remediation policy.
  • For host-platform scans, the project name is left empty rather than showing a temp or home directory. Credentials embedded in a git remote URL are stripped from the scan header.

JSON

{
  "executive_summary": {
    "overall_risk": { "level": "high", "label": "High", "rationale": "...", "business_impact": "..." },
    "cover": { "title": "...", "subject": "my-app", "report_id": "...", "generated_at": "2026-10-06T09:30:00Z", "tool": "...", "classification": "...", "statement": "..." },
    "bottom_line": { "risk_label": "High", "summary": "...",
                     "top_risks": [ { "severity": "high", "title": "...", "detail": "..." } ],
                     "do_first": [ { "action": "...", "timeframe": "Immediately", "owner": "..." } ],
                     "figures": [ { "label": "...", "value": 9, "sub": "...", "tone": "high" } ] },
    "glance_cards": [ { "label": "...", "value": 142, "sub": "...", "tone": null } ],
    "headline": "This scan reviewed 142 software components and found ...",
    "verdict": { "status": "blocked", "label": "Does not meet security policy", "statement": "...", "technical_reason": "..." },
    "at_a_glance": { "vulnerabilities_assessed": true, "components_reviewed": 142, "components_with_issues": 4, "malicious_components": 0,
                     "total_vulnerabilities": 9, "by_severity": { "critical": 1, "high": 2, "medium": 4, "low": 2, "unknown": 0 },
                     "fix_available": 7, "fix_available_percent": 78, "likely_in_use": 6, "not_in_use": 3,
                     "blocking_policy": 3, "exposed_credentials": 2,
                     "known_exploited": 1, "high_exploit_likelihood": 2, "exploit_data_complete": true },
    "key_findings": [ { "severity": "high", "title": "...", "detail": "..." } ],
    "components_to_fix_first": [ { "name": "lodash", "version": "4.17.15", "issue_count": 3, "worst_severity": "critical",
                                   "worst_severity_label": "Critical", "likely_in_use": true, "blocks_policy": true, "known_exploited": 1, "max_epss": 0.42, "upgrade_to": "4.17.21",
                                   "fix_options": [ { "version": "4.17.21", "range": "4.17.x", "scope": "minor", "resolves": 3, "of": 3, "resolves_known_exploited": 1, "known_exploited_total": 1, "recommended": true } ], "fix_options_more": 0, "no_fix_yet": 0,
                                   "references": [ "GHSA-xxxx-xxxx-xxxx" ], "more_references": 2, "action": "Upgrade to version 4.17.21." } ],
    "recommended_actions": [ { "priority": 1, "timeframe": "Immediately", "owner": "...", "action": "...", "why": "..." } ],
    "compliance_overview": { "frameworks_touched": 3, "most_affected": [ { "framework": "...", "findings": 5 } ], "statement": "...", "disclaimer": "..." },
    "scope": { "scan_type": "health", "description": "...", "target": "a code repository", "subject": { "name": "my-app", "repository": "...", "branch": "main", "commit": "a1b2c3d4", "scanned_at": "...", "tool": "..." }, "ecosystems": ["npm"], "components_reviewed": 142 },
    "methodology": { "steps": [], "rating_rules": [], "prioritization": "...", "timeframes": "...", "limitations": [] },
    "notes": [ "..." ],
    "glossary": [ { "term": "Vulnerability", "meaning": "..." } ]
  }
}

Methodology. executive_summary.methodology (and the Methodology section of the HTML tab) describes how that specific report was produced. Steps are included only if the stage ran for that scan: the usage estimate needs reachability results, the exploit-intelligence lookup needs vulnerability lookups (and is labelled incomplete when a feed failed), the credential search needs secrets scanning on, license review appears on health scans, and compliance mapping needs a compliance_summary. The Policy check step prints the policy values actually in force, including the KEV and EPSS rules.

Fix options. The suggested upgrade for each component comes from the per-package analysis in Recommended Package Fixes: the closest upgrade path that resolves the most of that component's issues (a major version change is called out, and any issues it leaves open are stated). Every upgrade path is also listed as fix_options — closest release line first, at most five, the recommended one always kept — with the version, its range, whether it is a major change, how many of the component's issues it resolves (and how many of the known-exploited ones), plus no_fix_yet for issues no version fixes. The HTML tab shows them as a Possible fixes list with Best and Major badges. If the per-package analysis is missing or failed, the older per-issue heuristic is used instead. Either way, the suggested upgrade is indicative, not a guarantee.


Recommended Package Fixes

For every package in the inventory, UBEL looks at all of its vulnerabilities and the fixed version of every affected range, then suggests which versions to upgrade to. Instead of a single "latest" version, suggestions are grouped per version range, so you can pick the least disruptive upgrade that still closes the most issues:

  • One group per minor range above your installed version — e.g. 4.17.x, 4.18.x (same major version, so typically non-breaking)
  • Then one group per higher major range — e.g. 5.x, 6.x (may be breaking)

Groups are listed closest-first: minor ranges, then major ranges, with the highest version first inside a range.

How versions are picked. Inside each range, UBEL picks the version that fixes the most still-open vulnerabilities (ties go to the highest version), then repeats until nothing more can be fixed in that range — so you get the fewest, highest versions that cover the most. A vulnerability can therefore appear in several ranges: each range is an alternative upgrade path, not a sequence of steps to apply together.

Branch-aware matching. A candidate version is only counted as a fix if it genuinely falls outside every affected range of that vulnerability — not merely because it is numerically higher than some fixed version. This matters for advisories fixed on several release branches (e.g. a fix backported to 4.17.21 while 4.18.0–4.18.2 stays affected). When an advisory carries no usable range data, UBEL falls back to "at or after a known fixed version". Git-commit ranges are ignored since they can't be compared to package versions. For NVD-sourced advisories, each branch's lower bound is kept alongside its fix version, so a fix on a lower branch (say 1.2.5 next to 1.3.2) is recognized as a fix for that branch rather than looking still-affected.

Unfixed vulnerabilities. Anything with no fixed version above the one you have installed — or that no candidate version actually clears — is listed separately as unfixed rather than silently dropped, so you can see what an upgrade won't solve.

In the HTML report

Open the Inventory tab and click a package: its detail modal has a Suggested Fixes section.

  • A table with one row per suggested version — Range (label plus minor range / major range), Suggested version (with "fixes N"), and Vulnerabilities fixed, each with its severity badge. Click any vulnerability to jump to its detail modal.
  • Below it, Vulnerabilities with no fix (N) — ID, severity, and score for everything no suggested version resolves.
  • For OS packages, a Release match badge on each suggested version (and on the unfixed list) shows how sure UBEL is that it is for your distro release — see OS Package Release Matching.
  • Malicious (infection) entries are shown with an infection badge. If nothing can be fixed by upgrading, the modal says so; if the package has no suggestion data, it says that instead.

Ordering. Vulnerabilities inside every group are ordered most severe first: malicious (infection), then critical, high, medium, low, unknown; ties are broken by CVSS score, then ID.

Ecosystem-aware version comparison. Versions are compared with one ecosystem-agnostic comparator that handles semver, PEP 440, and deb/rpm-style versions — including epochs (1:2.3), v prefixes, pre-release tags (1.0.0-rc1 sorts below 1.0.0), and ignored build metadata (+build) — across every supported ecosystem.

In SBOM and SARIF. SBOM: each component has a suggested_fixes property (JSON string), and each vulnerability gets the properties suggested_fix_version and suggested_fix_bulk_count when a suggested version covers it. SARIF: each result has suggested_fix_version and suggested_fixes in properties, and the run's properties.inventory_suggested_fixes lists the plan per package.

Suggestions are computed only when vulnerability lookups ran, so they are absent from license-only scans.

JSON

Each inventory item in the JSON report carries a suggested_fixes object:

{
  "suggested_fixes": {
    "fixes": [
      {
        "version": "4.17.21",
        "count": 3,
        "range": { "key": "m:4.17", "label": "4.17.x", "scope": "minor" },
        "vulnerabilities": [
          { "id": "GHSA-xxxx-xxxx-xxxx", "severity": "critical", "severity_score": 9.8, "is_infection": false }
        ]
      }
    ],
    "unfixed": [
      { "id": "CVE-0000-0000", "severity": "medium", "severity_score": 5.3, "is_infection": false }
    ]
  }
}
Field Meaning
fixes[].version A suggested version to upgrade to
fixes[].count How many of the package's vulnerabilities that version resolves
fixes[].range The range the version belongs to: scope is minor (same major as installed) or major (higher major); label is e.g. 4.17.x or 5.x
fixes[].vulnerabilities The vulnerabilities resolved, most severe first
fixes[].release_confidence, fixes[].release_confidence_reason OS packages only: how sure UBEL is that the version is built for the installed distro release (lowest of the vulnerabilities it covers; low if the version is visibly for another release). unfixed[] entries carry release_confidence too — see OS Package Release Matching
unfixed Vulnerabilities no suggested version resolves

If suggestions can't be computed for a package, its suggested_fixes carries an error message with empty fixes/unfixed, and the rest of the report is unaffected. Suggestions are computed once per scan, right after vulnerabilities are matched, and before the executive summary is built.

OS Package Release Matching

An OS package (pkg:deb/…, pkg:rpm/…, pkg:apk/…) is only affected by advisories issued for the distro release it is installed on — an AlmaLinux 8 package is not vulnerable to an AlmaLinux 9 erratum whose fixed version is …el9_8. OSV's PURL lookup returns advisories for every release of a distro, so UBEL checks the release itself and reports how sure it is. In the extension this applies to the OS packages found by UBEL: Scan Host Platform on Linux; project and extension scans contain no OS packages, and application packages (npm, PyPI, …) are unaffected. Docker image scans and the apt/dnf/yum firewall use the same logic in the CLI (see the CLI docs).

How it works

  1. Release detection. Every OS package is tagged with a distro value — the scanned system's os-release ID and VERSION_ID, e.g. almalinux-8.10, debian-12, alpine-3.22.2. It is metadata only: the package's PURL and id are unchanged. A system with no VERSION_ID (e.g. Debian testing/sid) gets no distro.
  2. Lookup. The value is added to the OSV query as a PURL distro qualifier (pkg:rpm/almalinux/acl@2.4.0-1.el8_10?distro=almalinux-8.10); results are still reported under the plain PURL. UBEL does not rely on OSV honouring the qualifier.
  3. Response check. Each advisory names its release in affected[].package.ecosystem (e.g. AlmaLinux:9). UBEL compares it with the installed release and drops the advisory only when it names the same distro family for that package but never the installed release. Anything ambiguous is kept (see the confidence levels below), so a doubtful case adds a finding rather than hiding one. The scan log reports how many were dropped (Dropped N OS advisories that apply to a different distro release than the one scanned.). An advisory that covers several releases is kept but trimmed to the matching entries, so its fixed versions and fix recommendations describe only your release.

A release matches when it equals the installed version or is its leading dotted part, so 8 matches 8.10 but not 10 or 18.

Distros matched

Distro (os-release ID) Advisory ecosystem matched Example
Debian Debian:<major> debian-12 ↔ Debian:12
Ubuntu Ubuntu:<VERSION_ID> (including the :LTS suffix and Ubuntu:Pro:…) ubuntu-24.04 ↔ Ubuntu:24.04:LTS
Alpine Alpine:v<major.minor> alpine-3.22.2 ↔ Alpine:v3.22
AlmaLinux AlmaLinux:<major> almalinux-8.10 ↔ AlmaLinux:8
Rocky Linux Rocky Linux:<major> rocky-9.3 ↔ Rocky Linux:9

Every other distro — Red Hat / RHEL, CentOS, Fedora, Amazon Linux, Oracle Linux, and derivatives such as Linux Mint, Kali or Raspbian (their own os-release ID isn't one of the above) — is not filtered: all advisories are kept, with low confidence. Rocky Linux (ID=rocky) is detected as Rocky Linux (pkg:rpm/rocky-linux/), not as Red Hat.

Release confidence

Each OS-package vulnerability gets a release_confidence and a one-line release_confidence_reason:

Level Meaning
high The advisory explicitly names the installed release (e.g. AlmaLinux:8 for almalinux-8.10)
medium Same distro family, but the advisory names no release (e.g. a bare Debian), so it could not be confirmed
low Nothing to verify against: the distro isn't in the table above, its release is unknown, or the advisory has no entry for the family — it may belong to another release

Fixes. A suggested version's confidence is the lowest of the vulnerabilities it covers, and is forced to low when the version is visibly built for another release than the installed one (installed 2.4.0-1.el8_10, fix 2.4.0-1.el10_2; +deb12u1 vs +deb11u3). The same level is attached to every entry of a vulnerability's ranked fix versions (fix_versions_ranked[]).

Confidence is informational — it never changes the policy decision. Application packages (npm, PyPI, …) carry no confidence fields: the concept only applies to distro releases.

Where it appears

  • JSON: release_confidence and release_confidence_reason on each OS-package vulnerability, on every fix_versions_ranked[] entry and on every suggested_fixes.fixes[] entry; release_confidence on suggested_fixes.unfixed[] entries and in findings_summary.
  • HTML report: a Release match badge (reason on hover) in the package's Suggested Fixes table and its unfixed list; a Release match line in the vulnerability detail modal and a column in its fix-version table.
  • SBOM (CycloneDX v1.6): the vulnerability properties os.release_confidence and os.release_confidence_reason.
{
  "id": "ALSA-XXXX:NNNN",
  "affected_package_id": "pkg:rpm/almalinux/acl@2.4.0-1.el8_10",
  "release_confidence": "high",
  "release_confidence_reason": "Advisory ecosystem AlmaLinux:8 matches the installed almalinux-8.10.",
  "fix_versions_ranked": [
    { "version": "2.4.0-1.el8_10.1", "release_confidence": "high", "release_confidence_reason": "…" }
  ]
}

Exploit Intelligence (KEV & EPSS)

Severity says how bad a flaw could be; these two feeds say whether it is actually being exploited or is likely to be. Every vulnerability found by a scan that performs vulnerability lookups is enriched with:

Field Source Meaning
is_kev CISA KEV catalog true if the CVE is in the catalog, false if not, null if the catalog couldn't be fetched
kev_added CISA KEV Date the CVE was added to the catalog (YYYY-MM-DD), else null
kev_deadline CISA KEV CISA's remediation due date, else null
epss_score FIRST EPSS Probability (0–1) of exploitation in the next 30 days, else null
epss_percentile FIRST EPSS Percentile (0–1) of that score among all scored CVEs, else null

null always means unknown (feed down, or no EPSS score exists for the CVE), never "not exploited" or 0. The HTML report shows EPSS as percentages and flags KEV entries with a badge; the JSON report keeps the raw 0–1 values.

CVE matching. OSV advisories are frequently GHSA/other ids, so the CVE is taken from the vulnerability id if it starts with CVE-, and from its aliases otherwise. If an advisory maps to several CVEs, it is KEV if any of them is, and the highest EPSS score is reported.

Policy. With the defaults, a scan blocks on any KEV entry (block_kev: true) and on any vulnerability with epss_score >= 0.1 (10%), whatever its severity. Both are tunable in config.json — see Policy. The block reason names the offending ids, e.g. Blocked by policy: 1 known-exploited (CISA KEV) vulnerability detected: GHSA-xxxx-xxxx-xxxx.

When a feed is unreachable. Unlike OSV/NVD (where a failed lookup fails the scan, because a missing answer would look like "no vulnerabilities"), KEV/EPSS only add risk signal, so an outage degrades the result instead of aborting it:

  • the scan completes and the affected fields are null;
  • a warning is shown in the HTML decision box and recorded under threat_intel in the report (status of ok, partial, unavailable or skipped per feed, plus the error);
  • the matching policy rule is not enforced for that run, and a passing verdict says so — e.g. Policy passed (note: CISA KEV data unavailable — not enforced for this scan);
  • other rules (severity, the other feed, infections, secrets) are unaffected.

Each feed is tried with a 15-second timeout and two retries. There is currently no endpoint override for either feed, so a fully air-gapped setup runs without KEV/EPSS data.

Other report formats

  • SBOM (CycloneDX v1.6): each vulnerabilities[] entry gets the properties kev.listed (true, false or unknown), kev.date_added, kev.due_date, epss.score and epss.percentile; KEV entries also get an advisory link to the CISA catalog, and EPSS is added as a second ratings[] entry (source FIRST EPSS, score in percent). The root properties carry kev_vulnerabilities and ubel:threat_intel — read the count together with the feed status, since an outage leaves the count at 0.
  • SARIF 2.1.0: rules and results carry is_kev, kev_added, kev_deadline, epss_score and epss_percentile in properties; rules get the tags kev / known-exploited / epss where they apply. Each result gets a rank (0–100): 100 for a KEV entry, otherwise the EPSS probability ×100. A KEV result is reported at level error whatever its severity; reachability analysis never downgrades it.

Reachability Analysis

Every vulnerability in the report is annotated with a heuristic reachability assessment. The analyzer operates on the existing report fields — package type, scope, dependency depth, CVSS attack vector, and the dependency graph — and optionally performs a source-level import scan over the project files to confirm or refute whether the vulnerable package is actually used by application code. The host-platform scan has no project source, so it has no import-scan signal.

The goal is prioritization: to separate vulnerabilities in packages your code actively exercises from those in packages that are installed but unreachable from any production code path.

Decision ladder

Signals are evaluated in strict priority order. The first matching rule wins. Each rung yields a reachable flag and a level; unconditional reachability is reported as level: "critical".

Priority Signal Result Confidence
0a Vuln ID starts with MAL- reachable · critical high
0b Package scope includes env reachable · critical high
1 Package type is non-library (app, framework, plugin, OS package, …) reachable · critical high
2 Scope is dev or test unreachable · low high
3 Import scan: package imported in source files reachable · high or medium high
4a Import scan: direct import absent, but importing parent found reachable · medium or low medium
4b Import scan: no direct or parent import found unreachable · low medium
5 Orphan tool (no dependents in graph, no import scan available) unreachable · low medium
6 Depth + attack vector heuristics reachable (high / medium), or unreachable · low low

Priority 0a (MAL-) — Malware advisories represent active supply-chain infections. The vulnerable code is the infection vector; reachability is unconditional regardless of how or whether the package is imported.

Priority 0b (env scope) — Packages carrying the env scope are part of the execution environment itself — OS packages, system libraries, runtimes, container-layer components. They are not imported by application code; they are the environment. Reachability is unconditional.

Priority 1 (non-library type) — Frameworks, applications, plugins, and OS-level packages have no meaningful import boundary. The component itself is the attack surface.

Priority 2 (dev/test scope) — Packages that are exclusively development or test dependencies are excluded from production runtimes. Scope is derived from package.json devDependencies (for Rust, from the [dev-dependencies] / [build-dependencies] sections of Cargo.toml and its workspace members) and propagated through the dependency graph via BFS.

Priorities 3–4 (import scan) — When a project root is provided, UBEL scans source files for import statements matching the package. For transitive dependencies where the package itself is not directly imported, it checks whether any of the package's parents in the dependency graph are imported — confirming that the transitive path is exercised.

Priority 5 (orphan tool) — Root packages with no dependents and no import scan result are most likely standalone CLI tools included in the environment but not called by application code.

Priority 6 (heuristics) — When no higher-priority signal is available, depth in the dependency tree and the CVSS attack vector are used as weak proxies. A root package (depth 0) with a network attack vector is reachable · high; a root with a local/physical vector, a transitive package with a network vector, or an unknown vector is reachable · medium; only a transitive package with a local/physical vector is unreachable · low.

Import scan coverage

Source files are scanned for ecosystem-appropriate import patterns:

Ecosystem Extensions Patterns matched
Node.js .js .ts .mjs .cjs .jsx .tsx require('<pkg>'), from '<pkg>'
Python .py import <pkg>, from <pkg>
Java / Kotlin .java .kt .groovy .scala import <group>.<artifact>
C# / .NET .cs .vb .fs .fsx using <Namespace>
PHP .php use <Vendor>\\, require '<pkg>'
Go .go "<module-path>"
Rust .rs use <crate>::, extern crate <crate>
Ruby .rb require '<gem>'
Flutter / Dart .dart import 'package:<pkg>/…', export 'package:<pkg>/…'
Swift .swift .m .mm .h import <Module>, @import <Module>, #import <Module/…>

Reachability results appear in the Vulnerabilities tab of the HTML report and in the machine-readable JSON report. Each vulnerability record includes a reachability object:

{
  "reachability": {
    "reachable": true,
    "level": "high",
    "confidence": "high",
    "rationale": "Import of this package was found in project source code. Found in 2 source file(s): src/index.js, src/utils.js. Depth=0, AV=N.",
    "tags": ["import_confirmed", "network_av"],
    "signals": {
      "depth": 0,
      "attack_vector": "N",
      "is_orphan_tool": false,
      "scope": "prod",
      "num_paths": 3,
      "introduced_by_count": 1,
      "pkg_type": "library",
      "is_non_library": false,
      "is_malware": false,
      "has_env_scope": false,
      "import_scan": {
        "searched": true,
        "found": true,
        "files_scanned": 87,
        "matched_files": ["src/index.js", "src/utils.js"],
        "skipped_no_source": false
      }
    }
  }
}
Field Description
reachable true if the vulnerable code is considered reachable from production
level critical, high, medium, or low (critical = unconditionally reachable)
confidence high, medium, or low — how much evidence backs the verdict
rationale Human-readable explanation of which signal drove the decision
tags Machine-readable labels for the signals that fired (e.g. import_confirmed, dev_scope, malware, env_scope)
signals Full signal snapshot — all inputs that were considered, regardless of which rule fired

License Compliance

Every project scan classifies each package's declared license by default. Licenses arrive in inconsistent shapes across ecosystems (SPDX ids, free text like "Apache 2.0", npm's UNLICENSED proprietary sentinel, Python trove classifiers, OR/AND SPDX expressions, or missing entirely); UBEL normalizes all of them to a canonical SPDX identifier, checks it against the OSI-approved license list, and assigns a risk rating:

Category Examples Risk
Permissive MIT, Apache-2.0, BSD-2/3-Clause, ISC, 0BSD low
Public domain Unlicense (OSI-approved), CC0-1.0 (not OSI-approved but permissive in practice) low
Weak copyleft MPL-2.0, LGPL-2.1/3.0, EPL-2.0, CDDL medium
Strong copyleft with linking exception GPL-2.0-only WITH Classpath-exception-2.0 (OpenJDK JRE/JDK) medium
Strong copyleft GPL-2.0/3.0, AGPL-3.0 high
Source-available / rejected by OSI SSPL-1.0, BUSL-1.1, Elastic-2.0 high
Proprietary npm UNLICENSED, Proprietary, LicenseRef-* vendor EULAs (Windows, Defender, Chrome, Docker Desktop, …) high
None / unrecognized missing, unknown, or unparseable text unknown

npm's UNLICENSED sentinel (proprietary — all rights reserved) is deliberately not confused with the SPDX Unlicense public-domain license; dual-licensed packages (OR) are classified using the most favorable option, since the consumer may legally choose it, while conjunctively-licensed packages (AND) are classified using the most restrictive one, since all obligations stack.

Other shapes that are normalized: case/whitespace variants, SPDX expressions (GPL-2.0-or-later, GPL-2.0+), WITH exception expressions (the Classpath exception permits linking without inheriting GPL's copyleft obligations, so it's capped at medium), LicenseRef-* identifiers (a real, named vendor license with no SPDX id — treated as proprietary-leaning rather than "unrecognized"), legacy npm object/array forms, and Python trove classifiers (License :: OSI Approved :: MIT License → MIT). SEE LICENSE IN <file> is flagged as unverifiable rather than guessed at, and missing/empty/unknown values are classified as none. osi_approved is true only for licenses on the OSI-approved list, false for a real license that isn't on it (proprietary, source-available, Creative Commons), and null when there's nothing to check.

Run standalone via UBEL: Scan project for License Compliance (Ctrl+Alt+L) — see above — when you want license data only, with no vulnerability lookups or secrets scan.

Output fields. Each inventory item gets a license_info object:

{
  "license": "UNLICENSED",
  "license_info": {
    "raw": "UNLICENSED",
    "spdx": null,
    "identifiers": [],
    "osi_approved": false,
    "risk": "high",
    "category": "proprietary",
    "reason": "npm \"UNLICENSED\" marker — explicitly no license grant (all rights reserved). Not to be confused with the SPDX \"Unlicense\" public-domain license."
  }
}

The top-level stats.license_stats field summarizes the whole inventory:

{
  "total": 142,
  "osi_approved": 118,
  "not_osi_approved": 9,
  "unknown": 15,
  "by_risk": { "low": 112, "medium": 6, "high": 9, "unknown": 15 }
}
  • HTML report: the Inventory tab and the per-package detail modal render license_info as a risk-badged license table (SPDX id, identifiers, OSI-approved, risk, category, reason) rather than a bare string, and the Dashboard carries a license-risk stats card.

The SBOM and SARIF outputs below are produced by regular project and extension scans (sca tag); the standalone license command (licenses tag) writes JSON and HTML only.

  • SBOM (CycloneDX v1.6): components[].licenses uses the normalized SPDX expression form when a usable identifier was found, falling back to free-text license.name; OSI status, risk, category, and reason are added as component properties, and the root properties carry the OSI/unknown counts.
  • SARIF 2.1.0: a dedicated ubel-license-compliance run, separate from the vulnerability and secrets runs. Only packages that need review are reported — any package with risk: "high", or osi_approved not equal to true — so a fully permissively-licensed tree produces no findings. Result level maps from risk (high → error, medium → warning, low → note).

Compliance Framework Mapping

Every dependency vulnerability and secrets-in-source finding is mapped onto industry compliance/security frameworks by default — no separate flag or mode needed, and included in every report format. This is distinct from License Compliance above, which is about license-obligation risk on installed software; this is about mapping security findings onto the frameworks an org is typically audited against.

Each finding is first assigned one or more internal risk categories — for a dependency vulnerability, derived from its advisory's CWE(s); for a secrets finding, always secrets_management (e.g. injection, secrets_management, vulnerable_components — the latter always applied to a dependency finding as a baseline, since every SCA finding is a known-vulnerable-component finding by definition), and each category carries a fixed list of framework control references, so two findings with the same underlying risk always map identically.

Frameworks covered

Framework Notes
OWASP Top 10 (2021) Category codes (A01:2021–A10:2021)
PCI DSS v4.0 Requirement numbers
HIPAA Security Rule §164.312 / §164.308 citations
SOC 2 (Trust Services Criteria) CC*/A1.* codes
ISO/IEC 27001:2022 (Annex A) A.* control numbers
NIST SP 800-53 Rev. 5 Control IDs (e.g. SI-10, AC-3)
GDPR Article citations
CIS Controls v8 Numbered controls

This is best-effort guidance, not a certified compliance assessment. Control identifiers are the stable, publicly documented ones for each framework, but framework text, versioning, and applicable scope can change, and always depend on the org's own environment. Every report carries this disclaimer verbatim in compliance_summary.disclaimer — treat the mapping as a starting point for an audit conversation, not a citation to quote in one.

Output fields. Each vulnerability and secrets finding gets a compliance object:

{
  "compliance": {
    "categories": ["vulnerable_components", "injection"],
    "frameworks": [
      {
        "id": "owasp_top10_2021",
        "name": "OWASP Top 10 (2021)",
        "controls": [
          { "id": "A06:2021", "title": "Vulnerable and Outdated Components" },
          { "id": "A03:2021", "title": "Injection" }
        ]
      }
    ]
  }
}

The top-level compliance_summary field aggregates every vulnerability and secrets finding in the report into per-framework, per-control finding counts:

{
  "disclaimer": "Compliance framework references are best-effort guidance ...",
  "frameworks": [
    {
      "id": "owasp_top10_2021",
      "name": "OWASP Top 10 (2021)",
      "findings_count": 14,
      "controls": [
        { "id": "A06:2021", "title": "Vulnerable and Outdated Components", "findings_count": 11 },
        { "id": "A03:2021", "title": "Injection", "findings_count": 3 }
      ]
    }
  ],
  "by_category": { "vulnerable_components": 11, "injection": 3, "secrets_management": 2 }
}

Output

  • HTML report: a dedicated Compliance tab with one card per framework (control breakdown + finding counts), plus a Compliance Frameworks section in each vulnerability's detail modal.
  • JSON report: a compliance object on every vulnerability and secrets finding, plus a report-level compliance_summary aggregating all findings into per-framework, per-control counts.
  • SARIF report: compliance_categories / compliance_frameworks on each rule, and the full compliance object on each result — in both the dependency-vulnerability run and the secrets run.

Diffs Against the Previous Report

Project, extension and host-platform scans carry a diffs block in the JSON report, shown as the Diffs tab of the HTML report (between Compliance and System Info). It says what changed since the previous report of the same asset and the same scan type: a sca scan is compared only with an earlier sca scan, an os_sca scan only with an earlier os_sca scan, and so on for each editor's <editor>_sca scan. The previous report is read from the newest earlier zip of that type in the asset's history folder (~/.ubel/history/<asset_id>/, see Reports); a zip that can't be read is skipped in favour of the next older one.

What Matched by Lists
Dependencies component id (a PURL; for Windows host scans, a CPE 2.3 string) new / unchanged / removed
Vulnerabilities advisory id + affected component new / still existing / fixed
Secrets rule id + file + masked match (not the line, which moves when the file is edited) new / still existing / removed
Compliance frameworks the same keys as the vulnerabilities and secrets, grouped by each framework the finding maps to new / existing / resolved, per framework

In the HTML report each count is a button that opens the list behind it. The compliance frameworks appear below the secrets, one card per framework (OWASP Top 10, PCI DSS, HIPAA, SOC 2, ISO/IEC 27001, NIST SP 800-53, GDPR, CIS Controls v8) that either report mapped findings to; its list shows each finding's type, id, severity, title, component or file, and the framework's control ids. A finding that maps to several frameworks is counted under each of them.

  • An upgrade is a removal plus an addition. The version is part of a component's id, so an upgraded package appears once under removed (old version) and once under new (new version). The same CVE on a second component is a new vulnerability.
  • Secrets are compared only when both reports scanned for them. Otherwise the secrets lists are omitted and the tab says so, so a scan without the secrets pass is never read as "all secrets removed". The compliance section then covers vulnerabilities only, and says so.
  • Older previous reports. A previous report from before compliance mapping existed has no per-framework data, so the compliance section is omitted rather than reporting every finding as new. A previous report that lists only advisory ids has its vulnerabilities compared by advisory id alone.
  • First scan. With no earlier report for the asset and scan type, the tab says there is nothing to compare against.
  • Never blocks a scan. If building the diff fails, the scan still completes and the tab is empty.

Policy

All ecosystems share the same policy engine. Policy is stored as JSON at .ubel/local/policy/config.json, relative to the scan root — <project-root> for project, secrets, and license scans, the editor's extensions directory for the extensions scan, and ~ for the host-platform scan. The file is created with defaults on the first scan.

The extension has no threshold / block-unknown / per-run-flag commands (those belong to the CLI) — to change policy, edit config.json directly. Default policy:

{
  "severity_threshold": "high",
  "block_unknown_vulnerabilities": true,
  "license_risk_threshold": "none",
  "block_unknown_license_risk": false,
  "block_kev": true,
  "epss_threshold": 0.1,
  "block_undetermined": false,
  "reachable_only": false
}
Field Values Default Behaviour
severity_threshold low medium high critical none high Block packages at or above this severity
block_unknown_vulnerabilities true false true Block packages with CVEs but no CVSS score
license_risk_threshold none low medium high none Block packages whose license risk is at or above this level; never blocks on unknown regardless of setting (see block_unknown_license_risk)
block_unknown_license_risk true false false Separately block packages whose license couldn't be classified at all
block_kev true false true Block any vulnerability listed in the CISA KEV catalog, regardless of severity
epss_threshold fraction in (0, 1], or "none" 0.1 Block any vulnerability whose EPSS score is at or above this value (0.1 = 10%), regardless of severity
block_undetermined true false false Block when any component's state is undetermined — no version, resolved from git / a tarball URL / a local path, or a non-Python conda package. These aren't matched against OSV/NVD, so a clean result says nothing about them. Not affected by reachable_only
reachable_only true false false Apply the severity / unknown-severity / KEV / EPSS rules to reachable vulnerabilities only. One the reachability analysis confirms is unreachable is still listed in the report but marked allow (policy_exempt: "unreachable") and never blocks; one with no reachability result is not exempted. By default reachability is informational and does not change the verdict
Infections (MAL-*) — always blocked Cannot be toggled; unconditionally blocked

The severity threshold is inclusive — high blocks both high and critical. KEV and EPSS blocking apply on top of it: a Low-severity vulnerability that is known to be exploited, or whose EPSS score is at or above the threshold, still blocks. Both rules need data from an external feed; if it couldn't be fetched, that rule can't fire for the run (see Exploit Intelligence), and a passing verdict says so. Policy files created before these fields existed pick up the defaults automatically. Setting none disables severity blocking but infections are still blocked. license_risk_threshold/block_unknown_license_risk are opt-in (both default off) since license-risk tolerance varies by org and license detection has real gaps (free-text licenses, missing metadata); every extension scan runs in health mode, where these two gates are active.


Coverage at a Glance

Surface
Source repos & monorepos
Exposed secrets in source
Developer machines (Windows / Linux)
VS Code extension

Repos and Monorepos

UBEL walks the entire directory tree and detects all supported ecosystems in a single pass — no per-language configuration needed. Monorepos with mixed stacks (e.g. a Node.js frontend, Python backend, and Rust service in the same repo) are fully covered in one invocation.

Developer Machines

The VS Code extension (Ctrl+Alt+P) and the ubel-platform CLI binary scan the host machine: OS, installed runtimes, browsers, developer tools, and security software. Vulnerabilities are matched using CPE 2.3 identifiers against the CVE/NVD database.

This surface catches what dependency scanners miss — a vulnerable version of Git, an unpatched Python interpreter, or an outdated Docker Desktop install.

Windows

Detected via registry probes and PowerShell — no elevated privileges required.

Category Components
Operating system Windows 10 / 11 (build-accurate CPE version)
Security Windows Defender
Runtimes Node.js, Python, PHP, Go, Rust, Ruby, JRE, JDK
.NET All installed .NET Core / Desktop / ASP.NET runtimes (multi-version)
Browsers Chrome, Firefox, Microsoft Edge
Developer tools Git, Docker Desktop, Visual Studio Code / Visual Studio IDE, Cursor, VSCodium, Claude Code
Shell PowerShell

Linux

Detected by reading the system package database directly.

Distribution Package manager Source PURL type
Ubuntu dpkg /var/lib/dpkg/status pkg:deb/ubuntu/
Debian dpkg /var/lib/dpkg/status pkg:deb/debian/
Alpine / Alpaquita apk /lib/apk/db/installed pkg:apk/alpine/
Red Hat / RHEL rpm rpm database (rpmdb.sqlite or Packages) pkg:rpm/redhat/
AlmaLinux rpm rpm database (rpmdb.sqlite or Packages) pkg:rpm/almalinux/
Rocky Linux rpm rpm database (rpmdb.sqlite or Packages) pkg:rpm/rocky-linux/
CentOS / Fedora rpm rpm database (rpmdb.sqlite or Packages) pkg:rpm/redhat/

The distribution is read from /etc/os-release (/usr/lib/os-release as a fallback): ID is tried first, then each ID_LIKE token, so derivatives such as Amazon Linux (ID_LIKE="centos rhel fedora") resolve to their parent family. A distribution that matches none of the above (e.g. SUSE, Arch) stops the scan with an "Unsupported OS ecosystem" error rather than guessing.

Each package entry includes its binary install paths and direct dependency edges as reported by the package database.

No package-manager binaries are executed to read installed packages (dpkg, apk and rpm are not required): the rpm database is parsed in-process and its format is detected from the file's magic bytes — Berkeley DB Packages (RHEL/CentOS/Alma/Rocky ≤ 8, Amazon Linux 2) and SQLite rpmdb.sqlite (RHEL/Alma/Rocky ≥ 9, Fedora, Amazon Linux 2023). NDB (Packages.db, SUSE) is not supported and stops the scan with a clear error.

Each package is also tagged with the distro release it was read from (e.g. almalinux-8.10), which UBEL uses to match advisories to that release — see OS Package Release Matching.


Supported Ecosystems (Project Scan)

Ecosystem Package Manager Resolved From
Node.js npm, pnpm, yarn, bun node_modules/ (on-disk walk)
Python pip / virtualenv .venv, venv, virtual environment directories
PHP Composer vendor/, composer.lock
Rust Cargo Cargo.lock (next to a Cargo.toml; workspace members are followed)
Go Go Modules go.mod (module root and selected versions) cross-referenced with go.sum (optional)
C#/.NET NuGet packages.lock.json, falling back to obj/project.assets.json
Java Maven pom.xml declarations (parent poms, <dependencyManagement>, properties and modules followed); full transitive tree from .ubel/maven-deps.txt when present. Gradle is not read
Ruby Bundler Gemfile.lock (with a Gemfile alongside)
Swift SwiftPM, Carthage Package.resolved / .build/workspace-state.json / Cartfile.resolved
Flutter/Dart pub pubspec.lock / .dart_tool/package_config.json

Swift and Flutter/Dart notes. PURLs are pkg:swift/<host>/<owner>/<repo>@<version> (OSV ecosystem SwiftURL) and pkg:pub/<name>@<version>, with ?repository_url= / ?vcs_url= qualifiers on pub packages from a non-pub.dev registry or a git repository. Local packages (SwiftPM fileSystem / localSourceControl, pub path) and pub sdk packages are skipped, and CocoaPods (Podfile.lock) is intentionally not scanned because OSV has no CocoaPods ecosystem. A SwiftPM pin on a branch or bare commit is inventoried with an empty version and dropped from vulnerability lookups. Swift lockfiles carry no dev/prod signal, so every Swift package is prod; for pub, direct dev → dev and everything else → prod. Neither lockfile records a dependency graph or license data, so these packages have no introduced-by/parent edges and their license is unknown.


Reports

Every scan writes a self-contained interactive HTML report plus a machine-readable JSON file, and — depending on the scan type — a SBOM and SARIF file. A scan-type tag sits between the file name and the extension (<file_name>.<tag>.<extension>), so each command keeps its own set of files:

Command <tag> JSON HTML SBOM SARIF
Scan Project sca ✅ ✅ ✅ ✅
Scan VS Code Extensions vscode_sca / cursor_sca / vscodium_sca (the editor you run it in) ✅ ✅ ✅ ✅
Scan Host Platform (Windows / Linux) os_sca ✅ ✅ ✅ ❌
Scan project for Exposed Secrets secrets ✅ ✅ ❌ ❌
Scan project for License Compliance licenses ✅ ✅ ❌ ❌

On a platform other than Windows or Linux, Scan Host Platform falls back to the sca tag.

File (SCA scans shown) Format Notes
latest.sca.html Self-contained HTML Works fully offline, no server needed
latest.sca.json JSON Full report, including the executive summary, suggested fixes, reachability, KEV/EPSS, and compliance data
latest.sca.cdx.json CycloneDX v1.6 SBOM Components, dependency graph, and vulnerabilities as VEX; secrets, license, KEV/EPSS, and suggested-fix data in properties
latest.sca.sarif.json SARIF 2.1.0 Separate runs for dependency vulnerabilities, secrets, and license compliance

For the other commands, swap the tag: latest.os_sca.html, latest.secrets.html, latest.licenses.json, and so on (only the formats marked ✅ above exist for that tag). Open Report in each notification opens that command's latest.<tag>.html.

Scan target Report path
Workspace <project-root>/.ubel/reports/latest.sca.*
Secrets-only scan <project-root>/.ubel/reports/latest.secrets.*
License-only scan <project-root>/.ubel/reports/latest.licenses.*
VS Code / VS Codium / Cursor extensions ~/.vscode/extensions/.ubel/reports/latest.vscode_sca.* or ~/.vscode-oss/extensions/.ubel/reports/latest.vscodium_sca.* or ~/.cursor/extensions/.ubel/reports/latest.cursor_sca.*
Host platform ~/.ubel/reports/latest.os_sca.* on Windows / Linux, latest.sca.* elsewhere (every report also carries the machine id from ~/.ubel/ubel_machine.json)

Written atomically. Every report file UBEL writes — the latest.<tag>.* files above, the timestamped history zips and the latest_reports copy below — is first written to a temporary file in the same folder and then renamed over the target in one step. A script, CI job, or browser tab that reads latest.sca.json / latest.sca.html while a scan is finishing therefore gets either the complete previous report or the complete new one, never a partial file. If a write fails, the temporary file is removed, the previous file is left as it was, and the scan reports the error (on Windows, replacing a report can fail while another program holds it open — close it and scan again).

Previous scans are retained as timestamped zipped snapshots (<timestamp>.<tag>.zip, where <timestamp> is YYYY_MM_DD__HH_MM_SS in UTC, e.g. 2026_10_10__12_30_05.sca.zip). Inside each zip the files are always named report — report.<tag>.json, report.<tag>.html, and, where the scan type produces them, report.<tag>.cdx.json and report.<tag>.sarif.json. Zips are stored in ~/.ubel/history/<mode>/:

Every command files its zip in the same place — ~/.ubel/history/<asset_id>/. There is no per-command folder; the tag in the file name tells them apart: sca (Scan Project), <editor>_sca (Scan VS Code Extensions), os_sca on Windows / Linux (Scan Host Platform), secrets (Scan project for Exposed Secrets) and licenses (Scan project for License Compliance).

All projects and commands share this one folder, but each asset's zips live in a sub-folder named after its asset id — ~/.ubel/history/<asset_id>/<timestamp>.<tag>.zip, e.g. ~/.ubel/history/3f6c2a9e1b7d4c589a426e0d8b5f7a13/2026_10_10__12_30_05.sca.zip. The id comes from ubel_asset.json in the .ubel folder of whatever was scanned — <project-root>/.ubel/ubel_asset.json for the workspace, secrets and license commands, and the .ubel folder inside the extensions directory for Scan VS Code Extensions. The first scan in a folder creates it; the id never changes afterwards:

{ "asset_id": "3f6c2a9e1b7d4c589a426e0d8b5f7a13", "asset_name": "shop", "created_at": "2026-10-10T12:30:05.000Z" }

asset_name is a readable label — the git repository name when the workspace is a checkout, otherwise the folder name — and you can edit it. The id and name are also written into each report and shown as Asset and Asset ID in the HTML report's Scan Info panel, so a zip that has been moved or renamed still says where it came from. Each report also carries an asset_type (shown as Asset type), which depends on the command: Scan Project, Scan project for Exposed Secrets and Scan project for License Compliance are codebase, Scan VS Code Extensions is editor (and gets one asset id per editor), and Scan Host Platform is machine — see Asset types in the CLI docs. The type is not stored in ubel_asset.json; it is set per scan. Every report also has its own report_id, UBEL-<tag>-<asset_id>-<uuid> (for example UBEL-sca-… for Scan Project), the same in the report's JSON, HTML, SBOM and SARIF — see Report ID.

Scan Host Platform scans your home directory, so its asset id is the one in ~/.ubel/ubel_asset.json. Separately, every report carries a machine_id (shown as Machine ID in the HTML) from ~/.ubel/ubel_machine.json — the id of this machine, an id only, no name; the CLI's ubel-apt / ubel-dnf / ubel-yum use it as their asset id. If a ubel_asset.json can't be written, the scan still runs and its zip is written straight into ~/.ubel/history/ and the latest zip into ~/.ubel/latest_reports/. Zips from earlier versions stay where they are (~/.ubel/history/<mode>/<asset_id>/). If two scans of the same folder finish in the same second, the later zip gets a _2 suffix instead of overwriting the first.

Latest zip per scan type (latest_reports)

The newest scan of each tag is also kept in a folder of its own, apart from the history — ~/.ubel/latest_reports/<asset_id>/latest.<tag>.zip (one folder per asset, no mode folder):

Command Latest zip
Scan Project ~/.ubel/latest_reports/<asset_id>/latest.sca.zip
Scan VS Code Extensions ~/.ubel/latest_reports/<asset_id>/latest.<editor>_sca.zip (vscode_sca, cursor_sca or vscodium_sca)
Scan Host Platform (Windows / Linux) ~/.ubel/latest_reports/<asset_id>/latest.os_sca.zip
Scan project for Exposed Secrets ~/.ubel/latest_reports/<asset_id>/latest.secrets.zip
Scan project for License Compliance ~/.ubel/latest_reports/<asset_id>/latest.licenses.zip

It is a copy of the zip the scan just wrote and has the same contents (report.<tag>.json, .html, and .cdx.json / .sarif.json where applicable). It is refreshed after every scan, so a script can read the newest scan of a project from one fixed path without sorting timestamps, while history/ holds only timestamped zips and is never touched by this. The asset_id is the one in that folder's ubel_asset.json. The CLI uses the same folders, so the extension and the CLI share one history and one latest_reports tree.

  • Projects scanned before this was added get their latest_reports zip on their next scan; nothing is moved or deleted from history/.
  • If the copy can't be written (for example a read-only home directory), the scan still completes with a warning and the history zip is unaffected; the previous latest zip, if any, is left in place.

Extension vs. CLI

The extension runs the same engine as the CLI's health mode. These parts of the @arcane-spark/ubel-node package are not in the extension:

  • The install-time firewall (check / install modes) for npm, pnpm, bun, composer, pip, uv, pipx, conda, cargo, apt, dnf, and yum, including lockfile backup/revert and TOCTOU integrity protection (always with install scripts blocked)
  • ubel-docker container-image scanning
  • Fixed-configuration CLIs for AI-agent sandboxes and CI/CD (ubel-agent, ubel-cicd)
  • The git pre-commit hook (install-hook / uninstall-hook) and the ubel-secrets extras: git-history scanning (--history), staged-changes scanning (--staged), and baselining (--write-baseline) — the extension scans the working tree only
  • Persistent policy modes and per-run policy flags (--threshold, --block-kev, --epss-threshold, …) — in the extension, edit config.json instead
  • The GitHub Action

Requirements

  • Node.js >=18.0.0
  • VS Code ^1.85.0 (extension only)
  • Network access to osv.dev and the NVD API (or your configured mirrors) for vulnerability lookups; KEV/EPSS lookups are best-effort

Privacy

UBEL is fully local. The external calls it makes are:

Service Purpose Failure behavior
osv.dev public API Vulnerability lookups — receives package PURLs (package name + version) Scan fails with an error
NVD API Host-platform CPE lookups and advisory enrichment Scan fails with an error
CISA KEV catalog (www.cisa.gov) Known-exploited flag Best-effort — scan completes, KEV fields are null
FIRST EPSS API (api.first.org) Exploit-likelihood score, queried by CVE id Best-effort — scan completes, EPSS fields are null

No file contents, no dependency graphs, no machine identifiers, and no telemetry are sent anywhere. UBEL does not look up your public IP address, and reports do not record your local IP addresses or ports. Secrets findings never leave the machine at all — match previews shown in reports are redacted before being written to disk. If an OSV or NVD lookup can't be completed, the scan ends with an error message rather than reporting a clean result.

The OSV and NVD endpoints can be redirected to an internal mirror by setting UBEL_OSV_ENDPOINT / UBEL_NVD_ENDPOINT in the environment the editor was launched from (e.g. via VS Code's own terminal.integrated.env.* settings, or the OS environment) — useful for air-gapped or regulated environments where those calls need to stay on an internal network. A mirror must expose the same path shape as the public API, and the "view online" reference links in reports still point at the public sites. There is currently no endpoint override for KEV/EPSS: in a fully air-gapped setup the scan runs without that data and says so. See sca/README.md for details; this extension reads the same engine, so the same variables apply.


License

Source-available, internal use only: you may use UBEL on your own projects and systems and within your own organization. Redistribution, wrapping, and offering it as a service to others are not permitted.
See LICENSE.md for the full terms, or contact ala.bouali.1997@gmail.com to discuss licensing beyond internal use.

  • Contact us
  • Jobs
  • Privacy
  • Manage cookies
  • Terms of use
  • Trademarks
  • Your Privacy Choices
  • Consumer Health Privacy
© 2026 Microsoft