Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
feat: final wiring between components
  • Loading branch information
tembleking committed Oct 3, 2025
commit 629a208aec02838274e479704ee90c2d95daebb2
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
}
}
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: 31 additions & 3 deletions src/app/lsp_server/lsp_server_inner.rs
Original file line number Diff line number Diff line change
@@ -1,12 +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, HoverProviderCapability, 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 @@ -230,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
59 changes: 2 additions & 57 deletions src/app/lsp_server/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,63 +90,8 @@ where
self.inner.read().await.execute_command(params).await
}

async fn hover(&self, _params: HoverParams) -> Result<Option<Hover>> {
Ok(Some(Hover {
contents: tower_lsp::lsp_types::HoverContents::Markup(
tower_lsp::lsp_types::MarkupContent {
kind: tower_lsp::lsp_types::MarkupKind::Markdown,
value: "## Sysdig Scan Result
### Summary
* **Type**: dockerImage
* **PullString**: ubuntu:23.04
* **ImageID**: `sha256:f4cdeba72b994748f5eb1f525a70a9cc553b66037ec37e23645fbf3f0f5c160d`
* **Digest**: `sha256:5a828e28de105c3d7821c4442f0f5d1c52dc16acf4999d5f31a3bc0f03f06edd`
* **BaseOS**: ubuntu 23.04

| TOTAL VULNS FOUND | CRITICAL | HIGH | MEDIUM | LOW | NEGLIGIBLE |
|:------------------:|:--------:|:----:|:--------------:|:--------------:|:----------:|
| 11 | 0 | 0 | 9 (9 Fixable) | 2 (2 Fixable) | 0 |

### Fixable Packages
| PACKAGE | TYPE | VERSION | SUGGESTED FIX | CRITICAL | HIGH | MEDIUM | LOW | NEGLIGIBLE | EXPLOIT |
|:-------------------|:----:|:-----------------------|:-----------------------|:--------:|:----:|:------:|:---:|:----------:|:-------:|
| libgnutls30 | os | 3.7.8-5ubuntu1.1 | 3.7.8-5ubuntu1.2 | - | - | 2 | - | - | - |
| libc-bin | os | 2.37-0ubuntu2.1 | 2.37-0ubuntu2.2 | - | - | 1 | 1 | - | - |
| libc6 | os | 2.37-0ubuntu2.1 | 2.37-0ubuntu2.2 | - | - | 1 | 1 | - | - |
| libpam-modules | os | 1.5.2-5ubuntu1 | 1.5.2-5ubuntu1.1 | - | - | 1 | - | - | - |
| libpam-modules-bin | os | 1.5.2-5ubuntu1 | 1.5.2-5ubuntu1.1 | - | - | 1 | - | - | - |
| libpam-runtime | os | 1.5.2-5ubuntu1 | 1.5.2-5ubuntu1.1 | - | - | 1 | - | - | - |
| libpam0g | os | 1.5.2-5ubuntu1 | 1.5.2-5ubuntu1.1 | - | - | 1 | - | - | - |
| tar | os | 1.34+dfsg-1.2ubuntu0.1 | 1.34+dfsg-1.2ubuntu0.2 | - | - | 1 | - | - | - |

### Policy Evaluation

| POLICY | STATUS | FAILURES | RISKS ACCEPTED |
|:--------------------------------------|:------:|:--------:|:--------------:|
| carholder policy - pk | ❌ | 1 | 0 |
| Critical Vulnerability Found | ✅ | 0 | 0 |
| Forbid Secrets in Images | ✅ | 0 | 0 |
| NIST SP 800-Star | ❌ | 14 | 0 |
| PolicyCardHolder | ❌ | 1 | 0 |
| Sensitive Information or Secret Found | ✅ | 0 | 0 |
| Sysdig Best Practices | ✅ | 0 | 0 |

### Vulnerability Detail

| VULN CVE | SEVERITY | PACKAGES | FIXABLE | EXPLOITABLE | ACCEPTED RISK | AGE |
|---------------|----------|----------|---------|-------------|---------------|-------------|
| CVE-2024-22365| Medium | 4 | ✅ | ❌ | ❌ | 2 years ago |
| CVE-2023-5156 | Medium | 2 | ✅ | ❌ | ❌ | 2 years ago |
| CVE-2023-39804| Medium | 1 | ✅ | ❌ | ❌ | 2 years ago |
| CVE-2024-0553 | Medium | 1 | ✅ | ❌ | ❌ | 2 years ago |
| CVE-2024-0567 | Medium | 1 | ✅ | ❌ | ❌ | 2 years ago |
| CVE-2023-4806 | Low | 2 | ✅ | ❌ | ❌ | 2 years ago |
"
.to_string(),
},
),
range: None,
}))
async fn hover(&self, params: HoverParams) -> Result<Option<Hover>> {
self.inner.read().await.hover(params).await
}

async fn shutdown(&self) -> Result<()> {
Expand Down
38 changes: 19 additions & 19 deletions src/app/markdown/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,6 @@ impl MarkdownData {
Heading::new("FIXABLE".to_string(), Some(HeadingAlignment::Left)),
Heading::new("EXPLOITABLE".to_string(), Some(HeadingAlignment::Left)),
Heading::new("ACCEPTED RISK".to_string(), Some(HeadingAlignment::Left)),
Heading::new("AGE".to_string(), Some(HeadingAlignment::Left)),
];

let data = self
Expand All @@ -213,7 +212,6 @@ impl MarkdownData {
if v.fixable { "✅" } else { "❌" }.to_string(),
if v.exploitable { "✅" } else { "❌" }.to_string(),
if v.accepted_risk { "✅" } else { "❌" }.to_string(),
v.age.to_string(),
]
})
.collect();
Expand Down Expand Up @@ -328,23 +326,32 @@ fn policies_from(value: &ScanResult) -> Vec<PolicyEvaluated> {
failures: p.bundles().iter().map(|b| b.rules().len()).sum::<usize>() as u32,
risks_accepted: 0, // Cannot determine this from the current data model
})
.sorted_by(|a, b| b.failures.cmp(&a.failures))
.sorted_by_key(|p| p.passed)
.collect()
}

fn vulnerabilities_from(value: &ScanResult) -> Vec<VulnerabilityEvaluated> {
value
.vulnerabilities()
.iter()
.sorted_by_key(|v| v.cve())
.sorted_by(|a, b| {
b.found_in_packages()
.len()
.cmp(&a.found_in_packages().len())
})
.sorted_by(|a, b| b.fixable().cmp(&a.fixable()))
.sorted_by(|a, b| b.exploitable().cmp(&a.exploitable()))
.sorted_by_key(|v| v.severity())
.map(|v| VulnerabilityEvaluated {
cve: v.cve().to_string(),
severity: v.severity().to_string(),
packages_found: v.found_in_packages().len() as u32,
fixable: v.fixable(),
exploitable: v.exploitable(),
accepted_risk: !v.accepted_risks().is_empty(),
age: "N/A", // Calculating age requires current time, which is not ideal here.
})
.sorted_by(|a, b| a.cve.cmp(&b.cve))
.collect()
}

Expand Down Expand Up @@ -415,7 +422,6 @@ pub struct VulnerabilityEvaluated {
pub fixable: bool,
pub exploitable: bool,
pub accepted_risk: bool,
pub age: &'static str,
}

#[cfg(test)]
Expand Down Expand Up @@ -614,7 +620,6 @@ mod test {
fixable: true,
exploitable: false,
accepted_risk: false,
age: "2 years ago",
},
VulnerabilityEvaluated {
cve: "CVE-2023-4806".to_string(),
Expand All @@ -623,7 +628,6 @@ mod test {
fixable: true,
exploitable: false,
accepted_risk: false,
age: "2 years ago",
},
VulnerabilityEvaluated {
cve: "CVE-2023-5156".to_string(),
Expand All @@ -632,7 +636,6 @@ mod test {
fixable: true,
exploitable: false,
accepted_risk: false,
age: "2 years ago",
},
VulnerabilityEvaluated {
cve: "CVE-2024-0553".to_string(),
Expand All @@ -641,7 +644,6 @@ mod test {
fixable: true,
exploitable: false,
accepted_risk: false,
age: "2 years ago",
},
VulnerabilityEvaluated {
cve: "CVE-2024-0567".to_string(),
Expand All @@ -650,7 +652,6 @@ mod test {
fixable: true,
exploitable: false,
accepted_risk: false,
age: "2 years ago",
},
VulnerabilityEvaluated {
cve: "CVE-2024-22365".to_string(),
Expand All @@ -659,7 +660,6 @@ mod test {
fixable: true,
exploitable: false,
accepted_risk: false,
age: "2 years ago",
},
],
};
Expand Down Expand Up @@ -703,14 +703,14 @@ mod test {

### Vulnerability Detail

| VULN CVE | SEVERITY | PACKAGES | FIXABLE | EXPLOITABLE | ACCEPTED RISK | AGE |
| :----------- | :----- | :----- | :---- | :-------- | :---------- | :-------- |
| CVE-2023-39804 | Medium | 1 | ✅ | ❌ | ❌ | 2 years ago |
| CVE-2023-4806 | Low | 2 | ✅ | ❌ | ❌ | 2 years ago |
| CVE-2023-5156 | Medium | 2 | ✅ | ❌ | ❌ | 2 years ago |
| CVE-2024-0553 | Medium | 1 | ✅ | ❌ | ❌ | 2 years ago |
| CVE-2024-0567 | Medium | 1 | ✅ | ❌ | ❌ | 2 years ago |
| CVE-2024-22365 | Medium | 4 | ✅ | ❌ | ❌ | 2 years ago |"#;
| VULN CVE | SEVERITY | PACKAGES | FIXABLE | EXPLOITABLE | ACCEPTED RISK |
| :----------- | :----- | :----- | :---- | :-------- | :---------- |
| CVE-2023-39804 | Medium | 1 | ✅ | ❌ | ❌ |
| CVE-2023-4806 | Low | 2 | ✅ | ❌ | ❌ |
| CVE-2023-5156 | Medium | 2 | ✅ | ❌ | ❌ |
| CVE-2024-0553 | Medium | 1 | ✅ | ❌ | ❌ |
| CVE-2024-0567 | Medium | 1 | ✅ | ❌ | ❌ |
| CVE-2024-22365 | Medium | 4 | ✅ | ❌ | ❌ |"#;

assert_eq!(
markdown_data.to_string().trim(),
Expand Down
2 changes: 1 addition & 1 deletion tests/fixtures/compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ services:

# Another service for good measure
api:
image: my-api:1.0
image: quay.io/sysdig/agent-slim:latest
Loading