Add go binary h1 digest to SPDX (#1265)

This commit is contained in:
Keith Zantow 2022-10-19 16:33:10 -04:00 committed by GitHub
parent 6e764815d0
commit 78a0af2e2d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 468 additions and 36 deletions

View file

@ -12,6 +12,7 @@ import (
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/formats/common/util"
"github.com/anchore/syft/syft/linux"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/sbom"
@ -293,6 +294,7 @@ func toSyftPackage(p *spdx.Package2_2) *pkg.Package {
return &sP
}
//nolint:funlen
func extractMetadata(p *spdx.Package2_2, info pkgInfo) (pkg.MetadataType, interface{}) {
arch := info.qualifierValue(pkg.PURLQualifierArch)
upstreamValue := info.qualifierValue(pkg.PURLQualifierUpstream)
@ -352,6 +354,20 @@ func extractMetadata(p *spdx.Package2_2, info pkgInfo) (pkg.MetadataType, interf
return pkg.JavaMetadataType, pkg.JavaMetadata{
ArchiveDigests: digests,
}
case pkg.GoModulePkg:
var h1Digest string
for _, value := range p.PackageChecksums {
digest, err := util.HDigestFromSHA(string(value.Algorithm), value.Value)
if err != nil {
log.Debugf("invalid h1digest: %v %v", value, err)
continue
}
h1Digest = digest
break
}
return pkg.GolangBinMetadataType, pkg.GolangBinMetadata{
H1Digest: h1Digest,
}
}
return pkg.UnknownMetadataType, nil
}

View file

@ -234,3 +234,84 @@ func TestExtractSourceFromNamespaces(t *testing.T) {
require.Equal(t, tt.expected, extractSchemeFromNamespace(tt.namespace))
}
}
func TestH1Digest(t *testing.T) {
tests := []struct {
name string
pkg spdx.Package2_2
expectedDigest string
}{
{
name: "valid h1digest",
pkg: spdx.Package2_2{
PackageName: "github.com/googleapis/gnostic",
PackageVersion: "v0.5.5",
PackageExternalReferences: []*spdx.PackageExternalReference2_2{
{
Category: "PACKAGE_MANAGER",
Locator: "pkg:golang/github.com/googleapis/gnostic@v0.5.5",
RefType: "purl",
},
},
PackageChecksums: map[spdx.ChecksumAlgorithm]spdx.Checksum{
spdx.SHA256: {
Algorithm: spdx.SHA256,
Value: "f5f1c0b4ad2e0dfa6f79eaaaa3586411925c16f61702208ddd4bad2fc17dc47c",
},
},
},
expectedDigest: "h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw=",
},
{
name: "invalid h1digest algorithm",
pkg: spdx.Package2_2{
PackageName: "github.com/googleapis/gnostic",
PackageVersion: "v0.5.5",
PackageExternalReferences: []*spdx.PackageExternalReference2_2{
{
Category: "PACKAGE_MANAGER",
Locator: "pkg:golang/github.com/googleapis/gnostic@v0.5.5",
RefType: "purl",
},
},
PackageChecksums: map[spdx.ChecksumAlgorithm]spdx.Checksum{
spdx.SHA256: {
Algorithm: spdx.SHA1,
Value: "f5f1c0b4ad2e0dfa6f79eaaaa3586411925c16f61702208ddd4bad2fc17dc47c",
},
},
},
expectedDigest: "",
},
{
name: "invalid h1digest digest",
pkg: spdx.Package2_2{
PackageName: "github.com/googleapis/gnostic",
PackageVersion: "v0.5.5",
PackageExternalReferences: []*spdx.PackageExternalReference2_2{
{
Category: "PACKAGE_MANAGER",
Locator: "pkg:golang/github.com/googleapis/gnostic@v0.5.5",
RefType: "purl",
},
},
PackageChecksums: map[spdx.ChecksumAlgorithm]spdx.Checksum{
spdx.SHA256: {
Algorithm: spdx.SHA256,
Value: "",
},
},
},
expectedDigest: "",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
p := toSyftPackage(&test.pkg)
require.Equal(t, pkg.GolangBinMetadataType, p.MetadataType)
meta := p.Metadata.(pkg.GolangBinMetadata)
require.Equal(t, test.expectedDigest, meta.H1Digest)
})
}
}

View file

@ -0,0 +1,57 @@
package util
import (
"encoding/base64"
"encoding/hex"
"fmt"
"strings"
)
// HDigestToSHA converts a h# digest, such as h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= to an
// algorithm such as sha256 and a hex encoded digest
func HDigestToSHA(digest string) (string, string, error) {
// hash is base64, but we need hex encode
parts := strings.Split(digest, ":")
if len(parts) == 2 {
algo := parts[0]
hash := parts[1]
checksum, err := base64.StdEncoding.DecodeString(hash)
if err != nil {
return "", "", err
}
hexStr := hex.EncodeToString(checksum)
switch algo {
// golang h1 hash == sha256
case "h1":
algo = "sha256"
default:
return "", "", fmt.Errorf("unsupported h#digest algorithm: %s", algo)
}
return algo, hexStr, nil
}
return "", "", fmt.Errorf("invalid h#digest: %s", digest)
}
// HDigestFromSHA converts an algorithm, such sha256 with a hex encoded digest to a
// h# value such as h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
func HDigestFromSHA(algorithm string, digest string) (string, error) {
if digest == "" {
return "", fmt.Errorf("no digest value provided")
}
// digest is hex, but we need to base64 encode
algorithm = strings.ToLower(algorithm)
if algorithm == "sha256" {
checksum, err := hex.DecodeString(digest)
if err != nil {
return "", err
}
// hash is hex, but we need base64
b64digest := base64.StdEncoding.EncodeToString(checksum)
return fmt.Sprintf("h1:%s", b64digest), nil
}
return "", fmt.Errorf("not a recognized h#digest algorithm: %s", algorithm)
}

View file

@ -0,0 +1,107 @@
package util
import (
"fmt"
"strings"
"testing"
"github.com/stretchr/testify/require"
)
func Test_HDigestToSHA(t *testing.T) {
tests := []struct {
name string
hDigest string
expected string
error bool
}{
{
name: "valid h1digest",
hDigest: "h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=",
expected: "sha256:f10a9c0e0ceb52a9546ff1b63d04d68ae786a33b91d7cf3881d74ccb093a88b5",
error: false,
},
{
name: "other valid h1digest",
hDigest: "h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=",
expected: "sha256:4933fc0ef0f273f748e5bf13e61b21b648d2f84e364e7cac34250f7637221a16",
error: false,
},
{
name: "invalid h1digest",
hDigest: "h12:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=",
expected: "",
error: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
algo, digest, err := HDigestToSHA(test.hDigest)
if test.error {
require.Error(t, err)
return
} else {
require.NoError(t, err)
}
got := fmt.Sprintf("%s:%s", algo, digest)
require.Equal(t, test.expected, got)
})
}
}
func Test_HDigestFromSHA(t *testing.T) {
tests := []struct {
name string
sha string
expected string
error bool
}{
{
name: "valid sha",
sha: "sha256:f10a9c0e0ceb52a9546ff1b63d04d68ae786a33b91d7cf3881d74ccb093a88b5",
expected: "h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=",
error: false,
},
{
name: "other valid sha",
sha: "sha256:4933fc0ef0f273f748e5bf13e61b21b648d2f84e364e7cac34250f7637221a16",
expected: "h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=",
error: false,
},
{
name: "invalid sha",
expected: "h12:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=",
sha: "sha256:f10a9c0e0zzzzceb52a99968ae786a33b91d7cf3881d74ccb093a88b5",
error: true,
},
{
name: "invalid algorithm",
expected: "h12:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=",
sha: "sha1:f10a9c0e0ceb52a9546ff1b63d04d68ae786a33b91d7cf3881d74ccb093a88b5",
error: true,
},
{
name: "empty sha",
expected: "",
sha: "sha256:",
error: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
parts := strings.Split(test.sha, ":")
algo := parts[0]
digest := parts[1]
got, err := HDigestFromSHA(algo, digest)
if test.error {
require.Error(t, err)
return
} else {
require.NoError(t, err)
}
require.Equal(t, test.expected, got)
})
}
}

View file

@ -12,6 +12,7 @@ import (
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/formats/common/spdxhelpers"
"github.com/anchore/syft/syft/formats/common/util"
"github.com/anchore/syft/syft/formats/spdx22json/model"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/sbom"
@ -53,24 +54,8 @@ func toPackages(catalog *pkg.Catalog, relationships []artifact.Relationship) []m
for _, p := range catalog.Sorted() {
license := spdxhelpers.License(p)
packageSpdxID := model.ElementID(p.ID()).String()
filesAnalyzed := false
checksums, filesAnalyzed := toPackageChecksums(p)
// we generate digest for some Java packages
// see page 33 of the spdx specification for 2.2
// spdx.github.io/spdx-spec/package-information/#710-package-checksum-field
var checksums []model.Checksum
if p.MetadataType == pkg.JavaMetadataType {
javaMetadata := p.Metadata.(pkg.JavaMetadata)
if len(javaMetadata.ArchiveDigests) > 0 {
filesAnalyzed = true
for _, digest := range javaMetadata.ArchiveDigests {
checksums = append(checksums, model.Checksum{
Algorithm: strings.ToUpper(digest.Algorithm),
ChecksumValue: digest.Value,
})
}
}
}
// note: the license concluded and declared should be the same since we are collecting license information
// from the project data itself (the installed package files).
packages = append(packages, model.Package{
@ -100,6 +85,38 @@ func toPackages(catalog *pkg.Catalog, relationships []artifact.Relationship) []m
return packages
}
func toPackageChecksums(p pkg.Package) ([]model.Checksum, bool) {
filesAnalyzed := false
var checksums []model.Checksum
switch meta := p.Metadata.(type) {
// we generate digest for some Java packages
// see page 33 of the spdx specification for 2.2
// spdx.github.io/spdx-spec/package-information/#710-package-checksum-field
case pkg.JavaMetadata:
if len(meta.ArchiveDigests) > 0 {
filesAnalyzed = true
for _, digest := range meta.ArchiveDigests {
checksums = append(checksums, model.Checksum{
Algorithm: strings.ToUpper(digest.Algorithm),
ChecksumValue: digest.Value,
})
}
}
case pkg.GolangBinMetadata:
algo, hexStr, err := util.HDigestToSHA(meta.H1Digest)
if err != nil {
log.Debugf("invalid h1digest: %s: %v", meta.H1Digest, err)
break
}
algo = strings.ToUpper(algo)
checksums = append(checksums, model.Checksum{
Algorithm: strings.ToUpper(algo),
ChecksumValue: hexStr,
})
}
return checksums, filesAnalyzed
}
func fileIDsForPackage(packageSpdxID string, relationships []artifact.Relationship) (fileIDs []string) {
for _, relationship := range relationships {
if relationship.Type != artifact.ContainsRelationship {

View file

@ -1,9 +1,11 @@
package spdx22json
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
@ -253,3 +255,64 @@ func Test_fileIDsForPackage(t *testing.T) {
})
}
}
func Test_H1Digest(t *testing.T) {
tests := []struct {
name string
pkg pkg.Package
expectedDigest string
}{
{
name: "valid h1digest",
pkg: pkg.Package{
Name: "github.com/googleapis/gnostic",
Version: "v0.5.5",
MetadataType: pkg.GolangBinMetadataType,
Metadata: pkg.GolangBinMetadata{
H1Digest: "h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw=",
},
},
expectedDigest: "SHA256:f5f1c0b4ad2e0dfa6f79eaaaa3586411925c16f61702208ddd4bad2fc17dc47c",
},
{
name: "invalid h1digest",
pkg: pkg.Package{
Name: "github.com/googleapis/gnostic",
Version: "v0.5.5",
MetadataType: pkg.GolangBinMetadataType,
Metadata: pkg.GolangBinMetadata{
H1Digest: "h1:9fHAtK0uzzz",
},
},
expectedDigest: "",
},
{
name: "unsupported h-digest",
pkg: pkg.Package{
Name: "github.com/googleapis/gnostic",
Version: "v0.5.5",
MetadataType: pkg.GolangBinMetadataType,
Metadata: pkg.GolangBinMetadata{
H1Digest: "h12:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw=",
},
},
expectedDigest: "",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
catalog := pkg.NewCatalog(test.pkg)
pkgs := toPackages(catalog, nil)
require.Len(t, pkgs, 1)
p := pkgs[0]
if test.expectedDigest == "" {
require.Len(t, p.Checksums, 0)
} else {
require.Len(t, p.Checksums, 1)
c := p.Checksums[0]
require.Equal(t, test.expectedDigest, fmt.Sprintf("%s:%s", c.Algorithm, c.ChecksumValue))
}
})
}
}

View file

@ -2,13 +2,16 @@ package spdx22tagvalue
import (
"fmt"
"strings"
"time"
"github.com/spdx/tools-golang/spdx"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/spdxlicense"
"github.com/anchore/syft/syft/formats/common/spdxhelpers"
"github.com/anchore/syft/syft/formats/common/util"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/sbom"
)
@ -101,24 +104,7 @@ func toFormatPackages(catalog *pkg.Catalog) map[spdx.ElementID]*spdx.Package2_2
// in the Comments on License field (section 3.16). With respect to NOASSERTION, a written explanation in
// the Comments on License field (section 3.16) is preferred.
license := spdxhelpers.License(p)
filesAnalyzed := false
checksums := make(map[spdx.ChecksumAlgorithm]spdx.Checksum)
// If the pkg type is Java we have attempted to generated a digest
// FilesAnalyzed should be true in this case
if p.MetadataType == pkg.JavaMetadataType {
javaMetadata := p.Metadata.(pkg.JavaMetadata)
if len(javaMetadata.ArchiveDigests) > 0 {
filesAnalyzed = true
for _, digest := range javaMetadata.ArchiveDigests {
checksums[spdx.ChecksumAlgorithm(digest.Algorithm)] = spdx.Checksum{
Algorithm: spdx.ChecksumAlgorithm(digest.Algorithm),
Value: digest.Value,
}
}
}
}
checksums, filesAnalyzed := toPackageChecksums(p)
results[spdx.ElementID(id)] = &spdx.Package2_2{
@ -181,7 +167,7 @@ func toFormatPackages(catalog *pkg.Catalog) map[spdx.ElementID]*spdx.Package2_2
IsFilesAnalyzedTagPresent: true,
// 3.9: Package Verification Code
// Cardinality: mandatory, one if filesAnalyzed is true / omitted;
// Cardinality: optional, one if filesAnalyzed is true / omitted;
// zero (must be omitted) if filesAnalyzed is false
PackageVerificationCode: "",
// Spec also allows specifying a single file to exclude from the
@ -280,6 +266,38 @@ func toFormatPackages(catalog *pkg.Catalog) map[spdx.ElementID]*spdx.Package2_2
return results
}
func toPackageChecksums(p pkg.Package) (map[spdx.ChecksumAlgorithm]spdx.Checksum, bool) {
filesAnalyzed := false
checksums := map[spdx.ChecksumAlgorithm]spdx.Checksum{}
switch meta := p.Metadata.(type) {
// we generate digest for some Java packages
// see page 33 of the spdx specification for 2.2
// spdx.github.io/spdx-spec/package-information/#710-package-checksum-field
case pkg.JavaMetadata:
if len(meta.ArchiveDigests) > 0 {
filesAnalyzed = true
for _, digest := range meta.ArchiveDigests {
checksums[spdx.ChecksumAlgorithm(digest.Algorithm)] = spdx.Checksum{
Algorithm: spdx.ChecksumAlgorithm(digest.Algorithm),
Value: digest.Value,
}
}
}
case pkg.GolangBinMetadata:
algo, hexStr, err := util.HDigestToSHA(meta.H1Digest)
if err != nil {
log.Debugf("invalid h1digest: %s: %v", meta.H1Digest, err)
break
}
algo = strings.ToUpper(algo)
checksums[spdx.ChecksumAlgorithm(algo)] = spdx.Checksum{
Algorithm: spdx.ChecksumAlgorithm(algo),
Value: hexStr,
}
}
return checksums, filesAnalyzed
}
func formatSPDXExternalRefs(p pkg.Package) (refs []*spdx.PackageExternalReference2_2) {
for _, ref := range spdxhelpers.ExternalRefs(p) {
refs = append(refs, &spdx.PackageExternalReference2_2{

View file

@ -0,0 +1,73 @@
package spdx22tagvalue
import (
"fmt"
"testing"
"github.com/stretchr/testify/require"
"github.com/anchore/syft/syft/pkg"
)
func Test_H1Digest(t *testing.T) {
tests := []struct {
name string
pkg pkg.Package
expectedDigest string
}{
{
name: "valid h1digest",
pkg: pkg.Package{
Name: "github.com/googleapis/gnostic",
Version: "v0.5.5",
MetadataType: pkg.GolangBinMetadataType,
Metadata: pkg.GolangBinMetadata{
H1Digest: "h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw=",
},
},
expectedDigest: "SHA256:f5f1c0b4ad2e0dfa6f79eaaaa3586411925c16f61702208ddd4bad2fc17dc47c",
},
{
name: "invalid h1digest",
pkg: pkg.Package{
Name: "github.com/googleapis/gnostic",
Version: "v0.5.5",
MetadataType: pkg.GolangBinMetadataType,
Metadata: pkg.GolangBinMetadata{
H1Digest: "h1:9fHAtK0uzzz",
},
},
expectedDigest: "",
},
{
name: "unsupported h-digest",
pkg: pkg.Package{
Name: "github.com/googleapis/gnostic",
Version: "v0.5.5",
MetadataType: pkg.GolangBinMetadataType,
Metadata: pkg.GolangBinMetadata{
H1Digest: "h12:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw=",
},
},
expectedDigest: "",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
catalog := pkg.NewCatalog(test.pkg)
pkgs := toFormatPackages(catalog)
require.Len(t, pkgs, 1)
for _, p := range pkgs {
if test.expectedDigest == "" {
require.Len(t, p.PackageChecksums, 0)
} else {
require.Len(t, p.PackageChecksums, 1)
for _, c := range p.PackageChecksums {
require.Equal(t, test.expectedDigest, fmt.Sprintf("%s:%s", c.Algorithm, c.Value))
}
}
}
})
}
}