Skip to content

Commit 79b6d5d

Browse files
authored
Allow decoding of anchorectl json files (#3973)
* allow decoding of import sbom file shape Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * address formatting Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * add file mode and type processing Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * use type to interpret the raw value Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * safe mode convert should use uint32 Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * simpler decoder type Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> --------- Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
1 parent cfa7cc5 commit 79b6d5d

File tree

6 files changed

+477
-4
lines changed

6 files changed

+477
-4
lines changed

syft/format/syftjson/model/document.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
package model
22

3+
import (
4+
"encoding/json"
5+
"fmt"
6+
)
7+
38
// Document represents the syft cataloging findings as a JSON document
49
type Document struct {
510
Artifacts []Package `json:"artifacts"` // Artifacts is the list of packages discovered and placed into the catalog
@@ -11,6 +16,25 @@ type Document struct {
1116
Schema Schema `json:"schema"` // Schema is a block reserved for defining the version for the shape of this JSON document and where to find the schema document to validate the shape
1217
}
1318

19+
func (d *Document) UnmarshalJSON(data []byte) error {
20+
type Alias *Document
21+
aux := Alias(d)
22+
23+
if err := json.Unmarshal(data, aux); err != nil {
24+
return fmt.Errorf("could not unmarshal syft JSON document: %w", err)
25+
}
26+
27+
// in previous versions of anchorectl, the file modes were stored as decimal values instead of octal.
28+
if d.Schema.Version == "1.0.0" && d.Descriptor.Name == "anchorectl" {
29+
// convert all file modes from decimal to octal
30+
for i := range d.Files {
31+
d.Files[i].Metadata.Mode = convertFileModeToBase8(d.Files[i].Metadata.Mode)
32+
}
33+
}
34+
35+
return nil
36+
}
37+
1438
// Descriptor describes what created the document as well as surrounding metadata
1539
type Descriptor struct {
1640
Name string `json:"name"`
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package model
2+
3+
import (
4+
"encoding/json"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
func TestDocumentUnmarshalJSON_SchemaDetection(t *testing.T) {
12+
tests := []struct {
13+
name string
14+
jsonData string
15+
modes []int
16+
}{
17+
{
18+
name: "schema version 1.0.0 + anchorectl",
19+
jsonData: `{
20+
"files": [
21+
{"metadata": {"mode": 493}},
22+
{"metadata": {"mode": 420}}
23+
],
24+
"schema": {"version": "1.0.0"},
25+
"descriptor": {
26+
"name": "anchorectl"
27+
}
28+
}`,
29+
modes: []int{755, 644},
30+
},
31+
{
32+
name: "schema version 1.0.0 + syft",
33+
jsonData: `{
34+
"files": [
35+
{"metadata": {"mode": 755}},
36+
{"metadata": {"mode": 644}}
37+
],
38+
"schema": {"version": "1.0.0"},
39+
"descriptor": {
40+
"name": "syft"
41+
}
42+
}`,
43+
modes: []int{755, 644},
44+
},
45+
{
46+
name: "schema version 2.0.0 + anchorectl",
47+
jsonData: `{
48+
"files": [
49+
{"metadata": {"mode": 755}},
50+
{"metadata": {"mode": 644}}
51+
],
52+
"schema": {"version": "2.0.0"},
53+
"descriptor": {
54+
"name": "anchorectl"
55+
}
56+
}`,
57+
modes: []int{755, 644},
58+
},
59+
{
60+
name: "missing schema version should not convert modes",
61+
jsonData: `{
62+
"files": [
63+
{"metadata": {"mode": 755}}
64+
],
65+
"schema": {}
66+
}`,
67+
modes: []int{755},
68+
},
69+
{
70+
name: "empty files array with version 1.0.0",
71+
jsonData: `{
72+
"files": [],
73+
"schema": {"version": "1.0.0"},
74+
"descriptor": {
75+
"name": "anchorectl"
76+
}
77+
}`,
78+
},
79+
}
80+
81+
for _, tt := range tests {
82+
t.Run(tt.name, func(t *testing.T) {
83+
var doc Document
84+
85+
err := json.Unmarshal([]byte(tt.jsonData), &doc)
86+
if err != nil {
87+
t.Fatalf("Failed to unmarshal JSON: %v", err)
88+
}
89+
90+
var modes []int
91+
for _, file := range doc.Files {
92+
modes = append(modes, file.Metadata.Mode)
93+
}
94+
95+
require.Len(t, doc.Files, len(tt.modes), "Unexpected number of files")
96+
assert.Equal(t, tt.modes, modes, "File modes do not match expected values")
97+
})
98+
}
99+
}

syft/format/syftjson/model/file.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
package model
22

33
import (
4+
"encoding/json"
5+
"fmt"
6+
"strconv"
7+
8+
stereoFile "github.com/anchore/stereoscope/pkg/file"
49
"github.com/anchore/syft/syft/file"
510
"github.com/anchore/syft/syft/license"
611
)
@@ -26,6 +31,44 @@ type FileMetadataEntry struct {
2631
Size int64 `json:"size"`
2732
}
2833

34+
func (f *FileMetadataEntry) UnmarshalJSON(data []byte) error {
35+
type Alias FileMetadataEntry
36+
aux := (*Alias)(f)
37+
38+
if err := json.Unmarshal(data, aux); err == nil {
39+
// we should have at least one field set to a non-zero value... otherwise this is a legacy entry
40+
if f.Mode != 0 || f.Type != "" || f.LinkDestination != "" ||
41+
f.UserID != 0 || f.GroupID != 0 || f.MIMEType != "" || f.Size != 0 {
42+
return nil
43+
}
44+
}
45+
46+
var legacy sbomImportLegacyFileMetadataEntry
47+
if err := json.Unmarshal(data, &legacy); err != nil {
48+
return err
49+
}
50+
51+
f.Mode = legacy.Mode
52+
f.Type = string(legacy.Type)
53+
f.LinkDestination = legacy.LinkDestination
54+
f.UserID = legacy.UserID
55+
f.GroupID = legacy.GroupID
56+
f.MIMEType = legacy.MIMEType
57+
f.Size = legacy.Size
58+
59+
return nil
60+
}
61+
62+
type sbomImportLegacyFileMetadataEntry struct {
63+
Mode int `json:"Mode"`
64+
Type intOrStringFileType `json:"Type"`
65+
LinkDestination string `json:"LinkDestination"`
66+
UserID int `json:"UserID"`
67+
GroupID int `json:"GroupID"`
68+
MIMEType string `json:"MIMEType"`
69+
Size int64 `json:"Size"`
70+
}
71+
2972
type FileLicense struct {
3073
Value string `json:"value"`
3174
SPDXExpression string `json:"spdxExpression"`
@@ -38,3 +81,28 @@ type FileLicenseEvidence struct {
3881
Offset int `json:"offset"`
3982
Extent int `json:"extent"`
4083
}
84+
85+
type intOrStringFileType string
86+
87+
func (lt *intOrStringFileType) UnmarshalJSON(data []byte) error {
88+
var str string
89+
if err := json.Unmarshal(data, &str); err == nil {
90+
*lt = intOrStringFileType(str)
91+
return nil
92+
}
93+
94+
var num stereoFile.Type
95+
if err := json.Unmarshal(data, &num); err != nil {
96+
return fmt.Errorf("file.Type must be either string or int, got: %s", string(data))
97+
}
98+
99+
*lt = intOrStringFileType(num.String())
100+
return nil
101+
}
102+
103+
func convertFileModeToBase8(rawMode int) int {
104+
octalStr := fmt.Sprintf("%o", rawMode)
105+
// we don't need to check that this is a valid octal string since the input is always an integer
106+
result, _ := strconv.Atoi(octalStr)
107+
return result
108+
}

0 commit comments

Comments
 (0)