Skip to content

Commit c96ba99

Browse files
committed
feat: add vuln explanation as hints with url
1 parent 4d10b2e commit c96ba99

File tree

5 files changed

+78
-53
lines changed

5 files changed

+78
-53
lines changed

src/app/commands.rs

Lines changed: 47 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ use tower_lsp::{
1111
use crate::infra::parse_dockerfile;
1212

1313
use super::{
14-
ImageBuilder, ImageScanResult, ImageScanner, InMemoryDocumentDatabase, LSPClient, VulnSeverity,
15-
lsp_server::WithContext,
14+
ImageBuilder, ImageScanResult, ImageScanner, InMemoryDocumentDatabase, LSPClient,
15+
LayerScanResult, VulnSeverity, lsp_server::WithContext,
1616
};
1717

1818
pub struct CommandExecutor<C> {
@@ -246,53 +246,64 @@ pub fn diagnostics_for_layers(
246246
break;
247247
}
248248

249-
if layer.layer_text.contains(&instr.arguments_str) {
250-
instr_idx = instr_idx.and_then(|x| x.checked_sub(1));
251-
layer_idx = layer_idx.and_then(|x| x.checked_sub(1));
252-
253-
let mut msg = String::new();
254-
if layer.count_vulns_of_severity(VulnSeverity::Critical) > 0 {
255-
msg += &format!(
256-
"🟣 {} ",
257-
layer.count_vulns_of_severity(VulnSeverity::Critical)
258-
)
259-
}
260-
if layer.count_vulns_of_severity(VulnSeverity::High) > 0 {
261-
msg += &format!("🔴 {} ", layer.count_vulns_of_severity(VulnSeverity::High))
262-
}
263-
if layer.count_vulns_of_severity(VulnSeverity::Medium) > 0 {
264-
msg += &format!(
265-
"🟠 {} ",
266-
layer.count_vulns_of_severity(VulnSeverity::Medium)
267-
)
268-
}
269-
if layer.count_vulns_of_severity(VulnSeverity::Low) > 0 {
270-
msg += &format!("🟡 {} ", layer.count_vulns_of_severity(VulnSeverity::Low))
271-
}
272-
if layer.count_vulns_of_severity(VulnSeverity::Negligible) > 0 {
273-
msg += &format!(
274-
"⚪ {} ",
275-
layer.count_vulns_of_severity(VulnSeverity::Negligible)
276-
)
277-
}
278-
249+
instr_idx = instr_idx.and_then(|x| x.checked_sub(1));
250+
layer_idx = layer_idx.and_then(|x| x.checked_sub(1));
251+
252+
if layer.has_vulnerabilities() {
253+
let msg = format!(
254+
"Vulnerabilities found in layer: {} Critical, {} High, {} Medium, {} Low, {} Negligible",
255+
layer.count_vulns_of_severity(VulnSeverity::Critical),
256+
layer.count_vulns_of_severity(VulnSeverity::High),
257+
layer.count_vulns_of_severity(VulnSeverity::Medium),
258+
layer.count_vulns_of_severity(VulnSeverity::Low),
259+
layer.count_vulns_of_severity(VulnSeverity::Negligible),
260+
);
279261
let diagnostic = Diagnostic {
280262
range: instr.range,
281263
severity: Some(DiagnosticSeverity::WARNING),
282-
source: Some("Sysdig".to_string()),
283264
message: msg,
284265
..Default::default()
285266
};
286267

287268
diagnostics.push(diagnostic);
288-
} else {
289-
layer_idx = layer_idx.and_then(|x| x.checked_sub(1));
269+
270+
fill_vulnerability_hints_for_layer(layer, instr.range, &mut diagnostics)
290271
}
291272
}
292273

293274
Ok(diagnostics)
294275
}
295276

277+
fn fill_vulnerability_hints_for_layer(
278+
layer: &LayerScanResult,
279+
range: Range,
280+
diagnostics: &mut Vec<Diagnostic>,
281+
) {
282+
let vulnerability_types = [
283+
VulnSeverity::Critical,
284+
VulnSeverity::High,
285+
VulnSeverity::Medium,
286+
VulnSeverity::Low,
287+
VulnSeverity::Negligible,
288+
];
289+
290+
let vulns_per_severity = vulnerability_types
291+
.iter()
292+
.flat_map(|sev| layer.vulnerabilities.iter().filter(|l| l.severity == *sev));
293+
294+
// TODO(fede): eventually we would want to add here a .take() to truncate the number
295+
// of vulnerabilities shown as hint per layer.
296+
vulns_per_severity.for_each(|vuln| {
297+
let url = format!("https://nvd.nist.gov/vuln/detail/{}", vuln.id);
298+
diagnostics.push(Diagnostic {
299+
range,
300+
severity: Some(DiagnosticSeverity::HINT),
301+
message: format!("Vulnerability: {} ({:?}) {}", vuln.id, vuln.severity, url),
302+
..Default::default()
303+
});
304+
});
305+
}
306+
296307
fn diagnostic_for_image(
297308
line: u32,
298309
document_text: &str,
@@ -319,7 +330,7 @@ fn diagnostic_for_image(
319330

320331
if scan_result.has_vulnerabilities() {
321332
diagnostic.message = format!(
322-
"Vulnerabilities found: {} Critical, {} High, {} Medium, {} Low, {} Negligible",
333+
"Total vulnerabilities found: {} Critical, {} High, {} Medium, {} Low, {} Negligible",
323334
scan_result.count_vulns_of_severity(VulnSeverity::Critical),
324335
scan_result.count_vulns_of_severity(VulnSeverity::High),
325336
scan_result.count_vulns_of_severity(VulnSeverity::Medium),

src/app/lsp_server.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ where
144144
}
145145

146146
async fn did_change(&self, params: DidChangeTextDocumentParams) {
147-
if let Some(change) = params.content_changes.into_iter().last() {
147+
if let Some(change) = params.content_changes.into_iter().next_back() {
148148
self.command_executor
149149
.update_document_with_text(params.text_document.uri.as_str(), &change.text)
150150
.await;

src/infra/docker_image_builder.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ use bollard::{Docker, image::BuildImageOptions, secret::BuildInfo};
44
use bytes::Bytes;
55
use futures::StreamExt;
66
use thiserror::Error;
7-
use tracing::info;
87

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

@@ -55,20 +54,23 @@ impl DockerImageBuilder {
5554
.and_then(|osstr| osstr.to_str())
5655
.unwrap(),
5756
t: image_name.as_str(),
58-
rm: true,
57+
// rm: true,
58+
// forcerm: true,
5959
..Default::default()
6060
},
6161
None,
6262
Some(Bytes::from_owner(tar_contents)),
6363
);
6464

65+
let mut build_info = Err(DockerImageBuilderError::Generic(
66+
"image was built, but no id was detected, this should have never happened".to_string(),
67+
));
6568
while let Some(result) = results.next().await {
6669
match result {
6770
Ok(BuildInfo { aux, .. }) if aux.is_some() => {
6871
let image_id = aux.unwrap().id.unwrap();
69-
info!("image built: {}", &image_id);
70-
return Ok(ImageBuildResult {
71-
image_name,
72+
build_info = Ok(ImageBuildResult {
73+
image_name: image_name.clone(),
7274
image_id,
7375
});
7476
}
@@ -77,9 +79,7 @@ impl DockerImageBuilder {
7779
}
7880
}
7981

80-
Err(DockerImageBuilderError::Generic(
81-
"image was built, but no id was detected, this should have never happened".to_string(),
82-
))
82+
build_info
8383
}
8484

8585
async fn pack_containerfile_dir_into_a_tar(

src/infra/sysdig_image_scanner_result.rs

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#![allow(dead_code)]
22

33
use chrono::{DateTime, NaiveDate, Utc};
4+
use itertools::Itertools;
45
use serde::Deserialize;
56
use std::collections::HashMap;
67

@@ -14,9 +15,9 @@ impl From<SysdigImageScannerReport> for ImageScanResult {
1415
.as_ref()
1516
.and_then(|r| r.vulnerabilities.as_ref())
1617
.map(|map| {
17-
map.iter()
18-
.map(|(id, v)| VulnerabilityEntry {
19-
id: id.clone(),
18+
map.values()
19+
.map(|v| VulnerabilityEntry {
20+
id: v.name.clone(),
2021
severity: severity_for(&v.severity),
2122
})
2223
.collect::<Vec<_>>()
@@ -46,7 +47,7 @@ impl From<SysdigImageScannerReport> for ImageScanResult {
4647
fn layers_for_result(scan: &ScanResultResponse) -> Option<Vec<LayerScanResult>> {
4748
// Agrupa cada vuln por digest de capa
4849
let mut layer_map: HashMap<&String, Vec<VulnerabilityEntry>> = HashMap::new();
49-
for (vuln_id, vuln) in scan.vulnerabilities.as_ref()? {
50+
for vuln in scan.vulnerabilities.as_ref()?.values() {
5051
if let (Some(_pkg), Some(layer_ref)) = (
5152
vuln.package_ref.as_ref().and_then(|r| scan.packages.get(r)),
5253
scan.packages
@@ -58,7 +59,7 @@ fn layers_for_result(scan: &ScanResultResponse) -> Option<Vec<LayerScanResult>>
5859
.entry(layer_ref)
5960
.or_default()
6061
.push(VulnerabilityEntry {
61-
id: vuln_id.clone(),
62+
id: vuln.name.clone(),
6263
severity: severity_for(&vuln.severity),
6364
});
6465
}
@@ -67,8 +68,13 @@ fn layers_for_result(scan: &ScanResultResponse) -> Option<Vec<LayerScanResult>>
6768
Some(
6869
scan.layers
6970
.as_ref()?
70-
.iter()
71-
.map(|(_, layer)| {
71+
.values()
72+
.sorted_by(|left, right| {
73+
left.index
74+
.unwrap_or_default()
75+
.cmp(&right.index.unwrap_or_default())
76+
})
77+
.map(|layer| {
7278
let entries = layer_map.get(&layer.digest).cloned().unwrap_or_default();
7379
LayerScanResult {
7480
layer_instruction: layer

tests/fixtures/Dockerfile

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
1+
FROM alpine AS builder
2+
3+
RUN apk update
4+
5+
RUN apk add curl
6+
7+
RUN curl -L https://ftp.belnet.be/mirror/jenkins/war/2.455/jenkins.war -o /jenkins.war
8+
19
FROM nginx:latest
210

311
RUN apt update && apt full-upgrade -y
412

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

0 commit comments

Comments
 (0)