From 4c1f70edefeb9dc015931a41100006108d2fdd10 Mon Sep 17 00:00:00 2001 From: Vivek Kumar Sahu Date: Thu, 5 Sep 2024 13:23:24 +0530 Subject: [PATCH 01/11] add OWASP SCVS support Signed-off-by: Vivek Kumar Sahu --- pkg/licenses/license.go | 23 ++- pkg/sbom/cdx.go | 43 ++++++ pkg/sbom/document.go | 1 + pkg/sbom/signature.go | 53 +++++++ pkg/sbom/spdx.go | 11 ++ pkg/scvs/features.go | 282 ++++++++++++++++++++++++++++++++++ pkg/scvs/scvs.go | 320 +++++++++++++++++++++++++++++++++++++++ pkg/scvs/scvsCriteria.go | 44 ++++++ pkg/scvs/scvsScore.go | 73 +++++++++ pkg/scvs/scvsScorer.go | 53 +++++++ pkg/scvs/scvsScores.go | 53 +++++++ 11 files changed, 954 insertions(+), 2 deletions(-) create mode 100644 pkg/sbom/signature.go create mode 100644 pkg/scvs/features.go create mode 100644 pkg/scvs/scvs.go create mode 100644 pkg/scvs/scvsCriteria.go create mode 100644 pkg/scvs/scvsScore.go create mode 100644 pkg/scvs/scvsScorer.go create mode 100644 pkg/scvs/scvsScores.go diff --git a/pkg/licenses/license.go b/pkg/licenses/license.go index 3505513..d9cdbc6 100644 --- a/pkg/licenses/license.go +++ b/pkg/licenses/license.go @@ -96,6 +96,25 @@ func (m meta) AboutCode() bool { return m.source == "aboutcode" } +func IsValidLicenseID(licenseID string) bool { + if licenseID == "" { + return false + } + + lowerKey := strings.ToLower(licenseID) + + if lowerKey == "none" || lowerKey == "noassertion" { + return false + } + + tLicKey := strings.TrimRight(licenseID, "+") + + _, lok := licenseList[tLicKey] + _, aok := licenseListAboutCode[tLicKey] + + return lok || aok +} + func LookupSpdxLicense(licenseKey string) (License, error) { if licenseKey == "" { return nil, errors.New("license not found") @@ -183,14 +202,14 @@ func LookupExpression(expression string, customLicenses []License) []License { continue } - //if custom license list is provided use that. + // if custom license list is provided use that. license, err = customLookup(trimLicenseKey) if err == nil { licenses = append(licenses, license) continue } - //if nothing else this license is custom + // if nothing else this license is custom licenses = append(licenses, CreateCustomLicense(trimLicenseKey, trimLicenseKey)) } diff --git a/pkg/sbom/cdx.go b/pkg/sbom/cdx.go index 135f4e6..2c508af 100644 --- a/pkg/sbom/cdx.go +++ b/pkg/sbom/cdx.go @@ -52,6 +52,7 @@ type CdxDoc struct { PrimaryComponent PrimaryComp Dependencies map[string][]string composition map[string]string + signature []GetSignature } func newCDXDoc(ctx context.Context, f io.ReadSeeker, format FileFormat) (Document, error) { @@ -139,6 +140,10 @@ func (c CdxDoc) GetComposition(componentID string) string { return c.composition[componentID] } +func (c CdxDoc) Signature() []GetSignature { + return c.signature +} + func (c *CdxDoc) parse() { c.parseDoc() c.parseSpec() @@ -149,6 +154,7 @@ func (c *CdxDoc) parse() { c.parseCompositions() c.parsePrimaryCompAndRelationships() c.parseComps() + c.parseSignature() } func (c *CdxDoc) addToLogs(log string) { @@ -207,6 +213,43 @@ func (c *CdxDoc) parseSpec() { c.CdxSpec = sp } +func (c *CdxDoc) parseSignature() { + if c.doc.Declarations == nil { + fmt.Println("Declaratic.doc.Declarationsons field is nil ") + return + } + + if c.doc.Declarations.Signature != nil { + fmt.Println("c.doc.Declarations.Signature field is nil ") + return + } + + s := signature{} + s.keyID = c.doc.Declarations.Signature.KeyID + s.algorithm = c.doc.Declarations.Signature.Algorithm + s.value = c.doc.Declarations.Affirmation.Signature.Value + s.publicKey = c.doc.Declarations.Affirmation.Signature.PublicKey.CRV + c.signature = append(c.signature, s) + + for _, ss := range lo.FromPtr(c.doc.Declarations.Affirmation.Signature.Signers) { + sig := signature{} + sig.keyID = ss.KeyID + sig.algorithm = ss.Algorithm + sig.value = ss.Value + sig.publicKey = ss.PublicKey.CRV + c.signature = append(c.signature, sig) + } + + for _, sc := range lo.FromPtr(c.doc.Declarations.Affirmation.Signature.Chain) { + sig := signature{} + sig.keyID = sc.KeyID + sig.algorithm = sc.Algorithm + sig.value = sc.Value + sig.publicKey = sc.PublicKey.CRV + c.signature = append(c.signature, sig) + } +} + func (c *CdxDoc) requiredFields() bool { if c.doc == nil { c.addToLogs("cdx doc is not parsable") diff --git a/pkg/sbom/document.go b/pkg/sbom/document.go index 50b9458..39db9d8 100644 --- a/pkg/sbom/document.go +++ b/pkg/sbom/document.go @@ -31,4 +31,5 @@ type Document interface { PrimaryComp() GetPrimaryComp GetRelationships(string) []string + Signature() []GetSignature } diff --git a/pkg/sbom/signature.go b/pkg/sbom/signature.go new file mode 100644 index 0000000..6cabb20 --- /dev/null +++ b/pkg/sbom/signature.go @@ -0,0 +1,53 @@ +// Copyright 2023 Interlynk.io +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sbom + +type GetSignature interface { + CheckSignatureExists() bool + Key() string + Value() string + PublicKey() string + Algorithm() string +} + +type signature struct { + keyID string + algorithm string + value string + publicKey string + certificatePath string + certificate string + timestamp string +} + +func (s signature) CheckSignatureExists() bool { + return s.keyID != "" && s.algorithm != "" && s.value != "" && (s.publicKey != "" || s.certificate != "") +} + +func (s signature) Key() string { + return s.keyID +} + +func (s signature) Value() string { + return s.value +} + +func (s signature) PublicKey() string { + return s.publicKey +} + +func (s signature) Algorithm() string { + return s.algorithm +} diff --git a/pkg/sbom/spdx.go b/pkg/sbom/spdx.go index 3770531..2e78bc2 100644 --- a/pkg/sbom/spdx.go +++ b/pkg/sbom/spdx.go @@ -56,6 +56,7 @@ type SpdxDoc struct { lifecycles string Dependencies map[string][]string composition map[string]string + signature []GetSignature } func newSPDXDoc(ctx context.Context, f io.ReadSeeker, format FileFormat) (Document, error) { @@ -145,12 +146,22 @@ func (s SpdxDoc) GetComposition(componentID string) string { return s.composition[componentID] } +func (s SpdxDoc) Signature() []GetSignature { + return s.signature +} + func (s *SpdxDoc) parse() { s.parseSpec() s.parseAuthors() s.parseTool() s.parsePrimaryCompAndRelationships() s.parseComps() + s.parseSignature() +} + +func (s *SpdxDoc) parseSignature() { + s.signature = nil + return } func (s *SpdxDoc) parseSpec() { diff --git a/pkg/scvs/features.go b/pkg/scvs/features.go new file mode 100644 index 0000000..5cc858f --- /dev/null +++ b/pkg/scvs/features.go @@ -0,0 +1,282 @@ +// Copyright 2023 Interlynk.io +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package scvs + +import ( + "github.com/interlynk-io/sbomqs/pkg/sbom" +) + +const ( + green = "\033[32m" + red = "\033[31m" + reset = "\033[0m" + bold = "\033[1m" +) + +// Level: 123 +func scvsSBOMMachineReadableCheck(d sbom.Document, c *scvsCheck) scvsScore { + s := newScoreFromScvsCheck(c) + + if IsSBOMMachineReadable(d, s) { + s.setL3Score(green + bold + "✓" + reset) + s.setL2Score(green + bold + "✓" + reset) + s.setL1Score(green + bold + "✓" + reset) + } else { + s.setL3Score(red + bold + "✗" + reset) + s.setL2Score(red + bold + "✗" + reset) + s.setL1Score(red + bold + "✗" + reset) + } + return *s +} + +// Level: 23 +func scvsSBOMAutomationCreationCheck(d sbom.Document, c *scvsCheck) scvsScore { + s := newScoreFromScvsCheck(c) + + if IsSBOMCreationAutomated(d, s) { + s.setL3Score(green + bold + "✓" + reset) + s.setL2Score(green + bold + "✓" + reset) + } else { + s.setL3Score(red + bold + "✗" + reset) + s.setL2Score(red + bold + "✗" + reset) + } + return *s +} + +// Level: 123 +func scvsSBOMUniqIDCheck(d sbom.Document, c *scvsCheck) scvsScore { + s := newScoreFromScvsCheck(c) + + if IsSBOMHasUniqID(d, s) { + s.setL3Score(green + bold + "✓" + reset) + s.setL2Score(green + bold + "✓" + reset) + s.setL1Score(green + bold + "✓" + reset) + } else { + s.setL3Score(red + bold + "✗" + reset) + s.setL2Score(red + bold + "✗" + reset) + s.setL1Score(red + bold + "✗" + reset) + } + return *s +} + +// Level: 23 +func scvsSBOMSigcheck(d sbom.Document, c *scvsCheck) scvsScore { + s := newScoreFromScvsCheck(c) + + if IsSBOMHasSignature(d, s) { + s.setL3Score(green + bold + "✓" + reset) + s.setL2Score(green + bold + "✓" + reset) + } else { + s.setL3Score(red + bold + "✗" + reset) + s.setL2Score(red + bold + "✗" + reset) + } + return *s +} + +// Level: 23 +func scvsSBOMSigCorrectnessCheck(d sbom.Document, c *scvsCheck) scvsScore { + s := newScoreFromScvsCheck(c) + + if IsSBOMSignatureCorrect(d, s) { + s.setL3Score(green + bold + "✓" + reset) + s.setL2Score(green + bold + "✓" + reset) + } else { + s.setL3Score(red + bold + "✗" + reset) + s.setL2Score(red + bold + "✗" + reset) + } + return *s +} + +// Level: 3 +func scvsSBOMSigVerified(d sbom.Document, c *scvsCheck) scvsScore { + s := newScoreFromScvsCheck(c) + + if IsSBOMSignatureVerified(d, s) { + s.setL3Score(green + bold + "✓" + reset) + } else { + s.setL3Score(red + bold + "✗" + reset) + } + return *s +} + +// Level: 123 +func scvsSBOMTimestampCheck(d sbom.Document, c *scvsCheck) scvsScore { + s := newScoreFromScvsCheck(c) + + if IsSBOMTimestamped(d, s) { + s.setL3Score(green + bold + "✓" + reset) + s.setL2Score(green + bold + "✓" + reset) + s.setL1Score(green + bold + "✓" + reset) + } else { + s.setL3Score(red + bold + "✗" + reset) + s.setL2Score(red + bold + "✗" + reset) + s.setL1Score(red + bold + "✗" + reset) + } + return *s +} + +// Level: 123 +func scvsSBOMRiskAnalysisCheck(d sbom.Document, c *scvsCheck) scvsScore { + s := newScoreFromScvsCheck(c) + + if IsSBOMAnalyzedForRisk(d, s) { + s.setL3Score(green + bold + "✓" + reset) + s.setL2Score(green + bold + "✓" + reset) + s.setL1Score(green + bold + "✓" + reset) + } else { + s.setL3Score(red + bold + "✗" + reset) + s.setL2Score(red + bold + "✗" + reset) + s.setL1Score(red + bold + "✗" + reset) + } + return *s +} + +// Level: 123 +func scvsSBOMInventoryListCheck(d sbom.Document, c *scvsCheck) scvsScore { + s := newScoreFromScvsCheck(c) + + if IsSBOMHasInventoryOfDependencies(d, s) { + s.setL3Score(green + bold + "✓" + reset) + s.setL2Score(green + bold + "✓" + reset) + s.setL1Score(green + bold + "✓" + reset) + } else { + s.setL3Score(red + bold + "✗" + reset) + s.setL2Score(red + bold + "✗" + reset) + s.setL1Score(red + bold + "✗" + reset) + } + return *s +} + +// Level: 23 +func scvsSBOMTestInventoryListCheck(d sbom.Document, c *scvsCheck) scvsScore { + s := newScoreFromScvsCheck(c) + + if IsSBOMInventoryContainsTestComponents(d, s) { + s.setL3Score(green + bold + "✓" + reset) + s.setL2Score(green + bold + "✓" + reset) + } else { + s.setL3Score(red + bold + "✗" + reset) + s.setL2Score(red + bold + "✗" + reset) + } + return *s +} + +// Level: 23 +func scvsSBOMPrimaryCompCheck(d sbom.Document, c *scvsCheck) scvsScore { + s := newScoreFromScvsCheck(c) + + if IsSBOMHasPrimaryComponents(d, s) { + s.setL3Score(green + bold + "✓" + reset) + s.setL2Score(green + bold + "✓" + reset) + } else { + s.setL3Score(red + bold + "✗" + reset) + s.setL2Score(red + bold + "✗" + reset) + } + return *s +} + +// Level: 123 +func scvsCompHasIdentityIDCheck(d sbom.Document, c *scvsCheck) scvsScore { + s := newScoreFromScvsCheck(c) + + if IsComponentHasIdentityID(d, s) { + s.setL3Score(green + bold + "✓" + reset) + s.setL2Score(green + bold + "✓" + reset) + s.setL1Score(green + bold + "✓" + reset) + } else { + s.setL3Score(red + bold + "✗" + reset) + s.setL2Score(red + bold + "✗" + reset) + s.setL1Score(red + bold + "✗" + reset) + } + return *s +} + +// Level: 3 +func scvsCompHasOriginIDCheck(d sbom.Document, c *scvsCheck) scvsScore { + s := newScoreFromScvsCheck(c) + + if IsComponentHasOriginID(d, s) { + s.setL3Score(green + bold + "✓" + reset) + } else { + s.setL3Score(red + bold + "✗" + reset) + } + return *s +} + +// Level: 123 +func scvsCompHasLicensesCheck(d sbom.Document, c *scvsCheck) scvsScore { + s := newScoreFromScvsCheck(c) + + if IsComponentHasLicenses(d, s) { + s.setL3Score(green + bold + "✓" + reset) + s.setL2Score(green + bold + "✓" + reset) + s.setL1Score(green + bold + "✓" + reset) + } else { + s.setL3Score(red + bold + "✗" + reset) + s.setL2Score(red + bold + "✗" + reset) + s.setL1Score(red + bold + "✗" + reset) + } + return *s +} + +// Level: 23 +func scvsCompHasValidLicenseCheck(d sbom.Document, c *scvsCheck) scvsScore { + s := newScoreFromScvsCheck(c) + + if IsComponentHasVerifiedLicense(d, s) { + s.setL3Score(green + bold + "✓" + reset) + s.setL2Score(green + bold + "✓" + reset) + } else { + s.setL3Score(red + bold + "✗" + reset) + s.setL2Score(red + bold + "✗" + reset) + } + return *s +} + +// Level: 3 +func scvsCompHasCopyright(d sbom.Document, c *scvsCheck) scvsScore { + s := newScoreFromScvsCheck(c) + + if IsComponentHasCopyright(d, s) { + s.setL3Score(green + bold + "✓" + reset) + } else { + s.setL3Score(red + bold + "✗" + reset) + } + return *s +} + +// Level: 3 +func scvsCompHasModificationCheck(d sbom.Document, c *scvsCheck) scvsScore { + s := newScoreFromScvsCheck(c) + + if IsComponentContainsModificationChanges(d, s) { + s.setL3Score(green + bold + "✓" + reset) + } else { + s.setL3Score(red + bold + "✗" + reset) + } + return *s +} + +// Level: 3 +func scvsCompHashCheck(d sbom.Document, c *scvsCheck) scvsScore { + s := newScoreFromScvsCheck(c) + + if IsComponentContainsHash(d, s) { + s.setL3Score(green + bold + "✓" + reset) + } else { + s.setL3Score(red + bold + "✗" + reset) + } + return *s +} diff --git a/pkg/scvs/scvs.go b/pkg/scvs/scvs.go new file mode 100644 index 0000000..cd5be39 --- /dev/null +++ b/pkg/scvs/scvs.go @@ -0,0 +1,320 @@ +// Copyright 2023 Interlynk.io +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package scvs + +import ( + "fmt" + "os" + "os/exec" + "strings" + + "github.com/interlynk-io/sbomqs/pkg/licenses" + "github.com/interlynk-io/sbomqs/pkg/sbom" + "github.com/samber/lo" +) + +// A structured, machine readable software bill of materials (SBOM) format is present +func IsSBOMMachineReadable(d sbom.Document, s *scvsScore) bool { + // check spec is SPDX or CycloneDX + specs := sbom.SupportedSBOMSpecs() + + for _, spec := range specs { + if d.Spec().GetSpecType() == spec { + s.setDesc("SBOM is machine readable") + return true + } + } + return false +} + +// SBOM creation is automated and reproducible +func IsSBOMCreationAutomated(d sbom.Document, s *scvsScore) bool { + noOfTools := len(d.Tools()) + if tools := d.Tools(); tools != nil { + for _, tool := range tools { + name := tool.GetName() + fmt.Println("Name: ", name) + version := tool.GetVersion() + fmt.Println("version: ", version) + + if name != "" && version != "" { + s.setDesc(fmt.Sprintf("SBOM has %d authors", noOfTools)) + return true + } + } + } + + s.setDesc(fmt.Sprintf("SBOM has %d authors", noOfTools)) + return false +} + +// 2.3 Each SBOM has a unique identifier +func IsSBOMHasUniqID(d sbom.Document, s *scvsScore) bool { + if ns := d.Spec().GetNamespace(); ns != "" { + s.setDesc(fmt.Sprintf("SBOM has uniq ID")) + return true + } + s.setDesc(fmt.Sprintf("SBOM doesn't has uniq ID")) + return false +} + +func IsSBOMHasSignature(d sbom.Document, s *scvsScore) bool { + // isSignatureExists := d.Spec().GetSignature().CheckSignatureExists() + sig := d.Signature() + fmt.Println("Signature: ", sig) + + if sig != nil { + fmt.Println("Signature is not nil") + + for _, signature := range sig { + if signature != nil { + return signature.CheckSignatureExists() + } + } + } else { + fmt.Println("Signature is nil") + } + + return false +} + +func IsSBOMSignatureCorrect(d sbom.Document, s *scvsScore) bool { + return IsSBOMHasSignature(d, s) +} + +func IsSBOMSignatureVerified(d sbom.Document, s *scvsScore) bool { + // Save signature and public key to temporary files + signature := d.Signature() + if signature == nil { + return false + } + for _, sig := range signature { + if sig == nil { + return false + } + + sigFile, err := os.CreateTemp("", "signature-*.sig") + if err != nil { + fmt.Println("Error creating temp file for signature:", err) + return false + } + defer os.Remove(sigFile.Name()) + + pubKeyFile, err := os.CreateTemp("", "publickey-*.pem") + if err != nil { + fmt.Println("Error creating temp file for public key:", err) + return false + } + defer os.Remove(pubKeyFile.Name()) + + _, err = sigFile.WriteString(sig.Value()) + if err != nil { + fmt.Println("Error writing signature to temp file:", err) + return false + } + _, err = pubKeyFile.WriteString(sig.PublicKey()) + if err != nil { + fmt.Println("Error writing public key to temp file:", err) + return false + } + + // Use openssl to verify the signature + cmd := exec.Command("openssl", "dgst", "-verify", pubKeyFile.Name(), "-signature", sigFile.Name(), "data-to-verify.txt") + output, err := cmd.CombinedOutput() + if err != nil { + fmt.Println("Error verifying signature with openssl:", err) + return false + } + // // Use cosign to verify the signature + // cmd := exec.Command("cosign", "verify-blob", "--key", pubKeyFile.Name(), "--signature", sigFile.Name(), "data-to-verify.txt") + // output, err := cmd.CombinedOutput() + // if err != nil { + // fmt.Println("Error verifying signature with cosign:", err) + // fmt.Println(string(output)) + // return false + // } + + verificationResult := strings.Contains(string(output), "Verified OK") + fmt.Println("Verification result:", verificationResult) + + return verificationResult + } + return false +} + +func IsSBOMTimestamped(d sbom.Document, s *scvsScore) bool { + if d.Spec().GetCreationTimestamp() != "" { + s.setDesc(fmt.Sprintf("SBOM is timestamped")) + return true + } + s.setDesc(fmt.Sprintf("SBOM isn't timestamped")) + return false +} + +func IsSBOMAnalyzedForRisk(d sbom.Document, s *scvsScore) bool { return false } // 2.8 + +func IsSBOMHasInventoryOfDependencies(d sbom.Document, s *scvsScore) bool { return false } // 2.9 + +func IsSBOMInventoryContainsTestComponents(d sbom.Document, s *scvsScore) bool { return false } // 2.10 + +func IsSBOMHasPrimaryComponents(d sbom.Document, s *scvsScore) bool { + // + if d.PrimaryComponent() { + s.setDesc(fmt.Sprintf("SBOM have primary comp")) + return true + } + s.setDesc(fmt.Sprintf("SBOM doesn't have primary comp")) + return false +} + +func IsComponentHasIdentityID(d sbom.Document, s *scvsScore) bool { + totalComponents := len(d.Components()) + if totalComponents == 0 { + s.setDesc("N/A (no components)") + return false + } + + withIdentityID := lo.CountBy(d.Components(), func(c sbom.GetComponent) bool { + return len(c.Purls()) > 0 + }) + + if totalComponents > 0 { + if withIdentityID == totalComponents { + s.setDesc(fmt.Sprintf("%d/%d comp have Identity ID's", withIdentityID, totalComponents)) + return true + } + } + s.setDesc(fmt.Sprintf("%d/%d comp have Identity ID's", withIdentityID, totalComponents)) + return false +} + +func IsComponentHasOriginID(d sbom.Document, s *scvsScore) bool { + totalComponents := len(d.Components()) + if totalComponents == 0 { + s.setDesc("N/A (no components)") + return false + } + + withOriginID := lo.CountBy(d.Components(), func(c sbom.GetComponent) bool { + return len(c.Purls()) > 0 + }) + + if totalComponents > 0 { + if withOriginID == totalComponents { + s.setDesc(fmt.Sprintf("%d/%d comp have Origin ID's", withOriginID, totalComponents)) + return true + } + } + s.setDesc(fmt.Sprintf("%d/%d comp have Origin ID's", withOriginID, totalComponents)) + return false +} + +// 2.13 +func IsComponentHasLicenses(d sbom.Document, s *scvsScore) bool { + // + totalComponents := len(d.Components()) + if totalComponents == 0 { + s.setDesc("N/A (no components)") + return false + } + + withLicenses := lo.CountBy(d.Components(), func(c sbom.GetComponent) bool { + return len(c.Licenses()) > 0 + }) + + if totalComponents > 0 { + if withLicenses >= totalComponents { + s.setDesc(fmt.Sprintf("%d/%d comp has Licenses", withLicenses, totalComponents)) + return true + } + } + s.setDesc(fmt.Sprintf("%d/%d comp has Licenses", withLicenses, totalComponents)) + return false +} + +// 2.14 +func IsComponentHasVerifiedLicense(d sbom.Document, s *scvsScore) bool { + totalComponents := len(d.Components()) + if totalComponents == 0 { + s.setDesc("N/A (no components)") + return false + } + + // var countAllValidLicense int + // for _, comp := range d.Components() { + // for _, licen := range comp.Licenses() { + // if licenses.IsValidLicenseID(licen.Name()) { + // countAllValidLicense++ + // } + // } + // } + countAllValidLicense := lo.CountBy(d.Components(), func(comp sbom.GetComponent) bool { + return lo.SomeBy(comp.Licenses(), func(licen licenses.License) bool { + return licenses.IsValidLicenseID(licen.Name()) + }) + }) + + if totalComponents > 0 { + if countAllValidLicense >= totalComponents { + s.setDesc(fmt.Sprintf("%d/%d comp has valid Licenses", countAllValidLicense, totalComponents)) + return true + } + } + s.setDesc(fmt.Sprintf("%d/%d comp has valid Licenses", countAllValidLicense, totalComponents)) + return false +} + +func IsComponentHasCopyright(d sbom.Document, s *scvsScore) bool { + totalComponents := len(d.Components()) + if totalComponents == 0 { + s.setDesc("N/A (no components)") + return false + } + + withCopyrights := lo.CountBy(d.Components(), func(c sbom.GetComponent) bool { + return len(c.GetCopyRight()) > 0 + }) + + if totalComponents > 0 { + if withCopyrights == totalComponents { + s.setDesc(fmt.Sprintf("%d/%d comp has Copyright", withCopyrights, totalComponents)) + return true + } + } + s.setDesc(fmt.Sprintf("%d/%d comp has Copyright", withCopyrights, totalComponents)) + return false +} + +// 2.16 +func IsComponentContainsModificationChanges(d sbom.Document, s *scvsScore) bool { return false } // 2.17 + +func IsComponentContainsHash(d sbom.Document, s *scvsScore) bool { + totalComponents := len(d.Components()) + if totalComponents == 0 { + return false + } + + withChecksums := lo.CountBy(d.Components(), func(c sbom.GetComponent) bool { + return len(c.GetChecksums()) > 0 + }) + if totalComponents > 0 { + if withChecksums == totalComponents { + s.setDesc(fmt.Sprintf("%d/%d comp has Checksum", withChecksums, totalComponents)) + return true + } + } + s.setDesc(fmt.Sprintf("%d/%d comp has Checksum", withChecksums, totalComponents)) + return false +} // 2.18 diff --git a/pkg/scvs/scvsCriteria.go b/pkg/scvs/scvsCriteria.go new file mode 100644 index 0000000..26fc4d2 --- /dev/null +++ b/pkg/scvs/scvsCriteria.go @@ -0,0 +1,44 @@ +// Copyright 2023 Interlynk.io +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package scvs + +import "github.com/interlynk-io/sbomqs/pkg/sbom" + +type scvsCheck struct { + Key string `yaml:"feature"` + evaluate func(sbom.Document, *scvsCheck) scvsScore +} + +var scvsChecks = []scvsCheck{ + // scvs + {"A structured, machine readable software bill of materials (SBOM) format is present", scvsSBOMMachineReadableCheck}, + {"SBOM creation is automated and reproducible", scvsSBOMAutomationCreationCheck}, + {"Each SBOM has a unique identifier", scvsSBOMUniqIDCheck}, + {"SBOM has been signed by publisher, supplier, or certifying authority", scvsSBOMSigcheck}, + {"SBOM signature verification exists", scvsSBOMSigCorrectnessCheck}, + {"SBOM signature verification is performed", scvsSBOMSigVerified}, + {"SBOM is timestamped", scvsSBOMTimestampCheck}, + {"SBOM is analyzed for risk", scvsSBOMRiskAnalysisCheck}, + {"SBOM contains a complete and accurate inventory of all components the SBOM describes", scvsSBOMInventoryListCheck}, + {"SBOM contains an accurate inventory of all test components for the asset or application it describes", scvsSBOMTestInventoryListCheck}, + {"SBOM contains metadata about the asset or software the SBOM describes", scvsSBOMPrimaryCompCheck}, + {"Component identifiers are derived from their native ecosystems (if applicable)", scvsCompHasIdentityIDCheck}, + {"Component point of origin is identified in a consistent, machine readable format (e.g. PURL)", scvsCompHasOriginIDCheck}, + {"Components defined in SBOM have accurate license information", scvsCompHasLicensesCheck}, + {"Components defined in SBOM have valid SPDX license ID's or expressions (if applicable)", scvsCompHasValidLicenseCheck}, + {"Components defined in SBOM have valid copyright statements", scvsCompHasCopyright}, + {"Components defined in SBOM which have been modified from the original have detailed provenance", scvsCompHasModificationCheck}, + {"Components defined in SBOM have one or more file hashes (SHA-256, SHA-512, etc)", scvsCompHashCheck}, +} diff --git a/pkg/scvs/scvsScore.go b/pkg/scvs/scvsScore.go new file mode 100644 index 0000000..8e1e673 --- /dev/null +++ b/pkg/scvs/scvsScore.go @@ -0,0 +1,73 @@ +// Copyright 2023 Interlynk.io +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package scvs + +type ScvsScore interface { + Feature() string + L1Score() string + L2Score() string + L3Score() string + Descr() string +} + +type scvsScore struct { + feature string + l1Score string + l2Score string + l3Score string + descr string +} + +func newScoreFromScvsCheck(c *scvsCheck) *scvsScore { + return &scvsScore{ + feature: c.Key, + } +} + +func (s *scvsScore) setDesc(d string) { + s.descr = d +} + +func (s *scvsScore) setL3Score(v string) { + s.l3Score = v +} + +func (s *scvsScore) setL2Score(v string) { + s.l2Score = v +} + +func (s *scvsScore) setL1Score(v string) { + s.l1Score = v +} + +func (s scvsScore) Feature() string { + return s.feature +} + +func (s scvsScore) L3Score() string { + return s.l3Score +} + +func (s scvsScore) L1Score() string { + return s.l1Score +} + +func (s scvsScore) L2Score() string { + return s.l2Score +} + +func (s scvsScore) Descr() string { + return s.descr +} diff --git a/pkg/scvs/scvsScorer.go b/pkg/scvs/scvsScorer.go new file mode 100644 index 0000000..177bc7b --- /dev/null +++ b/pkg/scvs/scvsScorer.go @@ -0,0 +1,53 @@ +// Copyright 2023 Interlynk.io +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package scvs + +import ( + "context" + + "github.com/interlynk-io/sbomqs/pkg/sbom" +) + +type ScvsScorer struct { + ctx context.Context + doc sbom.Document +} + +func NewScvsScorer(ctx context.Context, doc sbom.Document) *ScvsScorer { + scorer := &ScvsScorer{ + ctx: ctx, + doc: doc, + } + + return scorer +} + +func (s *ScvsScorer) ScvsScore() ScvsScores { + if s.doc == nil { + return newScvsScores() + } + + return s.AllScvsScores() +} + +func (s *ScvsScorer) AllScvsScores() ScvsScores { + scores := newScvsScores() + + for _, c := range scvsChecks { + scores.addScore(c.evaluate(s.doc, &c)) + } + + return scores +} diff --git a/pkg/scvs/scvsScores.go b/pkg/scvs/scvsScores.go new file mode 100644 index 0000000..d436382 --- /dev/null +++ b/pkg/scvs/scvsScores.go @@ -0,0 +1,53 @@ +// Copyright 2023 Interlynk.io +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package scvs + +type ScvsScores interface { + Count() int + AvgScore() float64 + ScoreList() []ScvsScore +} + +type scvsScores struct { + scs []ScvsScore +} + +func newScvsScores() *scvsScores { + return &scvsScores{ + scs: []ScvsScore{}, + } +} + +func (s *scvsScores) addScore(ss scvsScore) { + s.scs = append(s.scs, ss) +} + +func (s scvsScores) Count() int { + return len(s.scs) +} + +func (s scvsScores) AvgScore() float64 { + score := 0.0 + for _, s := range s.scs { + if s.L1Score() == "✓" { + score++ + } + } + return score / float64(s.Count()) +} + +func (s scvsScores) ScoreList() []ScvsScore { + return s.scs +} From c1fbe71e02900936446967577a8bd1e10a0ad4ab Mon Sep 17 00:00:00 2001 From: Vivek Kumar Sahu Date: Thu, 5 Sep 2024 13:35:00 +0530 Subject: [PATCH 02/11] add scvs command Signed-off-by: Vivek Kumar Sahu --- cmd/scvs.go | 69 +++++++++++++++++++++ pkg/engine/scvs.go | 134 +++++++++++++++++++++++++++++++++++++++++ pkg/scvs/scvsReport.go | 82 +++++++++++++++++++++++++ 3 files changed, 285 insertions(+) create mode 100644 cmd/scvs.go create mode 100644 pkg/engine/scvs.go create mode 100644 pkg/scvs/scvsReport.go diff --git a/cmd/scvs.go b/cmd/scvs.go new file mode 100644 index 0000000..bdc8844 --- /dev/null +++ b/cmd/scvs.go @@ -0,0 +1,69 @@ +// Copyright 2023 Interlynk.io +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package cmd + +import ( + "context" + "fmt" + + "github.com/interlynk-io/sbomqs/pkg/engine" + "github.com/interlynk-io/sbomqs/pkg/logger" + "github.com/spf13/cobra" +) + +var scvsCmd = &cobra.Command{ + Use: "scvs", + Short: "sbom component vs", + SilenceUsage: true, + Args: func(cmd *cobra.Command, args []string) error { + if len(args) <= 0 { + if len(inFile) <= 0 && len(inDirPath) <= 0 { + return fmt.Errorf("provide a path to an sbom file or directory of sbom files") + } + } + return nil + }, + RunE: processScvs, +} + +func processScvs(cmd *cobra.Command, args []string) error { + debug, _ := cmd.Flags().GetBool("debug") + if debug { + logger.InitDebugLogger() + } else { + logger.InitProdLogger() + } + + ctx := logger.WithLogger(context.Background()) + uCmd := toUserCmd(cmd, args) + + if err := validateFlags(uCmd); err != nil { + return err + } + + engParams := toEngineParams(uCmd) + return engine.RunScvs(ctx, engParams) +} + +func init() { + rootCmd.AddCommand(scvsCmd) + + // Debug Control + scvsCmd.Flags().BoolP("debug", "D", false, "scvs compliance") + + // Output Control + // scvsCmd.Flags().BoolP("json", "j", false, "results in json") + scvsCmd.Flags().BoolP("detailed", "d", true, "results in table format, default") + // scvsCmd.Flags().BoolP("basic", "b", false, "results in single line format") +} diff --git a/pkg/engine/scvs.go b/pkg/engine/scvs.go new file mode 100644 index 0000000..689c999 --- /dev/null +++ b/pkg/engine/scvs.go @@ -0,0 +1,134 @@ +// Copyright 2023 Interlynk.io +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package engine + +import ( + "context" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/interlynk-io/sbomqs/pkg/logger" + "github.com/interlynk-io/sbomqs/pkg/sbom" + "github.com/interlynk-io/sbomqs/pkg/scvs" +) + +func RunScvs(ctx context.Context, ep *Params) error { + log := logger.FromContext(ctx) + log.Debug("engine.Run()") + log.Debug(ep) + + if len(ep.Path) <= 0 { + log.Fatal("path is required") + } + + return handleScvsPaths(ctx, ep) +} + +func handleScvsPaths(ctx context.Context, ep *Params) error { + log := logger.FromContext(ctx) + log.Debug("engine.handlePaths()") + + var docs []sbom.Document + var paths []string + var scores []scvs.ScvsScores + + for _, path := range ep.Path { + log.Debugf("Processing path :%s\n", path) + pathInfo, _ := os.Stat(path) + if pathInfo.IsDir() { + files, err := os.ReadDir(path) + if err != nil { + log.Debugf("os.ReadDir failed for path:%s\n", path) + log.Debugf("%s\n", err) + continue + } + for _, file := range files { + log.Debugf("Processing file :%s\n", file.Name()) + if file.IsDir() { + continue + } + path := filepath.Join(path, file.Name()) + doc, scs, err := processScvsFile(ctx, ep, path) + if err != nil { + continue + } + docs = append(docs, doc) + scores = append(scores, scs) + paths = append(paths, path) + } + continue + } + + doc, scs, err := processScvsFile(ctx, ep, path) + if err != nil { + continue + } + docs = append(docs, doc) + scores = append(scores, scs) + paths = append(paths, path) + } + + reportFormat := "detailed" + if ep.Basic { + reportFormat = "basic" + } else if ep.JSON { + reportFormat = "json" + } + + nr := scvs.NewScvsReport(ctx, + docs, + scores, + paths, + scvs.WithFormat(strings.ToLower(reportFormat))) + + fmt.Println("Print the scvs report") + nr.ScvsReport() + + return nil +} + +func processScvsFile(ctx context.Context, ep *Params, path string) (sbom.Document, scvs.ScvsScores, error) { + log := logger.FromContext(ctx) + log.Debugf("Processing file :%s\n", path) + + if _, err := os.Stat(path); err != nil { + log.Debugf("os.Stat failed for file :%s\n", path) + fmt.Printf("failed to stat %s\n", path) + return nil, nil, err + } + + f, err := os.Open(path) + if err != nil { + log.Debugf("os.Open failed for file :%s\n", path) + fmt.Printf("failed to open %s\n", path) + return nil, nil, err + } + defer f.Close() + + doc, err := sbom.NewSBOMDocument(ctx, f) + if err != nil { + log.Debugf("failed to create sbom document for :%s\n", path) + log.Debugf("%s\n", err) + fmt.Printf("failed to parse %s : %s\n", path, err) + return nil, nil, err + } + + sr := scvs.NewScvsScorer(ctx, doc) + + scores := sr.ScvsScore() + return doc, scores, nil +} diff --git a/pkg/scvs/scvsReport.go b/pkg/scvs/scvsReport.go new file mode 100644 index 0000000..f36a0ca --- /dev/null +++ b/pkg/scvs/scvsReport.go @@ -0,0 +1,82 @@ +package scvs + +import ( + "context" + "fmt" + "os" + "sort" + "strings" + + "github.com/interlynk-io/sbomqs/pkg/sbom" + "github.com/olekukonko/tablewriter" +) + +type ScvsReporter struct { + Ctx context.Context + + Docs []sbom.Document + Scores []ScvsScores + Paths []string + + // optional params + Format string +} +type Option func(r *ScvsReporter) + +func WithFormat(c string) Option { + return func(r *ScvsReporter) { + r.Format = c + } +} + +func NewScvsReport(ctx context.Context, doc []sbom.Document, scores []ScvsScores, paths []string, opts ...Option) *ScvsReporter { + r := &ScvsReporter{ + Ctx: ctx, + Docs: doc, + Scores: scores, + Paths: paths, + } + return r +} + +func (r *ScvsReporter) ScvsReport() { + r.detailedScvsReport() +} + +func (r *ScvsReporter) detailedScvsReport() { + for index := range r.Paths { + // doc := r.Docs[index] + scores := r.Scores[index] + + outDoc := [][]string{} + + for _, score := range scores.ScoreList() { + var l []string + + l = []string{score.Feature(), score.L1Score(), score.L2Score(), score.L3Score(), score.Descr()} + + outDoc = append(outDoc, l) + } + + sort.Slice(outDoc, func(i, j int) bool { + switch strings.Compare(outDoc[i][0], outDoc[j][0]) { + case -1: + return true + case 1: + return false + } + return outDoc[i][1] < outDoc[j][1] + }) + + // fmt.Printf("SBOM Quality by Interlynk Score:%0.1f\tcomponents:%d\t%s\n", scores.AvgScore(), len(doc.Components()), path) + fmt.Println("Analysis of SCVS Report by OWASP Organization using SBOMQS Tool") + table := tablewriter.NewWriter(os.Stdout) + table.SetHeader([]string{"Feature", "Level 1", "Level 2", "Level 3", "Desc"}) + table.SetRowLine(true) + table.SetAutoWrapText(false) + table.SetColMinWidth(0, 60) + table.SetAutoMergeCellsByColumnIndex([]int{0}) + table.AppendBulk(outDoc) + table.Render() + } +} From d078f73516509fdccc66f9c3599c2b0ef21b3544 Mon Sep 17 00:00:00 2001 From: Vivek Kumar Sahu Date: Thu, 5 Sep 2024 15:46:42 +0530 Subject: [PATCH 03/11] fix lint errors Signed-off-by: Vivek Kumar Sahu --- pkg/sbom/signature.go | 14 +++--- pkg/sbom/spdx.go | 1 - pkg/scvs/scvs.go | 103 ++++++++++++++++++----------------------- pkg/scvs/scvsReport.go | 3 +- 4 files changed, 53 insertions(+), 68 deletions(-) diff --git a/pkg/sbom/signature.go b/pkg/sbom/signature.go index 6cabb20..e98dcd7 100644 --- a/pkg/sbom/signature.go +++ b/pkg/sbom/signature.go @@ -23,13 +23,13 @@ type GetSignature interface { } type signature struct { - keyID string - algorithm string - value string - publicKey string - certificatePath string - certificate string - timestamp string + keyID string + algorithm string + value string + publicKey string + // certificatePath string + certificate string + // timestamp string } func (s signature) CheckSignatureExists() bool { diff --git a/pkg/sbom/spdx.go b/pkg/sbom/spdx.go index 2e78bc2..9f8a756 100644 --- a/pkg/sbom/spdx.go +++ b/pkg/sbom/spdx.go @@ -161,7 +161,6 @@ func (s *SpdxDoc) parse() { func (s *SpdxDoc) parseSignature() { s.signature = nil - return } func (s *SpdxDoc) parseSpec() { diff --git a/pkg/scvs/scvs.go b/pkg/scvs/scvs.go index cd5be39..82f00df 100644 --- a/pkg/scvs/scvs.go +++ b/pkg/scvs/scvs.go @@ -45,9 +45,7 @@ func IsSBOMCreationAutomated(d sbom.Document, s *scvsScore) bool { if tools := d.Tools(); tools != nil { for _, tool := range tools { name := tool.GetName() - fmt.Println("Name: ", name) version := tool.GetVersion() - fmt.Println("version: ", version) if name != "" && version != "" { s.setDesc(fmt.Sprintf("SBOM has %d authors", noOfTools)) @@ -63,21 +61,18 @@ func IsSBOMCreationAutomated(d sbom.Document, s *scvsScore) bool { // 2.3 Each SBOM has a unique identifier func IsSBOMHasUniqID(d sbom.Document, s *scvsScore) bool { if ns := d.Spec().GetNamespace(); ns != "" { - s.setDesc(fmt.Sprintf("SBOM has uniq ID")) + s.setDesc("SBOM has uniq ID") return true } - s.setDesc(fmt.Sprintf("SBOM doesn't has uniq ID")) + s.setDesc("SBOM doesn't has uniq ID") return false } func IsSBOMHasSignature(d sbom.Document, s *scvsScore) bool { // isSignatureExists := d.Spec().GetSignature().CheckSignatureExists() sig := d.Signature() - fmt.Println("Signature: ", sig) if sig != nil { - fmt.Println("Signature is not nil") - for _, signature := range sig { if signature != nil { return signature.CheckSignatureExists() @@ -100,66 +95,58 @@ func IsSBOMSignatureVerified(d sbom.Document, s *scvsScore) bool { if signature == nil { return false } - for _, sig := range signature { - if sig == nil { - return false - } - sigFile, err := os.CreateTemp("", "signature-*.sig") - if err != nil { - fmt.Println("Error creating temp file for signature:", err) - return false - } - defer os.Remove(sigFile.Name()) + // Use the first signature + sig := signature[0] + if sig == nil { + return false + } - pubKeyFile, err := os.CreateTemp("", "publickey-*.pem") - if err != nil { - fmt.Println("Error creating temp file for public key:", err) - return false - } - defer os.Remove(pubKeyFile.Name()) + sigFile, err := os.CreateTemp("", "signature-*.sig") + if err != nil { + fmt.Println("Error creating temp file for signature:", err) + return false + } + defer os.Remove(sigFile.Name()) - _, err = sigFile.WriteString(sig.Value()) - if err != nil { - fmt.Println("Error writing signature to temp file:", err) - return false - } - _, err = pubKeyFile.WriteString(sig.PublicKey()) - if err != nil { - fmt.Println("Error writing public key to temp file:", err) - return false - } + pubKeyFile, err := os.CreateTemp("", "publickey-*.pem") + if err != nil { + fmt.Println("Error creating temp file for public key:", err) + return false + } + defer os.Remove(pubKeyFile.Name()) - // Use openssl to verify the signature - cmd := exec.Command("openssl", "dgst", "-verify", pubKeyFile.Name(), "-signature", sigFile.Name(), "data-to-verify.txt") - output, err := cmd.CombinedOutput() - if err != nil { - fmt.Println("Error verifying signature with openssl:", err) - return false - } - // // Use cosign to verify the signature - // cmd := exec.Command("cosign", "verify-blob", "--key", pubKeyFile.Name(), "--signature", sigFile.Name(), "data-to-verify.txt") - // output, err := cmd.CombinedOutput() - // if err != nil { - // fmt.Println("Error verifying signature with cosign:", err) - // fmt.Println(string(output)) - // return false - // } - - verificationResult := strings.Contains(string(output), "Verified OK") - fmt.Println("Verification result:", verificationResult) - - return verificationResult + _, err = sigFile.WriteString(sig.Value()) + if err != nil { + fmt.Println("Error writing signature to temp file:", err) + return false } - return false + _, err = pubKeyFile.WriteString(sig.PublicKey()) + if err != nil { + fmt.Println("Error writing public key to temp file:", err) + return false + } + + // Use openssl to verify the signature + cmd := exec.Command("openssl", "dgst", "-verify", pubKeyFile.Name(), "-signature", sigFile.Name(), "data-to-verify.txt") + output, err := cmd.CombinedOutput() + if err != nil { + fmt.Println("Error verifying signature with openssl:", err) + return false + } + + verificationResult := strings.Contains(string(output), "Verified OK") + fmt.Println("Verification result:", verificationResult) + + return verificationResult } func IsSBOMTimestamped(d sbom.Document, s *scvsScore) bool { if d.Spec().GetCreationTimestamp() != "" { - s.setDesc(fmt.Sprintf("SBOM is timestamped")) + s.setDesc("SBOM is timestamped") return true } - s.setDesc(fmt.Sprintf("SBOM isn't timestamped")) + s.setDesc("SBOM isn't timestamped") return false } @@ -172,10 +159,10 @@ func IsSBOMInventoryContainsTestComponents(d sbom.Document, s *scvsScore) bool { func IsSBOMHasPrimaryComponents(d sbom.Document, s *scvsScore) bool { // if d.PrimaryComponent() { - s.setDesc(fmt.Sprintf("SBOM have primary comp")) + s.setDesc("SBOM have primary comp") return true } - s.setDesc(fmt.Sprintf("SBOM doesn't have primary comp")) + s.setDesc("SBOM doesn't have primary comp") return false } diff --git a/pkg/scvs/scvsReport.go b/pkg/scvs/scvsReport.go index f36a0ca..3fc5fff 100644 --- a/pkg/scvs/scvsReport.go +++ b/pkg/scvs/scvsReport.go @@ -51,9 +51,8 @@ func (r *ScvsReporter) detailedScvsReport() { outDoc := [][]string{} for _, score := range scores.ScoreList() { - var l []string - l = []string{score.Feature(), score.L1Score(), score.L2Score(), score.L3Score(), score.Descr()} + l := []string{score.Feature(), score.L1Score(), score.L2Score(), score.L3Score(), score.Descr()} outDoc = append(outDoc, l) } From 99bbf9bedb1060c64a5adda6a4a9dcd80e7c09f7 Mon Sep 17 00:00:00 2001 From: Vivek Kumar Sahu Date: Sat, 7 Sep 2024 17:50:28 +0530 Subject: [PATCH 04/11] replace namespace with uniq id Signed-off-by: Vivek Kumar Sahu --- cmd/scvs.go | 2 +- pkg/compliance/oct.go | 2 +- pkg/compliance/oct_test.go | 4 ++-- pkg/sbom/cdx.go | 4 ++-- pkg/sbom/spdx.go | 2 +- pkg/sbom/spec.go | 8 ++++---- pkg/scorer/ntia.go | 2 +- pkg/scvs/scvs.go | 2 +- 8 files changed, 13 insertions(+), 13 deletions(-) diff --git a/cmd/scvs.go b/cmd/scvs.go index bdc8844..1dc7787 100644 --- a/cmd/scvs.go +++ b/cmd/scvs.go @@ -24,7 +24,7 @@ import ( var scvsCmd = &cobra.Command{ Use: "scvs", - Short: "sbom component vs", + Short: "OWASP SCVS V2 Maturity Level Software Bill of Materials (SBOM) Requirements", SilenceUsage: true, Args: func(cmd *cobra.Command, args []string) error { if len(args) <= 0 { diff --git a/pkg/compliance/oct.go b/pkg/compliance/oct.go index a9da218..ea161a7 100644 --- a/pkg/compliance/oct.go +++ b/pkg/compliance/oct.go @@ -143,7 +143,7 @@ func octSbomComment(doc sbom.Document) *record { func octSbomNamespace(doc sbom.Document) *record { result, score := "", 0.0 - if ns := doc.Spec().GetNamespace(); ns != "" { + if ns := doc.Spec().GetUniqID(); ns != "" { result = ns score = 10.0 } diff --git a/pkg/compliance/oct_test.go b/pkg/compliance/oct_test.go index 2655a4b..0157018 100644 --- a/pkg/compliance/oct_test.go +++ b/pkg/compliance/oct_test.go @@ -14,7 +14,7 @@ func createDummyDocument() sbom.Document { s.Format = "json" s.SpecType = "spdx" s.Name = "nano" - s.Namespace = "https://anchore.com/syft/dir/sbomqs-6ec18b03-96cb-4951-b299-929890c1cfc8" + s.UniqID = "https://anchore.com/syft/dir/sbomqs-6ec18b03-96cb-4951-b299-929890c1cfc8" s.Organization = "interlynk" s.CreationTimestamp = "2023-05-04T09:33:40Z" s.Spdxid = "DOCUMENT" @@ -332,7 +332,7 @@ func createFailureDummyDocument() sbom.Document { s.Format = "xml" s.SpecType = "cyclonedx" s.Name = "" - s.Namespace = "" + s.UniqID = "" s.Organization = "" s.CreationTimestamp = "wrong-time-format" s.Spdxid = "" diff --git a/pkg/sbom/cdx.go b/pkg/sbom/cdx.go index 2c508af..506a395 100644 --- a/pkg/sbom/cdx.go +++ b/pkg/sbom/cdx.go @@ -203,10 +203,10 @@ func (c *CdxDoc) parseSpec() { sp.Licenses = aggregateLicenses(*c.doc.Metadata.Licenses) } } - sp.Namespace = c.doc.SerialNumber + sp.UniqID = c.doc.SerialNumber sp.SpecType = string(SBOMSpecCDX) - if c.doc.SerialNumber != "" && strings.HasPrefix(sp.Namespace, "urn:uuid:") { + if c.doc.SerialNumber != "" && strings.HasPrefix(sp.UniqID, "urn:uuid:") { sp.uri = fmt.Sprintf("%s/%d", c.doc.SerialNumber, c.doc.Version) } diff --git a/pkg/sbom/spdx.go b/pkg/sbom/spdx.go index 9f8a756..48054be 100644 --- a/pkg/sbom/spdx.go +++ b/pkg/sbom/spdx.go @@ -196,7 +196,7 @@ func (s *SpdxDoc) parseSpec() { sp.Licenses = append(sp.Licenses, lics...) - sp.Namespace = s.doc.DocumentNamespace + sp.UniqID = s.doc.DocumentNamespace if s.doc.DocumentNamespace != "" { sp.uri = s.doc.DocumentNamespace diff --git a/pkg/sbom/spec.go b/pkg/sbom/spec.go index 08036fa..74da847 100644 --- a/pkg/sbom/spec.go +++ b/pkg/sbom/spec.go @@ -27,7 +27,7 @@ type Spec interface { RequiredFields() bool GetCreationTimestamp() string GetLicenses() []licenses.License - GetNamespace() string + GetUniqID() string URI() string GetOrganization() string GetComment() string @@ -42,7 +42,7 @@ type Specs struct { isReqFieldsPresent bool Licenses []licenses.License CreationTimestamp string - Namespace string + UniqID string uri string Organization string Comment string @@ -97,8 +97,8 @@ func (s Specs) GetLicenses() []licenses.License { return s.Licenses } -func (s Specs) GetNamespace() string { - return s.Namespace +func (s Specs) GetUniqID() string { + return s.UniqID } func (s Specs) URI() string { diff --git a/pkg/scorer/ntia.go b/pkg/scorer/ntia.go index 9a5f552..d6162c5 100644 --- a/pkg/scorer/ntia.go +++ b/pkg/scorer/ntia.go @@ -102,7 +102,7 @@ func compWithUniqIDCheck(d sbom.Document, c *check) score { if c.GetID() == "" { return "", false } - return strings.Join([]string{d.Spec().GetNamespace(), c.GetID()}, ""), true + return strings.Join([]string{d.Spec().GetUniqID(), c.GetID()}, ""), true }) // uniqComps := lo.Uniq(compIDs) diff --git a/pkg/scvs/scvs.go b/pkg/scvs/scvs.go index 82f00df..c7b74fc 100644 --- a/pkg/scvs/scvs.go +++ b/pkg/scvs/scvs.go @@ -60,7 +60,7 @@ func IsSBOMCreationAutomated(d sbom.Document, s *scvsScore) bool { // 2.3 Each SBOM has a unique identifier func IsSBOMHasUniqID(d sbom.Document, s *scvsScore) bool { - if ns := d.Spec().GetNamespace(); ns != "" { + if ns := d.Spec().GetUniqID(); ns != "" { s.setDesc("SBOM has uniq ID") return true } From 1e91940b95d9a8e414b68048d691b7ba9b57212f Mon Sep 17 00:00:00 2001 From: Vivek Kumar Sahu Date: Sat, 7 Sep 2024 20:24:50 +0530 Subject: [PATCH 05/11] support valid license Signed-off-by: Vivek Kumar Sahu --- pkg/licenses/license.go | 15 +++----- pkg/scvs/scvs.go | 81 +++++++++++++++++++++++++++-------------- 2 files changed, 60 insertions(+), 36 deletions(-) diff --git a/pkg/licenses/license.go b/pkg/licenses/license.go index d9cdbc6..2ac1c84 100644 --- a/pkg/licenses/license.go +++ b/pkg/licenses/license.go @@ -96,21 +96,18 @@ func (m meta) AboutCode() bool { return m.source == "aboutcode" } -func IsValidLicenseID(licenseID string) bool { - if licenseID == "" { +func IsValidLicenseID(licenseKey string) bool { + if licenseKey == "" { return false } - lowerKey := strings.ToLower(licenseID) - - if lowerKey == "none" || lowerKey == "noassertion" { + // fmt.Println("licenseKey: ", licenseKey) + if licenseKey == "none" || licenseKey == "noassertion" { return false } - tLicKey := strings.TrimRight(licenseID, "+") - - _, lok := licenseList[tLicKey] - _, aok := licenseListAboutCode[tLicKey] + _, lok := licenseList[licenseKey] + _, aok := licenseListAboutCode[licenseKey] return lok || aok } diff --git a/pkg/scvs/scvs.go b/pkg/scvs/scvs.go index c7b74fc..b49c2a7 100644 --- a/pkg/scvs/scvs.go +++ b/pkg/scvs/scvs.go @@ -19,13 +19,14 @@ import ( "os" "os/exec" "strings" + "time" "github.com/interlynk-io/sbomqs/pkg/licenses" "github.com/interlynk-io/sbomqs/pkg/sbom" "github.com/samber/lo" ) -// A structured, machine readable software bill of materials (SBOM) format is present +// 2.1 A structured, machine readable software bill of materials (SBOM) format is present(L1, L2, L3) func IsSBOMMachineReadable(d sbom.Document, s *scvsScore) bool { // check spec is SPDX or CycloneDX specs := sbom.SupportedSBOMSpecs() @@ -39,7 +40,7 @@ func IsSBOMMachineReadable(d sbom.Document, s *scvsScore) bool { return false } -// SBOM creation is automated and reproducible +// 2.3 SBOM creation is automated and reproducible(L2, L3) func IsSBOMCreationAutomated(d sbom.Document, s *scvsScore) bool { noOfTools := len(d.Tools()) if tools := d.Tools(); tools != nil { @@ -58,7 +59,7 @@ func IsSBOMCreationAutomated(d sbom.Document, s *scvsScore) bool { return false } -// 2.3 Each SBOM has a unique identifier +// 2.3 Each SBOM has a unique identifier(L1, L2, L3) func IsSBOMHasUniqID(d sbom.Document, s *scvsScore) bool { if ns := d.Spec().GetUniqID(); ns != "" { s.setDesc("SBOM has uniq ID") @@ -68,6 +69,7 @@ func IsSBOMHasUniqID(d sbom.Document, s *scvsScore) bool { return false } +// 2.4 SBOM has been signed by publisher, supplier, or certifying authority(L2, L3) func IsSBOMHasSignature(d sbom.Document, s *scvsScore) bool { // isSignatureExists := d.Spec().GetSignature().CheckSignatureExists() sig := d.Signature() @@ -85,10 +87,12 @@ func IsSBOMHasSignature(d sbom.Document, s *scvsScore) bool { return false } +// 2.5 SBOM signature verification exists(L2, L3) func IsSBOMSignatureCorrect(d sbom.Document, s *scvsScore) bool { return IsSBOMHasSignature(d, s) } +// 2.6 SBOM signature verification is performed(L3) func IsSBOMSignatureVerified(d sbom.Document, s *scvsScore) bool { // Save signature and public key to temporary files signature := d.Signature() @@ -141,8 +145,14 @@ func IsSBOMSignatureVerified(d sbom.Document, s *scvsScore) bool { return verificationResult } +// 2.7 SBOM is timestamped(L1, L2, L3) func IsSBOMTimestamped(d sbom.Document, s *scvsScore) bool { - if d.Spec().GetCreationTimestamp() != "" { + if result := d.Spec().GetCreationTimestamp(); result != "" { + _, err := time.Parse(time.RFC3339, result) + if err != nil { + s.setDesc("SBOM timestamped is incorrect") + return false + } s.setDesc("SBOM is timestamped") return true } @@ -150,14 +160,28 @@ func IsSBOMTimestamped(d sbom.Document, s *scvsScore) bool { return false } +// 2.8 SBOM is analyzed for risk(L1, L2, L3) func IsSBOMAnalyzedForRisk(d sbom.Document, s *scvsScore) bool { return false } // 2.8 -func IsSBOMHasInventoryOfDependencies(d sbom.Document, s *scvsScore) bool { return false } // 2.9 +// 2.9 SBOM contains a complete and accurate inventory of all components the SBOM describes(L1, L2, L3) +func IsSBOMHasInventoryOfDependencies(d sbom.Document, s *scvsScore) bool { + // get primaryComponentID: d.PrimaryComponent().GetID() + // get all dependencies of primary component: loop through all relation and collect all dependencies of primary comp + // now check each dependencies are present in component section: now through each component and check depedncies aare present or not. + return false +} -func IsSBOMInventoryContainsTestComponents(d sbom.Document, s *scvsScore) bool { return false } // 2.10 +// 2,10 SBOM contains an accurate inventory of all test components for the asset or application it describes(L2, L3) +func IsSBOMInventoryContainsTestComponents(_ sbom.Document, s *scvsScore) bool { + // N/A + s.setDesc("Not Supported(N/A)") + return false +} +// 2.11 SBOM contains metadata about the asset or software the SBOM describes(L2, L3) func IsSBOMHasPrimaryComponents(d sbom.Document, s *scvsScore) bool { - // + // get primaryComponentID: d.PrimaryComponent().GetID() + // Update this after NTIA get's merged if d.PrimaryComponent() { s.setDesc("SBOM have primary comp") return true @@ -166,6 +190,7 @@ func IsSBOMHasPrimaryComponents(d sbom.Document, s *scvsScore) bool { return false } +// 2.12 Component identifiers are derived from their native ecosystems (if applicable)(L1, L2, L3) func IsComponentHasIdentityID(d sbom.Document, s *scvsScore) bool { totalComponents := len(d.Components()) if totalComponents == 0 { @@ -174,7 +199,7 @@ func IsComponentHasIdentityID(d sbom.Document, s *scvsScore) bool { } withIdentityID := lo.CountBy(d.Components(), func(c sbom.GetComponent) bool { - return len(c.Purls()) > 0 + return len(c.Purls()) > 0 || len(c.Cpes()) > 0 }) if totalComponents > 0 { @@ -187,6 +212,7 @@ func IsComponentHasIdentityID(d sbom.Document, s *scvsScore) bool { return false } +// 2.13 Component point of origin is identified in a consistent, machine readable format (e.g. PURL)(L3) func IsComponentHasOriginID(d sbom.Document, s *scvsScore) bool { totalComponents := len(d.Components()) if totalComponents == 0 { @@ -208,7 +234,7 @@ func IsComponentHasOriginID(d sbom.Document, s *scvsScore) bool { return false } -// 2.13 +// 2.14 Components defined in SBOM have accurate license information(L1, L2, L3) func IsComponentHasLicenses(d sbom.Document, s *scvsScore) bool { // totalComponents := len(d.Components()) @@ -231,7 +257,7 @@ func IsComponentHasLicenses(d sbom.Document, s *scvsScore) bool { return false } -// 2.14 +// 2.15 Components defined in SBOM have valid SPDX license ID's or expressions (if applicable)(L2, L3) func IsComponentHasVerifiedLicense(d sbom.Document, s *scvsScore) bool { totalComponents := len(d.Components()) if totalComponents == 0 { @@ -239,30 +265,30 @@ func IsComponentHasVerifiedLicense(d sbom.Document, s *scvsScore) bool { return false } - // var countAllValidLicense int - // for _, comp := range d.Components() { - // for _, licen := range comp.Licenses() { - // if licenses.IsValidLicenseID(licen.Name()) { - // countAllValidLicense++ - // } - // } - // } - countAllValidLicense := lo.CountBy(d.Components(), func(comp sbom.GetComponent) bool { - return lo.SomeBy(comp.Licenses(), func(licen licenses.License) bool { - return licenses.IsValidLicenseID(licen.Name()) + totalLicense := lo.FlatMap(d.Components(), func(comp sbom.GetComponent, _ int) []bool { + return lo.Map(comp.Licenses(), func(l licenses.License, _ int) bool { + isValidLicense := licenses.IsValidLicenseID(l.ShortID()) + fmt.Println("isValidLicense: ", isValidLicense) + return isValidLicense }) }) - if totalComponents > 0 { - if countAllValidLicense >= totalComponents { - s.setDesc(fmt.Sprintf("%d/%d comp has valid Licenses", countAllValidLicense, totalComponents)) + withValidLicense := lo.CountBy(totalLicense, func(l bool) bool { + return l + }) + + if totalComponents >= 0 { + if withValidLicense >= totalComponents { + s.setDesc(fmt.Sprintf("%d/%d comp has Licenses", withValidLicense, totalComponents)) return true } } - s.setDesc(fmt.Sprintf("%d/%d comp has valid Licenses", countAllValidLicense, totalComponents)) + + s.setDesc(fmt.Sprintf("%d/%d comp has valid Licenses", withValidLicense, totalComponents)) return false } +// 2.16 Components defined in SBOM have valid copyright statement(L3) func IsComponentHasCopyright(d sbom.Document, s *scvsScore) bool { totalComponents := len(d.Components()) if totalComponents == 0 { @@ -284,9 +310,10 @@ func IsComponentHasCopyright(d sbom.Document, s *scvsScore) bool { return false } -// 2.16 +// 2.17 Components defined in SBOM which have been modified from the original have detailed provenance and pedigree information(L3) func IsComponentContainsModificationChanges(d sbom.Document, s *scvsScore) bool { return false } // 2.17 +// 2.18 Components defined in SBOM have one or more file hashes (SHA-256, SHA-512, etc)(L3) func IsComponentContainsHash(d sbom.Document, s *scvsScore) bool { totalComponents := len(d.Components()) if totalComponents == 0 { @@ -304,4 +331,4 @@ func IsComponentContainsHash(d sbom.Document, s *scvsScore) bool { } s.setDesc(fmt.Sprintf("%d/%d comp has Checksum", withChecksums, totalComponents)) return false -} // 2.18 +} From b3f469235150d08af5f55800fb26aa1361996d16 Mon Sep 17 00:00:00 2001 From: Vivek Kumar Sahu Date: Sat, 7 Sep 2024 20:52:31 +0530 Subject: [PATCH 06/11] tool name and version can be seaprated by empty string too Signed-off-by: Vivek Kumar Sahu --- pkg/licenses/license.go | 1 - pkg/sbom/spdx.go | 10 ++++++++-- pkg/scvs/scvs.go | 18 ++++++++++++++---- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/pkg/licenses/license.go b/pkg/licenses/license.go index 2ac1c84..91061b4 100644 --- a/pkg/licenses/license.go +++ b/pkg/licenses/license.go @@ -101,7 +101,6 @@ func IsValidLicenseID(licenseKey string) bool { return false } - // fmt.Println("licenseKey: ", licenseKey) if licenseKey == "none" || licenseKey == "noassertion" { return false } diff --git a/pkg/sbom/spdx.go b/pkg/sbom/spdx.go index 48054be..3ac3061 100644 --- a/pkg/sbom/spdx.go +++ b/pkg/sbom/spdx.go @@ -365,9 +365,15 @@ func (s *SpdxDoc) parseTool() { // indicate the name and version for that tool extractVersion := func(inputName string) (string, string) { // Split the input string by "-" - parts := strings.Split(inputName, "-") + var parts []string + if strings.Contains(inputName, "-") { + parts = strings.Split(inputName, "-") + } else if strings.Contains(inputName, " ") { + // example: {FOSSA v0.12.0} + parts = strings.Split(inputName, " ") + } - // if there are no "-" its a bad string + // if there are no dash("-") or empty string(" ") b/w name and version, then its a bad string if len(parts) == 1 { return inputName, "" } diff --git a/pkg/scvs/scvs.go b/pkg/scvs/scvs.go index b49c2a7..abe982c 100644 --- a/pkg/scvs/scvs.go +++ b/pkg/scvs/scvs.go @@ -81,7 +81,7 @@ func IsSBOMHasSignature(d sbom.Document, s *scvsScore) bool { } } } else { - fmt.Println("Signature is nil") + s.setDesc("*Not Supported(N/A)") } return false @@ -97,12 +97,14 @@ func IsSBOMSignatureVerified(d sbom.Document, s *scvsScore) bool { // Save signature and public key to temporary files signature := d.Signature() if signature == nil { + s.setDesc("*Not Supported(N/A)") return false } // Use the first signature sig := signature[0] if sig == nil { + s.setDesc("*Not Supported(N/A)") return false } @@ -161,13 +163,18 @@ func IsSBOMTimestamped(d sbom.Document, s *scvsScore) bool { } // 2.8 SBOM is analyzed for risk(L1, L2, L3) -func IsSBOMAnalyzedForRisk(d sbom.Document, s *scvsScore) bool { return false } // 2.8 +func IsSBOMAnalyzedForRisk(d sbom.Document, s *scvsScore) bool { + // N/A + s.setDesc("Not Supported(N/A)") + return false +} // 2.8 // 2.9 SBOM contains a complete and accurate inventory of all components the SBOM describes(L1, L2, L3) func IsSBOMHasInventoryOfDependencies(d sbom.Document, s *scvsScore) bool { // get primaryComponentID: d.PrimaryComponent().GetID() // get all dependencies of primary component: loop through all relation and collect all dependencies of primary comp // now check each dependencies are present in component section: now through each component and check depedncies aare present or not. + s.setDesc("*Not Supported(N/A)") return false } @@ -268,7 +275,6 @@ func IsComponentHasVerifiedLicense(d sbom.Document, s *scvsScore) bool { totalLicense := lo.FlatMap(d.Components(), func(comp sbom.GetComponent, _ int) []bool { return lo.Map(comp.Licenses(), func(l licenses.License, _ int) bool { isValidLicense := licenses.IsValidLicenseID(l.ShortID()) - fmt.Println("isValidLicense: ", isValidLicense) return isValidLicense }) }) @@ -311,7 +317,11 @@ func IsComponentHasCopyright(d sbom.Document, s *scvsScore) bool { } // 2.17 Components defined in SBOM which have been modified from the original have detailed provenance and pedigree information(L3) -func IsComponentContainsModificationChanges(d sbom.Document, s *scvsScore) bool { return false } // 2.17 +func IsComponentContainsModificationChanges(d sbom.Document, s *scvsScore) bool { + // N/A + s.setDesc("Not Supported(N/A)") + return false +} // 2.17 // 2.18 Components defined in SBOM have one or more file hashes (SHA-256, SHA-512, etc)(L3) func IsComponentContainsHash(d sbom.Document, s *scvsScore) bool { From 480e81667a8bd0114a9f4879079269322938a542 Mon Sep 17 00:00:00 2001 From: Vivek Kumar Sahu Date: Mon, 9 Sep 2024 13:55:44 +0530 Subject: [PATCH 07/11] integrated opensca for risk analysis Signed-off-by: Vivek Kumar Sahu --- pkg/scvs/scvs.go | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/pkg/scvs/scvs.go b/pkg/scvs/scvs.go index abe982c..bc7bfe0 100644 --- a/pkg/scvs/scvs.go +++ b/pkg/scvs/scvs.go @@ -15,6 +15,7 @@ package scvs import ( + "bytes" "fmt" "os" "os/exec" @@ -164,10 +165,31 @@ func IsSBOMTimestamped(d sbom.Document, s *scvsScore) bool { // 2.8 SBOM is analyzed for risk(L1, L2, L3) func IsSBOMAnalyzedForRisk(d sbom.Document, s *scvsScore) bool { - // N/A - s.setDesc("Not Supported(N/A)") + // // N/A + // s.setDesc("Not Supported(N/A)") + // return false + + // Run OpenSCA CLI to check for vulnerabilities + cmd := exec.Command("opensca-cli", "scan", "--path", "~/sbom/sbomqs-fossa.spdx.json ") + + var out bytes.Buffer + cmd.Stdout = &out + err := cmd.Run() + if err != nil { + s.setDesc("Error running OpenSCA CLI: " + err.Error()) + return false + } + + // Parse the output to check for vulnerabilities + output := out.String() + if strings.Contains(output, "Vulnerabilities found") { + s.setDesc("Vulnerabilities found in SBOM components") + return true + } + + s.setDesc("No vulnerabilities found") return false -} // 2.8 +} // 2.9 SBOM contains a complete and accurate inventory of all components the SBOM describes(L1, L2, L3) func IsSBOMHasInventoryOfDependencies(d sbom.Document, s *scvsScore) bool { From 49d8c40ac64db5093a9496e555d6ccd3108df54f Mon Sep 17 00:00:00 2001 From: Vivek Kumar Sahu Date: Mon, 30 Sep 2024 18:12:23 +0530 Subject: [PATCH 08/11] fix merging Signed-off-by: Vivek Kumar Sahu --- pkg/scvs/scvs.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/scvs/scvs.go b/pkg/scvs/scvs.go index bc7bfe0..9ade77f 100644 --- a/pkg/scvs/scvs.go +++ b/pkg/scvs/scvs.go @@ -211,7 +211,7 @@ func IsSBOMInventoryContainsTestComponents(_ sbom.Document, s *scvsScore) bool { func IsSBOMHasPrimaryComponents(d sbom.Document, s *scvsScore) bool { // get primaryComponentID: d.PrimaryComponent().GetID() // Update this after NTIA get's merged - if d.PrimaryComponent() { + if d.PrimaryComp().IsPresent() { s.setDesc("SBOM have primary comp") return true } @@ -228,7 +228,7 @@ func IsComponentHasIdentityID(d sbom.Document, s *scvsScore) bool { } withIdentityID := lo.CountBy(d.Components(), func(c sbom.GetComponent) bool { - return len(c.Purls()) > 0 || len(c.Cpes()) > 0 + return len(c.GetPurls()) > 0 || len(c.GetCpes()) > 0 }) if totalComponents > 0 { @@ -250,7 +250,7 @@ func IsComponentHasOriginID(d sbom.Document, s *scvsScore) bool { } withOriginID := lo.CountBy(d.Components(), func(c sbom.GetComponent) bool { - return len(c.Purls()) > 0 + return len(c.GetPurls()) > 0 }) if totalComponents > 0 { From 3dc91843550dfc13f9f31558bbb3cf0e645d8c17 Mon Sep 17 00:00:00 2001 From: Vivek Kumar Sahu Date: Tue, 1 Oct 2024 23:50:38 +0530 Subject: [PATCH 09/11] added scvs tests Signed-off-by: Vivek Kumar Sahu --- pkg/sbom/cdx.go | 3 +- pkg/scvs/features.go | 4 +- pkg/scvs/features_test.go | 379 ++++++++++++++++++++++++++++++++++++++ pkg/scvs/scvs.go | 14 +- pkg/scvs/scvsCriteria.go | 2 +- 5 files changed, 390 insertions(+), 12 deletions(-) create mode 100644 pkg/scvs/features_test.go diff --git a/pkg/sbom/cdx.go b/pkg/sbom/cdx.go index 506a395..b969460 100644 --- a/pkg/sbom/cdx.go +++ b/pkg/sbom/cdx.go @@ -215,7 +215,7 @@ func (c *CdxDoc) parseSpec() { func (c *CdxDoc) parseSignature() { if c.doc.Declarations == nil { - fmt.Println("Declaratic.doc.Declarationsons field is nil ") + fmt.Println("c.doc.Declarations field is nil ") return } @@ -295,6 +295,7 @@ func copyC(cdxc *cydx.Component, c *CdxDoc) *Component { nc.Name = cdxc.Name nc.purpose = string(cdxc.Type) nc.isReqFieldsPresent = c.pkgRequiredFields(cdxc) + nc.CopyRight = cdxc.Copyright ncpe := cpe.NewCPE(cdxc.CPE) if ncpe.Valid() { diff --git a/pkg/scvs/features.go b/pkg/scvs/features.go index 5cc858f..e443517 100644 --- a/pkg/scvs/features.go +++ b/pkg/scvs/features.go @@ -59,7 +59,7 @@ func scvsSBOMAutomationCreationCheck(d sbom.Document, c *scvsCheck) scvsScore { func scvsSBOMUniqIDCheck(d sbom.Document, c *scvsCheck) scvsScore { s := newScoreFromScvsCheck(c) - if IsSBOMHasUniqID(d, s) { + if IsSBOMHasGlobalUniqID(d, s) { s.setL3Score(green + bold + "✓" + reset) s.setL2Score(green + bold + "✓" + reset) s.setL1Score(green + bold + "✓" + reset) @@ -115,7 +115,7 @@ func scvsSBOMSigVerified(d sbom.Document, c *scvsCheck) scvsScore { func scvsSBOMTimestampCheck(d sbom.Document, c *scvsCheck) scvsScore { s := newScoreFromScvsCheck(c) - if IsSBOMTimestamped(d, s) { + if DoesSBOMHasTimestamp(d, s) { s.setL3Score(green + bold + "✓" + reset) s.setL2Score(green + bold + "✓" + reset) s.setL1Score(green + bold + "✓" + reset) diff --git a/pkg/scvs/features_test.go b/pkg/scvs/features_test.go new file mode 100644 index 0000000..4b293a3 --- /dev/null +++ b/pkg/scvs/features_test.go @@ -0,0 +1,379 @@ +package scvs + +import ( + "testing" + + "github.com/interlynk-io/sbomqs/pkg/cpe" + "github.com/interlynk-io/sbomqs/pkg/purl" + "github.com/interlynk-io/sbomqs/pkg/sbom" + "gotest.tools/assert" +) + +func cdxDocWithTool() (d sbom.Document, c *scvsCheck) { + p := scvsCheck{ + Key: "SBOM creation is automated and reproducible", + } + tools := []sbom.GetTool{} + tool := sbom.Tool{} + tool.Name = "sbom-tool" + tool.Version = "9.1.2" + tools = append(tools, tool) + + doc := sbom.CdxDoc{ + CdxTools: tools, + } + return doc, &p +} + +func cdxToolWithoutVersion() (d sbom.Document, c *scvsCheck) { + p := scvsCheck{ + Key: "SBOM creation is automated and reproducible", + } + tools := []sbom.GetTool{} + tool := sbom.Tool{} + tool.Name = "sbom-tool" + tools = append(tools, tool) + + doc := sbom.CdxDoc{ + CdxTools: tools, + } + return doc, &p +} + +func cdxToolWithoutName() (d sbom.Document, c *scvsCheck) { + p := scvsCheck{ + Key: "SBOM creation is automated and reproducible", + } + tools := []sbom.GetTool{} + tool := sbom.Tool{} + tool.Name = "sbom-tool" + tools = append(tools, tool) + + doc := sbom.CdxDoc{ + CdxTools: tools, + } + return doc, &p +} + +type desired struct { + feature string + l1score string + l2score string + l3score string + desc string +} + +func TestSBOMAutomationCreation(t *testing.T) { + testCases := []struct { + name string + actual scvsScore + expected desired + }{ + { + name: "cdxSBOMWithToolNameAndVersion", + actual: scvsSBOMAutomationCreationCheck(cdxDocWithTool()), + expected: desired{ + feature: "SBOM creation is automated and reproducible", + l2score: green + bold + "✓" + reset, + l3score: green + bold + "✓" + reset, + desc: "SBOM creation is automated", + }, + }, + { + name: "cdxSBOMWithToolWithoutVersion", + actual: scvsSBOMAutomationCreationCheck(cdxToolWithoutVersion()), + expected: desired{ + feature: "SBOM creation is automated and reproducible", + // l1score: 10.0, + l2score: red + bold + "✗" + reset, + l3score: red + bold + "✗" + reset, + desc: "SBOM creation is non-automated", + }, + }, + { + name: "cdxSBOMWithToolWithoutName", + actual: scvsSBOMAutomationCreationCheck(cdxToolWithoutName()), + expected: desired{ + feature: "SBOM creation is automated and reproducible", + l2score: red + bold + "✗" + reset, + l3score: red + bold + "✗" + reset, + desc: "SBOM creation is non-automated", + }, + }, + } + for _, test := range testCases { + assert.Equal(t, test.expected.feature, test.actual.feature, "Score mismatch for %s", test.name) + assert.Equal(t, test.expected.l1score, test.actual.l1Score, "Key mismatch for %s", test.name) + assert.Equal(t, test.expected.l2score, test.actual.l2Score, "ID mismatch for %s", test.name) + assert.Equal(t, test.expected.l3score, test.actual.l3Score, "Result mismatch for %s", test.name) + assert.Equal(t, test.expected.desc, test.actual.descr, "Maturity mismatch for %s", test.name) + } +} + +func spdxSbomWithGlobalUniqID() (d sbom.Document, c *scvsCheck) { + p := scvsCheck{ + Key: "Each SBOM has global unique ID", + } + namespace := "https://anchore.com/syft/file/sbomqs-linux-amd64-ef8c4621-f421-44cd-8267-749e6cf75626" + + spec := sbom.NewSpec() + spec.UniqID = namespace + + doc := sbom.SpdxDoc{ + SpdxSpec: spec, + } + return doc, &p +} + +func cdxSbomWithGlobalUniqID() (d sbom.Document, c *scvsCheck) { + p := scvsCheck{ + Key: "Each SBOM has global unique ID", + } + serialNumber := "urn:uuid:59449365-e065-4fbe-aec6-6b2f852e8147" + spec := sbom.NewSpec() + spec.UniqID = serialNumber + + doc := sbom.CdxDoc{ + CdxSpec: spec, + } + return doc, &p +} + +func TestSBOMWithGlobalUniqIDs(t *testing.T) { + testCases := []struct { + name string + actual scvsScore + expected desired + }{ + { + name: "spdxSBOMWithGlobalUniqID", + actual: scvsSBOMUniqIDCheck(spdxSbomWithGlobalUniqID()), + expected: desired{ + feature: "Each SBOM has global unique ID", + l1score: green + bold + "✓" + reset, + l2score: green + bold + "✓" + reset, + l3score: green + bold + "✓" + reset, + desc: "SBOM have global uniq ID", + }, + }, + { + name: "cdxSBOMWithGlobalUniqID", + actual: scvsSBOMUniqIDCheck(cdxSbomWithGlobalUniqID()), + expected: desired{ + feature: "Each SBOM has global unique ID", + l1score: green + bold + "✓" + reset, + l2score: green + bold + "✓" + reset, + l3score: green + bold + "✓" + reset, + desc: "SBOM have global uniq ID", + }, + }, + } + for _, test := range testCases { + assert.Equal(t, test.expected.feature, test.actual.feature, "Score mismatch for %s", test.name) + assert.Equal(t, test.expected.l1score, test.actual.l1Score, "Key mismatch for %s", test.name) + assert.Equal(t, test.expected.l2score, test.actual.l2Score, "ID mismatch for %s", test.name) + assert.Equal(t, test.expected.l3score, test.actual.l3Score, "Result mismatch for %s", test.name) + assert.Equal(t, test.expected.desc, test.actual.descr, "Maturity mismatch for %s", test.name) + } +} + +func sbomWithTimestamp() (d sbom.Document, c *scvsCheck) { + p := scvsCheck{ + Key: "SBOM is timestamped", + } + + s := sbom.NewSpec() + s.CreationTimestamp = "2020-04-13T20:20:39+00:00" + doc := sbom.CdxDoc{ + CdxSpec: s, + } + return doc, &p +} + +func TestSBOMHasTimestamp(t *testing.T) { + testCases := []struct { + name string + actual scvsScore + expected desired + }{ + { + name: "sbomWithTimestamp", + actual: scvsSBOMTimestampCheck(sbomWithTimestamp()), + expected: desired{ + feature: "SBOM is timestamped", + l1score: green + bold + "✓" + reset, + l2score: green + bold + "✓" + reset, + l3score: green + bold + "✓" + reset, + desc: "SBOM is timestamped", + }, + }, + } + for _, test := range testCases { + assert.Equal(t, test.expected.feature, test.actual.feature, "Score mismatch for %s", test.name) + assert.Equal(t, test.expected.l1score, test.actual.l1Score, "Key mismatch for %s", test.name) + assert.Equal(t, test.expected.l2score, test.actual.l2Score, "ID mismatch for %s", test.name) + assert.Equal(t, test.expected.l3score, test.actual.l3Score, "Result mismatch for %s", test.name) + assert.Equal(t, test.expected.desc, test.actual.descr, "Maturity mismatch for %s", test.name) + } +} + +// func sbomWithTimestamp() (d sbom.Document, c *scvsCheck) { +// p := scvsCheck{ +// Key: "SBOM is timestamped", +// } + +// s := sbom.NewSpec() +// s.CreationTimestamp = "2020-04-13T20:20:39+00:00" +// doc := sbom.CdxDoc{ +// CdxSpec: s, +// } +// return doc, &p +// } + +func docWithPrimaryComponent() (d sbom.Document, c *scvsCheck) { + p := scvsCheck{ + Key: "SBOM contains metadata about the asset or software the SBOM describes", + } + + primary := sbom.PrimaryComp{} + primary.Present = true + primary.ID = "git@github.com:interlynk/sbomqs.git" + + doc := sbom.CdxDoc{ + PrimaryComponent: primary, + } + return doc, &p +} + +func TestSBOMWithPrimaryComp(t *testing.T) { + testCases := []struct { + name string + actual scvsScore + expected desired + }{ + { + name: "sbomWithPrimaryComp", + actual: scvsSBOMPrimaryCompCheck(docWithPrimaryComponent()), + expected: desired{ + feature: "SBOM contains metadata about the asset or software the SBOM describes", + l2score: green + bold + "✓" + reset, + l3score: green + bold + "✓" + reset, + desc: "SBOM have primary comp", + }, + }, + } + for _, test := range testCases { + assert.Equal(t, test.expected.feature, test.actual.feature, "Score mismatch for %s", test.name) + // assert.Equal(t, test.expected.l1score, test.actual.l1Score, "Key mismatch for %s", test.name) + assert.Equal(t, test.expected.l2score, test.actual.l2Score, "ID mismatch for %s", test.name) + assert.Equal(t, test.expected.l3score, test.actual.l3Score, "Result mismatch for %s", test.name) + assert.Equal(t, test.expected.desc, test.actual.descr, "Maturity mismatch for %s", test.name) + } +} + +func docWithIdentityCheck() (d sbom.Document, c *scvsCheck) { + p := scvsCheck{ + Key: "SBOM contains metadata about the asset or software the SBOM describes", + } + + primary := sbom.PrimaryComp{} + primary.Present = true + primary.ID = "git@github.com:interlynk/sbomqs.git" + + doc := sbom.CdxDoc{ + PrimaryComponent: primary, + } + return doc, &p +} + +type externalRef struct { + refCategory string + refType string + refLocator string +} + +func docWithCPE() (d sbom.Document, c *scvsCheck) { + p := scvsCheck{ + Key: "Component identifiers are derived from their native ecosystems (if applicable)", + } + + urls := []cpe.CPE{} + comps := []sbom.GetComponent{} + comp := sbom.NewComponent() + + comp.Name = "glibc" + comp.Spdxid = "SPDXRef-git-github.com-glibc-afb1ddc0824ce0052d72ac0d6917f144a1207424" + + ext := externalRef{ + // refCategory: "SECURITY", + // refType: "cpe23Type", + refLocator: "cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*", + } + + prl := cpe.NewCPE(ext.refLocator) + urls = append(urls, prl) + comp.Cpes = urls + + comps = append(comps, comp) + + doc := sbom.SpdxDoc{ + Comps: comps, + } + return doc, &p +} + +func docWithPurl() (d sbom.Document, c *scvsCheck) { + p := scvsCheck{ + Key: "Component point of origin is identified in a consistent, machine readable format (e.g. PURL)", + } + comps := []sbom.GetComponent{} + + comp := sbom.NewComponent() + comp.Name = "acme" + PackageURL := "pkg:npm/acme/component@1.0.0" + + prl := purl.NewPURL(PackageURL) + comp.Purls = []purl.PURL{prl} + comps = append(comps, comp) + + doc := sbom.CdxDoc{ + Comps: comps, + } + return doc, &p +} + +func TestComponentWithID(t *testing.T) { + testCases := []struct { + name string + actual scvsScore + expected desired + }{ + { + name: "sbomWithCPE", + actual: scvsCompHasIdentityIDCheck(docWithCPE()), + expected: desired{ + feature: "Component identifiers are derived from their native ecosystems (if applicable)", + l1score: green + bold + "✓" + reset, + l2score: green + bold + "✓" + reset, + l3score: green + bold + "✓" + reset, + desc: "1/1 comp have Identity ID's", + }, + }, + { + name: "sbomWithPurl", + actual: scvsCompHasOriginIDCheck(docWithPurl()), + expected: desired{ + feature: "Component point of origin is identified in a consistent, machine readable format (e.g. PURL)", + l3score: green + bold + "✓" + reset, + desc: "1/1 comp have Origin ID's", + }, + }, + } + for _, test := range testCases { + assert.Equal(t, test.expected.feature, test.actual.feature, "Score mismatch for %s", test.name) + assert.Equal(t, test.expected.l1score, test.actual.l1Score, "Key mismatch for %s", test.name) + assert.Equal(t, test.expected.l2score, test.actual.l2Score, "ID mismatch for %s", test.name) + assert.Equal(t, test.expected.l3score, test.actual.l3Score, "Result mismatch for %s", test.name) + assert.Equal(t, test.expected.desc, test.actual.descr, "Maturity mismatch for %s", test.name) + } +} diff --git a/pkg/scvs/scvs.go b/pkg/scvs/scvs.go index 9ade77f..8410018 100644 --- a/pkg/scvs/scvs.go +++ b/pkg/scvs/scvs.go @@ -43,30 +43,28 @@ func IsSBOMMachineReadable(d sbom.Document, s *scvsScore) bool { // 2.3 SBOM creation is automated and reproducible(L2, L3) func IsSBOMCreationAutomated(d sbom.Document, s *scvsScore) bool { - noOfTools := len(d.Tools()) if tools := d.Tools(); tools != nil { for _, tool := range tools { name := tool.GetName() version := tool.GetVersion() if name != "" && version != "" { - s.setDesc(fmt.Sprintf("SBOM has %d authors", noOfTools)) + s.setDesc(fmt.Sprintf("SBOM creation is automated")) return true } } } - - s.setDesc(fmt.Sprintf("SBOM has %d authors", noOfTools)) + s.setDesc(fmt.Sprintf("SBOM creation is non-automated")) return false } // 2.3 Each SBOM has a unique identifier(L1, L2, L3) -func IsSBOMHasUniqID(d sbom.Document, s *scvsScore) bool { +func IsSBOMHasGlobalUniqID(d sbom.Document, s *scvsScore) bool { if ns := d.Spec().GetUniqID(); ns != "" { - s.setDesc("SBOM has uniq ID") + s.setDesc("SBOM have global uniq ID") return true } - s.setDesc("SBOM doesn't has uniq ID") + s.setDesc("SBOM doesn't have global uniq ID") return false } @@ -149,7 +147,7 @@ func IsSBOMSignatureVerified(d sbom.Document, s *scvsScore) bool { } // 2.7 SBOM is timestamped(L1, L2, L3) -func IsSBOMTimestamped(d sbom.Document, s *scvsScore) bool { +func DoesSBOMHasTimestamp(d sbom.Document, s *scvsScore) bool { if result := d.Spec().GetCreationTimestamp(); result != "" { _, err := time.Parse(time.RFC3339, result) if err != nil { diff --git a/pkg/scvs/scvsCriteria.go b/pkg/scvs/scvsCriteria.go index 26fc4d2..9fa6a74 100644 --- a/pkg/scvs/scvsCriteria.go +++ b/pkg/scvs/scvsCriteria.go @@ -25,7 +25,7 @@ var scvsChecks = []scvsCheck{ // scvs {"A structured, machine readable software bill of materials (SBOM) format is present", scvsSBOMMachineReadableCheck}, {"SBOM creation is automated and reproducible", scvsSBOMAutomationCreationCheck}, - {"Each SBOM has a unique identifier", scvsSBOMUniqIDCheck}, + {"Each SBOM has global unique ID", scvsSBOMUniqIDCheck}, {"SBOM has been signed by publisher, supplier, or certifying authority", scvsSBOMSigcheck}, {"SBOM signature verification exists", scvsSBOMSigCorrectnessCheck}, {"SBOM signature verification is performed", scvsSBOMSigVerified}, From 8c694184777301f9447946986f40aa787493e6d7 Mon Sep 17 00:00:00 2001 From: Vivek Kumar Sahu Date: Wed, 2 Oct 2024 00:14:32 +0530 Subject: [PATCH 10/11] add checksum test Signed-off-by: Vivek Kumar Sahu --- pkg/scvs/features_test.go | 90 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 88 insertions(+), 2 deletions(-) diff --git a/pkg/scvs/features_test.go b/pkg/scvs/features_test.go index 4b293a3..6d078cc 100644 --- a/pkg/scvs/features_test.go +++ b/pkg/scvs/features_test.go @@ -371,9 +371,95 @@ func TestComponentWithID(t *testing.T) { } for _, test := range testCases { assert.Equal(t, test.expected.feature, test.actual.feature, "Score mismatch for %s", test.name) - assert.Equal(t, test.expected.l1score, test.actual.l1Score, "Key mismatch for %s", test.name) - assert.Equal(t, test.expected.l2score, test.actual.l2Score, "ID mismatch for %s", test.name) assert.Equal(t, test.expected.l3score, test.actual.l3Score, "Result mismatch for %s", test.name) assert.Equal(t, test.expected.desc, test.actual.descr, "Maturity mismatch for %s", test.name) } } + +func compWithCopyright() (d sbom.Document, c *scvsCheck) { + p := scvsCheck{ + Key: "Components defined in SBOM have valid copyright statements", + } + comps := []sbom.GetComponent{} + copyright := "2013-2023 The Cobra Authors" + comp := sbom.NewComponent() + comp.CopyRight = copyright + comps = append(comps, comp) + + doc := sbom.CdxDoc{ + Comps: comps, + } + return doc, &p +} + +func TestComponentWithCopyright(t *testing.T) { + testCases := []struct { + name string + actual scvsScore + expected desired + }{ + { + name: "compWithCopyright", + actual: scvsCompHasCopyright(compWithCopyright()), + expected: desired{ + feature: "Components defined in SBOM have valid copyright statements", + l3score: green + bold + "✓" + reset, + desc: "1/1 comp has Copyright", + }, + }, + } + for _, test := range testCases { + assert.Equal(t, test.expected.feature, test.actual.feature, "Score mismatch for %s", test.name) + assert.Equal(t, test.expected.l3score, test.actual.l3Score, "Result mismatch for %s", test.name) + assert.Equal(t, test.expected.desc, test.actual.descr, "Description mismatch for %s", test.name) + + } +} + +func compWithHigherChecksum() (d sbom.Document, c *scvsCheck) { + p := scvsCheck{ + Key: "Components defined in SBOM have one or more file hashes (SHA-256, SHA-512, etc)", + } + comps := []sbom.GetComponent{} + chks := []sbom.GetChecksum{} + + ck := sbom.Checksum{} + ck.Alg = "SHA256" + ck.Content = "11b6d3ee554eedf79299905a98f9b9a04e498210b59f15094c916c91d150efcd" + + chks = append(chks, ck) + + comp := sbom.Component{ + Checksums: chks, + } + comps = append(comps, comp) + + doc := sbom.SpdxDoc{ + Comps: comps, + } + return doc, &p +} + +func TestComponentWithHash(t *testing.T) { + testCases := []struct { + name string + actual scvsScore + expected desired + }{ + { + name: "compWithChecksum", + actual: scvsCompHashCheck(compWithHigherChecksum()), + expected: desired{ + feature: "Components defined in SBOM have one or more file hashes (SHA-256, SHA-512, etc)", + l3score: green + bold + "✓" + reset, + desc: "1/1 comp has Checksum", + }, + }, + } + for _, test := range testCases { + assert.Equal(t, test.expected.feature, test.actual.feature, "Score mismatch for %s", test.name) + assert.Equal(t, test.expected.l3score, test.actual.l3Score, "Result mismatch for %s", test.name) + assert.Equal(t, test.expected.desc, test.actual.descr, "Description mismatch for %s", test.name) + + } +} From 918ac5b6f7f83c3537fb42da260ca733f8e3cda5 Mon Sep 17 00:00:00 2001 From: Vivek Kumar Sahu Date: Fri, 4 Oct 2024 00:49:36 +0530 Subject: [PATCH 11/11] implement pedigree verification Signed-off-by: Vivek Kumar Sahu --- pkg/hshgrity/common.go | 18 +++++ pkg/hshgrity/utils.go | 149 +++++++++++++++++++++++++++++++++++++ pkg/sbom/cdx.go | 39 ++++++++++ pkg/sbom/commit.go | 52 +++++++++++++ pkg/sbom/commitAuthor.go | 39 ++++++++++ pkg/sbom/commitCommiter.go | 39 ++++++++++ pkg/sbom/component.go | 6 ++ pkg/sbom/pedigree.go | 27 +++++++ pkg/scvs/features.go | 2 +- pkg/scvs/scvs.go | 58 ++++++++++++++- 10 files changed, 427 insertions(+), 2 deletions(-) create mode 100644 pkg/hshgrity/common.go create mode 100644 pkg/hshgrity/utils.go create mode 100644 pkg/sbom/commit.go create mode 100644 pkg/sbom/commitAuthor.go create mode 100644 pkg/sbom/commitCommiter.go create mode 100644 pkg/sbom/pedigree.go diff --git a/pkg/hshgrity/common.go b/pkg/hshgrity/common.go new file mode 100644 index 0000000..0116d54 --- /dev/null +++ b/pkg/hshgrity/common.go @@ -0,0 +1,18 @@ +package hshgrity + +// type GitHubCommit struct { +// SHA string `json:"sha"` +// Commit CommitDetails `json:"commit"` +// } + +// type CommitDetails struct { +// Author sbom.CommitAuthor `json:"author"` +// Committer sbom.CommitCommitter `json:"committer"` +// Message string `json:"message"` +// } + +// type GitHubAuthor struct { +// Name string `json:"name"` +// Email string `json:"email"` +// Date string `json:"date"` +// } diff --git a/pkg/hshgrity/utils.go b/pkg/hshgrity/utils.go new file mode 100644 index 0000000..4451302 --- /dev/null +++ b/pkg/hshgrity/utils.go @@ -0,0 +1,149 @@ +package hshgrity + +import ( + "bufio" + "encoding/json" + "fmt" + "net/http" + "strings" + + "github.com/interlynk-io/sbomqs/pkg/sbom" +) + +// GetCalculatedHash constructs the URL for the checksum file and retrieves the hash +func GetCalculatedHash(purl string) ([]string, error) { + // Extract information from purl + purlParts := strings.Split(purl, "/") + if len(purlParts) < 4 { + return nil, fmt.Errorf("invalid purl format") + } + fmt.Println("purlParts: ", purlParts) + + typeParts := strings.Split(purlParts[0], ":") + fmt.Println("repoParts: ", typeParts) + if len(typeParts) < 2 { + return nil, fmt.Errorf("invalid purl format") + } + org := purlParts[2] + // repo := repoParts[2] + + repoParts := strings.Split(purlParts[3], "@") + if len(repoParts) < 2 { + return nil, fmt.Errorf("invalid purl format") + } + repo := repoParts[0] + version := repoParts[1] + + // Remove the 'v' prefix from the version if it exists + if strings.HasPrefix(version, "v") { + version = version[1:] + } + + // Construct the URL for the checksum file + versionedChecksumURL := fmt.Sprintf("https://github.com/%s/%s/releases/download/v%s/%s_%s_checksums.txt", org, repo, version, repo, version) + fmt.Println("versionedChecksumURL : ", versionedChecksumURL) + + // Attempt to download the versioned checksum file + hashes, err := downloadChecksumFile(versionedChecksumURL) + if err == nil { + return hashes, nil + } + + // If the versioned checksum file is not found, construct the URL for the general checksum file + generalChecksumURL := fmt.Sprintf("https://github.com/%s/%s/releases/download/v%s/%s_checksums.txt", org, repo, version, repo) + fmt.Println("generalChecksumURL: ", generalChecksumURL) + + // Attempt to download the general checksum file + hashes, err = downloadChecksumFile(generalChecksumURL) + if err != nil { + return nil, fmt.Errorf("error downloading checksum file: %v", err) + } + + return hashes, nil +} + +// downloadChecksumFile downloads the checksum file from the given URL and extracts all hashes +func downloadChecksumFile(url string) ([]string, error) { + resp, err := http.Get(url) + if err != nil { + return nil, fmt.Errorf("error downloading checksum file: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("checksum file not found at URL: %s", url) + } + + var hashes []string + scanner := bufio.NewScanner(resp.Body) + for scanner.Scan() { + line := scanner.Text() + parts := strings.Fields(line) + if len(parts) > 0 { + hashes = append(hashes, parts[0]) + } + } + fmt.Println("hashes: ", hashes) + + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("error reading checksum file: %v", err) + } + + return hashes, nil +} + +func FetchCommitDetails(commitURL string) (*sbom.Commit, error) { + resp, err := http.Get(commitURL) + if err != nil { + return nil, fmt.Errorf("error fetching commit details: %v", err) + } + fmt.Println("resp.Body: ", resp.Body) + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("commit not found at URL: %s", commitURL) + } + + var commit sbom.Commit + if err := json.NewDecoder(resp.Body).Decode(&commit); err != nil { + return nil, fmt.Errorf("error decoding commit details: %v", err) + } + + return &commit, nil +} + +func VerifyCommitDetails(sbomCommit sbom.GetCommit, githubCommit sbom.GetCommit) bool { + // fmt.Println("githubCommit.GetUID(): ", githubCommit.GetUID()) + // fmt.Println("sbomCommit.GetUID(): ", sbomCommit.GetUID()) + + // fmt.Println("sbomCommit.GetUID(): ", sbomCommit.GetUID()) + // fmt.Println("sbomCommit.GetUID(): ", sbomCommit.GetUID()) + + // fmt.Println("sbomCommit.GetUID(): ", sbomCommit.GetUID()) + // fmt.Println("sbomCommit.GetUID(): ", sbomCommit.GetUID()) + + if sbomCommit.GetUID() != githubCommit.GetUID() { + fmt.Println("false1") + return false + } + fmt.Println("sbomCommit.GetComitAuthor().GetName() : ", sbomCommit.GetComitAuthor().GetName()) + fmt.Println("githubCommit.GetComitAuthor().GetName() : ", githubCommit.GetComitAuthor().GetName()) + + fmt.Println("sbomCommit.GetComitAuthor().GetEmail() : ", sbomCommit.GetComitAuthor().GetEmail()) + fmt.Println("githubCommit.GetComitAuthor().GetEmail() : ", githubCommit.GetComitAuthor().GetEmail()) + + fmt.Println("sbomCommit.GetComitAuthor().GetTimestamp() : ", sbomCommit.GetComitAuthor().GetTimestamp()) + fmt.Println("githubCommit.GetComitAuthor().GetTimestamp() : ", githubCommit.GetComitAuthor().GetTimestamp()) + + if sbomCommit.GetComitAuthor().GetName() != githubCommit.GetComitAuthor().GetName() || sbomCommit.GetComitAuthor().GetEmail() != githubCommit.GetComitAuthor().GetEmail() || sbomCommit.GetComitAuthor().GetTimestamp() != githubCommit.GetComitAuthor().GetTimestamp() { + fmt.Println("false2") + + return false + } + if sbomCommit.GetComitComiter().GetName() != githubCommit.GetComitComiter().GetName() || sbomCommit.GetComitComiter().GetEmail() != githubCommit.GetComitComiter().GetEmail() || sbomCommit.GetComitComiter().GetTimestamp() != githubCommit.GetComitComiter().GetTimestamp() { + fmt.Println("false3") + + return false + } + return true +} diff --git a/pkg/sbom/cdx.go b/pkg/sbom/cdx.go index b969460..2a9a8df 100644 --- a/pkg/sbom/cdx.go +++ b/pkg/sbom/cdx.go @@ -250,6 +250,44 @@ func (c *CdxDoc) parseSignature() { } } +func (c *CdxDoc) parsePedigree(comp *cydx.Component) Pedigree { + pedigree := Pedigree{} + + if comp.Pedigree == nil { + return pedigree + } + + if comp.Pedigree.Commits == nil { + return pedigree + } + commits := []GetCommit{} + + for _, cmt := range *comp.Pedigree.Commits { + commit := Commit{} + commit.Uid = cmt.UID + commit.Url = cmt.URL + commit.Message = cmt.Message + + author := CommitAuthor{} + author.Name = cmt.Author.Name + author.Email = cmt.Author.Email + author.Timestamp = cmt.Author.Timestamp + commit.ComitAuthor = author + + committer := CommitCommitter{} + committer.Name = cmt.Committer.Name + committer.Email = cmt.Committer.Email + committer.Timestamp = cmt.Committer.Timestamp + commit.ComitComiter = committer + + commits = append(commits, commit) + + } + pedigree.Commits = commits + + return pedigree +} + func (c *CdxDoc) requiredFields() bool { if c.doc == nil { c.addToLogs("cdx doc is not parsable") @@ -340,6 +378,7 @@ func copyC(cdxc *cydx.Component, c *CdxDoc) *Component { if cdxc.BOMRef == c.PrimaryComponent.ID { nc.isPrimary = true } + nc.Pedigrees = c.parsePedigree(cdxc) nc.ID = cdxc.BOMRef return nc diff --git a/pkg/sbom/commit.go b/pkg/sbom/commit.go new file mode 100644 index 0000000..f5f8cb9 --- /dev/null +++ b/pkg/sbom/commit.go @@ -0,0 +1,52 @@ +// Copyright 2024 Interlynk.io +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sbom + +type GetCommit interface { + GetUID() string + GetURL() string + GetComitAuthor() CommitAuthor + GetComitComiter() CommitCommitter + GetMessage() string +} + +// nolint +type Commit struct { + Uid string `json:"sha"` + Url string `json:"url"` + ComitAuthor CommitAuthor `json:"author"` + ComitComiter CommitCommitter `json:"committer"` + Message string `json:"message"` +} + +func (c Commit) GetUID() string { + return c.Uid +} + +func (c Commit) GetURL() string { + return c.Url +} + +func (c Commit) GetComitAuthor() CommitAuthor { + return c.ComitAuthor +} + +func (c Commit) GetComitComiter() CommitCommitter { + return c.ComitComiter +} + +func (c Commit) GetMessage() string { + return c.Message +} diff --git a/pkg/sbom/commitAuthor.go b/pkg/sbom/commitAuthor.go new file mode 100644 index 0000000..417030b --- /dev/null +++ b/pkg/sbom/commitAuthor.go @@ -0,0 +1,39 @@ +// Copyright 2024 Interlynk.io +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sbom + +type GetCommitAuthor interface { + GetName() string + GetEmail() string + GetTimestamp() string +} + +type CommitAuthor struct { + Name string `json:"name"` + Email string `json:"email"` + Timestamp string `json:"date"` +} + +func (cc CommitAuthor) GetName() string { + return cc.Name +} + +func (cc CommitAuthor) GetEmail() string { + return cc.Email +} + +func (cc CommitAuthor) GetTimestamp() string { + return cc.Timestamp +} diff --git a/pkg/sbom/commitCommiter.go b/pkg/sbom/commitCommiter.go new file mode 100644 index 0000000..5fad166 --- /dev/null +++ b/pkg/sbom/commitCommiter.go @@ -0,0 +1,39 @@ +// Copyright 2024 Interlynk.io +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sbom + +type GetCommitCommitter interface { + GetName() string + GetEmail() string + GetTimestamp() string +} + +type CommitCommitter struct { + Timestamp string `json:"date"` + Name string `json:"name"` + Email string `json:"email"` +} + +func (cc CommitCommitter) GetName() string { + return cc.Name +} + +func (cc CommitCommitter) GetEmail() string { + return cc.Email +} + +func (cc CommitCommitter) GetTimestamp() string { + return cc.Timestamp +} diff --git a/pkg/sbom/component.go b/pkg/sbom/component.go index 02fae3b..ba1586f 100644 --- a/pkg/sbom/component.go +++ b/pkg/sbom/component.go @@ -47,6 +47,7 @@ type GetComponent interface { GetPackageLicenseConcluded() string ExternalReferences() []GetExternalReference GetComposition(string) string + GetPedigrees() GetPedigree } type Component struct { @@ -75,6 +76,7 @@ type Component struct { PackageLicenseDeclared string ExternalRefs []GetExternalReference composition map[string]string + Pedigrees Pedigree } func NewComponent() *Component { @@ -180,3 +182,7 @@ func (c Component) ExternalReferences() []GetExternalReference { func (c Component) GetComposition(componentID string) string { return c.composition[componentID] } + +func (c Component) GetPedigrees() GetPedigree { + return c.Pedigrees +} diff --git a/pkg/sbom/pedigree.go b/pkg/sbom/pedigree.go new file mode 100644 index 0000000..155d7c3 --- /dev/null +++ b/pkg/sbom/pedigree.go @@ -0,0 +1,27 @@ +// Copyright 2024 Interlynk.io +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sbom + +type GetPedigree interface { + GetCommits() []GetCommit +} + +type Pedigree struct { + Commits []GetCommit +} + +func (p Pedigree) GetCommits() []GetCommit { + return p.Commits +} diff --git a/pkg/scvs/features.go b/pkg/scvs/features.go index e443517..0ea7647 100644 --- a/pkg/scvs/features.go +++ b/pkg/scvs/features.go @@ -261,7 +261,7 @@ func scvsCompHasCopyright(d sbom.Document, c *scvsCheck) scvsScore { func scvsCompHasModificationCheck(d sbom.Document, c *scvsCheck) scvsScore { s := newScoreFromScvsCheck(c) - if IsComponentContainsModificationChanges(d, s) { + if IsComponentModified(d, s) { s.setL3Score(green + bold + "✓" + reset) } else { s.setL3Score(red + bold + "✗" + reset) diff --git a/pkg/scvs/scvs.go b/pkg/scvs/scvs.go index 8410018..e39f0b4 100644 --- a/pkg/scvs/scvs.go +++ b/pkg/scvs/scvs.go @@ -22,6 +22,7 @@ import ( "strings" "time" + "github.com/interlynk-io/sbomqs/pkg/hshgrity" "github.com/interlynk-io/sbomqs/pkg/licenses" "github.com/interlynk-io/sbomqs/pkg/sbom" "github.com/samber/lo" @@ -337,7 +338,62 @@ func IsComponentHasCopyright(d sbom.Document, s *scvsScore) bool { } // 2.17 Components defined in SBOM which have been modified from the original have detailed provenance and pedigree information(L3) -func IsComponentContainsModificationChanges(d sbom.Document, s *scvsScore) bool { +func IsComponentModified(d sbom.Document, s *scvsScore) bool { + for _, comp := range d.Components() { + // var purl string + var providedHash string + + for _, c := range comp.GetChecksums() { + providedHash = c.GetContent() + } + firstPurl := "pkg:pkg.go.dev/github.com/interlynk-io/sbomqs@v0.1.9" + + calculatedHashes, err := hshgrity.GetCalculatedHash(firstPurl) + if err != nil { + fmt.Println("Error getting calculated hashes:", err) + return false + } + + // Check if the provided hash is in the list of calculated hashes + hashFound := false + for _, hash := range calculatedHashes { + if providedHash == hash { + hashFound = true + // break + } + } + + if hashFound { + fmt.Println("The provided hash is present in the checksum file. The component has not been modified.") + return true + } else { + fmt.Println("The provided hash is not present in the checksum file. The component may have been modified.") + for _, commit := range comp.GetPedigrees().GetCommits() { + commitURL := commit.GetURL() + + fmt.Println("commitURL: ", commitURL) + githubCommit, err := hshgrity.FetchCommitDetails(commitURL) + if err != nil { + fmt.Println("Error fetching commit details:", err) + continue + } + fmt.Println("githubCommit: ", githubCommit) + + if hshgrity.VerifyCommitDetails(commit, githubCommit) { + fmt.Println("Commit verified successfully:") + fmt.Printf("Commit ID: %s\n", commit.GetUID()) + fmt.Printf("Author: %s <%s>\n", commit.GetComitAuthor().GetName(), commit.GetComitAuthor().GetEmail()) + fmt.Printf("Committer: %s <%s>\n", commit.GetComitComiter().GetName(), commit.GetComitComiter().GetEmail()) + fmt.Printf("Message: %s\n", commit.GetMessage()) + } else { + fmt.Println("Commit verification failed for commit ID:", commit.GetUID()) + } + + } + } + + } + // N/A s.setDesc("Not Supported(N/A)") return false