diff --git a/AGENTS.md b/AGENTS.md index b6c84d2..a49cc56 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -89,11 +89,14 @@ Key components: * **`DockerImageBuilder`** * Builds container images using Bollard (Docker API client). -* **Dockerfile / Compose AST Parsers** +* **Dockerfile / Compose / K8s Manifest AST Parsers** * Parse Dockerfiles to extract image references from `FROM` instructions (including multi-stage builds). * Parse Docker Compose YAML (e.g. service `image:` fields). + * Parse Kubernetes manifests YAML (e.g. `containers[].image` and `initContainers[].image` fields). + * K8s manifests are detected by checking for both `apiVersion:` and `kind:` fields in YAML files. + * Supports all common K8s resource types: Pods, Deployments, StatefulSets, DaemonSets, Jobs, CronJobs. * Handle complex scenarios such as build args and multi-platform images. - * Implemented via modules like `ast_parser.rs`. + * Implemented via modules like `dockerfile_ast_parser.rs`, `compose_ast_parser.rs`, and `k8s_manifest_ast_parser.rs`. * **`ScannerBinaryManager`** * Downloads the Sysdig CLI scanner binary on demand. @@ -165,6 +168,9 @@ The project uses `just` as a command runner to encapsulate common workflows. Additional helpful commands: * `cargo test -- --nocapture` – run tests with full output when debugging. +* `cargo test --lib` – run only unit tests (faster than running all tests). + +**Important:** The tests `infra::sysdig_image_scanner::tests::it_scans_popular_images_correctly_test::case_*` are very slow because they scan real container images. These tests should only be run when making changes to the image scanner. For day-to-day development, skip them or run focused tests instead. ### 3.4 Pre-commit Hooks @@ -313,7 +319,191 @@ Check the workflow file in case of doubt. --- -## 8. Commit & Pull Request Guidelines +## 8. Development Patterns & Common Gotchas + +This section documents important patterns, findings, and gotchas discovered during development that are critical for maintaining consistency and avoiding common pitfalls. + +### 8.1 Adding Support for New File Types + +When adding support for a new file type (e.g. Kubernetes manifests, Terraform files), follow this pattern established by Docker Compose and K8s manifest implementations: + +#### Step 1: Create a Parser Module + +1. **Create parser in `src/infra/`**: e.g. `k8s_manifest_ast_parser.rs` + - Define an `ImageInstruction` struct with `image_name` and `range` (LSP Range) + - Create a `parse_*` function that returns `Result, ParseError>` + - Use `marked_yaml` for YAML parsing to preserve position information for accurate LSP ranges + - Include comprehensive unit tests covering: + - Simple cases + - Multiple images + - Edge cases (empty, null, invalid YAML) + - Complex image names with registries + - Quoted values + +2. **Export the parser in `src/infra/mod.rs`**: + ```rust + mod k8s_manifest_ast_parser; + pub use k8s_manifest_ast_parser::parse_k8s_manifest; + ``` + +#### Step 2: Integrate into Command Generator + +3. **Update `src/app/lsp_server/command_generator.rs`**: + - Add import for the new parser + - Create a detection function (e.g. `is_k8s_manifest_file()`) + - **IMPORTANT**: Detect by content, not just file extension to avoid false positives + - Example: K8s manifests must contain both `apiVersion:` and `kind:` fields + - Add branch in `generate_commands_for_uri()` to route to the new file type + - Create a `generate_*_commands()` function following the established pattern: + ```rust + fn generate_k8s_manifest_commands(url: &Url, content: &str) -> Result, String> { + let mut commands = vec![]; + match parse_k8s_manifest(content) { + Ok(instructions) => { + for instruction in instructions { + commands.push( + SupportedCommands::ExecuteBaseImageScan { + location: Location::new(url.clone(), instruction.range), + image: instruction.image_name, + } + .into(), + ); + } + } + Err(err) => return Err(format!("{}", err)), + } + Ok(commands) + } + ``` + +#### Step 3: Add Integration Tests + +4. **Create fixture in `tests/fixtures/`**: e.g. `k8s-deployment.yaml` +5. **Add integration test in `tests/general.rs`**: + - Test code lens generation + - Verify correct ranges and image names + - Use existing patterns from compose tests as reference + +#### Step 4: Update Documentation + +6. **Update `README.md`**: Add feature to the features table with version number +7. **Update `AGENTS.md`**: Document the parser in architecture section +8. **Create feature doc**: Add `docs/features/.md` with examples +9. **Update `docs/features/README.md`**: Add entry for the new feature + +### 8.2 File Type Detection Gotchas + +**❌ DON'T**: Rely solely on file extensions for detection +```rust +// BAD: Matches ALL YAML files including compose files +fn is_k8s_manifest_file(file_uri: &str) -> bool { + file_uri.ends_with(".yaml") || file_uri.ends_with(".yml") +} +``` + +**✅ DO**: Combine file extension with content-based detection +```rust +// GOOD: Checks both extension AND content +fn is_k8s_manifest_file(file_uri: &str, content: &str) -> bool { + if !(file_uri.ends_with(".yaml") || file_uri.ends_with(".yml")) { + return false; + } + content.contains("apiVersion:") && content.contains("kind:") +} +``` + +**Why**: File extensions alone can cause false positives. Docker Compose files, K8s manifests, and generic YAML files all use `.yaml`/`.yml` extensions. Content-based detection ensures accurate routing. + +### 8.3 Diagnostic Severity Logic + +The diagnostic severity shown in the editor should reflect the **actual vulnerability severity**, not just policy evaluation results. + +**Current Implementation** (in `src/app/lsp_server/commands/scan_base_image.rs`): +```rust +diagnostic.severity = Some(if *critical_count > 0 || *high_count > 0 { + DiagnosticSeverity::ERROR // Red +} else if *medium_count > 0 { + DiagnosticSeverity::WARNING // Yellow +} else { + DiagnosticSeverity::INFORMATION // Blue +}); +``` + +**Gotcha**: The previous implementation used `scan_result.evaluation_result().is_passed()` which only reflected policy pass/fail. This caused High/Critical vulnerabilities to show as INFORMATION (blue) if the policy passed, which was confusing for users. + +**When modifying severity logic**: Always base it on vulnerability counts/severity, not policy evaluation. + +### 8.4 LSP Range Calculation + +When parsing files to extract ranges for code lenses: + +1. **Use position-aware parsers**: `marked_yaml` for YAML, custom parsers for Dockerfiles +2. **Account for quotes**: Image names might be quoted in YAML (`"nginx:latest"` or `'nginx:latest'`) + ```rust + let mut raw_len = image_name.len(); + if let Some(c) = first_char && (c == '"' || c == '\'') { + raw_len += 2; // Include quotes in range + } + ``` +3. **Test with various formats**: Unquoted, single-quoted, double-quoted values +4. **0-indexed LSP positions**: LSP uses 0-indexed line/character positions, but some parsers (like `marked_yaml`) use 1-indexed positions - convert accordingly: + ```rust + let start_line = start.line() as u32 - 1; + let start_char = start.column() as u32 - 1; + ``` + +### 8.5 Testing Patterns + +**Unit Tests** (`#[cfg(test)]` in modules): +- Test parser logic in isolation +- Use string literals for test input +- Cover edge cases exhaustively +- Run fast (no I/O) + +**Integration Tests** (`tests/general.rs`): +- Test full LSP flow: `did_open` → `code_lens` → `execute_command` +- Use fixtures from `tests/fixtures/` +- Mock external dependencies (ImageScanner) with `mockall` +- Verify JSON serialization of LSP responses + +**Slow Tests to Skip**: +- `infra::sysdig_image_scanner::tests::it_scans_popular_images_correctly_test::case_*` +- These scan real container images over the network +- Only run when changing scanner-related code +- Use `cargo test --lib -- --skip it_scans_popular_images_correctly_test` for faster feedback + +### 8.6 Common Command Patterns + +When adding new LSP commands: + +1. **Define in `supported_commands.rs`**: Add to `SupportedCommands` enum +2. **Implement in `commands/` directory**: Create a struct implementing `LspCommand` trait +3. **Wire in `lsp_server_inner.rs`**: Add execution handler +4. **Generate in `command_generator.rs`**: Create CommandInfo for code lenses +5. **Test in `tests/general.rs`**: Verify command execution and results + +### 8.7 Version Bumping Strategy + +Follow semantic versioning for unstable versions (0.X.Y): + +- **Patch (0.X.Y → 0.X.Y+1)**: Bug fixes, documentation, refactoring +- **Minor (0.X.Y → 0.X+1.0)**: New features, enhancements +- **Don't stabilize (1.0.0)** unless explicitly instructed + +**When to release**: +- ✅ New feature implemented +- ✅ Bug fixes +- ❌ CI/refactoring/internal changes (no user impact) +- ❌ Documentation-only changes + +**Release process**: +1. Update version in `Cargo.toml` +2. Commit and merge to default branch +3. GitHub Actions workflow automatically creates release with cross-compiled binaries + +--- + +## 9. Commit & Pull Request Guidelines To keep history clean and reviews manageable: diff --git a/Cargo.lock b/Cargo.lock index bd30eea..d4411ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1956,7 +1956,7 @@ dependencies = [ [[package]] name = "sysdig-lsp" -version = "0.7.5" +version = "0.8.0" dependencies = [ "async-trait", "bollard", diff --git a/Cargo.toml b/Cargo.toml index fc24b46..cc2fc54 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sysdig-lsp" -version = "0.7.5" +version = "0.8.0" edition = "2024" authors = [ "Sysdig Inc." ] readme = "README.md" diff --git a/README.md b/README.md index ca399a3..5a9844b 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ helping you detect vulnerabilities and misconfigurations earlier in the developm | Layered image analysis | Supported | [Supported](./docs/features/layered_analysis.md) (0.5.0+) | | Docker-compose image analysis | Supported | [Supported](./docs/features/docker_compose_image_analysis.md) (0.6.0+) | | Vulnerability explanation | Supported | [Supported](./docs/features/vulnerability_explanation.md) (0.7.0+) | -| K8s Manifest image analysis | Supported | In roadmap | +| K8s Manifest image analysis | Supported | [Supported](./docs/features/k8s_manifest_image_analysis.md) (0.8.0+) | | Infrastructure-as-code analysis | Supported | In roadmap | ## Build diff --git a/docs/features/README.md b/docs/features/README.md index 957c09a..a4dd40d 100644 --- a/docs/features/README.md +++ b/docs/features/README.md @@ -21,6 +21,10 @@ Sysdig LSP provides tools to integrate container security checks into your devel ## [Docker-compose Image Analysis](./docker_compose_image_analysis.md) - Scans the images defined in your `docker-compose.yml` files for vulnerabilities. +## [Kubernetes Manifest Image Analysis](./k8s_manifest_image_analysis.md) +- Scans container images defined in Kubernetes manifest files for vulnerabilities. +- Supports Pods, Deployments, StatefulSets, DaemonSets, Jobs, and CronJobs. + ## [Vulnerability Explanation](./vulnerability_explanation.md) - Displays a detailed summary of scan results when hovering over a scanned image name. - Provides immediate feedback on vulnerabilities, severities, and available fixes. diff --git a/docs/features/k8s_manifest_image_analysis.md b/docs/features/k8s_manifest_image_analysis.md new file mode 100644 index 0000000..ce826e2 --- /dev/null +++ b/docs/features/k8s_manifest_image_analysis.md @@ -0,0 +1,43 @@ +# Kubernetes Manifest Image Analysis + +Sysdig LSP scans the container images defined in your Kubernetes manifest files to identify vulnerabilities. + +> [!IMPORTANT] +> Sysdig LSP analyzes container images from `containers` and `initContainers` in your Kubernetes manifests. + +![Sysdig LSP executing k8s manifest image scan](./k8s_manifest_image_analysis.png) + +## Supported Kubernetes Resources + +Sysdig LSP supports scanning images from the following Kubernetes resource types: + +- Pods +- Deployments +- StatefulSets +- DaemonSets +- Jobs +- CronJobs +- ReplicaSets + +## Example + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: web-deployment +spec: + replicas: 3 + template: + spec: + initContainers: + - name: init-myservice + image: busybox:1.28 + containers: + - name: nginx + image: nginx:1.19 + - name: sidecar + image: busybox:latest +``` + +In this example, Sysdig LSP will provide actions to scan all three images: `busybox:1.28`, `nginx:1.19`, and `busybox:latest`. diff --git a/docs/features/k8s_manifest_image_analysis.png b/docs/features/k8s_manifest_image_analysis.png new file mode 100644 index 0000000..f35c612 Binary files /dev/null and b/docs/features/k8s_manifest_image_analysis.png differ diff --git a/src/app/lsp_server/command_generator.rs b/src/app/lsp_server/command_generator.rs index 1f0aa65..1d0c7e1 100644 --- a/src/app/lsp_server/command_generator.rs +++ b/src/app/lsp_server/command_generator.rs @@ -2,7 +2,7 @@ use serde_json::{Value, json}; use tower_lsp::lsp_types::{CodeLens, Command, Location, Range, Url}; use crate::app::lsp_server::supported_commands::SupportedCommands; -use crate::infra::{parse_compose_file, parse_dockerfile}; +use crate::infra::{parse_compose_file, parse_dockerfile, parse_k8s_manifest}; pub struct CommandInfo { pub title: String, @@ -64,6 +64,8 @@ pub fn generate_commands_for_uri(uri: &Url, content: &str) -> Result Result bool { + // Must be a YAML file + if !(file_uri.ends_with(".yaml") || file_uri.ends_with(".yml")) { + return false; + } + + // Check for K8s manifest indicators in content + // K8s manifests typically have "apiVersion" and "kind" fields + content.contains("apiVersion:") && content.contains("kind:") +} + +fn generate_k8s_manifest_commands(url: &Url, content: &str) -> Result, String> { + let mut commands = vec![]; + match parse_k8s_manifest(content) { + Ok(instructions) => { + for instruction in instructions { + commands.push( + SupportedCommands::ExecuteBaseImageScan { + location: Location::new(url.clone(), instruction.range), + image: instruction.image_name, + } + .into(), + ); + } + } + Err(err) => return Err(format!("{}", err)), + } + + Ok(commands) +} + fn generate_dockerfile_commands(uri: &Url, content: &str) -> Vec { let mut commands = vec![]; let instructions = parse_dockerfile(content); diff --git a/src/app/lsp_server/commands/scan_base_image.rs b/src/app/lsp_server/commands/scan_base_image.rs index bfc914e..8d2e6e8 100644 --- a/src/app/lsp_server/commands/scan_base_image.rs +++ b/src/app/lsp_server/commands/scan_base_image.rs @@ -80,20 +80,29 @@ where .vulnerabilities() .iter() .counts_by(|v| v.severity()); + let critical_count = vulns.get(&Severity::Critical).unwrap_or(&0_usize); + let high_count = vulns.get(&Severity::High).unwrap_or(&0_usize); + let medium_count = vulns.get(&Severity::Medium).unwrap_or(&0_usize); + let low_count = vulns.get(&Severity::Low).unwrap_or(&0_usize); + let negligible_count = vulns.get(&Severity::Negligible).unwrap_or(&0_usize); + diagnostic.message = format!( "Vulnerabilities found for {}: {} Critical, {} High, {} Medium, {} Low, {} Negligible", image_name, - vulns.get(&Severity::Critical).unwrap_or(&0_usize), - vulns.get(&Severity::High).unwrap_or(&0_usize), - vulns.get(&Severity::Medium).unwrap_or(&0_usize), - vulns.get(&Severity::Low).unwrap_or(&0_usize), - vulns.get(&Severity::Negligible).unwrap_or(&0_usize), + critical_count, + high_count, + medium_count, + low_count, + negligible_count, ); - diagnostic.severity = Some(if scan_result.evaluation_result().is_passed() { - DiagnosticSeverity::INFORMATION - } else { + // Determine severity based on vulnerability counts, not just policy evaluation + diagnostic.severity = Some(if *critical_count > 0 || *high_count > 0 { DiagnosticSeverity::ERROR + } else if *medium_count > 0 { + DiagnosticSeverity::WARNING + } else { + DiagnosticSeverity::INFORMATION }); } diff --git a/src/infra/k8s_manifest_ast_parser.rs b/src/infra/k8s_manifest_ast_parser.rs new file mode 100644 index 0000000..f4b9624 --- /dev/null +++ b/src/infra/k8s_manifest_ast_parser.rs @@ -0,0 +1,421 @@ +use thiserror::Error; +use tower_lsp::lsp_types::{Position, Range}; + +#[derive(Debug, PartialEq)] +pub struct ImageInstruction { + pub image_name: String, + pub range: Range, +} + +#[derive(Debug, Error)] +pub enum ParseError { + #[error("Invalid yaml: {0}")] + InvalidYaml(marked_yaml::LoadError), +} + +pub fn parse_k8s_manifest(content: &str) -> Result, ParseError> { + let mut instructions = Vec::new(); + + let node = marked_yaml::parse_yaml(0, content).map_err(ParseError::InvalidYaml)?; + find_images_recursive(&node, &mut instructions, content); + + Ok(instructions) +} + +fn find_images_recursive( + node: &marked_yaml::Node, + instructions: &mut Vec, + content: &str, +) { + match node { + marked_yaml::Node::Mapping(map) => { + // Check if this is a containers or initContainers array + for (key, value) in map.iter() { + let key_str = key.as_str(); + if key_str == "containers" || key_str == "initContainers" { + find_container_images(value, instructions, content); + } else if key_str == "image" { + if let Some(instruction) = try_create_image_instruction(value, content) { + instructions.push(instruction); + } + } else { + find_images_recursive(value, instructions, content); + } + } + } + marked_yaml::Node::Sequence(seq) => { + for item in seq.iter() { + find_images_recursive(item, instructions, content); + } + } + _ => {} + } +} + +fn find_container_images( + node: &marked_yaml::Node, + instructions: &mut Vec, + content: &str, +) { + let marked_yaml::Node::Sequence(containers) = node else { + return; + }; + + for container in containers.iter() { + let marked_yaml::Node::Mapping(container_map) = container else { + continue; + }; + + if let Some(image_node) = container_map.get("image") + && let Some(instruction) = try_create_image_instruction(image_node, content) + { + instructions.push(instruction); + } + } +} + +fn try_create_image_instruction( + node: &marked_yaml::Node, + content: &str, +) -> Option { + let marked_yaml::Node::Scalar(scalar) = node else { + return None; + }; + + let image_name = scalar.as_str().trim().to_string(); + if !is_valid_image_name(&image_name) { + return None; + } + + let start = node.span().start()?; + + let range = calculate_range(start, &image_name, content); + Some(ImageInstruction { image_name, range }) +} + +fn is_valid_image_name(name: &str) -> bool { + !name.is_empty() && name != "null" +} + +fn calculate_range(start: &marked_yaml::Marker, image_name: &str, content: &str) -> Range { + let start_line = start.line() as u32 - 1; + let start_char = start.column() as u32 - 1; + + let start_line_content = content.lines().nth(start_line as usize).unwrap_or(""); + let first_char = start_line_content.chars().nth(start_char as usize); + + let mut raw_len = image_name.len(); + if let Some(c) = first_char + && (c == '"' || c == '\'') + { + raw_len += 2; + } + + let end_char = start_char + raw_len as u32; + + Range { + start: Position { + line: start_line, + character: start_char, + }, + end: Position { + line: start_line, + character: end_char, + }, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tower_lsp::lsp_types::Position; + + #[test] + fn test_parse_simple_pod() { + let content = r#" +apiVersion: v1 +kind: Pod +metadata: + name: test-pod +spec: + containers: + - name: nginx + image: nginx:latest +"#; + let result = parse_k8s_manifest(content).unwrap(); + assert_eq!(result.len(), 1); + assert_eq!( + result[0], + ImageInstruction { + image_name: "nginx:latest".to_string(), + range: Range { + start: Position { + line: 8, + character: 11 + }, + end: Position { + line: 8, + character: 23 + }, + }, + } + ); + } + + #[test] + fn test_parse_deployment_with_multiple_containers() { + let content = r#" +apiVersion: apps/v1 +kind: Deployment +metadata: + name: web-deployment +spec: + template: + spec: + containers: + - name: web + image: nginx:1.19 + - name: sidecar + image: busybox:latest +"#; + let result = parse_k8s_manifest(content).unwrap(); + assert_eq!(result.len(), 2); + assert_eq!( + result[0], + ImageInstruction { + image_name: "nginx:1.19".to_string(), + range: Range { + start: Position { + line: 10, + character: 15 + }, + end: Position { + line: 10, + character: 25 + }, + }, + } + ); + assert_eq!( + result[1], + ImageInstruction { + image_name: "busybox:latest".to_string(), + range: Range { + start: Position { + line: 12, + character: 15 + }, + end: Position { + line: 12, + character: 29 + }, + }, + } + ); + } + + #[test] + fn test_parse_with_init_containers() { + let content = r#" +apiVersion: v1 +kind: Pod +metadata: + name: myapp-pod +spec: + initContainers: + - name: init-myservice + image: busybox:1.28 + containers: + - name: myapp-container + image: nginx:1.19 +"#; + let result = parse_k8s_manifest(content).unwrap(); + assert_eq!(result.len(), 2); + assert_eq!(result[0].image_name, "busybox:1.28"); + assert_eq!(result[1].image_name, "nginx:1.19"); + } + + #[test] + fn test_parse_statefulset() { + let content = r#" +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: web +spec: + template: + spec: + containers: + - name: nginx + image: nginx:stable +"#; + let result = parse_k8s_manifest(content).unwrap(); + assert_eq!(result.len(), 1); + assert_eq!(result[0].image_name, "nginx:stable"); + } + + #[test] + fn test_parse_cronjob() { + let content = r#" +apiVersion: batch/v1 +kind: CronJob +metadata: + name: hello +spec: + schedule: "* * * * *" + jobTemplate: + spec: + template: + spec: + containers: + - name: hello + image: busybox:1.28 +"#; + let result = parse_k8s_manifest(content).unwrap(); + assert_eq!(result.len(), 1); + assert_eq!(result[0].image_name, "busybox:1.28"); + } + + #[test] + fn test_parse_with_quoted_values() { + let content = r#" +apiVersion: v1 +kind: Pod +spec: + containers: + - name: web + image: "nginx:latest" + - name: db + image: 'postgres:13' +"#; + let result = parse_k8s_manifest(content).unwrap(); + assert_eq!(result.len(), 2); + assert_eq!( + result[0], + ImageInstruction { + image_name: "nginx:latest".to_string(), + range: Range { + start: Position { + line: 6, + character: 11 + }, + end: Position { + line: 6, + character: 25 + }, + }, + } + ); + assert_eq!( + result[1], + ImageInstruction { + image_name: "postgres:13".to_string(), + range: Range { + start: Position { + line: 8, + character: 11 + }, + end: Position { + line: 8, + character: 24 + }, + }, + } + ); + } + + #[test] + fn test_parse_with_complex_image_name() { + let content = r#" +apiVersion: v1 +kind: Pod +spec: + containers: + - name: app + image: private-registry.company.com:5000/project/team/service-image:1.2.3-beta +"#; + let result = parse_k8s_manifest(content).unwrap(); + assert_eq!(result.len(), 1); + assert_eq!( + result[0].image_name, + "private-registry.company.com:5000/project/team/service-image:1.2.3-beta" + ); + } + + #[test] + fn test_parse_empty_file() { + let content = ""; + let result = parse_k8s_manifest(content).unwrap(); + assert!(result.is_empty()); + } + + #[test] + fn test_parse_invalid_yaml() { + let content = r#" +apiVersion: v1 +kind: Pod +spec: + containers + - name: app + image: nginx +"#; + let result = parse_k8s_manifest(content); + assert!(result.is_err()); + } + + #[test] + fn test_parse_with_null_or_empty_image_values() { + let content = r#" +apiVersion: v1 +kind: Pod +spec: + containers: + - name: app1 + image: + - name: app2 + image: "" + - name: app3 + image: null +"#; + let result = parse_k8s_manifest(content).unwrap(); + assert!(result.is_empty()); + } + + #[test] + fn test_parse_daemonset() { + let content = r#" +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: fluentd +spec: + template: + spec: + containers: + - name: fluentd + image: fluentd:v1.0 +"#; + let result = parse_k8s_manifest(content).unwrap(); + assert_eq!(result.len(), 1); + assert_eq!(result[0].image_name, "fluentd:v1.0"); + } + + #[test] + fn test_parse_job() { + let content = r#" +apiVersion: batch/v1 +kind: Job +metadata: + name: pi +spec: + template: + spec: + containers: + - name: pi + image: perl:5.34 +"#; + let result = parse_k8s_manifest(content).unwrap(); + assert_eq!(result.len(), 1); + assert_eq!(result[0].image_name, "perl:5.34"); + } +} diff --git a/src/infra/mod.rs b/src/infra/mod.rs index f3e32bd..c350f32 100644 --- a/src/infra/mod.rs +++ b/src/infra/mod.rs @@ -2,6 +2,7 @@ mod component_factory_impl; mod compose_ast_parser; mod docker_image_builder; mod dockerfile_ast_parser; +mod k8s_manifest_ast_parser; mod scanner_binary_manager; mod sysdig_image_scanner; mod sysdig_image_scanner_json_scan_result_v1; @@ -12,3 +13,4 @@ pub use component_factory_impl::ConcreteComponentFactory; pub use compose_ast_parser::parse_compose_file; pub use docker_image_builder::DockerImageBuilder; pub use dockerfile_ast_parser::parse_dockerfile; +pub use k8s_manifest_ast_parser::parse_k8s_manifest; diff --git a/tests/fixtures/k8s-deployment.yaml b/tests/fixtures/k8s-deployment.yaml new file mode 100644 index 0000000..01fb52a --- /dev/null +++ b/tests/fixtures/k8s-deployment.yaml @@ -0,0 +1,11 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment +spec: + replicas: 3 + template: + spec: + containers: + - name: nginx + image: nginx:1.19 diff --git a/tests/general.rs b/tests/general.rs index a5fd65b..2569e08 100644 --- a/tests/general.rs +++ b/tests/general.rs @@ -339,7 +339,7 @@ async fn test_execute_command( diagnostic.message, "Vulnerabilities found for alpine: 0 Critical, 1 High, 0 Medium, 0 Low, 0 Negligible" ); - assert_eq!(diagnostic.severity, Some(DiagnosticSeverity::INFORMATION)); + assert_eq!(diagnostic.severity, Some(DiagnosticSeverity::ERROR)); assert_eq!( diagnostic.range, Range::new(Position::new(0, 0), Position::new(0, 11)) @@ -431,3 +431,63 @@ async fn test_shutdown(#[future] initialized_server: TestSetup) { let result = initialized_server.server.shutdown().await; assert!(result.is_ok()); } + +#[rstest] +#[awt] +#[tokio::test] +async fn test_k8s_manifest_code_lens(#[future] initialized_server: TestSetup) { + let k8s_url: Url = "file:///deployment.yaml".parse().unwrap(); + let k8s_content = include_str!("fixtures/k8s-deployment.yaml"); + + initialized_server + .server + .did_open(DidOpenTextDocumentParams { + text_document: TextDocumentItem::new( + k8s_url.clone(), + "yaml".to_string(), + 1, + k8s_content.to_string(), + ), + }) + .await; + + let params = tower_lsp::lsp_types::CodeLensParams { + text_document: TextDocumentIdentifier::new(k8s_url.clone()), + work_done_progress_params: WorkDoneProgressParams::default(), + partial_result_params: PartialResultParams::default(), + }; + + let result = initialized_server + .server + .code_lens(params) + .await + .unwrap() + .unwrap(); + + let result_json = serde_json::to_value(result).unwrap(); + + let expected_json = serde_json::json!([ + { + "command": { + "arguments": [ + { + "range": { + "end": { "character": 25, "line": 10 }, + "start": { "character": 15, "line": 10 } + }, + "uri": "file:///deployment.yaml" + }, + "nginx:1.19" + ], + "command": "sysdig-lsp.execute-scan", + "title": "Scan base image" + }, + "range": { + "end": { "character": 25, "line": 10 }, + "start": { "character": 15, "line": 10 } + } + } + ]); + + assert_eq!(result_json, expected_json); +}