diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..3ee32c1 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,25 @@ +name: CI + +on: + pull_request: + branches: + - main + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: '>=1.20' + + - name: Install dependencies + run: go mod download + + - name: Run tests + run: go test ./... -v diff --git a/Compliance.md b/Compliance.md index ad95c17..51c491b 100644 --- a/Compliance.md +++ b/Compliance.md @@ -66,3 +66,21 @@ The [OpenChain Telco](https://github.com/OpenChain-Project/Reference-Material/bl | Timing of SBOM delivery | 3.6 | `SBOM delivery time` | delivery time | | | Method of SBOM delivery | 3.7 | `SBOM delivery method` | delivery method | | | SBOM Scope | 3.8 | `SBOM scope` | sbom scope | | + +## NTIA minimum elements: SBOM Requirements for NTIA + +The [NTIA](https://www.bsi.bund.de/SharedDocs/Downloads/EN/BSI/Publications/TechGuidelines/TR03183/BSI-TR-03183-2.pdf) specifies mandatory properties for an SBOM. Below is how we have derived all the values. + +| NTIA minimum elements | Section ID | NTIA Fields | CycloneDX |SPDX(2.3) | Notes | +| :--- | :--- |:--- | :--- | :--- | :--- | +| Automation Support | 1.1 | `Machine Readable Format` | BomFormat & data forrmat | SPDXversion & data forrmat | optional | +| SBOM Data Fields | 2.1 | `SBOM Authors` | metadata->authors, metadata->supplier | creator->Person, creator->organization or creator->tool | Mandatory | +| | 2.2 | `SBOM Timestamp` | metadata->timestamp | created | Mandatory | +| | 2.3 | `SBOM Dependencies` | dependencies | relationships | Mandatory(number of dependencies primary comp have) | +| Package Data Fields | 2.4 | `Component Name` | component->name | package->name | Mandatory | +| | 2.3 | `Component Dependencies` | dependencies | relationships | Optional(Component to component dependencies) | +| | 2.6 | `Component Supplier Name` | component->supplier | packageSupplier, packageOriginator | Mandatory | +| | 2.7 | `Component Version` | component->version | package->version | Mandatory | +| | 2.8 | `Component with Uniq IDs` | component->cpe, component->purl | externalRef->cpe, externalRef->purl | Mandatory | +| Practices and Processes | 3.1 | `Depth` | dependencies, compositions | relationships | optional | +| | 3.2 | `Known Unknowns` | | | optional | diff --git a/cmd/compliance.go b/cmd/compliance.go index ee8eed6..6c6f8d0 100644 --- a/cmd/compliance.go +++ b/cmd/compliance.go @@ -71,6 +71,7 @@ func setupEngineParams(cmd *cobra.Command, args []string) *engine.Params { engParams.Detailed, _ = cmd.Flags().GetBool("detailed") engParams.JSON, _ = cmd.Flags().GetBool("json") + engParams.Ntia, _ = cmd.Flags().GetBool("ntia") // engParams.Ntia, _ = cmd.Flags().GetBool("ntia") engParams.Bsi, _ = cmd.Flags().GetBool("bsi") engParams.Oct, _ = cmd.Flags().GetBool("oct") @@ -96,7 +97,7 @@ func init() { complianceCmd.MarkFlagsMutuallyExclusive("json", "basic", "detailed") // Standards control - // complianceCmd.Flags().BoolP("ntia", "n", false, "check for NTIA minimum elements compliance") + complianceCmd.Flags().BoolP("ntia", "n", false, "check for NTIA minimum elements compliance") complianceCmd.Flags().BoolP("bsi", "c", false, "BSI TR-03183-2 v1.1 compliance") // complianceCmd.MarkFlagsMutuallyExclusive("ntia", "cra") complianceCmd.Flags().BoolP("oct", "t", false, "OpenChainTelco compliance") diff --git a/pkg/compliance/bsi.go b/pkg/compliance/bsi.go index b3f76b5..602430d 100644 --- a/pkg/compliance/bsi.go +++ b/pkg/compliance/bsi.go @@ -16,6 +16,7 @@ package compliance import ( "context" + "fmt" "strings" "github.com/interlynk-io/sbomqs/pkg/logger" @@ -67,6 +68,7 @@ const ( PACK_COPYRIGHT COMP_DEPTH SBOM_MACHINE_FORMAT + SBOM_DEPENDENCY SBOM_HUMAN_FORMAT SBOM_BUILD_INFO SBOM_DELIVERY_TIME @@ -160,31 +162,16 @@ func bsiBuildPhase(doc sbom.Document) *record { } func bsiSbomDepth(doc sbom.Document) *record { - if !doc.PrimaryComponent() { - return newRecordStmt(SBOM_DEPTH, "doc", "no-primary", 0.0) - } - - if len(doc.Relations()) == 0 { - return newRecordStmt(SBOM_DEPTH, "doc", "no-relationships", 0.0) - } - - primary, _ := lo.Find(doc.Components(), func(c sbom.GetComponent) bool { - return c.IsPrimaryComponent() - }) - - if !primary.HasRelationShips() { - return newRecordStmt(SBOM_DEPTH, "doc", "no-primary-relationships", 0.0) - } - - if primary.RelationShipState() == "complete" { - return newRecordStmt(SBOM_DEPTH, "doc", "complete", 10.0) - } + result, score := "", 0.0 + // for doc.Components() + totalDependencies := doc.PrimaryComp().GetTotalNoOfDependencies() - if primary.HasRelationShips() { - return newRecordStmt(SBOM_DEPTH, "doc", "unattested-has-relationships", 5.0) + if totalDependencies > 0 { + score = 10.0 } + result = fmt.Sprintf("doc has %d dependencies", totalDependencies) - return newRecordStmt(SBOM_DEPTH, "doc", "non-compliant", 0.0) + return newRecordStmt(SBOM_DEPTH, "doc", result, score) } func bsiCreator(doc sbom.Document) *record { @@ -192,8 +179,8 @@ func bsiCreator(doc sbom.Document) *record { score := 0.0 for _, author := range doc.Authors() { - if author.Email() != "" { - result = author.Email() + if author.GetEmail() != "" { + result = author.GetEmail() score = 10.0 break } @@ -305,13 +292,17 @@ func bsiComponents(doc sbom.Document) []*record { records := append(records, newRecordStmt(SBOM_COMPONENTS, "doc", "", 0.0)) return records } + // map package ID to Package Name + for _, component := range doc.Components() { + CompIDWithName[component.GetID()] = component.GetName() + } for _, component := range doc.Components() { records = append(records, bsiComponentCreator(component)) records = append(records, bsiComponentName(component)) records = append(records, bsiComponentVersion(component)) records = append(records, bsiComponentLicense(component)) - records = append(records, bsiComponentDepth(component)) + records = append(records, bsiComponentDepth(doc, component)) records = append(records, bsiComponentHash(component)) records = append(records, bsiComponentSourceCodeURL(component)) records = append(records, bsiComponentDownloadURL(component)) @@ -324,20 +315,36 @@ func bsiComponents(doc sbom.Document) []*record { return records } -func bsiComponentDepth(component sbom.GetComponent) *record { - if !component.HasRelationShips() { - return newRecordStmt(COMP_DEPTH, component.GetID(), "no-relationships", 0.0) +func bsiComponentDepth(doc sbom.Document, component sbom.GetComponent) *record { + result, score := "", 0.0 + var fResults []string + + dependencies := doc.GetRelationships(component.GetID()) + if dependencies == nil { + return newRecordStmt(COMP_DEPTH, component.GetName(), "no-relationships", 0.0) } - if component.RelationShipState() == "complete" { - return newRecordStmt(COMP_DEPTH, component.GetID(), "complete", 10.0) + for _, d := range dependencies { + state := component.GetComposition(d) + if state == "complete" { + componentName := extractName(d) + fResults = append(fResults, componentName) + score = 10.0 + } else { + componentName := extractName(d) + // state := "(unattested-has-relationships)" + fResults = append(fResults, componentName) + score = 5.0 + } } - if component.HasRelationShips() { - return newRecordStmt(COMP_DEPTH, component.GetID(), "unattested-has-relationships", 5.0) + if fResults != nil { + result = strings.Join(fResults, ", ") + } else { + result += "no-relationships" } - return newRecordStmt(COMP_DEPTH, component.GetID(), "non-compliant", 0.0) + return newRecordStmt(COMP_DEPTH, component.GetName(), result, score) } func bsiComponentLicense(component sbom.GetComponent) *record { @@ -345,8 +352,7 @@ func bsiComponentLicense(component sbom.GetComponent) *record { score := 0.0 if len(licenses) == 0 { - // fmt.Printf("component %s : %s has no licenses\n", component.Name(), component.Version()) - return newRecordStmt(COMP_LICENSE, component.GetID(), "not-compliant", score) + return newRecordStmt(COMP_LICENSE, component.GetName(), "not-compliant", score) } var spdx, aboutcode, custom int @@ -372,15 +378,12 @@ func bsiComponentLicense(component sbom.GetComponent) *record { total := spdx + aboutcode + custom - // fmt.Printf("component %s : %s has (total)%d = (all)%d licenses\n", component.Name(), component.Version(), total, len(licenses)) - // fmt.Printf("%+v\n", licenses) - if total != len(licenses) { score = 0.0 - return newRecordStmt(COMP_LICENSE, component.GetID(), "not-compliant", score) + return newRecordStmt(COMP_LICENSE, component.GetName(), "not-compliant", score) } - return newRecordStmt(COMP_LICENSE, component.GetID(), "compliant", 10.0) + return newRecordStmt(COMP_LICENSE, component.GetName(), "compliant", 10.0) } func bsiComponentSourceHash(component sbom.GetComponent) *record { @@ -392,51 +395,51 @@ func bsiComponentSourceHash(component sbom.GetComponent) *record { score = 10.0 } - return newRecordStmtOptional(COMP_SOURCE_HASH, component.GetID(), result, score) + return newRecordStmtOptional(COMP_SOURCE_HASH, component.GetName(), result, score) } func bsiComponentOtherUniqIDs(component sbom.GetComponent) *record { result := "" score := 0.0 - purl := component.Purls() + purl := component.GetPurls() if len(purl) > 0 { result = string(purl[0]) score = 10.0 - return newRecordStmtOptional(COMP_OTHER_UNIQ_IDS, component.GetID(), result, score) + return newRecordStmtOptional(COMP_OTHER_UNIQ_IDS, component.GetName(), result, score) } - cpes := component.Cpes() + cpes := component.GetCpes() if len(cpes) > 0 { result = string(cpes[0]) score = 10.0 - return newRecordStmtOptional(COMP_OTHER_UNIQ_IDS, component.GetID(), result, score) + return newRecordStmtOptional(COMP_OTHER_UNIQ_IDS, component.GetName(), result, score) } - return newRecordStmtOptional(COMP_OTHER_UNIQ_IDS, component.GetID(), "", 0.0) + return newRecordStmtOptional(COMP_OTHER_UNIQ_IDS, component.GetName(), "", 0.0) } func bsiComponentDownloadURL(component sbom.GetComponent) *record { result := component.GetDownloadLocationURL() if result != "" { - return newRecordStmtOptional(COMP_DOWNLOAD_URL, component.GetID(), result, 10.0) + return newRecordStmtOptional(COMP_DOWNLOAD_URL, component.GetName(), result, 10.0) } - return newRecordStmtOptional(COMP_DOWNLOAD_URL, component.GetID(), "", 0.0) + return newRecordStmtOptional(COMP_DOWNLOAD_URL, component.GetName(), "", 0.0) } func bsiComponentSourceCodeURL(component sbom.GetComponent) *record { result := component.SourceCodeURL() if result != "" { - return newRecordStmtOptional(COMP_SOURCE_CODE_URL, component.GetID(), result, 10.0) + return newRecordStmtOptional(COMP_SOURCE_CODE_URL, component.GetName(), result, 10.0) } - return newRecordStmtOptional(COMP_SOURCE_CODE_URL, component.GetID(), "", 0.0) + return newRecordStmtOptional(COMP_SOURCE_CODE_URL, component.GetName(), "", 0.0) } func bsiComponentHash(component sbom.GetComponent) *record { @@ -454,27 +457,27 @@ func bsiComponentHash(component sbom.GetComponent) *record { } } - return newRecordStmt(COMP_HASH, component.GetID(), result, score) + return newRecordStmt(COMP_HASH, component.GetName(), result, score) } func bsiComponentVersion(component sbom.GetComponent) *record { result := component.GetVersion() if result != "" { - return newRecordStmt(COMP_VERSION, component.GetID(), result, 10.0) + return newRecordStmt(COMP_VERSION, component.GetName(), result, 10.0) } - return newRecordStmt(COMP_VERSION, component.GetID(), "", 0.0) + return newRecordStmt(COMP_VERSION, component.GetName(), "", 0.0) } func bsiComponentName(component sbom.GetComponent) *record { result := component.GetName() if result != "" { - return newRecordStmt(COMP_NAME, component.GetID(), result, 10.0) + return newRecordStmt(COMP_NAME, component.GetName(), result, 10.0) } - return newRecordStmt(COMP_NAME, component.GetID(), "", 0.0) + return newRecordStmt(COMP_NAME, component.GetName(), "", 0.0) } func bsiComponentCreator(component sbom.GetComponent) *record { @@ -489,7 +492,7 @@ func bsiComponentCreator(component sbom.GetComponent) *record { } if result != "" { - return newRecordStmt(COMP_CREATOR, component.GetID(), result, score) + return newRecordStmt(COMP_CREATOR, component.GetName(), result, score) } if supplier.GetURL() != "" { @@ -498,7 +501,7 @@ func bsiComponentCreator(component sbom.GetComponent) *record { } if result != "" { - return newRecordStmt(COMP_CREATOR, component.GetID(), result, score) + return newRecordStmt(COMP_CREATOR, component.GetName(), result, score) } if supplier.GetContacts() != nil { @@ -511,7 +514,7 @@ func bsiComponentCreator(component sbom.GetComponent) *record { } if result != "" { - return newRecordStmt(COMP_CREATOR, component.GetID(), result, score) + return newRecordStmt(COMP_CREATOR, component.GetName(), result, score) } } } @@ -525,7 +528,7 @@ func bsiComponentCreator(component sbom.GetComponent) *record { } if result != "" { - return newRecordStmt(COMP_CREATOR, component.GetID(), result, score) + return newRecordStmt(COMP_CREATOR, component.GetName(), result, score) } if manufacturer.GetURL() != "" { @@ -534,7 +537,7 @@ func bsiComponentCreator(component sbom.GetComponent) *record { } if result != "" { - return newRecordStmt(COMP_CREATOR, component.GetID(), result, score) + return newRecordStmt(COMP_CREATOR, component.GetName(), result, score) } if manufacturer.GetContacts() != nil { @@ -547,10 +550,10 @@ func bsiComponentCreator(component sbom.GetComponent) *record { } if result != "" { - return newRecordStmt(COMP_CREATOR, component.GetID(), result, score) + return newRecordStmt(COMP_CREATOR, component.GetName(), result, score) } } } - return newRecordStmt(COMP_CREATOR, component.GetID(), "", 0.0) + return newRecordStmt(COMP_CREATOR, component.GetName(), "", 0.0) } diff --git a/pkg/compliance/ntia.go b/pkg/compliance/ntia.go index 2f3a686..2d01756 100644 --- a/pkg/compliance/ntia.go +++ b/pkg/compliance/ntia.go @@ -16,16 +16,356 @@ package compliance import ( "context" + "fmt" + "strings" + "time" "github.com/interlynk-io/sbomqs/pkg/logger" "github.com/interlynk-io/sbomqs/pkg/sbom" + "github.com/samber/lo" ) -func ntiaResult(ctx context.Context, _ sbom.Document, _ string, _ string) *db { +var ( + validSpec = []string{"cyclonedx", "spdx"} + validFormats = []string{"json", "xml", "yaml", "yml", "tag-value"} +) + +// nolint +const ( + SCORE_FULL = 10.0 + SCORE_ZERO = 0.0 +) + +func ntiaResult(ctx context.Context, doc sbom.Document, fileName string, outFormat string) { log := logger.FromContext(ctx) log.Debug("compliance.ntiaResult()") db := newDB() - return db + db.addRecord(ntiaAutomationSpec(doc)) + db.addRecord(ntiaSbomCreator(doc)) + db.addRecord(ntiaSbomCreatedTimestamp(doc)) + db.addRecord(ntiaSBOMDependency(doc)) + db.addRecords(ntiaComponents(doc)) + + if outFormat == "json" { + ntiaJSONReport(db, fileName) + } + + if outFormat == "basic" { + ntiaBasicReport(db, fileName) + } + + if outFormat == "detailed" { + ntiaDetailedReport(db, fileName) + } +} + +// format +func ntiaAutomationSpec(doc sbom.Document) *record { + result, score := "", SCORE_ZERO + spec := doc.Spec().GetSpecType() + fileFormat := doc.Spec().FileFormat() + + result = spec + ", " + fileFormat + + if lo.Contains(validFormats, fileFormat) && lo.Contains(validSpec, spec) { + result = spec + ", " + fileFormat + score = SCORE_FULL + } + return newRecordStmt(SBOM_MACHINE_FORMAT, "Automation Support", result, score) +} + +func ntiaSBOMDependency(doc sbom.Document) *record { + result, score := "", SCORE_ZERO + totalRootDependencies := doc.PrimaryComp().GetTotalNoOfDependencies() + + if totalRootDependencies > 0 { + score = SCORE_FULL + } + result = fmt.Sprintf("doc has %d dependencies", totalRootDependencies) + + return newRecordStmt(SBOM_DEPENDENCY, "SBOM Data Fields", result, score) +} + +func ntiaSbomCreator(doc sbom.Document) *record { + spec := doc.Spec().GetSpecType() + result, score := "", SCORE_ZERO + + switch spec { + case "spdx": + if tools := doc.Tools(); tools != nil { + if toolResult, found := getToolInfo(tools); found { + result = toolResult + score = SCORE_FULL + break + } + } + if authors := doc.Authors(); authors != nil { + if authorResult, found := getAuthorInfo(authors); found { + result = authorResult + score = SCORE_FULL + break + } + } + case "cyclonedx": + if authors := doc.Authors(); authors != nil { + if authorResult, found := getAuthorInfo(authors); found { + result = authorResult + score = SCORE_FULL + break + } + } + if result != "" { + return newRecordStmt(SBOM_CREATOR, "SBOM Data Fields", result, score) + } + if tools := doc.Tools(); tools != nil { + if toolResult, found := getToolInfo(tools); found { + result = toolResult + score = SCORE_FULL + break + } + } + if supplier := doc.Supplier(); supplier != nil { + if supplierResult, found := getSupplierInfo(supplier); found { + result = supplierResult + score = SCORE_FULL + break + } + } + if manufacturer := doc.Manufacturer(); manufacturer != nil { + if manufacturerResult, found := getManufacturerInfo(manufacturer); found { + result = manufacturerResult + score = SCORE_FULL + break + } + } + } + + return newRecordStmt(SBOM_CREATOR, "SBOM Data Fields", result, score) +} + +func getManufacturerInfo(manufacturer sbom.Manufacturer) (string, bool) { + if manufacturer == nil { + return "", false + } + if email := manufacturer.GetEmail(); email != "" { + return email, true + } + if url := manufacturer.GetURL(); url != "" { + return url, true + } + for _, contact := range manufacturer.GetContacts() { + if email := contact.Email(); email != "" { + return email, true + } + } + return "", false +} + +func getSupplierInfo(supplier sbom.GetSupplier) (string, bool) { + if supplier == nil { + return "", false + } + if email := supplier.GetEmail(); email != "" { + return email, true + } + if url := supplier.GetURL(); url != "" { + return url, true + } + for _, contact := range supplier.GetContacts() { + if email := contact.Email(); email != "" { + return email, true + } + } + return "", false +} + +func getAuthorInfo(authors []sbom.GetAuthor) (string, bool) { + for _, author := range authors { + if email := author.GetEmail(); email != "" { + return email, true + } + if name := author.GetName(); name != "" { + return name, true + } + } + return "", false +} + +func getToolInfo(tools []sbom.GetTool) (string, bool) { + for _, tool := range tools { + if name := tool.GetName(); name != "" { + return name, true + } + } + return "", false +} + +func ntiaSbomCreatedTimestamp(doc sbom.Document) *record { + score := SCORE_ZERO + result := doc.Spec().GetCreationTimestamp() + + if result != "" { + _, err := time.Parse(time.RFC3339, result) + if err != nil { + score = SCORE_ZERO + } else { + score = SCORE_FULL + } + } + return newRecordStmt(SBOM_TIMESTAMP, "SBOM Data Fields", result, score) +} + +var CompIDWithName = make(map[string]string) + +func extractName(comp string) string { + for x, y := range CompIDWithName { + if strings.Contains(comp, x) { + return y + } + } + return "" +} + +// Required component stuffs +func ntiaComponents(doc sbom.Document) []*record { + records := []*record{} + + if len(doc.Components()) == 0 { + records = append(records, newRecordStmt(SBOM_COMPONENTS, "SBOM Data Fields", "absent", SCORE_ZERO)) + return records + } + + // map package ID to Package Name + for _, component := range doc.Components() { + CompIDWithName[component.GetID()] = component.GetName() + } + + for _, component := range doc.Components() { + records = append(records, ntiaComponentName(component)) + records = append(records, ntiaComponentCreator(doc, component)) + records = append(records, ntiaComponentVersion(component)) + records = append(records, ntiaComponentOtherUniqIDs(doc, component)) + records = append(records, ntiaComponentDependencies(doc, component)) + } + return records +} + +func ntiaComponentName(component sbom.GetComponent) *record { + if result := component.GetName(); result != "" { + return newRecordStmt(COMP_NAME, component.GetName(), result, SCORE_FULL) + } + return newRecordStmt(COMP_NAME, component.GetName(), "", SCORE_ZERO) +} + +func ntiaComponentCreator(doc sbom.Document, component sbom.GetComponent) *record { + spec := doc.Spec().GetSpecType() + result, score := "", SCORE_ZERO + + switch spec { + case "spdx": + if supplier := component.Suppliers(); supplier != nil { + if supplierResult, found := getSupplierInfo(supplier); found { + result = supplierResult + score = SCORE_FULL + break + } + } + case "cyclonedx": + if supplier := component.Suppliers(); supplier != nil { + if supplierResult, found := getSupplierInfo(supplier); found { + result = supplierResult + score = SCORE_FULL + break + } + } + + if manufacturer := component.Manufacturer(); manufacturer != nil { + if manufacturerResult, found := getManufacturerInfo(manufacturer); found { + result = manufacturerResult + score = SCORE_FULL + break + } + } + } + return newRecordStmt(COMP_CREATOR, component.GetName(), result, score) +} + +func ntiaComponentVersion(component sbom.GetComponent) *record { + result := component.GetVersion() + + if result != "" { + return newRecordStmt(COMP_VERSION, component.GetName(), result, SCORE_FULL) + } + + return newRecordStmt(COMP_VERSION, component.GetName(), "", SCORE_ZERO) +} + +func ntiaComponentDependencies(doc sbom.Document, component sbom.GetComponent) *record { + result, score := "", SCORE_ZERO + var results []string + + dependencies := doc.GetRelationships(component.GetID()) + if dependencies == nil { + return newRecordStmt(COMP_DEPTH, component.GetName(), "no-relationships", SCORE_ZERO) + } + for _, d := range dependencies { + componentName := extractName(d) + results = append(results, componentName) + score = SCORE_FULL + } + + if results != nil { + result = strings.Join(results, ", ") + } else { + result += "no-relationships" + } + + return newRecordStmt(COMP_DEPTH, component.GetName(), result, score) +} + +func ntiaComponentOtherUniqIDs(doc sbom.Document, component sbom.GetComponent) *record { + spec := doc.Spec().GetSpecType() + + if spec == "spdx" { + result, score, totalElements, containPurlElement := "", SCORE_ZERO, 0, 0 + + if extRefs := component.ExternalReferences(); extRefs != nil { + for _, extRef := range extRefs { + totalElements++ + result = extRef.GetRefType() + if result == "purl" { + containPurlElement++ + } + } + } + if containPurlElement != 0 { + score = (float64(containPurlElement) / float64(totalElements)) * SCORE_FULL + x := fmt.Sprintf(":(%d/%d)", containPurlElement, totalElements) + result = result + x + } + return newRecordStmt(COMP_OTHER_UNIQ_IDS, component.GetName(), result, score) + } else if spec == "cyclonedx" { + result := "" + + purl := component.GetPurls() + + if len(purl) > 0 { + result = string(purl[0]) + + return newRecordStmtOptional(COMP_OTHER_UNIQ_IDS, component.GetName(), result, SCORE_FULL) + } + + cpes := component.GetCpes() + + if len(cpes) > 0 { + result = string(cpes[0]) + + return newRecordStmtOptional(COMP_OTHER_UNIQ_IDS, component.GetName(), result, SCORE_FULL) + } + + return newRecordStmtOptional(COMP_OTHER_UNIQ_IDS, component.GetName(), "", SCORE_ZERO) + } + return newRecordStmt(COMP_OTHER_UNIQ_IDS, component.GetName(), "", SCORE_ZERO) } diff --git a/pkg/compliance/ntia_report.go b/pkg/compliance/ntia_report.go new file mode 100644 index 0000000..04321bc --- /dev/null +++ b/pkg/compliance/ntia_report.go @@ -0,0 +1,150 @@ +package compliance + +import ( + "encoding/json" + "fmt" + "os" + "sort" + "time" + + "github.com/google/uuid" + "github.com/olekukonko/tablewriter" + "sigs.k8s.io/release-utils/version" +) + +var ntiaSectionDetails = map[int]ntiaSection{ + SBOM_MACHINE_FORMAT: {Title: "Automation Support", ID: "1.1", Required: true, DataField: "Machine-Readable Formats"}, + SBOM_CREATOR: {Title: "Required fields sboms ", ID: "2.1", Required: true, DataField: "Author"}, + SBOM_TIMESTAMP: {Title: "Required fields sboms", ID: "2.2", Required: true, DataField: "Timestamp"}, + SBOM_DEPENDENCY: {Title: "Required fields sboms", ID: "2.3", Required: true, DataField: "Dependencies"}, + COMP_NAME: {Title: "Required fields components", ID: "2.4", Required: true, DataField: "Package Name"}, + COMP_DEPTH: {Title: "Required fields components", ID: "2.5", Required: true, DataField: "Dependencies on other components"}, + COMP_CREATOR: {Title: "Required fields component", ID: "2.6", Required: true, DataField: "Package Supplier"}, + PACK_SUPPLIER: {Title: "Required fields component", ID: "2.6", Required: true, DataField: "Package Supplier"}, + COMP_VERSION: {Title: "Required fields components", ID: "2.7", Required: true, DataField: "Package Version"}, + COMP_OTHER_UNIQ_IDS: {Title: "Required fields component", ID: "2.8", Required: true, DataField: "Other Uniq IDs"}, +} + +type ntiaSection struct { + Title string `json:"section_title"` + ID string `json:"section_id"` + DataField string `json:"section_data_field"` + Required bool `json:"required"` + ElementID string `json:"element_id"` + ElementResult string `json:"element_result"` + Score float64 `json:"score"` +} + +type ntiaComplianceReport struct { + Name string `json:"report_name"` + Subtitle string `json:"subtitle"` + Revision string `json:"revision"` + Run run `json:"run"` + Tool tool `json:"tool"` + Summary Summary `json:"summary"` + Sections []ntiaSection `json:"sections"` +} + +func newNtiaJSONReport() *ntiaComplianceReport { + return &ntiaComplianceReport{ + Name: "NTIA-minimum elements Compliance Report", + Subtitle: "Part 2: Software Bill of Materials (SBOM)", + Revision: "", + Run: run{ + ID: uuid.New().String(), + GeneratedAt: time.Now().UTC().Format(time.RFC3339), + FileName: "", + EngineVersion: "1", + }, + Tool: tool{ + Name: "sbomqs", + Version: version.GetVersionInfo().GitVersion, + Vendor: "Interlynk (support@interlynk.io)", + }, + } +} + +func ntiaJSONReport(db *db, fileName string) { + jr := newNtiaJSONReport() + jr.Run.FileName = fileName + + score := ntiaAggregateScore(db) + summary := Summary{} + summary.MaxScore = 10.0 + summary.TotalScore = score.totalScore() + summary.TotalRequiredScore = score.totalRequiredScore() + summary.TotalOptionalScore = score.totalOptionalScore() + + jr.Summary = summary + jr.Sections = ntiaConstructSections(db) + + o, _ := json.MarshalIndent(jr, "", " ") + fmt.Println(string(o)) +} + +func ntiaConstructSections(db *db) []ntiaSection { + var sections []ntiaSection + allIDs := db.getAllIDs() + for _, id := range allIDs { + records := db.getRecordsByID(id) + + for _, r := range records { + section := ntiaSectionDetails[r.checkKey] + newSection := ntiaSection{ + Title: section.Title, + ID: section.ID, + DataField: section.DataField, + Required: section.Required, + } + score := ntiaKeyIDScore(db, r.checkKey, r.id) + newSection.Score = score.totalScore() + if r.id == "doc" { + newSection.ElementID = "sbom" + } else { + newSection.ElementID = r.id + } + + newSection.ElementResult = r.checkValue + + sections = append(sections, newSection) + } + } + return sections +} + +func ntiaDetailedReport(db *db, fileName string) { + table := tablewriter.NewWriter(os.Stdout) + score := ntiaAggregateScore(db) + + fmt.Printf("NTIA Report\n") + fmt.Printf("Compliance score by Interlynk Score:%0.1f RequiredScore:%0.1f OptionalScore:%0.1f for %s\n", score.totalScore(), score.totalRequiredScore(), score.totalOptionalScore(), fileName) + fmt.Printf("* indicates optional fields\n") + table.SetHeader([]string{"ELEMENT ID", "Section ID", "NTIA minimum elements", "Result", "Score"}) + table.SetRowLine(true) + table.SetAutoMergeCellsByColumnIndex([]int{0}) + + sections := ntiaConstructSections(db) + + // Sort sections by ElementId and then by SectionId + sort.Slice(sections, func(i, j int) bool { + if sections[i].ElementID == sections[j].ElementID { + return sections[i].ID < sections[j].ID + } + return sections[i].ElementID < sections[j].ElementID + }) + + for _, section := range sections { + sectionID := section.ID + if !section.Required { + sectionID = sectionID + "*" + } + table.Append([]string{section.ElementID, sectionID, section.DataField, section.ElementResult, fmt.Sprintf("%0.1f", section.Score)}) + } + table.Render() +} + +func ntiaBasicReport(db *db, fileName string) { + score := ntiaAggregateScore(db) + fmt.Printf("NTIA Report\n") + fmt.Printf("Score:%0.1f RequiredScore:%0.1f OptionalScore:%0.1f for %s\n", score.totalScore(), score.totalRequiredScore(), score.totalOptionalScore(), fileName) +} diff --git a/pkg/compliance/ntia_score.go b/pkg/compliance/ntia_score.go new file mode 100644 index 0000000..bfde2ee --- /dev/null +++ b/pkg/compliance/ntia_score.go @@ -0,0 +1,128 @@ +package compliance + +type ntiaScoreResult struct { + id string + requiredScore float64 + optionalScore float64 + requiredRecords int + optionalRecords int +} + +func newNtiaScoreResult(id string) *ntiaScoreResult { + return &ntiaScoreResult{id: id} +} + +func (r *ntiaScoreResult) totalScore() float64 { + if r.requiredRecords == 0 && r.optionalRecords == 0 { + return 0.0 + } + + if r.requiredRecords != 0 && r.optionalRecords != 0 { + return (r.totalRequiredScore() + r.totalOptionalScore()) / 2 + } + + if r.requiredRecords == 0 && r.optionalRecords != 0 { + return r.totalOptionalScore() + } + + return r.totalRequiredScore() +} + +func (r *ntiaScoreResult) totalRequiredScore() float64 { + if r.requiredRecords == 0 { + return 0.0 + } + + return r.requiredScore / float64(r.requiredRecords) +} + +func (r *ntiaScoreResult) totalOptionalScore() float64 { + if r.optionalRecords == 0 { + return 0.0 + } + + return r.optionalScore / float64(r.optionalRecords) +} + +func ntiaKeyIDScore(db *db, key int, id string) *ntiaScoreResult { + records := db.getRecordsByKeyID(key, id) + + if len(records) == 0 { + return newNtiaScoreResult(id) + } + + requiredScore := 0.0 + optionalScore := 0.0 + + requiredRecs := 0 + optionalRecs := 0 + + for _, r := range records { + if r.required { + requiredScore += r.score + requiredRecs++ + } else { + optionalScore += r.score + optionalRecs++ + } + } + + return &ntiaScoreResult{ + id: id, + requiredScore: requiredScore, + optionalScore: optionalScore, + requiredRecords: requiredRecs, + optionalRecords: optionalRecs, + } +} + +func ntiaAggregateScore(db *db) *ntiaScoreResult { + var results []ntiaScoreResult + var finalResult ntiaScoreResult + + ids := db.getAllIDs() + for _, id := range ids { + results = append(results, *ntiaIDScore(db, id)) + } + + for _, r := range results { + finalResult.requiredScore += r.requiredScore + finalResult.optionalScore += r.optionalScore + finalResult.requiredRecords += r.requiredRecords + finalResult.optionalRecords += r.optionalRecords + } + + return &finalResult +} + +func ntiaIDScore(db *db, id string) *ntiaScoreResult { + records := db.getRecordsByID(id) + + if len(records) == 0 { + return newNtiaScoreResult(id) + } + + requiredScore := 0.0 + optionalScore := 0.0 + + requiredRecs := 0 + optionalRecs := 0 + + for _, r := range records { + if r.required { + requiredScore += r.score + requiredRecs++ + } else { + optionalScore += r.score + optionalRecs++ + } + } + + return &ntiaScoreResult{ + id: id, + requiredScore: requiredScore, + optionalScore: optionalScore, + requiredRecords: requiredRecs, + optionalRecords: optionalRecs, + } +} diff --git a/pkg/compliance/ntia_test.go b/pkg/compliance/ntia_test.go new file mode 100644 index 0000000..e2068a1 --- /dev/null +++ b/pkg/compliance/ntia_test.go @@ -0,0 +1,475 @@ +package compliance + +import ( + "testing" + + "github.com/interlynk-io/sbomqs/pkg/purl" + "github.com/interlynk-io/sbomqs/pkg/sbom" + "gotest.tools/assert" +) + +func createSpdxDummyDocumentNtia() sbom.Document { + s := sbom.NewSpec() + s.Version = "SPDX-2.3" + s.SpecType = "spdx" + s.Format = "json" + s.CreationTimestamp = "2023-05-04T09:33:40Z" + + var creators []sbom.GetTool + creator := sbom.Tool{ + Name: "syft", + } + creators = append(creators, creator) + + pack := sbom.NewComponent() + pack.Version = "v0.7.1" + pack.Name = "tool-golang" + pack.ID = "github/spdx/tools-golang@9db247b854b9634d0109153d515fd1a9efd5a1b1" + + supplier := sbom.Supplier{ + Email: "hello@interlynk.io", + } + pack.Supplier = supplier + + extRef := sbom.ExternalReference{ + RefType: "purl", + } + + var primary sbom.PrimaryComp + primary.Dependecies = 1 + + var externalReferences []sbom.GetExternalReference + externalReferences = append(externalReferences, extRef) + pack.ExternalRefs = externalReferences + + var packages []sbom.GetComponent + packages = append(packages, pack) + + relationships := make(map[string][]string) + relationships["github/spdx/tools-golang@9db247b854b9634d0109153d515fd1a9efd5a1b1"] = append(relationships["github/spdx/tools-golang@9db247b854b9634d0109153d515fd1a9efd5a1b1"], "github/spdx/gordf@b735bd5aac89fe25cad4ef488a95bc00ea549edd") + + CompIDWithName["github/spdx/gordf@b735bd5aac89fe25cad4ef488a95bc00ea549edd"] = "gordf" + doc := sbom.SpdxDoc{ + SpdxSpec: s, + Comps: packages, + SpdxTools: creators, + Dependencies: relationships, + PrimaryComponent: primary, + } + return doc +} + +type desiredNtia struct { + score float64 + result string + key int + id string +} + +func TestNtiaSpdxSbomPass(t *testing.T) { + doc := createSpdxDummyDocumentNtia() + testCases := []struct { + name string + actual *record + expected desiredNtia + }{ + { + name: "AutomationSpec", + actual: ntiaAutomationSpec(doc), + expected: desiredNtia{ + score: 10.0, + result: "spdx, json", + key: SBOM_MACHINE_FORMAT, + id: "Automation Support", + }, + }, + { + name: "SbomCreator", + actual: ntiaSbomCreator(doc), + expected: desiredNtia{ + score: 10.0, + result: "syft", + key: SBOM_CREATOR, + id: "SBOM Data Fields", + }, + }, + { + name: "SbomCreatedTimestamp", + actual: ntiaSbomCreatedTimestamp(doc), + expected: desiredNtia{ + score: 10.0, + result: "2023-05-04T09:33:40Z", + key: SBOM_TIMESTAMP, + id: "SBOM Data Fields", + }, + }, + { + name: "SbomDependency", + actual: ntiaSBOMDependency(doc), + expected: desiredNtia{ + score: 10.0, + result: "doc has 1 dependencies", + key: SBOM_DEPENDENCY, + id: "SBOM Data Fields", + }, + }, + + { + name: "ComponentCreator", + actual: ntiaComponentCreator(doc, doc.Components()[0]), + expected: desiredNtia{ + score: 10.0, + result: "hello@interlynk.io", + key: COMP_CREATOR, + id: doc.Components()[0].GetName(), + }, + }, + + { + name: "ComponentName", + actual: ntiaComponentName(doc.Components()[0]), + expected: desiredNtia{ + score: 10.0, + result: "tool-golang", + key: COMP_NAME, + id: doc.Components()[0].GetName(), + }, + }, + { + name: "ComponentVersion", + actual: ntiaComponentVersion(doc.Components()[0]), + expected: desiredNtia{ + score: 10.0, + result: "v0.7.1", + key: COMP_VERSION, + id: doc.Components()[0].GetName(), + }, + }, + { + name: "ComponentOtherUniqIDs", + actual: ntiaComponentOtherUniqIDs(doc, doc.Components()[0]), + expected: desiredNtia{ + score: 10.0, + result: "purl:(1/1)", + key: COMP_OTHER_UNIQ_IDS, + id: doc.Components()[0].GetName(), + }, + }, + { + name: "ComponentDependencies", + actual: ntiaComponentDependencies(doc, doc.Components()[0]), + expected: desiredNtia{ + score: 10.0, + result: "gordf", + key: COMP_DEPTH, + id: doc.Components()[0].GetName(), + }, + }, + } + + for _, test := range testCases { + assert.Equal(t, test.expected.score, test.actual.score, "Score mismatch for %s", test.name) + assert.Equal(t, test.expected.key, test.actual.checkKey, "Key mismatch for %s", test.name) + assert.Equal(t, test.expected.id, test.actual.id, "ID mismatch for %s", test.name) + assert.Equal(t, test.expected.result, test.actual.checkValue, "Result mismatch for %s", test.name) + } +} + +func createCdxDummyDocumentNtia() sbom.Document { + cdxSpec := sbom.NewSpec() + cdxSpec.Version = "1.4" + cdxSpec.SpecType = "cyclonedx" + cdxSpec.CreationTimestamp = "2023-05-04T09:33:40Z" + cdxSpec.Format = "xml" + + var authors []sbom.GetAuthor + author := sbom.Author{ + Email: "hello@interlynk.io", + } + authors = append(authors, author) + + comp := sbom.NewComponent() + comp.Version = "v0.7.1" + comp.Name = "tool-golang" + comp.ID = "github/spdx/tools-golang@9db247b854b9634d0109153d515fd1a9efd5a1b1" + + supplier := sbom.Supplier{ + Email: "hello@interlynk.io", + } + comp.Supplier = supplier + + npurl := purl.NewPURL("vivek") + + comp.Purls = []purl.PURL{npurl} + + extRef := sbom.ExternalReference{ + RefType: "purl", + } + + var externalReferences []sbom.GetExternalReference + externalReferences = append(externalReferences, extRef) + comp.ExternalRefs = externalReferences + + var components []sbom.GetComponent + components = append(components, comp) + + relationships := make(map[string][]string) + relationships["github/spdx/tools-golang@9db247b854b9634d0109153d515fd1a9efd5a1b1"] = append(relationships["github/spdx/tools-golang@9db247b854b9634d0109153d515fd1a9efd5a1b1"], "github/spdx/gordf@b735bd5aac89fe25cad4ef488a95bc00ea549edd") + + var primary sbom.PrimaryComp + primary.Dependecies = 1 + + CompIDWithName["github/spdx/gordf@b735bd5aac89fe25cad4ef488a95bc00ea549edd"] = "gordf" + + doc := sbom.CdxDoc{ + CdxSpec: cdxSpec, + Comps: components, + CdxAuthors: authors, + Dependencies: relationships, + PrimaryComponent: primary, + } + return doc +} + +func TestNtiaCdxSbomPass(t *testing.T) { + doc := createCdxDummyDocumentNtia() + testCases := []struct { + name string + actual *record + expected desiredNtia + }{ + { + name: "AutomationSpec", + actual: ntiaAutomationSpec(doc), + expected: desiredNtia{ + score: 10.0, + result: "cyclonedx, xml", + key: SBOM_MACHINE_FORMAT, + id: "Automation Support", + }, + }, + { + name: "SbomCreator", + actual: ntiaSbomCreator(doc), + expected: desiredNtia{ + score: 10.0, + result: "hello@interlynk.io", + key: SBOM_CREATOR, + id: "SBOM Data Fields", + }, + }, + { + name: "SbomCreatedTimestamp", + actual: ntiaSbomCreatedTimestamp(doc), + expected: desiredNtia{ + score: 10.0, + result: "2023-05-04T09:33:40Z", + key: SBOM_TIMESTAMP, + id: "SBOM Data Fields", + }, + }, + { + name: "SbomDependency", + actual: ntiaSBOMDependency(doc), + expected: desiredNtia{ + score: 10.0, + result: "doc has 1 dependencies", + key: SBOM_DEPENDENCY, + id: "SBOM Data Fields", + }, + }, + { + name: "ComponentCreator", + actual: ntiaComponentCreator(doc, doc.Components()[0]), + expected: desiredNtia{ + score: 10.0, + result: "hello@interlynk.io", + key: COMP_CREATOR, + id: doc.Components()[0].GetName(), + }, + }, + { + name: "ComponentName", + actual: ntiaComponentName(doc.Components()[0]), + expected: desiredNtia{ + score: 10.0, + result: "tool-golang", + key: COMP_NAME, + id: doc.Components()[0].GetName(), + }, + }, + { + name: "ComponentVersion", + actual: ntiaComponentVersion(doc.Components()[0]), + expected: desiredNtia{ + score: 10.0, + result: "v0.7.1", + key: COMP_VERSION, + id: doc.Components()[0].GetName(), + }, + }, + { + name: "ComponentOtherUniqIDs", + actual: ntiaComponentOtherUniqIDs(doc, doc.Components()[0]), + expected: desiredNtia{ + score: 10.0, + result: "vivek", + key: COMP_OTHER_UNIQ_IDS, + id: doc.Components()[0].GetName(), + }, + }, + { + name: "ComponentDependencies", + actual: ntiaComponentDependencies(doc, doc.Components()[0]), + expected: desiredNtia{ + score: 10.0, + result: "gordf", + key: COMP_DEPTH, + id: doc.Components()[0].GetName(), + }, + }, + } + for _, test := range testCases { + assert.Equal(t, test.expected.score, test.actual.score, "Score mismatch for %s", test.name) + assert.Equal(t, test.expected.key, test.actual.checkKey, "Key mismatch for %s", test.name) + assert.Equal(t, test.expected.id, test.actual.id, "ID mismatch for %s", test.name) + assert.Equal(t, test.expected.result, test.actual.checkValue, "Result mismatch for %s", test.name) + } +} + +func createSpdxDummyDocumentFailNtia() sbom.Document { + s := sbom.NewSpec() + s.Version = "SPDX-4.0" + s.SpecType = "swid" + s.Format = "fjson" + s.CreationTimestamp = "2023-05-04" + + var creators []sbom.GetTool + creator := sbom.Tool{ + Name: "", + } + creators = append(creators, creator) + + pack := sbom.NewComponent() + pack.Version = "" + pack.Name = "" + + supplier := sbom.Supplier{ + Email: "", + } + pack.Supplier = supplier + + extRef := sbom.ExternalReference{ + RefType: "purl", + } + + var externalReferences []sbom.GetExternalReference + externalReferences = append(externalReferences, extRef) + pack.ExternalRefs = externalReferences + + var packages []sbom.GetComponent + packages = append(packages, pack) + + depend := sbom.Relation{ + From: "", + To: "", + } + var dependencies []sbom.GetRelation + dependencies = append(dependencies, depend) + + doc := sbom.SpdxDoc{ + SpdxSpec: s, + Comps: packages, + SpdxTools: creators, + Rels: dependencies, + } + return doc +} + +func TestNTIASbomFail(t *testing.T) { + doc := createSpdxDummyDocumentFailNtia() + testCases := []struct { + name string + actual *record + expected desiredNtia + }{ + { + name: "AutomationSpec", + actual: ntiaAutomationSpec(doc), + expected: desiredNtia{ + score: 0.0, + result: "swid, fjson", + key: SBOM_MACHINE_FORMAT, + id: "Automation Support", + }, + }, + { + name: "SbomCreator", + actual: ntiaSbomCreator(doc), + expected: desiredNtia{ + score: 0.0, + result: "", + key: SBOM_CREATOR, + id: "SBOM Data Fields", + }, + }, + { + name: "SbomCreatedTimestamp", + actual: ntiaSbomCreatedTimestamp(doc), + expected: desiredNtia{ + score: 0.0, + result: "2023-05-04", + key: SBOM_TIMESTAMP, + id: "SBOM Data Fields", + }, + }, + { + name: "ComponentCreator", + actual: ntiaComponentCreator(doc, doc.Components()[0]), + expected: desiredNtia{ + score: 0.0, + result: "", + key: COMP_CREATOR, + id: doc.Components()[0].GetID(), + }, + }, + + { + name: "ComponentName", + actual: ntiaComponentName(doc.Components()[0]), + expected: desiredNtia{ + score: 0.0, + result: "", + key: COMP_NAME, + id: doc.Components()[0].GetID(), + }, + }, + { + name: "ComponentVersion", + actual: ntiaComponentVersion(doc.Components()[0]), + expected: desiredNtia{ + score: 0.0, + result: "", + key: COMP_VERSION, + id: doc.Components()[0].GetID(), + }, + }, + { + name: "ComponentOtherUniqIDs", + actual: ntiaComponentOtherUniqIDs(doc, doc.Components()[0]), + expected: desiredNtia{ + score: 0.0, + result: "", + key: COMP_OTHER_UNIQ_IDS, + id: doc.Components()[0].GetID(), + }, + }, + } + + for _, test := range testCases { + assert.Equal(t, test.expected.score, test.actual.score, "Score mismatch for %s", test.name) + assert.Equal(t, test.expected.key, test.actual.checkKey, "Key mismatch for %s", test.name) + assert.Equal(t, test.expected.id, test.actual.id, "ID mismatch for %s", test.name) + assert.Equal(t, test.expected.result, test.actual.checkValue, "Result mismatch for %s", test.name) + } +} diff --git a/pkg/compliance/oct.go b/pkg/compliance/oct.go index 331cee9..a9da218 100644 --- a/pkg/compliance/oct.go +++ b/pkg/compliance/oct.go @@ -269,45 +269,45 @@ func octComponents(doc sbom.Document) []*record { func octPackageName(component sbom.GetComponent) *record { if result := component.GetName(); result != "" { - return newRecordStmt(PACK_NAME, component.GetID(), result, 10.0) + return newRecordStmt(PACK_NAME, component.GetName(), result, 10.0) } - return newRecordStmt(PACK_NAME, component.GetID(), "", 0.0) + return newRecordStmt(PACK_NAME, component.GetName(), "", 0.0) } func octPackageSpdxID(component sbom.GetComponent) *record { if result := component.GetSpdxID(); result != "" { - return newRecordStmt(PACK_SPDXID, component.GetID(), result, 10.0) + return newRecordStmt(PACK_SPDXID, component.GetName(), result, 10.0) } - return newRecordStmt(PACK_SPDXID, component.GetID(), "", 0.0) + return newRecordStmt(PACK_SPDXID, component.GetName(), "", 0.0) } func octPackageVersion(component sbom.GetComponent) *record { if result := component.GetVersion(); result != "" { - return newRecordStmt(PACK_VERSION, component.GetID(), result, 10.0) + return newRecordStmt(PACK_VERSION, component.GetName(), result, 10.0) } - return newRecordStmt(PACK_VERSION, component.GetID(), "", 0.0) + return newRecordStmt(PACK_VERSION, component.GetName(), "", 0.0) } func octPackageSupplier(component sbom.GetComponent) *record { if supplier := component.Suppliers().GetEmail(); supplier != "" { - return newRecordStmt(PACK_SUPPLIER, component.GetID(), supplier, 10.0) + return newRecordStmt(PACK_SUPPLIER, component.GetName(), supplier, 10.0) } - return newRecordStmt(PACK_SUPPLIER, component.GetID(), "", 0.0) + return newRecordStmt(PACK_SUPPLIER, component.GetName(), "", 0.0) } func octPackageDownloadURL(component sbom.GetComponent) *record { if result := component.GetDownloadLocationURL(); result != "" { - return newRecordStmt(PACK_DOWNLOAD_URL, component.GetID(), result, 10.0) + return newRecordStmt(PACK_DOWNLOAD_URL, component.GetName(), result, 10.0) } - return newRecordStmt(PACK_DOWNLOAD_URL, component.GetID(), "", 0.0) + return newRecordStmt(PACK_DOWNLOAD_URL, component.GetName(), "", 0.0) } func octPackageFileAnalyzed(component sbom.GetComponent) *record { if result := component.GetFileAnalyzed(); result { - return newRecordStmt(PACK_FILE_ANALYZED, component.GetID(), "yes", 10.0) + return newRecordStmt(PACK_FILE_ANALYZED, component.GetName(), "yes", 10.0) } - return newRecordStmt(PACK_FILE_ANALYZED, component.GetID(), "no", 0.0) + return newRecordStmt(PACK_FILE_ANALYZED, component.GetName(), "no", 0.0) } func octPackageHash(component sbom.GetComponent) *record { @@ -324,37 +324,37 @@ func octPackageHash(component sbom.GetComponent) *record { } } - return newRecordStmt(PACK_HASH, component.GetID(), result, score) + return newRecordStmt(PACK_HASH, component.GetName(), result, score) } func octPackageConLicense(component sbom.GetComponent) *record { result := "" if result = component.GetPackageLicenseConcluded(); result != "" && result != "NOASSERTION" && result != "NONE" { - return newRecordStmt(PACK_LICENSE_CON, component.GetID(), result, 10.0) + return newRecordStmt(PACK_LICENSE_CON, component.GetName(), result, 10.0) } - return newRecordStmt(PACK_LICENSE_CON, component.GetID(), result, 0.0) + return newRecordStmt(PACK_LICENSE_CON, component.GetName(), result, 0.0) } func octPackageDecLicense(component sbom.GetComponent) *record { result := "" if result = component.GetPackageLicenseDeclared(); result != "" && result != "NOASSERTION" && result != "NONE" { - return newRecordStmt(PACK_LICENSE_DEC, component.GetID(), result, 10.0) + return newRecordStmt(PACK_LICENSE_DEC, component.GetName(), result, 10.0) } - return newRecordStmt(PACK_LICENSE_DEC, component.GetID(), result, 0.0) + return newRecordStmt(PACK_LICENSE_DEC, component.GetName(), result, 0.0) } func octPackageCopyright(component sbom.GetComponent) *record { result := "" if result = component.GetCopyRight(); result != "" && result != "NOASSERTION" && result != "NONE" { - return newRecordStmt(PACK_COPYRIGHT, component.GetID(), result, 10.0) + return newRecordStmt(PACK_COPYRIGHT, component.GetName(), result, 10.0) } - return newRecordStmt(PACK_COPYRIGHT, component.GetID(), result, 0.0) + return newRecordStmt(PACK_COPYRIGHT, component.GetName(), result, 0.0) } func octPackageExternalRefs(component sbom.GetComponent) *record { @@ -374,5 +374,5 @@ func octPackageExternalRefs(component sbom.GetComponent) *record { x := fmt.Sprintf(":(%d/%d)", containPurlElement, totalElements) result = result + x } - return newRecordStmt(PACK_EXT_REF, component.GetID(), result, score) + return newRecordStmt(PACK_EXT_REF, component.GetName(), result, score) } diff --git a/pkg/compliance/oct_test.go b/pkg/compliance/oct_test.go index 7a42ac4..2655a4b 100644 --- a/pkg/compliance/oct_test.go +++ b/pkg/compliance/oct_test.go @@ -72,6 +72,7 @@ func createDummyDocument() sbom.Document { } type desired struct { + name string score float64 result string key int @@ -87,6 +88,7 @@ func TestOctSbomPass(t *testing.T) { { actual: octSpec(doc), expected: desired{ + name: "octSpec", score: 10.0, result: "spdx", key: SBOM_SPEC, @@ -96,6 +98,7 @@ func TestOctSbomPass(t *testing.T) { { actual: octSbomName(doc), expected: desired{ + name: "octSbomName", score: 10.0, result: "nano", key: SBOM_NAME, @@ -105,6 +108,7 @@ func TestOctSbomPass(t *testing.T) { { actual: octSbomNamespace(doc), expected: desired{ + name: "octSbomNamespace", score: 10.0, result: "https://anchore.com/syft/dir/sbomqs-6ec18b03-96cb-4951-b299-929890c1cfc8", key: SBOM_NAMESPACE, @@ -114,6 +118,7 @@ func TestOctSbomPass(t *testing.T) { { actual: octSbomOrganization(doc), expected: desired{ + name: "octSbomOrganization", score: 10.0, result: "interlynk", key: SBOM_ORG, @@ -123,6 +128,7 @@ func TestOctSbomPass(t *testing.T) { { actual: octSbomComment(doc), expected: desired{ + name: "octSbomComment", score: 10.0, result: "this is a general sbom created using syft tool", key: SBOM_COMMENT, @@ -132,6 +138,7 @@ func TestOctSbomPass(t *testing.T) { { actual: octSbomTool(doc), expected: desired{ + name: "octSbomTool", score: 10.0, result: "syft", key: SBOM_TOOL, @@ -141,6 +148,7 @@ func TestOctSbomPass(t *testing.T) { { actual: octSbomLicense(doc), expected: desired{ + name: "octSbomLicense", score: 10.0, result: "cc0-1.0", key: SBOM_LICENSE, @@ -150,6 +158,7 @@ func TestOctSbomPass(t *testing.T) { { actual: octSpecVersion(doc), expected: desired{ + name: "octSpecVersion", score: 10.0, result: "SPDX-2.3", key: SBOM_SPEC_VERSION, @@ -159,6 +168,7 @@ func TestOctSbomPass(t *testing.T) { { actual: octCreatedTimestamp(doc), expected: desired{ + name: "octCreatedTimestamp", score: 10.0, result: "2023-05-04T09:33:40Z", key: SBOM_TIMESTAMP, @@ -168,6 +178,7 @@ func TestOctSbomPass(t *testing.T) { { actual: octSpecSpdxID(doc), expected: desired{ + name: "octSpecSpdxID", score: 10.0, result: "DOCUMENT", key: SBOM_SPDXID, @@ -178,6 +189,7 @@ func TestOctSbomPass(t *testing.T) { { actual: octMachineFormat(doc), expected: desired{ + name: "octMachineFormat", score: 10.0, result: "spdx, json", key: SBOM_MACHINE_FORMAT, @@ -187,6 +199,7 @@ func TestOctSbomPass(t *testing.T) { { actual: octHumanFormat(doc), expected: desired{ + name: "octHumanFormat", score: 10.0, result: "json", key: SBOM_HUMAN_FORMAT, @@ -196,100 +209,111 @@ func TestOctSbomPass(t *testing.T) { { actual: octPackageName(doc.Components()[0]), expected: desired{ + name: "octPackageName", score: 10.0, result: "core-js", key: PACK_NAME, - id: doc.Components()[0].GetID(), + id: doc.Components()[0].GetName(), }, }, { actual: octPackageVersion(doc.Components()[0]), expected: desired{ + name: "octPackageVersion", score: 10.0, result: "v0.7.1", key: PACK_VERSION, - id: doc.Components()[0].GetID(), + id: doc.Components()[0].GetName(), }, }, { actual: octPackageSpdxID(doc.Components()[0]), expected: desired{ + name: "octPackageSpdxID", score: 10.0, result: "SPDXRef-npm-core-js-3.6.5", key: PACK_SPDXID, - id: doc.Components()[0].GetID(), + id: doc.Components()[0].GetName(), }, }, { actual: octPackageSupplier(doc.Components()[0]), expected: desired{ + name: "octPackageSupplier", score: 10.0, result: "vivekkumarsahu650@gmail.com", key: PACK_SUPPLIER, - id: doc.Components()[0].GetID(), + id: doc.Components()[0].GetName(), }, }, { actual: octPackageHash(doc.Components()[0]), expected: desired{ + name: "octPackageHash", score: 10.0, result: "ee1300ac533cebc2d070ce3765685d5f7fca2a5a78ca15068323f68ed63d4abf", key: PACK_HASH, - id: doc.Components()[0].GetID(), + id: doc.Components()[0].GetName(), }, }, { actual: octPackageExternalRefs(doc.Components()[0]), expected: desired{ + name: "octPackageExternalRefs", score: 10.0, result: "purl:(1/1)", key: PACK_EXT_REF, - id: doc.Components()[0].GetID(), + id: doc.Components()[0].GetName(), }, }, { actual: octPackageCopyright(doc.Components()[0]), expected: desired{ + name: "octPackageCopyright", score: 10.0, result: "Copyright 2001-2011 The Apache Software Foundation", key: PACK_COPYRIGHT, - id: doc.Components()[0].GetID(), + id: doc.Components()[0].GetName(), }, }, { actual: octPackageFileAnalyzed(doc.Components()[0]), expected: desired{ + name: "octPackageFileAnalyzed", score: 10.0, result: "yes", key: PACK_FILE_ANALYZED, - id: doc.Components()[0].GetID(), + id: doc.Components()[0].GetName(), }, }, { actual: octPackageConLicense(doc.Components()[0]), expected: desired{ + name: "octPackageConLicense", score: 10.0, result: "(LGPL-2.0-only OR LicenseRef-3)", key: PACK_LICENSE_CON, - id: doc.Components()[0].GetID(), + id: doc.Components()[0].GetName(), }, }, { actual: octPackageDecLicense(doc.Components()[0]), expected: desired{ + name: "octPackageDecLicense", score: 10.0, result: "(LGPL-2.0-only AND LicenseRef-3)", key: PACK_LICENSE_DEC, - id: doc.Components()[0].GetID(), + id: doc.Components()[0].GetName(), }, }, { actual: octPackageDownloadURL(doc.Components()[0]), expected: desired{ + name: "octPackageDownloadURL", score: 10.0, result: "https://registry.npmjs.org/core-js/-/core-js-3.6.5.tgz", key: PACK_DOWNLOAD_URL, - id: doc.Components()[0].GetID(), + id: doc.Components()[0].GetName(), }, }, } @@ -485,7 +509,7 @@ func TestOctSbomFail(t *testing.T) { score: 0.0, result: "", key: PACK_NAME, - id: doc.Components()[0].GetID(), + id: doc.Components()[0].GetName(), }, }, { @@ -494,7 +518,7 @@ func TestOctSbomFail(t *testing.T) { score: 0.0, result: "", key: PACK_VERSION, - id: doc.Components()[0].GetID(), + id: doc.Components()[0].GetName(), }, }, { @@ -503,7 +527,7 @@ func TestOctSbomFail(t *testing.T) { score: 0.0, result: "", key: PACK_SPDXID, - id: doc.Components()[0].GetID(), + id: doc.Components()[0].GetName(), }, }, { @@ -512,7 +536,7 @@ func TestOctSbomFail(t *testing.T) { score: 0.0, result: "", key: PACK_SUPPLIER, - id: doc.Components()[0].GetID(), + id: doc.Components()[0].GetName(), }, }, { @@ -521,7 +545,7 @@ func TestOctSbomFail(t *testing.T) { score: 0.0, result: "", key: PACK_HASH, - id: doc.Components()[0].GetID(), + id: doc.Components()[0].GetName(), }, }, { @@ -530,7 +554,7 @@ func TestOctSbomFail(t *testing.T) { score: 0.0, result: "cpe23Type", key: PACK_EXT_REF, - id: doc.Components()[0].GetID(), + id: doc.Components()[0].GetName(), }, }, { @@ -539,7 +563,7 @@ func TestOctSbomFail(t *testing.T) { score: 0.0, result: "NOASSERTION", key: PACK_COPYRIGHT, - id: doc.Components()[0].GetID(), + id: doc.Components()[0].GetName(), }, }, { @@ -548,7 +572,7 @@ func TestOctSbomFail(t *testing.T) { score: 0.0, result: "no", key: PACK_FILE_ANALYZED, - id: doc.Components()[0].GetID(), + id: doc.Components()[0].GetName(), }, }, { @@ -557,7 +581,7 @@ func TestOctSbomFail(t *testing.T) { score: 0.0, result: "NONE", key: PACK_LICENSE_CON, - id: doc.Components()[0].GetID(), + id: doc.Components()[0].GetName(), }, }, { @@ -566,7 +590,7 @@ func TestOctSbomFail(t *testing.T) { score: 0.0, result: "NOASSERTION", key: PACK_LICENSE_DEC, - id: doc.Components()[0].GetID(), + id: doc.Components()[0].GetName(), }, }, { @@ -575,7 +599,7 @@ func TestOctSbomFail(t *testing.T) { score: 0.0, result: "", key: PACK_DOWNLOAD_URL, - id: doc.Components()[0].GetID(), + id: doc.Components()[0].GetName(), }, }, } diff --git a/pkg/engine/score_test.go b/pkg/engine/score_test.go index 01d8638..ee61771 100644 --- a/pkg/engine/score_test.go +++ b/pkg/engine/score_test.go @@ -105,4 +105,4 @@ func TestProcessURL(t *testing.T) { } }) } -} \ No newline at end of file +} diff --git a/pkg/sbom/author.go b/pkg/sbom/author.go index 7b6de0e..c8d8ea6 100644 --- a/pkg/sbom/author.go +++ b/pkg/sbom/author.go @@ -15,25 +15,26 @@ package sbom //counterfeiter:generate . Author -type Author interface { - Name() string - Type() string - Email() string +type GetAuthor interface { + GetName() string + GetType() string + GetEmail() string } -type author struct { - name string - email string - authorType string //person or org +type Author struct { + Name string + Email string + AuthorType string // person or org } -func (a author) Name() string { - return a.name +func (a Author) GetName() string { + return a.Name } -func (a author) Type() string { - return a.authorType + +func (a Author) GetType() string { + return a.AuthorType } -func (a author) Email() string { - return a.email +func (a Author) GetEmail() string { + return a.Email } diff --git a/pkg/sbom/cdx.go b/pkg/sbom/cdx.go index 646d47d..135f4e6 100644 --- a/pkg/sbom/cdx.go +++ b/pkg/sbom/cdx.go @@ -35,22 +35,23 @@ var ( cdxPrimaryPurpose = []string{"application", "framework", "library", "container", "operating-system", "device", "firmware", "file"} ) -type cdxDoc struct { - doc *cydx.BOM - format FileFormat - ctx context.Context - spec *Specs - comps []GetComponent - authors []Author - tools []GetTool - rels []Relation - logs []string - primaryComponent bool - lifecycles []string - supplier GetSupplier - manufacturer Manufacturer - primaryComponentID string - compositions map[string]string +type CdxDoc struct { + doc *cydx.BOM + format FileFormat + ctx context.Context + CdxSpec *Specs + Comps []GetComponent + CdxAuthors []GetAuthor + CdxTools []GetTool + rels []GetRelation + logs []string + lifecycles []string + supplier GetSupplier + manufacturer Manufacturer + compositions map[string]string + PrimaryComponent PrimaryComp + Dependencies map[string][]string + composition map[string]string } func newCDXDoc(ctx context.Context, f io.ReadSeeker, format FileFormat) (Document, error) { @@ -80,7 +81,7 @@ func newCDXDoc(ctx context.Context, f io.ReadSeeker, format FileFormat) (Documen err = fmt.Errorf("unsupported cdx file format: %s", string(format)) } - doc := &cdxDoc{ + doc := &CdxDoc{ doc: bom, format: format, ctx: ctx, @@ -90,64 +91,71 @@ func newCDXDoc(ctx context.Context, f io.ReadSeeker, format FileFormat) (Documen return doc, err } -func (c cdxDoc) Spec() Spec { - return *c.spec +func (c CdxDoc) PrimaryComp() GetPrimaryComp { + return &c.PrimaryComponent } -func (c cdxDoc) Components() []GetComponent { - return c.comps +func (c CdxDoc) Spec() Spec { + return *c.CdxSpec } -func (c cdxDoc) Authors() []Author { - return c.authors +func (c CdxDoc) Components() []GetComponent { + return c.Comps } -func (c cdxDoc) Tools() []GetTool { - return c.tools +func (c CdxDoc) Authors() []GetAuthor { + return c.CdxAuthors } -func (c cdxDoc) Relations() []Relation { - return c.rels +func (c CdxDoc) Tools() []GetTool { + return c.CdxTools } -func (c cdxDoc) Logs() []string { - return c.logs +func (c CdxDoc) Relations() []GetRelation { + return c.rels } -func (c cdxDoc) PrimaryComponent() bool { - return c.primaryComponent +func (c CdxDoc) Logs() []string { + return c.logs } -func (c cdxDoc) Lifecycles() []string { +func (c CdxDoc) Lifecycles() []string { return c.lifecycles } -func (c cdxDoc) Supplier() GetSupplier { +func (c CdxDoc) Supplier() GetSupplier { return c.supplier } -func (c cdxDoc) Manufacturer() Manufacturer { +func (c CdxDoc) Manufacturer() Manufacturer { return c.manufacturer } -func (c *cdxDoc) parse() { +func (c CdxDoc) GetRelationships(componentID string) []string { + return c.Dependencies[componentID] +} + +func (c CdxDoc) GetComposition(componentID string) string { + return c.composition[componentID] +} + +func (c *CdxDoc) parse() { c.parseDoc() c.parseSpec() c.parseAuthors() c.parseSupplier() c.parseManufacturer() c.parseTool() - c.parsePrimaryComponent() c.parseCompositions() - c.parseRels() + c.parsePrimaryCompAndRelationships() c.parseComps() } -func (c *cdxDoc) addToLogs(log string) { +func (c *CdxDoc) addToLogs(log string) { c.logs = append(c.logs, log) } -func (c *cdxDoc) parseDoc() { +func (c *CdxDoc) parseDoc() { if c.doc == nil { c.addToLogs("cdx doc is not parsable") return @@ -176,7 +184,7 @@ func (c *cdxDoc) parseDoc() { }) } -func (c *cdxDoc) parseSpec() { +func (c *CdxDoc) parseSpec() { sp := NewSpec() sp.Format = string(c.format) sp.Version = c.doc.SpecVersion.String() @@ -196,10 +204,10 @@ func (c *cdxDoc) parseSpec() { sp.uri = fmt.Sprintf("%s/%d", c.doc.SerialNumber, c.doc.Version) } - c.spec = sp + c.CdxSpec = sp } -func (c *cdxDoc) requiredFields() bool { +func (c *CdxDoc) requiredFields() bool { if c.doc == nil { c.addToLogs("cdx doc is not parsable") return false @@ -234,7 +242,7 @@ func (c *cdxDoc) requiredFields() bool { return true } -func copyC(cdxc *cydx.Component, c *cdxDoc) *Component { +func copyC(cdxc *cydx.Component, c *CdxDoc) *Component { if cdxc == nil { return nil } @@ -247,14 +255,14 @@ func copyC(cdxc *cydx.Component, c *cdxDoc) *Component { ncpe := cpe.NewCPE(cdxc.CPE) if ncpe.Valid() { - nc.cpes = []cpe.CPE{ncpe} + nc.Cpes = []cpe.CPE{ncpe} } else { c.addToLogs(fmt.Sprintf("cdx base doc component %s at index %d invalid cpes found", cdxc.Name, -1)) } npurl := purl.NewPURL(cdxc.PackageURL) if npurl.Valid() { - nc.purls = []purl.PURL{npurl} + nc.Purls = []purl.PURL{npurl} } else { c.addToLogs(fmt.Sprintf("cdx base doc component %s at index %d invalid purl found", cdxc.Name, -1)) } @@ -285,59 +293,16 @@ func copyC(cdxc *cydx.Component, c *cdxDoc) *Component { } } - if cdxc.BOMRef == c.primaryComponentID { + if cdxc.BOMRef == c.PrimaryComponent.ID { nc.isPrimary = true } - fromRelsPresent := func(rels []Relation, compID string) bool { - for _, r := range rels { - if r.From() == compID { - return true - } - } - return false - } - - compNormalise := func(compID string) string { - switch cydx.CompositionAggregate(compID) { - case cydx.CompositionAggregateComplete: - return "complete" - case cydx.CompositionAggregateIncomplete: - return "incomplete" - case cydx.CompositionAggregateIncompleteFirstPartyOnly: - return "incomplete-first-party-only" - case cydx.CompositionAggregateIncompleteFirstPartyOpenSourceOnly: - return "incomplete-first-party-open-source-only" - case cydx.CompositionAggregateIncompleteFirstPartyProprietaryOnly: - return "incomplete-first-party-proprietary-only" - case cydx.CompositionAggregateIncompleteThirdPartyOnly: - return "incomplete-third-party-only" - case cydx.CompositionAggregateIncompleteThirdPartyOpenSourceOnly: - return "incomplete-third-party-open-source-only" - case cydx.CompositionAggregateIncompleteThirdPartyProprietaryOnly: - return "incomplete-third-party-proprietary-only" - case cydx.CompositionAggregateNotSpecified: - return "not-specified" - case cydx.CompositionAggregateUnknown: - return "unknown" - } - - return "not-specified" - } - - nc.hasRelationships = fromRelsPresent(c.rels, cdxc.BOMRef) - if c.compositions != nil { - if comp, ok := c.compositions[cdxc.BOMRef]; ok { - nc.relationshipState = compNormalise(comp) - } - } - nc.ID = cdxc.BOMRef return nc } -func (c *cdxDoc) parseComps() { - c.comps = []GetComponent{} +func (c *CdxDoc) parseComps() { + c.Comps = []GetComponent{} comps := map[string]*Component{} if c.doc.Metadata != nil && c.doc.Metadata.Component != nil { walkComponents(&[]cydx.Component{*c.doc.Metadata.Component}, c, comps) @@ -348,11 +313,11 @@ func (c *cdxDoc) parseComps() { } for _, v := range comps { - c.comps = append(c.comps, v) + c.Comps = append(c.Comps, v) } } -func walkComponents(comps *[]cydx.Component, doc *cdxDoc, store map[string]*Component) { +func walkComponents(comps *[]cydx.Component, doc *CdxDoc, store map[string]*Component) { if comps == nil { return } @@ -385,7 +350,7 @@ func compID(comp *cydx.Component) string { return id.String() } -func (c *cdxDoc) pkgRequiredFields(comp *cydx.Component) bool { +func (c *CdxDoc) pkgRequiredFields(comp *cydx.Component) bool { if string(comp.Type) == "" { c.addToLogs(fmt.Sprintf("cdx doc comp %s missing type field", comp.Name)) return false @@ -399,7 +364,7 @@ func (c *cdxDoc) pkgRequiredFields(comp *cydx.Component) bool { return true } -func (c *cdxDoc) checksums(comp *cydx.Component) []GetChecksum { +func (c *CdxDoc) checksums(comp *cydx.Component) []GetChecksum { chks := []GetChecksum{} if len(lo.FromPtr(comp.Hashes)) == 0 { @@ -416,7 +381,7 @@ func (c *cdxDoc) checksums(comp *cydx.Component) []GetChecksum { return chks } -func (c *cdxDoc) licenses(comp *cydx.Component) []licenses.License { +func (c *CdxDoc) licenses(comp *cydx.Component) []licenses.License { return aggregateLicenses(lo.FromPtr(comp.Licenses)) } @@ -446,8 +411,8 @@ func aggregateLicenses(clicenses cydx.Licenses) []licenses.License { return lics } -func (c *cdxDoc) parseTool() { - c.tools = []GetTool{} +func (c *CdxDoc) parseTool() { + c.CdxTools = []GetTool{} if c.doc.Metadata == nil { return @@ -461,41 +426,41 @@ func (c *cdxDoc) parseTool() { t := Tool{} t.Name = tt.Name t.Version = tt.Version - c.tools = append(c.tools, t) + c.CdxTools = append(c.CdxTools, t) } for _, ct := range lo.FromPtr(c.doc.Metadata.Tools.Components) { t := Tool{} t.Name = ct.Name t.Version = ct.Version - c.tools = append(c.tools, t) + c.CdxTools = append(c.CdxTools, t) } for _, ct := range lo.FromPtr(c.doc.Metadata.Tools.Services) { t := Tool{} t.Name = ct.Name t.Version = ct.Version - c.tools = append(c.tools, t) + c.CdxTools = append(c.CdxTools, t) } } -func (c *cdxDoc) parseAuthors() { - c.authors = []Author{} +func (c *CdxDoc) parseAuthors() { + c.CdxAuthors = []GetAuthor{} if c.doc.Metadata == nil { return } for _, auth := range lo.FromPtr(c.doc.Metadata.Authors) { - a := author{} - a.name = auth.Name - a.email = auth.Email - a.authorType = "person" - c.authors = append(c.authors, a) + a := Author{} + a.Name = auth.Name + a.Email = auth.Email + a.AuthorType = "person" + c.CdxAuthors = append(c.CdxAuthors, a) } } -func (c *cdxDoc) parseSupplier() { +func (c *CdxDoc) parseSupplier() { if c.doc.Metadata == nil { return } @@ -521,7 +486,7 @@ func (c *cdxDoc) parseSupplier() { c.supplier = supplier } -func (c *cdxDoc) parseManufacturer() { +func (c *CdxDoc) parseManufacturer() { if c.doc.Metadata == nil { return } @@ -547,20 +512,85 @@ func (c *cdxDoc) parseManufacturer() { c.manufacturer = m } -func (c *cdxDoc) parseRels() { - c.rels = []Relation{} +func (c *CdxDoc) parsePrimaryCompAndRelationships() { + if c.doc.Metadata == nil { + return + } + if c.doc.Metadata.Component == nil { + return + } + + c.Dependencies = make(map[string][]string) + + c.PrimaryComponent.Present = true + c.PrimaryComponent.ID = c.doc.Metadata.Component.BOMRef + var totalDependencies int + + c.rels = []GetRelation{} for _, r := range lo.FromPtr(c.doc.Dependencies) { for _, d := range lo.FromPtr(r.Dependencies) { - nr := relation{} - nr.from = r.Ref - nr.to = d - c.rels = append(c.rels, nr) + nr := Relation{} + nr.From = r.Ref + nr.To = d + if r.Ref == c.PrimaryComponent.ID { + c.PrimaryComponent.hasDependencies = true + totalDependencies++ + c.rels = append(c.rels, nr) + c.Dependencies[c.PrimaryComponent.ID] = append(c.Dependencies[c.PrimaryComponent.ID], d) + } else { + c.rels = append(c.rels, nr) + c.Dependencies[r.Ref] = append(c.Dependencies[r.Ref], d) + } } } + c.PrimaryComponent.Dependecies = totalDependencies } -func (c *cdxDoc) assignSupplier(comp *cydx.Component) *Supplier { +// nolint +func (c *CdxDoc) parseComposition() { + if c.doc.Metadata == nil { + return + } + if c.doc.Compositions == nil { + return + } + c.composition = make(map[string]string) + + for _, cp := range lo.FromPtr(c.doc.Compositions) { + state := compNormalise(cp.BOMRef) + c.composition[cp.BOMRef] = state + } +} + +// nolint +func compNormalise(compID string) string { + switch cydx.CompositionAggregate(compID) { + case cydx.CompositionAggregateComplete: + return "complete" + case cydx.CompositionAggregateIncomplete: + return "incomplete" + case cydx.CompositionAggregateIncompleteFirstPartyOnly: + return "incomplete-first-party-only" + case cydx.CompositionAggregateIncompleteFirstPartyOpenSourceOnly: + return "incomplete-first-party-open-source-only" + case cydx.CompositionAggregateIncompleteFirstPartyProprietaryOnly: + return "incomplete-first-party-proprietary-only" + case cydx.CompositionAggregateIncompleteThirdPartyOnly: + return "incomplete-third-party-only" + case cydx.CompositionAggregateIncompleteThirdPartyOpenSourceOnly: + return "incomplete-third-party-open-source-only" + case cydx.CompositionAggregateIncompleteThirdPartyProprietaryOnly: + return "incomplete-third-party-proprietary-only" + case cydx.CompositionAggregateNotSpecified: + return "not-specified" + case cydx.CompositionAggregateUnknown: + return "unknown" + } + return "not-specified" +} + +func (c *CdxDoc) assignSupplier(comp *cydx.Component) *Supplier { if comp.Supplier == nil { c.addToLogs(fmt.Sprintf("cdx doc comp %s no supplier found", comp.Name)) return nil @@ -588,20 +618,7 @@ func (c *cdxDoc) assignSupplier(comp *cydx.Component) *Supplier { return &supplier } -func (c *cdxDoc) parsePrimaryComponent() { - if c.doc.Metadata == nil { - return - } - - if c.doc.Metadata.Component == nil { - return - } - - c.primaryComponent = true - c.primaryComponentID = c.doc.Metadata.Component.BOMRef -} - -func (c *cdxDoc) parseCompositions() { +func (c *CdxDoc) parseCompositions() { if c.doc.Compositions == nil { c.compositions = map[string]string{} return diff --git a/pkg/sbom/component.go b/pkg/sbom/component.go index f128e77..02fae3b 100644 --- a/pkg/sbom/component.go +++ b/pkg/sbom/component.go @@ -25,8 +25,8 @@ type GetComponent interface { GetID() string GetName() string GetVersion() string - Cpes() []cpe.CPE - Purls() []purl.PURL + GetCpes() []cpe.CPE + GetPurls() []purl.PURL Licenses() []licenses.License GetChecksums() []GetChecksum PrimaryPurpose() string @@ -46,13 +46,14 @@ type GetComponent interface { GetPackageLicenseDeclared() string GetPackageLicenseConcluded() string ExternalReferences() []GetExternalReference + GetComposition(string) string } type Component struct { Name string Version string - cpes []cpe.CPE - purls []purl.PURL + Cpes []cpe.CPE + Purls []purl.PURL licenses []licenses.License Checksums []GetChecksum purpose string @@ -66,13 +67,14 @@ type Component struct { sourceCodeHash string isPrimary bool hasRelationships bool - relationshipState string + RelationshipState string Spdxid string FileAnalyzed bool CopyRight string PackageLicenseConcluded string PackageLicenseDeclared string ExternalRefs []GetExternalReference + composition map[string]string } func NewComponent() *Component { @@ -87,12 +89,12 @@ func (c Component) GetVersion() string { return c.Version } -func (c Component) Purls() []purl.PURL { - return c.purls +func (c Component) GetPurls() []purl.PURL { + return c.Purls } -func (c Component) Cpes() []cpe.CPE { - return c.cpes +func (c Component) GetCpes() []cpe.CPE { + return c.Cpes } func (c Component) Licenses() []licenses.License { @@ -148,7 +150,7 @@ func (c Component) HasRelationShips() bool { } func (c Component) RelationShipState() string { - return c.relationshipState + return c.RelationshipState } func (c Component) GetSpdxID() string { @@ -174,3 +176,7 @@ func (c Component) GetPackageLicenseDeclared() string { func (c Component) ExternalReferences() []GetExternalReference { return c.ExternalRefs } + +func (c Component) GetComposition(componentID string) string { + return c.composition[componentID] +} diff --git a/pkg/sbom/component_test.go b/pkg/sbom/component_test.go index aaa6709..b460f82 100644 --- a/pkg/sbom/component_test.go +++ b/pkg/sbom/component_test.go @@ -33,10 +33,10 @@ func TestGetCpeFromCompo(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cp := Component{ - cpes: tt.input, + Cpes: tt.input, } - if len(tt.input) != len(cp.cpes) { - t.Errorf("got %d, want %d", len(cp.cpes), len(tt.input)) + if len(tt.input) != len(cp.Cpes) { + t.Errorf("got %d, want %d", len(cp.Cpes), len(tt.input)) } }) } @@ -54,10 +54,10 @@ func Test_component_Purls(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { pl := Component{ - purls: tt.input, + Purls: tt.input, } - if len(tt.input) != len(pl.purls) { - t.Errorf("got %d, want %d", len(pl.purls), len(tt.input)) + if len(tt.input) != len(pl.Purls) { + t.Errorf("got %d, want %d", len(pl.Purls), len(tt.input)) } }) } diff --git a/pkg/sbom/document.go b/pkg/sbom/document.go index 3cca7ca..50b9458 100644 --- a/pkg/sbom/document.go +++ b/pkg/sbom/document.go @@ -20,14 +20,15 @@ package sbom type Document interface { Spec() Spec Components() []GetComponent - Relations() []Relation - Authors() []Author + Relations() []GetRelation + Authors() []GetAuthor Tools() []GetTool Logs() []string - PrimaryComponent() bool - Lifecycles() []string Manufacturer() Manufacturer Supplier() GetSupplier + + PrimaryComp() GetPrimaryComp + GetRelationships(string) []string } diff --git a/pkg/sbom/primarycomp.go b/pkg/sbom/primarycomp.go new file mode 100644 index 0000000..b1ebfe2 --- /dev/null +++ b/pkg/sbom/primarycomp.go @@ -0,0 +1,44 @@ +// 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 +// +// https://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 GetPrimaryComp interface { + IsPresent() bool + GetID() string + GetTotalNoOfDependencies() int +} + +type PrimaryComp struct { + Present bool + ID string + Dependecies int + hasDependencies bool +} + +func (pc *PrimaryComp) IsPresent() bool { + return pc.Present +} + +func (pc *PrimaryComp) GetID() string { + return pc.ID +} + +func (pc *PrimaryComp) GetTotalNoOfDependencies() int { + return pc.Dependecies +} + +func (pc *PrimaryComp) HasDependencies() bool { + return pc.hasDependencies +} diff --git a/pkg/sbom/relation.go b/pkg/sbom/relation.go index c756619..87dbaa1 100644 --- a/pkg/sbom/relation.go +++ b/pkg/sbom/relation.go @@ -15,20 +15,20 @@ package sbom //counterfeiter:generate . Relation -type Relation interface { - From() string - To() string +type GetRelation interface { + GetFrom() string + GetTo() string } -type relation struct { - from string - to string +type Relation struct { + From string + To string } -func (r relation) From() string { - return r.from +func (r Relation) GetFrom() string { + return r.From } -func (r relation) To() string { - return r.to +func (r Relation) GetTo() string { + return r.To } diff --git a/pkg/sbom/sbomfakes/fake_author.go b/pkg/sbom/sbomfakes/fake_author.go index d559dcc..6528087 100644 --- a/pkg/sbom/sbomfakes/fake_author.go +++ b/pkg/sbom/sbomfakes/fake_author.go @@ -42,7 +42,7 @@ type FakeAuthor struct { invocationsMutex sync.RWMutex } -func (fake *FakeAuthor) Email() string { +func (fake *FakeAuthor) GetEmail() string { fake.emailMutex.Lock() ret, specificReturn := fake.emailReturnsOnCall[len(fake.emailArgsForCall)] fake.emailArgsForCall = append(fake.emailArgsForCall, struct { @@ -95,7 +95,7 @@ func (fake *FakeAuthor) EmailReturnsOnCall(i int, result1 string) { }{result1} } -func (fake *FakeAuthor) Name() string { +func (fake *FakeAuthor) GetName() string { fake.nameMutex.Lock() ret, specificReturn := fake.nameReturnsOnCall[len(fake.nameArgsForCall)] fake.nameArgsForCall = append(fake.nameArgsForCall, struct { @@ -148,7 +148,7 @@ func (fake *FakeAuthor) NameReturnsOnCall(i int, result1 string) { }{result1} } -func (fake *FakeAuthor) Type() string { +func (fake *FakeAuthor) GetType() string { fake.typeMutex.Lock() ret, specificReturn := fake.typeReturnsOnCall[len(fake.typeArgsForCall)] fake.typeArgsForCall = append(fake.typeArgsForCall, struct { @@ -229,4 +229,4 @@ func (fake *FakeAuthor) recordInvocation(key string, args []interface{}) { fake.invocations[key] = append(fake.invocations[key], args) } -var _ sbom.Author = new(FakeAuthor) +var _ sbom.GetAuthor = new(FakeAuthor) diff --git a/pkg/sbom/spdx.go b/pkg/sbom/spdx.go index c7509f3..3770531 100644 --- a/pkg/sbom/spdx.go +++ b/pkg/sbom/spdx.go @@ -43,18 +43,19 @@ var ( ) type SpdxDoc struct { - doc *spdx.Document - format FileFormat - ctx context.Context - SpdxSpec *Specs - Comps []GetComponent - authors []Author - SpdxTools []GetTool - rels []Relation - logs []string - primaryComponent bool - primaryComponentID string - lifecycles string + doc *spdx.Document + format FileFormat + ctx context.Context + SpdxSpec *Specs + Comps []GetComponent + authors []GetAuthor + SpdxTools []GetTool + Rels []GetRelation + logs []string + PrimaryComponent PrimaryComp + lifecycles string + Dependencies map[string][]string + composition map[string]string } func newSPDXDoc(ctx context.Context, f io.ReadSeeker, format FileFormat) (Document, error) { @@ -96,6 +97,10 @@ func newSPDXDoc(ctx context.Context, f io.ReadSeeker, format FileFormat) (Docume return doc, err } +func (s SpdxDoc) PrimaryComp() GetPrimaryComp { + return &s.PrimaryComponent +} + func (s SpdxDoc) Spec() Spec { return *s.SpdxSpec } @@ -104,7 +109,7 @@ func (s SpdxDoc) Components() []GetComponent { return s.Comps } -func (s SpdxDoc) Authors() []Author { +func (s SpdxDoc) Authors() []GetAuthor { return s.authors } @@ -112,18 +117,14 @@ func (s SpdxDoc) Tools() []GetTool { return s.SpdxTools } -func (s SpdxDoc) Relations() []Relation { - return s.rels +func (s SpdxDoc) Relations() []GetRelation { + return s.Rels } func (s SpdxDoc) Logs() []string { return s.logs } -func (s SpdxDoc) PrimaryComponent() bool { - return s.primaryComponent -} - func (s SpdxDoc) Lifecycles() []string { return []string{s.lifecycles} } @@ -136,12 +137,19 @@ func (s SpdxDoc) Supplier() GetSupplier { return nil } +func (s SpdxDoc) GetRelationships(componentID string) []string { + return s.Dependencies[componentID] +} + +func (s SpdxDoc) GetComposition(componentID string) string { + return s.composition[componentID] +} + func (s *SpdxDoc) parse() { s.parseSpec() s.parseAuthors() s.parseTool() - s.parseRels() - s.parsePrimaryComponent() + s.parsePrimaryCompAndRelationships() s.parseComps() } @@ -200,8 +208,8 @@ func (s *SpdxDoc) parseComps() { nc.CopyRight = sc.PackageCopyrightText nc.FileAnalyzed = sc.FilesAnalyzed nc.isReqFieldsPresent = s.pkgRequiredFields(index) - nc.purls = s.purls(index) - nc.cpes = s.cpes(index) + nc.Purls = s.purls(index) + nc.Cpes = s.cpes(index) nc.Checksums = s.checksums(index) nc.ExternalRefs = s.externalRefs(index) nc.licenses = s.licenses(index) @@ -236,26 +244,26 @@ func (s *SpdxDoc) parseComps() { nc.DownloadLocation = sc.PackageDownloadLocation } - nc.isPrimary = s.primaryComponentID == string(sc.PackageSPDXIdentifier) + nc.isPrimary = s.PrimaryComponent.ID == string(sc.PackageSPDXIdentifier) - fromRelsPresent := func(rels []Relation, id string) bool { + fromRelsPresent := func(rels []GetRelation, id string) bool { for _, r := range rels { - if r.From() == id { + if strings.Contains(r.GetFrom(), id) { return true } } return false } - nc.hasRelationships = fromRelsPresent(s.rels, string(sc.PackageSPDXIdentifier)) - nc.relationshipState = "not-specified" + nc.hasRelationships = fromRelsPresent(s.Rels, string(sc.PackageSPDXIdentifier)) + nc.RelationshipState = "not-specified" s.Comps = append(s.Comps, nc) } } func (s *SpdxDoc) parseAuthors() { - s.authors = []Author{} + s.authors = []GetAuthor{} if s.doc.CreationInfo == nil { return @@ -266,47 +274,71 @@ func (s *SpdxDoc) parseAuthors() { if ctType == "tool" { continue } - a := author{} + a := Author{} entity := parseEntity(fmt.Sprintf("%s: %s", c.CreatorType, c.Creator)) if entity != nil { - a.name = entity.name - a.email = entity.email - a.authorType = ctType + a.Name = entity.name + a.Email = entity.email + a.AuthorType = ctType s.authors = append(s.authors, a) } } } -func (s *SpdxDoc) parseRels() { - s.rels = []Relation{} - +func (s *SpdxDoc) parsePrimaryCompAndRelationships() { + s.Rels = []GetRelation{} + s.Dependencies = make(map[string][]string) var err error var aBytes, bBytes []byte + var primaryComponent string + var totalDependencies int for _, r := range s.doc.Relationships { - nr := relation{} - switch strings.ToUpper(r.Relationship) { - case spdx_common.TypeRelationshipDescribe: - fallthrough - case spdx_common.TypeRelationshipContains: - fallthrough - case spdx_common.TypeRelationshipDependsOn: - aBytes, err = r.RefA.MarshalJSON() + // check relation type DESCRIBE + if strings.ToUpper(r.Relationship) == spdx_common.TypeRelationshipDescribe { + bBytes, err = r.RefB.ElementRefID.MarshalJSON() if err != nil { continue } + primaryComponent = string(bBytes) + s.PrimaryComponent.ID = primaryComponent + s.PrimaryComponent.Present = true + } + } - bBytes, err = r.RefB.MarshalJSON() + for _, r := range s.doc.Relationships { + if strings.ToUpper(r.Relationship) == spdx_common.TypeRelationshipDependsOn { + aBytes, err = r.RefA.MarshalJSON() if err != nil { continue } - nr.from = string(aBytes) - nr.to = string(bBytes) - s.rels = append(s.rels, nr) + if string(aBytes) == primaryComponent { + bBytes, err = r.RefB.MarshalJSON() + if err != nil { + continue + } + + nr := Relation{ + From: primaryComponent, + To: string(bBytes), + } + totalDependencies++ + + s.Rels = append(s.Rels, nr) + s.Dependencies[primaryComponent] = append(s.Dependencies[primaryComponent], string(bBytes)) + } else { + nr := Relation{ + From: string(aBytes), + To: string(bBytes), + } + s.Dependencies[string(aBytes)] = append(s.Dependencies[string(aBytes)], string(bBytes)) + s.Rels = append(s.Rels, nr) + } } } + s.PrimaryComponent.Dependecies = totalDependencies } // creationInfo.Creators.Tool @@ -605,23 +637,27 @@ func (s *SpdxDoc) getSupplier(index int) *Supplier { } } -func (s *SpdxDoc) parsePrimaryComponent() { - pkgIDs := make(map[string]*spdx.Package) +// nolint +// https://github.com/spdx/ntia-conformance-checker/issues/100 +// Add spdx support to check both supplier and originator +func (s *SpdxDoc) addSupplierName(index int) string { + supplier := s.getSupplier(index) + manufacturer := s.getManufacturer(index) - for _, pkg := range s.doc.Packages { - pkgIDs[string(pkg.PackageSPDXIdentifier)] = pkg + if supplier == nil && manufacturer == nil { + s.addToLogs(fmt.Sprintf("spdx doc pkg %s at index %d no supplier/originator found", s.doc.Packages[index].PackageName, index)) + return "" } - for _, r := range s.doc.Relationships { - if strings.ToUpper(r.Relationship) == spdx_common.TypeRelationshipDescribe { - _, ok := pkgIDs[string(r.RefB.ElementRefID)] - if ok { - s.primaryComponentID = string(r.RefB.ElementRefID) - s.primaryComponent = true - return - } - } + if supplier != nil { + return supplier.Name } + + if manufacturer != nil { + return manufacturer.Name + } + + return "" } type entity struct { diff --git a/pkg/scorer/ntia.go b/pkg/scorer/ntia.go index aab6244..9a5f552 100644 --- a/pkg/scorer/ntia.go +++ b/pkg/scorer/ntia.go @@ -116,11 +116,11 @@ func compWithUniqIDCheck(d sbom.Document, c *check) score { func docWithDepedenciesCheck(d sbom.Document, c *check) score { s := newScoreFromCheck(c) - withRelations := len(d.Relations()) - if withRelations > 0 { + totalDependencies := d.PrimaryComp().GetTotalNoOfDependencies() + if totalDependencies > 0 { s.setScore(10.0) } - s.setDesc(fmt.Sprintf("doc has %d relationships ", withRelations)) + s.setDesc(fmt.Sprintf("doc has %d dependencies ", totalDependencies)) return *s } diff --git a/pkg/scorer/quality.go b/pkg/scorer/quality.go index 290dd21..4ee96e9 100644 --- a/pkg/scorer/quality.go +++ b/pkg/scorer/quality.go @@ -170,7 +170,7 @@ func compWithAnyLookupIDCheck(d sbom.Document, c *check) score { } withAnyLookupID := lo.CountBy(d.Components(), func(c sbom.GetComponent) bool { - if len(c.Cpes()) > 0 || len(c.Purls()) > 0 { + if len(c.GetCpes()) > 0 || len(c.GetPurls()) > 0 { return true } return false @@ -197,7 +197,7 @@ func compWithMultipleIDCheck(d sbom.Document, c *check) score { } withMultipleID := lo.CountBy(d.Components(), func(c sbom.GetComponent) bool { - if len(c.Cpes()) > 0 && len(c.Purls()) > 0 { + if len(c.GetCpes()) > 0 && len(c.GetPurls()) > 0 { return true } return false @@ -231,7 +231,7 @@ func docWithCreatorCheck(d sbom.Document, c *check) score { func docWithPrimaryComponentCheck(d sbom.Document, c *check) score { s := newScoreFromCheck(c) - if d.PrimaryComponent() { + if d.PrimaryComp().IsPresent() { s.setScore(10.0) s.setDesc("primary component found") return *s