Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
196 changes: 193 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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<Vec<ImageInstruction>, 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<Vec<CommandInfo>, 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/<feature>.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:

Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "sysdig-lsp"
version = "0.7.5"
version = "0.8.0"
edition = "2024"
authors = [ "Sysdig Inc." ]
readme = "README.md"
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions docs/features/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
43 changes: 43 additions & 0 deletions docs/features/k8s_manifest_image_analysis.md
Original file line number Diff line number Diff line change
@@ -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`.
Binary file added docs/features/k8s_manifest_image_analysis.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
35 changes: 34 additions & 1 deletion src/app/lsp_server/command_generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -64,6 +64,8 @@ pub fn generate_commands_for_uri(uri: &Url, content: &str) -> Result<Vec<Command
|| file_uri.contains("compose.yaml")
{
generate_compose_commands(uri, content)
} else if is_k8s_manifest_file(file_uri, content) {
generate_k8s_manifest_commands(uri, content)
} else {
Ok(generate_dockerfile_commands(uri, content))
}
Expand All @@ -89,6 +91,37 @@ fn generate_compose_commands(url: &Url, content: &str) -> Result<Vec<CommandInfo
Ok(commands)
}

fn is_k8s_manifest_file(file_uri: &str, content: &str) -> 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<Vec<CommandInfo>, 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<CommandInfo> {
let mut commands = vec![];
let instructions = parse_dockerfile(content);
Expand Down
25 changes: 17 additions & 8 deletions src/app/lsp_server/commands/scan_base_image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
});
}

Expand Down
Loading