Skip to content

feat(ISV-7642): static check for leaks in operator files - #1077

Open
mantomas wants to merge 1 commit into
mainfrom
ISV-7642
Open

mantomas wants to merge 1 commit into
mainfrom
ISV-7642

Conversation

@mantomas

@mantomas mantomas commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

Added static check for leak detection in affected operator files (catalog files intentionally excluded after discussion).

  • updated existing leak detection logic to prevent leaks on possible leaktk errors
  • only file paths are part of the check output
  • update detect-changes result with list of operator paths; stored in workspace instead a plain results, as the list might exceed limit for results and this way we are not blocking larger PRs

Tested on separate deployment - example PR here with the check output in gist here.

Closes: ISV-7642

Merge Request Checklists

  • Development is done in feature branches
  • Code changes are submitted as pull request into a primary branch [Provide reason for non-primary branch submissions]
  • Code changes are covered with unit and integration tests.
  • Code passes all automated code tests:
    • Linting
    • Code formatter - Black
    • Security scanners
    • Unit tests
    • Integration tests
  • Code is reviewed by at least 1 team member
  • Pull request is tagged with "risk/good-to-go" label for minor changes

Signed-off-by: tman <tman@redhat.com>
@qodo-redhat-openshift-ecosystem

Copy link
Copy Markdown

PR Summary by Qodo

Scan changed operator files for secret leaks

✨ Enhancement 🐞 Bug fix 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Scan added and modified operator files for secrets, excluding catalogs and deleted files.
• Pass changed-file paths through a workspace to avoid Tekton result size limits.
• Report paths without secret content and sanitize LeakTK failures.
Diagram

graph TD
  PR["PR file list"] --> Detector["Change detector"] --> Workspace["Changes workspace"] --> Task["Static test task"] --> Suite["Operator check"] --> Scanner["LeakTK scanner"] --> Results["Path-only results"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Run one file-list scan outside operator checks
  • ➕ Scans every eligible path even when a PR changes multiple operators.
  • ➕ Avoids module-global affected-file state.
  • ➖ Needs separate integration with suite selection, skip labels, and result formatting.
  • ➖ Moves the scan away from the existing operator-check lifecycle.

Recommendation: The workspace handoff is appropriate for potentially large path lists, and reusing the check framework preserves existing waiver and reporting behavior. Before relying on full changed-file coverage, verify multi-operator PRs: the static-tests entrypoint currently selects one operator object, while the new check scans only paths beneath that object's operator.

Files changed (16) +494 / -51

Enhancement (8) +200 / -38
operator-hosted-pipeline.ymlShare detected changes with the static-test task +3/-0

Share detected changes with the static-test task

• Mounts the detect-changes output directory as a changes workspace for static tests, making the affected-file list available without a Tekton result.

ansible/roles/operator-pipeline/templates/openshift/pipelines/operator-hosted-pipeline.yml

parse-repo-changes.ymlWrite affected operator paths to the workspace +3/-0

Write affected operator paths to the workspace

• Extracts newline-separated operator file paths from the change-detection JSON into a workspace file, avoiding Tekton result size limits.

ansible/roles/operator-pipeline/templates/openshift/tasks/parse-repo-changes.yml

run-static-tests.ymlRun static tests for changed operator files +21/-3

Run static tests for changed operator files

• Adds a changes workspace and passes its path list to static-tests. Operator-file changes now prevent the task from taking its no-changes exit path.

ansible/roles/operator-pipeline/templates/openshift/tasks/run-static-tests.yml

detect_changed_operators.pyCollect existing changed operator files +9/-0

Collect existing changed operator files

• Adds sorted paths under operators/ that still exist at the PR head to change-detection results. Catalog paths and deleted files are excluded.

operatorcert/entrypoints/detect_changed_operators.py

static_tests.pyLoad affected paths for static checks +75/-33

Load affected paths for static checks

• Adds a CLI option for the newline-separated path list and supplies it to the suite through temporary check context. Also accommodates operator-only changes without a bundle version.

operatorcert/entrypoints/static_tests.py

parsed_file.pyExpose affected operator files in parser results +6/-1

Expose affected operator files in parser results

• Stores the affected-file list in ParserResults and serializes it into the change-detection JSON.

operatorcert/parsed_file.py

operator.pyAdd an operator-scoped secret leak check +66/-0

Add an operator-scoped secret leak check

• Scans existing affected files beneath the operator being tested. Emits path-only failures for findings and a generic failure when scanning errors occur.

operatorcert/static_tests/common/operator.py

helpers.pyShare affected-file context with checks +17/-1

Share affected-file context with checks

• Adds setters and getters for the affected operator paths consumed by the new common operator check.

operatorcert/static_tests/helpers.py

Bug fix (1) +50 / -7
redact.pyReturn leak paths and sanitize scanner failures +50/-7

Return leak paths and sanitize scanner failures

• Adds a path-only LeakTK result helper. Suppresses scanner stderr, replaces subprocess and parse failures with generic errors, and ignores extra result fields that could contain secrets.

operatorcert/redact.py

Tests (6) +231 / -6
conftest.pyIsolate affected-file state between tests +10/-1

Isolate affected-file state between tests

• Adds an autouse fixture that resets the new module-global path list before and after each test.

tests/conftest.py

test_detect_changed_operators.pyAssert affected-file detection results +5/-0

Assert affected-file detection results

• Extends expected change-detection output with sorted, existing paths under operators/.

tests/entrypoints/test_detect_changed_operators.py

test_static_tests.pyTest path-list loading and operator-only targets +36/-1

Test path-list loading and operator-only targets

• Covers absent and populated path lists, CLI forwarding, and selection of an operator without a bundle version.

tests/entrypoints/test_static_tests.py

test_operator.pyExercise changed-file leak-check behavior +100/-0

Exercise changed-file leak-check behavior

• Tests operator-path filtering, missing files, clean scans, path-only findings, and generic scan-error failures.

tests/static_tests/common/test_operator.py

test_helpers.pyTest affected-file context helpers +14/-1

Test affected-file context helpers

• Verifies that affected operator paths can be set and retrieved by the static-check helper.

tests/static_tests/test_helpers.py

test_redact.pyTest safe LeakTK parsing and path extraction +66/-3

Test safe LeakTK parsing and path extraction

• Covers blank output lines, ignored secret fields, sanitized subprocess and parse errors, and the path-only scan helper.

tests/test_redact.py

Documentation (1) +13 / -0
static_checks.mdDocument the changed-file leak check +13/-0

Document the changed-file leak check

• Explains the operator-only scan scope, path-only failure output, and the label or command for skipping the check.

docs/users/static_checks.md

@qodo-redhat-openshift-ecosystem

qodo-redhat-openshift-ecosystem Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (0) 🔗 Cross-repo conflicts (1) 📜 Skill insights (0)

Grey Divider


Action required

1. Operator-only edits bypass leak scans ✗ Dismissed
Description
static-tests still runs after apply-test-waivers, whose condition requires a nonempty
added_bundle result. An edit confined to operator files has no added bundle, so the prerequisite
is skipped and the new affected-file handling in run-static-tests never runs.
Code

ansible/roles/operator-pipeline/templates/openshift/tasks/run-static-tests.yml[R78-80]

+        if [ -z "$(params.bundle_path)" ] \
+          && [ -z "$(params.affected_catalog_operators)" ] \
+          && { [ ! -f "$AFFECTED_OPERATOR_FILES_FILE" ] || [ ! -s "$AFFECTED_OPERATOR_FILES_FILE" ]; }; then
Relevance

●●● Strong

Operator-only changes need to trigger static tests; accepted history favors guarding empty change
inputs explicitly.

PR-#987

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The pipeline conditions apply-test-waivers on added_bundle and makes it the prerequisite of
static-tests; the changed task script explicitly expects to run for operator-file changes without
a bundle.

ansible/roles/operator-pipeline/templates/openshift/pipelines/operator-hosted-pipeline.yml[445-478]
ansible/roles/operator-pipeline/templates/openshift/tasks/run-static-tests.yml[76-84]
operatorcert/entrypoints/detect_changed_operators.py[238-254]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Operator-file-only PRs never reach the new leak check because static tests depend on a task skipped when no bundle is added.
## Fix Focus Areas
- ansible/roles/operator-pipeline/templates/openshift/pipelines/operator-hosted-pipeline.yml[445-478]
- ansible/roles/operator-pipeline/templates/openshift/tasks/run-static-tests.yml[76-84]
## Recommended Fix
Schedule static tests after change detection independently of the bundle-only waiver task, while preserving any ordering needed for bundle submissions. Verify an operator-file-only PR executes static tests.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Second operator's secrets go unscanned 🔗 Cross-repo conflict ⛨ Security
Description
check_leaks_in_changed_files filters changed files to the operator passed to it, while
ParserResults.enrich_result() selects only the first affected operator for static tests. When an
allowed community-operators-prod pull request changes non-bundle files for multiple operators, such
as their ci.yaml files, the other operators’ paths remain in change detection but are excluded
from the scan.
Code

operatorcert/static_tests/common/operator.py[R34-36]

+    for rel_path in affected_files:
+        if not rel_path.startswith(operator_prefix):
+            continue
Relevance

●● Moderate

The multi-operator gap is plausible, but changing single-operator static-test targeting is
architectural and lacks close precedent.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The parser permits multiple affected operators when no bundles change and retains their changed
paths, but selects only the first operator for the static-test task. get_objects_to_test builds
one operator target, and the leak check keeps only paths under that target’s prefix, so
operator-level files belonging to the other affected operators are not scanned.

operator-pipelines -> community-operators-prod
operatorcert/entrypoints/detect_changed_operators.py[500-506]
operatorcert/static_tests/common/operator.py[27-42]
operatorcert/parsed_file.py[210-225]
operatorcert/parsed_file.py[250-263]
ansible/roles/operator-pipeline/templates/openshift/pipelines/operator-hosted-pipeline.yml[474-506]
operatorcert/parsed_file.py[205-222]
operatorcert/parsed_file.py[249-264]
operatorcert/entrypoints/static_tests.py[109-121]
operatorcert/static_tests/common/operator.py[27-40]
External repo: konflux-ci/community-operators-prod, operators/3scale-community-operator/ci.yaml [1-7]
External repo: konflux-ci/community-operators-prod, operators/alloydb-omni-operator/ci.yaml [1-14]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
An allowed multi-operator pull request can change non-bundle files for several operators, but the hosted pipeline selects one operator as its static-test target and the operator-scoped leak check excludes the others’ files.

## Fix Focus Areas
- operatorcert/static_tests/common/operator.py[27-50]
- operatorcert/parsed_file.py[205-225]
- operatorcert/entrypoints/static_tests.py[109-121]
- ansible/roles/operator-pipeline/templates/openshift/pipelines/operator-hosted-pipeline.yml[474-506]

## Recommended Fix
Ensure the leak check scans changed files for every affected operator, independently of the single operator and bundle selected for other static checks. Either scan all affected operator paths directly or run the operator check for every affected operator; keep the changed-file list in the workspace so the community pipeline can check each one. Add a test covering non-bundle changes to multiple operators.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗



Remediation recommended

3. Newline filenames evade the leak check 🐞 Bug ≡ Correctness
Description
parse-repo-changes writes raw paths separated by newlines, while load_affected_operator_files
treats each line as a separate path. If a changed filename beneath a valid bundle contains a
newline, its two fragments do not identify the existing file and the check silently omits it.
Code

ansible/roles/operator-pipeline/templates/openshift/tasks/parse-repo-changes.yml[R115-116]

+        # Newline-separated paths under operators/ (avoids Tekton result size limits).
+        jq -r '.affected_operator_files[]?' < changes.json > affected_operator_files.txt
Relevance

●●● Strong

Raw newline framing cannot represent newline-containing paths; this deterministically causes
affected files to be skipped.

PR-#920

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Only the operator name and bundle-directory segments are validated for safe characters. The new
writer emits deeper filename segments without escaping, the loader splits on newlines, and the check
silently skips resulting entries that are not files.

operatorcert/entrypoints/detect_changed_operators.py[155-167]
operatorcert/entrypoints/detect_changed_operators.py[176-205]
ansible/roles/operator-pipeline/templates/openshift/tasks/parse-repo-changes.yml[114-116]
operatorcert/entrypoints/static_tests.py[83-87]
operatorcert/static_tests/common/operator.py[34-42]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A newline within a changed filename splits the new path list into entries that cannot be scanned.
## Fix Focus Areas
- ansible/roles/operator-pipeline/templates/openshift/tasks/parse-repo-changes.yml[114-116]
- operatorcert/entrypoints/static_tests.py[67-87]
- operatorcert/static_tests/common/operator.py[34-42]
## Recommended Fix
Store and read the affected paths using a format that preserves arbitrary filename characters, such as a JSON array, instead of newline delimiting. Cover a filename containing a newline.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗


4. A linked operator file scans outside checkout 🐞 Bug ⛨ Security
Description
check_leaks_in_changed_files accepts a joined path when is_file() succeeds, which also accepts
symbolic links to files outside the repository. If a changed operator path links to an external file
readable by the task, that external file is submitted to LeakTK and its reported path can appear in
the check result.
Code

operatorcert/static_tests/common/operator.py[R37-39]

+        absolute_path = repo_root / rel_path
+        if absolute_path.is_file():
+            paths_to_scan.append(absolute_path)
Relevance

●● Moderate

Symlink escape is a concrete security concern, but repository path semantics and intended symlink
handling are uncertain.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Both the new change collector and the new check use is_file() without a resolved-path containment
check; the check passes accepted paths directly to the scanner and renders scanner-reported paths in
failure messages.

operatorcert/entrypoints/detect_changed_operators.py[500-506]
operatorcert/static_tests/common/operator.py[31-50]
operatorcert/static_tests/common/operator.py[66-75]
operatorcert/redact.py[48-69]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new scan follows changed-file symlinks outside the operator checkout.
## Fix Focus Areas
- operatorcert/entrypoints/detect_changed_operators.py[500-506]
- operatorcert/static_tests/common/operator.py[34-39]
## Recommended Fix
Reject symlinks or resolve each candidate and require its target to remain inside the checkout and intended operator directory before adding it to the scan. Test a link to an external readable file.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗



Informational

5. LeakTK failures leave no diagnostic trail 🐞 Bug ◔ Observability
Description
scan now sends LeakTK's stderr to subprocess.DEVNULL and raises a RuntimeError that carries
only the exit code. check_leaks_in_changed_files then logs only the exception type, so the leak
check, upload_artifacts and create_github_gist all fail with no hint of the cause. That includes
config or pattern-fetch errors, a missing binary config, or a timeout, and operators can't tell
which one it was.
Code

operatorcert/redact.py[R64-74]

+        results_jsonl = subprocess.check_output(
+            ["leaktk", "listen"],
+            input=requests,
+            text=True,
+            stderr=subprocess.DEVNULL,
+        )
+    except subprocess.CalledProcessError as exc:
+        # LeakTK output may contain secret match text; do not propagate it.
+        raise RuntimeError(
+            f"LeakTK scan failed with exit code {exc.returncode}"
+        ) from None
Relevance

●● Moderate

Diagnostics are valuable, but sanitizing LeakTK output is an explicit security tradeoff with mixed
precedent.

PR-#998

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Before this PR, stderr from leaktk listen reached the pod logs. Now it is discarded, and the error
raised contains only exc.returncode. The new check logs type(exc).__name__ and nothing else
(operator.py lines 53-58). scan_and_redact, which the gist and artifact upload entrypoints use,
goes through the same scan, so those flows lose their diagnostics too. Stderr from LeakTK's own
logging is normally config or network errors, not match text. Match results go to stdout, and the PR
already keeps stdout out of the exception.

operatorcert/redact.py[63-74]
operatorcert/static_tests/common/operator.py[49-64]
operatorcert/entrypoints/upload_artifacts.py[113-113]
operatorcert/entrypoints/create_github_gist.py[90-90]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`scan` discards LeakTK stderr and only reports the exit code. Every leaktk failure in the static check and in the artifact/gist upload flows has no root cause in the logs.

## Fix Focus Areas
- operatorcert/redact.py[63-74]
- operatorcert/static_tests/common/operator.py[49-58]

## Recommended Fix
Use `stderr=subprocess.PIPE` instead of DEVNULL. When `CalledProcessError` is raised, run the captured stderr through `leaktk redact --kind Stdio`, or truncate it, then log it at error level. Keep the raised exception message generic. Don't log stdout, because it can contain match text.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗


Grey Divider

Context sources
✅ Compliance rules (platform): 15 rules
✅ Cross-repo context — repo relationships
  Explored: repo: konflux-ci/community-operators-prod (sha: 63702a67) — View relationship
Review mode: Auto: 🧠 Deep: Cross-cutting leak detection spans runtime, CI, data flow, and security paths.

Grey Divider

Tip of the day
💡 Did you know, you can show, collapse, or hide each part of a finding: code, evidence, and all

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment on lines +37 to +39
absolute_path = repo_root / rel_path
if absolute_path.is_file():
paths_to_scan.append(absolute_path)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

4. A linked operator file scans outside checkout 🐞 Bug ⛨ Security

check_leaks_in_changed_files accepts a joined path when is_file() succeeds, which also accepts
symbolic links to files outside the repository. If a changed operator path links to an external file
readable by the task, that external file is submitted to LeakTK and its reported path can appear in
the check result.
Agent Prompt
## Issue description
The new scan follows changed-file symlinks outside the operator checkout.
## Fix Focus Areas
- operatorcert/entrypoints/detect_changed_operators.py[500-506]
- operatorcert/static_tests/common/operator.py[34-39]
## Recommended Fix
Reject symlinks or resolve each candidate and require its target to remain inside the checkout and intended operator directory before adding it to the scan. Test a link to an external readable file.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗

Comment on lines +115 to +116
# Newline-separated paths under operators/ (avoids Tekton result size limits).
jq -r '.affected_operator_files[]?' < changes.json > affected_operator_files.txt

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

3. Newline filenames evade the leak check 🐞 Bug ≡ Correctness

parse-repo-changes writes raw paths separated by newlines, while load_affected_operator_files
treats each line as a separate path. If a changed filename beneath a valid bundle contains a
newline, its two fragments do not identify the existing file and the check silently omits it.
Agent Prompt
## Issue description
A newline within a changed filename splits the new path list into entries that cannot be scanned.
## Fix Focus Areas
- ansible/roles/operator-pipeline/templates/openshift/tasks/parse-repo-changes.yml[114-116]
- operatorcert/entrypoints/static_tests.py[67-87]
- operatorcert/static_tests/common/operator.py[34-42]
## Recommended Fix
Store and read the affected paths using a format that preserves arbitrary filename characters, such as a JSON array, instead of newline delimiting. Cover a filename containing a newline.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗

Comment thread operatorcert/redact.py
Comment on lines +64 to +74
results_jsonl = subprocess.check_output(
["leaktk", "listen"],
input=requests,
text=True,
stderr=subprocess.DEVNULL,
)
except subprocess.CalledProcessError as exc:
# LeakTK output may contain secret match text; do not propagate it.
raise RuntimeError(
f"LeakTK scan failed with exit code {exc.returncode}"
) from None

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Informational

5. Leaktk failures leave no diagnostic trail 🐞 Bug ◔ Observability

scan now sends LeakTK's stderr to subprocess.DEVNULL and raises a RuntimeError that carries
only the exit code. check_leaks_in_changed_files then logs only the exception type, so the leak
check, upload_artifacts and create_github_gist all fail with no hint of the cause. That includes
config or pattern-fetch errors, a missing binary config, or a timeout, and operators can't tell
which one it was.
Agent Prompt
## Issue description
`scan` discards LeakTK stderr and only reports the exit code. Every leaktk failure in the static check and in the artifact/gist upload flows has no root cause in the logs.

## Fix Focus Areas
- operatorcert/redact.py[63-74]
- operatorcert/static_tests/common/operator.py[49-58]

## Recommended Fix
Use `stderr=subprocess.PIPE` instead of DEVNULL. When `CalledProcessError` is raised, run the captured stderr through `leaktk redact --kind Stdio`, or truncate it, then log it at error level. Keep the raised exception message generic. Don't log stdout, because it can contain match text.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗

Comment on lines +34 to +36
for rel_path in affected_files:
if not rel_path.startswith(operator_prefix):
continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

2. Second operator's secrets go unscanned 🔗 Cross-repo conflict ⛨ Security

check_leaks_in_changed_files filters changed files to the operator passed to it, while
ParserResults.enrich_result() selects only the first affected operator for static tests. When an
allowed community-operators-prod pull request changes non-bundle files for multiple operators, such
as their ci.yaml files, the other operators’ paths remain in change detection but are excluded
from the scan.
Agent Prompt
## Issue description
An allowed multi-operator pull request can change non-bundle files for several operators, but the hosted pipeline selects one operator as its static-test target and the operator-scoped leak check excludes the others’ files.

## Fix Focus Areas
- operatorcert/static_tests/common/operator.py[27-50]
- operatorcert/parsed_file.py[205-225]
- operatorcert/entrypoints/static_tests.py[109-121]
- ansible/roles/operator-pipeline/templates/openshift/pipelines/operator-hosted-pipeline.yml[474-506]

## Recommended Fix
Ensure the leak check scans changed files for every affected operator, independently of the single operator and bundle selected for other static checks. Either scan all affected operator paths directly or run the operator check for every affected operator; keep the changed-file list in the workspace so the community pipeline can check each one. Add a test covering non-bundle changes to multiple operators.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant