Skip to content

Commit 0e61021

Browse files
committed
feat: add layered analysis scanning
1 parent e73d288 commit 0e61021

File tree

8 files changed

+552
-102
lines changed

8 files changed

+552
-102
lines changed

src/app/commands.rs

Lines changed: 126 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@ use tower_lsp::{
88
lsp_types::{Diagnostic, DiagnosticSeverity, MessageType, Position, Range},
99
};
1010

11+
use crate::infra::parse_dockerfile;
12+
1113
use super::{
12-
ImageBuilder, ImageScanner, InMemoryDocumentDatabase, LSPClient, lsp_server::WithContext,
14+
ImageBuilder, ImageScanResult, ImageScanner, InMemoryDocumentDatabase, LSPClient, VulnSeverity,
15+
lsp_server::WithContext,
1316
};
1417

1518
pub struct CommandExecutor<C> {
@@ -126,10 +129,14 @@ where
126129
};
127130

128131
if scan_result.has_vulnerabilities() {
129-
let v = &scan_result.vulnerabilities;
130132
diagnostic.message = format!(
131133
"Vulnerabilities found for {}: {} Critical, {} High, {} Medium, {} Low, {} Negligible",
132-
image_for_selected_line, v.critical, v.high, v.medium, v.low, v.negligible
134+
image_for_selected_line,
135+
scan_result.count_vulns_of_severity(VulnSeverity::Critical),
136+
scan_result.count_vulns_of_severity(VulnSeverity::High),
137+
scan_result.count_vulns_of_severity(VulnSeverity::Medium),
138+
scan_result.count_vulns_of_severity(VulnSeverity::Low),
139+
scan_result.count_vulns_of_severity(VulnSeverity::Negligible),
133140
);
134141

135142
diagnostic.severity = Some(if scan_result.is_compliant {
@@ -203,49 +210,129 @@ where
203210
)
204211
.await;
205212

206-
let diagnostic = {
207-
let range_for_selected_line = Range::new(
208-
Position::new(line, 0),
209-
Position::new(
210-
line,
211-
document_text
212-
.lines()
213-
.nth(line as usize)
214-
.map(|x| x.len() as u32)
215-
.unwrap_or(u32::MAX),
216-
),
217-
);
218-
219-
let mut diagnostic = Diagnostic {
220-
range: range_for_selected_line,
221-
severity: Some(DiagnosticSeverity::HINT),
222-
message: "No vulnerabilities found.".to_owned(),
223-
..Default::default()
224-
};
225-
226-
if scan_result.has_vulnerabilities() {
227-
let v = &scan_result.vulnerabilities;
228-
diagnostic.message = format!(
229-
"Vulnerabilities found for Dockerfile in {}: {} Critical, {} High, {} Medium, {} Low, {} Negligible",
230-
uri_without_file_path, v.critical, v.high, v.medium, v.low, v.negligible
231-
);
232-
233-
diagnostic.severity = Some(if scan_result.is_compliant {
234-
DiagnosticSeverity::INFORMATION
235-
} else {
236-
DiagnosticSeverity::ERROR
237-
});
238-
}
239-
240-
diagnostic
241-
};
213+
let diagnostic = diagnostic_for_image(line, &document_text, &scan_result);
214+
let diagnostics_per_layer = diagnostics_for_layers(&document_text, &scan_result)?;
242215

243216
self.document_database
244217
.remove_diagnostics(uri.to_str().unwrap())
245218
.await;
246219
self.document_database
247220
.append_document_diagnostics(uri.to_str().unwrap(), &[diagnostic])
248221
.await;
222+
self.document_database
223+
.append_document_diagnostics(uri.to_str().unwrap(), &diagnostics_per_layer)
224+
.await;
249225
self.publish_all_diagnostics().await
250226
}
251227
}
228+
229+
pub fn diagnostics_for_layers(
230+
document_text: &str,
231+
scan_result: &ImageScanResult,
232+
) -> Result<Vec<Diagnostic>> {
233+
let instructions = parse_dockerfile(document_text);
234+
let layers = &scan_result.layers;
235+
236+
let mut instr_idx = instructions.len().checked_sub(1);
237+
let mut layer_idx = layers.len().checked_sub(1);
238+
239+
let mut diagnostics = Vec::new();
240+
241+
while let (Some(i), Some(l)) = (instr_idx, layer_idx) {
242+
let instr = &instructions[i];
243+
let layer = &layers[l];
244+
245+
if instr.keyword == "FROM" {
246+
break;
247+
}
248+
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+
279+
let diagnostic = Diagnostic {
280+
range: instr.range,
281+
severity: Some(DiagnosticSeverity::WARNING),
282+
source: Some("Sysdig".to_string()),
283+
message: msg,
284+
..Default::default()
285+
};
286+
287+
diagnostics.push(diagnostic);
288+
} else {
289+
layer_idx = layer_idx.and_then(|x| x.checked_sub(1));
290+
}
291+
}
292+
293+
Ok(diagnostics)
294+
}
295+
296+
fn diagnostic_for_image(
297+
line: u32,
298+
document_text: &str,
299+
scan_result: &ImageScanResult,
300+
) -> Diagnostic {
301+
let range_for_selected_line = Range::new(
302+
Position::new(line, 0),
303+
Position::new(
304+
line,
305+
document_text
306+
.lines()
307+
.nth(line as usize)
308+
.map(|x| x.len() as u32)
309+
.unwrap_or(u32::MAX),
310+
),
311+
);
312+
313+
let mut diagnostic = Diagnostic {
314+
range: range_for_selected_line,
315+
severity: Some(DiagnosticSeverity::HINT),
316+
message: "No vulnerabilities found.".to_owned(),
317+
..Default::default()
318+
};
319+
320+
if scan_result.has_vulnerabilities() {
321+
diagnostic.message = format!(
322+
"Vulnerabilities found: {} Critical, {} High, {} Medium, {} Low, {} Negligible",
323+
scan_result.count_vulns_of_severity(VulnSeverity::Critical),
324+
scan_result.count_vulns_of_severity(VulnSeverity::High),
325+
scan_result.count_vulns_of_severity(VulnSeverity::Medium),
326+
scan_result.count_vulns_of_severity(VulnSeverity::Low),
327+
scan_result.count_vulns_of_severity(VulnSeverity::Negligible),
328+
);
329+
330+
diagnostic.severity = Some(if scan_result.is_compliant {
331+
DiagnosticSeverity::INFORMATION
332+
} else {
333+
DiagnosticSeverity::ERROR
334+
});
335+
}
336+
337+
diagnostic
338+
}

src/app/image_scanner.rs

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,62 @@ pub trait ImageScanner {
77
async fn scan_image(&self, image_pull_string: &str) -> Result<ImageScanResult, ImageScanError>;
88
}
99

10-
#[derive(Clone, Copy, Debug)]
10+
#[derive(Clone, Debug)]
1111
pub struct ImageScanResult {
12-
pub vulnerabilities: Vulnerabilities,
12+
pub vulnerabilities: Vec<VulnerabilityEntry>,
1313
pub is_compliant: bool,
14+
pub layers: Vec<LayerScanResult>,
1415
}
1516

1617
impl ImageScanResult {
18+
pub fn count_vulns_of_severity(&self, severity: VulnSeverity) -> usize {
19+
self.vulnerabilities
20+
.iter()
21+
.filter(|v| v.severity == severity)
22+
.count()
23+
}
24+
25+
pub fn has_vulnerabilities(&self) -> bool {
26+
!self.vulnerabilities.is_empty()
27+
}
28+
}
29+
30+
#[derive(Clone, Debug)]
31+
pub struct VulnerabilityEntry {
32+
pub id: String,
33+
pub severity: VulnSeverity,
34+
}
35+
36+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
37+
pub enum VulnSeverity {
38+
Critical,
39+
High,
40+
Medium,
41+
Low,
42+
Negligible,
43+
}
44+
45+
#[derive(Clone, Debug)]
46+
pub struct LayerScanResult {
47+
pub layer_instruction: String,
48+
pub layer_text: String,
49+
pub vulnerabilities: Vec<VulnerabilityEntry>,
50+
}
51+
52+
impl LayerScanResult {
53+
pub fn count_vulns_of_severity(&self, severity: VulnSeverity) -> usize {
54+
self.vulnerabilities
55+
.iter()
56+
.filter(|v| v.severity == severity)
57+
.count()
58+
}
59+
1760
pub fn has_vulnerabilities(&self) -> bool {
18-
let v = &self.vulnerabilities;
19-
v.critical > 0 || v.high > 0 || v.medium > 0 || v.low > 0 || v.negligible > 0
61+
!self.vulnerabilities.is_empty()
2062
}
2163
}
2264

23-
#[derive(Clone, Copy, Debug)]
65+
#[derive(Clone, Copy, Debug, Default)]
2466
pub struct Vulnerabilities {
2567
pub critical: usize,
2668
pub high: usize,

src/app/mod.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ mod queries;
99

1010
pub use document_database::*;
1111
pub use image_builder::{ImageBuildError, ImageBuildResult, ImageBuilder};
12-
pub use image_scanner::{ImageScanError, ImageScanResult, ImageScanner, Vulnerabilities};
12+
pub use image_scanner::{
13+
ImageScanError, ImageScanResult, ImageScanner, LayerScanResult, VulnSeverity, Vulnerabilities,
14+
VulnerabilityEntry,
15+
};
1316
pub use lsp_client::LSPClient;
1417
pub use lsp_server::LSPServer;

0 commit comments

Comments
 (0)