github.com/anchore/syft@v1.4.2-0.20240516191711-1bec1fc5d397/syft/format/syftjson/decoder_test.go (about) 1 package syftjson 2 3 import ( 4 "bytes" 5 "errors" 6 "fmt" 7 "strings" 8 "testing" 9 10 "github.com/go-test/deep" 11 "github.com/stretchr/testify/assert" 12 "github.com/stretchr/testify/require" 13 14 stereoscopeFile "github.com/anchore/stereoscope/pkg/file" 15 "github.com/anchore/syft/internal" 16 "github.com/anchore/syft/syft/cpe" 17 "github.com/anchore/syft/syft/file" 18 "github.com/anchore/syft/syft/format/internal/testutil" 19 "github.com/anchore/syft/syft/linux" 20 "github.com/anchore/syft/syft/pkg" 21 "github.com/anchore/syft/syft/sbom" 22 "github.com/anchore/syft/syft/source" 23 ) 24 25 func Test_EncodeDecodeCycle(t *testing.T) { 26 27 table := []struct { 28 name string 29 fixtureImage string 30 cfg EncoderConfig 31 }{ 32 { 33 name: "go case", 34 fixtureImage: "image-simple", 35 cfg: DefaultEncoderConfig(), 36 }, 37 { 38 name: "default alpine", 39 fixtureImage: "image-alpine", 40 cfg: DefaultEncoderConfig(), 41 }, 42 { 43 name: "legacy alpine", 44 fixtureImage: "image-alpine", 45 cfg: EncoderConfig{ 46 Legacy: true, 47 }, 48 }, 49 } 50 51 for _, tt := range table { 52 t.Run(tt.name, func(t *testing.T) { 53 originalSBOM := testutil.ImageInput(t, tt.fixtureImage) 54 55 enc, err := NewFormatEncoderWithConfig(tt.cfg) 56 require.NoError(t, err) 57 dec := NewFormatDecoder() 58 59 var buf bytes.Buffer 60 assert.NoError(t, enc.Encode(&buf, originalSBOM)) 61 62 actualSBOM, decodedID, decodedVersion, err := dec.Decode(bytes.NewReader(buf.Bytes())) 63 assert.NoError(t, err) 64 assert.Equal(t, ID, decodedID) 65 assert.Equal(t, internal.JSONSchemaVersion, decodedVersion) 66 67 for _, d := range deep.Equal(originalSBOM.Source, actualSBOM.Source) { 68 if strings.HasSuffix(d, "<nil slice> != []") { 69 // semantically the same 70 continue 71 } 72 t.Errorf("metadata difference: %+v", d) 73 } 74 75 actualPackages := actualSBOM.Artifacts.Packages.Sorted() 76 for idx, p := range originalSBOM.Artifacts.Packages.Sorted() { 77 if !assert.Equal(t, p.Name, actualPackages[idx].Name) { 78 t.Errorf("different package at idx=%d: %s vs %s", idx, p.Name, actualPackages[idx].Name) 79 continue 80 } 81 82 for _, d := range deep.Equal(p, actualPackages[idx]) { 83 if strings.Contains(d, ".AccessPath: ") { 84 // location.Virtual path is not exposed in the json output 85 continue 86 } 87 if strings.HasSuffix(d, "<nil slice> != []") { 88 // semantically the same 89 continue 90 } 91 t.Errorf("%q package difference (%s): %+v", tt.fixtureImage, p.Name, d) 92 } 93 } 94 }) 95 } 96 } 97 98 func TestOutOfDateParser(t *testing.T) { 99 tests := []struct { 100 name string 101 documentVersion string 102 parserVersion string 103 want error 104 }{{ 105 name: "no warning when doc version is older", 106 documentVersion: "1.0.9", 107 parserVersion: "3.1.0", 108 }, { 109 name: "warning when parser is older", 110 documentVersion: "4.3.2", 111 parserVersion: "3.1.0", 112 want: fmt.Errorf("document has schema version %s, but parser has older schema version (%s)", "4.3.2", "3.1.0"), 113 }, { 114 name: "warning when document version is unparseable", 115 documentVersion: "some-nonsense", 116 parserVersion: "3.1.0", 117 want: fmt.Errorf("error comparing document schema version with parser schema version: %w", errors.New("Invalid Semantic Version")), 118 }, { 119 name: "warning when parser version is unparseable", 120 documentVersion: "7.1.0", 121 parserVersion: "some-nonsense", 122 want: fmt.Errorf("error comparing document schema version with parser schema version: %w", errors.New("Invalid Semantic Version")), 123 }} 124 125 for _, tt := range tests { 126 t.Run(tt.name, func(t *testing.T) { 127 got := checkSupportedSchema(tt.documentVersion, tt.parserVersion) 128 assert.Equal(t, tt.want, got) 129 }) 130 } 131 } 132 133 func Test_encodeDecodeFileMetadata(t *testing.T) { 134 p := pkg.Package{ 135 Name: "pkg", 136 Version: "version", 137 FoundBy: "something", 138 Locations: file.NewLocationSet(file.Location{ 139 LocationData: file.LocationData{ 140 Coordinates: file.Coordinates{ 141 RealPath: "/somewhere", 142 FileSystemID: "id", 143 }, 144 }, 145 LocationMetadata: file.LocationMetadata{ 146 Annotations: map[string]string{ 147 "key": "value", 148 }, 149 }, 150 }), 151 Licenses: pkg.NewLicenseSet(pkg.License{ 152 Value: "MIT", 153 SPDXExpression: "MIT", 154 Type: "MIT", 155 URLs: []string{"https://example.org/license"}, 156 Locations: file.LocationSet{}, 157 }), 158 Language: "language", 159 Type: "type", 160 CPEs: []cpe.CPE{ 161 { 162 Attributes: cpe.Attributes{ 163 Part: "a", 164 Vendor: "vendor", 165 Product: "product", 166 Version: "version", 167 Update: "update", 168 }, 169 Source: "test-source", 170 }, 171 }, 172 PURL: "pkg:generic/pkg@version", 173 Metadata: nil, 174 } 175 p.SetID() 176 177 c := file.Coordinates{ 178 RealPath: "some-file", 179 FileSystemID: "some-fs-id", 180 } 181 182 s := sbom.SBOM{ 183 Artifacts: sbom.Artifacts{ 184 Packages: pkg.NewCollection(p), 185 FileMetadata: map[file.Coordinates]file.Metadata{ 186 c: { 187 FileInfo: stereoscopeFile.ManualInfo{ 188 NameValue: c.RealPath, 189 ModeValue: 0644, 190 SizeValue: 7, 191 }, 192 Path: c.RealPath, 193 Type: stereoscopeFile.TypeRegular, 194 UserID: 1, 195 GroupID: 2, 196 MIMEType: "text/plain", 197 }, 198 }, 199 FileDigests: map[file.Coordinates][]file.Digest{ 200 c: { 201 { 202 Algorithm: "sha1", 203 Value: "d34db33f", 204 }, 205 }, 206 }, 207 FileContents: map[file.Coordinates]string{ 208 c: "some contents", 209 }, 210 FileLicenses: map[file.Coordinates][]file.License{ 211 c: { 212 { 213 Value: "MIT", 214 SPDXExpression: "MIT", 215 Type: "MIT", 216 LicenseEvidence: &file.LicenseEvidence{ 217 Confidence: 1, 218 Offset: 2, 219 Extent: 3, 220 }, 221 }, 222 }, 223 }, 224 Executables: map[file.Coordinates]file.Executable{ 225 c: { 226 Format: file.ELF, 227 ELFSecurityFeatures: &file.ELFSecurityFeatures{ 228 SymbolTableStripped: false, 229 StackCanary: boolRef(true), 230 NoExecutable: false, 231 RelocationReadOnly: "partial", 232 PositionIndependentExecutable: false, 233 DynamicSharedObject: false, 234 LlvmSafeStack: boolRef(false), 235 LlvmControlFlowIntegrity: boolRef(true), 236 ClangFortifySource: boolRef(true), 237 }, 238 }, 239 }, 240 LinuxDistribution: &linux.Release{ 241 PrettyName: "some os", 242 Name: "os", 243 ID: "os-id", 244 IDLike: []string{"os"}, 245 Version: "version", 246 VersionID: "version", 247 VersionCodename: "codename", 248 BuildID: "build-id", 249 ImageID: "image-id", 250 ImageVersion: "image-version", 251 Variant: "variant", 252 VariantID: "variant-id", 253 HomeURL: "https://example.org/os", 254 SupportURL: "https://example.org/os/support", 255 BugReportURL: "https://example.org/os/bugs", 256 PrivacyPolicyURL: "https://example.org/os/privacy", 257 CPEName: "os-cpe", 258 SupportEnd: "now", 259 }, 260 }, 261 Relationships: nil, 262 Source: source.Description{ 263 ID: "some-id", 264 Name: "some-name", 265 Version: "some-version", 266 Metadata: source.FileMetadata{ 267 Path: "/some-file-source-path", 268 Digests: []file.Digest{ 269 { 270 Algorithm: "sha1", 271 Value: "d34db33f", 272 }, 273 }, 274 MIMEType: "file/zip", 275 }, 276 }, 277 Descriptor: sbom.Descriptor{ 278 Name: "syft", 279 Version: "this-version", 280 }, 281 } 282 283 buf := &bytes.Buffer{} 284 enc := NewFormatEncoder() 285 err := enc.Encode(buf, s) 286 require.NoError(t, err) 287 288 dec := NewFormatDecoder() 289 290 got, decodedID, decodedVersion, err := dec.Decode(bytes.NewReader(buf.Bytes())) 291 require.NoError(t, err) 292 assert.Equal(t, ID, decodedID) 293 assert.Equal(t, internal.JSONSchemaVersion, decodedVersion) 294 295 require.Equal(t, s, *got) 296 }