mirror of
https://github.com/anchore/syft.git
synced 2025-11-17 16:33:21 +01:00
Expose RPM signature information (for RPM DB and RPM archives) (#3179)
* feat: expose rpm signature information This helps with more confident identification of an rpm. In theory, two rpms can be built that have the same purl string, and otherwise look identical in syft's output, but the PGP information would distinguish them as signed either by different keys, or signed at different times. In practice, this usually makes no difference since rpms tend to have unique name/version/release strings. This just gives increased confidence about the identity of the rpm found in the db. Signed-off-by: Ralph Bean <rbean@redhat.com> * chore: generate json schema Signed-off-by: Ralph Bean <rbean@redhat.com> * re-generate json schema Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * rename to a more generic signature field Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * rename rpm.pgp to rpm.signatures Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * split out signature fields Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * bump json schema Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * include RPM archives Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * update json schema Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * dont fail on unknown signature type Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> --------- Signed-off-by: Ralph Bean <rbean@redhat.com> Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
parent
5effed06a8
commit
b369b02f4f
@ -3,18 +3,18 @@ package integration
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
"github.com/anchore/syft/syft/pkg"
|
"github.com/anchore/syft/syft/pkg"
|
||||||
"github.com/anchore/syft/syft/source"
|
"github.com/anchore/syft/syft/source"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPhotonPackageRegression(t *testing.T) { // Regression: https://github.com/anchore/syft/pull/1997
|
func TestPhotonPackageRegression(t *testing.T) { // Regression: https://github.com/anchore/syft/pull/1997
|
||||||
sbom, _ := catalogFixtureImage(t, "image-photon-all-layers", source.AllLayersScope)
|
sbom, _ := catalogFixtureImage(t, "image-photon-all-layers", source.AllLayersScope)
|
||||||
var packages []pkg.Package
|
var count int
|
||||||
for p := range sbom.Artifacts.Packages.Enumerate() {
|
for range sbom.Artifacts.Packages.Enumerate(pkg.RpmPkg) {
|
||||||
packages = append(packages, p)
|
count++
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(packages) < 1 {
|
assert.Greater(t, count, 0, "expected to find RPM packages in the SBOM (but did not)")
|
||||||
t.Errorf("failed to find packages for photon distro; wanted > 0 got 0")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
5
go.mod
5
go.mod
@ -100,7 +100,7 @@ require (
|
|||||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
github.com/Microsoft/hcsshim v0.11.7 // indirect
|
github.com/Microsoft/hcsshim v0.11.7 // indirect
|
||||||
github.com/ProtonMail/go-crypto v1.1.6 // indirect
|
github.com/ProtonMail/go-crypto v1.2.0 // indirect
|
||||||
github.com/STARRY-S/zip v0.2.1 // indirect
|
github.com/STARRY-S/zip v0.2.1 // indirect
|
||||||
github.com/agext/levenshtein v1.2.1 // indirect; indirectt
|
github.com/agext/levenshtein v1.2.1 // indirect; indirectt
|
||||||
github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 // indirect
|
github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 // indirect
|
||||||
@ -263,3 +263,6 @@ retract (
|
|||||||
v0.53.2
|
v0.53.2
|
||||||
v0.53.1 // Published accidentally with incorrect license in depdencies
|
v0.53.1 // Published accidentally with incorrect license in depdencies
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// adds RSA Headers from RPMDB via https://github.com/knqyf263/go-rpmdb/pull/58 (in review)
|
||||||
|
replace github.com/knqyf263/go-rpmdb => github.com/anchore/go-rpmdb v0.0.0-20250515153519-38be2efee8ed
|
||||||
|
|||||||
8
go.sum
8
go.sum
@ -78,8 +78,8 @@ github.com/Microsoft/hcsshim v0.11.7/go.mod h1:MV8xMfmECjl5HdO7U/3/hFVnkmSBjAjmA
|
|||||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||||
github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8=
|
github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8=
|
||||||
github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
|
github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
|
||||||
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
|
github.com/ProtonMail/go-crypto v1.2.0 h1:+PhXXn4SPGd+qk76TlEePBfOfivE0zkWFenhGhFLzWs=
|
||||||
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
|
github.com/ProtonMail/go-crypto v1.2.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
|
||||||
github.com/STARRY-S/zip v0.2.1 h1:pWBd4tuSGm3wtpoqRZZ2EAwOmcHK6XFf7bU9qcJXyFg=
|
github.com/STARRY-S/zip v0.2.1 h1:pWBd4tuSGm3wtpoqRZZ2EAwOmcHK6XFf7bU9qcJXyFg=
|
||||||
github.com/STARRY-S/zip v0.2.1/go.mod h1:xNvshLODWtC4EJ702g7cTYn13G53o1+X9BWnPFpcWV4=
|
github.com/STARRY-S/zip v0.2.1/go.mod h1:xNvshLODWtC4EJ702g7cTYn13G53o1+X9BWnPFpcWV4=
|
||||||
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8=
|
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8=
|
||||||
@ -110,6 +110,8 @@ github.com/anchore/go-logger v0.0.0-20250318195838-07ae343dd722 h1:2SqmFgE7h+Ql4
|
|||||||
github.com/anchore/go-logger v0.0.0-20250318195838-07ae343dd722/go.mod h1:oFuE8YuTCM+spgMXhePGzk3asS94yO9biUfDzVTFqNw=
|
github.com/anchore/go-logger v0.0.0-20250318195838-07ae343dd722/go.mod h1:oFuE8YuTCM+spgMXhePGzk3asS94yO9biUfDzVTFqNw=
|
||||||
github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb h1:iDMnx6LIjtjZ46C0akqveX83WFzhpTD3eqOthawb5vU=
|
github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb h1:iDMnx6LIjtjZ46C0akqveX83WFzhpTD3eqOthawb5vU=
|
||||||
github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb/go.mod h1:DmTY2Mfcv38hsHbG78xMiTDdxFtkHpgYNVDPsF2TgHk=
|
github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb/go.mod h1:DmTY2Mfcv38hsHbG78xMiTDdxFtkHpgYNVDPsF2TgHk=
|
||||||
|
github.com/anchore/go-rpmdb v0.0.0-20250515153519-38be2efee8ed h1:ixtJ1s+L3+AvxlGn4Tm5VcV7RiMVLcUGmNOGXQ6U9Gg=
|
||||||
|
github.com/anchore/go-rpmdb v0.0.0-20250515153519-38be2efee8ed/go.mod h1:0A7fN6+ED0l7YrO4GNEz6kgDmkKUwzK2bDl2v0E2Hog=
|
||||||
github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 h1:aM1rlcoLz8y5B2r4tTLMiVTrMtpfY0O8EScKJxaSaEc=
|
github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 h1:aM1rlcoLz8y5B2r4tTLMiVTrMtpfY0O8EScKJxaSaEc=
|
||||||
github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092/go.mod h1:rYqSE9HbjzpHTI74vwPvae4ZVYZd1lue2ta6xHPdblA=
|
github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092/go.mod h1:rYqSE9HbjzpHTI74vwPvae4ZVYZd1lue2ta6xHPdblA=
|
||||||
github.com/anchore/go-sync v0.0.0-20250326131806-4eda43a485b6 h1:Ha+LSCVuXYSYGi7wIkJK6G8g6jI3LH7y6LbyEVyp4Io=
|
github.com/anchore/go-sync v0.0.0-20250326131806-4eda43a485b6 h1:Ha+LSCVuXYSYGi7wIkJK6G8g6jI3LH7y6LbyEVyp4Io=
|
||||||
@ -548,8 +550,6 @@ github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYW
|
|||||||
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||||
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
|
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
|
||||||
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||||
github.com/knqyf263/go-rpmdb v0.1.1 h1:oh68mTCvp1XzxdU7EfafcWzzfstUZAEa3MW0IJye584=
|
|
||||||
github.com/knqyf263/go-rpmdb v0.1.1/go.mod h1:9LQcoMCMQ9vrF7HcDtXfvqGO4+ddxFQ8+YF/0CVGDww=
|
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||||
|
|||||||
@ -3,5 +3,5 @@ package internal
|
|||||||
const (
|
const (
|
||||||
// JSONSchemaVersion is the current schema version output by the JSON encoder
|
// JSONSchemaVersion is the current schema version output by the JSON encoder
|
||||||
// This is roughly following the "SchemaVer" guidelines for versioning the JSON schema. Please see schema/json/README.md for details on how to increment.
|
// This is roughly following the "SchemaVer" guidelines for versioning the JSON schema. Please see schema/json/README.md for details on how to increment.
|
||||||
JSONSchemaVersion = "16.0.32"
|
JSONSchemaVersion = "16.0.33"
|
||||||
)
|
)
|
||||||
|
|||||||
3087
schema/json/schema-16.0.33.json
Normal file
3087
schema/json/schema-16.0.33.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"$id": "anchore.io/schema/syft/json/16.0.32/document",
|
"$id": "anchore.io/schema/syft/json/16.0.33/document",
|
||||||
"$ref": "#/$defs/Document",
|
"$ref": "#/$defs/Document",
|
||||||
"$defs": {
|
"$defs": {
|
||||||
"AlpmDbEntry": {
|
"AlpmDbEntry": {
|
||||||
@ -2677,6 +2677,12 @@
|
|||||||
"sourceRpm": {
|
"sourceRpm": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"signatures": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/RpmSignature"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
"size": {
|
"size": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
@ -2745,6 +2751,12 @@
|
|||||||
"sourceRpm": {
|
"sourceRpm": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"signatures": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/RpmSignature"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
"size": {
|
"size": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
@ -2821,6 +2833,29 @@
|
|||||||
"flags"
|
"flags"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"RpmSignature": {
|
||||||
|
"properties": {
|
||||||
|
"algo": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"hash": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"created": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"issuer": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"algo",
|
||||||
|
"hash",
|
||||||
|
"created",
|
||||||
|
"issuer"
|
||||||
|
]
|
||||||
|
},
|
||||||
"RubyGemspec": {
|
"RubyGemspec": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"name": {
|
"name": {
|
||||||
|
|||||||
@ -96,6 +96,10 @@ func (p *CatalogTester) WithContext(ctx context.Context) *CatalogTester {
|
|||||||
func (p *CatalogTester) FromDirectory(t *testing.T, path string) *CatalogTester {
|
func (p *CatalogTester) FromDirectory(t *testing.T, path string) *CatalogTester {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
|
if path == "" {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
s, err := directorysource.NewFromPath(path)
|
s, err := directorysource.NewFromPath(path)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@ -109,6 +113,10 @@ func (p *CatalogTester) FromDirectory(t *testing.T, path string) *CatalogTester
|
|||||||
func (p *CatalogTester) FromFile(t *testing.T, path string) *CatalogTester {
|
func (p *CatalogTester) FromFile(t *testing.T, path string) *CatalogTester {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
|
if path == "" {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
fixture, err := os.Open(path)
|
fixture, err := os.Open(path)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@ -157,6 +165,11 @@ func (p *CatalogTester) WithResolver(r file.Resolver) *CatalogTester {
|
|||||||
|
|
||||||
func (p *CatalogTester) WithImageResolver(t *testing.T, fixtureName string) *CatalogTester {
|
func (p *CatalogTester) WithImageResolver(t *testing.T, fixtureName string) *CatalogTester {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
|
if fixtureName == "" {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
img := imagetest.GetFixtureImage(t, "docker-archive", fixtureName)
|
img := imagetest.GetFixtureImage(t, "docker-archive", fixtureName)
|
||||||
|
|
||||||
s := stereoscopesource.New(img, stereoscopesource.ImageConfig{
|
s := stereoscopesource.New(img, stereoscopesource.ImageConfig{
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/google/go-cmp/cmp/cmpopts"
|
"github.com/google/go-cmp/cmp/cmpopts"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
_ "modernc.org/sqlite"
|
_ "modernc.org/sqlite"
|
||||||
|
|
||||||
"github.com/anchore/syft/syft/artifact"
|
"github.com/anchore/syft/syft/artifact"
|
||||||
@ -36,6 +37,7 @@ func Test_DBCataloger(t *testing.T) {
|
|||||||
SourceRpm: "basesystem-11-13.el9.src.rpm",
|
SourceRpm: "basesystem-11-13.el9.src.rpm",
|
||||||
Size: 0,
|
Size: 0,
|
||||||
Vendor: "Rocky Enterprise Software Foundation",
|
Vendor: "Rocky Enterprise Software Foundation",
|
||||||
|
Signatures: mustParseSignatures(t, "RSA/SHA256, Wed May 11 11:12:32 2022, Key ID 702d426d350d275d"),
|
||||||
Provides: []string{"basesystem"},
|
Provides: []string{"basesystem"},
|
||||||
Requires: []string{
|
Requires: []string{
|
||||||
"filesystem",
|
"filesystem",
|
||||||
@ -65,6 +67,7 @@ func Test_DBCataloger(t *testing.T) {
|
|||||||
Release: "6.el9_1",
|
Release: "6.el9_1",
|
||||||
SourceRpm: "bash-5.1.8-6.el9_1.src.rpm",
|
SourceRpm: "bash-5.1.8-6.el9_1.src.rpm",
|
||||||
Size: 7738634,
|
Size: 7738634,
|
||||||
|
Signatures: mustParseSignatures(t, "RSA/SHA256, Mon Jan 23 22:49:22 2023, Key ID 702d426d350d275d"),
|
||||||
ModularityLabel: strRef(""),
|
ModularityLabel: strRef(""),
|
||||||
Vendor: "Rocky Enterprise Software Foundation",
|
Vendor: "Rocky Enterprise Software Foundation",
|
||||||
Provides: []string{
|
Provides: []string{
|
||||||
@ -117,6 +120,7 @@ func Test_DBCataloger(t *testing.T) {
|
|||||||
Release: "2.el9",
|
Release: "2.el9",
|
||||||
SourceRpm: "filesystem-3.16-2.el9.src.rpm",
|
SourceRpm: "filesystem-3.16-2.el9.src.rpm",
|
||||||
Size: 106,
|
Size: 106,
|
||||||
|
Signatures: mustParseSignatures(t, "RSA/SHA256, Mon May 16 12:32:55 2022, Key ID 702d426d350d275d"),
|
||||||
ModularityLabel: strRef(""),
|
ModularityLabel: strRef(""),
|
||||||
Vendor: "Rocky Enterprise Software Foundation",
|
Vendor: "Rocky Enterprise Software Foundation",
|
||||||
Provides: []string{
|
Provides: []string{
|
||||||
@ -334,3 +338,9 @@ func Test_denySelfReferences(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func mustParseSignatures(t testing.TB, sigs ...string) []pkg.RpmSignature {
|
||||||
|
signatures, err := parseSignatures(sigs...)
|
||||||
|
require.NoError(t, err)
|
||||||
|
return signatures
|
||||||
|
}
|
||||||
|
|||||||
@ -1,9 +1,14 @@
|
|||||||
package redhat
|
package redhat
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
rpmdb "github.com/knqyf263/go-rpmdb/pkg"
|
rpmdb "github.com/knqyf263/go-rpmdb/pkg"
|
||||||
"github.com/sassoftware/go-rpmutils"
|
"github.com/sassoftware/go-rpmutils"
|
||||||
@ -15,6 +20,42 @@ import (
|
|||||||
"github.com/anchore/syft/syft/pkg/cataloger/generic"
|
"github.com/anchore/syft/syft/pkg/cataloger/generic"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type pgpSig struct {
|
||||||
|
_ [3]byte
|
||||||
|
Date int32
|
||||||
|
KeyID [8]byte
|
||||||
|
PubKeyAlgo uint8
|
||||||
|
HashAlgo uint8
|
||||||
|
}
|
||||||
|
|
||||||
|
type textSig struct {
|
||||||
|
_ [2]byte
|
||||||
|
PubKeyAlgo uint8
|
||||||
|
HashAlgo uint8
|
||||||
|
_ [4]byte
|
||||||
|
Date int32
|
||||||
|
_ [4]byte
|
||||||
|
KeyID [8]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type pgp4Sig struct {
|
||||||
|
_ [2]byte
|
||||||
|
PubKeyAlgo uint8
|
||||||
|
HashAlgo uint8
|
||||||
|
_ [17]byte
|
||||||
|
KeyID [8]byte
|
||||||
|
_ [2]byte
|
||||||
|
Date int32
|
||||||
|
}
|
||||||
|
|
||||||
|
var pubKeyLookup = map[uint8]string{
|
||||||
|
0x01: "RSA",
|
||||||
|
}
|
||||||
|
var hashLookup = map[uint8]string{
|
||||||
|
0x02: "SHA1",
|
||||||
|
0x08: "SHA256",
|
||||||
|
}
|
||||||
|
|
||||||
// parseRpmArchive parses a single RPM
|
// parseRpmArchive parses a single RPM
|
||||||
func parseRpmArchive(ctx context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
|
func parseRpmArchive(ctx context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
|
||||||
rpm, err := rpmutils.ReadRpm(reader)
|
rpm, err := rpmutils.ReadRpm(reader)
|
||||||
@ -44,6 +85,18 @@ func parseRpmArchive(ctx context.Context, _ file.Resolver, _ *generic.Environmen
|
|||||||
files, err := rpm.Header.GetFiles()
|
files, err := rpm.Header.GetFiles()
|
||||||
logRpmArchiveErr(reader.Location, "files", err)
|
logRpmArchiveErr(reader.Location, "files", err)
|
||||||
|
|
||||||
|
rsa, err := rpm.Header.GetBytes(rpmutils.SIG_RSA)
|
||||||
|
logRpmArchiveErr(reader.Location, "rsa signature", err)
|
||||||
|
|
||||||
|
pgp, err := rpm.Header.GetBytes(rpmutils.SIG_PGP)
|
||||||
|
logRpmArchiveErr(reader.Location, "pgp signature", err)
|
||||||
|
|
||||||
|
var allSigs [][]byte
|
||||||
|
allSigs = append(allSigs, rsa)
|
||||||
|
allSigs = append(allSigs, pgp)
|
||||||
|
sigs, err := parseSignatureHeaders(allSigs)
|
||||||
|
logRpmArchiveErr(reader.Location, "signature", err)
|
||||||
|
|
||||||
metadata := pkg.RpmArchive{
|
metadata := pkg.RpmArchive{
|
||||||
Name: nevra.Name,
|
Name: nevra.Name,
|
||||||
Version: nevra.Version,
|
Version: nevra.Version,
|
||||||
@ -51,6 +104,7 @@ func parseRpmArchive(ctx context.Context, _ file.Resolver, _ *generic.Environmen
|
|||||||
Arch: nevra.Arch,
|
Arch: nevra.Arch,
|
||||||
Release: nevra.Release,
|
Release: nevra.Release,
|
||||||
SourceRpm: sourceRpm,
|
SourceRpm: sourceRpm,
|
||||||
|
Signatures: sigs,
|
||||||
Vendor: vendor,
|
Vendor: vendor,
|
||||||
Size: int(size),
|
Size: int(size),
|
||||||
Files: mapFiles(files, digestAlgorithm),
|
Files: mapFiles(files, digestAlgorithm),
|
||||||
@ -59,6 +113,112 @@ func parseRpmArchive(ctx context.Context, _ file.Resolver, _ *generic.Environmen
|
|||||||
return []pkg.Package{newArchivePackage(ctx, reader.Location, metadata, licenses)}, nil, nil
|
return []pkg.Package{newArchivePackage(ctx, reader.Location, metadata, licenses)}, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseSignatureHeaders(data [][]byte) ([]pkg.RpmSignature, error) {
|
||||||
|
sigMap := make(map[string]pkg.RpmSignature)
|
||||||
|
var keys []string
|
||||||
|
for _, sig := range data {
|
||||||
|
if len(sig) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s, err := parsePGP(sig)
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields("error", err).Trace("unable to parse RPM archive signature")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
k := s.String()
|
||||||
|
if _, ok := sigMap[k]; ok {
|
||||||
|
// if we have a duplicate signature, just skip it
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
sigMap[k] = *s
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
var signatures []pkg.RpmSignature
|
||||||
|
sort.Strings(keys)
|
||||||
|
for _, k := range keys {
|
||||||
|
signatures = append(signatures, sigMap[k])
|
||||||
|
}
|
||||||
|
|
||||||
|
return signatures, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parsePGP(data []byte) (*pkg.RpmSignature, error) {
|
||||||
|
var tag, signatureType, version uint8
|
||||||
|
|
||||||
|
r := bytes.NewReader(data)
|
||||||
|
err := binary.Read(r, binary.BigEndian, &tag)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = binary.Read(r, binary.BigEndian, &signatureType)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = binary.Read(r, binary.BigEndian, &version)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch signatureType {
|
||||||
|
case 0x01:
|
||||||
|
switch version {
|
||||||
|
case 0x1c:
|
||||||
|
sig := textSig{}
|
||||||
|
err = binary.Read(r, binary.BigEndian, &sig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid PGP signature on decode: %w", err)
|
||||||
|
}
|
||||||
|
return &pkg.RpmSignature{
|
||||||
|
PublicKeyAlgorithm: pubKeyLookup[sig.PubKeyAlgo],
|
||||||
|
HashAlgorithm: hashLookup[sig.HashAlgo],
|
||||||
|
Created: time.Unix(int64(sig.Date), 0).UTC().Format("Mon Jan _2 15:04:05 2006"),
|
||||||
|
IssuerKeyID: fmt.Sprintf("%x", sig.KeyID),
|
||||||
|
}, nil
|
||||||
|
default:
|
||||||
|
return decodePGPSig(version, r)
|
||||||
|
}
|
||||||
|
case 0x02:
|
||||||
|
return decodePGPSig(version, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("unknown signature type: %d", signatureType)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodePGPSig(version uint8, r io.Reader) (*pkg.RpmSignature, error) {
|
||||||
|
var pubKeyAlgo, hashAlgo, pkgDate string
|
||||||
|
var keyID [8]byte
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case version > 0x15:
|
||||||
|
sig := pgp4Sig{}
|
||||||
|
err := binary.Read(r, binary.BigEndian, &sig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid PGP v4 signature on decode: %w", err)
|
||||||
|
}
|
||||||
|
pubKeyAlgo = pubKeyLookup[sig.PubKeyAlgo]
|
||||||
|
hashAlgo = hashLookup[sig.HashAlgo]
|
||||||
|
pkgDate = time.Unix(int64(sig.Date), 0).UTC().Format("Mon Jan _2 15:04:05 2006")
|
||||||
|
keyID = sig.KeyID
|
||||||
|
|
||||||
|
default:
|
||||||
|
sig := pgpSig{}
|
||||||
|
err := binary.Read(r, binary.BigEndian, &sig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid PGP signature on decode: %w", err)
|
||||||
|
}
|
||||||
|
pubKeyAlgo = pubKeyLookup[sig.PubKeyAlgo]
|
||||||
|
hashAlgo = hashLookup[sig.HashAlgo]
|
||||||
|
pkgDate = time.Unix(int64(sig.Date), 0).UTC().Format("Mon Jan _2 15:04:05 2006")
|
||||||
|
keyID = sig.KeyID
|
||||||
|
}
|
||||||
|
return &pkg.RpmSignature{
|
||||||
|
PublicKeyAlgorithm: pubKeyAlgo,
|
||||||
|
HashAlgorithm: hashAlgo,
|
||||||
|
Created: pkgDate,
|
||||||
|
IssuerKeyID: fmt.Sprintf("%x", keyID),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func getDigestAlgorithm(location file.Location, header *rpmutils.RpmHeader) string {
|
func getDigestAlgorithm(location file.Location, header *rpmutils.RpmHeader) string {
|
||||||
digestAlgorithm, err := header.GetString(rpmutils.FILEDIGESTALGO)
|
digestAlgorithm, err := header.GetString(rpmutils.FILEDIGESTALGO)
|
||||||
logRpmArchiveErr(location, "file digest algo", err)
|
logRpmArchiveErr(location, "file digest algo", err)
|
||||||
@ -109,6 +269,6 @@ func parseEpoch(epoch string) *int {
|
|||||||
|
|
||||||
func logRpmArchiveErr(location file.Location, operation string, err error) {
|
func logRpmArchiveErr(location file.Location, operation string, err error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debugf("ERROR in parse_rpm_archive %s file: %s: %v", operation, location.RealPath, err)
|
log.WithFields("error", err, "operation", operation, "path", location.RealPath).Trace("unable to parse RPM archive")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,8 +2,14 @@ package redhat
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/hex"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/google/go-cmp/cmp/cmpopts"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/anchore/syft/syft/file"
|
"github.com/anchore/syft/syft/file"
|
||||||
"github.com/anchore/syft/syft/pkg"
|
"github.com/anchore/syft/syft/pkg"
|
||||||
"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
|
"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
|
||||||
@ -14,11 +20,15 @@ func TestParseRpmFiles(t *testing.T) {
|
|||||||
abcRpmLocation := file.NewLocation("abc-1.01-9.hg20160905.el7.x86_64.rpm")
|
abcRpmLocation := file.NewLocation("abc-1.01-9.hg20160905.el7.x86_64.rpm")
|
||||||
zorkRpmLocation := file.NewLocation("zork-1.0.3-1.el7.x86_64.rpm")
|
zorkRpmLocation := file.NewLocation("zork-1.0.3-1.el7.x86_64.rpm")
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
fixture string
|
name string
|
||||||
|
fixtureDir string
|
||||||
|
fixtureImage string
|
||||||
|
skipFiles bool
|
||||||
expected []pkg.Package
|
expected []pkg.Package
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
fixture: "test-fixtures/rpms",
|
name: "go case",
|
||||||
|
fixtureDir: "test-fixtures/rpms",
|
||||||
expected: []pkg.Package{
|
expected: []pkg.Package{
|
||||||
{
|
{
|
||||||
Name: "abc",
|
Name: "abc",
|
||||||
@ -37,6 +47,14 @@ func TestParseRpmFiles(t *testing.T) {
|
|||||||
Release: "9.hg20160905.el7",
|
Release: "9.hg20160905.el7",
|
||||||
Version: "1.01",
|
Version: "1.01",
|
||||||
SourceRpm: "abc-1.01-9.hg20160905.el7.src.rpm",
|
SourceRpm: "abc-1.01-9.hg20160905.el7.src.rpm",
|
||||||
|
Signatures: []pkg.RpmSignature{
|
||||||
|
{
|
||||||
|
PublicKeyAlgorithm: "RSA",
|
||||||
|
HashAlgorithm: "SHA256",
|
||||||
|
Created: "Wed Sep 21 07:09:44 2016",
|
||||||
|
IssuerKeyID: "6a2faea2352c64e5",
|
||||||
|
},
|
||||||
|
},
|
||||||
Size: 17396,
|
Size: 17396,
|
||||||
Vendor: "Fedora Project",
|
Vendor: "Fedora Project",
|
||||||
Files: []pkg.RpmFileRecord{
|
Files: []pkg.RpmFileRecord{
|
||||||
@ -66,6 +84,14 @@ func TestParseRpmFiles(t *testing.T) {
|
|||||||
Version: "1.0.3",
|
Version: "1.0.3",
|
||||||
SourceRpm: "zork-1.0.3-1.el7.src.rpm",
|
SourceRpm: "zork-1.0.3-1.el7.src.rpm",
|
||||||
Size: 262367,
|
Size: 262367,
|
||||||
|
Signatures: []pkg.RpmSignature{
|
||||||
|
{
|
||||||
|
PublicKeyAlgorithm: "RSA",
|
||||||
|
HashAlgorithm: "SHA256",
|
||||||
|
Created: "Tue Mar 2 17:32:21 2021",
|
||||||
|
IssuerKeyID: "6a2faea2352c64e5",
|
||||||
|
},
|
||||||
|
},
|
||||||
Vendor: "Fedora Project",
|
Vendor: "Fedora Project",
|
||||||
Files: []pkg.RpmFileRecord{
|
Files: []pkg.RpmFileRecord{
|
||||||
{"/usr/bin/zork", 33261, 115440, file.Digest{"sha256", "31b2ffc20b676a8fff795a45308f584273b9c47e8f7e196b4f36220b2734b472"}, "root", "root", ""},
|
{"/usr/bin/zork", 33261, 115440, file.Digest{"sha256", "31b2ffc20b676a8fff795a45308f584273b9c47e8f7e196b4f36220b2734b472"}, "root", "root", ""},
|
||||||
@ -83,20 +109,128 @@ func TestParseRpmFiles(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fixture: "test-fixtures/bad",
|
name: "bad rpms",
|
||||||
|
fixtureDir: "test-fixtures/bad",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "rpms with signatures from RSA header",
|
||||||
|
fixtureImage: "image-rpm-archive",
|
||||||
|
skipFiles: true,
|
||||||
|
expected: []pkg.Package{
|
||||||
|
{
|
||||||
|
Name: "postgresql14-server",
|
||||||
|
Version: "0:14.10-1PGDG.rhel9",
|
||||||
|
PURL: "pkg:rpm/postgresql14-server@14.10-1PGDG.rhel9?arch=x86_64&epoch=0&upstream=postgresql14-14.10-1PGDG.rhel9.src.rpm",
|
||||||
|
Locations: file.NewLocationSet(file.NewLocation("/postgresql14-server-14.10-1PGDG.rhel9.x86_64.rpm")),
|
||||||
|
FoundBy: "rpm-archive-cataloger",
|
||||||
|
Type: pkg.RpmPkg,
|
||||||
|
Licenses: pkg.NewLicenseSet(pkg.NewLicenseFromLocations("PostgreSQL", file.NewLocation("/postgresql14-server-14.10-1PGDG.rhel9.x86_64.rpm"))),
|
||||||
|
Language: "",
|
||||||
|
CPEs: nil,
|
||||||
|
Metadata: pkg.RpmArchive{
|
||||||
|
Name: "postgresql14-server",
|
||||||
|
Version: "14.10",
|
||||||
|
Epoch: ref(0),
|
||||||
|
Arch: "x86_64",
|
||||||
|
Release: "1PGDG.rhel9",
|
||||||
|
SourceRpm: "postgresql14-14.10-1PGDG.rhel9.src.rpm",
|
||||||
|
Size: 24521699,
|
||||||
|
Signatures: []pkg.RpmSignature{
|
||||||
|
{
|
||||||
|
PublicKeyAlgorithm: "RSA",
|
||||||
|
HashAlgorithm: "SHA256",
|
||||||
|
Created: "Tue Jan 2 16:45:56 2024",
|
||||||
|
IssuerKeyID: "40bca2b408b40d20",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Vendor: "PostgreSQL Global Development Group",
|
||||||
|
// note: files are not asserted in this test
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.fixture, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
var opts []cmp.Option
|
||||||
|
if test.skipFiles {
|
||||||
|
opts = append(opts, cmpopts.IgnoreFields(pkg.RpmArchive{}, "Files"))
|
||||||
|
}
|
||||||
pkgtest.NewCatalogTester().
|
pkgtest.NewCatalogTester().
|
||||||
FromDirectory(t, test.fixture).
|
WithCompareOptions(opts...).
|
||||||
|
FromDirectory(t, test.fixtureDir).
|
||||||
|
WithImageResolver(t, test.fixtureImage).
|
||||||
|
IgnoreLocationLayer().
|
||||||
Expects(test.expected, nil).
|
Expects(test.expected, nil).
|
||||||
TestCataloger(t, NewArchiveCataloger())
|
TestCataloger(t, NewArchiveCataloger())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_parseRSA(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
data string
|
||||||
|
want *pkg.RpmSignature
|
||||||
|
wantErr assert.ErrorAssertionFunc
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "older RSA header",
|
||||||
|
data: "89021503050058d3e39b0946fca2c105b9de0102b12a1000a2b3d347b51142e83b2de5e03ba9096f6330b72c140e46200d662b01c78534d14fab2ad4f07325119386830dd590219f27a22e420680283c500c40e6fba95404884b0a0abca8f198030ddc03653b7db2883b8230687e9e73d43eb5a24dbabfa48bbb3d1151ed264744e5e8ca169b0c4673a1440a9b99e53e693c9722f6423833cd7795e3044227fb922e21b7c007f03e923fae3f04d1ac2e8581e68c6790115b6dccfc02c8cb41681ed84785df086d6e26008c257d088a524ba2e7a7a5f41ad26b106c67b87fe48118b69662db612c23d2140059286f1ba7764627def6867ad0e11fe3a01fb1422dabe6f5cdf4cd876dc4fadfd2364bc3ba3758db94aaf3b82368cba65cf762287f713eb7ddc773acf93b083c739577a7eaf1f99e7dcbb8db1da050490e9fb67c838448db060a9e619d318c96f03e4363808d84ce29e8c102c290cc2bfab5746f3d9ddc9eb8b428f3ad2678abb2d46e846ddca7fc41322d76a97be6d416b4750f23320ec725e082be4496483b4cd3a3d2c515b3c8a6e27541139d809245140303877b84842ed2dd0454a78b2dfb7d6213784697077a8167942ebda5995a28d8256957e33e301706c35944ae05c7a54a4dd89be654d26cefa5cf0f616bbeaf317138371b09c5bbd5531f716020e553354ce5dbce3d9bb72f21e1857408dfd5a35250ff40f61ae1e25409ae9d21db76b8878341f4762a22be2189",
|
||||||
|
// RSA/SHA1, Thu Mar 23 15:02:51 2017, Key ID 0946fca2c105b9de
|
||||||
|
want: &pkg.RpmSignature{
|
||||||
|
PublicKeyAlgorithm: "RSA",
|
||||||
|
HashAlgorithm: "SHA1",
|
||||||
|
Created: "Thu Mar 23 15:02:51 2017",
|
||||||
|
IssuerKeyID: "0946fca2c105b9de",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "newer RSA header",
|
||||||
|
data: "89024a04000108003416210421cb256ae16fc54c6e652949702d426d350d275d050262804369161c72656c656e6740726f636b796c696e75782e6f7267000a0910702d426d350d275dc8910ffd14f0f80297481fea648e7ba5a74bce10c5faccc2bbe588caece04be34d304a6a445538afc97a7033d43c983d27cc8f5ee515b2dd92f3e03354c413e55372a4d19386eb0f2354f9a26ee5fc2e56dfda49555e4a58b49279b70cd2036b04f28125f85942f640f2984e29e079f26bf6f76831d83d95983aa084a3e7b6327be2e23d0d799c4b4d1cfb36147ddfb782bf9df7b331d97f4f46b38f968b6130d87b0ef6bb0d424390fe34e38092babed37440569a93f55f50a2bdb58be0259f35badf7e728bd49824ed47f69fa53b6e26736bde4d8358d959b090e88054c3e179745dc7377e41b54b4e10223f4859e88162c7c5ec64b78d36cf8a914c1c2deb8c4f19a70d406e70756a89195d6aee488a9b40b9dbb76b2c38e528eb88d08ec35774a48ed9ce4e0dfac45cb7613ad5921f54c61d3aae5d7b3ab0e2e6ff867ac8f395b37af78b5c01022a4a4e62f7a99425fccb7439880cd6b393a3050b2e9512693bc36f6fe9de2921dda59710a1508965065244cf9f0f8cfc5bd554777f1a84d2249339234d62f2441249f617ad7df4fb01367a91d3a880e86fdb84bc6d03a127b44a28c6ceadef89e438db9640aa59b8a3f460b07272511f8187a5f3b163c8fd1caa61667401bce2ccdb1c176c46be10ef8033903132cca5889fa3661b2fba590c41fa1c104c08426677bdbf745a52ccd28f581960cf9d7e4ede3b9584aacb2f20ef93",
|
||||||
|
// RSA/SHA256, Sun May 15 00:03:53 2022, Key ID 702d426d350d275d
|
||||||
|
want: &pkg.RpmSignature{
|
||||||
|
PublicKeyAlgorithm: "RSA",
|
||||||
|
HashAlgorithm: "SHA256",
|
||||||
|
Created: "Sun May 15 00:03:53 2022",
|
||||||
|
IssuerKeyID: "702d426d350d275d",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "example from rocky9",
|
||||||
|
data: "8901b304000108001d162104d4bf08ae67a0b4c7a1dbccd240bca2b408b40d20050265943dc4000a091040bca2b408b40d203b270bff71678ffeb190833a19a82112f59eee64cba186ab454d4526e0b3c8797e723f6916daff1b1f18cbf53c0da5d398a3a42065e79e5ca939f721652f38400dd4cac1107a902b1dae880649437ad0242444f3f07115172cae0a207b7cf8340af2f4a94976325f1dc165d5c2a564be322c4e130adb6217e7138b689f08898c407b223aa1ff8f8d592f31eba2256c02fae70ce4022d688a487972646b8bf1b518b5d6549c1e60fd812134422d9fdb41cf799f5eab80e48b4ab7cff84362dc867ed1af1416dd78e92bcc59217de7064b9a015d94a5097788689b9b6fbdeea679cfe4a6947f73dc3a6c810f2cb999d279b01564422d1500fc1bd8bd1eefa2d60660127ffef24067354660f93c0faf81f4edd599dd7e4b77fe4bff6c7a0ea83530c817c38d1f2364175883c6ef7b6dec86ad282bdd5138b8597567db96810c4ed6454a4ab1d98f0425dcd8892a5d46ed9289cb3ae3e1f1e2663d3e8188e873428f6cf7163563ed3860edc4fee81522389508847e692e2d13310eb4b40f7fdd7eb364a0b2dc",
|
||||||
|
// RSA/SHA256, Tue Jan 2 16:45:56 2024, Key ID 40bca2b408b40d20
|
||||||
|
want: &pkg.RpmSignature{
|
||||||
|
PublicKeyAlgorithm: "RSA",
|
||||||
|
HashAlgorithm: "SHA256",
|
||||||
|
Created: "Tue Jan 2 16:45:56 2024",
|
||||||
|
IssuerKeyID: "40bca2b408b40d20",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if tt.wantErr == nil {
|
||||||
|
tt.wantErr = assert.NoError
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := hex.DecodeString(tt.data)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
got, err := parsePGP(data)
|
||||||
|
if !tt.wantErr(t, err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert.Equal(t, tt.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ref[T any](v T) *T {
|
||||||
|
return &v
|
||||||
|
}
|
||||||
|
|
||||||
func Test_corruptRpmArchive(t *testing.T) {
|
func Test_corruptRpmArchive(t *testing.T) {
|
||||||
pkgtest.NewCatalogTester().
|
pkgtest.NewCatalogTester().
|
||||||
FromFile(t, "test-fixtures/bad/bad.rpm").
|
FromFile(t, "test-fixtures/bad/bad.rpm").
|
||||||
|
|||||||
@ -2,9 +2,11 @@ package redhat
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
rpmdb "github.com/knqyf263/go-rpmdb/pkg"
|
rpmdb "github.com/knqyf263/go-rpmdb/pkg"
|
||||||
|
|
||||||
@ -69,6 +71,14 @@ func parseRpmDB(ctx context.Context, resolver file.Resolver, env *generic.Enviro
|
|||||||
files, err := extractRpmFileRecords(resolver, *entry)
|
files, err := extractRpmFileRecords(resolver, *entry)
|
||||||
errs = unknown.Join(errs, err)
|
errs = unknown.Join(errs, err)
|
||||||
|
|
||||||
|
// there is a period of time when RPM DB entries contain both PGP and RSA signatures that are the same.
|
||||||
|
// This appears to be a holdover, where nowadays only the RSA Header is used.
|
||||||
|
sigs, err := parseSignatures(strings.TrimSpace(entry.PGP), strings.TrimSpace(entry.RSAHeader))
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields("error", err, "location", reader.RealPath, "pkg", fmt.Sprintf("%s@%s", entry.Name, entry.Version)).Trace("unable to parse signatures for package %s", entry.Name)
|
||||||
|
sigs = nil
|
||||||
|
}
|
||||||
|
|
||||||
metadata := pkg.RpmDBEntry{
|
metadata := pkg.RpmDBEntry{
|
||||||
Name: entry.Name,
|
Name: entry.Name,
|
||||||
Version: entry.Version,
|
Version: entry.Version,
|
||||||
@ -76,6 +86,7 @@ func parseRpmDB(ctx context.Context, resolver file.Resolver, env *generic.Enviro
|
|||||||
Arch: entry.Arch,
|
Arch: entry.Arch,
|
||||||
Release: entry.Release,
|
Release: entry.Release,
|
||||||
SourceRpm: entry.SourceRpm,
|
SourceRpm: entry.SourceRpm,
|
||||||
|
Signatures: sigs,
|
||||||
Vendor: entry.Vendor,
|
Vendor: entry.Vendor,
|
||||||
Size: entry.Size,
|
Size: entry.Size,
|
||||||
ModularityLabel: &entry.Modularitylabel,
|
ModularityLabel: &entry.Modularitylabel,
|
||||||
@ -110,6 +121,70 @@ func parseRpmDB(ctx context.Context, resolver file.Resolver, env *generic.Enviro
|
|||||||
return allPkgs, nil, errs
|
return allPkgs, nil, errs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseSignatures(sigs ...string) ([]pkg.RpmSignature, error) {
|
||||||
|
var parsedSigs []pkg.RpmSignature
|
||||||
|
var errs error
|
||||||
|
for _, sig := range sigs {
|
||||||
|
if sig == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
parts := strings.Split(sig, ",")
|
||||||
|
if len(parts) != 3 {
|
||||||
|
errs = errors.Join(fmt.Errorf("invalid signature format: %s", sig))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
methodParts := strings.SplitN(strings.TrimSpace(parts[0]), "/", 2)
|
||||||
|
if len(methodParts) != 2 {
|
||||||
|
errs = errors.Join(fmt.Errorf("invalid signature method format: %s", parts[0]))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
pka := strings.TrimSpace(methodParts[0])
|
||||||
|
hash := strings.TrimSpace(methodParts[1])
|
||||||
|
|
||||||
|
if pka == "" || hash == "" {
|
||||||
|
errs = errors.Join(fmt.Errorf("invalid signature method values: public-key=%q hash=%q", pka, hash))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
created := strings.TrimSpace(parts[1])
|
||||||
|
if created == "" {
|
||||||
|
errs = errors.Join(fmt.Errorf("invalid signature created value: %q", parts[1]))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
issuerFields := strings.Split(strings.TrimSpace(parts[2]), " ")
|
||||||
|
var issuer string
|
||||||
|
switch len(issuerFields) {
|
||||||
|
case 0:
|
||||||
|
errs = errors.Join(fmt.Errorf("no signature issuer value: %q", parts[2]))
|
||||||
|
case 1:
|
||||||
|
issuer = issuerFields[0]
|
||||||
|
default:
|
||||||
|
issuer = issuerFields[len(issuerFields)-1]
|
||||||
|
if issuer == "" {
|
||||||
|
errs = errors.Join(fmt.Errorf("invalid signature issuer value: %q", parts[2]))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(issuer) < 5 {
|
||||||
|
errs = errors.Join(fmt.Errorf("invalid signature issuer length: %q", parts[2]))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedSig := pkg.RpmSignature{
|
||||||
|
PublicKeyAlgorithm: pka,
|
||||||
|
HashAlgorithm: hash,
|
||||||
|
Created: created,
|
||||||
|
IssuerKeyID: issuer,
|
||||||
|
}
|
||||||
|
parsedSigs = append(parsedSigs, parsedSig)
|
||||||
|
}
|
||||||
|
return parsedSigs, errs
|
||||||
|
}
|
||||||
|
|
||||||
// The RPM naming scheme is [name]-[version]-[release]-[arch], where version is implicitly expands to [epoch]:[version].
|
// The RPM naming scheme is [name]-[version]-[release]-[arch], where version is implicitly expands to [epoch]:[version].
|
||||||
// RPM version comparison depends on comparing at least the version and release fields together as a subset of the
|
// RPM version comparison depends on comparing at least the version and release fields together as a subset of the
|
||||||
// naming scheme. This toELVersion function takes a RPM DB package information and converts it into a minimally comparable
|
// naming scheme. This toELVersion function takes a RPM DB package information and converts it into a minimally comparable
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/anchore/syft/syft/file"
|
"github.com/anchore/syft/syft/file"
|
||||||
"github.com/anchore/syft/syft/pkg"
|
"github.com/anchore/syft/syft/pkg"
|
||||||
@ -222,6 +223,132 @@ func Test_corruptRpmDbEntry(t *testing.T) {
|
|||||||
TestParser(t, parseRpmDB)
|
TestParser(t, parseRpmDB)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseSignatures(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
sigs []string
|
||||||
|
expected []pkg.RpmSignature
|
||||||
|
expectedError require.ErrorAssertionFunc
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid signature",
|
||||||
|
sigs: []string{"RSA/SHA256, Mon May 16 12:32:55 2022, Key ID 702d426d350d275d"},
|
||||||
|
expected: []pkg.RpmSignature{
|
||||||
|
{
|
||||||
|
PublicKeyAlgorithm: "RSA",
|
||||||
|
HashAlgorithm: "SHA256",
|
||||||
|
Created: "Mon May 16 12:32:55 2022",
|
||||||
|
IssuerKeyID: "702d426d350d275d",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple valid signatures",
|
||||||
|
sigs: []string{
|
||||||
|
"RSA/SHA256, Mon May 16 12:32:55 2022, Key ID 702d426d350d275d",
|
||||||
|
"DSA/SHA1, Tue Jun 14 09:45:12 2023, Key ID 123abc456def789",
|
||||||
|
},
|
||||||
|
expected: []pkg.RpmSignature{
|
||||||
|
{
|
||||||
|
PublicKeyAlgorithm: "RSA",
|
||||||
|
HashAlgorithm: "SHA256",
|
||||||
|
Created: "Mon May 16 12:32:55 2022",
|
||||||
|
IssuerKeyID: "702d426d350d275d",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
PublicKeyAlgorithm: "DSA",
|
||||||
|
HashAlgorithm: "SHA1",
|
||||||
|
Created: "Tue Jun 14 09:45:12 2023",
|
||||||
|
IssuerKeyID: "123abc456def789",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no signatures",
|
||||||
|
sigs: []string{},
|
||||||
|
expected: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty signatures",
|
||||||
|
sigs: []string{"", "", ""},
|
||||||
|
expected: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid parts count",
|
||||||
|
sigs: []string{"RSA/SHA256, Mon May 16 12:32:55 2022"},
|
||||||
|
expected: nil,
|
||||||
|
expectedError: require.Error,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid method format",
|
||||||
|
sigs: []string{"RSASHA256, Mon May 16 12:32:55 2022, Key ID 702d426d350d275d"},
|
||||||
|
expected: nil,
|
||||||
|
expectedError: require.Error,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty method values",
|
||||||
|
sigs: []string{"/, Mon May 16 12:32:55 2022, Key ID 702d426d350d275d"},
|
||||||
|
expected: nil,
|
||||||
|
expectedError: require.Error,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty created value",
|
||||||
|
sigs: []string{"RSA/SHA256, , Key ID 702d426d350d275d"},
|
||||||
|
expected: nil,
|
||||||
|
expectedError: require.Error,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty issuer value",
|
||||||
|
sigs: []string{"RSA/SHA256, Mon May 16 12:32:55 2022, Key ID "},
|
||||||
|
expected: nil,
|
||||||
|
expectedError: require.Error,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "issuer without prefix",
|
||||||
|
sigs: []string{"RSA/SHA256, Mon May 16 12:32:55 2022, 702d426d350d275d"},
|
||||||
|
expected: []pkg.RpmSignature{
|
||||||
|
{
|
||||||
|
PublicKeyAlgorithm: "RSA",
|
||||||
|
HashAlgorithm: "SHA256",
|
||||||
|
Created: "Mon May 16 12:32:55 2022",
|
||||||
|
IssuerKeyID: "702d426d350d275d",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mixed valid and invalid signatures",
|
||||||
|
sigs: []string{
|
||||||
|
"RSA/SHA256, Mon May 16 12:32:55 2022, Key ID 702d426d350d275d",
|
||||||
|
"DSASHA1, Tue Jun 14 09:45:12 2023, Key ID 123abc456def789",
|
||||||
|
},
|
||||||
|
expected: []pkg.RpmSignature{
|
||||||
|
{
|
||||||
|
PublicKeyAlgorithm: "RSA",
|
||||||
|
HashAlgorithm: "SHA256",
|
||||||
|
Created: "Mon May 16 12:32:55 2022",
|
||||||
|
IssuerKeyID: "702d426d350d275d",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedError: require.Error,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if tt.expectedError == nil {
|
||||||
|
tt.expectedError = require.NoError
|
||||||
|
}
|
||||||
|
got, err := parseSignatures(tt.sigs...)
|
||||||
|
tt.expectedError(t, err)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
require.Equal(t, tt.expected, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func intRef(i int) *int {
|
func intRef(i int) *int {
|
||||||
return &i
|
return &i
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,26 @@
|
|||||||
|
|
||||||
|
FROM --platform=linux/amd64 rockylinux:9 AS rpm-downloader
|
||||||
|
|
||||||
|
# download a signed RPM (PostgreSQL in this example, known to be signed)
|
||||||
|
# using PostgreSQL official RPM which is signed with their GPG key
|
||||||
|
# $ rpm -Kv postgresql14-server-14.10-1PGDG.rhel9.x86_64.rpm
|
||||||
|
# postgresql14-server-14.10-1PGDG.rhel9.x86_64.rpm:
|
||||||
|
# Header V4 RSA/SHA256 Signature, key ID 08b40d20: NOKEY
|
||||||
|
# Header SHA256 digest: OK
|
||||||
|
# Header SHA1 digest: OK
|
||||||
|
# Payload SHA256 digest: OK
|
||||||
|
# MD5 digest: OK
|
||||||
|
#
|
||||||
|
# $ rpm -ivh --nodeps --force postgresql14-server-14.10-1PGDG.rhel9.x86_64.rpm
|
||||||
|
# warning: postgresql14-server-14.10-1PGDG.rhel9.x86_64.rpm: Header V4 RSA/SHA256 Signature, key ID 08b40d20: NOKEY
|
||||||
|
# Verifying... ################################# [100%]
|
||||||
|
# Preparing... ################################# [100%]
|
||||||
|
#
|
||||||
|
# $ rpm -q --qf '%{NAME}-%{VERSION}-%{RELEASE} %{RSAHEADER:pgpsig}\n' postgresql14-server-14.10-1PGDG.rhel9.x86_64
|
||||||
|
# postgresql14-server-14.10-1PGDG.rhel9 RSA/SHA256, Tue Jan 2 16:45:56 2024, Key ID 40bca2b408b40d20
|
||||||
|
|
||||||
|
RUN curl -O https://download.postgresql.org/pub/repos/yum/14/redhat/rhel-9-x86_64/postgresql14-server-14.10-1PGDG.rhel9.x86_64.rpm
|
||||||
|
|
||||||
|
FROM scratch
|
||||||
|
|
||||||
|
COPY --from=rpm-downloader /postgresql14-server-14.10-1PGDG.rhel9.x86_64.rpm /postgresql14-server-14.10-1PGDG.rhel9.x86_64.rpm
|
||||||
@ -2,6 +2,7 @@ package pkg
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"sort"
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/scylladb/go-set/strset"
|
"github.com/scylladb/go-set/strset"
|
||||||
|
|
||||||
@ -32,6 +33,7 @@ type RpmDBEntry struct {
|
|||||||
Arch string `json:"architecture"`
|
Arch string `json:"architecture"`
|
||||||
Release string `json:"release" cyclonedx:"release"`
|
Release string `json:"release" cyclonedx:"release"`
|
||||||
SourceRpm string `json:"sourceRpm" cyclonedx:"sourceRpm"`
|
SourceRpm string `json:"sourceRpm" cyclonedx:"sourceRpm"`
|
||||||
|
Signatures []RpmSignature `json:"signatures,omitempty" cyclonedx:"signatures"`
|
||||||
Size int `json:"size" cyclonedx:"size"`
|
Size int `json:"size" cyclonedx:"size"`
|
||||||
Vendor string `json:"vendor"`
|
Vendor string `json:"vendor"`
|
||||||
ModularityLabel *string `json:"modularityLabel,omitempty"`
|
ModularityLabel *string `json:"modularityLabel,omitempty"`
|
||||||
@ -40,6 +42,22 @@ type RpmDBEntry struct {
|
|||||||
Files []RpmFileRecord `json:"files"`
|
Files []RpmFileRecord `json:"files"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RpmSignature struct {
|
||||||
|
PublicKeyAlgorithm string `json:"algo"`
|
||||||
|
HashAlgorithm string `json:"hash"`
|
||||||
|
Created string `json:"created"`
|
||||||
|
IssuerKeyID string `json:"issuer"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s RpmSignature) String() string {
|
||||||
|
if s.PublicKeyAlgorithm == "" && s.HashAlgorithm == "" && s.Created == "" && s.IssuerKeyID == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
// mimics the output you would see from rpm -q --qf "%{RSAHEADER}"
|
||||||
|
// e.g."RSA/SHA256, Mon May 16 12:32:55 2022, Key ID 702d426d350d275d"
|
||||||
|
return strings.Join([]string{s.PublicKeyAlgorithm + "/" + s.HashAlgorithm, s.Created, "Key ID " + s.IssuerKeyID}, ", ")
|
||||||
|
}
|
||||||
|
|
||||||
// RpmFileRecord represents the file metadata for a single file attributed to a RPM package.
|
// RpmFileRecord represents the file metadata for a single file attributed to a RPM package.
|
||||||
type RpmFileRecord struct {
|
type RpmFileRecord struct {
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/go-test/deep"
|
"github.com/go-test/deep"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestRpmMetadata_FileOwner(t *testing.T) {
|
func TestRpmMetadata_FileOwner(t *testing.T) {
|
||||||
@ -46,3 +47,49 @@ func TestRpmMetadata_FileOwner(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRpmSignature_String(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
signature RpmSignature
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "standard signature",
|
||||||
|
signature: RpmSignature{
|
||||||
|
PublicKeyAlgorithm: "RSA",
|
||||||
|
HashAlgorithm: "SHA256",
|
||||||
|
Created: "Mon May 16 12:32:55 2022",
|
||||||
|
IssuerKeyID: "702d426d350d275d",
|
||||||
|
},
|
||||||
|
expected: "RSA/SHA256, Mon May 16 12:32:55 2022, Key ID 702d426d350d275d",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty fields",
|
||||||
|
signature: RpmSignature{
|
||||||
|
PublicKeyAlgorithm: "",
|
||||||
|
HashAlgorithm: "",
|
||||||
|
Created: "",
|
||||||
|
IssuerKeyID: "",
|
||||||
|
},
|
||||||
|
expected: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "partial empty fields",
|
||||||
|
signature: RpmSignature{
|
||||||
|
PublicKeyAlgorithm: "RSA",
|
||||||
|
HashAlgorithm: "",
|
||||||
|
Created: "Mon May 16 12:32:55 2022",
|
||||||
|
IssuerKeyID: "",
|
||||||
|
},
|
||||||
|
expected: "RSA/, Mon May 16 12:32:55 2022, Key ID ",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := tt.signature.String()
|
||||||
|
assert.Equal(t, tt.expected, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user