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
27 changes: 12 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ ever-changing **vulnerability feed**, and understand **Alpine's APK versioning**
rules to tell whether an installed version is actually older than the one that
fixes a given CVE. Sentinel automates exactly that loop.

Point it at an image and it works as a **pipeline**: it pulls and unpacks the
image, reads the **Alpine package database** out of the layers to build a
lightweight **SBOM** (software bill of materials), detects the Alpine OS version,
Point it at an image and it works as a **pipeline**: it pulls and saves the
image, then **streams** the **Alpine package database** out of the layers to
build a lightweight **SBOM** (software bill of materials), detects the Alpine OS version,
downloads the matching **Alpine Security Database (SecDB)**, and compares every
installed package against the known fixes using an **APK-aware version
comparator**. The result is a clear report of vulnerable packages with their
Expand All @@ -27,7 +27,7 @@ pulls in **no third-party dependencies**.
- Scans **Docker images** from the command line (`--image`).
- Machine-readable **JSON output** for **CI pipelines** (`--format json`).
- **Policy-based build failure** thresholds (`--fail-on`).
- Extracts and analyzes **image layers** to build an **SBOM**.
- **Streams image layers** from the saved tar (no full extraction to disk) to build an **SBOM**.
- Detects the Alpine version from `alpine-release` (with safe fallback to image-tag parsing).
- Fetches and merges **Alpine SecDB** from both the `main` and `community` repositories.
- **APK-aware version comparison**, including pre/post-release suffix semantics (see below).
Expand All @@ -47,13 +47,12 @@ next, and the whole thing is orchestrated by `cmd/scanner/main.go`.
┌───────────────────┐
│ docker pull │ ensure the image exists locally
│ docker save │ export it to image.tar ── internal/extractor
│ unpack tarball │ untar into a temp dir (zip-slip guarded)
└─────────┬─────────┘
│ unpacked image (manifest.json + layer tarballs)
│ saved image.tar (manifest.json + layer blobs)
▼
┌───────────────────┐
│ read manifest │ ordered list of layers
│ walk each layer │ gzip/plain auto-detected ── internal/analyzer
│ stream outer tar │ find manifest.json (any order)
│ walk each layer │ gzip/plain, in-memory, cap-guarded ── internal/analyzer
│ parse apk DB │ lib/apk/db/installed → SBOM
└─────────┬─────────┘
│ SBOM: []{name, version}
Expand All @@ -77,14 +76,14 @@ next, and the whole thing is orchestrated by `cmd/scanner/main.go`.
Pipeline in one line:

```
docker pull → docker save → unpack → SBOM → SecDB → match → report
docker pull → docker save → stream layers → SBOM → SecDB → match → report
```

### Project structure

- `cmd/scanner/main.go` — **CLI entrypoint**, matching loop, scoring, and report rendering.
- `internal/extractor/` — pulls, saves, and unpacks **Docker images**.
- `internal/analyzer/` — reads the manifest, walks layers, and builds the **SBOM** from the Alpine package DB.
- `internal/extractor/` — pulls and saves **Docker images**, and provides the cap-guarded streaming tar walker.
- `internal/analyzer/` — reads the manifest and streams layers to build the **SBOM** from the Alpine package DB.
- `internal/matcher/` — fetches/merges **SecDB** and provides the **apk-aware version comparison**.

## Why a custom APK version comparator?
Expand Down Expand Up @@ -158,9 +157,8 @@ version, so nothing is flagged:
Target: alpine:3.14

[*] Phase 1: Extracting Image...
Pulling and saving image 'alpine:3.14' (this might take a moment)...
Pulling image 'alpine:3.14' from registry...
Unpacking image layers...
Saving image 'alpine:3.14' (using binary: /usr/local/bin/docker)...
[*] Phase 2: Analyzing Layers...
Found Alpine package database in layer: blobs/sha256/422ed46b1a92...
-> Generated SBOM with 14 installed packages.
Expand Down Expand Up @@ -189,9 +187,8 @@ reports a vulnerable `apk-tools` with its CVE, severity, and remediation:
Target: alpine:3.10

[*] Phase 1: Extracting Image...
Pulling and saving image 'alpine:3.10' (this might take a moment)...
Pulling image 'alpine:3.10' from registry...
Unpacking image layers...
Saving image 'alpine:3.10' (using binary: /usr/local/bin/docker)...
[*] Phase 2: Analyzing Layers...
Found Alpine package database in layer: blobs/sha256/26d14edc4f17...
-> Generated SBOM with 14 installed packages.
Expand Down
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Living document. Tick items off as they land; reorder when priorities change.

- [x] **Real APK version comparison parity.** Current tokenizer in `internal/matcher/version.go` misses APK suffix semantics (`_alpha`, `_beta`, `_rc`, `_pre`, `_p`, `_git`). E.g. `1.2.2_pre2` should rank lower than `1.2.2`. Port logic from `apk-tools` `apk_version.c` and add a golden-file test suite.
- [x] **Bounded tar extraction.** `internal/extractor/extractor.go` does unbounded `io.Copy` — malicious layer can fill disk (tar bomb). Wrap in `io.LimitReader` with configurable cap and reject symlinks/hardlinks pointing outside target.
- [ ] **Stream layers instead of extracting to disk.** Today we untar the whole image then walk layers again. Read layers directly from the saved tar stream — faster, lower disk footprint, removes temp dir need for most scans.
- [x] **Stream layers instead of extracting to disk.** Today we untar the whole image then walk layers again. Read layers directly from the saved tar stream — faster, lower disk footprint, removes temp dir need for most scans.
- [ ] **SecDB caching.** On-disk cache keyed by `(version, repo, ETag/Last-Modified)` under `$XDG_CACHE_HOME/sentinel`. Adds offline support and cuts repeated network calls in CI.
- [ ] **Context + cancellation.** Plumb `context.Context` through `extractor`, `analyzer`, `matcher`. A hung `docker pull` is currently unkillable.

Expand Down
8 changes: 2 additions & 6 deletions cmd/scanner/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ func main() {
fmt.Println("[*] Phase 1: Extracting Image...")
}

extractedPath, cleanup, err := extractor.ExtractImage(*imageFlag, textOutput)
imageTarPath, cleanup, err := extractor.SaveImage(*imageFlag, extractor.Options{Verbose: textOutput})
if err != nil {
log.Fatalf("Extraction failed: %v", err)
}
Expand All @@ -81,12 +81,8 @@ func main() {
if textOutput {
fmt.Println("[*] Phase 2: Analyzing Layers...")
}
layers, err := analyzer.GetImageLayers(extractedPath)
if err != nil {
log.Fatalf("Failed to read manifest: %v", err)
}

sbom, err := analyzer.BuildSBOM(extractedPath, layers, textOutput)
sbom, err := analyzer.BuildSBOM(imageTarPath, extractor.New(), analyzer.Options{Verbose: textOutput})
if err != nil {
log.Fatalf("Failed to build SBOM: %v", err)
}
Expand Down
Loading
Loading