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: add vuln explanation as hints with url
  • Loading branch information
tembleking committed Apr 22, 2025
commit c96ba99cd955ea60c6a4c57e61a61129943a1f24
83 changes: 47 additions & 36 deletions src/app/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ use tower_lsp::{
use crate::infra::parse_dockerfile;

use super::{
ImageBuilder, ImageScanResult, ImageScanner, InMemoryDocumentDatabase, LSPClient, VulnSeverity,
lsp_server::WithContext,
ImageBuilder, ImageScanResult, ImageScanner, InMemoryDocumentDatabase, LSPClient,
LayerScanResult, VulnSeverity, lsp_server::WithContext,
};

pub struct CommandExecutor<C> {
Expand Down Expand Up @@ -246,53 +246,64 @@ pub fn diagnostics_for_layers(
break;
}

if layer.layer_text.contains(&instr.arguments_str) {
instr_idx = instr_idx.and_then(|x| x.checked_sub(1));
layer_idx = layer_idx.and_then(|x| x.checked_sub(1));

let mut msg = String::new();
if layer.count_vulns_of_severity(VulnSeverity::Critical) > 0 {
msg += &format!(
"🟣 {} ",
layer.count_vulns_of_severity(VulnSeverity::Critical)
)
}
if layer.count_vulns_of_severity(VulnSeverity::High) > 0 {
msg += &format!("🔴 {} ", layer.count_vulns_of_severity(VulnSeverity::High))
}
if layer.count_vulns_of_severity(VulnSeverity::Medium) > 0 {
msg += &format!(
"🟠 {} ",
layer.count_vulns_of_severity(VulnSeverity::Medium)
)
}
if layer.count_vulns_of_severity(VulnSeverity::Low) > 0 {
msg += &format!("🟡 {} ", layer.count_vulns_of_severity(VulnSeverity::Low))
}
if layer.count_vulns_of_severity(VulnSeverity::Negligible) > 0 {
msg += &format!(
"⚪ {} ",
layer.count_vulns_of_severity(VulnSeverity::Negligible)
)
}

instr_idx = instr_idx.and_then(|x| x.checked_sub(1));
layer_idx = layer_idx.and_then(|x| x.checked_sub(1));

if layer.has_vulnerabilities() {
let msg = format!(
"Vulnerabilities found in layer: {} Critical, {} High, {} Medium, {} Low, {} Negligible",
layer.count_vulns_of_severity(VulnSeverity::Critical),
layer.count_vulns_of_severity(VulnSeverity::High),
layer.count_vulns_of_severity(VulnSeverity::Medium),
layer.count_vulns_of_severity(VulnSeverity::Low),
layer.count_vulns_of_severity(VulnSeverity::Negligible),
);
let diagnostic = Diagnostic {
range: instr.range,
severity: Some(DiagnosticSeverity::WARNING),
source: Some("Sysdig".to_string()),
message: msg,
..Default::default()
};

diagnostics.push(diagnostic);
} else {
layer_idx = layer_idx.and_then(|x| x.checked_sub(1));

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

Ok(diagnostics)
}

fn fill_vulnerability_hints_for_layer(
layer: &LayerScanResult,
range: Range,
diagnostics: &mut Vec<Diagnostic>,
) {
let vulnerability_types = [
VulnSeverity::Critical,
VulnSeverity::High,
VulnSeverity::Medium,
VulnSeverity::Low,
VulnSeverity::Negligible,
];

let vulns_per_severity = vulnerability_types
.iter()
.flat_map(|sev| layer.vulnerabilities.iter().filter(|l| l.severity == *sev));

// TODO(fede): eventually we would want to add here a .take() to truncate the number
// of vulnerabilities shown as hint per layer.
vulns_per_severity.for_each(|vuln| {
let url = format!("https://nvd.nist.gov/vuln/detail/{}", vuln.id);
diagnostics.push(Diagnostic {
range,
severity: Some(DiagnosticSeverity::HINT),
message: format!("Vulnerability: {} ({:?}) {}", vuln.id, vuln.severity, url),
..Default::default()
});
});
}

fn diagnostic_for_image(
line: u32,
document_text: &str,
Expand All @@ -319,7 +330,7 @@ fn diagnostic_for_image(

if scan_result.has_vulnerabilities() {
diagnostic.message = format!(
"Vulnerabilities found: {} Critical, {} High, {} Medium, {} Low, {} Negligible",
"Total vulnerabilities found: {} Critical, {} High, {} Medium, {} Low, {} Negligible",
scan_result.count_vulns_of_severity(VulnSeverity::Critical),
scan_result.count_vulns_of_severity(VulnSeverity::High),
scan_result.count_vulns_of_severity(VulnSeverity::Medium),
Expand Down
2 changes: 1 addition & 1 deletion src/app/lsp_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ where
}

async fn did_change(&self, params: DidChangeTextDocumentParams) {
if let Some(change) = params.content_changes.into_iter().last() {
if let Some(change) = params.content_changes.into_iter().next_back() {
self.command_executor
.update_document_with_text(params.text_document.uri.as_str(), &change.text)
.await;
Expand Down
16 changes: 8 additions & 8 deletions src/infra/docker_image_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ use bollard::{Docker, image::BuildImageOptions, secret::BuildInfo};
use bytes::Bytes;
use futures::StreamExt;
use thiserror::Error;
use tracing::info;

use crate::app::{ImageBuildError, ImageBuildResult, ImageBuilder};

Expand Down Expand Up @@ -55,20 +54,23 @@ impl DockerImageBuilder {
.and_then(|osstr| osstr.to_str())
.unwrap(),
t: image_name.as_str(),
rm: true,
// rm: true,
// forcerm: true,
..Default::default()
},
None,
Some(Bytes::from_owner(tar_contents)),
);

let mut build_info = Err(DockerImageBuilderError::Generic(
"image was built, but no id was detected, this should have never happened".to_string(),
));
while let Some(result) = results.next().await {
match result {
Ok(BuildInfo { aux, .. }) if aux.is_some() => {
let image_id = aux.unwrap().id.unwrap();
info!("image built: {}", &image_id);
return Ok(ImageBuildResult {
image_name,
build_info = Ok(ImageBuildResult {
image_name: image_name.clone(),
image_id,
});
}
Expand All @@ -77,9 +79,7 @@ impl DockerImageBuilder {
}
}

Err(DockerImageBuilderError::Generic(
"image was built, but no id was detected, this should have never happened".to_string(),
))
build_info
}

async fn pack_containerfile_dir_into_a_tar(
Expand Down
20 changes: 13 additions & 7 deletions src/infra/sysdig_image_scanner_result.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#![allow(dead_code)]

use chrono::{DateTime, NaiveDate, Utc};
use itertools::Itertools;
use serde::Deserialize;
use std::collections::HashMap;

Expand All @@ -14,9 +15,9 @@ impl From<SysdigImageScannerReport> for ImageScanResult {
.as_ref()
.and_then(|r| r.vulnerabilities.as_ref())
.map(|map| {
map.iter()
.map(|(id, v)| VulnerabilityEntry {
id: id.clone(),
map.values()
.map(|v| VulnerabilityEntry {
id: v.name.clone(),
severity: severity_for(&v.severity),
})
.collect::<Vec<_>>()
Expand Down Expand Up @@ -46,7 +47,7 @@ impl From<SysdigImageScannerReport> for ImageScanResult {
fn layers_for_result(scan: &ScanResultResponse) -> Option<Vec<LayerScanResult>> {
// Agrupa cada vuln por digest de capa
let mut layer_map: HashMap<&String, Vec<VulnerabilityEntry>> = HashMap::new();
for (vuln_id, vuln) in scan.vulnerabilities.as_ref()? {
for vuln in scan.vulnerabilities.as_ref()?.values() {
if let (Some(_pkg), Some(layer_ref)) = (
vuln.package_ref.as_ref().and_then(|r| scan.packages.get(r)),
scan.packages
Expand All @@ -58,7 +59,7 @@ fn layers_for_result(scan: &ScanResultResponse) -> Option<Vec<LayerScanResult>>
.entry(layer_ref)
.or_default()
.push(VulnerabilityEntry {
id: vuln_id.clone(),
id: vuln.name.clone(),
severity: severity_for(&vuln.severity),
});
}
Expand All @@ -67,8 +68,13 @@ fn layers_for_result(scan: &ScanResultResponse) -> Option<Vec<LayerScanResult>>
Some(
scan.layers
.as_ref()?
.iter()
.map(|(_, layer)| {
.values()
.sorted_by(|left, right| {
left.index
.unwrap_or_default()
.cmp(&right.index.unwrap_or_default())
})
.map(|layer| {
let entries = layer_map.get(&layer.digest).cloned().unwrap_or_default();
LayerScanResult {
layer_instruction: layer
Expand Down
10 changes: 9 additions & 1 deletion tests/fixtures/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
FROM alpine AS builder

RUN apk update

RUN apk add curl

RUN curl -L https://ftp.belnet.be/mirror/jenkins/war/2.455/jenkins.war -o /jenkins.war

FROM nginx:latest

RUN apt update && apt full-upgrade -y

RUN curl -L https://ftp.belnet.be/mirror/jenkins/war/2.455/jenkins.war -o /jenkins.war
COPY --from=builder /jenkins.war /jenkins.war