Skip to content

Commit 66f9fa9

Browse files
authored
feat(hover): display vulnerability summary on hover for images and layers (#21)
This introduces on-hover vulnerability summaries for Docker images and individual layers, providing immediate feedback directly in the editor. Previously, diagnostics would flag that vulnerabilities were present, but offered no details on the affected packages, their severities, or available fixes. This forced users to leave their editor and consult external tools to understand the security risks. With this change, developers can now hover over a scanned image or a `Dockerfile` instruction to see a concise summary, including severity breakdowns and fixable packages. This provides the actionable context needed to assess security impact and begin remediation without interrupting the development workflow.
1 parent 48ad67e commit 66f9fa9

25 files changed

+1233
-18
lines changed

Cargo.lock

Lines changed: 33 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "sysdig-lsp"
3-
version = "0.6.0"
3+
version = "0.7.0"
44
edition = "2024"
55
authors = [ "Sysdig Inc." ]
66
readme = "README.md"
@@ -18,6 +18,7 @@ clap = { version = "4.5.34", features = ["derive"] }
1818
dirs = "6.0.0"
1919
futures = "0.3.31"
2020
itertools = "0.14.0"
21+
markdown-table = "0.2.0"
2122
marked-yaml = { version = "0.8.0", features = ["serde"] }
2223
rand = "0.9.0"
2324
regex = "1.11.1"

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ helping you detect vulnerabilities and misconfigurations earlier in the developm
2222
| Build and Scan Dockerfile | Supported | [Supported](./docs/features/build_and_scan.md) (0.4.0+) |
2323
| Layered image analysis | Supported | [Supported](./docs/features/layered_analysis.md) (0.5.0+) |
2424
| Docker-compose image analysis | Supported | [Supported](./docs/features/docker_compose_image_analysis.md) (0.6.0+) |
25+
| Vulnerability explanation | Supported | [Supported](./docs/features/vulnerability_explanation.md) (0.7.0+) |
2526
| K8s Manifest image analysis | Supported | In roadmap |
2627
| Infrastructure-as-code analysis | Supported | In roadmap |
27-
| Vulnerability explanation | Supported | In roadmap |
2828

2929
## Build
3030

docs/features/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,8 @@ Sysdig LSP provides tools to integrate container security checks into your devel
2121
## [Docker-compose Image Analysis](./docker_compose_image_analysis.md)
2222
- Scans the images defined in your `docker-compose.yml` files for vulnerabilities.
2323

24+
## [Vulnerability Explanation](./vulnerability_explanation.md)
25+
- Displays a detailed summary of scan results when hovering over a scanned image name.
26+
- Provides immediate feedback on vulnerabilities, severities, and available fixes.
27+
2428
See the linked documents for more details.
38.6 MB
Loading
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Vulnerability Explanation
2+
3+
Sysdig LSP provides on-demand vulnerability explanations directly in your editor. After running a scan on an image (e.g., base image, Docker Compose service), you can hover over the image name to see a detailed summary of the scan results.
4+
5+
This feature allows you to quickly assess the security posture of an image without leaving your code, displaying information such as total vulnerabilities, severity breakdown, and fixable packages in a convenient tooltip.
6+
7+
![Sysdig LSP showing a vulnerability summary on hover](./vulnerability_explanation.gif)
8+
9+
## How It Works
10+
11+
1. **Run a Scan**: Use a code action or code lens to scan an image in your `Dockerfile` or `docker-compose.yml`.
12+
2. **Hover to View**: Move your cursor over the image name you just scanned.
13+
3. **Get Instant Feedback**: A tooltip will appear with a formatted Markdown summary of the vulnerabilities found.
14+
15+
This provides immediate context, helping you decide whether to update a base image or investigate a specific package.

src/app/document_database.rs

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::{collections::HashMap, sync::Arc};
22

33
use tokio::sync::RwLock;
4-
use tower_lsp::lsp_types::Diagnostic;
4+
use tower_lsp::lsp_types::{Diagnostic, Position, Range};
55

66
#[derive(Default, Debug, Clone)]
77
pub struct InMemoryDocumentDatabase {
@@ -12,6 +12,13 @@ pub struct InMemoryDocumentDatabase {
1212
struct Document {
1313
pub text: String,
1414
pub diagnostics: Vec<Diagnostic>,
15+
pub documentations: Vec<Documentation>,
16+
}
17+
18+
#[derive(Default, Debug, Clone)]
19+
struct Documentation {
20+
pub range: Range,
21+
pub content: String,
1522
}
1623

1724
impl InMemoryDocumentDatabase {
@@ -71,6 +78,46 @@ impl InMemoryDocumentDatabase {
7178
.into_iter()
7279
.map(|(uri, doc)| (uri, doc.diagnostics))
7380
}
81+
82+
pub async fn append_documentation(&self, uri: &str, range: Range, documentation: String) {
83+
self.documents
84+
.write()
85+
.await
86+
.entry(uri.into())
87+
.and_modify(|d| {
88+
d.documentations.push(Documentation {
89+
range,
90+
content: documentation.clone(),
91+
})
92+
})
93+
.or_insert_with(|| Document {
94+
documentations: vec![Documentation {
95+
range,
96+
content: documentation,
97+
}],
98+
..Default::default()
99+
});
100+
}
101+
102+
pub async fn read_documentation_at(&self, uri: &str, position: Position) -> Option<String> {
103+
let documents = self.documents.read().await;
104+
let document_asked_for = documents.get(uri);
105+
let mut documentations_for_document = document_asked_for
106+
.iter()
107+
.flat_map(|d| d.documentations.iter());
108+
let first_documentation_in_range = documentations_for_document.find(|documentation| {
109+
position > documentation.range.start && position < documentation.range.end
110+
});
111+
112+
first_documentation_in_range.map(|d| d.content.clone())
113+
}
114+
115+
pub async fn remove_documentations(&self, uri: &str) {
116+
let mut documents = self.documents.write().await;
117+
if let Some(document_asked_for) = documents.get_mut(uri) {
118+
document_asked_for.documentations.clear();
119+
};
120+
}
74121
}
75122

76123
#[cfg(test)]

src/app/lsp_interactor.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use tower_lsp::{
22
jsonrpc::Result,
3-
lsp_types::{Diagnostic, MessageType},
3+
lsp_types::{Diagnostic, MessageType, Position, Range},
44
};
55

66
use super::{InMemoryDocumentDatabase, LSPClient};
@@ -26,6 +26,7 @@ where
2626
pub async fn update_document_with_text(&self, uri: &str, text: &str) {
2727
self.document_database.write_document_text(uri, text).await;
2828
self.document_database.remove_diagnostics(uri).await;
29+
self.document_database.remove_documentations(uri).await;
2930
let _ = self.publish_all_diagnostics().await;
3031
}
3132

@@ -56,4 +57,15 @@ where
5657
.append_document_diagnostics(uri, diagnostics)
5758
.await
5859
}
60+
61+
pub async fn append_documentation(&self, uri: &str, range: Range, documentation: String) {
62+
self.document_database
63+
.append_documentation(uri, range, documentation)
64+
.await
65+
}
66+
pub async fn read_documentation_at(&self, uri: &str, position: Position) -> Option<String> {
67+
self.document_database
68+
.read_documentation_at(uri, position)
69+
.await
70+
}
5971
}

src/app/lsp_server/commands/build_and_scan.rs

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use tower_lsp::lsp_types::{
66
Diagnostic, DiagnosticSeverity, Location, MessageType, Position, Range,
77
};
88

9+
use crate::app::markdown::{MarkdownData, MarkdownLayerData};
910
use crate::{
1011
app::{ImageBuilder, ImageScanner, LSPClient, LspInteractor, lsp_server::WithContext},
1112
domain::scanresult::{layer::Layer, scan_result::ScanResult, severity::Severity},
@@ -108,7 +109,8 @@ where
108109
.await;
109110

110111
let diagnostic = diagnostic_for_image(line, &document_text, &scan_result);
111-
let diagnostics_per_layer = diagnostics_for_layers(&document_text, &scan_result)?;
112+
let (diagnostics_per_layer, docs_per_layer) =
113+
diagnostics_for_layers(&document_text, &scan_result)?;
112114

113115
self.interactor.remove_diagnostics(uri).await;
114116
self.interactor
@@ -117,21 +119,34 @@ where
117119
self.interactor
118120
.append_document_diagnostics(uri, &diagnostics_per_layer)
119121
.await;
122+
self.interactor
123+
.append_documentation(
124+
uri,
125+
self.location.range,
126+
MarkdownData::from(scan_result).to_string(),
127+
)
128+
.await;
129+
for (range, docs) in docs_per_layer {
130+
self.interactor.append_documentation(uri, range, docs).await;
131+
}
120132
self.interactor.publish_all_diagnostics().await
121133
}
122134
}
123135

136+
pub type LayerScanResult = (Vec<Diagnostic>, Vec<(Range, String)>);
137+
124138
pub fn diagnostics_for_layers(
125139
document_text: &str,
126140
scan_result: &ScanResult,
127-
) -> Result<Vec<Diagnostic>> {
141+
) -> Result<LayerScanResult> {
128142
let instructions = parse_dockerfile(document_text);
129143
let layers = &scan_result.layers();
130144

131145
let mut instr_idx = instructions.len().checked_sub(1);
132146
let mut layer_idx = layers.len().checked_sub(1);
133147

134148
let mut diagnostics = Vec::new();
149+
let mut docs = Vec::new();
135150

136151
while let (Some(i), Some(l)) = (instr_idx, layer_idx) {
137152
let instr = &instructions[i];
@@ -162,12 +177,16 @@ pub fn diagnostics_for_layers(
162177
};
163178

164179
diagnostics.push(diagnostic);
180+
docs.push((
181+
instr.range,
182+
MarkdownLayerData::from(layer.clone()).to_string(),
183+
));
165184

166185
fill_vulnerability_hints_for_layer(layer, instr.range, &mut diagnostics)
167186
}
168187
}
169188

170-
Ok(diagnostics)
189+
Ok((diagnostics, docs))
171190
}
172191

173192
fn fill_vulnerability_hints_for_layer(

src/app/lsp_server/commands/scan_base_image.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ use itertools::Itertools;
22
use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity, Location, MessageType};
33

44
use crate::{
5-
app::{ImageScanner, LSPClient, LspInteractor, lsp_server::WithContext},
5+
app::{
6+
ImageScanner, LSPClient, LspInteractor, lsp_server::WithContext, markdown::MarkdownData,
7+
},
68
domain::scanresult::severity::Severity,
79
};
810

@@ -103,6 +105,14 @@ where
103105
self.interactor
104106
.append_document_diagnostics(uri, &[diagnostic])
105107
.await;
106-
self.interactor.publish_all_diagnostics().await
108+
self.interactor.publish_all_diagnostics().await?;
109+
self.interactor
110+
.append_documentation(
111+
self.location.uri.as_str(),
112+
self.location.range,
113+
MarkdownData::from(scan_result).to_string(),
114+
)
115+
.await;
116+
Ok(())
107117
}
108118
}

0 commit comments

Comments
 (0)