Skip to content

Commit 0e36f9e

Browse files
authored
feat: add build and scan dockerfile (#4)
1 parent bf85a4c commit 0e36f9e

19 files changed

+982
-249
lines changed

Cargo.lock

Lines changed: 281 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "sysdig-lsp"
3-
version = "0.3.0"
3+
version = "0.4.0"
44
edition = "2024"
55
authors = [ "Sysdig Inc." ]
66
readme = "README.md"
@@ -11,16 +11,21 @@ publish = false # We don't want to publish it to crates.io yet.
1111

1212
[dependencies]
1313
async-trait = "0.1.85"
14+
bollard = "0.18.1"
15+
bytes = "1.10.1"
1416
chrono = { version = "0.4.40", features = ["serde"] }
1517
clap = { version = "4.5.34", features = ["derive"] }
1618
dirs = "6.0.0"
19+
futures = "0.3.31"
1720
itertools = "0.14.0"
21+
rand = "0.9.0"
1822
regex = "1.11.1"
1923
reqwest = "0.12.14"
2024
semver = "1.0.26"
2125
serde = { version = "1.0.219", features = ["alloc", "derive"] }
2226
serde_json = "1.0.135"
2327
serial_test = { version = "3.2.0", features = ["file_locks"] }
28+
tar = "0.4.44"
2429
thiserror = "2.0.12"
2530
tokio = { version = "1.43.0", features = ["full"] }
2631
tower-lsp = "0.20.0"

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@ helping you detect vulnerabilities and misconfigurations earlier in the developm
1515
1616
## Features
1717

18-
| Feature | **[VSCode Extension](https://github.com/sysdiglabs/vscode-extension)** | **Sysdig LSP** |
18+
| Feature | **[VSCode Extension](https://github.com/sysdiglabs/vscode-extension)** | **[Sysdig LSP](./docs/features/README.md)** |
1919
|---------------------------------|------------------------------------------------------------------------|----------------------------------------------------------|
2020
| Scan base image in Dockerfile | Supported | [Supported](./docs/features/scan_base_image.md) (0.1.0+) |
2121
| Code lens support | Supported | [Supported](./docs/features/code_lens.md) (0.2.0+) |
22-
| Build and Scan Dockerfile | Supported | In roadmap |
22+
| Build and Scan Dockerfile | Supported | [Supported](./docs/features/build_and_scan.md) (0.4.0+) |
2323
| Layered image analysis | Supported | In roadmap |
2424
| Docker-compose image analysis | Supported | In roadmap |
2525
| K8s Manifest image analysis | Supported | In roadmap |

docs/features/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,8 @@ Sysdig LSP provides tools to integrate container security checks into your devel
1010
- Displays actionable commands directly within the editor (e.g., initiating base image scans).
1111
- Enables quick access to frequently performed actions.
1212

13+
## [Build and Scan](./build_and_scan.md)
14+
- Builds and scans the entire final Dockerfile image used in production.
15+
- Supports multi-stage Dockerfiles, analyzing final stage and explicitly copied artifacts from intermediate stages.
16+
1317
See the linked documents for more details.

docs/features/build_and_scan.gif

1.58 MB
Loading

docs/features/build_and_scan.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Build and Scan
2+
3+
Sysdig LSP builds your entire Dockerfile and scans the resulting final image to identify vulnerabilities early in your development workflow.
4+
This ensures the exact image used in production is secure and compliant.
5+
6+
> [!IMPORTANT]
7+
> Sysdig LSP analyzes the fully built final image, including all instructions executed during the build process.
8+
>
9+
> In multi-stage Dockerfiles, only artifacts copied into the final stage using instructions like `COPY --from=build` are analyzed, as intermediate stages are not part of the final runtime environment.
10+
11+
![Sysdig LSP executing build and scan in idea-community](./build_and_scan.gif)
12+
13+
## Examples
14+
15+
### Single-stage Dockerfile (scanned entirely)
16+
17+
```dockerfile
18+
# Base image and all instructions are scanned
19+
FROM alpine:latest
20+
RUN apk add --no-cache python3
21+
COPY ./app /app
22+
```
23+
24+
### Multi-stage Dockerfile (partially scanned)
25+
26+
```dockerfile
27+
# Build stage (scanned only for artifacts copied to final stage)
28+
FROM golang:1.19 AS build
29+
RUN go build -o app main.go
30+
31+
# Final image (fully scanned)
32+
FROM alpine:3.17
33+
COPY --from=build /app /app
34+
ENTRYPOINT ["/app"]
35+
```
36+
37+
In this multi-stage Dockerfile, Sysdig LSP scans the complete final built image, including the final runtime stage (`alpine:3.17`) and any artifacts explicitly copied from previous stages (`golang:1.19`).

flake.lock

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/app/commands.rs

Lines changed: 133 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
1+
use std::{
2+
path::{Path, PathBuf},
3+
str::FromStr,
4+
};
5+
16
use tower_lsp::{
2-
jsonrpc::{Error, ErrorCode, Result},
7+
jsonrpc::{Error, Result},
38
lsp_types::{Diagnostic, DiagnosticSeverity, MessageType, Position, Range},
49
};
510

6-
use super::{ImageScanner, InMemoryDocumentDatabase, LSPClient};
11+
use super::{
12+
ImageBuilder, ImageScanner, InMemoryDocumentDatabase, LSPClient, lsp_server::WithContext,
13+
};
714

815
pub struct CommandExecutor<C> {
916
client: C,
@@ -59,36 +66,27 @@ impl<C> CommandExecutor<C>
5966
where
6067
C: LSPClient,
6168
{
62-
pub async fn scan_image_from_file<S: ImageScanner>(
69+
pub async fn scan_image_from_file(
6370
&self,
6471
uri: &str,
6572
line: u32,
66-
image_scanner: &S,
73+
image_scanner: &impl ImageScanner,
6774
) -> Result<()> {
6875
let document_text = self
6976
.document_database
7077
.read_document_text(uri)
7178
.await
72-
.ok_or_else(|| Error {
73-
code: ErrorCode::InternalError,
74-
message: "unable to obtain document to scan".into(),
75-
data: None,
79+
.ok_or_else(|| {
80+
Error::internal_error().with_message("unable to obtain document to scan")
7681
})?;
7782

7883
let image_for_selected_line =
79-
self.image_from_line(line, &document_text)
80-
.ok_or_else(|| Error {
81-
code: ErrorCode::ParseError,
82-
message: format!("unable to retrieve image for the selected line: {}", line)
83-
.into(),
84-
data: None,
85-
})?;
86-
87-
let range_for_selected_line = document_text
88-
.lines()
89-
.nth(line as usize)
90-
.map(|x| x.len() as u32)
91-
.unwrap_or(u32::MAX);
84+
self.image_from_line(line, &document_text).ok_or_else(|| {
85+
Error::parse_error().with_message(format!(
86+
"unable to retrieve image for the selected line: {}",
87+
line
88+
))
89+
})?;
9290

9391
self.show_message(
9492
MessageType::INFO,
@@ -99,11 +97,7 @@ where
9997
let scan_result = image_scanner
10098
.scan_image(image_for_selected_line)
10199
.await
102-
.map_err(|e| Error {
103-
code: ErrorCode::InternalError,
104-
message: e.to_string().into(),
105-
data: None,
106-
})?;
100+
.map_err(|e| Error::internal_error().with_message(e.to_string()))?;
107101

108102
self.show_message(
109103
MessageType::INFO,
@@ -112,11 +106,20 @@ where
112106
.await;
113107

114108
let diagnostic = {
109+
let range_for_selected_line = Range::new(
110+
Position::new(line, 0),
111+
Position::new(
112+
line,
113+
document_text
114+
.lines()
115+
.nth(line as usize)
116+
.map(|x| x.len() as u32)
117+
.unwrap_or(u32::MAX),
118+
),
119+
);
120+
115121
let mut diagnostic = Diagnostic {
116-
range: Range {
117-
start: Position::new(line, 0),
118-
end: Position::new(line, range_for_selected_line),
119-
},
122+
range: range_for_selected_line,
120123
severity: Some(DiagnosticSeverity::HINT),
121124
message: "No vulnerabilities found.".to_owned(),
122125
..Default::default()
@@ -145,4 +148,104 @@ where
145148
.await;
146149
self.publish_all_diagnostics().await
147150
}
151+
152+
pub async fn build_and_scan_from_file(
153+
&self,
154+
uri: &Path,
155+
line: u32,
156+
image_builder: &impl ImageBuilder,
157+
image_scanner: &impl ImageScanner,
158+
) -> Result<()> {
159+
let document_text = self
160+
.document_database
161+
.read_document_text(uri.to_str().unwrap_or_default())
162+
.await
163+
.ok_or_else(|| {
164+
Error::internal_error().with_message("unable to obtain document to scan")
165+
})?;
166+
167+
let uri_without_file_path = uri
168+
.to_str()
169+
.and_then(|s| s.strip_prefix("file://"))
170+
.ok_or_else(|| {
171+
Error::internal_error().with_message("unable to strip prefix file:// from uri")
172+
})?;
173+
174+
self.show_message(
175+
MessageType::INFO,
176+
format!("Starting build of {}...", uri_without_file_path).as_str(),
177+
)
178+
.await;
179+
180+
let build_result = image_builder
181+
.build_image(&PathBuf::from_str(uri_without_file_path).unwrap())
182+
.await
183+
.map_err(|e| Error::internal_error().with_message(e.to_string()))?;
184+
185+
self.show_message(
186+
MessageType::INFO,
187+
format!(
188+
"Temporal image built '{}', starting scan...",
189+
&build_result.image_name
190+
)
191+
.as_str(),
192+
)
193+
.await;
194+
195+
let scan_result = image_scanner
196+
.scan_image(&build_result.image_name)
197+
.await
198+
.map_err(|e| Error::internal_error().with_message(e.to_string()))?;
199+
200+
self.show_message(
201+
MessageType::INFO,
202+
format!("Finished scan of {}.", &build_result.image_name).as_str(),
203+
)
204+
.await;
205+
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+
};
242+
243+
self.document_database
244+
.remove_diagnostics(uri.to_str().unwrap())
245+
.await;
246+
self.document_database
247+
.append_document_diagnostics(uri.to_str().unwrap(), &[diagnostic])
248+
.await;
249+
self.publish_all_diagnostics().await
250+
}
148251
}

0 commit comments

Comments
 (0)