This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 83126c90cf60 CAMEL-23869: Add CVE Audit tab to camel-tui
83126c90cf60 is described below
commit 83126c90cf60b830a1f3fea5d12d54eae90cf4b0
Author: Claus Ibsen <[email protected]>
AuthorDate: Sat Jul 4 23:02:54 2026 +0200
CAMEL-23869: Add CVE Audit tab to camel-tui
New tab that scans integration classpath dependencies against the
OSV.dev vulnerability database using the batch query API. Features
master/detail layout with severity-colored table, MarkdownView detail
panel with CVSS vector, and global caching across tab visits.
CAMEL-23831: Remove redundant sort:xxx from tab titles in camel-tui
The column headers already show sort indicators so the sort info in
the title was visual noise. Other title metadata (filter, scope,
duration) is kept since it is not shown elsewhere.
CAMEL-23869: Fix NPE and rescan on integration switch in CVE Audit tab
Fix NPE when no vulnerabilities found (null passed to tableState.select).
Trigger rescan automatically when switching integrations via F3.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---
.../dsl/jbang/core/commands/tui/BeansTab.java | 4 +-
.../dsl/jbang/core/commands/tui/BrowseTab.java | 2 +-
.../jbang/core/commands/tui/ConfigurationTab.java | 2 +-
.../dsl/jbang/core/commands/tui/ConsumersTab.java | 2 +-
.../dsl/jbang/core/commands/tui/CveAuditTab.java | 682 +++++++++++++++++++++
.../dsl/jbang/core/commands/tui/DataSourceTab.java | 2 +-
.../dsl/jbang/core/commands/tui/EndpointsTab.java | 2 +-
.../dsl/jbang/core/commands/tui/ErrorsTab.java | 2 +-
.../jbang/core/commands/tui/HeapHistogramTab.java | 4 +-
.../dsl/jbang/core/commands/tui/HistoryTab.java | 2 +-
.../camel/dsl/jbang/core/commands/tui/HttpTab.java | 2 +-
.../dsl/jbang/core/commands/tui/InflightTab.java | 2 +-
.../dsl/jbang/core/commands/tui/McpFacade.java | 2 +-
.../dsl/jbang/core/commands/tui/MemoryLeakTab.java | 4 +-
.../dsl/jbang/core/commands/tui/MetricsTab.java | 2 +-
.../dsl/jbang/core/commands/tui/OsvClient.java | 338 ++++++++++
.../dsl/jbang/core/commands/tui/PopupManager.java | 32 +-
.../dsl/jbang/core/commands/tui/RoutesTab.java | 6 +-
.../dsl/jbang/core/commands/tui/SqlTraceTab.java | 2 +-
.../dsl/jbang/core/commands/tui/TabRegistry.java | 40 +-
.../dsl/jbang/core/commands/tui/ThreadsTab.java | 4 +-
.../core/commands/tui/ConsumersTabRenderTest.java | 2 +-
.../core/commands/tui/CveAuditTabRenderTest.java | 168 +++++
.../core/commands/tui/DataSourceTabRenderTest.java | 2 +-
.../core/commands/tui/EndpointsTabRenderTest.java | 4 +-
.../core/commands/tui/ErrorsTabRenderTest.java | 4 +-
.../jbang/core/commands/tui/HttpTabRenderTest.java | 4 +-
.../core/commands/tui/InflightTabRenderTest.java | 4 +-
.../core/commands/tui/RoutesTabRenderTest.java | 4 +-
.../core/commands/tui/SqlTraceTabRenderTest.java | 2 +-
30 files changed, 1266 insertions(+), 66 deletions(-)
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/BeansTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/BeansTab.java
index 35a7e3de6824..212153e73d71 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/BeansTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/BeansTab.java
@@ -211,8 +211,8 @@ class BeansTab extends AbstractTableTab {
String mode = filterModes()[filterIndex];
String title = filterTerm != null
- ? String.format(" Beans [%d] sort:%s scope:%s filter:\"%s\" ",
visible.size(), sort, mode, filterTerm)
- : String.format(" Beans [%d] sort:%s scope:%s ",
visible.size(), sort, mode);
+ ? String.format(" Beans [%d] scope:%s filter:\"%s\" ",
visible.size(), mode, filterTerm)
+ : String.format(" Beans [%d] scope:%s ", visible.size(), mode);
Table table = Table.builder()
.rows(rows)
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/BrowseTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/BrowseTab.java
index 480f707f5e9d..de5bd8507ed3 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/BrowseTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/BrowseTab.java
@@ -301,7 +301,7 @@ class BrowseTab extends AbstractTab {
Cell.from(""), Cell.from(""), Cell.from("")));
}
- String title = String.format(" Browse [%d] sort:%s ", sorted.size(),
sort);
+ String title = String.format(" Browse [%d] ", sorted.size());
Table table = Table.builder()
.rows(rows)
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConfigurationTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConfigurationTab.java
index 1ac4aef18b68..b0339299c619 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConfigurationTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConfigurationTab.java
@@ -165,7 +165,7 @@ class ConfigurationTab extends AbstractTableTab {
rows.add(emptyRow("No properties", 3));
}
- String title = String.format(" Configuration [%d] sort:%s ",
props.size(), sort);
+ String title = String.format(" Configuration [%d] ", props.size());
Table table = Table.builder()
.rows(rows)
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumersTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumersTab.java
index 2bea6caa2865..bb94296e810f 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumersTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumersTab.java
@@ -135,7 +135,7 @@ class ConsumersTab extends AbstractTableTab {
Constraint.length(22),
Constraint.fill())
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
- .title(" Consumers sort:" + sort + " ").build())
+ .title(" Consumers ").build())
.build();
lastTableArea = area;
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CveAuditTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CveAuditTab.java
new file mode 100644
index 000000000000..2aba0c006de7
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CveAuditTab.java
@@ -0,0 +1,682 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import dev.tamboui.layout.Constraint;
+import dev.tamboui.layout.Layout;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.markdown.MarkdownView;
+import dev.tamboui.style.Color;
+import dev.tamboui.style.Style;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.Line;
+import dev.tamboui.text.Span;
+import dev.tamboui.text.Text;
+import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.tui.event.MouseEvent;
+import dev.tamboui.widgets.block.Block;
+import dev.tamboui.widgets.block.BorderType;
+import dev.tamboui.widgets.block.Borders;
+import dev.tamboui.widgets.paragraph.Paragraph;
+import dev.tamboui.widgets.table.Cell;
+import dev.tamboui.widgets.table.Row;
+import dev.tamboui.widgets.table.Table;
+import org.apache.camel.dsl.jbang.core.common.PathUtils;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+
+import static org.apache.camel.dsl.jbang.core.commands.tui.TuiHelper.*;
+
+class CveAuditTab extends AbstractTableTab {
+
+ private static final List<String> SEVERITY_ORDER = List.of("CRITICAL",
"HIGH", "MEDIUM", "LOW", "UNKNOWN");
+
+ private final OsvClient osvClient = new OsvClient();
+ private final AtomicBoolean loading = new AtomicBoolean(false);
+
+ private int detailScroll;
+
+ private List<ClasspathTab.JarEntry> jarEntries = Collections.emptyList();
+ private List<VulnGroup> allGroups = Collections.emptyList();
+ private String lastPid;
+ private String errorMessage;
+ private boolean dataLoaded;
+ private int scannedCount;
+ private int totalCount;
+
+ CveAuditTab(MonitorContext ctx) {
+ super(ctx, "severity", "id", "artifact");
+ }
+
+ @Override
+ protected int getRowCount() {
+ return allGroups.size();
+ }
+
+ @Override
+ public void onTabSelected() {
+ String pid = ctx.selectedPid;
+ if (pid != null && !pid.equals(lastPid)) {
+ lastPid = pid;
+ allGroups = Collections.emptyList();
+ dataLoaded = false;
+ }
+ if (!dataLoaded) {
+ loadAndScan();
+ }
+ }
+
+ @Override
+ public void onIntegrationChanged() {
+ allGroups = Collections.emptyList();
+ jarEntries = Collections.emptyList();
+ lastPid = null;
+ errorMessage = null;
+ dataLoaded = false;
+ detailScroll = 0;
+ loading.set(false);
+ if (ctx.selectedPid != null) {
+ lastPid = ctx.selectedPid;
+ loadAndScan();
+ }
+ }
+
+ @Override
+ public boolean handleKeyEvent(KeyEvent ke) {
+ if (ke.isChar('r')) {
+ rescan();
+ return true;
+ }
+ return super.handleKeyEvent(ke);
+ }
+
+ @Override
+ protected boolean handleTabKeyEvent(KeyEvent ke) {
+ if (ke.isPageUp() || ke.isKey(KeyCode.PAGE_UP)) {
+ detailScroll = Math.max(0, detailScroll - 10);
+ return true;
+ }
+ if (ke.isPageDown() || ke.isKey(KeyCode.PAGE_DOWN)) {
+ detailScroll += 10;
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean handleMouseEvent(MouseEvent me, Rect area) {
+ boolean handled = super.handleMouseEvent(me, area);
+ if (handled) {
+ detailScroll = 0;
+ }
+ return handled;
+ }
+
+ @Override
+ public boolean handleEscape() {
+ return false;
+ }
+
+ @Override
+ public void navigateUp() {
+ tableState.selectPrevious();
+ detailScroll = 0;
+ }
+
+ @Override
+ public void navigateDown() {
+ tableState.selectNext(allGroups.size());
+ detailScroll = 0;
+ }
+
+ @Override
+ protected void renderContent(Frame frame, Rect area, IntegrationInfo info)
{
+ if (loading.get() && allGroups.isEmpty()) {
+ String msg = totalCount > 0
+ ? String.format(" Scanning %d dependencies...",
totalCount)
+ : " Loading classpath...";
+ frame.renderWidget(
+ Paragraph.builder()
+ .text(Text.from(Line.from(Span.styled(msg,
Style.EMPTY.dim()))))
+
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+ .title(" CVE Audit ").build())
+ .build(),
+ area);
+ return;
+ }
+
+ if (errorMessage != null && allGroups.isEmpty()) {
+ frame.renderWidget(
+ Paragraph.builder()
+ .text(Text.from(Line.from(
+ Span.styled(" " + errorMessage,
Style.EMPTY.fg(Color.LIGHT_RED)))))
+
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+ .title(" CVE Audit ").build())
+ .build(),
+ area);
+ return;
+ }
+
+ if (dataLoaded && allGroups.isEmpty()) {
+ frame.renderWidget(
+ Paragraph.builder()
+ .text(Text.from(
+ Line.from(Span.raw("")),
+ Line.from(Span.styled(" No
vulnerabilities found",
+ Style.EMPTY.fg(Color.GREEN))),
+ Line.from(Span.raw("")),
+ Line.from(Span.styled(
+ String.format(" Scanned %d
dependencies", scannedCount),
+ Style.EMPTY.dim()))))
+
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+ .title(" CVE Audit ").build())
+ .build(),
+ area);
+ return;
+ }
+
+ List<Rect> chunks = Layout.vertical()
+ .constraints(Constraint.fill(), Constraint.percentage(60))
+ .split(area);
+ renderTable(frame, chunks.get(0));
+ renderDetail(frame, chunks.get(1));
+ }
+
+ private void renderTable(Frame frame, Rect area) {
+ List<VulnGroup> visible = sortedGroups();
+ List<Row> rows = new ArrayList<>();
+ for (VulnGroup group : visible) {
+ String firstArtifact = group.affectedGavs.isEmpty() ? "" :
group.affectedGavs.get(0);
+ int artCount = group.affectedGavs.size();
+ String artDisplay = artCount > 1 ? firstArtifact + " (+" +
(artCount - 1) + ")" : firstArtifact;
+
+ rows.add(Row.from(
+ Cell.from(Span.styled(group.severity != null ?
group.severity : "", severityStyle(group.severity))),
+ Cell.from(Span.styled(group.canonicalId != null ?
group.canonicalId : "", Style.EMPTY.bold())),
+ Cell.from(Span.styled(artDisplay,
Style.EMPTY.fg(Color.CYAN))),
+ Cell.from(Span.styled(group.summary != null ?
group.summary : "", Style.EMPTY.dim()))));
+ }
+
+ if (rows.isEmpty() && dataLoaded) {
+ rows.add(emptyRow("No matching vulnerabilities", 4));
+ }
+
+ String title = buildTitle();
+
+ Table table = Table.builder()
+ .rows(rows)
+ .header(Row.from(
+ Cell.from(Span.styled(sortLabel("SEVERITY",
"severity"), sortStyle("severity"))),
+ Cell.from(Span.styled(sortLabel("CVE ID", "id"),
sortStyle("id"))),
+ Cell.from(Span.styled(sortLabel("ARTIFACT",
"artifact"), sortStyle("artifact"))),
+ Cell.from(Span.styled("SUMMARY", Style.EMPTY.dim()))))
+ .widths(
+ Constraint.length(10),
+ Constraint.length(20),
+ Constraint.percentage(30),
+ Constraint.fill())
+ .highlightStyle(Theme.selectionBg())
+ .highlightSpacing(Table.HighlightSpacing.ALWAYS)
+
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).title(title).build())
+ .build();
+
+ lastTableArea = area;
+ frame.renderStatefulWidget(table, area, tableState);
+ renderScrollbar(frame, visible.size());
+ }
+
+ private void renderDetail(Frame frame, Rect area) {
+ Integer sel = tableState.selected();
+ List<VulnGroup> visible = sortedGroups();
+ if (sel == null || sel < 0 || sel >= visible.size()) {
+ frame.renderWidget(
+ Paragraph.builder()
+ .text(Text.from(Line.from(Span.styled(" Select a
vulnerability", Style.EMPTY.dim()))))
+
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+ .title(" Details ").build())
+ .build(),
+ area);
+ return;
+ }
+
+ VulnGroup group = visible.get(sel);
+ String title = " " + group.canonicalId + " ";
+
+ StringBuilder md = new StringBuilder();
+ md.append("**Severity:** ").append(group.severity != null ?
group.severity : "UNKNOWN");
+ if (group.cvssVector != null) {
+ md.append(" (`").append(group.cvssVector).append("`)");
+ }
+ md.append(" \n");
+ if (group.published != null) {
+ String date = group.published.length() > 10 ?
group.published.substring(0, 10) : group.published;
+ md.append("**Published:** ").append(date).append(" \n");
+ }
+ md.append("\n");
+ if (group.summary != null) {
+ md.append(group.summary).append("\n\n");
+ }
+ md.append("**Affected artifacts:**\n");
+ for (String gav : group.affectedGavs) {
+ md.append("- `").append(gav).append("`\n");
+ }
+ if (!group.fixedVersions.isEmpty()) {
+ md.append("\n**Fixed in:** ").append(String.join(", ",
group.fixedVersions)).append("\n");
+ }
+ if (!group.aliases.isEmpty()) {
+ md.append("\n**Aliases:** ").append(String.join(", ",
group.aliases)).append("\n");
+ }
+
md.append("\nhttps://osv.dev/vulnerability/").append(group.canonicalId).append("\n");
+ if (group.details != null && !group.details.isEmpty()) {
+ md.append("\n---\n\n").append(group.details).append("\n");
+ }
+
+ frame.renderWidget(
+ MarkdownView.builder()
+ .source(md.toString())
+ .scroll(detailScroll)
+
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).title(title).build())
+ .build(),
+ area);
+ }
+
+ private String buildTitle() {
+ StringBuilder sb = new StringBuilder(" CVE Audit (scanned: ");
+ sb.append(scannedCount).append(", vulnerable:
").append(allGroups.size()).append(")");
+ Map<String, Integer> counts = new LinkedHashMap<>();
+ for (String sev : SEVERITY_ORDER) {
+ int count = 0;
+ for (VulnGroup g : allGroups) {
+ if (sev.equals(g.severity)) {
+ count++;
+ }
+ }
+ if (count > 0) {
+ counts.put(sev, count);
+ }
+ }
+ if (!counts.isEmpty()) {
+ sb.append(" [");
+ boolean first = true;
+ for (Map.Entry<String, Integer> e : counts.entrySet()) {
+ if (!first) {
+ sb.append(", ");
+ }
+ sb.append(e.getValue()).append(" ").append(e.getKey());
+ first = false;
+ }
+ sb.append("]");
+ }
+ sb.append(" ");
+ return sb.toString();
+ }
+
+ static Style severityStyle(String severity) {
+ if (severity == null) {
+ return Style.EMPTY.dim();
+ }
+ return switch (severity) {
+ case "CRITICAL" -> Theme.error().bold();
+ case "HIGH" -> Theme.error();
+ case "MEDIUM" -> Theme.warning();
+ case "LOW" -> Style.EMPTY.dim();
+ default -> Style.EMPTY.dim();
+ };
+ }
+
+ @Override
+ public void renderFooter(List<Span> spans) {
+ hint(spans, "Esc", "back");
+ hint(spans, "r", "rescan");
+ super.renderFooter(spans);
+ hintLast(spans, "↑↓", "navigate");
+ }
+
+ private void rescan() {
+ if (loading.get()) {
+ return;
+ }
+ osvClient.clearCache(jarEntries);
+ allGroups = Collections.emptyList();
+ dataLoaded = false;
+ errorMessage = null;
+ loadAndScan();
+ }
+
+ private void loadAndScan() {
+ if (ctx.selectedPid == null || ctx.runner == null) {
+ return;
+ }
+ if (!loading.compareAndSet(false, true)) {
+ return;
+ }
+
+ String pid = ctx.selectedPid;
+ ctx.runner.scheduler().execute(() -> {
+ try {
+ Path outputFile = ctx.getOutputFile(pid);
+ PathUtils.deleteFile(outputFile);
+
+ JsonObject action = new JsonObject();
+ action.put("action", "jvm");
+
+ Path actionFile = ctx.getActionFile(pid);
+ PathUtils.writeTextSafely(action.toJson(), actionFile);
+
+ JsonObject response = pollJsonResponse(outputFile, 5000);
+ PathUtils.deleteFile(outputFile);
+
+ if (response == null) {
+ applyResult(Collections.emptyList(),
Collections.emptyList(), "No response from integration");
+ return;
+ }
+
+ List<String> paths = new ArrayList<>();
+ Object cp = response.get("classpath");
+ if (cp instanceof JsonArray arr) {
+ for (Object item : arr) {
+ paths.add(String.valueOf(item));
+ }
+ }
+
+ if (paths.isEmpty()) {
+ applyResult(Collections.emptyList(),
Collections.emptyList(), "No classpath information available");
+ return;
+ }
+
+ List<ClasspathTab.JarEntry> jars = new ArrayList<>();
+ for (String path : paths) {
+ ClasspathTab.JarEntry entry =
ClasspathTab.parseJarEntry(path);
+ if (entry.groupId() != null) {
+ jars.add(entry);
+ }
+ }
+
+ updateProgress(jars);
+
+ Map<String, List<OsvClient.Vulnerability>> vulnMap =
osvClient.queryBatch(jars);
+
+ List<VulnGroup> groups = buildVulnGroups(vulnMap);
+ groups.sort(Comparator
+ .comparingInt((VulnGroup g) ->
severityIndex(g.severity))
+ .thenComparing(g -> g.canonicalId));
+
+ applyResult(jars, groups, null);
+ } catch (Exception e) {
+ applyResult(Collections.emptyList(), Collections.emptyList(),
"Error: " + e.getMessage());
+ } finally {
+ loading.set(false);
+ }
+ });
+ }
+
+ private void updateProgress(List<ClasspathTab.JarEntry> jars) {
+ if (ctx.runner == null) {
+ return;
+ }
+ ctx.runner.runOnRenderThread(() -> {
+ totalCount = jars.size();
+ });
+ }
+
+ private void applyResult(List<ClasspathTab.JarEntry> jars, List<VulnGroup>
groups, String error) {
+ if (ctx.runner == null) {
+ return;
+ }
+ ctx.runner.runOnRenderThread(() -> {
+ jarEntries = jars;
+ allGroups = groups;
+ errorMessage = error;
+ dataLoaded = true;
+ scannedCount = jars.size();
+ if (!allGroups.isEmpty()) {
+ tableState.select(0);
+ }
+ detailScroll = 0;
+ });
+ }
+
+ private List<VulnGroup> sortedGroups() {
+ List<VulnGroup> result = new ArrayList<>(allGroups);
+ result.sort((a, b) -> {
+ int cmp = switch (sort) {
+ case "id" -> compareStr(a.canonicalId, b.canonicalId);
+ case "artifact" -> compareStr(
+ a.affectedGavs.isEmpty() ? "" : a.affectedGavs.get(0),
+ b.affectedGavs.isEmpty() ? "" : b.affectedGavs.get(0));
+ default -> Integer.compare(severityIndex(a.severity),
severityIndex(b.severity));
+ };
+ return sortReversed ? -cmp : cmp;
+ });
+ return result;
+ }
+
+ static List<VulnGroup> buildVulnGroups(Map<String,
List<OsvClient.Vulnerability>> vulnMap) {
+ Map<String, String> aliasToCanonical = new HashMap<>();
+ Map<String, VulnGroup> groups = new LinkedHashMap<>();
+
+ for (Map.Entry<String, List<OsvClient.Vulnerability>> entry :
vulnMap.entrySet()) {
+ String gav = entry.getKey();
+ for (OsvClient.Vulnerability vuln : entry.getValue()) {
+ String canonicalId = resolveCanonicalId(vuln,
aliasToCanonical);
+
+ VulnGroup group = groups.get(canonicalId);
+ if (group == null) {
+ group = new VulnGroup(
+ canonicalId, normalizeSeverity(vuln.severity()),
vuln.cvssVector(), vuln.summary(),
+ vuln.details(), vuln.published(), new
ArrayList<>(), new ArrayList<>(), new ArrayList<>());
+ groups.put(canonicalId, group);
+ aliasToCanonical.put(vuln.id(), canonicalId);
+ for (String alias : vuln.aliases()) {
+ aliasToCanonical.put(alias, canonicalId);
+ }
+ }
+
+ if (!group.affectedGavs.contains(gav)) {
+ group.affectedGavs.add(gav);
+ }
+ for (String alias : vuln.aliases()) {
+ if (!alias.equals(canonicalId) &&
!group.aliases.contains(alias)) {
+ group.aliases.add(alias);
+ }
+ }
+ if (!vuln.id().equals(canonicalId) &&
!group.aliases.contains(vuln.id())) {
+ group.aliases.add(vuln.id());
+ }
+ if (vuln.fixedVersions() != null) {
+ for (String fv : vuln.fixedVersions()) {
+ if (!group.fixedVersions.contains(fv)) {
+ group.fixedVersions.add(fv);
+ }
+ }
+ }
+ }
+ }
+ return new ArrayList<>(groups.values());
+ }
+
+ private static String resolveCanonicalId(OsvClient.Vulnerability vuln,
Map<String, String> aliasToCanonical) {
+ String existing = aliasToCanonical.get(vuln.id());
+ if (existing != null) {
+ return existing;
+ }
+ for (String alias : vuln.aliases()) {
+ existing = aliasToCanonical.get(alias);
+ if (existing != null) {
+ return existing;
+ }
+ }
+ for (String alias : vuln.aliases()) {
+ if (alias.startsWith("CVE-")) {
+ return alias;
+ }
+ }
+ return vuln.id();
+ }
+
+ static String normalizeSeverity(String severity) {
+ if (severity == null) {
+ return "UNKNOWN";
+ }
+ return switch (severity.toUpperCase()) {
+ case "MODERATE" -> "MEDIUM";
+ default -> severity.toUpperCase();
+ };
+ }
+
+ private static int severityIndex(String severity) {
+ int idx = SEVERITY_ORDER.indexOf(severity);
+ return idx >= 0 ? idx : SEVERITY_ORDER.size();
+ }
+
+ @Override
+ public SelectionContext getSelectionContext() {
+ List<VulnGroup> visible = sortedGroups();
+ if (visible.isEmpty()) {
+ return null;
+ }
+ List<String> items = visible.stream().map(g -> g.canonicalId != null ?
g.canonicalId : "").toList();
+ Integer sel = tableState.selected();
+ return new SelectionContext("table", items, sel != null ? sel : -1,
items.size(), "CVE Audit");
+ }
+
+ @Override
+ public String description() {
+ return "Vulnerability scanning via OSV.dev";
+ }
+
+ @Override
+ public String getHelpText() {
+ return """
+ # CVE Audit
+
+ The CVE Audit tab scans the integration's classpath
dependencies
+ against the **OSV.dev** vulnerability database
(https://osv.dev).
+ OSV.dev aggregates vulnerabilities from multiple sources
including
+ the GitHub Advisory Database (GHSA), the National Vulnerability
+ Database (NVD/CVE), and other ecosystem-specific databases.
+
+ It queries all Maven JARs using the OSV batch API and displays
+ known CVEs grouped by severity.
+
+ ## Table Columns
+
+ - **SEVERITY** — CRITICAL (red), HIGH (red), MEDIUM (yellow),
LOW (dim)
+ - **CVE ID** — the canonical vulnerability identifier (prefers
CVE- over GHSA-)
+ - **ARTIFACT** — the affected Maven artifact
(groupId:artifactId:version)
+ - **SUMMARY** — brief description of the vulnerability
+
+ ## Detail View
+
+ The detail panel at the bottom shows the full summary, affected
+ artifacts, aliases (e.g., both CVE and GHSA IDs for the same
issue),
+ published date, and a link to the OSV.dev page.
+
+ ## Caching
+
+ Results are cached globally so re-visiting the tab is instant.
+ Switching to another integration that shares the same JARs will
+ also benefit from the cache. Press `r` to force a rescan.
+
+ ## Keys
+
+ - `Up/Down` — navigate vulnerabilities
+ - `PgUp/PgDn` — scroll detail panel
+ - `s` — cycle sort column
+ - `S` — reverse sort order
+ - `r` — rescan (clear cache and re-query)
+ - `Esc` — back
+ """;
+ }
+
+ @Override
+ public JsonObject getTableDataAsJson() {
+ List<VulnGroup> visible = sortedGroups();
+ if (visible.isEmpty()) {
+ return null;
+ }
+ JsonObject result = new JsonObject();
+ result.put("tab", "CVE Audit");
+ JsonArray rows = new JsonArray();
+ for (VulnGroup g : visible) {
+ JsonObject row = new JsonObject();
+ row.put("cveId", g.canonicalId);
+ row.put("severity", g.severity);
+ row.put("summary", g.summary);
+ row.put("published", g.published);
+ JsonArray arts = new JsonArray();
+ for (String gav : g.affectedGavs) {
+ arts.add(gav);
+ }
+ row.put("affectedArtifacts", arts);
+ if (!g.fixedVersions.isEmpty()) {
+ JsonArray fixed = new JsonArray();
+ for (String fv : g.fixedVersions) {
+ fixed.add(fv);
+ }
+ row.put("fixedVersions", fixed);
+ }
+ JsonArray aliases = new JsonArray();
+ for (String alias : g.aliases) {
+ aliases.add(alias);
+ }
+ row.put("aliases", aliases);
+ rows.add(row);
+ }
+ result.put("rows", rows);
+ result.put("totalRows", visible.size());
+ Integer sel = tableState.selected();
+ result.put("selectedIndex", sel != null ? sel : -1);
+ return result;
+ }
+
+ static class VulnGroup {
+ final String canonicalId;
+ final String severity;
+ final String cvssVector;
+ final String summary;
+ final String details;
+ final String published;
+ final List<String> affectedGavs;
+ final List<String> aliases;
+ final List<String> fixedVersions;
+
+ VulnGroup(String canonicalId, String severity, String cvssVector,
String summary, String details,
+ String published, List<String> affectedGavs, List<String>
aliases, List<String> fixedVersions) {
+ this.canonicalId = canonicalId;
+ this.severity = severity;
+ this.cvssVector = cvssVector;
+ this.summary = summary;
+ this.details = details;
+ this.published = published;
+ this.affectedGavs = affectedGavs;
+ this.aliases = aliases;
+ this.fixedVersions = fixedVersions;
+ }
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DataSourceTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DataSourceTab.java
index 6d434e75c943..8600f6ef787d 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DataSourceTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DataSourceTab.java
@@ -129,7 +129,7 @@ class DataSourceTab extends AbstractTableTab {
Constraint.length(8),
Constraint.fill())
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
- .title(" DataSource sort:" + sort + " ").build())
+ .title(" DataSource ").build())
.build();
lastTableArea = area;
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
index 23da8ba7b73a..7e7921846590 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
@@ -231,7 +231,7 @@ class EndpointsTab extends AbstractTableTab {
.highlightStyle(Theme.selectionBg())
.highlightSpacing(Table.HighlightSpacing.ALWAYS)
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
- .title(" Endpoints sort:" + sort
+ .title(" Endpoints"
+ (filter == 1 ? " filter:remote" : filter == 2
? " filter:remote+stub" : "")
+ " ")
.build())
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTab.java
index ce0904344cb1..46bad5ff1efa 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTab.java
@@ -385,7 +385,7 @@ class ErrorsTab extends AbstractTableTab {
.highlightStyle(Theme.selectionBg())
.highlightSpacing(Table.HighlightSpacing.ALWAYS)
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
- .title(" Errors (" + sorted.size() + ") sort:" + sort
+ " ").build())
+ .title(" Errors (" + sorted.size() + ") ").build())
.build();
lastTableArea = chunks.get(0);
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HeapHistogramTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HeapHistogramTab.java
index f6b713975777..fbaa0efe5018 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HeapHistogramTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HeapHistogramTab.java
@@ -148,9 +148,9 @@ class HeapHistogramTab extends AbstractTableTab {
visibleBytes += e.bytes;
}
- String title = String.format(" Heap Histogram [%d] instances:%s
bytes:%s sort:%s filter:%s ",
+ String title = String.format(" Heap Histogram [%d] instances:%s
bytes:%s filter:%s ",
visible.size(), formatNumber(visibleInstances),
formatBytes(visibleBytes),
- sort, FILTER_LABELS[filter]);
+ FILTER_LABELS[filter]);
Table table = Table.builder()
.rows(rows)
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java
index 883325c325b7..9dd9db56290d 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java
@@ -1218,7 +1218,7 @@ class HistoryTab extends AbstractTab {
rightCell("STEPS", 6, Style.EMPTY.bold()),
Cell.from(Span.styled(traceSortLabel("EXCHANGE", "exchange"),
traceSortStyle("exchange"))));
- String traceTitle = String.format(" Traces [%d] sort:%s ",
summaries.size(), traceSort);
+ String traceTitle = String.format(" Traces [%d] ", summaries.size());
Table table = Table.builder()
.rows(rows)
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HttpTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HttpTab.java
index 00528093544a..799a836eae5b 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HttpTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HttpTab.java
@@ -1286,7 +1286,7 @@ class HttpTab extends AbstractTableTab {
"Stopped".equals(state) ?
Style.EMPTY.fg(Color.LIGHT_RED) : Style.EMPTY))));
}
- String title = String.format(" HTTP Services [%d] sort:%s ",
visible.size(), sort);
+ String title = String.format(" HTTP Services [%d] ", visible.size());
Row header = Row.from(
Cell.from(Span.styled(sortLabel("METHOD", "method"),
sortStyle("method"))),
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/InflightTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/InflightTab.java
index 9fc158057591..3bbad15dc01b 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/InflightTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/InflightTab.java
@@ -127,7 +127,7 @@ class InflightTab extends AbstractTableTab {
rows.add(emptyRow("No inflight or blocked exchanges", 5));
}
- String title = " Inflight (" + sorted.size() + ") sort:" + sort + " ";
+ String title = " Inflight (" + sorted.size() + ") ";
Table table = Table.builder()
.rows(rows)
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
index 55b58af1e523..d15d8ee4cd92 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
@@ -89,7 +89,7 @@ class McpFacade {
static final String[] MORE_TAB_NAMES = {
"Beans", "Browse", "Circuit Breaker", "Classpath", "Configuration",
- "Consumers", "DataSource", "Heap Histogram", "Inflight", "Memory",
"Memory Leak", "Metrics",
+ "Consumers", "CVE Audit", "DataSource", "Heap Histogram",
"Inflight", "Memory", "Memory Leak", "Metrics",
"SQL Query", "SQL Trace", "Spans", "Process", "Startup", "Threads"
};
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryLeakTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryLeakTab.java
index 8cd5fa016cf4..45daa1511fd8 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryLeakTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryLeakTab.java
@@ -416,8 +416,8 @@ class MemoryLeakTab extends AbstractTab {
}
}
String gcLabel = gcCount > 0 ? " gc:" + gcCount : "";
- String title = String.format(" Memory Leak [%d] duration:%s%s
sort:%s%s%s ",
- visible.size(), formatDuration(recordingDurationMs), gcLabel,
sort, minLabel, agoLabel);
+ String title = String.format(" Memory Leak [%d] duration:%s%s%s%s ",
+ visible.size(), formatDuration(recordingDurationMs), gcLabel,
minLabel, agoLabel);
Table table = Table.builder()
.rows(rows)
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsTab.java
index ed1e7abc3ac7..4652d62f29a8 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsTab.java
@@ -493,7 +493,7 @@ class MetricsTab extends AbstractTableTab {
if (!"all".equals(filterType)) {
title += " filter:" + filterType;
}
- title += " sort:" + sort + " (" + sorted.size() + ") ";
+ title += " (" + sorted.size() + ") ";
Table table = Table.builder()
.rows(rows)
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OsvClient.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OsvClient.java
new file mode 100644
index 000000000000..f140d00bc4fd
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OsvClient.java
@@ -0,0 +1,338 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+
+/**
+ * Lightweight HTTP client for the OSV.dev vulnerability database. Uses the
batch query endpoint for efficiency and
+ * maintains a global in-memory cache keyed by Maven GAV coordinates so
results persist across tab visits and
+ * integration switches.
+ */
+class OsvClient {
+
+ private static final String BATCH_URL =
"https://api.osv.dev/v1/querybatch";
+ private static final String VULN_URL = "https://api.osv.dev/v1/vulns/";
+ private static final ConcurrentHashMap<String, List<Vulnerability>> CACHE
= new ConcurrentHashMap<>();
+ private static final ConcurrentHashMap<String, Vulnerability>
VULN_DETAIL_CACHE = new ConcurrentHashMap<>();
+
+ record Vulnerability(String id, String summary, String details, String
severity, String cvssVector,
+ String published, List<String> aliases, List<String>
fixedVersions) {
+ }
+
+ /**
+ * Query OSV.dev for vulnerabilities in the given JARs. Cached results are
returned immediately; only uncached JARs
+ * are sent to the API.
+ *
+ * @return map from GAV string (groupId:artifactId:version) to list of
vulnerabilities
+ */
+ Map<String, List<Vulnerability>> queryBatch(List<ClasspathTab.JarEntry>
entries) {
+ Map<String, List<Vulnerability>> result = new HashMap<>();
+ List<ClasspathTab.JarEntry> uncached = new ArrayList<>();
+
+ for (ClasspathTab.JarEntry entry : entries) {
+ if (entry.groupId() == null) {
+ continue;
+ }
+ String gav = entry.display();
+ List<Vulnerability> cached = CACHE.get(gav);
+ if (cached != null) {
+ result.put(gav, cached);
+ } else {
+ uncached.add(entry);
+ }
+ }
+
+ if (uncached.isEmpty()) {
+ return result;
+ }
+
+ JsonArray queries = new JsonArray();
+ for (ClasspathTab.JarEntry entry : uncached) {
+ JsonObject pkg = new JsonObject();
+ pkg.put("name", entry.groupId() + ":" + entry.artifactId());
+ pkg.put("ecosystem", "Maven");
+
+ JsonObject query = new JsonObject();
+ query.put("version", entry.version());
+ query.put("package", pkg);
+ queries.add(query);
+ }
+
+ JsonObject requestBody = new JsonObject();
+ requestBody.put("queries", queries);
+
+ try {
+ HttpClient client = HttpClient.newBuilder()
+ .connectTimeout(Duration.ofSeconds(5))
+ .build();
+ HttpRequest request = HttpRequest.newBuilder()
+ .uri(URI.create(BATCH_URL))
+ .timeout(Duration.ofSeconds(30))
+ .header("Content-Type", "application/json")
+
.POST(HttpRequest.BodyPublishers.ofString(requestBody.toJson()))
+ .build();
+ HttpResponse<String> response = client.send(request,
HttpResponse.BodyHandlers.ofString());
+
+ if (response.statusCode() == 200) {
+ JsonObject body = (JsonObject)
Jsoner.deserialize(response.body());
+ JsonArray results = (JsonArray) body.get("results");
+ if (results != null) {
+ // batch response returns abbreviated vulns (id + modified
only)
+ // collect vuln IDs per GAV, then fetch full details
+ Map<String, List<String>> gavToVulnIds = new HashMap<>();
+ for (int i = 0; i < results.size() && i < uncached.size();
i++) {
+ ClasspathTab.JarEntry entry = uncached.get(i);
+ String gav = entry.display();
+ JsonObject queryResult = (JsonObject) results.get(i);
+ JsonArray vulns = (JsonArray) queryResult.get("vulns");
+ List<String> ids = new ArrayList<>();
+ if (vulns != null) {
+ for (Object v : vulns) {
+ JsonObject vuln = (JsonObject) v;
+ String id = getString(vuln, "id");
+ if (id != null) {
+ ids.add(id);
+ }
+ }
+ }
+ gavToVulnIds.put(gav, ids);
+ }
+
+ // fetch full details for each unique vuln ID
+ fetchVulnDetails(client, gavToVulnIds);
+
+ // build result from detail cache
+ for (Map.Entry<String, List<String>> e :
gavToVulnIds.entrySet()) {
+ String gav = e.getKey();
+ List<Vulnerability> vulns = new ArrayList<>();
+ for (String id : e.getValue()) {
+ Vulnerability v = VULN_DETAIL_CACHE.get(id);
+ if (v != null) {
+ vulns.add(v);
+ }
+ }
+ CACHE.put(gav, vulns);
+ result.put(gav, vulns);
+ }
+ }
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ } catch (Exception e) {
+ // network errors — return whatever we have from cache
+ }
+
+ // cache empty results for uncached entries that got no response
+ for (ClasspathTab.JarEntry entry : uncached) {
+ String gav = entry.display();
+ result.putIfAbsent(gav, Collections.emptyList());
+ CACHE.putIfAbsent(gav, Collections.emptyList());
+ }
+
+ return result;
+ }
+
+ private void fetchVulnDetails(HttpClient client, Map<String, List<String>>
gavToVulnIds) {
+ // collect unique IDs not already in detail cache
+ List<String> toFetch = gavToVulnIds.values().stream()
+ .flatMap(List::stream)
+ .distinct()
+ .filter(id -> !VULN_DETAIL_CACHE.containsKey(id))
+ .toList();
+
+ for (String id : toFetch) {
+ try {
+ HttpRequest req = HttpRequest.newBuilder()
+ .uri(URI.create(VULN_URL + id))
+ .timeout(Duration.ofSeconds(10))
+ .GET()
+ .build();
+ HttpResponse<String> resp = client.send(req,
HttpResponse.BodyHandlers.ofString());
+ if (resp.statusCode() == 200) {
+ JsonObject vuln = (JsonObject)
Jsoner.deserialize(resp.body());
+ Vulnerability v = parseVuln(vuln);
+ if (v != null) {
+ VULN_DETAIL_CACHE.put(id, v);
+ }
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ break;
+ } catch (Exception e) {
+ // skip this vuln on error
+ }
+ }
+ }
+
+ void clearCache() {
+ CACHE.clear();
+ VULN_DETAIL_CACHE.clear();
+ }
+
+ void clearCache(List<ClasspathTab.JarEntry> entries) {
+ for (ClasspathTab.JarEntry entry : entries) {
+ if (entry.groupId() != null) {
+ CACHE.remove(entry.display());
+ }
+ }
+ }
+
+ private Vulnerability parseVuln(JsonObject vuln) {
+ String id = getString(vuln, "id");
+ if (id == null) {
+ return null;
+ }
+ String summary = getString(vuln, "summary");
+ String details = getString(vuln, "details");
+ String published = getString(vuln, "published");
+ String severity = extractSeverity(vuln);
+ String cvssVector = extractCvssVector(vuln);
+ List<String> aliases = extractAliases(vuln);
+ List<String> fixedVersions = extractFixedVersions(vuln);
+ return new Vulnerability(id, summary, details, severity, cvssVector,
published, aliases, fixedVersions);
+ }
+
+ private List<String> extractFixedVersions(JsonObject vuln) {
+ JsonArray affected = (JsonArray) vuln.get("affected");
+ if (affected == null) {
+ return Collections.emptyList();
+ }
+ List<String> fixed = new ArrayList<>();
+ for (Object a : affected) {
+ JsonObject aff = (JsonObject) a;
+ JsonArray ranges = (JsonArray) aff.get("ranges");
+ if (ranges == null) {
+ continue;
+ }
+ for (Object r : ranges) {
+ JsonObject range = (JsonObject) r;
+ JsonArray events = (JsonArray) range.get("events");
+ if (events == null) {
+ continue;
+ }
+ for (Object e : events) {
+ JsonObject event = (JsonObject) e;
+ String fixedVer = getString(event, "fixed");
+ if (fixedVer != null && !fixed.contains(fixedVer)) {
+ fixed.add(fixedVer);
+ }
+ }
+ }
+ }
+ return fixed;
+ }
+
+ static String extractCvssVector(JsonObject vuln) {
+ JsonArray severityArr = (JsonArray) vuln.get("severity");
+ if (severityArr != null && !severityArr.isEmpty()) {
+ JsonObject first = (JsonObject) severityArr.get(0);
+ String score = getString(first, "score");
+ if (score != null && score.startsWith("CVSS:")) {
+ return score;
+ }
+ }
+ return null;
+ }
+
+ static String extractSeverity(JsonObject vuln) {
+ // first try database_specific.severity
+ JsonObject dbSpecific = (JsonObject) vuln.get("database_specific");
+ if (dbSpecific != null) {
+ String sev = getString(dbSpecific, "severity");
+ if (sev != null) {
+ return sev.toUpperCase();
+ }
+ }
+ // fall back to severity array with CVSS score
+ JsonArray severityArr = (JsonArray) vuln.get("severity");
+ if (severityArr != null && !severityArr.isEmpty()) {
+ JsonObject first = (JsonObject) severityArr.get(0);
+ String score = getString(first, "score");
+ if (score != null) {
+ return cvssToSeverity(score);
+ }
+ }
+ return "UNKNOWN";
+ }
+
+ static String cvssToSeverity(String cvssVector) {
+ if (cvssVector == null) {
+ return "UNKNOWN";
+ }
+ // extract numeric score from CVSS vector if present (e.g.,
"CVSS:3.1/AV:N/AC:L/...")
+ // parse from the vector heuristically
+ boolean network = cvssVector.contains("AV:N");
+ boolean lowComplexity = cvssVector.contains("AC:L");
+ int highCount = 0;
+ if (cvssVector.contains("/C:H")) {
+ highCount++;
+ }
+ if (cvssVector.contains("/I:H")) {
+ highCount++;
+ }
+ if (cvssVector.contains("/A:H")) {
+ highCount++;
+ }
+
+ if (network && lowComplexity && highCount == 3) {
+ return "CRITICAL";
+ }
+ if (network && highCount >= 2) {
+ return "HIGH";
+ }
+ if (highCount >= 1) {
+ return "MEDIUM";
+ }
+ boolean allNone = cvssVector.contains("/C:N") &&
cvssVector.contains("/I:N") && cvssVector.contains("/A:N");
+ if (allNone) {
+ return "LOW";
+ }
+ return "MEDIUM";
+ }
+
+ private List<String> extractAliases(JsonObject vuln) {
+ JsonArray aliases = (JsonArray) vuln.get("aliases");
+ if (aliases == null || aliases.isEmpty()) {
+ return Collections.emptyList();
+ }
+ List<String> list = new ArrayList<>();
+ for (Object a : aliases) {
+ list.add(String.valueOf(a));
+ }
+ return list;
+ }
+
+ private static String getString(JsonObject obj, String key) {
+ Object val = obj.get(key);
+ return val != null ? val.toString() : null;
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManager.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManager.java
index ad7880195c56..29fa97496356 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManager.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/PopupManager.java
@@ -82,7 +82,7 @@ class PopupManager {
// Last rendered popup rects for mouse hit-testing
private Rect lastMorePopupRect;
private Rect lastSwitchPopupRect;
- private static final int MORE_POPUP_ITEM_COUNT = 18;
+ private static final int MORE_POPUP_ITEM_COUNT = 19;
PopupManager(MonitorContext ctx, Supplier<List<IntegrationInfo>>
nonVanishingIntegrationsSupplier,
FilesBrowser filesBrowser, PopupCallbacks callbacks) {
@@ -383,7 +383,7 @@ class PopupManager {
void renderMorePopup(Frame frame, Rect area) {
int popupW = 22;
- int popupH = 20;
+ int popupH = 21;
// Position just below the "0 More▾" tab label
int dividerW = CharWidth.of(" | ");
int tabBarX = 0;
@@ -412,6 +412,7 @@ class PopupManager {
ListItem.from(Line.from(Span.raw(" Cl"), Span.styled("a",
keyStyle), Span.raw("sspath"))),
ListItem.from(Line.from(Span.raw(" Confi"), Span.styled("g",
keyStyle), Span.raw("uration"))),
ListItem.from(Line.from(Span.raw(" Co"), Span.styled("n",
keyStyle), Span.raw("sumers"))),
+ ListItem.from(Line.from(Span.raw(" C"), Span.styled("V",
keyStyle), Span.raw("E Audit"))),
ListItem.from(Line.from(Span.raw(" "), Span.styled("D",
keyStyle), Span.raw("ataSource"))),
ListItem.from(Line.from(Span.raw(" "), Span.styled("H",
keyStyle), Span.raw("eap Histogram"))),
ListItem.from(Line.from(Span.raw(" "), Span.styled("I",
keyStyle), Span.raw("nflight"))),
@@ -542,42 +543,45 @@ class PopupManager {
if (ke.isChar('n')) {
return 5;
}
- if (ke.isChar('d')) {
+ if (ke.isChar('v')) {
return 6;
}
- if (ke.isChar('h')) {
+ if (ke.isChar('d')) {
return 7;
}
- if (ke.isChar('i')) {
+ if (ke.isChar('h')) {
return 8;
}
- if (ke.isChar('m')) {
+ if (ke.isChar('i')) {
return 9;
}
- if (ke.isChar('k')) {
+ if (ke.isChar('m')) {
return 10;
}
- if (ke.isChar('e')) {
+ if (ke.isChar('k')) {
return 11;
}
- if (ke.isChar('q')) {
+ if (ke.isChar('e')) {
return 12;
}
- if (ke.isChar('r')) {
+ if (ke.isChar('q')) {
return 13;
}
- if (ke.isChar('o')) {
+ if (ke.isChar('r')) {
return 14;
}
- if (ke.isChar('p')) {
+ if (ke.isChar('o')) {
return 15;
}
- if (ke.isChar('s')) {
+ if (ke.isChar('p')) {
return 16;
}
- if (ke.isChar('t')) {
+ if (ke.isChar('s')) {
return 17;
}
+ if (ke.isChar('t')) {
+ return 18;
+ }
return -1;
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java
index 3b52b4753e76..a98246cdf8dc 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java
@@ -615,7 +615,7 @@ class RoutesTab extends AbstractTab {
.highlightStyle(Theme.selectionBg())
.highlightSpacing(Table.HighlightSpacing.ALWAYS)
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
- .title(" Top Routes sort:" + routeTopSort + "
").build())
+ .title(" Top Routes ").build())
.build();
} else {
List<Row> routeRows = new ArrayList<>();
@@ -675,7 +675,7 @@ class RoutesTab extends AbstractTab {
.highlightStyle(Theme.selectionBg())
.highlightSpacing(Table.HighlightSpacing.ALWAYS)
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
- .title(" Routes sort:" + routeSort + " ").build())
+ .title(" Routes ").build())
.build();
}
@@ -1151,7 +1151,7 @@ class RoutesTab extends AbstractTab {
Constraint.length(6),
Constraint.length(9))
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
- .title(" Top Processors [" + route.routeId + "]
sort:" + routeTopSort + " ").build())
+ .title(" Top Processors [" + route.routeId + "]
").build())
.build();
} else {
List<Row> rows = new ArrayList<>();
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SqlTraceTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SqlTraceTab.java
index 0787cfb02df5..ffd152b908ef 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SqlTraceTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SqlTraceTab.java
@@ -288,7 +288,7 @@ class SqlTraceTab extends AbstractTableTab {
.highlightStyle(Theme.selectionBg())
.highlightSpacing(Table.HighlightSpacing.ALWAYS)
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
- .title(" Statements sort:" + sort + " ").build())
+ .title(" Statements ").build())
.build();
lastTableArea = area;
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
index 51a667b10340..fa8538ecf3ed 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
@@ -80,6 +80,7 @@ class TabRegistry {
private BeansTab beansTab;
private BrowseTab browseTab;
private ClasspathTab classpathTab;
+ private CveAuditTab cveAuditTab;
private InflightTab inflightTab;
private MemoryTab memoryTab;
private HeapHistogramTab heapHistogramTab;
@@ -124,6 +125,7 @@ class TabRegistry {
beansTab = new BeansTab(ctx);
browseTab = new BrowseTab(ctx);
classpathTab = new ClasspathTab(ctx);
+ cveAuditTab = new CveAuditTab(ctx);
inflightTab = new InflightTab(ctx);
memoryTab = new MemoryTab(ctx, dataService.metrics());
threadsTab = new ThreadsTab(ctx);
@@ -134,7 +136,7 @@ class TabRegistry {
resetIntegrationTabState);
sqlTraceTab.setEditSqlAction(sql -> {
- selectMoreTab(12); // switch to SQL Query tab
+ selectMoreTab(13); // switch to SQL Query tab
sqlQueryTab.setInputValue("sql", sql);
});
}
@@ -220,18 +222,19 @@ class TabRegistry {
case 3 -> classpathTab;
case 4 -> configurationTab;
case 5 -> consumersTab;
- case 6 -> dataSourceTab;
- case 7 -> heapHistogramTab;
- case 8 -> inflightTab;
- case 9 -> memoryTab;
- case 10 -> memoryLeakTab;
- case 11 -> metricsTab;
- case 12 -> sqlQueryTab;
- case 13 -> sqlTraceTab;
- case 14 -> spansTab;
- case 15 -> processTab;
- case 16 -> startupTab;
- case 17 -> threadsTab;
+ case 6 -> cveAuditTab;
+ case 7 -> dataSourceTab;
+ case 8 -> heapHistogramTab;
+ case 9 -> inflightTab;
+ case 10 -> memoryTab;
+ case 11 -> memoryLeakTab;
+ case 12 -> metricsTab;
+ case 13 -> sqlQueryTab;
+ case 14 -> sqlTraceTab;
+ case 15 -> spansTab;
+ case 16 -> processTab;
+ case 17 -> startupTab;
+ case 18 -> threadsTab;
default -> null;
};
if (activeMoreTab != null) {
@@ -253,6 +256,7 @@ class TabRegistry {
startupTab.onIntegrationChanged();
configurationTab.onIntegrationChanged();
consumersTab.onIntegrationChanged();
+ cveAuditTab.onIntegrationChanged();
dataSourceTab.onIntegrationChanged();
heapHistogramTab.onIntegrationChanged();
memoryLeakTab.onIntegrationChanged();
@@ -325,13 +329,17 @@ class TabRegistry {
return classpathTab;
}
+ CveAuditTab cveAuditTab() {
+ return cveAuditTab;
+ }
+
// ---- Tab entries for Go-to and MCP ----
record TabEntry(String name, String description, String shortcut, int
tabIndex, int moreIndex) {
}
private static final String[] MORE_SHORTCUTS = {
- "B", "W", "C", "A", "G", "N", "D", "H", "I", "M", "K", "E", "Q",
"R", "O", "P", "S", "T"
+ "B", "W", "C", "A", "G", "N", "V", "D", "H", "I", "M", "K", "E",
"Q", "R", "O", "P", "S", "T"
};
List<TabEntry> allTabEntries() {
@@ -348,13 +356,13 @@ class TabRegistry {
// More sub-tabs
MonitorTab[] moreTabs = {
beansTab, browseTab, circuitBreakerTab, classpathTab,
configurationTab,
- consumersTab, dataSourceTab, heapHistogramTab, inflightTab,
memoryTab,
+ consumersTab, cveAuditTab, dataSourceTab, heapHistogramTab,
inflightTab, memoryTab,
memoryLeakTab, metricsTab, sqlQueryTab, sqlTraceTab, spansTab,
processTab, startupTab, threadsTab
};
String[] moreNames = {
"Beans", "Browse", "Circuit Breaker", "Classpath",
"Configuration",
- "Consumers", "DataSource", "Heap Histogram", "Inflight",
"Memory",
+ "Consumers", "CVE Audit", "DataSource", "Heap Histogram",
"Inflight", "Memory",
"Memory Leak", "Metrics", "SQL Query", "SQL Trace", "Spans",
"Process", "Startup", "Threads"
};
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ThreadsTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ThreadsTab.java
index 5d48490bde09..9cbe8e061333 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ThreadsTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ThreadsTab.java
@@ -184,8 +184,8 @@ class ThreadsTab extends AbstractTableTab {
rows.add(emptyRow("No threads", 5));
}
- String title = String.format(" Threads [%d/%d] peak:%d sort:%s
filter:%s ",
- visible.size(), threadCount, peakThreadCount, sort,
FILTER_LABELS[filter]);
+ String title = String.format(" Threads [%d/%d] peak:%d filter:%s ",
+ visible.size(), threadCount, peakThreadCount,
FILTER_LABELS[filter]);
Table table = Table.builder()
.rows(rows)
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumersTabRenderTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumersTabRenderTest.java
index dfdd12731439..849b3a2d88bf 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumersTabRenderTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumersTabRenderTest.java
@@ -170,7 +170,7 @@ class ConsumersTabRenderTest {
ConsumersTab tab = new ConsumersTab(ctx);
String rendered = TuiTestHelper.renderToString(tab, 120, 20);
- assertTrue(rendered.contains("sort:id"), "Block title should contain
default sort column 'sort:id'");
+ assertTrue(rendered.contains("ROUTE▼"), "Column header should show
sort indicator on default sort column");
}
@Test
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/CveAuditTabRenderTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/CveAuditTabRenderTest.java
new file mode 100644
index 000000000000..77983cb3c6a4
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/CveAuditTabRenderTest.java
@@ -0,0 +1,168 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+
+import dev.tamboui.buffer.Buffer;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.Span;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class CveAuditTabRenderTest {
+
+ private MonitorContext ctx;
+ private IntegrationInfo info;
+
+ @BeforeEach
+ void setUp() {
+ Theme.resetForTesting();
+ info = new IntegrationInfo();
+ info.pid = "1234";
+ info.name = "test-app";
+
+ AtomicReference<List<IntegrationInfo>> data = new
AtomicReference<>(List.of(info));
+ AtomicReference<List<InfraInfo>> infraData = new
AtomicReference<>(List.of());
+ ctx = new MonitorContext(data, infraData);
+ ctx.selectedPid = "1234";
+ }
+
+ @Test
+ void renderNoSelectionShowsPrompt() {
+ ctx.selectedPid = null;
+ CveAuditTab tab = new CveAuditTab(ctx);
+ String rendered = TuiTestHelper.renderToString(tab, 120, 30);
+ assertTrue(rendered.contains("No integration selected") ||
rendered.contains("Select an integration"));
+ }
+
+ @Test
+ void renderShowsBlockTitle() {
+ CveAuditTab tab = new CveAuditTab(ctx);
+ String rendered = TuiTestHelper.renderToString(tab, 120, 30);
+ assertTrue(rendered.contains("CVE Audit"), "Should show CVE Audit in
the block title");
+ }
+
+ @Test
+ void renderFooterHints() {
+ CveAuditTab tab = new CveAuditTab(ctx);
+ List<Span> footerSpans = new ArrayList<>();
+ tab.renderFooter(footerSpans);
+ String footer = footerSpans.stream().map(Span::content).reduce("",
String::concat);
+ assertTrue(footer.contains("Esc"), "Footer should contain Esc hint");
+ assertTrue(footer.contains("rescan"), "Footer should contain rescan
hint");
+ }
+
+ @Test
+ void buildVulnGroupsDeduplicatesByAlias() {
+ Map<String, List<OsvClient.Vulnerability>> vulnMap = new HashMap<>();
+ vulnMap.put("com.example:lib-a:1.0", List.of(
+ new OsvClient.Vulnerability(
+ "GHSA-xxxx", "Test vuln", null, "HIGH", null,
"2024-01-01",
+ List.of("CVE-2024-1234"), List.of("1.1"))));
+ vulnMap.put("com.example:lib-b:2.0", List.of(
+ new OsvClient.Vulnerability(
+ "CVE-2024-1234", "Test vuln", null, "HIGH", null,
"2024-01-01",
+ List.of("GHSA-xxxx"), List.of("1.1"))));
+
+ List<CveAuditTab.VulnGroup> groups =
CveAuditTab.buildVulnGroups(vulnMap);
+
+ assertEquals(1, groups.size(), "Should deduplicate CVE-2024-1234 and
GHSA-xxxx into one group");
+ assertEquals("CVE-2024-1234", groups.get(0).canonicalId, "Should
prefer CVE- prefix as canonical ID");
+ assertEquals(2, groups.get(0).affectedGavs.size(), "Should list both
affected artifacts");
+ }
+
+ @Test
+ void buildVulnGroupsSeparatesDistinctCves() {
+ Map<String, List<OsvClient.Vulnerability>> vulnMap = new HashMap<>();
+ vulnMap.put("com.example:lib:1.0", List.of(
+ new OsvClient.Vulnerability(
+ "CVE-2024-1111", "Vuln A", null, "CRITICAL", null,
"2024-01-01", List.of(), List.of()),
+ new OsvClient.Vulnerability(
+ "CVE-2024-2222", "Vuln B", null, "MEDIUM", null,
"2024-02-01", List.of(), List.of())));
+
+ List<CveAuditTab.VulnGroup> groups =
CveAuditTab.buildVulnGroups(vulnMap);
+
+ assertEquals(2, groups.size(), "Should create separate groups for
distinct CVEs");
+ }
+
+ @Test
+ void severityStyleCriticalIsRedBold() {
+ Rect area = new Rect(0, 0, 120, 30);
+ Buffer buffer = Buffer.empty(area);
+ Frame frame = Frame.forTesting(buffer);
+
+ Map<String, List<OsvClient.Vulnerability>> vulnMap = new HashMap<>();
+ vulnMap.put("com.example:lib:1.0", List.of(
+ new OsvClient.Vulnerability(
+ "CVE-2024-9999", "Critical vuln", null, "CRITICAL",
null, "2024-01-01",
+ List.of(), List.of())));
+
+ List<CveAuditTab.VulnGroup> groups =
CveAuditTab.buildVulnGroups(vulnMap);
+ assertFalse(groups.isEmpty());
+ assertEquals("CRITICAL", groups.get(0).severity);
+
+ // verify the severity style method returns red for CRITICAL
+ var style = CveAuditTab.severityStyle("CRITICAL");
+ assertNotNull(style);
+ }
+
+ @Test
+ void severityExtraction() {
+ assertEquals("CRITICAL",
OsvClient.cvssToSeverity("CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H"));
+ assertEquals("HIGH",
OsvClient.cvssToSeverity("CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N"));
+ assertEquals("MEDIUM",
OsvClient.cvssToSeverity("CVSS:3.1/AV:L/AC:H/PR:H/UI:R/S:U/C:H/I:N/A:N"));
+ assertEquals("MEDIUM",
OsvClient.cvssToSeverity("CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:H/A:N"));
+ }
+
+ @Test
+ void descriptionIsNotNull() {
+ CveAuditTab tab = new CveAuditTab(ctx);
+ assertNotNull(tab.description());
+ assertFalse(tab.description().isEmpty());
+ }
+
+ @Test
+ void helpTextIsNotNull() {
+ CveAuditTab tab = new CveAuditTab(ctx);
+ assertNotNull(tab.getHelpText());
+ assertTrue(tab.getHelpText().contains("CVE Audit"));
+ }
+
+ @Test
+ void buildVulnGroupsMultipleArtifacts() {
+ Map<String, List<OsvClient.Vulnerability>> vulnMap = new HashMap<>();
+ vulnMap.put("org.apache.camel:camel-core:4.12.0", List.of(
+ new OsvClient.Vulnerability(
+ "CVE-2024-5555", "A camel vuln", null, "HIGH", null,
"2024-01-01",
+ List.of(), List.of("4.13.0"))));
+ vulnMap.put("com.example:other:1.0", List.of(
+ new OsvClient.Vulnerability(
+ "CVE-2024-6666", "Other vuln", null, "LOW", null,
"2024-01-01",
+ List.of(), List.of())));
+
+ List<CveAuditTab.VulnGroup> groups =
CveAuditTab.buildVulnGroups(vulnMap);
+ assertEquals(2, groups.size());
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DataSourceTabRenderTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DataSourceTabRenderTest.java
index b7bfd7d9bcb1..83b4ee9613c7 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DataSourceTabRenderTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DataSourceTabRenderTest.java
@@ -152,7 +152,7 @@ class DataSourceTabRenderTest {
tab.handleKeyEvent(KeyEvent.ofChar('s', KeyModifiers.NONE));
String rendered = TuiTestHelper.renderToString(tab, 140, 20);
- assertTrue(rendered.contains("sort:pool"), "Sort should cycle to
'pool' after pressing 's'");
+ assertTrue(rendered.contains("POOL▼"), "Sort should cycle to 'pool'
after pressing 's'");
}
@Test
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTabRenderTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTabRenderTest.java
index 6b42f900459a..f1921e7f59d5 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTabRenderTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTabRenderTest.java
@@ -165,7 +165,7 @@ class EndpointsTabRenderTest {
EndpointsTab tab = new EndpointsTab(ctx, new MetricsCollector());
String rendered = TuiTestHelper.renderToString(tab, 140, 30);
- assertTrue(rendered.contains("sort:route"), "Title should show default
sort column 'route'");
+ assertTrue(rendered.contains("ROUTE▼"), "Column header should show
sort indicator on default sort column");
}
@Test
@@ -178,7 +178,7 @@ class EndpointsTabRenderTest {
tab.handleKeyEvent(KeyEvent.ofChar('s', KeyModifiers.NONE));
String rendered = TuiTestHelper.renderToString(tab, 140, 30);
- assertTrue(rendered.contains("sort:dir"), "Sort should cycle to 'dir'
after pressing 's'");
+ assertTrue(rendered.contains("DIR▼"), "Sort should cycle to 'dir'
after pressing 's'");
}
@Test
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTabRenderTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTabRenderTest.java
index 0eefc7ad793a..ea395c6ba878 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTabRenderTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ErrorsTabRenderTest.java
@@ -187,7 +187,7 @@ class ErrorsTabRenderTest {
ErrorsTab tab = new ErrorsTab(ctx);
String rendered = renderToString(tab, 160, 30);
- assertTrue(rendered.contains("sort:id"), "Title should show current
sort column");
+ assertTrue(rendered.contains("ID▼"), "Column header should show sort
indicator on default sort column");
}
@Test
@@ -200,7 +200,7 @@ class ErrorsTabRenderTest {
tab.handleKeyEvent(KeyEvent.ofChar('s', KeyModifiers.NONE));
String rendered = renderToString(tab, 160, 30);
- assertTrue(rendered.contains("sort:age"), "Sort should cycle to
'age'");
+ assertTrue(rendered.contains("AGO▼"), "Sort should cycle to 'age'");
}
@Test
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/HttpTabRenderTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/HttpTabRenderTest.java
index 80afe5ea0348..d7dd1b5bef92 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/HttpTabRenderTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/HttpTabRenderTest.java
@@ -151,7 +151,7 @@ class HttpTabRenderTest {
HttpTab tab = new HttpTab(ctx);
String rendered = TuiTestHelper.renderToString(tab, 140, 30);
- assertTrue(rendered.contains("sort:method"), "Title should show
default sort column 'method'");
+ assertTrue(rendered.contains("METHOD▼"), "Column header should show
sort indicator on default sort column");
}
@Test
@@ -164,7 +164,7 @@ class HttpTabRenderTest {
tab.handleKeyEvent(KeyEvent.ofChar('s', KeyModifiers.NONE));
String rendered = TuiTestHelper.renderToString(tab, 140, 30);
- assertTrue(rendered.contains("sort:path"), "Sort should cycle to
'path' after pressing 's'");
+ assertTrue(rendered.contains("PATH▼"), "Sort should cycle to 'path'
after pressing 's'");
}
@Test
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/InflightTabRenderTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/InflightTabRenderTest.java
index 3127aeff28cc..717e89e08731 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/InflightTabRenderTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/InflightTabRenderTest.java
@@ -175,7 +175,7 @@ class InflightTabRenderTest {
InflightTab tab = new InflightTab(ctx);
String rendered = TuiTestHelper.renderToString(tab, 160, 20);
- assertTrue(rendered.contains("sort:duration"), "Title should show
current sort column");
+ assertTrue(rendered.contains("DURATION▼"), "Column header should show
sort indicator on default sort column");
}
@Test
@@ -188,7 +188,7 @@ class InflightTabRenderTest {
tab.handleKeyEvent(KeyEvent.ofChar('s', KeyModifiers.NONE));
String rendered = TuiTestHelper.renderToString(tab, 160, 20);
- assertTrue(rendered.contains("sort:status"), "Sort should cycle to
'status'");
+ assertTrue(rendered.contains("STATUS▼"), "Sort should cycle to
'status'");
}
@Test
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTabRenderTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTabRenderTest.java
index d1a7b8480515..7ca0c40eaeaa 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTabRenderTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTabRenderTest.java
@@ -237,7 +237,7 @@ class RoutesTabRenderTest {
RoutesTab tab = new RoutesTab(ctx);
String rendered = renderToString(tab, 140, 30);
- assertTrue(rendered.contains("sort:name"), "Title should show current
sort column");
+ assertTrue(rendered.contains("ROUTE▼"), "Column header should show
sort indicator on default sort column");
}
@Test
@@ -249,7 +249,7 @@ class RoutesTabRenderTest {
// Press 's' to cycle sort
tab.handleKeyEvent(KeyEvent.ofChar('s', KeyModifiers.NONE));
String rendered = renderToString(tab, 140, 30);
- assertTrue(rendered.contains("sort:from"), "Sort should cycle to
'from'");
+ assertTrue(rendered.contains("FROM▼"), "Sort should cycle to 'from'");
}
@Test
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SqlTraceTabRenderTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SqlTraceTabRenderTest.java
index 833c3f3e79f5..b52c5e53d7f3 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SqlTraceTabRenderTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SqlTraceTabRenderTest.java
@@ -151,7 +151,7 @@ class SqlTraceTabRenderTest {
tab.handleKeyEvent(KeyEvent.ofChar('s', KeyModifiers.NONE));
String rendered = TuiTestHelper.renderToString(tab, 140, 25);
- assertTrue(rendered.contains("sort:type"), "Sort should cycle to
'type' after pressing 's'");
+ assertTrue(rendered.contains("TYPE▼"), "Sort should cycle to 'type'
after pressing 's'");
}
@Test