cmd/push.go (175 lines of code) (raw):
package cmd
import (
"errors"
"fmt"
"os"
"strings"
"github.com/Azure/obom/internal/print"
obom "github.com/Azure/obom/pkg"
"github.com/spf13/cobra"
"oras.land/oras-go/v2/registry"
"oras.land/oras-go/v2/registry/remote"
"oras.land/oras-go/v2/registry/remote/auth"
"oras.land/oras-go/v2/registry/remote/credentials"
"oras.land/oras-go/v2/registry/remote/retry"
)
type pushOpts struct {
filename string
reference string
username string
password string
disableStrict bool
pushSummary bool
ManifestAnnotations []string
attachArtifacts []string
}
var (
errAnnotationFormat = errors.New("missing key in `--annotation` flag")
errAnnotationDuplication = errors.New("duplicate annotation key")
errAttachArtifactFormat = errors.New("missing key in `--attach` flag")
APPLICATION_USERAGENT = "obom"
)
func pushCmd() *cobra.Command {
var opts pushOpts
var pushCmd = &cobra.Command{
Use: "push",
Short: "Push the SPDX SBOM to the registry",
Long: `Push the SDPX with the annotations to an OCI registry
Example - Push an SPDX SBOM to a registry
obom push -f spdx.json localhost:5000/spdx:latest
Example - Push an SPDX SBOM to a registry with annotations
obom push -f spdx.json localhost:5000/spdx:latest --annotation key1=value1 --annotation key2=value2
Example - Push an SPDX SBOM to a registry with strict SPDX parsing disabled
obom push -f spdx.json localhost:5000/spdx:latest --disable-strict
Example - Push an SPDX SBOM to a registry with annotations
obom push -f spdx.json localhost:5000/spdx:latest --annotation key1=value1 --annotation key2=value2
Example - Push an SPDX SBOM to a registry with annotations and credentials
obom push -f spdx.json localhost:5000/spdx:latest --annotation key1=value1 --annotation key2=value2 --username user --password pass
Example - Push an SPDX SBOM to a registry with attached artifacts where the key is the artifactType and the value is the path to the artifact
obom push -f spdx.json localhost:5000/spdx:latest --attach vnd.example.artifactType=/path/to/artifact --attach vnd.example.artifactType=/path/to/artifact2
`,
Run: func(cmd *cobra.Command, args []string) {
// get the reference as the first argument
opts.reference = args[0]
// validate if reference is valid
ref, err := registry.ParseReference(opts.reference)
if err != nil {
fmt.Println("Error parsing reference:", err)
os.Exit(1)
}
// parse the annotations from the flags
inputAnnotations, err := parseAnnotationFlags(opts.ManifestAnnotations)
if err != nil {
fmt.Println("Error parsing annotations:", err)
os.Exit(1)
}
// parse the attach artifacts from the flags
attachArtifacts, err := parseAttachArtifactFlags(opts.attachArtifacts)
if err != nil {
fmt.Println("Error parsing attach artifacts:", err)
os.Exit(1)
}
// set the strict mode to the opposite of the disableStrict flag
strict := !opts.disableStrict
sbom, desc, bytes, err := obom.LoadSBOMFromFile(opts.filename, strict)
if err != nil {
fmt.Println("Error loading SBOM:", err)
os.Exit(1)
}
print.PrintSBOMSummary(sbom, desc)
annotations, err := obom.GetAnnotations(sbom)
if err != nil {
fmt.Println("Error getting annotations:", err)
os.Exit(1)
}
// merge the input annotations with the annotations from the SBOM
for k, v := range inputAnnotations {
annotations[k] = v
}
// get the credentials resolver
resolver, err := getCredentialsResolver(ref.Registry, opts.username, opts.password)
if err != nil {
fmt.Println("Error getting credentials resolver:", err)
os.Exit(1)
}
repo, err := getRemoteRepoTarget(opts.reference, resolver)
if err != nil {
fmt.Println("Error getting remote repository:", err)
os.Exit(1)
}
fmt.Printf("Pushing SBOM to %s@%s...\n", opts.reference, desc.Digest)
subject, err := obom.PushSBOM(sbom.Document, desc, bytes, opts.reference, annotations, opts.pushSummary, attachArtifacts, repo)
if err != nil {
fmt.Println("Error pushing SBOM:", err)
os.Exit(1)
}
fmt.Printf("SBOM pushed to %s@%s\n", opts.reference, subject.Digest)
},
}
pushCmd.Flags().StringVarP(&opts.filename, "file", "f", "", "Path to the SPDX SBOM file")
pushCmd.MarkFlagRequired("file")
pushCmd.Flags().StringArrayVarP(&opts.ManifestAnnotations, "annotation", "a", nil, "manifest annotations")
pushCmd.Flags().StringVarP(&opts.username, "username", "u", "", "Username for the registry")
pushCmd.Flags().StringVarP(&opts.password, "password", "p", "", "Password for the registry")
pushCmd.Flags().BoolVarP(&opts.pushSummary, "pushSummary", "s", false, "Push summary blob to the registry")
pushCmd.Flags().BoolVarP(&opts.disableStrict, "disable-strict", "r", false, "Disable strict SPDX parsing as per the SPDX specification. When disabled, obom will fall back to a simple JSON parsing strategy")
pushCmd.Flags().StringArrayVarP(&opts.attachArtifacts, "attach", "t", nil, "Attach artifacts to the SBOM")
// Add positional argument called reference to pushCmd
pushCmd.Args = cobra.ExactArgs(1)
return pushCmd
}
func parseAnnotationFlags(flags []string) (map[string]string, error) {
manifestAnnotations := make(map[string]string)
for _, anno := range flags {
key, val, success := strings.Cut(anno, "=")
if !success {
return nil, fmt.Errorf("%w: %s", errAnnotationFormat, anno)
}
if _, ok := manifestAnnotations[key]; ok {
return nil, fmt.Errorf("%w: %v, ", errAnnotationDuplication, key)
}
manifestAnnotations[key] = val
}
return manifestAnnotations, nil
}
func parseAttachArtifactFlags(flags []string) (map[string][]string, error) {
attachArtifacts := make(map[string][]string)
for _, attach := range flags {
key, val, success := strings.Cut(attach, "=")
if !success {
return nil, fmt.Errorf("%w: %s", errAttachArtifactFormat, attach)
}
attachArtifacts[key] = append(attachArtifacts[key], val)
}
return attachArtifacts, nil
}
func getCredentialsResolver(registry string, username string, password string) (obom.CredentialsResolver, error) {
if len(username) != 0 && len(password) != 0 {
return auth.StaticCredential(registry, auth.Credential{
Username: username,
Password: password,
}), nil
} else {
storeOpts := credentials.StoreOptions{}
store, err := credentials.NewStoreFromDocker(storeOpts)
if err != nil {
return nil, err
}
return credentials.Credential(store), nil
}
}
func getRemoteRepoTarget(reference string, credsResolver obom.CredentialsResolver) (*remote.Repository, error) {
// Parse the reference
ref, err := registry.ParseReference(reference)
if err != nil {
return nil, fmt.Errorf("error parsing reference: %w", err)
}
// Construct the repository string without the tag/digest
// This allows us to reuse this repository target for pushing the SBOM and attaching artifacts
repoStr := fmt.Sprintf("%s/%s", ref.Registry, ref.Repository)
// Connect to a remote repository
repo, err := remote.NewRepository(repoStr)
if err != nil {
return nil, fmt.Errorf("error connecting to remote repository: %w", err)
}
// Check if registry has is localhost or starts with localhost:
reg := repo.Reference.Registry
if strings.HasPrefix(reg, "localhost:") {
repo.PlainHTTP = true
}
// Prepare the auth client for the registry
client := &auth.Client{
Client: retry.DefaultClient,
Cache: auth.DefaultCache,
}
client.Credential = credsResolver
client.SetUserAgent(APPLICATION_USERAGENT)
repo.Client = client
return repo, nil
}