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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Releases: every artifact (per-platform tarballs and `checksums.txt`) is now signed with [cosign](https://github.com/sigstore/cosign) keyless signing backed by GitHub's OIDC token. Each artifact has a sibling `<artifact>.sigstore.json` bundle containing the signature, certificate (with the workflow identity baked in), and Rekor transparency-log inclusion proof. Verifying a download is a single `cosign verify-blob --bundle ...` invocation; see the new [Verifying releases](README.md#verifying-releases) section in the README for the exact commands. Requires cosign v2.0+ on the verifier side.
- `NOTICE` file at repo root carrying the `Copyright 2026 Leonardo Di Donato` attribution. Apache 2.0 distinguishes the license text (canonical, verbatim, in `LICENSE`) from project-level attribution (in a `NOTICE` file that downstream consumers must propagate). The previous setup folded the copyright line into `LICENSE` itself; that conflated the two and is one of the deviations that caused licensecheck to mis-classify the file (see corresponding `### Fixed` entry).
- README License section: "Why Apache 2.0" paragraph. Documents the kernel-uABI posture (no kernel source, no cgo, no GPL deps; `/proc` and `/sys` reads fall under the kernel `COPYING` "normal syscalls" carve-out) and the Apache-2.0-over-MIT rationale (patent grant for security-adjacent probing; same-license adopter base of Cilium, Tetragon, Falco, etc.).
- `ReadIMARuntimeMeasurementsCount()`: exported helper that reads `/sys/kernel/security/ima/runtime_measurements_count` and returns the current count. No side effects. Useful for diagnostics and for callers building their own before/after measurement probes.

### Breaking

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ Neither tells you whether your tool can **actually run**. For example, BPF LSM r
| **BPF LSM enabled** (config + boot params + program type) | ✗ | ✗ | ✓ |
| **IMA detection** (LSM list + securityfs directory) | ✗ | ✗ | ✓ |
| **IMA any measurement active** (runtime measurement activity) | ✗ | ✗ | ✓ |
| **IMA runtime measurement count** (raw count for before/after probes) | ✗ | ✗ | ✓ |
| **Process capabilities** (CAP_BPF, CAP_SYS_ADMIN, CAP_PERFMON) | ✗ | ✗ | ✓ |
| **Unprivileged BPF status** | ✗ | ✓ | ✓ |
| **Mount-state gates** (bpffs/tracefs/custom paths via superblock magic) | ✗ | ✗ | ✓ |
Expand Down
18 changes: 16 additions & 2 deletions probe.go
Original file line number Diff line number Diff line change
Expand Up @@ -441,9 +441,23 @@ func probeIMAAnyMeasurementActive() ProbeResult {
return ProbeResult{Supported: false}
}

// readMeasurementCount reads the IMA runtime measurement count.
// ReadIMARuntimeMeasurementsCount returns the current IMA runtime measurement
// count from /sys/kernel/security/ima/runtime_measurements_count. No side
// effects. Useful for diagnostics and for callers building before/after probes.
func ReadIMARuntimeMeasurementsCount() (int, error) {
return readMeasurementCount()
}

// readMeasurementCount reads the IMA runtime measurement count from the
// default path.
func readMeasurementCount() (int, error) {
data, err := os.ReadFile(imaMeasurementCountPath)
return readMeasurementCountFrom(imaMeasurementCountPath)
}

// readMeasurementCountFrom reads an IMA runtime measurement count from the
// given path. Separated from readMeasurementCount for testability.
func readMeasurementCountFrom(path string) (int, error) {
data, err := os.ReadFile(path)
if err != nil {
return 0, err
}
Expand Down
83 changes: 83 additions & 0 deletions probe_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -360,3 +360,86 @@ func TestProbeKprobeMulti_WithConfig(t *testing.T) {
}
})
}

func TestReadMeasurementCountFrom(t *testing.T) {
t.Run("valid count", func(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "runtime_measurements_count")
if err := os.WriteFile(path, []byte("42\n"), 0644); err != nil {
t.Fatal(err)
}

count, err := readMeasurementCountFrom(path)
if err != nil {
t.Fatalf("readMeasurementCountFrom() error = %v", err)
}
if count != 42 {
t.Errorf("count = %d, want 42", count)
}
})

t.Run("zero count", func(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "runtime_measurements_count")
if err := os.WriteFile(path, []byte("0\n"), 0644); err != nil {
t.Fatal(err)
}

count, err := readMeasurementCountFrom(path)
if err != nil {
t.Fatalf("readMeasurementCountFrom() error = %v", err)
}
if count != 0 {
t.Errorf("count = %d, want 0", count)
}
})

t.Run("whitespace around number", func(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "runtime_measurements_count")
if err := os.WriteFile(path, []byte(" 7 \n"), 0644); err != nil {
t.Fatal(err)
}

count, err := readMeasurementCountFrom(path)
if err != nil {
t.Fatalf("readMeasurementCountFrom() error = %v", err)
}
if count != 7 {
t.Errorf("count = %d, want 7", count)
}
})

t.Run("malformed content", func(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "runtime_measurements_count")
if err := os.WriteFile(path, []byte("not-a-number\n"), 0644); err != nil {
t.Fatal(err)
}

_, err := readMeasurementCountFrom(path)
if err == nil {
t.Error("expected error for malformed content")
}
})

t.Run("empty content", func(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "runtime_measurements_count")
if err := os.WriteFile(path, []byte(""), 0644); err != nil {
t.Fatal(err)
}

_, err := readMeasurementCountFrom(path)
if err == nil {
t.Error("expected error for empty content")
}
})

t.Run("missing file", func(t *testing.T) {
_, err := readMeasurementCountFrom("/nonexistent/path")
if err == nil {
t.Error("expected error for missing file")
}
})
}
Loading