Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "sysdig-lsp"
version = "0.6.0"
version = "0.7.0"
edition = "2024"
authors = [ "Sysdig Inc." ]
readme = "README.md"
Expand All @@ -18,6 +18,7 @@ clap = { version = "4.5.34", features = ["derive"] }
dirs = "6.0.0"
futures = "0.3.31"
itertools = "0.14.0"
markdown-table = "0.2.0"
marked-yaml = { version = "0.8.0", features = ["serde"] }
rand = "0.9.0"
regex = "1.11.1"
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ helping you detect vulnerabilities and misconfigurations earlier in the developm
| Build and Scan Dockerfile | Supported | [Supported](./docs/features/build_and_scan.md) (0.4.0+) |
| Layered image analysis | Supported | [Supported](./docs/features/layered_analysis.md) (0.5.0+) |
| Docker-compose image analysis | Supported | [Supported](./docs/features/docker_compose_image_analysis.md) (0.6.0+) |
| Vulnerability explanation | Supported | [Supported](./docs/features/vulnerability_explanation.md) (0.7.0+) |
| K8s Manifest image analysis | Supported | In roadmap |
| Infrastructure-as-code analysis | Supported | In roadmap |
| Vulnerability explanation | Supported | In roadmap |

## Build

Expand Down
4 changes: 4 additions & 0 deletions docs/features/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,8 @@ Sysdig LSP provides tools to integrate container security checks into your devel
## [Docker-compose Image Analysis](./docker_compose_image_analysis.md)
- Scans the images defined in your `docker-compose.yml` files for vulnerabilities.

## [Vulnerability Explanation](./vulnerability_explanation.md)
- Displays a detailed summary of scan results when hovering over a scanned image name.
- Provides immediate feedback on vulnerabilities, severities, and available fixes.

See the linked documents for more details.
Binary file added docs/features/vulnerability_explanation.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 15 additions & 0 deletions docs/features/vulnerability_explanation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Vulnerability Explanation

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.

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.

![Sysdig LSP showing a vulnerability summary on hover](./vulnerability_explanation.gif)

## How It Works

1. **Run a Scan**: Use a code action or code lens to scan an image in your `Dockerfile` or `docker-compose.yml`.
2. **Hover to View**: Move your cursor over the image name you just scanned.
3. **Get Instant Feedback**: A tooltip will appear with a formatted Markdown summary of the vulnerabilities found.

This provides immediate context, helping you decide whether to update a base image or investigate a specific package.
49 changes: 48 additions & 1 deletion src/app/document_database.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::{collections::HashMap, sync::Arc};

use tokio::sync::RwLock;
use tower_lsp::lsp_types::Diagnostic;
use tower_lsp::lsp_types::{Diagnostic, Position, Range};

#[derive(Default, Debug, Clone)]
pub struct InMemoryDocumentDatabase {
Expand All @@ -12,6 +12,13 @@ pub struct InMemoryDocumentDatabase {
struct Document {
pub text: String,
pub diagnostics: Vec<Diagnostic>,
pub documentations: Vec<Documentation>,
}

#[derive(Default, Debug, Clone)]
struct Documentation {
pub range: Range,
pub content: String,
}

impl InMemoryDocumentDatabase {
Expand Down Expand Up @@ -71,6 +78,46 @@ impl InMemoryDocumentDatabase {
.into_iter()
.map(|(uri, doc)| (uri, doc.diagnostics))
}

pub async fn append_documentation(&self, uri: &str, range: Range, documentation: String) {
self.documents
.write()
.await
.entry(uri.into())
.and_modify(|d| {
d.documentations.push(Documentation {
range,
content: documentation.clone(),
})
})
.or_insert_with(|| Document {
documentations: vec![Documentation {
range,
content: documentation,
}],
..Default::default()
});
}

pub async fn read_documentation_at(&self, uri: &str, position: Position) -> Option<String> {
let documents = self.documents.read().await;
let document_asked_for = documents.get(uri);
let mut documentations_for_document = document_asked_for
.iter()
.flat_map(|d| d.documentations.iter());
let first_documentation_in_range = documentations_for_document.find(|documentation| {
position > documentation.range.start && position < documentation.range.end
});

first_documentation_in_range.map(|d| d.content.clone())
}

pub async fn remove_documentations(&self, uri: &str) {
let mut documents = self.documents.write().await;
if let Some(document_asked_for) = documents.get_mut(uri) {
document_asked_for.documentations.clear();
};
}
}

#[cfg(test)]
Expand Down
14 changes: 13 additions & 1 deletion src/app/lsp_interactor.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use tower_lsp::{
jsonrpc::Result,
lsp_types::{Diagnostic, MessageType},
lsp_types::{Diagnostic, MessageType, Position, Range},
};

use super::{InMemoryDocumentDatabase, LSPClient};
Expand All @@ -26,6 +26,7 @@ where
pub async fn update_document_with_text(&self, uri: &str, text: &str) {
self.document_database.write_document_text(uri, text).await;
self.document_database.remove_diagnostics(uri).await;
self.document_database.remove_documentations(uri).await;
let _ = self.publish_all_diagnostics().await;
}

Expand Down Expand Up @@ -56,4 +57,15 @@ where
.append_document_diagnostics(uri, diagnostics)
.await
}

pub async fn append_documentation(&self, uri: &str, range: Range, documentation: String) {
self.document_database
.append_documentation(uri, range, documentation)
.await
}
pub async fn read_documentation_at(&self, uri: &str, position: Position) -> Option<String> {
self.document_database
.read_documentation_at(uri, position)
.await
}
}
25 changes: 22 additions & 3 deletions src/app/lsp_server/commands/build_and_scan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use tower_lsp::lsp_types::{
Diagnostic, DiagnosticSeverity, Location, MessageType, Position, Range,
};

use crate::app::markdown::{MarkdownData, MarkdownLayerData};
use crate::{
app::{ImageBuilder, ImageScanner, LSPClient, LspInteractor, lsp_server::WithContext},
domain::scanresult::{layer::Layer, scan_result::ScanResult, severity::Severity},
Expand Down Expand Up @@ -108,7 +109,8 @@ where
.await;

let diagnostic = diagnostic_for_image(line, &document_text, &scan_result);
let diagnostics_per_layer = diagnostics_for_layers(&document_text, &scan_result)?;
let (diagnostics_per_layer, docs_per_layer) =
diagnostics_for_layers(&document_text, &scan_result)?;

self.interactor.remove_diagnostics(uri).await;
self.interactor
Expand All @@ -117,21 +119,34 @@ where
self.interactor
.append_document_diagnostics(uri, &diagnostics_per_layer)
.await;
self.interactor
.append_documentation(
uri,
self.location.range,
MarkdownData::from(scan_result).to_string(),
)
.await;
for (range, docs) in docs_per_layer {
self.interactor.append_documentation(uri, range, docs).await;
}
self.interactor.publish_all_diagnostics().await
}
}

pub type LayerScanResult = (Vec<Diagnostic>, Vec<(Range, String)>);

pub fn diagnostics_for_layers(
document_text: &str,
scan_result: &ScanResult,
) -> Result<Vec<Diagnostic>> {
) -> Result<LayerScanResult> {
let instructions = parse_dockerfile(document_text);
let layers = &scan_result.layers();

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

let mut diagnostics = Vec::new();
let mut docs = Vec::new();

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

diagnostics.push(diagnostic);
docs.push((
instr.range,
MarkdownLayerData::from(layer.clone()).to_string(),
));

fill_vulnerability_hints_for_layer(layer, instr.range, &mut diagnostics)
}
}

Ok(diagnostics)
Ok((diagnostics, docs))
}

fn fill_vulnerability_hints_for_layer(
Expand Down
14 changes: 12 additions & 2 deletions src/app/lsp_server/commands/scan_base_image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ use itertools::Itertools;
use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity, Location, MessageType};

use crate::{
app::{ImageScanner, LSPClient, LspInteractor, lsp_server::WithContext},
app::{
ImageScanner, LSPClient, LspInteractor, lsp_server::WithContext, markdown::MarkdownData,
},
domain::scanresult::severity::Severity,
};

Expand Down Expand Up @@ -103,6 +105,14 @@ where
self.interactor
.append_document_diagnostics(uri, &[diagnostic])
.await;
self.interactor.publish_all_diagnostics().await
self.interactor.publish_all_diagnostics().await?;
self.interactor
.append_documentation(
self.location.uri.as_str(),
self.location.range,
MarkdownData::from(scan_result).to_string(),
)
.await;
Ok(())
}
}
34 changes: 32 additions & 2 deletions src/app/lsp_server/lsp_server_inner.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
use serde_json::Value;
use tower_lsp::jsonrpc::{Error, ErrorCode, Result};
use tower_lsp::lsp_types::HoverContents::Markup;
use tower_lsp::lsp_types::MarkupKind::Markdown;
use tower_lsp::lsp_types::{
CodeActionOrCommand, CodeActionParams, CodeActionProviderCapability, CodeActionResponse,
CodeLens, CodeLensOptions, CodeLensParams, DidChangeConfigurationParams,
DidChangeTextDocumentParams, DidOpenTextDocumentParams, ExecuteCommandOptions,
ExecuteCommandParams, InitializeParams, InitializeResult, InitializedParams, MessageType,
ServerCapabilities, TextDocumentSyncCapability, TextDocumentSyncKind,
ExecuteCommandParams, Hover, HoverParams, HoverProviderCapability, InitializeParams,
InitializeResult, InitializedParams, MarkupContent, MessageType, ServerCapabilities,
TextDocumentSyncCapability, TextDocumentSyncKind,
};
use tracing::{debug, info};

Expand Down Expand Up @@ -105,6 +108,7 @@ where
commands: SupportedCommands::all_supported_commands_as_string(),
..Default::default()
}),
hover_provider: Some(HoverProviderCapability::Simple(true)),
..Default::default()
},
..Default::default()
Expand Down Expand Up @@ -228,6 +232,32 @@ where
}
}

pub async fn hover(&self, params: HoverParams) -> Result<Option<Hover>> {
let documentation_found = self
.interactor
.read_documentation_at(
params
.text_document_position_params
.text_document
.uri
.as_str(),
params.text_document_position_params.position,
)
.await;

if documentation_found.is_none() {
return Ok(None);
}

Ok(Some(Hover {
contents: Markup(MarkupContent {
kind: Markdown,
value: documentation_found.unwrap(),
}),
range: None,
}))
}

pub async fn shutdown(&self) -> Result<()> {
Ok(())
}
Expand Down
Loading