mirror of
https://github.com/syntrex-lab/gomcp.git
synced 2026-05-05 01:02:37 +02:00
Release prep: 54 engines, self-hosted signatures, i18n, dashboard updates
This commit is contained in:
parent
694e32be26
commit
41cbfd6e0a
178 changed files with 36008 additions and 399 deletions
193
internal/infrastructure/sbom/sbom.go
Normal file
193
internal/infrastructure/sbom/sbom.go
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
// Package sbom implements SEC-010 SBOM + Release Signing.
|
||||
//
|
||||
// Generates SPDX Software Bill of Materials and provides
|
||||
// binary signing using Ed25519 (with Sigstore Cosign integration point).
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// gen := sbom.NewGenerator("SENTINEL AI SOC", "2.1.0")
|
||||
// gen.AddDependency("golang.org/x/crypto", "v0.21.0", "BSD-3-Clause")
|
||||
// spdx, _ := gen.GenerateSPDX()
|
||||
package sbom
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SPDXDocument is an SPDX 2.3 SBOM document.
|
||||
type SPDXDocument struct {
|
||||
SPDXVersion string `json:"spdxVersion"`
|
||||
DataLicense string `json:"dataLicense"`
|
||||
SPDXID string `json:"SPDXID"`
|
||||
DocumentName string `json:"name"`
|
||||
Namespace string `json:"documentNamespace"`
|
||||
CreationInfo CreationInfo `json:"creationInfo"`
|
||||
Packages []Package `json:"packages"`
|
||||
Relationships []Relationship `json:"relationships,omitempty"`
|
||||
}
|
||||
|
||||
// CreationInfo describes when and how the SBOM was created.
|
||||
type CreationInfo struct {
|
||||
Created string `json:"created"`
|
||||
Creators []string `json:"creators"`
|
||||
Comment string `json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
// Package is an SPDX package entry.
|
||||
type Package struct {
|
||||
SPDXID string `json:"SPDXID"`
|
||||
Name string `json:"name"`
|
||||
Version string `json:"versionInfo"`
|
||||
Supplier string `json:"supplier,omitempty"`
|
||||
License string `json:"licenseConcluded"`
|
||||
DownloadURL string `json:"downloadLocation"`
|
||||
Checksum string `json:"checksum,omitempty"` // SHA256:hex
|
||||
}
|
||||
|
||||
// Relationship links packages.
|
||||
type Relationship struct {
|
||||
Element string `json:"spdxElementId"`
|
||||
Type string `json:"relationshipType"`
|
||||
Related string `json:"relatedSpdxElement"`
|
||||
}
|
||||
|
||||
// ReleaseSignature is a signed release record.
|
||||
type ReleaseSignature struct {
|
||||
Binary string `json:"binary"`
|
||||
Version string `json:"version"`
|
||||
Hash string `json:"hash"` // SHA-256
|
||||
Signature string `json:"signature"` // Ed25519 hex
|
||||
KeyID string `json:"key_id"`
|
||||
SignedAt string `json:"signed_at"`
|
||||
}
|
||||
|
||||
// Generator produces SBOM documents.
|
||||
type Generator struct {
|
||||
productName string
|
||||
version string
|
||||
packages []Package
|
||||
}
|
||||
|
||||
// NewGenerator creates an SBOM generator.
|
||||
func NewGenerator(productName, version string) *Generator {
|
||||
return &Generator{
|
||||
productName: productName,
|
||||
version: version,
|
||||
}
|
||||
}
|
||||
|
||||
// AddDependency adds a dependency to the SBOM.
|
||||
func (g *Generator) AddDependency(name, version, license string) {
|
||||
g.packages = append(g.packages, Package{
|
||||
SPDXID: fmt.Sprintf("SPDXRef-%s", sanitizeID(name)),
|
||||
Name: name,
|
||||
Version: version,
|
||||
License: license,
|
||||
DownloadURL: fmt.Sprintf("https://pkg.go.dev/%s@%s", name, version),
|
||||
})
|
||||
}
|
||||
|
||||
// GenerateSPDX creates an SPDX 2.3 JSON document.
|
||||
func (g *Generator) GenerateSPDX() (*SPDXDocument, error) {
|
||||
doc := &SPDXDocument{
|
||||
SPDXVersion: "SPDX-2.3",
|
||||
DataLicense: "CC0-1.0",
|
||||
SPDXID: "SPDXRef-DOCUMENT",
|
||||
DocumentName: fmt.Sprintf("%s-%s", g.productName, g.version),
|
||||
Namespace: fmt.Sprintf("https://sentinel.xn--80akacl3adqr.xn--p1acf/spdx/%s/%s", g.productName, g.version),
|
||||
CreationInfo: CreationInfo{
|
||||
Created: time.Now().UTC().Format(time.RFC3339),
|
||||
Creators: []string{"Tool: sentinel-sbom-gen", "Organization: Syntrex"},
|
||||
},
|
||||
Packages: append([]Package{{
|
||||
SPDXID: "SPDXRef-Product",
|
||||
Name: g.productName,
|
||||
Version: g.version,
|
||||
License: "Proprietary",
|
||||
DownloadURL: "https://github.com/syntrex/gomcp",
|
||||
}}, g.packages...),
|
||||
}
|
||||
|
||||
// Add relationships.
|
||||
for _, pkg := range g.packages {
|
||||
doc.Relationships = append(doc.Relationships, Relationship{
|
||||
Element: "SPDXRef-Product",
|
||||
Type: "DEPENDS_ON",
|
||||
Related: pkg.SPDXID,
|
||||
})
|
||||
}
|
||||
|
||||
return doc, nil
|
||||
}
|
||||
|
||||
// ExportJSON serializes the SBOM to JSON.
|
||||
func ExportJSON(doc *SPDXDocument) ([]byte, error) {
|
||||
return json.MarshalIndent(doc, "", " ")
|
||||
}
|
||||
|
||||
// SignRelease signs a binary for release verification.
|
||||
func SignRelease(binaryPath, version string, privateKey ed25519.PrivateKey, keyID string) (*ReleaseSignature, error) {
|
||||
hash, err := hashFile(binaryPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sbom: hash %s: %w", binaryPath, err)
|
||||
}
|
||||
|
||||
hashBytes, _ := hex.DecodeString(hash)
|
||||
sig := ed25519.Sign(privateKey, hashBytes)
|
||||
|
||||
return &ReleaseSignature{
|
||||
Binary: binaryPath,
|
||||
Version: version,
|
||||
Hash: hash,
|
||||
Signature: hex.EncodeToString(sig),
|
||||
KeyID: keyID,
|
||||
SignedAt: time.Now().UTC().Format(time.RFC3339),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// VerifyRelease verifies a signed release.
|
||||
func VerifyRelease(sig *ReleaseSignature, publicKey ed25519.PublicKey) bool {
|
||||
hashBytes, err := hex.DecodeString(sig.Hash)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
sigBytes, err := hex.DecodeString(sig.Signature)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return ed25519.Verify(publicKey, hashBytes, sigBytes)
|
||||
}
|
||||
|
||||
// --- Helpers ---
|
||||
|
||||
func sanitizeID(name string) string {
|
||||
result := make([]byte, 0, len(name))
|
||||
for _, c := range name {
|
||||
if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' {
|
||||
result = append(result, byte(c))
|
||||
} else {
|
||||
result = append(result, '-')
|
||||
}
|
||||
}
|
||||
return string(result)
|
||||
}
|
||||
|
||||
func hashFile(path string) (string, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
h := sha256.New()
|
||||
if _, err := io.Copy(h, f); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(h.Sum(nil)), nil
|
||||
}
|
||||
83
internal/infrastructure/sbom/sbom_test.go
Normal file
83
internal/infrastructure/sbom/sbom_test.go
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
package sbom
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewGenerator(t *testing.T) {
|
||||
g := NewGenerator("SENTINEL", "2.1.0")
|
||||
if g.productName != "SENTINEL" {
|
||||
t.Errorf("product = %s", g.productName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateSPDX(t *testing.T) {
|
||||
g := NewGenerator("SENTINEL AI SOC", "2.1.0")
|
||||
g.AddDependency("golang.org/x/crypto", "v0.21.0", "BSD-3-Clause")
|
||||
g.AddDependency("gopkg.in/yaml.v3", "v3.0.1", "Apache-2.0")
|
||||
|
||||
doc, err := g.GenerateSPDX()
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateSPDX: %v", err)
|
||||
}
|
||||
|
||||
if doc.SPDXVersion != "SPDX-2.3" {
|
||||
t.Errorf("version = %s", doc.SPDXVersion)
|
||||
}
|
||||
// Product + 2 deps = 3 packages.
|
||||
if len(doc.Packages) != 3 {
|
||||
t.Errorf("packages = %d, want 3", len(doc.Packages))
|
||||
}
|
||||
if len(doc.Relationships) != 2 {
|
||||
t.Errorf("relationships = %d, want 2", len(doc.Relationships))
|
||||
}
|
||||
}
|
||||
|
||||
func TestExportJSON(t *testing.T) {
|
||||
g := NewGenerator("test", "1.0.0")
|
||||
g.AddDependency("dep1", "v1.0.0", "MIT")
|
||||
doc, _ := g.GenerateSPDX()
|
||||
|
||||
data, err := ExportJSON(doc)
|
||||
if err != nil {
|
||||
t.Fatalf("ExportJSON: %v", err)
|
||||
}
|
||||
|
||||
var parsed SPDXDocument
|
||||
if err := json.Unmarshal(data, &parsed); err != nil {
|
||||
t.Fatalf("parse JSON: %v", err)
|
||||
}
|
||||
if parsed.DocumentName != "test-1.0.0" {
|
||||
t.Errorf("name = %s", parsed.DocumentName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignAndVerifyRelease(t *testing.T) {
|
||||
pub, priv, _ := ed25519.GenerateKey(nil)
|
||||
|
||||
exe, _ := os.Executable()
|
||||
sig, err := SignRelease(exe, "2.1.0", priv, "release-key-1")
|
||||
if err != nil {
|
||||
t.Fatalf("SignRelease: %v", err)
|
||||
}
|
||||
|
||||
if sig.Version != "2.1.0" {
|
||||
t.Errorf("version = %s", sig.Version)
|
||||
}
|
||||
if sig.Hash == "" || sig.Signature == "" {
|
||||
t.Error("hash/signature empty")
|
||||
}
|
||||
|
||||
if !VerifyRelease(sig, pub) {
|
||||
t.Error("verification failed for valid signature")
|
||||
}
|
||||
|
||||
// Tamper with hash.
|
||||
sig.Hash = "0000000000000000000000000000000000000000000000000000000000000000"
|
||||
if VerifyRelease(sig, pub) {
|
||||
t.Error("verification should fail for tampered hash")
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue