Skip to content
Open
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
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Flags:
-v, --version string Version for the SBOM document (default "0.0.1")
```

By default, `sbomit` parses `material`, `command-run`, and `product` attestations. To restrict parsing on demand:
By default, `sbomit` parses `material`, `command-run`, `product`, and `network-trace` attestations. To restrict parsing on demand:

```bash
sbomit generate attestation.json --types command-run
Expand All @@ -55,9 +55,10 @@ sbomit generate attestation.json --catalog syft --project-dir /path/to/project
### Attestation Extractors

Modular extractors for different attestation types:
- `MaterialExtractor` - Build Input materials
- `MaterialExtractor` - Build input materials
- `CommandRunExtractor` - Opened files from processes
- `ProductExtractor` - Built artifacts
- `NetworkTrace` - External download connections

Implement `Extractor` interface to add new types.

Expand All @@ -74,8 +75,9 @@ Each resolver implements `Resolver` and optionally `PackageFileFilterer` to filt
### Processing Pipeline

```
Attestation → Extract Files → Filter Cache Files →
Run Resolvers → Filter Package Files → Generate SBOM
Attestation → Extract Files & Network Conns → Run Resolvers →
Filter Package Files → Resolve Network PURLs → Merge with Catalog (Syft) →
Generate SBOM Document
```

## Testing
Expand Down
168 changes: 168 additions & 0 deletions pkg/resolver/rust_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
package resolver

import (
"testing"
)

func TestRustResolver_Resolve(t *testing.T) {
tests := []struct {
name string
files []FileInfo
wantPackages []PackageInfo
wantRemaining int
}{
{
name: "Valid Cargo registry path",
files: []FileInfo{
{Path: "/home/user/.cargo/registry/src/github.com-1ecc6299db9ec823/serde-1.0.130/src/lib.rs"},
},
wantPackages: []PackageInfo{
{
Name: "serde",
Version: "1.0.130",
Ecosystem: "cargo",
PURL: "pkg:cargo/serde@1.0.130",
},
},
wantRemaining: 1,
},
{
name: "Cargo cache crate file",
files: []FileInfo{
{Path: "/home/user/.cargo/registry/cache/github.com-1ecc6299db9ec823/tokio-1.28.2.crate"},
},
wantPackages: []PackageInfo{
{
Name: "tokio",
Version: "1.28.2",
Ecosystem: "cargo",
PURL: "pkg:cargo/tokio@1.28.2",
},
},
wantRemaining: 1,
},
{
name: "Multiple crates and ignored files",
files: []FileInfo{
{Path: "/home/user/.cargo/registry/src/index.crates.io-6f17d22bba15001f/regex-1.8.4/src/lib.rs"},
{Path: "/home/user/.cargo/registry/src/index.crates.io-6f17d22bba15001f/regex-1.8.4/target/debug/libregex.rlib"},
{Path: "/home/user/.cargo/registry/src/index.crates.io-6f17d22bba15001f/libc-0.2.147/src/lib.rs"},
},
wantPackages: []PackageInfo{
{Name: "regex", Version: "1.8.4", Ecosystem: "cargo", PURL: "pkg:cargo/regex@1.8.4"},
{Name: "libc", Version: "0.2.147", Ecosystem: "cargo", PURL: "pkg:cargo/libc@0.2.147"},
},
wantRemaining: 2,
},
{
name: "Path with version-like string in directory name but not crate",
files: []FileInfo{
{Path: "/home/user/not-a-registry/some-package-1.2.3/src/main.rs"},
},
wantPackages: nil,
wantRemaining: 1,
},
{
name: "Malformed version string",
files: []FileInfo{
{Path: "/usr/local/cargo/registry/src/unrecognized/invalid-pkg/src/lib.rs"},
},
wantPackages: nil,
wantRemaining: 1,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := NewRustResolver()
gotPackages, gotRemaining := r.Resolve(tt.files)

if len(gotPackages) != len(tt.wantPackages) {
t.Errorf("Resolve() gotPackages length = %v, want %v", len(gotPackages), len(tt.wantPackages))
return
}

for _, wp := range tt.wantPackages {
found := false
for _, gp := range gotPackages {
if gp.Name == wp.Name && gp.Version == wp.Version && gp.PURL == wp.PURL {
found = true
break
}
}
if !found {
t.Errorf("Resolve() did not find expected package %+v", wp)
}
}

if len(gotRemaining) != tt.wantRemaining {
t.Errorf("Resolve() gotRemaining length = %v, want %v", len(gotRemaining), tt.wantRemaining)
}
})
}
}

func TestRustPackageFilter_Matches(t *testing.T) {
tests := []struct {
name string
packageName string
version string
path string
want bool
}{
{
name: "Match registry src path",
packageName: "serde",
version: "1.0.130",
path: "/home/user/.cargo/registry/src/github.com-1ecc6299db9ec823/serde-1.0.130/src/lib.rs",
want: true,
},
{
name: "Match registry cache crate",
packageName: "tokio",
version: "1.28.2",
path: "/home/user/.cargo/registry/cache/github.com-1ecc6299db9ec823/tokio-1.28.2.crate",
want: true,
},
{
name: "Match from crates directory",
packageName: "anyhow",
version: "1.0.71",
path: "/usr/local/cargo/crates/anyhow-1.0.71/src/lib.rs",
want: true,
},
{
name: "Mismatch version",
packageName: "serde",
version: "1.0.130",
path: "/home/user/.cargo/registry/src/github.com-1ecc6299db9ec823/serde-1.0.131/src/lib.rs",
want: false,
},
{
name: "Mismatch package name",
packageName: "serde",
version: "1.0.130",
path: "/home/user/.cargo/registry/src/github.com-1ecc6299db9ec823/serde_json-1.0.130/src/lib.rs",
want: false,
},
{
name: "Non-registry path",
packageName: "serde",
version: "1.0.130",
path: "/home/user/not-a-registry/serde-1.0.130/src/lib.rs",
want: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := &rustPackageFilter{
packageName: tt.packageName,
version: tt.version,
}
if got := f.Matches(tt.path); got != tt.want {
t.Errorf("Matches() = %v, want %v", got, tt.want)
}
})
}
}