feat: RHEL EUS detection (#4023)

* feat: rhel eus detection

Signed-off-by: Keith Zantow <kzantow@gmail.com>

* chore: update tests

Signed-off-by: Keith Zantow <kzantow@gmail.com>

* chore: update more tests

Signed-off-by: Keith Zantow <kzantow@gmail.com>

* rename feature detection functions

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

---------

Signed-off-by: Keith Zantow <kzantow@gmail.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:
Keith Zantow 2025-07-07 10:11:20 -04:00 committed by GitHub
parent 9cbd52bdd7
commit 02703d5c80
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 3435 additions and 7 deletions

View File

@ -3,5 +3,5 @@ package internal
const (
// 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.
JSONSchemaVersion = "16.0.34"
JSONSchemaVersion = "16.0.35"
)

View File

@ -0,0 +1,61 @@
package os
import (
"context"
"fmt"
"regexp"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/sbomsync"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/linux"
"github.com/anchore/syft/syft/sbom"
)
func DetectFeatures(_ context.Context, resolver file.Resolver, builder sbomsync.Builder) error {
builder.(sbomsync.Accessor).WriteToSBOM(func(s *sbom.SBOM) {
if s.Artifacts.LinuxDistribution == nil {
return
}
err := findRhelFeatures(resolver, s.Artifacts.LinuxDistribution)
if err != nil {
log.WithFields("error", err, "release", s.Artifacts.LinuxDistribution).Trace("error searching for extended support")
}
})
return nil
}
func findRhelFeatures(resolver file.Resolver, release *linux.Release) error {
if release == nil || release.ID != "rhel" {
return nil
}
contentManifestFiles, err := resolver.FilesByGlob("/root/buildinfo/content_manifests/*.json")
if err != nil {
return fmt.Errorf("unable to find content manifests: %w", err)
}
for _, contentManifestFile := range contentManifestFiles {
found, err := hasRhelExtendedSupportInContentManifest(resolver, contentManifestFile)
if err != nil {
return fmt.Errorf("unable to read content manifest from %s: %w", contentManifestFile.String(), err)
}
if found {
release.ExtendedSupport = true
break
}
}
return nil
}
func hasRhelExtendedSupportInContentManifest(resolver file.Resolver, contentManifestFile file.Location) (bool, error) {
contents, err := resolver.FileContentsByLocation(contentManifestFile)
if err != nil {
return false, fmt.Errorf("unable to read content manifest from %s: %w", contentManifestFile.String(), err)
}
defer internal.CloseAndLogError(contents, "content-manifest")
patterns := []*regexp.Regexp{regexp.MustCompile(`baseos-eus`), regexp.MustCompile(`baseos-eus|appstream-eus`)}
return internal.MatchAnyFromReader(contents, patterns...)
}

View File

@ -0,0 +1,97 @@
package os_test
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"github.com/anchore/stereoscope/pkg/imagetest"
"github.com/anchore/syft/internal/os"
"github.com/anchore/syft/internal/sbomsync"
"github.com/anchore/syft/internal/task"
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/linux"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
)
func Test_EnvironmentTask(t *testing.T) {
tests := []struct {
name string
expected linux.Release
}{
{
name: "not_rhel",
expected: linux.Release{
PrettyName: "Red Hat Enterprise Linux 9.4 (Plow)",
Name: "Red Hat Enterprise Linux",
ID: "not-rhel",
IDLike: []string{
"fedora",
},
Version: "9.4 (Plow)",
VersionID: "9.4",
HomeURL: "https://www.redhat.com/",
BugReportURL: "https://issues.redhat.com/",
CPEName: "cpe:/o:redhat:enterprise_linux:9::baseos",
ExtendedSupport: false, // important
},
},
{
name: "rhel_content_manifests",
expected: linux.Release{
PrettyName: "Red Hat Enterprise Linux 9.4 (Plow)",
Name: "Red Hat Enterprise Linux",
ID: "rhel",
IDLike: []string{
"fedora",
},
Version: "9.4 (Plow)",
VersionID: "9.4",
HomeURL: "https://www.redhat.com/",
BugReportURL: "https://issues.redhat.com/",
CPEName: "cpe:/o:redhat:enterprise_linux:9::baseos",
ExtendedSupport: true, // important
},
},
{
name: "rhel_no_manifests",
expected: linux.Release{
PrettyName: "Red Hat Enterprise Linux 9.4 (Plow)",
Name: "Red Hat Enterprise Linux",
ID: "rhel",
IDLike: []string{
"fedora",
},
Version: "9.4 (Plow)",
VersionID: "9.4",
HomeURL: "https://www.redhat.com/",
BugReportURL: "https://issues.redhat.com/",
CPEName: "cpe:/o:redhat:enterprise_linux:9::baseos",
ExtendedSupport: false, // important
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
tarPath := imagetest.GetFixtureImageTarPath(t, test.name)
// get the source
theSource, err := syft.GetSource(context.Background(), tarPath, syft.DefaultGetSourceConfig().WithSources("docker-archive"))
require.NoError(t, err)
resolver, err := theSource.FileResolver(source.SquashedScope)
require.NoError(t, err)
s := sbom.SBOM{}
err = task.NewEnvironmentTask().Execute(context.Background(), resolver, sbomsync.NewBuilder(&s))
require.NoError(t, err)
err = os.DetectFeatures(context.Background(), resolver, sbomsync.NewBuilder(&s))
require.NoError(t, err)
require.Equal(t, &test.expected, s.Artifacts.LinuxDistribution)
})
}
}

View File

@ -0,0 +1,4 @@
FROM scratch
COPY etc/os-release /etc/
COPY root/buildinfo/content_manifests/*.json /root/buildinfo/content_manifests/

View File

@ -0,0 +1,18 @@
NAME="Red Hat Enterprise Linux"
VERSION="9.4 (Plow)"
ID="not-rhel"
ID_LIKE="fedora"
VERSION_ID="9.4"
PLATFORM_ID="platform:el9"
PRETTY_NAME="Red Hat Enterprise Linux 9.4 (Plow)"
ANSI_COLOR="0;31"
LOGO="fedora-logo-icon"
CPE_NAME="cpe:/o:redhat:enterprise_linux:9::baseos"
HOME_URL="https://www.redhat.com/"
DOCUMENTATION_URL="https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/9"
BUG_REPORT_URL="https://issues.redhat.com/"
REDHAT_BUGZILLA_PRODUCT="Red Hat Enterprise Linux 9"
REDHAT_BUGZILLA_PRODUCT_VERSION=9.4
REDHAT_SUPPORT_PRODUCT="Red Hat Enterprise Linux"
REDHAT_SUPPORT_PRODUCT_VERSION="9.4"

View File

@ -0,0 +1,14 @@
{
"image_contents": [],
"metadata": {
"icm_spec": "https://raw.githubusercontent.com/containerbuildsystem/atomic-reactor/f4abcfdaf8247a6b074f94fa84f3846f82d781c6/atomic_reactor/schemas/content_manifest.json",
"icm_version": 1,
"image_layer_index": 3
},
"content_sets": [
"rhel-9-for-x86_64-appstream-eus-rpms__9_DOT_4",
"rhel-9-for-x86_64-baseos-eus-rpms__9_DOT_4",
"rhel-9-for-x86_64-rt-rpms__9_DOT_4",
"rhocp-4.16-for-rhel-9-x86_64-rpms"
]
}

View File

@ -0,0 +1,4 @@
FROM scratch
COPY etc/os-release /etc/
COPY root/buildinfo/content_manifests/*.json /root/buildinfo/content_manifests/

View File

@ -0,0 +1,18 @@
NAME="Red Hat Enterprise Linux"
VERSION="9.4 (Plow)"
ID="rhel"
ID_LIKE="fedora"
VERSION_ID="9.4"
PLATFORM_ID="platform:el9"
PRETTY_NAME="Red Hat Enterprise Linux 9.4 (Plow)"
ANSI_COLOR="0;31"
LOGO="fedora-logo-icon"
CPE_NAME="cpe:/o:redhat:enterprise_linux:9::baseos"
HOME_URL="https://www.redhat.com/"
DOCUMENTATION_URL="https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/9"
BUG_REPORT_URL="https://issues.redhat.com/"
REDHAT_BUGZILLA_PRODUCT="Red Hat Enterprise Linux 9"
REDHAT_BUGZILLA_PRODUCT_VERSION=9.4
REDHAT_SUPPORT_PRODUCT="Red Hat Enterprise Linux"
REDHAT_SUPPORT_PRODUCT_VERSION="9.4"

View File

@ -0,0 +1,14 @@
{
"image_contents": [],
"metadata": {
"icm_spec": "https://raw.githubusercontent.com/containerbuildsystem/atomic-reactor/f4abcfdaf8247a6b074f94fa84f3846f82d781c6/atomic_reactor/schemas/content_manifest.json",
"icm_version": 1,
"image_layer_index": 3
},
"content_sets": [
"rhel-9-for-x86_64-appstream-eus-rpms__9_DOT_4",
"rhel-9-for-x86_64-baseos-eus-rpms__9_DOT_4",
"rhel-9-for-x86_64-rt-rpms__9_DOT_4",
"rhocp-4.16-for-rhel-9-x86_64-rpms"
]
}

View File

@ -0,0 +1,13 @@
{
"image_contents": [],
"metadata": {
"icm_spec": "https://raw.githubusercontent.com/containerbuildsystem/atomic-reactor/f4abcfdaf8247a6b074f94fa84f3846f82d781c6/atomic_reactor/schemas/content_manifest.json",
"icm_version": 1,
"image_layer_index": 1
},
"content_sets": [
"rhel-9-for-x86_64-appstream-eus-rpms__9_DOT_4",
"rhel-9-for-x86_64-baseos-eus-rpms__9_DOT_4",
"rhocp-4.16-for-rhel-9-x86_64-rpms"
]
}

View File

@ -0,0 +1,3 @@
FROM scratch
COPY ./etc/os-release /etc/

View File

@ -0,0 +1,18 @@
NAME="Red Hat Enterprise Linux"
VERSION="9.4 (Plow)"
ID="rhel"
ID_LIKE="fedora"
VERSION_ID="9.4"
PLATFORM_ID="platform:el9"
PRETTY_NAME="Red Hat Enterprise Linux 9.4 (Plow)"
ANSI_COLOR="0;31"
LOGO="fedora-logo-icon"
CPE_NAME="cpe:/o:redhat:enterprise_linux:9::baseos"
HOME_URL="https://www.redhat.com/"
DOCUMENTATION_URL="https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/9"
BUG_REPORT_URL="https://issues.redhat.com/"
REDHAT_BUGZILLA_PRODUCT="Red Hat Enterprise Linux 9"
REDHAT_BUGZILLA_PRODUCT_VERSION=9.4
REDHAT_SUPPORT_PRODUCT="Red Hat Enterprise Linux"
REDHAT_SUPPORT_PRODUCT_VERSION="9.4"

View File

@ -0,0 +1,7 @@
package task
import "github.com/anchore/syft/internal/os"
func NewOSFeatureDetectionTask() Task {
return NewTask("os-feature-detection", os.DetectFeatures)
}

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "anchore.io/schema/syft/json/16.0.34/document",
"$id": "anchore.io/schema/syft/json/16.0.35/document",
"$ref": "#/$defs/Document",
"$defs": {
"AlpmDbEntry": {
@ -1628,6 +1628,9 @@
},
"supportEnd": {
"type": "string"
},
"extendedSupport": {
"type": "boolean"
}
},
"type": "object"

View File

@ -191,6 +191,7 @@ func (c *CreateSBOMConfig) makeTaskGroups(src source.Description) ([][]task.Task
scopeTasks := c.scopeTasks()
relationshipsTasks := c.relationshipTasks(src)
unknownTasks := c.unknownsTasks()
osFeatureDetectionTasks := c.osFeatureDetectionTasks()
pkgTasks, fileTasks, selectionEvidence, err := c.selectTasks(src)
if err != nil {
@ -220,6 +221,11 @@ func (c *CreateSBOMConfig) makeTaskGroups(src source.Description) ([][]task.Task
taskGroups = append(taskGroups, unknownTasks)
}
// osFeatureDetectionTasks should happen after package scanning is complete
if len(osFeatureDetectionTasks) > 0 {
taskGroups = append(taskGroups, osFeatureDetectionTasks)
}
// identifying the environment (i.e. the linux release) must be done first as this is required for package cataloging
taskGroups = append(
[][]task.Task{
@ -444,6 +450,17 @@ func (c *CreateSBOMConfig) unknownsTasks() []task.Task {
return tasks
}
// osFeatureDetectionTasks returns a set of tasks that perform post-processing feature detection and update the SBOM accordingly
func (c *CreateSBOMConfig) osFeatureDetectionTasks() []task.Task {
var tasks []task.Task
if t := task.NewOSFeatureDetectionTask(); t != nil {
tasks = append(tasks, t)
}
return tasks
}
func (c *CreateSBOMConfig) validate() error {
if c.Relationships.ExcludeBinaryPackagesWithFileOwnershipOverlap {
if !c.Relationships.PackageFileOwnershipOverlap {

View File

@ -91,6 +91,7 @@ func TestCreateSBOMConfig_makeTaskGroups(t *testing.T) {
fileCatalogerNames(),
relationshipCatalogerNames(),
unknownsTaskNames(),
osFeatureDetectionTaskNames(),
},
wantManifest: &catalogerManifest{
Requested: cataloging.SelectionRequest{
@ -110,6 +111,7 @@ func TestCreateSBOMConfig_makeTaskGroups(t *testing.T) {
fileCatalogerNames(),
relationshipCatalogerNames(),
unknownsTaskNames(),
osFeatureDetectionTaskNames(),
},
wantManifest: &catalogerManifest{
Requested: cataloging.SelectionRequest{
@ -130,6 +132,7 @@ func TestCreateSBOMConfig_makeTaskGroups(t *testing.T) {
fileCatalogerNames(),
relationshipCatalogerNames(),
unknownsTaskNames(),
osFeatureDetectionTaskNames(),
},
wantManifest: &catalogerManifest{
Requested: cataloging.SelectionRequest{
@ -149,6 +152,7 @@ func TestCreateSBOMConfig_makeTaskGroups(t *testing.T) {
fileCatalogerNames("file-metadata", "content", "binary-metadata"),
relationshipCatalogerNames(),
unknownsTaskNames(),
osFeatureDetectionTaskNames(),
},
wantManifest: &catalogerManifest{
Requested: cataloging.SelectionRequest{
@ -169,6 +173,7 @@ func TestCreateSBOMConfig_makeTaskGroups(t *testing.T) {
nil, // note: there is a file cataloging group, with no items in it
relationshipCatalogerNames(),
unknownsTaskNames(),
osFeatureDetectionTaskNames(),
},
wantManifest: &catalogerManifest{
Requested: cataloging.SelectionRequest{
@ -192,6 +197,7 @@ func TestCreateSBOMConfig_makeTaskGroups(t *testing.T) {
),
relationshipCatalogerNames(),
unknownsTaskNames(),
osFeatureDetectionTaskNames(),
},
wantManifest: &catalogerManifest{
Requested: cataloging.SelectionRequest{
@ -213,6 +219,7 @@ func TestCreateSBOMConfig_makeTaskGroups(t *testing.T) {
fileCatalogerNames(),
relationshipCatalogerNames(),
unknownsTaskNames(),
osFeatureDetectionTaskNames(),
},
wantManifest: &catalogerManifest{
Requested: cataloging.SelectionRequest{
@ -234,6 +241,7 @@ func TestCreateSBOMConfig_makeTaskGroups(t *testing.T) {
fileCatalogerNames(),
relationshipCatalogerNames(),
unknownsTaskNames(),
osFeatureDetectionTaskNames(),
},
wantManifest: &catalogerManifest{
Requested: cataloging.SelectionRequest{
@ -255,6 +263,7 @@ func TestCreateSBOMConfig_makeTaskGroups(t *testing.T) {
fileCatalogerNames(),
relationshipCatalogerNames(),
unknownsTaskNames(),
osFeatureDetectionTaskNames(),
},
wantManifest: &catalogerManifest{
Requested: cataloging.SelectionRequest{
@ -277,6 +286,7 @@ func TestCreateSBOMConfig_makeTaskGroups(t *testing.T) {
fileCatalogerNames(),
relationshipCatalogerNames(),
unknownsTaskNames(),
osFeatureDetectionTaskNames(),
},
wantManifest: &catalogerManifest{
Requested: cataloging.SelectionRequest{
@ -298,6 +308,7 @@ func TestCreateSBOMConfig_makeTaskGroups(t *testing.T) {
fileCatalogerNames(),
relationshipCatalogerNames(),
unknownsTaskNames(),
osFeatureDetectionTaskNames(),
},
wantManifest: &catalogerManifest{
Requested: cataloging.SelectionRequest{
@ -430,6 +441,10 @@ func unknownsTaskNames() []string {
return []string{"unknowns-labeler"}
}
func osFeatureDetectionTaskNames() []string {
return []string{"os-feature-detection"}
}
func environmentCatalogerNames() []string {
return []string{"environment-cataloger"}
}

View File

@ -480,6 +480,10 @@ func Test_toOsComponent(t *testing.T) {
Version: "myVersion",
},
Properties: &[]cyclonedx.Property{
{
Name: "syft:distro:extendedSupport",
Value: "false",
},
{
Name: "syft:distro:id",
Value: "myLinux",

View File

@ -102,6 +102,10 @@
"version": "1.2.3"
},
"properties": [
{
"name": "syft:distro:extendedSupport",
"value": "false"
},
{
"name": "syft:distro:id",
"value": "debian"

View File

@ -111,6 +111,10 @@
"version": "1.2.3"
},
"properties": [
{
"name": "syft:distro:extendedSupport",
"value": "false"
},
{
"name": "syft:distro:id",
"value": "debian"

View File

@ -53,6 +53,7 @@
<description>debian</description>
<swid tagId="debian" name="debian" version="1.2.3"></swid>
<properties>
<property name="syft:distro:extendedSupport">false</property>
<property name="syft:distro:id">debian</property>
<property name="syft:distro:idLike:0">like!</property>
<property name="syft:distro:prettyName">debian</property>

View File

@ -11,7 +11,7 @@
</component>
</components>
</tools>
<component bom-ref="f28a4ba3ddfdddad" type="container">
<component bom-ref="redacted" type="container">
<name>user-image-input</name>
<version>sha256:redacted</version>
</component>
@ -56,6 +56,7 @@
<description>debian</description>
<swid tagId="debian" name="debian" version="1.2.3"></swid>
<properties>
<property name="syft:distro:extendedSupport">false</property>
<property name="syft:distro:id">debian</property>
<property name="syft:distro:idLike:0">like!</property>
<property name="syft:distro:prettyName">debian</property>

View File

@ -25,6 +25,7 @@ type LinuxRelease struct {
PrivacyPolicyURL string `json:"privacyPolicyURL,omitempty"`
CPEName string `json:"cpeName,omitempty"`
SupportEnd string `json:"supportEnd,omitempty"`
ExtendedSupport bool `json:"extendedSupport,omitempty"`
}
func (s *IDLikes) UnmarshalJSON(data []byte) error {

View File

@ -41,7 +41,7 @@ func ToFormatModel(s sbom.SBOM, cfg EncoderConfig) model.Document {
ArtifactRelationships: toRelationshipModel(s.Relationships),
Files: toFile(s, coordinateSorter),
Source: toSourceModel(s.Source),
Distro: toLinuxReleaser(s.Artifacts.LinuxDistribution),
Distro: toLinuxRelease(s.Artifacts.LinuxDistribution),
Descriptor: toDescriptor(s.Descriptor),
Schema: model.Schema{
Version: internal.JSONSchemaVersion,
@ -50,7 +50,7 @@ func ToFormatModel(s sbom.SBOM, cfg EncoderConfig) model.Document {
}
}
func toLinuxReleaser(d *linux.Release) model.LinuxRelease {
func toLinuxRelease(d *linux.Release) model.LinuxRelease {
if d == nil {
return model.LinuxRelease{}
}
@ -73,6 +73,7 @@ func toLinuxReleaser(d *linux.Release) model.LinuxRelease {
PrivacyPolicyURL: d.PrivacyPolicyURL,
CPEName: d.CPEName,
SupportEnd: d.SupportEnd,
ExtendedSupport: d.ExtendedSupport,
}
}

View File

@ -40,7 +40,7 @@ func toSyftModel(doc model.Document) *sbom.SBOM {
FileLicenses: fileArtifacts.FileLicenses,
Executables: fileArtifacts.Executables,
Unknowns: fileArtifacts.Unknowns,
LinuxDistribution: toSyftLinuxRelease(doc.Distro),
LinuxDistribution: toInternalLinuxRelease(doc.Distro),
},
Source: *toSyftSourceData(doc.Source),
Descriptor: toSyftDescriptor(doc.Descriptor),
@ -195,7 +195,7 @@ func toSyftFileType(ty string) stereoscopeFile.Type {
}
}
func toSyftLinuxRelease(d model.LinuxRelease) *linux.Release {
func toInternalLinuxRelease(d model.LinuxRelease) *linux.Release {
if cmp.Equal(d, model.LinuxRelease{}) {
return nil
}
@ -218,6 +218,7 @@ func toSyftLinuxRelease(d model.LinuxRelease) *linux.Release {
PrivacyPolicyURL: d.PrivacyPolicyURL,
CPEName: d.CPEName,
SupportEnd: d.SupportEnd,
ExtendedSupport: d.ExtendedSupport,
}
}

View File

@ -20,6 +20,7 @@ type Release struct {
PrivacyPolicyURL string
CPEName string // A CPE name for the operating system, in URI binding syntax
SupportEnd string // The date at which support for this version of the OS ends.
ExtendedSupport bool `cyclonedx:"extendedSupport"` // indicates there was some evidence of extended support found
}
func (r *Release) String() string {