Skip to content

Commit 5b3636d

Browse files
authored
chore(feature/go): License Mapper (#54)
Signed-off-by: Julio Jimenez <julio@clickhouse.com>
1 parent 3568d11 commit 5b3636d

File tree

6 files changed

+368
-9
lines changed

6 files changed

+368
-9
lines changed

.golangci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ linters:
2020

2121
run:
2222
timeout: 5m
23-
tests: true
23+
tests: false
2424

2525
issues:
2626
max-issues-per-linter: 0

internal/config/config.go

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ type Config struct {
5454
Include string
5555
Exclude string
5656
Debug bool
57+
58+
// License mapping
59+
LicenseMappingFile string
5760
}
5861

5962
// LoadConfig loads configuration from environment variables.
@@ -97,12 +100,13 @@ func LoadConfig() (*Config, error) {
97100
TruncateTable: getEnvAsBool("TRUNCATE_TABLE", false),
98101

99102
// General
100-
SBOMSource: getEnvOrDefault("SBOM_SOURCE", "github"),
101-
SBOMFormat: getEnvOrDefault("SBOM_FORMAT", "cyclonedx"),
102-
Merge: getEnvAsBool("MERGE", false),
103-
Include: os.Getenv("INCLUDE"),
104-
Exclude: os.Getenv("EXCLUDE"),
105-
Debug: getEnvAsBool("DEBUG", false),
103+
SBOMSource: getEnvOrDefault("SBOM_SOURCE", "github"),
104+
SBOMFormat: getEnvOrDefault("SBOM_FORMAT", "cyclonedx"),
105+
Merge: getEnvAsBool("MERGE", false),
106+
Include: os.Getenv("INCLUDE"),
107+
Exclude: os.Getenv("EXCLUDE"),
108+
Debug: getEnvAsBool("DEBUG", false),
109+
LicenseMappingFile: getEnvOrDefault("LICENSE_MAPPING_FILE", "/app/license-mappings.json"),
106110
}
107111

108112
// Sanitize inputs

internal/sbom/license_mapper.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package sbom
2+
3+
import (
4+
"encoding/json"
5+
"os"
6+
7+
"github.com/ClickHouse/ClickBOM/pkg/logger"
8+
)
9+
10+
// LicenseMapper handles mapping of unknown licenses to known licenses
11+
type LicenseMapper struct {
12+
mappings map[string]string
13+
}
14+
15+
// NewLicenseMapper creates a new license mapper from a JSON file
16+
func NewLicenseMapper(mappingFile string) (*LicenseMapper, error) {
17+
data, err := os.ReadFile(mappingFile)
18+
if err != nil {
19+
return nil, err
20+
}
21+
22+
var mappings map[string]string
23+
if err := json.Unmarshal(data, &mappings); err != nil {
24+
return nil, err
25+
}
26+
27+
logger.Debug("Loaded %d license mappings", len(mappings))
28+
29+
return &LicenseMapper{
30+
mappings: mappings,
31+
}, nil
32+
}
33+
34+
// MapLicense maps an unknown license to a known one, or returns the original
35+
func (m *LicenseMapper) MapLicense(componentName, license string) string {
36+
// If license is already known, return it
37+
if license != "" && license != "unknown" && license != "null" {
38+
return license
39+
}
40+
41+
// Try to find a mapping for this component
42+
if mapped, exists := m.mappings[componentName]; exists {
43+
logger.Debug("Mapped license for %s: unknown -> %s", componentName, mapped)
44+
return mapped
45+
}
46+
47+
// No mapping found, return unknown
48+
return "unknown"
49+
}
50+
51+
// MapComponent maps the license for a component (modifies in place)
52+
func (m *LicenseMapper) MapComponent(comp map[string]interface{}) {
53+
name, _ := comp["name"].(string)
54+
license, _ := comp["license"].(string)
55+
56+
if name != "" {
57+
mappedLicense := m.MapLicense(name, license)
58+
comp["license"] = mappedLicense
59+
}
60+
}
61+
62+
// MapComponents maps licenses for multiple components
63+
func (m *LicenseMapper) MapComponents(components []map[string]interface{}) {
64+
for _, comp := range components {
65+
m.MapComponent(comp)
66+
}
67+
}
68+
69+
// GetMapping returns the mapping for a specific component, if it exists
70+
func (m *LicenseMapper) GetMapping(componentName string) (string, bool) {
71+
license, exists := m.mappings[componentName]
72+
return license, exists
73+
}
74+
75+
// HasMapping checks if a mapping exists for a component
76+
func (m *LicenseMapper) HasMapping(componentName string) bool {
77+
_, exists := m.mappings[componentName]
78+
return exists
79+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
//go:build integration
2+
3+
package sbom
4+
5+
import (
6+
"testing"
7+
)
8+
9+
func TestLicenseMapperWithRealFile(t *testing.T) {
10+
// Test with the actual license-mappings.json file
11+
mapper, err := NewLicenseMapper("../../license-mappings.json")
12+
if err != nil {
13+
t.Fatalf("Failed to load real license mappings: %v", err)
14+
}
15+
16+
// Test some known mappings
17+
tests := []struct {
18+
component string
19+
want string
20+
}{
21+
{"4d63.com/gocheckcompilerdirectives", "MIT"},
22+
{"actions/cache", "MIT"},
23+
{"CycloneDX/gh-gomod-generate-sbom", "Apache-2.0"},
24+
}
25+
26+
for _, tt := range tests {
27+
t.Run(tt.component, func(t *testing.T) {
28+
got := mapper.MapLicense(tt.component, "unknown")
29+
if got != tt.want {
30+
t.Errorf("MapLicense(%s) = %v, want %v", tt.component, got, tt.want)
31+
}
32+
})
33+
}
34+
}
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
package sbom
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
)
8+
9+
func TestNewLicenseMapper(t *testing.T) {
10+
// Create temp mapping file
11+
tempDir := t.TempDir()
12+
mappingFile := filepath.Join(tempDir, "test-mappings.json")
13+
14+
mappingContent := `{
15+
"4d63.com/gocheckcompilerdirectives": "MIT",
16+
"actions/cache": "MIT",
17+
"test-component": "Apache-2.0"
18+
}`
19+
20+
if err := os.WriteFile(mappingFile, []byte(mappingContent), 0644); err != nil {
21+
t.Fatalf("Failed to create test mapping file: %v", err)
22+
}
23+
24+
mapper, err := NewLicenseMapper(mappingFile)
25+
if err != nil {
26+
t.Fatalf("NewLicenseMapper() error = %v", err)
27+
}
28+
29+
if mapper == nil {
30+
t.Fatal("Expected mapper, got nil")
31+
}
32+
33+
if len(mapper.mappings) != 3 {
34+
t.Errorf("Expected 3 mappings, got %d", len(mapper.mappings))
35+
}
36+
}
37+
38+
func TestNewLicenseMapper_FileNotFound(t *testing.T) {
39+
_, err := NewLicenseMapper("/nonexistent/file.json")
40+
if err == nil {
41+
t.Error("Expected error for nonexistent file, got nil")
42+
}
43+
}
44+
45+
func TestNewLicenseMapper_InvalidJSON(t *testing.T) {
46+
tempDir := t.TempDir()
47+
mappingFile := filepath.Join(tempDir, "invalid.json")
48+
49+
if err := os.WriteFile(mappingFile, []byte("not valid json"), 0644); err != nil {
50+
t.Fatalf("Failed to create test file: %v", err)
51+
}
52+
53+
_, err := NewLicenseMapper(mappingFile)
54+
if err == nil {
55+
t.Error("Expected error for invalid JSON, got nil")
56+
}
57+
}
58+
59+
func TestMapLicense(t *testing.T) {
60+
mapper := &LicenseMapper{
61+
mappings: map[string]string{
62+
"test-component": "MIT",
63+
"another-component": "Apache-2.0",
64+
},
65+
}
66+
67+
tests := []struct {
68+
name string
69+
componentName string
70+
license string
71+
want string
72+
}{
73+
{
74+
name: "known license - keep it",
75+
componentName: "any-component",
76+
license: "BSD-3-Clause",
77+
want: "BSD-3-Clause",
78+
},
79+
{
80+
name: "unknown license with mapping",
81+
componentName: "test-component",
82+
license: "unknown",
83+
want: "MIT",
84+
},
85+
{
86+
name: "empty license with mapping",
87+
componentName: "test-component",
88+
license: "",
89+
want: "MIT",
90+
},
91+
{
92+
name: "null license with mapping",
93+
componentName: "test-component",
94+
license: "null",
95+
want: "MIT",
96+
},
97+
{
98+
name: "unknown license without mapping",
99+
componentName: "unmapped-component",
100+
license: "unknown",
101+
want: "unknown",
102+
},
103+
{
104+
name: "different component with mapping",
105+
componentName: "another-component",
106+
license: "",
107+
want: "Apache-2.0",
108+
},
109+
}
110+
111+
for _, tt := range tests {
112+
t.Run(tt.name, func(t *testing.T) {
113+
got := mapper.MapLicense(tt.componentName, tt.license)
114+
if got != tt.want {
115+
t.Errorf("MapLicense() = %v, want %v", got, tt.want)
116+
}
117+
})
118+
}
119+
}
120+
121+
func TestMapComponent(t *testing.T) {
122+
mapper := &LicenseMapper{
123+
mappings: map[string]string{
124+
"test-component": "MIT",
125+
},
126+
}
127+
128+
comp := map[string]interface{}{
129+
"name": "test-component",
130+
"version": "1.0.0",
131+
"license": "unknown",
132+
}
133+
134+
mapper.MapComponent(comp)
135+
136+
if comp["license"] != "MIT" {
137+
t.Errorf("Expected license to be mapped to MIT, got %v", comp["license"])
138+
}
139+
}
140+
141+
func TestMapComponents(t *testing.T) {
142+
mapper := &LicenseMapper{
143+
mappings: map[string]string{
144+
"component-a": "MIT",
145+
"component-b": "Apache-2.0",
146+
},
147+
}
148+
149+
components := []map[string]interface{}{
150+
{
151+
"name": "component-a",
152+
"license": "unknown",
153+
},
154+
{
155+
"name": "component-b",
156+
"license": "",
157+
},
158+
{
159+
"name": "component-c",
160+
"license": "BSD-3-Clause",
161+
},
162+
}
163+
164+
mapper.MapComponents(components)
165+
166+
// Check first component
167+
if components[0]["license"] != "MIT" {
168+
t.Errorf("Component A: expected MIT, got %v", components[0]["license"])
169+
}
170+
171+
// Check second component
172+
if components[1]["license"] != "Apache-2.0" {
173+
t.Errorf("Component B: expected Apache-2.0, got %v", components[1]["license"])
174+
}
175+
176+
// Check third component (should remain unchanged)
177+
if components[2]["license"] != "BSD-3-Clause" {
178+
t.Errorf("Component C: expected BSD-3-Clause, got %v", components[2]["license"])
179+
}
180+
}
181+
182+
func TestGetMapping(t *testing.T) {
183+
mapper := &LicenseMapper{
184+
mappings: map[string]string{
185+
"test-component": "MIT",
186+
},
187+
}
188+
189+
t.Run("existing mapping", func(t *testing.T) {
190+
license, exists := mapper.GetMapping("test-component")
191+
if !exists {
192+
t.Error("Expected mapping to exist")
193+
}
194+
if license != "MIT" {
195+
t.Errorf("Expected MIT, got %v", license)
196+
}
197+
})
198+
199+
t.Run("non-existing mapping", func(t *testing.T) {
200+
_, exists := mapper.GetMapping("nonexistent")
201+
if exists {
202+
t.Error("Expected mapping to not exist")
203+
}
204+
})
205+
}
206+
207+
func TestHasMapping(t *testing.T) {
208+
mapper := &LicenseMapper{
209+
mappings: map[string]string{
210+
"test-component": "MIT",
211+
},
212+
}
213+
214+
if !mapper.HasMapping("test-component") {
215+
t.Error("Expected HasMapping to return true for test-component")
216+
}
217+
218+
if mapper.HasMapping("nonexistent") {
219+
t.Error("Expected HasMapping to return false for nonexistent")
220+
}
221+
}

0 commit comments

Comments
 (0)