Skip to content

Commit 6c5dd01

Browse files
Phase 3 Implementation
1 parent 17e8686 commit 6c5dd01

8 files changed

Lines changed: 2962 additions & 10 deletions

File tree

cmd/compliance/sbom.go

Lines changed: 433 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
package compliance
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
8+
"github.com/go-nv/goenv/internal/sbom"
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
func TestSBOMSigning_KeyGeneration(t *testing.T) {
14+
tmpDir := t.TempDir()
15+
16+
privateKeyPath := filepath.Join(tmpDir, "private.pem")
17+
publicKeyPath := filepath.Join(tmpDir, "public.pem")
18+
19+
// Test key generation
20+
err := sbom.GenerateKeyPair(privateKeyPath, publicKeyPath)
21+
require.NoError(t, err, "Key generation should succeed")
22+
23+
// Verify files exist
24+
assert.FileExists(t, privateKeyPath, "Private key should be created")
25+
assert.FileExists(t, publicKeyPath, "Public key should be created")
26+
27+
// Verify files are not empty
28+
privInfo, err := os.Stat(privateKeyPath)
29+
require.NoError(t, err)
30+
assert.Greater(t, privInfo.Size(), int64(0), "Private key should not be empty")
31+
32+
pubInfo, err := os.Stat(publicKeyPath)
33+
require.NoError(t, err)
34+
assert.Greater(t, pubInfo.Size(), int64(0), "Public key should not be empty")
35+
}
36+
37+
func TestSBOMSigning_SignAndVerify(t *testing.T) {
38+
if testing.Short() {
39+
t.Skip("Skipping signing test in short mode")
40+
}
41+
42+
tmpDir := t.TempDir()
43+
44+
// Create test SBOM
45+
sbomPath := filepath.Join(tmpDir, "test.sbom.json")
46+
testSBOM := []byte(`{
47+
"bomFormat": "CycloneDX",
48+
"specVersion": "1.5",
49+
"version": 1,
50+
"metadata": {
51+
"component": {
52+
"name": "test-component",
53+
"version": "1.0.0"
54+
}
55+
}
56+
}`)
57+
err := os.WriteFile(sbomPath, testSBOM, 0644)
58+
require.NoError(t, err)
59+
60+
// Generate keys
61+
privateKeyPath := filepath.Join(tmpDir, "private.pem")
62+
publicKeyPath := filepath.Join(tmpDir, "public.pem")
63+
err = sbom.GenerateKeyPair(privateKeyPath, publicKeyPath)
64+
require.NoError(t, err)
65+
66+
// Sign SBOM
67+
signaturePath := filepath.Join(tmpDir, "test.sbom.json.sig")
68+
69+
signer := sbom.NewSigner(sbom.SignatureOptions{
70+
KeyPath: privateKeyPath,
71+
})
72+
sig, err := signer.SignSBOM(sbomPath)
73+
require.NoError(t, err, "Signing should succeed")
74+
75+
err = signer.WriteSignature(sig, signaturePath)
76+
require.NoError(t, err)
77+
assert.FileExists(t, signaturePath, "Signature file should be created")
78+
79+
// Verify signature
80+
verifier := sbom.NewVerifier(sbom.VerificationOptions{
81+
SBOMPath: sbomPath,
82+
SignaturePath: signaturePath,
83+
PublicKeyPath: publicKeyPath,
84+
})
85+
86+
verified, err := verifier.VerifySignature()
87+
require.NoError(t, err, "Verification should succeed")
88+
assert.True(t, verified, "Signature should be valid")
89+
}
90+
91+
func TestSBOMSigning_InvalidSignature(t *testing.T) {
92+
if testing.Short() {
93+
t.Skip("Skipping signing test in short mode")
94+
}
95+
96+
tmpDir := t.TempDir()
97+
98+
// Create test SBOM
99+
sbomPath := filepath.Join(tmpDir, "test.sbom.json")
100+
testSBOM := []byte(`{"bomFormat": "CycloneDX", "specVersion": "1.5"}`)
101+
err := os.WriteFile(sbomPath, testSBOM, 0644)
102+
require.NoError(t, err)
103+
104+
// Create tampered signature
105+
signaturePath := filepath.Join(tmpDir, "test.sbom.json.sig")
106+
tamperedSig := []byte(`{"signature": "aW52YWxpZA==", "algorithm": "ECDSA-SHA256", "timestamp": "2024-01-01T00:00:00Z"}`)
107+
err = os.WriteFile(signaturePath, tamperedSig, 0644)
108+
require.NoError(t, err)
109+
110+
// Generate keys (for public key)
111+
privateKeyPath := filepath.Join(tmpDir, "private.pem")
112+
publicKeyPath := filepath.Join(tmpDir, "public.pem")
113+
err = sbom.GenerateKeyPair(privateKeyPath, publicKeyPath)
114+
require.NoError(t, err)
115+
116+
// Try to verify - should fail
117+
verifier := sbom.NewVerifier(sbom.VerificationOptions{
118+
SBOMPath: sbomPath,
119+
SignaturePath: signaturePath,
120+
PublicKeyPath: publicKeyPath,
121+
})
122+
123+
verified, err := verifier.VerifySignature()
124+
// Either the verification should fail with an error or return false
125+
if err == nil {
126+
assert.False(t, verified, "Verification should fail for tampered signature")
127+
}
128+
}
129+
130+
func TestSBOMAttestation_Generation(t *testing.T) {
131+
if testing.Short() {
132+
t.Skip("Skipping attestation test in short mode")
133+
}
134+
135+
tmpDir := t.TempDir()
136+
137+
// Create minimal go.mod for testing
138+
goModPath := filepath.Join(tmpDir, "go.mod")
139+
goModContent := []byte(`module example.com/test
140+
141+
go 1.23
142+
143+
require (
144+
github.com/example/dep v1.0.0
145+
)
146+
`)
147+
err := os.WriteFile(goModPath, goModContent, 0644)
148+
require.NoError(t, err)
149+
150+
// Create go.sum
151+
goSumPath := filepath.Join(tmpDir, "go.sum")
152+
goSumContent := []byte(`github.com/example/dep v1.0.0 h1:abc123
153+
github.com/example/dep v1.0.0/go.mod h1:xyz789
154+
`)
155+
err = os.WriteFile(goSumPath, goSumContent, 0644)
156+
require.NoError(t, err)
157+
158+
// Generate attestation
159+
attestPath := filepath.Join(tmpDir, "provenance.json")
160+
161+
// Change to tmpDir for go.mod detection
162+
oldWd, _ := os.Getwd()
163+
defer os.Chdir(oldWd)
164+
err = os.Chdir(tmpDir)
165+
require.NoError(t, err)
166+
167+
// Create a dummy SBOM file for the attestation
168+
sbomPath := filepath.Join(tmpDir, "test.sbom.json")
169+
err = os.WriteFile(sbomPath, []byte(`{"bomFormat":"CycloneDX"}`), 0644)
170+
require.NoError(t, err)
171+
172+
generator := sbom.NewProvenanceGenerator(sbom.ProvenanceOptions{
173+
SBOMPath: sbomPath,
174+
ProjectDir: tmpDir,
175+
GoVersion: "1.23",
176+
})
177+
provenance, err := generator.Generate()
178+
require.NoError(t, err, "Attestation generation should succeed")
179+
180+
err = generator.WriteProvenance(provenance, attestPath)
181+
require.NoError(t, err)
182+
assert.FileExists(t, attestPath, "Attestation file should be created")
183+
184+
// Verify it's valid JSON with expected fields
185+
data, err := os.ReadFile(attestPath)
186+
require.NoError(t, err)
187+
// SLSA v1.0 uses predicateType, not slsaVersion
188+
assert.Contains(t, string(data), "predicateType", "Should contain predicate type")
189+
assert.Contains(t, string(data), "buildDefinition", "Should contain build definition")
190+
assert.Contains(t, string(data), "runDetails", "Should contain run details")
191+
}
192+
193+
func TestSBOMAttestation_WithInToto(t *testing.T) {
194+
if testing.Short() {
195+
t.Skip("Skipping attestation test in short mode")
196+
}
197+
198+
tmpDir := t.TempDir()
199+
200+
// Create minimal go.mod
201+
goModPath := filepath.Join(tmpDir, "go.mod")
202+
goModContent := []byte(`module example.com/test
203+
204+
go 1.23
205+
`)
206+
err := os.WriteFile(goModPath, goModContent, 0644)
207+
require.NoError(t, err)
208+
209+
// Create go.sum
210+
goSumPath := filepath.Join(tmpDir, "go.sum")
211+
err = os.WriteFile(goSumPath, []byte(""), 0644)
212+
require.NoError(t, err)
213+
214+
// Change to tmpDir
215+
oldWd, _ := os.Getwd()
216+
defer os.Chdir(oldWd)
217+
err = os.Chdir(tmpDir)
218+
require.NoError(t, err)
219+
220+
// Create a dummy SBOM file
221+
sbomPath := filepath.Join(tmpDir, "test.sbom.json")
222+
err = os.WriteFile(sbomPath, []byte(`{"bomFormat":"CycloneDX"}`), 0644)
223+
require.NoError(t, err)
224+
225+
// Generate provenance
226+
generator := sbom.NewProvenanceGenerator(sbom.ProvenanceOptions{
227+
SBOMPath: sbomPath,
228+
ProjectDir: tmpDir,
229+
GoVersion: "1.23",
230+
})
231+
provenance, err := generator.Generate()
232+
require.NoError(t, err)
233+
234+
// Create in-toto attestation
235+
attestation, err := sbom.CreateInTotoAttestation(provenance, nil)
236+
require.NoError(t, err, "In-toto attestation creation should succeed")
237+
238+
// Write to file
239+
attestPath := filepath.Join(tmpDir, "attestation.json")
240+
err = sbom.WriteInTotoAttestation(attestation, attestPath)
241+
require.NoError(t, err)
242+
assert.FileExists(t, attestPath, "In-toto attestation file should be created")
243+
244+
// Verify contents - in-toto format
245+
data, err := os.ReadFile(attestPath)
246+
require.NoError(t, err)
247+
assert.Contains(t, string(data), "payloadType", "Should contain payloadType field")
248+
assert.Contains(t, string(data), "application/vnd.in-toto+json", "Should use in-toto payload type")
249+
assert.Contains(t, string(data), "payload", "Should contain base64-encoded payload")
250+
assert.Contains(t, string(data), "signatures", "Should contain signatures array")
251+
}
252+
253+
func TestSBOMSigning_CosignAvailability(t *testing.T) {
254+
// This is a simple check - not a full integration test
255+
available := sbom.IsCosignAvailable()
256+
257+
// Just verify the function runs without panic
258+
t.Logf("Cosign available: %v", available)
259+
260+
// If cosign is not available, that's okay - this is a feature check
261+
if !available {
262+
t.Log("Cosign not available - keyless signing will not work")
263+
}
264+
}

docs/roadmap/SBOM_STRATEGY.md

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -515,9 +515,10 @@ func useCryptography() { ... } // Only compiled on Linux with CGO
515515

516516
---
517517

518-
### Phase 2: Policy Validation (v3.2)
518+
### Phase 2: Policy Validation (v3.2) ✅ COMPLETED
519519

520520
**Timeline:** Q2 2026 (3 months)
521+
**Status:****COMPLETED** - Policy validation is implemented and functional
521522
**Priority:** ⚡ MEDIUM
522523

523524
**Why third:** Teams need enforcement, not just intelligence.
@@ -554,15 +555,19 @@ rules:
554555
555556
**Success Criteria:**
556557
557-
- 30%+ of users enable policy validation
558-
- Integration with 3+ CI platforms
559-
- 10+ organizations share policies
558+
- ✅ `goenv sbom validate` command implemented
559+
- ✅ YAML-based policy engine functional
560+
- ✅ License, supply-chain, and security rule types supported
561+
- 🎯 30%+ of users enable policy validation (in progress)
562+
- 🎯 Integration with 3+ CI platforms (in progress)
563+
- 🎯 10+ organizations share policies (in progress)
560564

561565
---
562566

563-
### Phase 3: Signing & Attestation (v3.3)
567+
### Phase 3: Signing & Attestation (v3.3) ✅ COMPLETE
564568

565569
**Timeline:** Q3 2026 (3 months)
570+
**Status:** ✅ **COMPLETE** - Core implementation finished, ready for adoption
566571
**Priority:** ⚡ MEDIUM
567572

568573
**Why fourth:** Supply chain security + SLSA compliance.
@@ -578,9 +583,16 @@ rules:
578583

579584
**Success Criteria:**
580585

581-
- SLSA Level 3 capability
582-
- 10+ organizations use signing
583-
- Featured in supply chain security guides
586+
- ✅ `goenv sbom sign` command implemented
587+
- ✅ `goenv sbom verify-signature` command implemented
588+
- ✅ `goenv sbom attest` command for SLSA provenance
589+
- ✅ Key-based signing (ECDSA P-256) working
590+
- ✅ Keyless signing via Sigstore/cosign integrated
591+
- ✅ SLSA v1.0 provenance generation
592+
- ✅ In-toto attestation support
593+
- ✅ Integration tests for signing/verification workflows
594+
- 🎯 10+ organizations use signing (adoption phase)
595+
- 🎯 Featured in supply chain security guides (pending)
584596

585597
---
586598

@@ -665,13 +677,20 @@ As more organizations adopt:
665677
- **Reproducibility:** 95%+ builds produce identical hashes
666678
- **Validation:** 5+ security teams provide feedback
667679

668-
### Phase 2-3 (Policy + Signing)
680+
### Phase 2 (Policy Validation) ✅ COMPLETED
669681

670682
- **Policy adoption:** 30%+ enable validation
671683
- **Enterprise usage:** 10+ organizations in production
672-
- **SLSA compliance:** Featured in SLSA implementation guides
673684
- **Integrations:** 3+ CI platforms have official examples
674685

686+
### Phase 3 (Signing & Attestation) 🚧 IN PROGRESS
687+
688+
- ✅ **Core signing:** Key-based and keyless signing implemented
689+
- ✅ **Signature verification:** Verification with keys and cosign working
690+
- 🚧 **SLSA provenance:** Attestation generation in progress
691+
- 🎯 **Signing adoption:** 10+ organizations use signing
692+
- 🎯 **SLSA compliance:** Featured in SLSA implementation guides
693+
675694
### Phase 4-6 (Integration Features)
676695

677696
- **Scanner integration:** 20%+ use vuln scanning

0 commit comments

Comments
 (0)