Skip to content

node scanning wip#19376

Draft
BradLugo wants to merge 1 commit intomasterfrom
blugo/node-scanning-wip
Draft

node scanning wip#19376
BradLugo wants to merge 1 commit intomasterfrom
blugo/node-scanning-wip

Conversation

@BradLugo
Copy link
Contributor

Description

change me!

User-facing documentation

Testing and quality

  • the change is production ready: the change is GA, or otherwise the functionality is gated by a feature flag
  • CI results are inspected

Automated testing

  • added unit tests
  • added e2e tests
  • added regression tests
  • added compatibility tests
  • modified existing tests

How I validated my change

change me!

@openshift-ci
Copy link

openshift-ci bot commented Mar 11, 2026

Skipping CI for Draft Pull Request.
If you want CI signal for your change, please convert it to an actual PR.
You can still manually trigger a test run with /test all

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • The new rhcosOSImageRegexp is much stricter than the previous version and assumes a very specific 4.<minor>[.<suffix>] layout; consider adding tests for older/variant osImage formats (e.g. including additional patch segments or different separators) or relaxing the regex to avoid breaking RHCOS detection on existing clusters.
  • extractRHELMajorVersion currently defaults to "9" on parse failure, which could silently misclassify RHEL CPEs; it may be safer to return an empty value and handle the fallback explicitly where it is used, or log and skip the RHEL CPE path instead of assuming 9.
  • The new ocpVersion parameter passed into attachRPMtoRHCOS and buildRHCOSIndexReport is not used for any logic (only logged and then ignored); if there is no planned use, you could simplify the signatures back down to reduce cognitive load and avoid confusion.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new rhcosOSImageRegexp is much stricter than the previous version and assumes a very specific `4.<minor>[.<suffix>]` layout; consider adding tests for older/variant osImage formats (e.g. including additional patch segments or different separators) or relaxing the regex to avoid breaking RHCOS detection on existing clusters.
- extractRHELMajorVersion currently defaults to "9" on parse failure, which could silently misclassify RHEL CPEs; it may be safer to return an empty value and handle the fallback explicitly where it is used, or log and skip the RHEL CPE path instead of assuming 9.
- The new ocpVersion parameter passed into attachRPMtoRHCOS and buildRHCOSIndexReport is not used for any logic (only logged and then ignored); if there is no planned use, you could simplify the signatures back down to reduce cognitive load and avoid confusion.

## Individual Comments

### Comment 1
<location path="sensor/common/compliance/node_inventory_handler_impl.go" line_range="491-494" />
<code_context>
+// isNewRHCOSVersionSchema returns true if the RHCOS version follows the RHEL-style
+// (9.x, 10.x) rather than the OCP-style (4xx.x).
+// RHCOS 4.19+ reports versions like "9.6.20260217-1" instead of "418.94.202501011408".
+func isNewRHCOSVersionSchema(version string) bool {
+	if len(version) == 0 {
+		return false
+	}
+	parts := strings.SplitN(version, ".", 2)
+	if len(parts) < 2 {
+		return false
+	}
+	major, err := strconv.Atoi(parts[0])
+	if err != nil {
+		return false
+	}
+	// RHEL versions are 8.x, 9.x, 10.x (single/double digit major)
+	// OCP-derived versions are 412.x, 413.x, 418.x (triple digit major)
+	return major < 100
</code_context>
<issue_to_address>
**suggestion:** Align `isNewRHCOSVersionSchema` comment with the actual heuristic

The logic currently classifies any version with `major < 100` as using the new schema, but the comment frames this specifically as RHEL (8/9/10) vs OCP 4xx. That’s a broader heuristic than the comment implies. Please either (a) tighten the check to the expected ranges (e.g., known RHEL/OCP majors) or (b) update the comment to clearly document the actual behavior ("major < 100" = RHEL-style, `>= 100` = OCP-style).

```suggestion
 // isNewRHCOSVersionSchema returns true if the RHCOS version is interpreted as
 // "RHEL-style" (major < 100, e.g. 8.x, 9.x, 10.x) rather than "OCP-style"
 // (major >= 100, e.g. 412.x, 418.x).
 // RHCOS 4.19+ reports versions like "9.6.20260217-1" instead of "418.94.202501011408".
func isNewRHCOSVersionSchema(version string) bool {
```
</issue_to_address>

### Comment 2
<location path="sensor/common/compliance/node_inventory_handler_impl.go" line_range="475" />
<code_context>
 	return []int32{v[0], v[1], 0, 0, 0, 0, 0, 0, 0, 0}
 }

-func noop(_, _ string, rpm *v4.IndexReport) *v4.IndexReport {
+// extractRHELMajorVersion extracts the major version from a RHEL-style version string.
+// For example, "9.6.20260217-1" returns "9".
</code_context>
<issue_to_address>
**issue (complexity):** Consider simplifying the new RHCOS handling by narrowing the wrapper function signatures and centralizing version and repository selection logic into focused helpers.

You can preserve the new behavior while significantly reducing complexity by tightening the function signatures and consolidating the version/repo logic.

### 1. Drop `ocpVersion` from the wrapper function type

`ocpVersion` is only used for logging in `sendNodeIndex` and is not consulted in `buildRHCOSIndexReport`. Passing it through multiple layers just to satisfy a function type increases cognitive load.

You can keep `GetRHCOSVersion` as-is (it may be useful elsewhere) but narrow the wrapper function to exactly what’s needed:

```go
// Narrowed wrapper type: only pass what is actually needed downstream.
type indexWrapperFunc func(version, arch string, rpm *v4.IndexReport) *v4.IndexReport

func noop(_ , _ string, rpm *v4.IndexReport) *v4.IndexReport {
	return rpm
}

func (c *nodeInventoryHandlerImpl) sendNodeIndex(toC chan<- *message.ExpiringMessage, indexWrap *index.IndexReportWrap) {
	// ...
	isRHCOS, ocpVersion, version, err := c.nodeRHCOSMatcher.GetRHCOSVersion(indexWrap.NodeName)
	// keep ocpVersion for logging only
	log.Debugf("Node=%q discovered RHCOS=%t ocp-version=%q rhcos-version=%q", indexWrap.NodeName, isRHCOS, ocpVersion, version)

	irWrapperFunc := indexWrapperFunc(noop)
	arch := c.archCache[indexWrap.NodeName]
	if isRHCOS {
		// ...
		irWrapperFunc = attachRPMtoRHCOS
	}

	toC <- message.New(&central.MsgFromSensor{
		Msg: &central.MsgFromSensor_Event{
			Event: &central.SensorEvent{
				// ...
				Resource: &central.SensorEvent_IndexReport{
					IndexReport: irWrapperFunc(version, arch, indexWrap.IndexReport),
				},
			},
		},
	})
}
```

And adjust `attachRPMtoRHCOS` accordingly:

```go
func attachRPMtoRHCOS(version, arch string, rpm *v4.IndexReport) *v4.IndexReport {
	idCandidate := 600
	// ... unchanged ...
	strID := strconv.Itoa(idCandidate)
	oci := buildRHCOSIndexReport(strID, version, arch)
	// ... unchanged ...
	return oci
}
```

This removes unused parameters from `noop` and `attachRPMtoRHCOS` while keeping the same behavior (ocpVersion is still logged).

### 2. Collapse the version-schema helpers into one utility

`isNewRHCOSVersionSchema` and `extractRHELMajorVersion` both parse the same `version` string. You can do this once and return both the scheme and the major:

```go
type rhcosVersionScheme struct {
	RHELMajor string
	IsRHEL    bool
}

// parseRHCOSVersionSchema parses the version once and indicates whether it is RHEL-style.
func parseRHCOSVersionSchema(version string) rhcosVersionScheme {
	parts := strings.SplitN(version, ".", 2)
	if len(parts) < 2 {
		return rhcosVersionScheme{RHELMajor: "9", IsRHEL: false}
	}

	majorStr := parts[0]
	majorInt, err := strconv.Atoi(majorStr)
	if err != nil {
		return rhcosVersionScheme{RHELMajor: "9", IsRHEL: false}
	}

	// RHEL: 8.x, 9.x, 10.x; OCP-style: 412.x, 418.x, ...
	if majorInt < 100 {
		return rhcosVersionScheme{RHELMajor: majorStr, IsRHEL: true}
	}

	return rhcosVersionScheme{RHELMajor: "9", IsRHEL: false}
}
```

Then `buildRHCOSIndexReport` only calls this once.

### 3. Extract a repo config helper to remove duplication in `buildRHCOSIndexReport`

Right now `buildRHCOSIndexReport` sets `repoKey`, `repoName`, `repoURI`, `repoCPE` with a default and overrides them conditionally, then uses the same values multiple times. A small struct plus a helper makes the function shorter and clearer:

```go
type rhcosRepoConfig struct {
	Name string
	Key  string
	URI  string
	CPE  string
}

func determineRHCOSRepo(version string) rhcosRepoConfig {
	scheme := parseRHCOSVersionSchema(version)

	// Default: golden image repo
	cfg := rhcosRepoConfig{
		Name: goldenName,
		Key:  goldenKey,
		URI:  goldenURI,
		CPE:  "cpe:2.3:*",
	}

	if scheme.IsRHEL {
		// RHCOS 4.19+ RHEL-style versions
		rhelMajor := scheme.RHELMajor
		cfg.Key = rhelCPEKey
		cfg.Name = fmt.Sprintf("cpe:/o:redhat:enterprise_linux:%s::baseos", rhelMajor)
		cfg.URI = ""
		cfg.CPE = fmt.Sprintf("cpe:2.3:o:redhat:enterprise_linux:%s:*:baseos:*:*:*:*:*", rhelMajor)
		log.Debugf("RHCOS 4.19+ detected (version=%s): using RHEL %s CPE repository for vulnerability matching", version, rhelMajor)
	}

	return cfg
}
```

Then `buildRHCOSIndexReport` becomes:

```go
func buildRHCOSIndexReport(id, version, arch string) *v4.IndexReport {
	repo := determineRHCOSRepo(version)

	return &v4.IndexReport{
		// ...
		Contents: &v4.Contents{
			Packages: map[string]*v4.Package{
				id: {
					// ...
					Source: &v4.Package{
						Id:      id,
						Name:    "rhcos",
						Kind:    "source",
						Version: version,
						Cpe:     repo.CPE,
					},
					Arch: arch,
					Cpe:  repo.CPE,
				},
			},
			PackagesDEPRECATED: []*v4.Package{
				{
					// same fields, use repo.CPE
				},
			},
			Repositories: map[string]*v4.Repository{
				id: {
					Id:   id,
					Name: repo.Name,
					Key:  repo.Key,
					Uri:  repo.URI,
					Cpe:  repo.CPE,
				},
			},
			RepositoriesDEPRECATED: []*v4.Repository{
				{
					Id:   id,
					Name: repo.Name,
					Key:  repo.Key,
					Uri:  repo.URI,
					Cpe:  repo.CPE,
				},
			},
			// ...
		},
	}
}
```

This keeps all the new behavior (including the RHEL CPE selection and logging) but:

- Removes unused parameters from `noop` and `attachRPMtoRHCOS`.
- Centralizes version parsing into one helper.
- Encapsulates repo selection and removes duplicated assignment.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +491 to +494
// isNewRHCOSVersionSchema returns true if the RHCOS version follows the RHEL-style
// (9.x, 10.x) rather than the OCP-style (4xx.x).
// RHCOS 4.19+ reports versions like "9.6.20260217-1" instead of "418.94.202501011408".
func isNewRHCOSVersionSchema(version string) bool {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Align isNewRHCOSVersionSchema comment with the actual heuristic

The logic currently classifies any version with major < 100 as using the new schema, but the comment frames this specifically as RHEL (8/9/10) vs OCP 4xx. That’s a broader heuristic than the comment implies. Please either (a) tighten the check to the expected ranges (e.g., known RHEL/OCP majors) or (b) update the comment to clearly document the actual behavior ("major < 100" = RHEL-style, >= 100 = OCP-style).

Suggested change
// isNewRHCOSVersionSchema returns true if the RHCOS version follows the RHEL-style
// (9.x, 10.x) rather than the OCP-style (4xx.x).
// RHCOS 4.19+ reports versions like "9.6.20260217-1" instead of "418.94.202501011408".
func isNewRHCOSVersionSchema(version string) bool {
// isNewRHCOSVersionSchema returns true if the RHCOS version is interpreted as
// "RHEL-style" (major < 100, e.g. 8.x, 9.x, 10.x) rather than "OCP-style"
// (major >= 100, e.g. 412.x, 418.x).
// RHCOS 4.19+ reports versions like "9.6.20260217-1" instead of "418.94.202501011408".
func isNewRHCOSVersionSchema(version string) bool {

return []int32{v[0], v[1], 0, 0, 0, 0, 0, 0, 0, 0}
}

func noop(_, _ string, rpm *v4.IndexReport) *v4.IndexReport {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (complexity): Consider simplifying the new RHCOS handling by narrowing the wrapper function signatures and centralizing version and repository selection logic into focused helpers.

You can preserve the new behavior while significantly reducing complexity by tightening the function signatures and consolidating the version/repo logic.

1. Drop ocpVersion from the wrapper function type

ocpVersion is only used for logging in sendNodeIndex and is not consulted in buildRHCOSIndexReport. Passing it through multiple layers just to satisfy a function type increases cognitive load.

You can keep GetRHCOSVersion as-is (it may be useful elsewhere) but narrow the wrapper function to exactly what’s needed:

// Narrowed wrapper type: only pass what is actually needed downstream.
type indexWrapperFunc func(version, arch string, rpm *v4.IndexReport) *v4.IndexReport

func noop(_ , _ string, rpm *v4.IndexReport) *v4.IndexReport {
	return rpm
}

func (c *nodeInventoryHandlerImpl) sendNodeIndex(toC chan<- *message.ExpiringMessage, indexWrap *index.IndexReportWrap) {
	// ...
	isRHCOS, ocpVersion, version, err := c.nodeRHCOSMatcher.GetRHCOSVersion(indexWrap.NodeName)
	// keep ocpVersion for logging only
	log.Debugf("Node=%q discovered RHCOS=%t ocp-version=%q rhcos-version=%q", indexWrap.NodeName, isRHCOS, ocpVersion, version)

	irWrapperFunc := indexWrapperFunc(noop)
	arch := c.archCache[indexWrap.NodeName]
	if isRHCOS {
		// ...
		irWrapperFunc = attachRPMtoRHCOS
	}

	toC <- message.New(&central.MsgFromSensor{
		Msg: &central.MsgFromSensor_Event{
			Event: &central.SensorEvent{
				// ...
				Resource: &central.SensorEvent_IndexReport{
					IndexReport: irWrapperFunc(version, arch, indexWrap.IndexReport),
				},
			},
		},
	})
}

And adjust attachRPMtoRHCOS accordingly:

func attachRPMtoRHCOS(version, arch string, rpm *v4.IndexReport) *v4.IndexReport {
	idCandidate := 600
	// ... unchanged ...
	strID := strconv.Itoa(idCandidate)
	oci := buildRHCOSIndexReport(strID, version, arch)
	// ... unchanged ...
	return oci
}

This removes unused parameters from noop and attachRPMtoRHCOS while keeping the same behavior (ocpVersion is still logged).

2. Collapse the version-schema helpers into one utility

isNewRHCOSVersionSchema and extractRHELMajorVersion both parse the same version string. You can do this once and return both the scheme and the major:

type rhcosVersionScheme struct {
	RHELMajor string
	IsRHEL    bool
}

// parseRHCOSVersionSchema parses the version once and indicates whether it is RHEL-style.
func parseRHCOSVersionSchema(version string) rhcosVersionScheme {
	parts := strings.SplitN(version, ".", 2)
	if len(parts) < 2 {
		return rhcosVersionScheme{RHELMajor: "9", IsRHEL: false}
	}

	majorStr := parts[0]
	majorInt, err := strconv.Atoi(majorStr)
	if err != nil {
		return rhcosVersionScheme{RHELMajor: "9", IsRHEL: false}
	}

	// RHEL: 8.x, 9.x, 10.x; OCP-style: 412.x, 418.x, ...
	if majorInt < 100 {
		return rhcosVersionScheme{RHELMajor: majorStr, IsRHEL: true}
	}

	return rhcosVersionScheme{RHELMajor: "9", IsRHEL: false}
}

Then buildRHCOSIndexReport only calls this once.

3. Extract a repo config helper to remove duplication in buildRHCOSIndexReport

Right now buildRHCOSIndexReport sets repoKey, repoName, repoURI, repoCPE with a default and overrides them conditionally, then uses the same values multiple times. A small struct plus a helper makes the function shorter and clearer:

type rhcosRepoConfig struct {
	Name string
	Key  string
	URI  string
	CPE  string
}

func determineRHCOSRepo(version string) rhcosRepoConfig {
	scheme := parseRHCOSVersionSchema(version)

	// Default: golden image repo
	cfg := rhcosRepoConfig{
		Name: goldenName,
		Key:  goldenKey,
		URI:  goldenURI,
		CPE:  "cpe:2.3:*",
	}

	if scheme.IsRHEL {
		// RHCOS 4.19+ RHEL-style versions
		rhelMajor := scheme.RHELMajor
		cfg.Key = rhelCPEKey
		cfg.Name = fmt.Sprintf("cpe:/o:redhat:enterprise_linux:%s::baseos", rhelMajor)
		cfg.URI = ""
		cfg.CPE = fmt.Sprintf("cpe:2.3:o:redhat:enterprise_linux:%s:*:baseos:*:*:*:*:*", rhelMajor)
		log.Debugf("RHCOS 4.19+ detected (version=%s): using RHEL %s CPE repository for vulnerability matching", version, rhelMajor)
	}

	return cfg
}

Then buildRHCOSIndexReport becomes:

func buildRHCOSIndexReport(id, version, arch string) *v4.IndexReport {
	repo := determineRHCOSRepo(version)

	return &v4.IndexReport{
		// ...
		Contents: &v4.Contents{
			Packages: map[string]*v4.Package{
				id: {
					// ...
					Source: &v4.Package{
						Id:      id,
						Name:    "rhcos",
						Kind:    "source",
						Version: version,
						Cpe:     repo.CPE,
					},
					Arch: arch,
					Cpe:  repo.CPE,
				},
			},
			PackagesDEPRECATED: []*v4.Package{
				{
					// same fields, use repo.CPE
				},
			},
			Repositories: map[string]*v4.Repository{
				id: {
					Id:   id,
					Name: repo.Name,
					Key:  repo.Key,
					Uri:  repo.URI,
					Cpe:  repo.CPE,
				},
			},
			RepositoriesDEPRECATED: []*v4.Repository{
				{
					Id:   id,
					Name: repo.Name,
					Key:  repo.Key,
					Uri:  repo.URI,
					Cpe:  repo.CPE,
				},
			},
			// ...
		},
	}
}

This keeps all the new behavior (including the RHEL CPE selection and logging) but:

  • Removes unused parameters from noop and attachRPMtoRHCOS.
  • Centralizes version parsing into one helper.
  • Encapsulates repo selection and removes duplicated assignment.

@codecov
Copy link

codecov bot commented Mar 11, 2026

Codecov Report

❌ Patch coverage is 62.82051% with 29 lines in your changes missing coverage. Please review.
✅ Project coverage is 49.64%. Comparing base (8bde9d9) to head (9d995a5).
⚠️ Report is 6 commits behind head on master.

Files with missing lines Patch % Lines
sensor/common/compliance/node_matcher.go 0.00% 23 Missing ⚠️
...r/common/compliance/node_inventory_handler_impl.go 89.09% 6 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##           master   #19376   +/-   ##
=======================================
  Coverage   49.64%   49.64%           
=======================================
  Files        2698     2698           
  Lines      203066   203117   +51     
=======================================
+ Hits       100809   100845   +36     
- Misses      94734    94750   +16     
+ Partials     7523     7522    -1     
Flag Coverage Δ
go-unit-tests 49.64% <62.82%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@rhacs-bot
Copy link
Contributor

Images are ready for the commit at 9d995a5.

To use with deploy scripts, first export MAIN_IMAGE_TAG=4.11.x-288-g9d995a5427.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants