From 96c34ffc43c6b4090de8b2d6f24de0c6260c6345 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Wed, 11 Jun 2025 13:11:40 -0400 Subject: [PATCH] account for non-import shapes (#3997) Signed-off-by: Alex Goodman --- syft/format/syftjson/model/document.go | 2 +- syft/format/syftjson/model/file.go | 48 ++++++--- syft/format/syftjson/model/file_test.go | 129 ++++++++++++++++-------- 3 files changed, 123 insertions(+), 56 deletions(-) diff --git a/syft/format/syftjson/model/document.go b/syft/format/syftjson/model/document.go index a650af4fb..c1cb0e381 100644 --- a/syft/format/syftjson/model/document.go +++ b/syft/format/syftjson/model/document.go @@ -28,7 +28,7 @@ func (d *Document) UnmarshalJSON(data []byte) error { if d.Schema.Version == "1.0.0" && d.Descriptor.Name == "anchorectl" { // convert all file modes from decimal to octal for i := range d.Files { - d.Files[i].Metadata.Mode = convertFileModeToBase8(d.Files[i].Metadata.Mode) + d.Files[i].Metadata.Mode = convertBase10ToBase8(d.Files[i].Metadata.Mode) } } diff --git a/syft/format/syftjson/model/file.go b/syft/format/syftjson/model/file.go index be406d2ba..87b4754e4 100644 --- a/syft/format/syftjson/model/file.go +++ b/syft/format/syftjson/model/file.go @@ -31,14 +31,21 @@ type FileMetadataEntry struct { Size int64 `json:"size"` } -func (f *FileMetadataEntry) UnmarshalJSON(data []byte) error { - type Alias FileMetadataEntry - aux := (*Alias)(f) +type auxFileMetadataEntry FileMetadataEntry +type fileMetadataEntryWithLegacyHint struct { + *auxFileMetadataEntry `json:",inline"` + LegacyHint any `json:"FileInfo"` +} - if err := json.Unmarshal(data, aux); err == nil { - // we should have at least one field set to a non-zero value... otherwise this is a legacy entry - if f.Mode != 0 || f.Type != "" || f.LinkDestination != "" || - f.UserID != 0 || f.GroupID != 0 || f.MIMEType != "" || f.Size != 0 { +func (f *FileMetadataEntry) UnmarshalJSON(data []byte) error { + aux := fileMetadataEntryWithLegacyHint{ + auxFileMetadataEntry: (*auxFileMetadataEntry)(f), + } + if err := json.Unmarshal(data, &aux); err == nil { + fieldsSpecified := f.Mode != 0 || f.Type != "" || f.LinkDestination != "" || + f.UserID != 0 || f.GroupID != 0 || f.MIMEType != "" || f.Size != 0 + if aux.LegacyHint == nil && fieldsSpecified { + // we should have at least one field set to a non-zero value... (this is not a legacy shape) return nil } } @@ -48,8 +55,14 @@ func (f *FileMetadataEntry) UnmarshalJSON(data []byte) error { return err } + if !legacy.Type.WasInt { + // this occurs for document shapes from a non-import path and indicates that the mode has already been converted to octal. + // That being said, we want to handle all legacy shapes the same, so we will convert this to base 10 for consistency. + legacy.Mode = convertBase8ToBase10(legacy.Mode) + } + f.Mode = legacy.Mode - f.Type = string(legacy.Type) + f.Type = legacy.Type.Value f.LinkDestination = legacy.LinkDestination f.UserID = legacy.UserID f.GroupID = legacy.GroupID @@ -82,12 +95,15 @@ type FileLicenseEvidence struct { Extent int `json:"extent"` } -type intOrStringFileType string +type intOrStringFileType struct { + Value string + WasInt bool +} func (lt *intOrStringFileType) UnmarshalJSON(data []byte) error { var str string if err := json.Unmarshal(data, &str); err == nil { - *lt = intOrStringFileType(str) + lt.Value = str return nil } @@ -96,13 +112,21 @@ func (lt *intOrStringFileType) UnmarshalJSON(data []byte) error { return fmt.Errorf("file.Type must be either string or int, got: %s", string(data)) } - *lt = intOrStringFileType(num.String()) + lt.Value = num.String() + lt.WasInt = true return nil } -func convertFileModeToBase8(rawMode int) int { +func convertBase10ToBase8(rawMode int) int { octalStr := fmt.Sprintf("%o", rawMode) // we don't need to check that this is a valid octal string since the input is always an integer result, _ := strconv.Atoi(octalStr) return result } + +func convertBase8ToBase10(octalMode int) int { + octalStr := strconv.Itoa(octalMode) + result, _ := strconv.ParseInt(octalStr, 8, 64) + + return int(result) +} diff --git a/syft/format/syftjson/model/file_test.go b/syft/format/syftjson/model/file_test.go index e2aee3ed3..f2ec1f18f 100644 --- a/syft/format/syftjson/model/file_test.go +++ b/syft/format/syftjson/model/file_test.go @@ -36,8 +36,9 @@ func Test_FileMetadataEntry_UnmarshalJSON(t *testing.T) { }, }, { - name: "unmarshal legacy sbom import format", + name: "unmarshal legacy image add internal document format", jsonData: []byte(`{ + "FileInfo": {}, "Mode": 644, "Type": "RegularFile", "LinkDestination": "/usr/bin/python3", @@ -47,7 +48,7 @@ func Test_FileMetadataEntry_UnmarshalJSON(t *testing.T) { "Size": 10174 }`), expected: FileMetadataEntry{ - Mode: 644, + Mode: 420, // important! we convert this to base 10 so that all documents are consistent Type: "RegularFile", LinkDestination: "/usr/bin/python3", UserID: 1000, @@ -57,8 +58,9 @@ func Test_FileMetadataEntry_UnmarshalJSON(t *testing.T) { }, }, { - name: "unmarshal legacy sbom import format - integer type", + name: "unmarshal legacy sbom import format", jsonData: []byte(`{ + "FileInfo": {}, "Mode": 644, "Type": 0, "LinkDestination": "/usr/bin/python3", @@ -93,6 +95,7 @@ func Test_FileMetadataEntry_UnmarshalJSON(t *testing.T) { { name: "unmarshal minimal legacy format", jsonData: []byte(`{ + "FileInfo": {}, "Mode": 0, "Type": "RegularFile", "UserID": 0, @@ -120,10 +123,11 @@ func Test_FileMetadataEntry_UnmarshalJSON(t *testing.T) { func Test_intOrStringFileType_UnmarshalJSON(t *testing.T) { tests := []struct { - name string - jsonData []byte - expected string - wantErr require.ErrorAssertionFunc + name string + jsonData []byte + expected string + expectedWasInt bool + wantErr require.ErrorAssertionFunc }{ // string inputs - should pass through unchanged { @@ -148,59 +152,70 @@ func Test_intOrStringFileType_UnmarshalJSON(t *testing.T) { }, // integer inputs - should convert to string representation { - name: "int 0 (TypeRegular)", - jsonData: []byte(`0`), - expected: "RegularFile", + name: "int 0 (TypeRegular)", + jsonData: []byte(`0`), + expected: "RegularFile", + expectedWasInt: true, }, { - name: "int 1 (TypeHardLink)", - jsonData: []byte(`1`), - expected: "HardLink", + name: "int 1 (TypeHardLink)", + jsonData: []byte(`1`), + expected: "HardLink", + expectedWasInt: true, }, { - name: "int 2 (TypeSymLink)", - jsonData: []byte(`2`), - expected: "SymbolicLink", + name: "int 2 (TypeSymLink)", + jsonData: []byte(`2`), + expected: "SymbolicLink", + expectedWasInt: true, }, { - name: "int 3 (TypeCharacterDevice)", - jsonData: []byte(`3`), - expected: "CharacterDevice", + name: "int 3 (TypeCharacterDevice)", + jsonData: []byte(`3`), + expected: "CharacterDevice", + expectedWasInt: true, }, { - name: "int 4 (TypeBlockDevice)", - jsonData: []byte(`4`), - expected: "BlockDevice", + name: "int 4 (TypeBlockDevice)", + jsonData: []byte(`4`), + expected: "BlockDevice", + expectedWasInt: true, }, { - name: "int 5 (TypeDirectory)", - jsonData: []byte(`5`), - expected: "Directory", + name: "int 5 (TypeDirectory)", + jsonData: []byte(`5`), + expected: "Directory", + expectedWasInt: true, }, { - name: "int 6 (TypeFIFO)", - jsonData: []byte(`6`), - expected: "FIFONode", + name: "int 6 (TypeFIFO)", + jsonData: []byte(`6`), + expected: "FIFONode", + expectedWasInt: true, }, { - name: "int 7 (TypeSocket)", - jsonData: []byte(`7`), - expected: "Socket", + name: "int 7 (TypeSocket)", + jsonData: []byte(`7`), + expected: "Socket", + expectedWasInt: true, }, { - name: "int 8 (TypeIrregular)", - jsonData: []byte(`8`), - expected: "IrregularFile", + name: "int 8 (TypeIrregular)", + jsonData: []byte(`8`), + expected: "IrregularFile", + expectedWasInt: true, }, { - name: "unknown int", - jsonData: []byte(`99`), - expected: "Unknown", + name: "unknown int", + jsonData: []byte(`99`), + expected: "Unknown", + expectedWasInt: true, }, { - name: "negative int", - jsonData: []byte(`-1`), - expected: "Unknown", + name: "negative int", + jsonData: []byte(`-1`), + expected: "Unknown", + expectedWasInt: true, }, { name: "null value", @@ -244,12 +259,13 @@ func Test_intOrStringFileType_UnmarshalJSON(t *testing.T) { if err != nil { return } - assert.Equal(t, test.expected, string(ft)) + assert.Equal(t, test.expected, ft.Value) + assert.Equal(t, test.expectedWasInt, ft.WasInt) }) } } -func Test_convertFileModeToBase8(t *testing.T) { +func Test_convertBase10ToBase8(t *testing.T) { tests := []struct { name string input int @@ -269,7 +285,34 @@ func Test_convertFileModeToBase8(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - actual := convertFileModeToBase8(tt.input) + actual := convertBase10ToBase8(tt.input) + + require.Equal(t, tt.expected, actual) + }) + } +} + +func Test_convertBase8ToBase10(t *testing.T) { + tests := []struct { + name string + input int + expected int + }{ + { + name: "no permissions", + input: 0, + expected: 0, + }, + { + name: "symlink + rwxrwxrwx", + input: 1000000777, + expected: 134218239, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := convertBase8ToBase10(tt.input) require.Equal(t, tt.expected, actual) })