added signed artifacts and SBOM generation capabilities
This commit is contained in:
@@ -0,0 +1,293 @@
|
||||
package sbom_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/forgeo/forgebucket/internal/domain/sbom"
|
||||
)
|
||||
|
||||
// ─── go.mod ──────────────────────────────────────────────────────────────────
|
||||
|
||||
func TestParseGoMod_Block(t *testing.T) {
|
||||
content := []byte(`module github.com/example/app
|
||||
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/go-chi/chi/v5 v5.2.5
|
||||
golang.org/x/crypto v0.50.0 // indirect
|
||||
)
|
||||
|
||||
require github.com/lib/pq v1.12.3
|
||||
`)
|
||||
comps := sbom.ParseGoMod(content)
|
||||
if len(comps) != 3 {
|
||||
t.Fatalf("expected 3 components, got %d", len(comps))
|
||||
}
|
||||
|
||||
byName := make(map[string]sbom.Component)
|
||||
for _, c := range comps {
|
||||
byName[c.Name] = c
|
||||
}
|
||||
|
||||
if c, ok := byName["github.com/go-chi/chi/v5"]; !ok {
|
||||
t.Error("missing github.com/go-chi/chi/v5")
|
||||
} else {
|
||||
if c.Version != "v5.2.5" {
|
||||
t.Errorf("wrong version: %s", c.Version)
|
||||
}
|
||||
if c.PURL != "pkg:golang/github.com/go-chi/chi/v5@v5.2.5" {
|
||||
t.Errorf("wrong PURL: %s", c.PURL)
|
||||
}
|
||||
}
|
||||
|
||||
if _, ok := byName["golang.org/x/crypto"]; !ok {
|
||||
t.Error("missing golang.org/x/crypto (indirect deps must be included)")
|
||||
}
|
||||
if _, ok := byName["github.com/lib/pq"]; !ok {
|
||||
t.Error("missing github.com/lib/pq (single-line require)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseGoMod_Empty(t *testing.T) {
|
||||
comps := sbom.ParseGoMod([]byte("module foo\n\ngo 1.21\n"))
|
||||
if len(comps) != 0 {
|
||||
t.Errorf("expected 0 components, got %d", len(comps))
|
||||
}
|
||||
}
|
||||
|
||||
// ─── package.json ─────────────────────────────────────────────────────────────
|
||||
|
||||
func TestParsePackageJSON(t *testing.T) {
|
||||
content := []byte(`{
|
||||
"name": "my-app",
|
||||
"dependencies": {
|
||||
"react": "^18.2.0",
|
||||
"axios": "1.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vitest": "~1.0.0"
|
||||
}
|
||||
}`)
|
||||
|
||||
comps := sbom.ParsePackageJSON(content)
|
||||
if len(comps) != 3 {
|
||||
t.Fatalf("expected 3 components, got %d", len(comps))
|
||||
}
|
||||
|
||||
byName := make(map[string]sbom.Component)
|
||||
for _, c := range comps {
|
||||
byName[c.Name] = c
|
||||
}
|
||||
|
||||
if c, ok := byName["react"]; !ok {
|
||||
t.Error("missing react")
|
||||
} else {
|
||||
if c.Version != "18.2.0" {
|
||||
t.Errorf("expected version stripped of ^, got %s", c.Version)
|
||||
}
|
||||
if c.PURL != "pkg:npm/react@18.2.0" {
|
||||
t.Errorf("wrong PURL: %s", c.PURL)
|
||||
}
|
||||
if c.Scope != "required" {
|
||||
t.Errorf("expected scope required, got %s", c.Scope)
|
||||
}
|
||||
}
|
||||
|
||||
if c, ok := byName["vitest"]; !ok {
|
||||
t.Error("missing vitest")
|
||||
} else if c.Scope != "optional" {
|
||||
t.Errorf("devDependency should be optional, got %s", c.Scope)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePackageJSON_Invalid(t *testing.T) {
|
||||
comps := sbom.ParsePackageJSON([]byte("not json"))
|
||||
if len(comps) != 0 {
|
||||
t.Errorf("expected 0 on invalid JSON, got %d", len(comps))
|
||||
}
|
||||
}
|
||||
|
||||
// ─── requirements.txt ────────────────────────────────────────────────────────
|
||||
|
||||
func TestParseRequirementsTxt(t *testing.T) {
|
||||
content := []byte(`# comment
|
||||
requests==2.31.0
|
||||
flask>=2.3.0
|
||||
numpy~=1.24.0
|
||||
boto3[s3]==1.28.0 # with extras
|
||||
no-version-package
|
||||
-r other-requirements.txt
|
||||
`)
|
||||
comps := sbom.ParseRequirementsTxt(content)
|
||||
|
||||
byName := make(map[string]sbom.Component)
|
||||
for _, c := range comps {
|
||||
byName[c.Name] = c
|
||||
}
|
||||
|
||||
if c, ok := byName["requests"]; !ok {
|
||||
t.Error("missing requests")
|
||||
} else if c.Version != "2.31.0" {
|
||||
t.Errorf("requests version: %s", c.Version)
|
||||
}
|
||||
|
||||
if c, ok := byName["flask"]; !ok {
|
||||
t.Error("missing flask")
|
||||
} else if c.Version != "2.3.0" {
|
||||
t.Errorf("flask version: %s", c.Version)
|
||||
}
|
||||
|
||||
if _, ok := byName["boto3"]; !ok {
|
||||
t.Error("missing boto3 (extras should be stripped from name)")
|
||||
}
|
||||
|
||||
if _, ok := byName["no-version-package"]; !ok {
|
||||
t.Error("missing no-version-package")
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Cargo.toml ──────────────────────────────────────────────────────────────
|
||||
|
||||
func TestParseCargoToml(t *testing.T) {
|
||||
content := []byte(`[package]
|
||||
name = "my-crate"
|
||||
version = "0.1.0"
|
||||
|
||||
[dependencies]
|
||||
serde = "1.0"
|
||||
tokio = { version = "1.28", features = ["full"] }
|
||||
clap = "4.3"
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = "0.5"
|
||||
`)
|
||||
comps := sbom.ParseCargoToml(content)
|
||||
if len(comps) != 4 {
|
||||
t.Fatalf("expected 4 components, got %d: %v", len(comps), comps)
|
||||
}
|
||||
|
||||
byName := make(map[string]sbom.Component)
|
||||
for _, c := range comps {
|
||||
byName[c.Name] = c
|
||||
}
|
||||
|
||||
if c, ok := byName["serde"]; !ok {
|
||||
t.Error("missing serde")
|
||||
} else if c.Version != "1.0" {
|
||||
t.Errorf("serde version: %s", c.Version)
|
||||
}
|
||||
|
||||
if c, ok := byName["tokio"]; !ok {
|
||||
t.Error("missing tokio")
|
||||
} else if c.Version != "1.28" {
|
||||
t.Errorf("tokio version: %s", c.Version)
|
||||
}
|
||||
|
||||
if _, ok := byName["criterion"]; !ok {
|
||||
t.Error("missing criterion (dev-dependency)")
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Gemfile.lock ─────────────────────────────────────────────────────────────
|
||||
|
||||
func TestParseGemfileLock(t *testing.T) {
|
||||
content := []byte(`GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
rails (7.1.0)
|
||||
actionpack (= 7.1.0)
|
||||
railties (= 7.1.0)
|
||||
actionpack (7.1.0)
|
||||
rake (13.0.6)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
rails (~> 7.1.0)
|
||||
`)
|
||||
comps := sbom.ParseGemfileLock(content)
|
||||
if len(comps) != 3 {
|
||||
t.Fatalf("expected 3 components, got %d: %v", len(comps), comps)
|
||||
}
|
||||
|
||||
byName := make(map[string]sbom.Component)
|
||||
for _, c := range comps {
|
||||
byName[c.Name] = c
|
||||
}
|
||||
|
||||
if c, ok := byName["rails"]; !ok {
|
||||
t.Error("missing rails")
|
||||
} else if c.Version != "7.1.0" {
|
||||
t.Errorf("rails version: %s", c.Version)
|
||||
}
|
||||
|
||||
if c, ok := byName["rake"]; !ok {
|
||||
t.Error("missing rake")
|
||||
} else if c.Version != "13.0.6" {
|
||||
t.Errorf("rake version: %s", c.Version)
|
||||
}
|
||||
}
|
||||
|
||||
// ─── pom.xml ─────────────────────────────────────────────────────────────────
|
||||
|
||||
func TestParsePomXML(t *testing.T) {
|
||||
content := []byte(`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
<version>3.1.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<version>42.6.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>`)
|
||||
|
||||
comps := sbom.ParsePomXML(content)
|
||||
if len(comps) != 2 {
|
||||
t.Fatalf("expected 2 components, got %d", len(comps))
|
||||
}
|
||||
|
||||
byName := make(map[string]sbom.Component)
|
||||
for _, c := range comps {
|
||||
byName[c.Name] = c
|
||||
}
|
||||
|
||||
if c, ok := byName["org.springframework.boot:spring-boot-starter-web"]; !ok {
|
||||
t.Error("missing spring-boot-starter-web")
|
||||
} else {
|
||||
if c.Version != "3.1.0" {
|
||||
t.Errorf("spring-boot version: %s", c.Version)
|
||||
}
|
||||
if c.PURL != "pkg:maven/org.springframework.boot/spring-boot-starter-web@3.1.0" {
|
||||
t.Errorf("wrong PURL: %s", c.PURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Document builder ─────────────────────────────────────────────────────────
|
||||
|
||||
func TestNewDocument(t *testing.T) {
|
||||
doc := sbom.NewDocument("my-repo", "abc1234567890")
|
||||
if doc.BOMFormat != "CycloneDX" {
|
||||
t.Errorf("BOMFormat: %s", doc.BOMFormat)
|
||||
}
|
||||
if doc.SpecVersion != "1.4" {
|
||||
t.Errorf("SpecVersion: %s", doc.SpecVersion)
|
||||
}
|
||||
if doc.Metadata.Component.Name != "my-repo" {
|
||||
t.Errorf("metadata component name: %s", doc.Metadata.Component.Name)
|
||||
}
|
||||
if len(doc.Metadata.Tools) == 0 {
|
||||
t.Error("expected at least one tool in metadata")
|
||||
}
|
||||
if doc.Components == nil {
|
||||
t.Error("expected non-nil Components slice")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user