From c0456a05b37dfef784755011d2355d5b0fe9bee1 Mon Sep 17 00:00:00 2001 From: Suman Mandal Date: Mon, 9 Nov 2026 05:36:50 +0000 Subject: [PATCH 1/3] chore: migrate ChainRisk Lens to organization module Signed-off-by: Suman Mandal --- cmd/chainrisk-lens/main.go | 10 +- cmd/chainrisk-lens/main_test.go | 24 +- cmd/chainrisk-lens/setup.go | 2 +- cmd/chainrisk-lens/setup_test.go | 6 +- go.mod | 2 +- internal/agent/investigator.go | 2 +- internal/agent/investigator_test.go | 2 +- internal/analysis/analysis.go | 2 +- internal/analysis/analysis_test.go | 4 +- internal/graph/graph.go | 2 +- internal/graph/graph_test.go | 2 +- internal/investigation/investigation.go | 4 +- internal/investigation/investigation_test.go | 539 +++++++++---------- internal/sbom/sbom.go | 90 ++-- internal/sbom/sbom_test.go | 129 ++--- 15 files changed, 416 insertions(+), 404 deletions(-) diff --git a/cmd/chainrisk-lens/main.go b/cmd/chainrisk-lens/main.go index b0939bf..7641a76 100644 --- a/cmd/chainrisk-lens/main.go +++ b/cmd/chainrisk-lens/main.go @@ -11,11 +11,11 @@ import ( "sort" "strings" - "github.com/jijo-OO7/chainrisk-lens/internal/agent" - "github.com/jijo-OO7/chainrisk-lens/internal/analysis" - "github.com/jijo-OO7/chainrisk-lens/internal/graph" - "github.com/jijo-OO7/chainrisk-lens/internal/investigation" - "github.com/jijo-OO7/chainrisk-lens/internal/sbom" + "github.com/Chain-Risk/chainrisk-lens/internal/agent" + "github.com/Chain-Risk/chainrisk-lens/internal/analysis" + "github.com/Chain-Risk/chainrisk-lens/internal/graph" + "github.com/Chain-Risk/chainrisk-lens/internal/investigation" + "github.com/Chain-Risk/chainrisk-lens/internal/sbom" ) const ( diff --git a/cmd/chainrisk-lens/main_test.go b/cmd/chainrisk-lens/main_test.go index 640180b..09aba75 100644 --- a/cmd/chainrisk-lens/main_test.go +++ b/cmd/chainrisk-lens/main_test.go @@ -11,11 +11,11 @@ import ( "strings" "testing" - "github.com/jijo-OO7/chainrisk-lens/internal/agent" - "github.com/jijo-OO7/chainrisk-lens/internal/analysis" - "github.com/jijo-OO7/chainrisk-lens/internal/graph" - "github.com/jijo-OO7/chainrisk-lens/internal/investigation" - "github.com/jijo-OO7/chainrisk-lens/internal/sbom" + "github.com/Chain-Risk/chainrisk-lens/internal/agent" + "github.com/Chain-Risk/chainrisk-lens/internal/analysis" + "github.com/Chain-Risk/chainrisk-lens/internal/graph" + "github.com/Chain-Risk/chainrisk-lens/internal/investigation" + "github.com/Chain-Risk/chainrisk-lens/internal/sbom" ) type fakeInvestigator struct { @@ -650,13 +650,25 @@ func fakeInvestigationResult() investigation.Result { func writeSBOM(t *testing.T, document sbom.SBOM) string { t.Helper() - data, err := json.Marshal(document) + fixture := struct { + BOMFormat string `json:"bomFormat"` + Components []sbom.Component `json:"components"` + Dependencies []sbom.Dependency `json:"dependencies"` + }{ + BOMFormat: "CycloneDX", + Components: document.Components, + Dependencies: document.Dependencies, + } + + data, err := json.Marshal(fixture) if err != nil { t.Fatal(err) } + path := filepath.Join(t.TempDir(), "sbom.json") if err := os.WriteFile(path, data, 0o600); err != nil { t.Fatal(err) } + return path } diff --git a/cmd/chainrisk-lens/setup.go b/cmd/chainrisk-lens/setup.go index a50cd90..fc92714 100644 --- a/cmd/chainrisk-lens/setup.go +++ b/cmd/chainrisk-lens/setup.go @@ -14,7 +14,7 @@ import ( "strings" "time" - "github.com/jijo-OO7/chainrisk-lens/internal/agent" + "github.com/Chain-Risk/chainrisk-lens/internal/agent" ) const ( diff --git a/cmd/chainrisk-lens/setup_test.go b/cmd/chainrisk-lens/setup_test.go index 3a10530..cac6544 100644 --- a/cmd/chainrisk-lens/setup_test.go +++ b/cmd/chainrisk-lens/setup_test.go @@ -9,9 +9,9 @@ import ( "strings" "testing" - "github.com/jijo-OO7/chainrisk-lens/internal/agent" - "github.com/jijo-OO7/chainrisk-lens/internal/investigation" - "github.com/jijo-OO7/chainrisk-lens/internal/sbom" + "github.com/Chain-Risk/chainrisk-lens/internal/agent" + "github.com/Chain-Risk/chainrisk-lens/internal/investigation" + "github.com/Chain-Risk/chainrisk-lens/internal/sbom" ) func TestDetectSetupStatusPlatformAndWSL(t *testing.T) { diff --git a/go.mod b/go.mod index 4bb7aa5..24ebf16 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ -module github.com/jijo-OO7/chainrisk-lens +module github.com/Chain-Risk/chainrisk-lens go 1.25.0 diff --git a/internal/agent/investigator.go b/internal/agent/investigator.go index d70d1d3..f4d7bcd 100644 --- a/internal/agent/investigator.go +++ b/internal/agent/investigator.go @@ -9,7 +9,7 @@ import ( "sort" "strings" - "github.com/jijo-OO7/chainrisk-lens/internal/investigation" + "github.com/Chain-Risk/chainrisk-lens/internal/investigation" ) // Model generates a response from a prompt. Runtime and provider details belong diff --git a/internal/agent/investigator_test.go b/internal/agent/investigator_test.go index 1fbfdb3..dc29948 100644 --- a/internal/agent/investigator_test.go +++ b/internal/agent/investigator_test.go @@ -8,7 +8,7 @@ import ( "strings" "testing" - "github.com/jijo-OO7/chainrisk-lens/internal/investigation" + "github.com/Chain-Risk/chainrisk-lens/internal/investigation" ) type fakeModel struct { diff --git a/internal/analysis/analysis.go b/internal/analysis/analysis.go index ff7693f..235986a 100644 --- a/internal/analysis/analysis.go +++ b/internal/analysis/analysis.go @@ -5,7 +5,7 @@ import ( "fmt" "sort" - dependencygraph "github.com/jijo-OO7/chainrisk-lens/internal/graph" + dependencygraph "github.com/Chain-Risk/chainrisk-lens/internal/graph" ) // ErrTargetNotFound indicates that the requested target is not in the graph. diff --git a/internal/analysis/analysis_test.go b/internal/analysis/analysis_test.go index 7c38042..edf0026 100644 --- a/internal/analysis/analysis_test.go +++ b/internal/analysis/analysis_test.go @@ -5,8 +5,8 @@ import ( "reflect" "testing" - dependencygraph "github.com/jijo-OO7/chainrisk-lens/internal/graph" - "github.com/jijo-OO7/chainrisk-lens/internal/sbom" + dependencygraph "github.com/Chain-Risk/chainrisk-lens/internal/graph" + "github.com/Chain-Risk/chainrisk-lens/internal/sbom" ) type propagationEdge struct { diff --git a/internal/graph/graph.go b/internal/graph/graph.go index c88b1ba..ee447c2 100644 --- a/internal/graph/graph.go +++ b/internal/graph/graph.go @@ -4,7 +4,7 @@ import ( "fmt" "sort" - "github.com/jijo-OO7/chainrisk-lens/internal/sbom" + "github.com/Chain-Risk/chainrisk-lens/internal/sbom" ) // Node is a component in the dependency graph. diff --git a/internal/graph/graph_test.go b/internal/graph/graph_test.go index 6999de5..ea7c6e4 100644 --- a/internal/graph/graph_test.go +++ b/internal/graph/graph_test.go @@ -5,7 +5,7 @@ import ( "strings" "testing" - "github.com/jijo-OO7/chainrisk-lens/internal/sbom" + "github.com/Chain-Risk/chainrisk-lens/internal/sbom" ) func TestGraphCases(t *testing.T) { diff --git a/internal/investigation/investigation.go b/internal/investigation/investigation.go index e49727b..7358b45 100644 --- a/internal/investigation/investigation.go +++ b/internal/investigation/investigation.go @@ -6,8 +6,8 @@ import ( "sort" "strings" - "github.com/jijo-OO7/chainrisk-lens/internal/analysis" - "github.com/jijo-OO7/chainrisk-lens/internal/graph" + "github.com/Chain-Risk/chainrisk-lens/internal/analysis" + "github.com/Chain-Risk/chainrisk-lens/internal/graph" ) // Component is component metadata included in deterministic evidence. diff --git a/internal/investigation/investigation_test.go b/internal/investigation/investigation_test.go index 7e10729..13d9acf 100644 --- a/internal/investigation/investigation_test.go +++ b/internal/investigation/investigation_test.go @@ -1,270 +1,269 @@ -package investigation - -import ( - "reflect" - "strings" - "testing" - - "github.com/jijo-OO7/chainrisk-lens/internal/analysis" - "github.com/jijo-OO7/chainrisk-lens/internal/graph" - "github.com/jijo-OO7/chainrisk-lens/internal/sbom" -) - -type testEdge struct { - from string - to string -} - -func TestFromAnalysis(t *testing.T) { - result := analyzeFixture(t, - []string{"target", "service", "worker"}, - []testEdge{{from: "target", to: "service"}, {from: "service", to: "worker"}, {from: "worker", to: "target"}}, - "target", - ) - - evidence, err := FromAnalysis(result) - if err != nil { - t.Fatalf("FromAnalysis() error = %v", err) - } - want := Evidence{ - Target: Component{BOMRef: "target", Name: "target", Version: "1"}, - Impacts: []Impact{ - {BOMRef: "service", Name: "service", Version: "1", Depth: 1, Path: []string{"target", "service"}}, - {BOMRef: "worker", Name: "worker", Version: "1", Depth: 2, Path: []string{"target", "service", "worker"}}, - }, - MaxDepth: 2, - Cycle: true, - } - if !reflect.DeepEqual(evidence, want) { - t.Fatalf("FromAnalysis() = %#v, want %#v", evidence, want) - } - if err := evidence.Validate(); err != nil { - t.Errorf("Evidence.Validate() error = %v", err) - } -} - -func TestFromAnalysisCopiesEvidence(t *testing.T) { - result := analyzeFixture(t, - []string{"target", "dependent"}, - []testEdge{{from: "target", to: "dependent"}}, - "target", - ) - - evidence, err := FromAnalysis(result) - if err != nil { - t.Fatal(err) - } - evidence.Impacts[0].Path[0] = "changed" - if evidence.Impacts[0].Path[0] == result.Target.BOMRef { - t.Fatal("test mutation did not change copied evidence path") - } - - path, exists := result.PathTo("dependent") - if !exists || path[0].BOMRef != "target" { - t.Fatalf("mutating evidence changed analysis path: %#v, %t", path, exists) - } -} - -func TestFromAnalysisEquivalentResults(t *testing.T) { - components := []string{"target", "alpha", "beta", "leaf"} - edges := []testEdge{ - {from: "target", to: "beta"}, - {from: "target", to: "alpha"}, - {from: "beta", to: "leaf"}, - {from: "alpha", to: "leaf"}, - } - first := analyzeFixture(t, components, edges, "target") - second := analyzeFixture(t, reverseComponents(components), reverseEdges(edges), "target") - - firstEvidence, err := FromAnalysis(first) - if err != nil { - t.Fatal(err) - } - secondEvidence, err := FromAnalysis(second) - if err != nil { - t.Fatal(err) - } - if !reflect.DeepEqual(firstEvidence, secondEvidence) { - t.Fatalf("equivalent analyses produced different evidence:\n%#v\n%#v", firstEvidence, secondEvidence) - } - if !reflect.DeepEqual(firstEvidence.Impacts[2].Path, []string{"target", "alpha", "leaf"}) { - t.Errorf("canonical path = %v, want [target alpha leaf]", firstEvidence.Impacts[2].Path) - } -} - -func TestEvidenceValidateRejectsInvalidEvidence(t *testing.T) { - valid := Evidence{ - Target: Component{BOMRef: "target"}, - Impacts: []Impact{{BOMRef: "dependent", Depth: 1, Path: []string{"target", "dependent"}}}, - MaxDepth: 1, - } - tests := []struct { - name string - mutate func(*Evidence) - wantPart string - }{ - { - name: "missing target BOMRef", - mutate: func(evidence *Evidence) { evidence.Target.BOMRef = "" }, - wantPart: "target BOM-REF is empty", - }, - { - name: "missing impact BOMRef", - mutate: func(evidence *Evidence) { evidence.Impacts[0].BOMRef = "" }, - wantPart: "impact BOM-REF is empty", - }, - { - name: "target included as impact", - mutate: func(evidence *Evidence) { evidence.Impacts[0].BOMRef = "target" }, - wantPart: "must not appear in impacts", - }, - { - name: "empty path", - mutate: func(evidence *Evidence) { evidence.Impacts[0].Path = nil }, - wantPart: "empty path", - }, - { - name: "path starts elsewhere", - mutate: func(evidence *Evidence) { evidence.Impacts[0].Path[0] = "other" }, - wantPart: "does not begin at the target", - }, - { - name: "path ends elsewhere", - mutate: func(evidence *Evidence) { evidence.Impacts[0].Path[1] = "other" }, - wantPart: "does not end at the impact", - }, - { - name: "invalid depth", - mutate: func(evidence *Evidence) { evidence.Impacts[0].Depth = 0 }, - wantPart: "invalid depth", - }, - { - name: "path depth mismatch", - mutate: func(evidence *Evidence) { evidence.Impacts[0].Depth = 2 }, - wantPart: "does not match path length", - }, - { - name: "max depth mismatch", - mutate: func(evidence *Evidence) { evidence.MaxDepth = 2 }, - wantPart: "does not match impacts max depth", - }, - { - name: "duplicate impact", - mutate: func(evidence *Evidence) { - evidence.Impacts = append(evidence.Impacts, evidence.Impacts[0]) - }, - wantPart: "duplicate impact BOM-REF", - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - evidence := cloneEvidence(valid) - test.mutate(&evidence) - if err := evidence.Validate(); err == nil || !strings.Contains(err.Error(), test.wantPart) { - t.Fatalf("Validate() error = %v, want error containing %q", err, test.wantPart) - } - }) - } -} - -func TestEvidenceValidateAllowsNoImpacts(t *testing.T) { - evidence := Evidence{Target: Component{BOMRef: "target"}, Impacts: []Impact{}, MaxDepth: 0} - if err := evidence.Validate(); err != nil { - t.Fatalf("Validate() error = %v", err) - } -} - -func TestResultValidateRequiresTraceableFindings(t *testing.T) { - evidence := Evidence{ - Target: Component{BOMRef: "target"}, - Impacts: []Impact{{BOMRef: "dependent", Depth: 1, Path: []string{"target", "dependent"}}}, - MaxDepth: 1, - } - valid := Result{Findings: []Finding{{ - Statement: "The dependent is potentially exposed through the supplied path.", - Evidence: []EvidenceRef{{BOMRefs: []string{"target", "dependent"}}}, - }}} - if err := valid.Validate(evidence); err != nil { - t.Fatalf("Validate() error = %v", err) - } - - tests := []struct { - name string - result Result - wantPart string - }{ - { - name: "missing evidence reference", - result: Result{Findings: []Finding{{Statement: "A claim."}}}, - wantPart: "no evidence references", - }, - { - name: "unknown BOMRef", - result: Result{Findings: []Finding{{ - Statement: "A claim.", Evidence: []EvidenceRef{{BOMRefs: []string{"not-supplied"}}}, - }}}, - wantPart: "not present in supplied evidence", - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if err := test.result.Validate(evidence); err == nil || !strings.Contains(err.Error(), test.wantPart) { - t.Fatalf("Validate() error = %v, want error containing %q", err, test.wantPart) - } - }) - } -} - -func analyzeFixture(t *testing.T, componentRefs []string, edges []testEdge, target string) analysis.Analysis { - t.Helper() - document := sbom.SBOM{ - Components: make([]sbom.Component, 0, len(componentRefs)), - } - for _, bomRef := range componentRefs { - document.Components = append(document.Components, sbom.Component{ - BOMRef: bomRef, Name: bomRef, Version: "1", - }) - } - for _, edge := range edges { - document.Dependencies = append(document.Dependencies, sbom.Dependency{ - Ref: edge.to, DependsOn: []string{edge.from}, - }) - } - dependencyGraph, err := graph.New(document) - if err != nil { - t.Fatalf("graph.New() error = %v", err) - } - result, err := analysis.Analyze(dependencyGraph, target) - if err != nil { - t.Fatalf("analysis.Analyze() error = %v", err) - } - return result -} - -func cloneEvidence(evidence Evidence) Evidence { - clone := evidence - clone.Impacts = make([]Impact, len(evidence.Impacts)) - for i, impact := range evidence.Impacts { - clone.Impacts[i] = impact - clone.Impacts[i].Path = append([]string(nil), impact.Path...) - } - return clone -} - -func reverseComponents(values []string) []string { - reversed := make([]string, len(values)) - for i, value := range values { - reversed[len(values)-1-i] = value - } - return reversed -} - -func reverseEdges(values []testEdge) []testEdge { - reversed := make([]testEdge, len(values)) - for i, value := range values { - reversed[len(values)-1-i] = value - } - return reversed -} - +package investigation + +import ( + "reflect" + "strings" + "testing" + + "github.com/Chain-Risk/chainrisk-lens/internal/analysis" + "github.com/Chain-Risk/chainrisk-lens/internal/graph" + "github.com/Chain-Risk/chainrisk-lens/internal/sbom" +) + +type testEdge struct { + from string + to string +} + +func TestFromAnalysis(t *testing.T) { + result := analyzeFixture(t, + []string{"target", "service", "worker"}, + []testEdge{{from: "target", to: "service"}, {from: "service", to: "worker"}, {from: "worker", to: "target"}}, + "target", + ) + + evidence, err := FromAnalysis(result) + if err != nil { + t.Fatalf("FromAnalysis() error = %v", err) + } + want := Evidence{ + Target: Component{BOMRef: "target", Name: "target", Version: "1"}, + Impacts: []Impact{ + {BOMRef: "service", Name: "service", Version: "1", Depth: 1, Path: []string{"target", "service"}}, + {BOMRef: "worker", Name: "worker", Version: "1", Depth: 2, Path: []string{"target", "service", "worker"}}, + }, + MaxDepth: 2, + Cycle: true, + } + if !reflect.DeepEqual(evidence, want) { + t.Fatalf("FromAnalysis() = %#v, want %#v", evidence, want) + } + if err := evidence.Validate(); err != nil { + t.Errorf("Evidence.Validate() error = %v", err) + } +} + +func TestFromAnalysisCopiesEvidence(t *testing.T) { + result := analyzeFixture(t, + []string{"target", "dependent"}, + []testEdge{{from: "target", to: "dependent"}}, + "target", + ) + + evidence, err := FromAnalysis(result) + if err != nil { + t.Fatal(err) + } + evidence.Impacts[0].Path[0] = "changed" + if evidence.Impacts[0].Path[0] == result.Target.BOMRef { + t.Fatal("test mutation did not change copied evidence path") + } + + path, exists := result.PathTo("dependent") + if !exists || path[0].BOMRef != "target" { + t.Fatalf("mutating evidence changed analysis path: %#v, %t", path, exists) + } +} + +func TestFromAnalysisEquivalentResults(t *testing.T) { + components := []string{"target", "alpha", "beta", "leaf"} + edges := []testEdge{ + {from: "target", to: "beta"}, + {from: "target", to: "alpha"}, + {from: "beta", to: "leaf"}, + {from: "alpha", to: "leaf"}, + } + first := analyzeFixture(t, components, edges, "target") + second := analyzeFixture(t, reverseComponents(components), reverseEdges(edges), "target") + + firstEvidence, err := FromAnalysis(first) + if err != nil { + t.Fatal(err) + } + secondEvidence, err := FromAnalysis(second) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(firstEvidence, secondEvidence) { + t.Fatalf("equivalent analyses produced different evidence:\n%#v\n%#v", firstEvidence, secondEvidence) + } + if !reflect.DeepEqual(firstEvidence.Impacts[2].Path, []string{"target", "alpha", "leaf"}) { + t.Errorf("canonical path = %v, want [target alpha leaf]", firstEvidence.Impacts[2].Path) + } +} + +func TestEvidenceValidateRejectsInvalidEvidence(t *testing.T) { + valid := Evidence{ + Target: Component{BOMRef: "target"}, + Impacts: []Impact{{BOMRef: "dependent", Depth: 1, Path: []string{"target", "dependent"}}}, + MaxDepth: 1, + } + tests := []struct { + name string + mutate func(*Evidence) + wantPart string + }{ + { + name: "missing target BOMRef", + mutate: func(evidence *Evidence) { evidence.Target.BOMRef = "" }, + wantPart: "target BOM-REF is empty", + }, + { + name: "missing impact BOMRef", + mutate: func(evidence *Evidence) { evidence.Impacts[0].BOMRef = "" }, + wantPart: "impact BOM-REF is empty", + }, + { + name: "target included as impact", + mutate: func(evidence *Evidence) { evidence.Impacts[0].BOMRef = "target" }, + wantPart: "must not appear in impacts", + }, + { + name: "empty path", + mutate: func(evidence *Evidence) { evidence.Impacts[0].Path = nil }, + wantPart: "empty path", + }, + { + name: "path starts elsewhere", + mutate: func(evidence *Evidence) { evidence.Impacts[0].Path[0] = "other" }, + wantPart: "does not begin at the target", + }, + { + name: "path ends elsewhere", + mutate: func(evidence *Evidence) { evidence.Impacts[0].Path[1] = "other" }, + wantPart: "does not end at the impact", + }, + { + name: "invalid depth", + mutate: func(evidence *Evidence) { evidence.Impacts[0].Depth = 0 }, + wantPart: "invalid depth", + }, + { + name: "path depth mismatch", + mutate: func(evidence *Evidence) { evidence.Impacts[0].Depth = 2 }, + wantPart: "does not match path length", + }, + { + name: "max depth mismatch", + mutate: func(evidence *Evidence) { evidence.MaxDepth = 2 }, + wantPart: "does not match impacts max depth", + }, + { + name: "duplicate impact", + mutate: func(evidence *Evidence) { + evidence.Impacts = append(evidence.Impacts, evidence.Impacts[0]) + }, + wantPart: "duplicate impact BOM-REF", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + evidence := cloneEvidence(valid) + test.mutate(&evidence) + if err := evidence.Validate(); err == nil || !strings.Contains(err.Error(), test.wantPart) { + t.Fatalf("Validate() error = %v, want error containing %q", err, test.wantPart) + } + }) + } +} + +func TestEvidenceValidateAllowsNoImpacts(t *testing.T) { + evidence := Evidence{Target: Component{BOMRef: "target"}, Impacts: []Impact{}, MaxDepth: 0} + if err := evidence.Validate(); err != nil { + t.Fatalf("Validate() error = %v", err) + } +} + +func TestResultValidateRequiresTraceableFindings(t *testing.T) { + evidence := Evidence{ + Target: Component{BOMRef: "target"}, + Impacts: []Impact{{BOMRef: "dependent", Depth: 1, Path: []string{"target", "dependent"}}}, + MaxDepth: 1, + } + valid := Result{Findings: []Finding{{ + Statement: "The dependent is potentially exposed through the supplied path.", + Evidence: []EvidenceRef{{BOMRefs: []string{"target", "dependent"}}}, + }}} + if err := valid.Validate(evidence); err != nil { + t.Fatalf("Validate() error = %v", err) + } + + tests := []struct { + name string + result Result + wantPart string + }{ + { + name: "missing evidence reference", + result: Result{Findings: []Finding{{Statement: "A claim."}}}, + wantPart: "no evidence references", + }, + { + name: "unknown BOMRef", + result: Result{Findings: []Finding{{ + Statement: "A claim.", Evidence: []EvidenceRef{{BOMRefs: []string{"not-supplied"}}}, + }}}, + wantPart: "not present in supplied evidence", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if err := test.result.Validate(evidence); err == nil || !strings.Contains(err.Error(), test.wantPart) { + t.Fatalf("Validate() error = %v, want error containing %q", err, test.wantPart) + } + }) + } +} + +func analyzeFixture(t *testing.T, componentRefs []string, edges []testEdge, target string) analysis.Analysis { + t.Helper() + document := sbom.SBOM{ + Components: make([]sbom.Component, 0, len(componentRefs)), + } + for _, bomRef := range componentRefs { + document.Components = append(document.Components, sbom.Component{ + BOMRef: bomRef, Name: bomRef, Version: "1", + }) + } + for _, edge := range edges { + document.Dependencies = append(document.Dependencies, sbom.Dependency{ + Ref: edge.to, DependsOn: []string{edge.from}, + }) + } + dependencyGraph, err := graph.New(document) + if err != nil { + t.Fatalf("graph.New() error = %v", err) + } + result, err := analysis.Analyze(dependencyGraph, target) + if err != nil { + t.Fatalf("analysis.Analyze() error = %v", err) + } + return result +} + +func cloneEvidence(evidence Evidence) Evidence { + clone := evidence + clone.Impacts = make([]Impact, len(evidence.Impacts)) + for i, impact := range evidence.Impacts { + clone.Impacts[i] = impact + clone.Impacts[i].Path = append([]string(nil), impact.Path...) + } + return clone +} + +func reverseComponents(values []string) []string { + reversed := make([]string, len(values)) + for i, value := range values { + reversed[len(values)-1-i] = value + } + return reversed +} + +func reverseEdges(values []testEdge) []testEdge { + reversed := make([]testEdge, len(values)) + for i, value := range values { + reversed[len(values)-1-i] = value + } + return reversed +} diff --git a/internal/sbom/sbom.go b/internal/sbom/sbom.go index a58cf9a..fe54aba 100644 --- a/internal/sbom/sbom.go +++ b/internal/sbom/sbom.go @@ -1,45 +1,45 @@ -package sbom - -import ( - "encoding/json" - "errors" - "fmt" -) - -// SBOM contains the component and dependency data extracted from a CycloneDX document. -type SBOM struct { - Components []Component `json:"components"` - Dependencies []Dependency `json:"dependencies"` -} - -// Component identifies a software component in the SBOM. -type Component struct { - Name string `json:"name"` - Version string `json:"version"` - BOMRef string `json:"bom-ref"` -} - -// Dependency lists the component references directly required by Ref. -type Dependency struct { - Ref string `json:"ref"` - DependsOn []string `json:"dependsOn"` -} - -// Parse extracts components and dependency relationships from CycloneDX JSON. -func Parse(data []byte) (SBOM, error) { - var document struct { - BOMFormat string `json:"bomFormat"` - Components []Component `json:"components"` - Dependencies []Dependency `json:"dependencies"` - } - if err := json.Unmarshal(data, &document); err != nil { - return SBOM{}, err - } - if document.BOMFormat == "" { - return SBOM{}, errors.New("missing CycloneDX bomFormat") - } - if document.BOMFormat != "CycloneDX" { - return SBOM{}, fmt.Errorf("unsupported bomFormat %q: expected CycloneDX", document.BOMFormat) - } - return SBOM{Components: document.Components, Dependencies: document.Dependencies}, nil -} \ No newline at end of file +package sbom + +import ( + "encoding/json" + "errors" + "fmt" +) + +// SBOM contains the component and dependency data extracted from a CycloneDX document. +type SBOM struct { + Components []Component `json:"components"` + Dependencies []Dependency `json:"dependencies"` +} + +// Component identifies a software component in the SBOM. +type Component struct { + Name string `json:"name"` + Version string `json:"version"` + BOMRef string `json:"bom-ref"` +} + +// Dependency lists the component references directly required by Ref. +type Dependency struct { + Ref string `json:"ref"` + DependsOn []string `json:"dependsOn"` +} + +// Parse extracts components and dependency relationships from CycloneDX JSON. +func Parse(data []byte) (SBOM, error) { + var document struct { + BOMFormat string `json:"bomFormat"` + Components []Component `json:"components"` + Dependencies []Dependency `json:"dependencies"` + } + if err := json.Unmarshal(data, &document); err != nil { + return SBOM{}, err + } + if document.BOMFormat == "" { + return SBOM{}, errors.New("missing CycloneDX bomFormat") + } + if document.BOMFormat != "CycloneDX" { + return SBOM{}, fmt.Errorf("unsupported bomFormat %q: expected CycloneDX", document.BOMFormat) + } + return SBOM{Components: document.Components, Dependencies: document.Dependencies}, nil +} diff --git a/internal/sbom/sbom_test.go b/internal/sbom/sbom_test.go index 05d1d18..68a3b0c 100644 --- a/internal/sbom/sbom_test.go +++ b/internal/sbom/sbom_test.go @@ -1,64 +1,65 @@ -package sbom - -import ( - "os" - "reflect" - "testing" -) - -func TestParse(t *testing.T) { - data, err := os.ReadFile("../../testdata/minimal-cyclonedx.json") - if err != nil { - t.Fatal(err) - } - - got, err := Parse(data) - if err != nil { - t.Fatal(err) - } - - want := SBOM{ - Components: []Component{ - {Name: "app", Version: "1.0.0", BOMRef: "app@1.0.0"}, - {Name: "library", Version: "2.3.4", BOMRef: "library@2.3.4"}, - }, - Dependencies: []Dependency{ - {Ref: "app@1.0.0", DependsOn: []string{"library@2.3.4"}}, - }, - } - if !reflect.DeepEqual(got, want) { - t.Fatalf("Parse() = %#v, want %#v", got, want) - } -} - -func TestParseRequiresCycloneDXFormat(t *testing.T) { - tests := []struct { - name string - data string - wantErr string - }{ - { - name: "missing format", - data: `{"components":[],"dependencies":[]}`, - wantErr: "missing CycloneDX bomFormat", - }, - { - name: "other format", - data: `{"bomFormat":"SPDX","components":[],"dependencies":[]}`, - wantErr: `unsupported bomFormat "SPDX"`, - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if _, err := Parse([]byte(test.data)); err == nil || !strings.Contains(err.Error(), test.wantErr) { - t.Fatalf("Parse() error = %v, want error containing %q", err, test.wantErr) - } - }) - } -} - -func TestParseRejectsMalformedJSON(t *testing.T) { - if _, err := Parse([]byte(`{"components":`)); err == nil { - t.Fatal("Parse() error = nil, want malformed JSON error") - } -} \ No newline at end of file +package sbom + +import ( + "os" + "reflect" + "strings" + "testing" +) + +func TestParse(t *testing.T) { + data, err := os.ReadFile("../../testdata/minimal-cyclonedx.json") + if err != nil { + t.Fatal(err) + } + + got, err := Parse(data) + if err != nil { + t.Fatal(err) + } + + want := SBOM{ + Components: []Component{ + {Name: "app", Version: "1.0.0", BOMRef: "app@1.0.0"}, + {Name: "library", Version: "2.3.4", BOMRef: "library@2.3.4"}, + }, + Dependencies: []Dependency{ + {Ref: "app@1.0.0", DependsOn: []string{"library@2.3.4"}}, + }, + } + if !reflect.DeepEqual(got, want) { + t.Fatalf("Parse() = %#v, want %#v", got, want) + } +} + +func TestParseRequiresCycloneDXFormat(t *testing.T) { + tests := []struct { + name string + data string + wantErr string + }{ + { + name: "missing format", + data: `{"components":[],"dependencies":[]}`, + wantErr: "missing CycloneDX bomFormat", + }, + { + name: "other format", + data: `{"bomFormat":"SPDX","components":[],"dependencies":[]}`, + wantErr: `unsupported bomFormat "SPDX"`, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if _, err := Parse([]byte(test.data)); err == nil || !strings.Contains(err.Error(), test.wantErr) { + t.Fatalf("Parse() error = %v, want error containing %q", err, test.wantErr) + } + }) + } +} + +func TestParseRejectsMalformedJSON(t *testing.T) { + if _, err := Parse([]byte(`{"components":`)); err == nil { + t.Fatal("Parse() error = nil, want malformed JSON error") + } +} From 8483233a295110a6136da7ecbbf517ebc7f077c1 Mon Sep 17 00:00:00 2001 From: Suman Mandal Date: Mon, 9 Nov 2026 06:22:39 +0000 Subject: [PATCH 2/3] ci: add GitHub Actions workflow Signed-off-by: Suman Mandal --- .github/workflows/ci.yml | 50 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..47d1cdf --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,50 @@ +name: CI + +on: + push: + branches: + - main + - "feat/**" + pull_request: + branches: + - main + +permissions: + contents: read + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + name: Test, Vet and Build + runs-on: ubuntu-latest + + steps: + - name: Checkout source + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + + - name: Check formatting + run: | + files=$(gofmt -l .) + if [ -n "$files" ]; then + echo "The following Go files need formatting:" + echo "$files" + exit 1 + fi + + - name: Run tests + run: go test ./... + + - name: Run static analysis + run: go vet ./... + + - name: Build + run: go build ./... From c3904df5c342cb0d7c2b74021cb9b8f74e1e9fc0 Mon Sep 17 00:00:00 2001 From: Suman Mandal Date: Mon, 9 Nov 2026 08:58:03 +0000 Subject: [PATCH 3/3] ci: fix Go dependency cache configuration Signed-off-by: Suman Mandal --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 47d1cdf..c702139 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,6 +30,7 @@ jobs: with: go-version-file: go.mod cache: true + cache-dependency-path: go.mod - name: Check formatting run: |