Repository: redhat-openshift-ecosystem/openshift-preflight
Pull request #1106 (2023-12-11 22:40:37)

Bump actions/setup-go from 4 to 5
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 4 to 5.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/actions/setup-go/releases">actions/setup-go's releases</a>.</em></p>
<blockquote>
<h2>v5.0.0</h2>
<h2>What's Changed</h2>
<p>In scope of this release, we change Nodejs runtime from node16 to node20 (<a href="https://redirect.github.com/actions/setup-go/pull/421">actions/setup-go#421</a>). Moreover, we update some dependencies to the latest versions (<a href="https://redirect.github.com/actions/setup-go/pull/445">actions/setup-go#445</a>).</p>
<p>Besides, this release contains such changes as:</p>
<ul>
<li>Fix hosted tool cache usage on windows by <a href="https://github.com/galargh"><code>@​galargh</code></a> in <a href="https://redirect.github.com/actions/setup-go/pull/411">actions/setup-go#411</a></li>
<li>Improve documentation regarding dependencies caching by <a href="https://github.com/artemgavrilov"><code>@​artemgavrilov</code></a> in <a href="https://redirect.github.com/actions/setup-go/pull/417">actions/setup-go#417</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/galargh"><code>@​galargh</code></a> made their first contribution in <a href="https://redirect.github.com/actions/setup-go/pull/411">actions/setup-go#411</a></li>
<li><a href="https://github.com/artemgavrilov"><code>@​artemgavrilov</code></a> made their first contribution in <a href="https://redirect.github.com/actions/setup-go/pull/417">actions/setup-go#417</a></li>
<li><a href="https://github.com/chenrui333"><code>@​chenrui333</code></a> made their first contribution in <a href="https://redirect.github.com/actions/setup-go/pull/421">actions/setup-go#421</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a href="https://github.com/actions/setup-go/compare/v4...v5.0.0">https://github.com/actions/setup-go/compare/v4...v5.0.0</a></p>
<h2>v4.1.0</h2>
<h2>What's Changed</h2>
<p>In scope of this release, slow installation on Windows was fixed by <a href="https://github.com/dsame"><code>@​dsame</code></a> in <a href="https://redirect.github.com/actions/setup-go/pull/393">actions/setup-go#393</a> and OS version was added to <code>primaryKey</code> for Ubuntu runners to avoid conflicts (<a href="https://redirect.github.com/actions/setup-go/pull/383">actions/setup-go#383</a>)</p>
<p>This release also includes the following changes:</p>
<ul>
<li>Remove implicit dependencies by <a href="https://github.com/nikolai-laevskii"><code>@​nikolai-laevskii</code></a> in <a href="https://redirect.github.com/actions/setup-go/pull/378">actions/setup-go#378</a></li>
<li>Update action.yml by <a href="https://github.com/mkelly"><code>@​mkelly</code></a> in <a href="https://redirect.github.com/actions/setup-go/pull/379">actions/setup-go#379</a></li>
<li>Added a description that go-version should be specified as a string type by <a href="https://github.com/n3xem"><code>@​n3xem</code></a> in <a href="https://redirect.github.com/actions/setup-go/pull/367">actions/setup-go#367</a></li>
<li>Add note about YAML parsing versions by <a href="https://github.com/dmitry-shibanov"><code>@​dmitry-shibanov</code></a> in <a href="https://redirect.github.com/actions/setup-go/pull/382">actions/setup-go#382</a></li>
<li>Automatic update of configuration files from 05/23/2023 by <a href="https://github.com/github-actions"><code>@​github-actions</code></a> in <a href="https://redirect.github.com/actions/setup-go/pull/377">actions/setup-go#377</a></li>
<li>Bump tough-cookie and <code>@​azure/ms-rest-js</code> by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/actions/setup-go/pull/392">actions/setup-go#392</a></li>
<li>Bump word-wrap from 1.2.3 to 1.2.4 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/actions/setup-go/pull/397">actions/setup-go#397</a></li>
<li>Bump semver from 6.3.0 to 6.3.1 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/actions/setup-go/pull/396">actions/setup-go#396</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/mkelly"><code>@​mkelly</code></a> made their first contribution in <a href="https://redirect.github.com/actions/setup-go/pull/379">actions/setup-go#379</a></li>
<li><a href="https://github.com/n3xem"><code>@​n3xem</code></a> made their first contribution in <a href="https://redirect.github.com/actions/setup-go/pull/367">actions/setup-go#367</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a href="https://github.com/actions/setup-go/compare/v4...v4.1.0">https://github.com/actions/setup-go/compare/v4...v4.1.0</a></p>
<h2>v4.0.1</h2>
<h2>What's Changed</h2>
<ul>
<li>Update documentation for <code>v4</code> by <a href="https://github.com/dsame"><code>@​dsame</code></a> in <a href="https://redirect.github.com/actions/setup-go/pull/354">actions/setup-go#354</a></li>
<li>Fix glob bug in the package.json scripts section by <a href="https://github.com/IvanZosimov"><code>@​IvanZosimov</code></a> in <a href="https://redirect.github.com/actions/setup-go/pull/359">actions/setup-go#359</a></li>
<li>Bump <code>xml2js</code> dependency by <a href="https://github.com/dmitry-shibanov"><code>@​dmitry-shibanov</code></a> in <a href="https://redirect.github.com/actions/setup-go/pull/370">actions/setup-go#370</a></li>
<li>Bump <code>@actions/cache</code> dependency to v3.2.1 by <a href="https://github.com/nikolai-laevskii"><code>@​nikolai-laevskii</code></a> in <a href="https://redirect.github.com/actions/setup-go/pull/374">actions/setup-go#374</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/nikolai-laevskii"><code>@​nikolai-laevskii</code></a> made their first contribution in <a href="https://redirect.github.com/actions/setup-go/pull/374">actions/setup-go#374</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a href="https://github.com/actions/setup-go/compare/v4...v4.0.1">https://github.com/actions/setup-go/compare/v4...v4.0.1</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/actions/setup-go/commit/0c52d547c9bc32b1aa3301fd7a9cb496313a4491"><code>0c52d54</code></a> Update dependencies for node20 (<a href="https://redirect.github.com/actions/setup-go/issues/445">#445</a>)</li>
<li><a href="https://github.com/actions/setup-go/commit/bfd2fb341f32be7281829126376a12a780ca79fc"><code>bfd2fb3</code></a> Merge pull request <a href="https://redirect.github.com/actions/setup-go/issues/421">#421</a> from chenrui333/node20-runtime</li>
<li><a href="https://github.com/actions/setup-go/commit/3d65fa57fcbfe4a359b6b71a6c65e6eec12984eb"><code>3d65fa5</code></a> feat: bump to use actions/checkout@v4</li>
<li><a href="https://github.com/actions/setup-go/commit/8a505c9cf2e2726eda7f3268d6992e386a12da52"><code>8a505c9</code></a> feat: bump to use node20 runtime</li>
<li><a href="https://github.com/actions/setup-go/commit/883490dfd06f396ebe0b738bc313a53cf9d851e5"><code>883490d</code></a> Merge pull request <a href="https://redirect.github.com/actions/setup-go/issues/417">#417</a> from artemgavrilov/main</li>
<li><a href="https://github.com/actions/setup-go/commit/d45ebba0ce181dc5604aaf69ce5a0bdcbd3b1807"><code>d45ebba</code></a> Rephrase sentence</li>
<li><a href="https://github.com/actions/setup-go/commit/317c6617fa9e4e67f1e5e20ad8bc98bf298a0f8f"><code>317c661</code></a> Replace <code>wildcards</code> term with <code>globs</code>.</li>
<li><a href="https://github.com/actions/setup-go/commit/f90673ad641a19d0689fba58b5c79adc54be5d81"><code>f90673a</code></a> Merge pull request <a href="https://redirect.github.com/actions/setup-go/issues/1">#1</a> from artemgavrilov/caching-docs-improvement</li>
<li><a href="https://github.com/actions/setup-go/commit/801823434715e45aa48743a38182d33b33675d02"><code>8018234</code></a> Improve documentation regarding dependencies cachin</li>
<li><a href="https://github.com/actions/setup-go/commit/d085b4fe57b6e17262cbebc67b4d2d341d8938c2"><code>d085b4f</code></a> Merge pull request <a href="https://redirect.github.com/actions/setup-go/issues/411">#411</a> from galargh/fix/windows-hostedtoolcache</li>
<li>Additional commits viewable in <a href="https://github.com/actions/setup-go/compare/v4...v5">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/setup-go&package-manager=github_actions&previous-version=4&new-version=5)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>

openshift-ci[bot]: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by: *<a href="https://github.com/redhat-openshift-ecosystem/openshift-preflight/pull/1106#" title="Author self-approved">dependabot[bot]</a>*
**Once this PR has been reviewed and has the lgtm label**, please assign [komish](https://github.com/komish) for approval. For more information see [the Kubernetes Code Review Process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process).

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=redhat-openshift-ecosystem%2Fopenshift-preflight).

<details open>
Needs approval from an approver in each of these files:

- **[OWNERS](https://github.com/redhat-openshift-ecosystem/openshift-preflight/blob/main/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["komish"]} -->
openshift-ci[bot]: Hi @dependabot[bot]. Thanks for your PR.

I'm waiting for a [redhat-openshift-ecosystem](https://github.com/orgs/redhat-openshift-ecosystem/people) member to verify that this patch is reasonable to test. If it is, they should reply with `/ok-to-test` on its own line. Until that is done, I will not automatically test new commits in this PR, but the usual testing commands by org members will still work. Regular contributors should [join the org](https://github.com/orgs/redhat-openshift-ecosystem/people) to skip this step.

Once the patch is verified, the new status will be reflected by the `ok-to-test` label.

I understand the commands that are listed [here](https://go.k8s.io/bot-commands?repo=redhat-openshift-ecosystem%2Fopenshift-preflight).

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository.
</details>

dcibot: - SUCCESS https://www.distributed-ci.io/jobs/f4cf5f90-350e-4c8c-8548-d023a8561161/jobStates
 from https://github.com/redhat-openshift-ecosystem/openshift-preflight/pull/1106
openshift-ci[bot]: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/redhat-openshift-ecosystem/openshift-preflight/pull/1106#pullrequestreview-1777565934" title="Approved">acornett21</a>*, *<a href="https://github.com/redhat-openshift-ecosystem/openshift-preflight/pull/1106#" title="Author self-approved">dependabot[bot]</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=redhat-openshift-ecosystem%2Fopenshift-preflight).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/redhat-openshift-ecosystem/openshift-preflight/blob/main/OWNERS)~~ [acornett21]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->

Patch:
diff --git a/.github/workflows/build-main.yml b/.github/workflows/build-main.yml
index 950f9eb..5d8da80 100644
--- a/.github/workflows/build-main.yml
+++ b/.github/workflows/build-main.yml
@@ -68,7 +68,7 @@ jobs:
       run: 'sudo apt update && sudo apt install -y libgpgme-dev libbtrfs-dev libdevmapper-dev'
 
     - name: Set up Go
-      uses: actions/setup-go@v4
+      uses: actions/setup-go@v5
       with:
         go-version-file: go.mod
 
diff --git a/.github/workflows/build-multiarch.yml b/.github/workflows/build-multiarch.yml
index 8fb3f13..27683ba 100644
--- a/.github/workflows/build-multiarch.yml
+++ b/.github/workflows/build-multiarch.yml
@@ -49,7 +49,7 @@ jobs:
     steps:
       - name: Install cosign
         if: ${{ inputs.sign == true && github.event.release && github.event.action == 'published' }}
-        uses: sigstore/cosign-installer@9c520b997e685cb69e1ac226f7dce8114e8f36df #v2.6.0
+        uses: sigstore/cosign-installer@b18d21aaa20ef40c4e681202a598b7591d5be577 #v2.6.0
         with:
           cosign-release: 'v2.0.0'
 
diff --git a/.github/workflows/copy-to-rhisv.yml b/.github/workflows/copy-to-rhisv.yml
index 5c5727c..1e4904b 100644
--- a/.github/workflows/copy-to-rhisv.yml
+++ b/.github/workflows/copy-to-rhisv.yml
@@ -28,7 +28,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
     - name: Set up Go
-      uses: actions/setup-go@v4
+      uses: actions/setup-go@v5
       with:
         go-version: 1.19
 
diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml
index b9b665c..490ecf3 100644
--- a/.github/workflows/go.yml
+++ b/.github/workflows/go.yml
@@ -16,7 +16,7 @@ jobs:
       run: 'sudo apt update && sudo apt install -y libgpgme-dev libbtrfs-dev libdevmapper-dev'
 
     - name: Set up Go
-      uses: actions/setup-go@v4
+      uses: actions/setup-go@v5
       with:
         go-version-file: go.mod
 
diff --git a/certification/results.go b/certification/results.go
index 19f0a34..e931c6d 100644
--- a/certification/results.go
+++ b/certification/results.go
@@ -22,4 +22,5 @@ type Results struct {
 	Passed            []Result
 	Failed            []Result
 	Errors            []Result
+	Warned            []Result
 }
diff --git a/internal/check/certification.go b/internal/check/certification.go
index 9303d86..9391e5d 100644
--- a/internal/check/certification.go
+++ b/internal/check/certification.go
@@ -6,6 +6,13 @@ import (
 	"github.com/redhat-openshift-ecosystem/openshift-preflight/internal/image"
 )
 
+// Indicates the possible levels that can be utilized in the implementation of a check.
+const (
+	LevelBest     = "best"
+	LevelOptional = "optional"
+	LevelWarn     = "warn"
+)
+
 // Check as an interface containing all methods necessary
 // to use and identify a given check.
 type Check interface {
diff --git a/internal/csv/csv.go b/internal/csv/csv.go
index 190ac9a..fbc27b7 100644
--- a/internal/csv/csv.go
+++ b/internal/csv/csv.go
@@ -10,9 +10,21 @@ import (
 	corev1 "k8s.io/api/core/v1"
 )
 
-const InfrastructureFeaturesAnnotation = "operators.openshift.io/infrastructure-features"
+const (
+	InfrastructureFeaturesAnnotation = "operators.openshift.io/infrastructure-features"
+	DisconnectedAnnotation           = "features.operators.openshift.io/disconnected"
+	FIPSCompliantAnnotation          = "features.operators.openshift.io/fips-compliant"
+	ProxyAwareAnnotation             = "features.operators.openshift.io/proxy-aware"
+	TLSProfilesAnnotation            = "features.operators.openshift.io/tls-profiles"
+	TokenAuthAWSAnnotation           = "features.operators.openshift.io/token-auth-aws"
+	TokenAuthAzureAnnotation         = "features.operators.openshift.io/token-auth-azure"
+	TokenAuthGCPAnnotation           = "features.operators.openshift.io/token-auth-gcp"
+	CNFAnnotation                    = "features.operators.openshift.io/cnf"
+	CNIAnnotation                    = "features.operators.openshift.io/cni"
+	CSIAnnotation                    = "features.operators.openshift.io/csi"
+)
 
-// SupportsDisconnected accepts a stringified list of supported features
+// SupportsDisconnectedViaInfrastructureFeatures accepts a stringified list of supported features
 // and returns true if "disconnected" is listed as a supported feature.
 //
 // E.g. '["disconnected"]'.
@@ -20,7 +32,9 @@ const InfrastructureFeaturesAnnotation = "operators.openshift.io/infrastructure-
 // This is case insensitive, as each infrastructure
 // is normalized before checking. A failure to unmarshal this structure
 // returns false.
-func SupportsDisconnected(infrastructureFeatures string) bool {
+//
+// This is a legacy annotation and will be superceded by the DisconnectedAnnotation in the future.
+func SupportsDisconnectedViaInfrastructureFeatures(infrastructureFeatures string) bool {
 	var features []string
 
 	err := json.Unmarshal([]byte(infrastructureFeatures), &features)
@@ -37,13 +51,28 @@ func SupportsDisconnected(infrastructureFeatures string) bool {
 	return false
 }
 
+// SupportsDisconnected accepts the value of the DisconnectedAnnotation and returns whether it is
+// set to the exact value of "true", which is the expected value indicating support. For this annotation,
+// an explicit value of "false" is treated the same as any other non-true value.
+func SupportsDisconnected(annotationValue string) bool {
+	return annotationValue == "true"
+}
+
 // HasInfrastructureFeaturesAnnotation returns true if the infrastructure-features annotation
-// exists in the .metadata.annotations block of csv.
+// exists in the .metadata.annotations block of csv. This is the legacy annotation
+// superceded by the DisconnectedAnnotation
 func HasInfrastructureFeaturesAnnotation(csv *operatorsv1alpha1.ClusterServiceVersion) bool {
 	_, ok := csv.GetAnnotations()[InfrastructureFeaturesAnnotation]
 	return ok
 }
 
+// HasDisconnectedAnnotation returns true if the disconnected annotation exists in the
+// .metadata.annotations block of csv.
+func HasDisconnectedAnnotation(csv *operatorsv1alpha1.ClusterServiceVersion) bool {
+	_, ok := csv.GetAnnotations()[DisconnectedAnnotation]
+	return ok
+}
+
 // HasRelatedImages returns true if the length of the .spec.relatedImages section of
 // csv is greater than 0.
 func HasRelatedImages(csv *operatorsv1alpha1.ClusterServiceVersion) bool {
diff --git a/internal/engine/engine.go b/internal/engine/engine.go
index 4398502..3b4cef9 100644
--- a/internal/engine/engine.go
+++ b/internal/engine/engine.go
@@ -216,33 +216,39 @@ func (c *craneEngine) ExecuteChecks(ctx context.Context) error {
 
 	// execute checks
 	logger.V(log.DBG).Info("executing checks")
-	for _, check := range c.checks {
+	for _, executedCheck := range c.checks {
 		c.results.TestedImage = c.image
 
-		logger.V(log.DBG).Info("running check", "check", check.Name())
-		if check.Metadata().Level == "optional" {
-			logger.Info(fmt.Sprintf("Check %s is not currently being enforced.", check.Name()))
+		logger.V(log.DBG).Info("running check", "check", executedCheck.Name())
+		if executedCheck.Metadata().Level == check.LevelOptional || executedCheck.Metadata().Level == check.LevelWarn {
+			logger.Info(fmt.Sprintf("Check %s is not currently being enforced.", executedCheck.Name()))
 		}
 
 		// run the validation
 		checkStartTime := time.Now()
-		checkPassed, err := check.Validate(ctx, c.imageRef)
+		checkPassed, err := executedCheck.Validate(ctx, c.imageRef)
 		checkElapsedTime := time.Since(checkStartTime)
 
 		if err != nil {
-			logger.WithValues("result", "ERROR", "err", err.Error()).Info("check completed", "check", check.Name())
-			c.results.Errors = appendUnlessOptional(c.results.Errors, certification.Result{Check: check, ElapsedTime: checkElapsedTime})
+			logger.WithValues("result", "ERROR", "err", err.Error()).Info("check completed", "check", executedCheck.Name())
+			c.results.Errors = appendUnlessOptional(c.results.Errors, certification.Result{Check: executedCheck, ElapsedTime: checkElapsedTime})
 			continue
 		}
 
 		if !checkPassed {
-			logger.WithValues("result", "FAILED").Info("check completed", "check", check.Name())
-			c.results.Failed = appendUnlessOptional(c.results.Failed, certification.Result{Check: check, ElapsedTime: checkElapsedTime})
+			// if a test doesn't pass but is of level warn include it in warning results, instead of failed results
+			if executedCheck.Metadata().Level == check.LevelWarn {
+				logger.WithValues("result", "WARNING").Info("check completed", "check", executedCheck.Name())
+				c.results.Warned = appendUnlessOptional(c.results.Warned, certification.Result{Check: executedCheck, ElapsedTime: checkElapsedTime})
+				continue
+			}
+			logger.WithValues("result", "FAILED").Info("check completed", "check", executedCheck.Name())
+			c.results.Failed = appendUnlessOptional(c.results.Failed, certification.Result{Check: executedCheck, ElapsedTime: checkElapsedTime})
 			continue
 		}
 
-		logger.WithValues("result", "PASSED").Info("check completed", "check", check.Name())
-		c.results.Passed = appendUnlessOptional(c.results.Passed, certification.Result{Check: check, ElapsedTime: checkElapsedTime})
+		logger.WithValues("result", "PASSED").Info("check completed", "check", executedCheck.Name())
+		c.results.Passed = appendUnlessOptional(c.results.Passed, certification.Result{Check: executedCheck, ElapsedTime: checkElapsedTime})
 	}
 
 	if len(c.results.Errors) > 0 || len(c.results.Failed) > 0 {
@@ -674,6 +680,7 @@ func InitializeOperatorChecks(ctx context.Context, p policy.Policy, cfg Operator
 			operatorpol.NewSecurityContextConstraintsCheck(),
 			&operatorpol.RelatedImagesCheck{},
 			operatorpol.FollowsRestrictedNetworkEnablementGuidelines{},
+			operatorpol.RequiredAnnotations{},
 		}, nil
 	}
 
diff --git a/internal/formatters/junitxml.go b/internal/formatters/junitxml.go
index 64d0787..879325f 100644
--- a/internal/formatters/junitxml.go
+++ b/internal/formatters/junitxml.go
@@ -18,6 +18,7 @@ type JUnitTestSuite struct {
 	XMLName    xml.Name        `xml:"testsuite"`
 	Tests      int             `xml:"tests,attr"`
 	Failures   int             `xml:"failures,attr"`
+	Warnings   int             `xml:"warnings,attr"`
 	Time       string          `xml:"time,attr"`
 	Name       string          `xml:"name,attr"`
 	Properties []JUnitProperty `xml:"properties>property,omitempty"`
@@ -30,7 +31,8 @@ type JUnitTestCase struct {
 	Name        string            `xml:"name,attr"`
 	Time        string            `xml:"time,attr"`
 	SkipMessage *JUnitSkipMessage `xml:"skipped,omitempty"`
-	Failure     *JUnitFailure     `xml:"failure,omitempty"`
+	Failure     *JUnitMessage     `xml:"failure,omitempty"`
+	Warning     *JUnitMessage     `xml:"warning,omitempty"`
 	SystemOut   string            `xml:"system-out,omitempty"`
 	Message     string            `xml:",chardata"`
 }
@@ -44,18 +46,19 @@ type JUnitProperty struct {
 	Value string `xml:"value,attr"`
 }
 
-type JUnitFailure struct {
+type JUnitMessage struct {
 	Message  string `xml:"message,attr"`
 	Type     string `xml:"type,attr"`
 	Contents string `xml:",chardata"`
 }
 
-func junitXMLFormatter(ctx context.Context, r certification.Results) ([]byte, error) {
+func junitXMLFormatter(_ context.Context, r certification.Results) ([]byte, error) {
 	response := getResponse(r)
 	suites := JUnitTestSuites{}
 	testsuite := JUnitTestSuite{
-		Tests:      len(r.Errors) + len(r.Failed) + len(r.Passed),
+		Tests:      len(r.Errors) + len(r.Failed) + len(r.Passed) + len(r.Warned),
 		Failures:   len(r.Errors) + len(r.Failed),
+		Warnings:   len(r.Warned),
 		Time:       "0s",
 		Name:       "Red Hat Certification",
 		Properties: []JUnitProperty{},
@@ -80,7 +83,7 @@ func junitXMLFormatter(ctx context.Context, r certification.Results) ([]byte, er
 			Classname: response.Image,
 			Name:      result.Name(),
 			Time:      result.ElapsedTime.String(),
-			Failure: &JUnitFailure{
+			Failure: &JUnitMessage{
 				Message:  "Failed",
 				Type:     "",
 				Contents: fmt.Sprintf("%s: Suggested Fix: %s", result.Help().Message, result.Help().Suggestion),
@@ -90,6 +93,21 @@ func junitXMLFormatter(ctx context.Context, r certification.Results) ([]byte, er
 		totalDuration += result.ElapsedTime
 	}
 
+	for _, result := range r.Warned {
+		testCase := JUnitTestCase{
+			Classname: response.Image,
+			Name:      result.Name(),
+			Time:      result.ElapsedTime.String(),
+			Warning: &JUnitMessage{
+				Message:  "Warn",
+				Type:     "",
+				Contents: fmt.Sprintf("%s: Suggested Fix: %s", result.Help().Message, result.Help().Suggestion),
+			},
+		}
+		testsuite.TestCases = append(testsuite.TestCases, testCase)
+		totalDuration += result.ElapsedTime
+	}
+
 	testsuite.Time = fmt.Sprintf("%f", totalDuration.Seconds())
 	suites.Suites = append(suites.Suites, testsuite)
 
diff --git a/internal/formatters/util.go b/internal/formatters/util.go
index 3cb59ad..3c1cf02 100644
--- a/internal/formatters/util.go
+++ b/internal/formatters/util.go
@@ -11,6 +11,7 @@ func getResponse(r certification.Results) UserResponse {
 	passedChecks := make([]checkExecutionInfo, 0, len(r.Passed))
 	failedChecks := make([]checkExecutionInfo, 0, len(r.Failed))
 	erroredChecks := make([]checkExecutionInfo, 0, len(r.Errors))
+	warnedChecks := make([]checkExecutionInfo, 0, len(r.Warned))
 
 	if len(r.Passed) > 0 {
 		for _, check := range r.Passed {
@@ -47,15 +48,30 @@ func getResponse(r certification.Results) UserResponse {
 		}
 	}
 
+	if len(r.Warned) > 0 {
+		for _, check := range r.Warned {
+			warnedChecks = append(warnedChecks, checkExecutionInfo{
+				Name:             check.Name(),
+				ElapsedTime:      float64(check.ElapsedTime.Milliseconds()),
+				Description:      check.Metadata().Description,
+				Help:             check.Help().Message,
+				Suggestion:       check.Help().Suggestion,
+				KnowledgeBaseURL: check.Metadata().KnowledgeBaseURL,
+				CheckURL:         check.Metadata().CheckURL,
+			})
+		}
+	}
+
 	response := UserResponse{
 		Image:             r.TestedImage,
 		Passed:            r.PassedOverall,
 		LibraryInfo:       version.Version,
 		CertificationHash: r.CertificationHash,
 		Results: resultsText{
-			Passed: passedChecks,
-			Failed: failedChecks,
-			Errors: erroredChecks,
+			Passed:   passedChecks,
+			Failed:   failedChecks,
+			Errors:   erroredChecks,
+			Warnings: warnedChecks,
 		},
 	}
 
@@ -73,9 +89,10 @@ type UserResponse struct {
 
 // resultsText represents the results of check execution against the asset.
 type resultsText struct {
-	Passed []checkExecutionInfo `json:"passed" xml:"passed"`
-	Failed []checkExecutionInfo `json:"failed" xml:"failed"`
-	Errors []checkExecutionInfo `json:"errors" xml:"errors"`
+	Passed   []checkExecutionInfo `json:"passed" xml:"passed"`
+	Failed   []checkExecutionInfo `json:"failed" xml:"failed"`
+	Errors   []checkExecutionInfo `json:"errors" xml:"errors"`
+	Warnings []checkExecutionInfo `json:"warning,omitempty" xml:"warning,omitempty"`
 }
 
 // checkExecutionInfo contains all possible output fields that a user might see in their result.
diff --git a/internal/policy/operator/required_annotations.go b/internal/policy/operator/required_annotations.go
new file mode 100644
index 0000000..03fa6eb
--- /dev/null
+++ b/internal/policy/operator/required_annotations.go
@@ -0,0 +1,102 @@
+package operator
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/go-logr/logr"
+	"github.com/operator-framework/api/pkg/manifests"
+	operatorsv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1"
+
+	"github.com/redhat-openshift-ecosystem/openshift-preflight/internal/check"
+	libcsv "github.com/redhat-openshift-ecosystem/openshift-preflight/internal/csv"
+	"github.com/redhat-openshift-ecosystem/openshift-preflight/internal/image"
+	"github.com/redhat-openshift-ecosystem/openshift-preflight/internal/log"
+)
+
+var infraAnnotations = map[string]string{
+	libcsv.DisconnectedAnnotation:   "required",
+	libcsv.FIPSCompliantAnnotation:  "required",
+	libcsv.ProxyAwareAnnotation:     "required",
+	libcsv.TLSProfilesAnnotation:    "required",
+	libcsv.TokenAuthAWSAnnotation:   "required",
+	libcsv.TokenAuthAzureAnnotation: "required",
+	libcsv.TokenAuthGCPAnnotation:   "required",
+	libcsv.CNFAnnotation:            "optional",
+	libcsv.CNIAnnotation:            "optional",
+	libcsv.CSIAnnotation:            "optional",
+}
+
+var _ check.Check = &RequiredAnnotations{}
+
+type RequiredAnnotations struct{}
+
+func (h RequiredAnnotations) Validate(ctx context.Context, imageReference image.ImageReference) (result bool, err error) {
+	return h.validate(ctx, imageReference.ImageFSPath)
+}
+
+func (h RequiredAnnotations) getBundleCSV(_ context.Context, bundlepath string) (*operatorsv1alpha1.ClusterServiceVersion, error) {
+	bundle, err := manifests.GetBundleFromDir(bundlepath)
+	if err != nil {
+		return nil, err
+	}
+	return bundle.CSV, nil
+}
+
+func (h RequiredAnnotations) validate(ctx context.Context, bundledir string) (bool, error) {
+	logger := logr.FromContextOrDiscard(ctx)
+	csv, err := h.getBundleCSV(ctx, bundledir)
+	if err != nil {
+		return false, err
+	}
+
+	var missingAnnotations []string
+	incorrectValues := map[string]string{}
+	for annotation, status := range infraAnnotations {
+		value, ok := csv.GetAnnotations()[annotation]
+		if !ok {
+			// only add the required annotations to the missing list
+			if status == "required" {
+				missingAnnotations = append(missingAnnotations, annotation)
+			}
+			continue
+		}
+		// the only string values allowed are lower case 'true' or 'false'
+		if !(value == "true" || value == "false") {
+			incorrectValues[annotation] = value
+		}
+	}
+
+	if len(missingAnnotations) > 0 {
+		logger.V(log.DBG).Info("expected annotations are missing", "missingAnnotations", missingAnnotations)
+	}
+
+	if len(incorrectValues) > 0 {
+		for key, value := range incorrectValues {
+			logger.V(log.DBG).Info(fmt.Sprintf("expected annotation: %s to have either 'true' or 'false' value, but had value of: %s.", key, value))
+		}
+	}
+
+	return len(missingAnnotations) == 0 && len(incorrectValues) == 0, nil
+}
+
+func (h RequiredAnnotations) Name() string {
+	return "RequiredAnnotations"
+}
+
+func (h RequiredAnnotations) Metadata() check.Metadata {
+	return check.Metadata{
+		Description: "Checks that the CSV has all of the required feature annotations.",
+		// TODO: This will start as warn, but will need to move to `best`
+		Level:            check.LevelWarn,
+		KnowledgeBaseURL: "https://access.redhat.com/documentation/en-us/red_hat_software_certification/8.69/html-single/red_hat_openshift_software_certification_policy_guide/index#con-operator-requirements_openshift-sw-cert-policy-products-managed",
+		CheckURL:         "https://access.redhat.com/documentation/en-us/red_hat_software_certification/8.69/html-single/red_hat_openshift_software_certification_policy_guide/index#con-operator-requirements_openshift-sw-cert-policy-products-managed",
+	}
+}
+
+func (h RequiredAnnotations) Help() check.HelpText {
+	return check.HelpText{
+		Message:    "Check that the CSV has all of the required feature annotations.",
+		Suggestion: "Add all of the required annotations, and make sure the value is set to either 'true' or 'false'",
+	}
+}
diff --git a/internal/policy/operator/restricted_network_aware.go b/internal/policy/operator/restricted_network_aware.go
index a6d96f8..e8a3db4 100644
--- a/internal/policy/operator/restricted_network_aware.go
+++ b/internal/policy/operator/restricted_network_aware.go
@@ -37,14 +37,18 @@ func (p FollowsRestrictedNetworkEnablementGuidelines) validate(ctx context.Conte
 		return false, err
 	}
 
+	restrictedNetworkSupport := false
 	// If the CSV does not claim to support disconnected environments, there's no reason to check that it followed guidelines.
-	if !libcsv.HasInfrastructureFeaturesAnnotation(csv) {
-		logger.Info("this operator does not have the infrastructure-features annotation. You can safely ignore this if you your operator is not intended to be restricted-network aware.")
-		return false, nil
+	if libcsv.HasDisconnectedAnnotation(csv) && libcsv.SupportsDisconnected(csv.Annotations[libcsv.DisconnectedAnnotation]) {
+		restrictedNetworkSupport = true
+	}
+
+	if libcsv.HasInfrastructureFeaturesAnnotation(csv) && libcsv.SupportsDisconnectedViaInfrastructureFeatures(csv.Annotations[libcsv.InfrastructureFeaturesAnnotation]) {
+		restrictedNetworkSupport = true
 	}
 
-	if !libcsv.SupportsDisconnected(csv.Annotations[libcsv.InfrastructureFeaturesAnnotation]) {
-		logger.Info("the infrastructure-features enabled for this operator did not include the \"disconnected\" identifier. You can safely ignore this if you your operator is not intended to be restricted-network aware.")
+	if !restrictedNetworkSupport {
+		logger.Info("this operator does not indicate it supports installation into restricted networks. This is safe to ignore if you are not intending to deploy in these environments.")
 		return false, nil
 	}
 


Test patch:
diff --git a/internal/csv/csv_test.go b/internal/csv/csv_test.go
index d63b221..f172720 100644
--- a/internal/csv/csv_test.go
+++ b/internal/csv/csv_test.go
@@ -12,10 +12,37 @@ import (
 )
 
 var _ = Describe("CSV Functions", func() {
+	DescribeTable(
+		"Validating the independent disconnected annotation values",
+		func(value string, expected bool) {
+			actual := SupportsDisconnected(value)
+			Expect(actual).To(Equal(expected))
+		},
+		Entry("set to \"true\"", "true", true),
+		Entry("set to \"false\"", "false", false),
+		Entry("set to \"TRUE\"", "TRUE", false),
+		Entry("set to a random string", "abc", false),
+	)
+
+	When("Checking CSVs for the Disconnected Annotation", func() {
+		var csv operatorsv1alpha1.ClusterServiceVersion
+		It("Should return false if the annotation is missing", func() {
+			annotations := map[string]string{}
+			csv.Annotations = annotations
+			Expect(HasDisconnectedAnnotation(&csv)).To(BeFalse())
+		})
+
+		It("Should return true if the annotation is present, regardless of its value", func() {
+			annotations := map[string]string{DisconnectedAnnotation: "foo"}
+			csv.Annotations = annotations
+			Expect(HasDisconnectedAnnotation(&csv)).To(BeTrue())
+		})
+	})
+
 	DescribeTable(
 		"Validating infrastructure features list disconnected",
 		func(featurelist string, expected bool) {
-			actual := SupportsDisconnected(featurelist)
+			actual := SupportsDisconnectedViaInfrastructureFeatures(featurelist)
 			Expect(actual).To(Equal(expected))
 		},
 		Entry("disconnected in all caps", `["DISCONNECTED"]`, true),
diff --git a/internal/engine/engine_test.go b/internal/engine/engine_test.go
index 3d2dfb8..5f2ea47 100644
--- a/internal/engine/engine_test.go
+++ b/internal/engine/engine_test.go
@@ -108,6 +108,24 @@ var _ = Describe("Execute Checks tests", func() {
 			check.HelpText{},
 		)
 
+		warningCheckPassing := check.NewGenericCheck(
+			"warnCheckPassing",
+			func(context.Context, image.ImageReference) (bool, error) {
+				return true, nil
+			},
+			check.Metadata{Level: check.LevelWarn},
+			check.HelpText{},
+		)
+
+		warningCheckFailing := check.NewGenericCheck(
+			"warnCheckFailing",
+			func(context.Context, image.ImageReference) (bool, error) {
+				return false, nil
+			},
+			check.Metadata{Level: check.LevelWarn},
+			check.HelpText{},
+		)
+
 		emptyConfig := runtime.Config{}
 		engine = craneEngine{
 			dockerConfig: emptyConfig.DockerConfig,
@@ -118,6 +136,8 @@ var _ = Describe("Execute Checks tests", func() {
 				failedCheck,
 				optionalCheckPassing,
 				optionalCheckFailing,
+				warningCheckPassing,
+				warningCheckFailing,
 			},
 			isBundle:  false,
 			isScratch: false,
@@ -127,9 +147,10 @@ var _ = Describe("Execute Checks tests", func() {
 		It("should succeed", func() {
 			err := engine.ExecuteChecks(testcontext)
 			Expect(err).ToNot(HaveOccurred())
-			Expect(engine.results.Passed).To(HaveLen(1))
+			Expect(engine.results.Passed).To(HaveLen(2))
 			Expect(engine.results.Failed).To(HaveLen(1))
 			Expect(engine.results.Errors).To(HaveLen(1))
+			Expect(engine.results.Warned).To(HaveLen(1))
 			Expect(engine.results.CertificationHash).To(BeEmpty())
 		})
 		Context("it is a bundle", func() {
@@ -330,6 +351,7 @@ var _ = Describe("Check Name Queries", func() {
 			"SecurityContextConstraintsInCSV",
 			"AllImageRefsInRelatedImages",
 			"FollowsRestrictedNetworkEnablementGuidelines",
+			"RequiredAnnotations",
 		}),
 		Entry("scratch container policy", ScratchContainerPolicy, []string{
 			"HasLicense",
diff --git a/internal/formatters/junitxml_test.go b/internal/formatters/junitxml_test.go
index 06d6613..9e81059 100644
--- a/internal/formatters/junitxml_test.go
+++ b/internal/formatters/junitxml_test.go
@@ -78,6 +78,38 @@ var _ = Describe("JUnitXML Formatter", func() {
 						ElapsedTime: 0,
 					},
 				},
+				Warned: []certification.Result{
+					{
+						Check: check.NewGenericCheck(
+							"WarningCheckPass",
+							func(ctx context.Context, ir image.ImageReference) (bool, error) { return true, nil },
+							check.Metadata{
+								Description:      "description",
+								KnowledgeBaseURL: "kburl",
+								CheckURL:         "checkurl",
+							},
+							check.HelpText{
+								Message:    "helptext",
+								Suggestion: "suggestion",
+							}),
+						ElapsedTime: 0,
+					},
+					{
+						Check: check.NewGenericCheck(
+							"WarningCheckFail",
+							func(ctx context.Context, ir image.ImageReference) (bool, error) { return false, nil },
+							check.Metadata{
+								Description:      "description",
+								KnowledgeBaseURL: "kburl",
+								CheckURL:         "checkurl",
+							},
+							check.HelpText{
+								Message:    "helptext",
+								Suggestion: "suggestion",
+							}),
+						ElapsedTime: 0,
+					},
+				},
 			}
 		})
 		It("should format without error", func() {
diff --git a/internal/policy/operator/required_annotations_test.go b/internal/policy/operator/required_annotations_test.go
new file mode 100644
index 0000000..b79cc5b
--- /dev/null
+++ b/internal/policy/operator/required_annotations_test.go
@@ -0,0 +1,60 @@
+package operator
+
+import (
+	"context"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+)
+
+var _ = Describe("RequiredAnnotations", func() {
+	var h RequiredAnnotations
+	BeforeEach(func() {
+		h = RequiredAnnotations{}
+	})
+
+	AssertMetaData(h)
+
+	When("Getting the CSV from a bundle", func() {
+		It("Should fail if a CSV is not found", func() {
+			_, err := h.getBundleCSV(context.TODO(), "./testdata/doesnotexist")
+			Expect(err).To(HaveOccurred())
+		})
+
+		It("Should successfully find a CSV in a valid bundle", func() {
+			csv, err := h.getBundleCSV(context.TODO(), "./testdata/disconnected_bundle")
+			Expect(err).ToNot(HaveOccurred())
+			Expect(csv.ObjectMeta.Name).ToNot(BeEmpty())
+		})
+	})
+
+	When("Validating that a CSV has all of the required annotations", func() {
+		var bundlepath string
+		It("Should succeed with a bundle that has been prepared as expected", func() {
+			bundlepath = "./testdata/required_annotations_bundle"
+			passed, err := h.validate(context.TODO(), bundlepath)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(passed).To(BeTrue())
+		})
+
+		It("Should fail with a bundle that has not been prepared as expected", func() {
+			bundlepath = "./testdata/invalid_bundle"
+			passed, err := h.validate(context.TODO(), bundlepath)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(passed).To(BeFalse())
+		})
+
+		It("Should fail with a bundle that has not been prepared as expected, and is missing an entry", func() {
+			bundlepath = "./testdata/incorrect_required_annotations_bundle"
+			passed, err := h.validate(context.TODO(), bundlepath)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(passed).To(BeFalse())
+		})
+		It("Should fail with a bundle that has an incorrect optional value", func() {
+			bundlepath = "./testdata/incorrect_optional_annotations_bundle"
+			passed, err := h.validate(context.TODO(), bundlepath)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(passed).To(BeFalse())
+		})
+	})
+})
diff --git a/internal/policy/operator/testdata/incorrect_optional_annotations_bundle/manifests/optional-annotations-operator.clusterserviceversion.yaml b/internal/policy/operator/testdata/incorrect_optional_annotations_bundle/manifests/optional-annotations-operator.clusterserviceversion.yaml
new file mode 100644
index 0000000..5756dbc
--- /dev/null
+++ b/internal/policy/operator/testdata/incorrect_optional_annotations_bundle/manifests/optional-annotations-operator.clusterserviceversion.yaml
@@ -0,0 +1,283 @@
+apiVersion: operators.coreos.com/v1alpha1
+kind: ClusterServiceVersion
+metadata:
+  annotations:
+    alm-examples: |-
+      [
+        {
+          "apiVersion": "tools.opdev.io/v1alpha1",
+          "kind": "optionalAnnotationsApp",
+          "metadata": {
+            "labels": {
+              "app.kubernetes.io/created-by": "optional-annotations-operator",
+              "app.kubernetes.io/instance": "optionalannotationsapp-sample",
+              "app.kubernetes.io/managed-by": "kustomize",
+              "app.kubernetes.io/name": "optionalannotationsapp",
+              "app.kubernetes.io/part-of": "optional-annotations-operator"
+            },
+            "name": "optionalannotationsapp-sample"
+          },
+          "spec": null
+        }
+      ]
+    capabilities: Basic Install
+    createdAt: "2022-12-16T20:14:33Z"
+    features.operators.openshift.io/disconnected: "false"
+    features.operators.openshift.io/fips-compliant: "false"
+    features.operators.openshift.io/proxy-aware: "false"
+    features.operators.openshift.io/tls-profiles: "false"
+    features.operators.openshift.io/token-auth-aws: "false"
+    features.operators.openshift.io/token-auth-azure: "false"
+    features.operators.openshift.io/token-auth-gcp: "false"
+    features.operators.openshift.io/cnf: "foo"
+    operators.operatorframework.io/builder: operator-sdk-v1.31.0
+    operators.operatorframework.io/project_layout: go.kubebuilder.io/v3
+  name: optional-annotations-operator.v0.0.1
+  namespace: placeholder
+spec:
+  apiservicedefinitions: {}
+  customresourcedefinitions:
+    owned:
+      - description: optionalAnnotationsApp is the Schema for the optionalannotationsapps API
+        displayName: Disconnected Friendly App
+        kind: optionalAnnotationsApp
+        name: optionalAnnotationsapps.tools.opdev.io
+        version: v1alpha1
+  description: Deploys a minimal optional-annotations-operator-aware operator
+  displayName: optional Annotations Operator
+  icon:
+    - base64data: ""
+      mediatype: ""
+  install:
+    spec:
+      clusterPermissions:
+        - rules:
+            - apiGroups:
+                - apps
+              resources:
+                - deployments
+              verbs:
+                - create
+                - delete
+                - get
+                - list
+                - patch
+                - update
+                - watch
+            - apiGroups:
+                - apps
+              resources:
+                - deployments/finalizers
+              verbs:
+                - update
+            - apiGroups:
+                - tools.opdev.io
+              resources:
+                - optionalAnnotationsapps
+              verbs:
+                - create
+                - delete
+                - get
+                - list
+                - patch
+                - update
+                - watch
+            - apiGroups:
+                - tools.opdev.io
+              resources:
+                - optionalAnnotationsapps/finalizers
+              verbs:
+                - update
+            - apiGroups:
+                - tools.opdev.io
+              resources:
+                - optionalAnnotationsapps/status
+              verbs:
+                - get
+                - patch
+                - update
+            - apiGroups:
+                - authentication.k8s.io
+              resources:
+                - tokenreviews
+              verbs:
+                - create
+            - apiGroups:
+                - authorization.k8s.io
+              resources:
+                - subjectaccessreviews
+              verbs:
+                - create
+          serviceAccountName: optional-annotations-operator-controller-manager
+      deployments:
+        - label:
+            app.kubernetes.io/component: manager
+            app.kubernetes.io/created-by: optional-annotations-operator
+            app.kubernetes.io/instance: controller-manager
+            app.kubernetes.io/managed-by: kustomize
+            app.kubernetes.io/name: deployment
+            app.kubernetes.io/part-of: optional-annotations-operator
+            control-plane: controller-manager
+          name: optional-annotations-operator-controller-manager
+          spec:
+            replicas: 1
+            selector:
+              matchLabels:
+                control-plane: controller-manager
+            strategy: {}
+            template:
+              metadata:
+                annotations:
+                  kubectl.kubernetes.io/default-container: manager
+                labels:
+                  control-plane: controller-manager
+              spec:
+                affinity:
+                  nodeAffinity:
+                    optionalDuringSchedulingIgnoredDuringExecution:
+                      nodeSelectorTerms:
+                        - matchExpressions:
+                            - key: kubernetes.io/arch
+                              operator: In
+                              values:
+                                - amd64
+                                - arm64
+                                - ppc64le
+                                - s390x
+                            - key: kubernetes.io/os
+                              operator: In
+                              values:
+                                - linux
+                containers:
+                  - args:
+                      - --secure-listen-address=0.0.0.0:8443
+                      - --upstream=http://127.0.0.1:8080/
+                      - --logtostderr=true
+                      - --v=0
+                    image: gcr.io/kubebuilder/kube-rbac-proxy@sha256:d99a8d144816b951a67648c12c0b988936ccd25cf3754f3cd85ab8c01592248f
+                    name: kube-rbac-proxy
+                    ports:
+                      - containerPort: 8443
+                        name: https
+                        protocol: TCP
+                    resources:
+                      limits:
+                        cpu: 500m
+                        memory: 128Mi
+                      requests:
+                        cpu: 5m
+                        memory: 64Mi
+                    securityContext:
+                      allowPrivilegeEscalation: false
+                      capabilities:
+                        drop:
+                          - ALL
+                  - args:
+                      - --health-probe-bind-address=:8081
+                      - --metrics-bind-address=127.0.0.1:8080
+                      - --leader-elect
+                    command:
+                      - /manager
+                    env:
+                      - name: RELATED_IMAGE_FEDORA
+                        value: quay.io/fedora/fedora@sha256:ce08a91085403ecbc637eb2a96bd3554d75537871a12a14030b89243501050f2
+                      - name: DFA_SLEEPER_IMAGE
+                        value: $(RELATED_IMAGE_FEDORA)
+                      - name: DFA_BUSYBOX_IMAGE
+                        value: $(RELATED_IMAGE_FEDORA)
+                    image: quay.io/opdev/optional-annotations-operator-cm@sha256:b4d060da584f7f5f7935e8fb78a0b62b1abb829717ad522190ac7747abd9fbd1
+                    livenessProbe:
+                      httpGet:
+                        path: /healthz
+                        port: 8081
+                      initialDelaySeconds: 15
+                      periodSeconds: 20
+                    name: manager
+                    readinessProbe:
+                      httpGet:
+                        path: /readyz
+                        port: 8081
+                      initialDelaySeconds: 5
+                      periodSeconds: 10
+                    resources:
+                      limits:
+                        cpu: 500m
+                        memory: 128Mi
+                      requests:
+                        cpu: 10m
+                        memory: 64Mi
+                    securityContext:
+                      allowPrivilegeEscalation: false
+                      capabilities:
+                        drop:
+                          - ALL
+                securityContext:
+                  runAsNonRoot: true
+                serviceAccountName: optional-annotations-operator-controller-manager
+                terminationGracePeriodSeconds: 10
+      permissions:
+        - rules:
+            - apiGroups:
+                - ""
+              resources:
+                - configmaps
+              verbs:
+                - get
+                - list
+                - watch
+                - create
+                - update
+                - patch
+                - delete
+            - apiGroups:
+                - coordination.k8s.io
+              resources:
+                - leases
+              verbs:
+                - get
+                - list
+                - watch
+                - create
+                - update
+                - patch
+                - delete
+            - apiGroups:
+                - ""
+              resources:
+                - events
+              verbs:
+                - create
+                - patch
+          serviceAccountName: optional-annotations-operator-controller-manager
+    strategy: deployment
+  installModes:
+    - supported: false
+      type: OwnNamespace
+    - supported: false
+      type: SingleNamespace
+    - supported: false
+      type: MultiNamespace
+    - supported: true
+      type: AllNamespaces
+  keywords:
+    - sample
+    - example
+    - disconnected
+  links:
+    - name: optional Annotations Operator
+      url: https://optional-annotations-operator.domain
+  maintainers:
+    - email: admins@example.com
+      name: Admins
+  maturity: alpha
+  provider:
+    name: The Operator Enablement Team
+    url: https://github.com/opdev
+  relatedImages:
+    - image: quay.io/fedora/fedora@sha256:ce08a91085403ecbc637eb2a96bd3554d75537871a12a14030b89243501050f2
+      name: fedora
+    - image: gcr.io/kubebuilder/kube-rbac-proxy@sha256:d99a8d144816b951a67648c12c0b988936ccd25cf3754f3cd85ab8c01592248f
+      name: kube-rbac-proxy
+    - image: quay.io/opdev/optional-annotations-operator-cm@sha256:b4d060da584f7f5f7935e8fb78a0b62b1abb829717ad522190ac7747abd9fbd1
+      name: manager
+  version: 0.0.1
diff --git a/internal/policy/operator/testdata/incorrect_optional_annotations_bundle/metadata/annotations.yaml b/internal/policy/operator/testdata/incorrect_optional_annotations_bundle/metadata/annotations.yaml
new file mode 100644
index 0000000..13571b0
--- /dev/null
+++ b/internal/policy/operator/testdata/incorrect_optional_annotations_bundle/metadata/annotations.yaml
@@ -0,0 +1,14 @@
+annotations:
+  # Core bundle annotations.
+  operators.operatorframework.io.bundle.mediatype.v1: registry+v1
+  operators.operatorframework.io.bundle.manifests.v1: manifests/
+  operators.operatorframework.io.bundle.metadata.v1: metadata/
+  operators.operatorframework.io.bundle.package.v1: required-annotations-operator
+  operators.operatorframework.io.bundle.channels.v1: alpha
+  operators.operatorframework.io.metrics.builder: operator-sdk-v1.31.0
+  operators.operatorframework.io.metrics.mediatype.v1: metrics+v1
+  operators.operatorframework.io.metrics.project_layout: go.kubebuilder.io/v3
+
+  # Annotations for testing.
+  operators.operatorframework.io.test.mediatype.v1: scorecard+v1
+  operators.operatorframework.io.test.config.v1: tests/scorecard/
diff --git a/internal/policy/operator/testdata/incorrect_required_annotations_bundle/manifests/required-annotations-operator.clusterserviceversion.yaml b/internal/policy/operator/testdata/incorrect_required_annotations_bundle/manifests/required-annotations-operator.clusterserviceversion.yaml
new file mode 100644
index 0000000..31db368
--- /dev/null
+++ b/internal/policy/operator/testdata/incorrect_required_annotations_bundle/manifests/required-annotations-operator.clusterserviceversion.yaml
@@ -0,0 +1,282 @@
+apiVersion: operators.coreos.com/v1alpha1
+kind: ClusterServiceVersion
+metadata:
+  annotations:
+    alm-examples: |-
+      [
+        {
+          "apiVersion": "tools.opdev.io/v1alpha1",
+          "kind": "RequiredAnnotationsApp",
+          "metadata": {
+            "labels": {
+              "app.kubernetes.io/created-by": "required-annotations-operator",
+              "app.kubernetes.io/instance": "requiredannotationsapp-sample",
+              "app.kubernetes.io/managed-by": "kustomize",
+              "app.kubernetes.io/name": "requiredannotationsapp",
+              "app.kubernetes.io/part-of": "required-annotations-operator"
+            },
+            "name": "requiredannotationsapp-sample"
+          },
+          "spec": null
+        }
+      ]
+    capabilities: Basic Install
+    createdAt: "2022-12-16T20:14:33Z"
+    features.operators.openshift.io/disconnected: "foo"
+    features.operators.openshift.io/fips-compliant: "bar"
+    features.operators.openshift.io/proxy-aware: "foo"
+    features.operators.openshift.io/tls-profiles: "bar"
+    features.operators.openshift.io/token-auth-aws: "cat"
+    features.operators.openshift.io/token-auth-azure: "hat"
+    features.operators.openshift.io/token-auth-gcp: ""
+    operators.operatorframework.io/builder: operator-sdk-v1.31.0
+    operators.operatorframework.io/project_layout: go.kubebuilder.io/v3
+  name: required-annotations-operator.v0.0.1
+  namespace: placeholder
+spec:
+  apiservicedefinitions: {}
+  customresourcedefinitions:
+    owned:
+      - description: RequiredAnnotationsApp is the Schema for the requiredannotationsapps API
+        displayName: Disconnected Friendly App
+        kind: RequiredAnnotationsApp
+        name: RequiredAnnotationsapps.tools.opdev.io
+        version: v1alpha1
+  description: Deploys a minimal required-annotations-operator-aware operator
+  displayName: Required Annotations Operator
+  icon:
+    - base64data: ""
+      mediatype: ""
+  install:
+    spec:
+      clusterPermissions:
+        - rules:
+            - apiGroups:
+                - apps
+              resources:
+                - deployments
+              verbs:
+                - create
+                - delete
+                - get
+                - list
+                - patch
+                - update
+                - watch
+            - apiGroups:
+                - apps
+              resources:
+                - deployments/finalizers
+              verbs:
+                - update
+            - apiGroups:
+                - tools.opdev.io
+              resources:
+                - RequiredAnnotationsapps
+              verbs:
+                - create
+                - delete
+                - get
+                - list
+                - patch
+                - update
+                - watch
+            - apiGroups:
+                - tools.opdev.io
+              resources:
+                - RequiredAnnotationsapps/finalizers
+              verbs:
+                - update
+            - apiGroups:
+                - tools.opdev.io
+              resources:
+                - RequiredAnnotationsapps/status
+              verbs:
+                - get
+                - patch
+                - update
+            - apiGroups:
+                - authentication.k8s.io
+              resources:
+                - tokenreviews
+              verbs:
+                - create
+            - apiGroups:
+                - authorization.k8s.io
+              resources:
+                - subjectaccessreviews
+              verbs:
+                - create
+          serviceAccountName: required-annotations-operator-controller-manager
+      deployments:
+        - label:
+            app.kubernetes.io/component: manager
+            app.kubernetes.io/created-by: required-annotations-operator
+            app.kubernetes.io/instance: controller-manager
+            app.kubernetes.io/managed-by: kustomize
+            app.kubernetes.io/name: deployment
+            app.kubernetes.io/part-of: required-annotations-operator
+            control-plane: controller-manager
+          name: required-annotations-operator-controller-manager
+          spec:
+            replicas: 1
+            selector:
+              matchLabels:
+                control-plane: controller-manager
+            strategy: {}
+            template:
+              metadata:
+                annotations:
+                  kubectl.kubernetes.io/default-container: manager
+                labels:
+                  control-plane: controller-manager
+              spec:
+                affinity:
+                  nodeAffinity:
+                    requiredDuringSchedulingIgnoredDuringExecution:
+                      nodeSelectorTerms:
+                        - matchExpressions:
+                            - key: kubernetes.io/arch
+                              operator: In
+                              values:
+                                - amd64
+                                - arm64
+                                - ppc64le
+                                - s390x
+                            - key: kubernetes.io/os
+                              operator: In
+                              values:
+                                - linux
+                containers:
+                  - args:
+                      - --secure-listen-address=0.0.0.0:8443
+                      - --upstream=http://127.0.0.1:8080/
+                      - --logtostderr=true
+                      - --v=0
+                    image: gcr.io/kubebuilder/kube-rbac-proxy@sha256:d99a8d144816b951a67648c12c0b988936ccd25cf3754f3cd85ab8c01592248f
+                    name: kube-rbac-proxy
+                    ports:
+                      - containerPort: 8443
+                        name: https
+                        protocol: TCP
+                    resources:
+                      limits:
+                        cpu: 500m
+                        memory: 128Mi
+                      requests:
+                        cpu: 5m
+                        memory: 64Mi
+                    securityContext:
+                      allowPrivilegeEscalation: false
+                      capabilities:
+                        drop:
+                          - ALL
+                  - args:
+                      - --health-probe-bind-address=:8081
+                      - --metrics-bind-address=127.0.0.1:8080
+                      - --leader-elect
+                    command:
+                      - /manager
+                    env:
+                      - name: RELATED_IMAGE_FEDORA
+                        value: quay.io/fedora/fedora@sha256:ce08a91085403ecbc637eb2a96bd3554d75537871a12a14030b89243501050f2
+                      - name: DFA_SLEEPER_IMAGE
+                        value: $(RELATED_IMAGE_FEDORA)
+                      - name: DFA_BUSYBOX_IMAGE
+                        value: $(RELATED_IMAGE_FEDORA)
+                    image: quay.io/opdev/required-annotations-operator-cm@sha256:b4d060da584f7f5f7935e8fb78a0b62b1abb829717ad522190ac7747abd9fbd1
+                    livenessProbe:
+                      httpGet:
+                        path: /healthz
+                        port: 8081
+                      initialDelaySeconds: 15
+                      periodSeconds: 20
+                    name: manager
+                    readinessProbe:
+                      httpGet:
+                        path: /readyz
+                        port: 8081
+                      initialDelaySeconds: 5
+                      periodSeconds: 10
+                    resources:
+                      limits:
+                        cpu: 500m
+                        memory: 128Mi
+                      requests:
+                        cpu: 10m
+                        memory: 64Mi
+                    securityContext:
+                      allowPrivilegeEscalation: false
+                      capabilities:
+                        drop:
+                          - ALL
+                securityContext:
+                  runAsNonRoot: true
+                serviceAccountName: required-annotations-operator-controller-manager
+                terminationGracePeriodSeconds: 10
+      permissions:
+        - rules:
+            - apiGroups:
+                - ""
+              resources:
+                - configmaps
+              verbs:
+                - get
+                - list
+                - watch
+                - create
+                - update
+                - patch
+                - delete
+            - apiGroups:
+                - coordination.k8s.io
+              resources:
+                - leases
+              verbs:
+                - get
+                - list
+                - watch
+                - create
+                - update
+                - patch
+                - delete
+            - apiGroups:
+                - ""
+              resources:
+                - events
+              verbs:
+                - create
+                - patch
+          serviceAccountName: required-annotations-operator-controller-manager
+    strategy: deployment
+  installModes:
+    - supported: false
+      type: OwnNamespace
+    - supported: false
+      type: SingleNamespace
+    - supported: false
+      type: MultiNamespace
+    - supported: true
+      type: AllNamespaces
+  keywords:
+    - sample
+    - example
+    - disconnected
+  links:
+    - name: Required Annotations Operator
+      url: https://required-annotations-operator.domain
+  maintainers:
+    - email: admins@example.com
+      name: Admins
+  maturity: alpha
+  provider:
+    name: The Operator Enablement Team
+    url: https://github.com/opdev
+  relatedImages:
+    - image: quay.io/fedora/fedora@sha256:ce08a91085403ecbc637eb2a96bd3554d75537871a12a14030b89243501050f2
+      name: fedora
+    - image: gcr.io/kubebuilder/kube-rbac-proxy@sha256:d99a8d144816b951a67648c12c0b988936ccd25cf3754f3cd85ab8c01592248f
+      name: kube-rbac-proxy
+    - image: quay.io/opdev/required-annotations-operator-cm@sha256:b4d060da584f7f5f7935e8fb78a0b62b1abb829717ad522190ac7747abd9fbd1
+      name: manager
+  version: 0.0.1
diff --git a/internal/policy/operator/testdata/incorrect_required_annotations_bundle/metadata/annotations.yaml b/internal/policy/operator/testdata/incorrect_required_annotations_bundle/metadata/annotations.yaml
new file mode 100644
index 0000000..13571b0
--- /dev/null
+++ b/internal/policy/operator/testdata/incorrect_required_annotations_bundle/metadata/annotations.yaml
@@ -0,0 +1,14 @@
+annotations:
+  # Core bundle annotations.
+  operators.operatorframework.io.bundle.mediatype.v1: registry+v1
+  operators.operatorframework.io.bundle.manifests.v1: manifests/
+  operators.operatorframework.io.bundle.metadata.v1: metadata/
+  operators.operatorframework.io.bundle.package.v1: required-annotations-operator
+  operators.operatorframework.io.bundle.channels.v1: alpha
+  operators.operatorframework.io.metrics.builder: operator-sdk-v1.31.0
+  operators.operatorframework.io.metrics.mediatype.v1: metrics+v1
+  operators.operatorframework.io.metrics.project_layout: go.kubebuilder.io/v3
+
+  # Annotations for testing.
+  operators.operatorframework.io.test.mediatype.v1: scorecard+v1
+  operators.operatorframework.io.test.config.v1: tests/scorecard/
diff --git a/internal/policy/operator/testdata/required_annotations_bundle/manifests/required-annotations-operator.clusterserviceversion.yaml b/internal/policy/operator/testdata/required_annotations_bundle/manifests/required-annotations-operator.clusterserviceversion.yaml
new file mode 100644
index 0000000..7a3407a
--- /dev/null
+++ b/internal/policy/operator/testdata/required_annotations_bundle/manifests/required-annotations-operator.clusterserviceversion.yaml
@@ -0,0 +1,282 @@
+apiVersion: operators.coreos.com/v1alpha1
+kind: ClusterServiceVersion
+metadata:
+  annotations:
+    alm-examples: |-
+      [
+        {
+          "apiVersion": "tools.opdev.io/v1alpha1",
+          "kind": "RequiredAnnotationsApp",
+          "metadata": {
+            "labels": {
+              "app.kubernetes.io/created-by": "required-annotations-operator",
+              "app.kubernetes.io/instance": "requiredannotationsapp-sample",
+              "app.kubernetes.io/managed-by": "kustomize",
+              "app.kubernetes.io/name": "requiredannotationsapp",
+              "app.kubernetes.io/part-of": "required-annotations-operator"
+            },
+            "name": "requiredannotationsapp-sample"
+          },
+          "spec": null
+        }
+      ]
+    capabilities: Basic Install
+    createdAt: "2022-12-16T20:14:33Z"
+    features.operators.openshift.io/disconnected: "true"
+    features.operators.openshift.io/fips-compliant: "false"
+    features.operators.openshift.io/proxy-aware: "false"
+    features.operators.openshift.io/tls-profiles: "false"
+    features.operators.openshift.io/token-auth-aws: "false"
+    features.operators.openshift.io/token-auth-azure: "false"
+    features.operators.openshift.io/token-auth-gcp: "false"
+    operators.operatorframework.io/builder: operator-sdk-v1.31.0
+    operators.operatorframework.io/project_layout: go.kubebuilder.io/v3
+  name: required-annotations-operator.v0.0.1
+  namespace: placeholder
+spec:
+  apiservicedefinitions: {}
+  customresourcedefinitions:
+    owned:
+      - description: RequiredAnnotationsApp is the Schema for the requiredannotationsapps API
+        displayName: Disconnected Friendly App
+        kind: RequiredAnnotationsApp
+        name: RequiredAnnotationsapps.tools.opdev.io
+        version: v1alpha1
+  description: Deploys a minimal required-annotations-operator-aware operator
+  displayName: Required Annotations Operator
+  icon:
+    - base64data: ""
+      mediatype: ""
+  install:
+    spec:
+      clusterPermissions:
+        - rules:
+            - apiGroups:
+                - apps
+              resources:
+                - deployments
+              verbs:
+                - create
+                - delete
+                - get
+                - list
+                - patch
+                - update
+                - watch
+            - apiGroups:
+                - apps
+              resources:
+                - deployments/finalizers
+              verbs:
+                - update
+            - apiGroups:
+                - tools.opdev.io
+              resources:
+                - RequiredAnnotationsapps
+              verbs:
+                - create
+                - delete
+                - get
+                - list
+                - patch
+                - update
+                - watch
+            - apiGroups:
+                - tools.opdev.io
+              resources:
+                - RequiredAnnotationsapps/finalizers
+              verbs:
+                - update
+            - apiGroups:
+                - tools.opdev.io
+              resources:
+                - RequiredAnnotationsapps/status
+              verbs:
+                - get
+                - patch
+                - update
+            - apiGroups:
+                - authentication.k8s.io
+              resources:
+                - tokenreviews
+              verbs:
+                - create
+            - apiGroups:
+                - authorization.k8s.io
+              resources:
+                - subjectaccessreviews
+              verbs:
+                - create
+          serviceAccountName: required-annotations-operator-controller-manager
+      deployments:
+        - label:
+            app.kubernetes.io/component: manager
+            app.kubernetes.io/created-by: required-annotations-operator
+            app.kubernetes.io/instance: controller-manager
+            app.kubernetes.io/managed-by: kustomize
+            app.kubernetes.io/name: deployment
+            app.kubernetes.io/part-of: required-annotations-operator
+            control-plane: controller-manager
+          name: required-annotations-operator-controller-manager
+          spec:
+            replicas: 1
+            selector:
+              matchLabels:
+                control-plane: controller-manager
+            strategy: {}
+            template:
+              metadata:
+                annotations:
+                  kubectl.kubernetes.io/default-container: manager
+                labels:
+                  control-plane: controller-manager
+              spec:
+                affinity:
+                  nodeAffinity:
+                    requiredDuringSchedulingIgnoredDuringExecution:
+                      nodeSelectorTerms:
+                        - matchExpressions:
+                            - key: kubernetes.io/arch
+                              operator: In
+                              values:
+                                - amd64
+                                - arm64
+                                - ppc64le
+                                - s390x
+                            - key: kubernetes.io/os
+                              operator: In
+                              values:
+                                - linux
+                containers:
+                  - args:
+                      - --secure-listen-address=0.0.0.0:8443
+                      - --upstream=http://127.0.0.1:8080/
+                      - --logtostderr=true
+                      - --v=0
+                    image: gcr.io/kubebuilder/kube-rbac-proxy@sha256:d99a8d144816b951a67648c12c0b988936ccd25cf3754f3cd85ab8c01592248f
+                    name: kube-rbac-proxy
+                    ports:
+                      - containerPort: 8443
+                        name: https
+                        protocol: TCP
+                    resources:
+                      limits:
+                        cpu: 500m
+                        memory: 128Mi
+                      requests:
+                        cpu: 5m
+                        memory: 64Mi
+                    securityContext:
+                      allowPrivilegeEscalation: false
+                      capabilities:
+                        drop:
+                          - ALL
+                  - args:
+                      - --health-probe-bind-address=:8081
+                      - --metrics-bind-address=127.0.0.1:8080
+                      - --leader-elect
+                    command:
+                      - /manager
+                    env:
+                      - name: RELATED_IMAGE_FEDORA
+                        value: quay.io/fedora/fedora@sha256:ce08a91085403ecbc637eb2a96bd3554d75537871a12a14030b89243501050f2
+                      - name: DFA_SLEEPER_IMAGE
+                        value: $(RELATED_IMAGE_FEDORA)
+                      - name: DFA_BUSYBOX_IMAGE
+                        value: $(RELATED_IMAGE_FEDORA)
+                    image: quay.io/opdev/required-annotations-operator-cm@sha256:b4d060da584f7f5f7935e8fb78a0b62b1abb829717ad522190ac7747abd9fbd1
+                    livenessProbe:
+                      httpGet:
+                        path: /healthz
+                        port: 8081
+                      initialDelaySeconds: 15
+                      periodSeconds: 20
+                    name: manager
+                    readinessProbe:
+                      httpGet:
+                        path: /readyz
+                        port: 8081
+                      initialDelaySeconds: 5
+                      periodSeconds: 10
+                    resources:
+                      limits:
+                        cpu: 500m
+                        memory: 128Mi
+                      requests:
+                        cpu: 10m
+                        memory: 64Mi
+                    securityContext:
+                      allowPrivilegeEscalation: false
+                      capabilities:
+                        drop:
+                          - ALL
+                securityContext:
+                  runAsNonRoot: true
+                serviceAccountName: required-annotations-operator-controller-manager
+                terminationGracePeriodSeconds: 10
+      permissions:
+        - rules:
+            - apiGroups:
+                - ""
+              resources:
+                - configmaps
+              verbs:
+                - get
+                - list
+                - watch
+                - create
+                - update
+                - patch
+                - delete
+            - apiGroups:
+                - coordination.k8s.io
+              resources:
+                - leases
+              verbs:
+                - get
+                - list
+                - watch
+                - create
+                - update
+                - patch
+                - delete
+            - apiGroups:
+                - ""
+              resources:
+                - events
+              verbs:
+                - create
+                - patch
+          serviceAccountName: required-annotations-operator-controller-manager
+    strategy: deployment
+  installModes:
+    - supported: false
+      type: OwnNamespace
+    - supported: false
+      type: SingleNamespace
+    - supported: false
+      type: MultiNamespace
+    - supported: true
+      type: AllNamespaces
+  keywords:
+    - sample
+    - example
+    - disconnected
+  links:
+    - name: Required Annotations Operator
+      url: https://required-annotations-operator.domain
+  maintainers:
+    - email: admins@example.com
+      name: Admins
+  maturity: alpha
+  provider:
+    name: The Operator Enablement Team
+    url: https://github.com/opdev
+  relatedImages:
+    - image: quay.io/fedora/fedora@sha256:ce08a91085403ecbc637eb2a96bd3554d75537871a12a14030b89243501050f2
+      name: fedora
+    - image: gcr.io/kubebuilder/kube-rbac-proxy@sha256:d99a8d144816b951a67648c12c0b988936ccd25cf3754f3cd85ab8c01592248f
+      name: kube-rbac-proxy
+    - image: quay.io/opdev/required-annotations-operator-cm@sha256:b4d060da584f7f5f7935e8fb78a0b62b1abb829717ad522190ac7747abd9fbd1
+      name: manager
+  version: 0.0.1
diff --git a/internal/policy/operator/testdata/required_annotations_bundle/metadata/annotations.yaml b/internal/policy/operator/testdata/required_annotations_bundle/metadata/annotations.yaml
new file mode 100644
index 0000000..13571b0
--- /dev/null
+++ b/internal/policy/operator/testdata/required_annotations_bundle/metadata/annotations.yaml
@@ -0,0 +1,14 @@
+annotations:
+  # Core bundle annotations.
+  operators.operatorframework.io.bundle.mediatype.v1: registry+v1
+  operators.operatorframework.io.bundle.manifests.v1: manifests/
+  operators.operatorframework.io.bundle.metadata.v1: metadata/
+  operators.operatorframework.io.bundle.package.v1: required-annotations-operator
+  operators.operatorframework.io.bundle.channels.v1: alpha
+  operators.operatorframework.io.metrics.builder: operator-sdk-v1.31.0
+  operators.operatorframework.io.metrics.mediatype.v1: metrics+v1
+  operators.operatorframework.io.metrics.project_layout: go.kubebuilder.io/v3
+
+  # Annotations for testing.
+  operators.operatorframework.io.test.mediatype.v1: scorecard+v1
+  operators.operatorframework.io.test.config.v1: tests/scorecard/