Skip to content

experiment (tools): tools/gate CI/CD policy gate#2535

Draft
rh-jfuller wants to merge 1 commit into
guacsec:mainfrom
rh-jfuller:feat/ci_gate
Draft

experiment (tools): tools/gate CI/CD policy gate#2535
rh-jfuller wants to merge 1 commit into
guacsec:mainfrom
rh-jfuller:feat/ci_gate

Conversation

@rh-jfuller

@rh-jfuller rh-jfuller commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Illustrates a CLI tool that answers "should this build be allowed to ship?" by evaluating SBOMs against configurable policies using data from a running Trustify instance. This can be used either via the command line or in CI job.

Policy evaluation uses plugin system. Each checker implements a Checker protocol, registers itself at import time, and receives a CheckContext populated with Trustify data (vulnerabilities, advisories, licenses, PURLs). The gate config file specifies which checkers to run.

Checkers

Checker What it does
builtin Severity thresholds, CVSS score limits, CVE deny/ignore lists, license allow/deny globs
conforma OPA/Rego policy evaluation via Conforma (https://conforma.dev)

Adding a new checker is four steps: create a module, implement the protocol, call register_checker(), add the import.

SBOM input modes

  • --sbom file.json — extract PURLs locally, analyze via Trustify API
  • --sbom file.json --upload — upload to Trustify, full advisory + license analysis
  • --sbom-name "product-1.0" — look up already-ingested SBOM

Example command line usage

Local check with default policy (fail on critical vulns)

uv run python gate.py check --sbom ./sbom.json

Upload + check with custom config, SARIF output

uv run python gate.py check --sbom ./sbom.json --upload
--config examples/builtin-only.json --format sarif --output results.sarif

Local check with conforma policy (needs ec installed).
ensure a local EnterpriseContractPolicy spec exists

cat > my-policy.yaml <<'EOF'
sources:
  - name: my-rules
    policy:
      - "git::https://github.com/conforma/policy//policy/release?ref=main"
configuration:
  exclude:
    - friday_policy
    - room_temperature
EOF

now run gate with that policy file

uv run python gate.py check \
  --sbom ./sbom.cdx.json \
  --config <(cat <<'JSON'
{
  "checkers": [
    {
      "name": "conforma",
      "config": {
        "mode": "cli",
        "policy_file": "./my-policy.yaml",
        "extra_args": ["--ignore-rekor"]
      }
    }
  ]
}
JSON
)

Example CI

  1. Standalone gate check (simplest)
name: Trustify Gate

on:
  push:
    branches: [main]
  pull_request:

jobs:
  gate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: astral-sh/setup-uv@v6

      - name: Generate SBOM
        run: |
          curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
          syft . -o cyclonedx-json=sbom.cdx.json

      - name: Run Trustify Gate
        working-directory: tools/gate
        env:
          TRUSTIFY_URL: ${{ vars.TRUSTIFY_URL }}
          TRUSTIFY_TOKEN: ${{ secrets.TRUSTIFY_TOKEN }}
        run: |
          uv sync
          uv run python gate.py check \
            --sbom ${{ github.workspace }}/sbom.cdx.json \
            --upload \
            --config examples/builtin-only.json
  1. SARIF upload to GitHub Code Scanning
name: Trustify Gate (SARIF)

on:
  push:
    branches: [main]
  pull_request:

permissions:
  security-events: write

jobs:
  gate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: astral-sh/setup-uv@v6

      - name: Generate SBOM
        run: |
          curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
          syft . -o cyclonedx-json=sbom.cdx.json

      - name: Run Trustify Gate
        id: gate
        continue-on-error: true
        working-directory: tools/gate
        env:
          TRUSTIFY_URL: ${{ vars.TRUSTIFY_URL }}
          TRUSTIFY_TOKEN: ${{ secrets.TRUSTIFY_TOKEN }}
        run: |
          uv sync
          uv run python gate.py check \
            --sbom ${{ github.workspace }}/sbom.cdx.json \
            --upload \
            --config examples/builtin-only.json \
            --format sarif \
            --output ${{ github.workspace }}/gate-results.sarif

      - name: Upload SARIF
        uses: github/codeql-action/upload-sarif@v3
        if: always()
        with:
          sarif_file: gate-results.sarif

      - name: Fail on violations
        if: steps.gate.outcome == 'failure'
        run: exit 1
  1. JUnit report (shows in PR checks tab)
name: Trustify Gate (JUnit)

on:
  pull_request:

jobs:
  gate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: astral-sh/setup-uv@v6

      - name: Run Trustify Gate
        id: gate
        continue-on-error: true
        working-directory: tools/gate
        env:
          TRUSTIFY_URL: ${{ vars.TRUSTIFY_URL }}
          TRUSTIFY_TOKEN: ${{ secrets.TRUSTIFY_TOKEN }}
        run: |
          uv sync
          uv run python gate.py check \
            --sbom ${{ github.workspace }}/path/to/sbom.json \
            --upload \
            --format junit \
            --output ${{ github.workspace }}/gate-results.xml

      - name: Publish JUnit report
        uses: mikepenz/action-junit-report@v5
        if: always()
        with:
          report_paths: gate-results.xml
          fail_on_failure: true
  1. Full pipeline with Conforma
name: Trustify Gate (Full Pipeline)

on:
  push:
    branches: [main]

permissions:
  security-events: write

jobs:
  gate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: astral-sh/setup-uv@v6

      - name: Install Conforma CLI
        run: |
          curl -sL https://github.com/conforma/cli/releases/latest/download/ec_linux_amd64 \
            -o /usr/local/bin/ec
          chmod +x /usr/local/bin/ec

      - name: Generate SBOM
        run: |
          curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
          syft . -o cyclonedx-json=sbom.cdx.json

      - name: Run Trustify Gate (builtin + conforma)
        id: gate
        continue-on-error: true
        working-directory: tools/gate
        env:
          TRUSTIFY_URL: ${{ vars.TRUSTIFY_URL }}
          TRUSTIFY_TOKEN: ${{ secrets.TRUSTIFY_TOKEN }}
        run: |
          uv sync
          uv run python gate.py check \
            --sbom ${{ github.workspace }}/sbom.cdx.json \
            --upload \
            --config examples/full-pipeline.json \
            --format sarif \
            --output ${{ github.workspace }}/gate-results.sarif

      - name: Upload SARIF
        uses: github/codeql-action/upload-sarif@v3
        if: always()
        with:
          sarif_file: gate-results.sarif

      - name: Fail on violations
        if: steps.gate.outcome == 'failure'
        run: exit 1

Summary by Sourcery

Introduce a new tools/gate CLI-based CI/CD policy gate that evaluates SBOMs against Trustify-backed security and compliance policies and fails builds on violations.

New Features:

  • Add a trustify-gate CLI with commands to check SBOMs (local, uploaded, or pre-ingested) and list available checker plugins.
  • Provide a builtin threshold-based checker for vulnerabilities and licenses, plus a Conforma-based checker integrating OPA/Rego policies via CLI or server modes.
  • Support multiple output formats (table, JSON, SARIF, JUnit) and a configurable gate policy that aggregates results from multiple checkers.
  • Add a Makefile and example configs to simplify local usage and CI integration of the gate tool.

Enhancements:

  • Define shared policy data models and severity ranking, along with pluggable checker and formatter infrastructure.
  • Introduce a dedicated Trustify REST API client and environment-driven configuration for connecting to Trustify instances.

Build:

  • Add a pyproject.toml for the trustify-gate project, including dependencies, test extras, scripts entry point, and tooling configuration.

Documentation:

  • Add comprehensive README and DESIGN documentation describing Trustify Gate architecture, configuration, usage patterns, and CI/CD workflows.

Tests:

  • Add unit tests for the builtin checker and policy models to validate violation handling, severity ranking, and config loading behavior.

Chores:

  • Add project scaffolding under tools/gate including examples, Makefile, pytest configuration, and .gitignore for the new gate tool.

Python CLI that evaluates SBOMs against configurable security and
compliance policies using running Trustify instance.
Exits non-zero when policy violations are found, designed to run
in CI/CD pipelines as a quality gate.

Key features:
- Plugin-based checker architecture (Checker protocol + registry)
- Built-in checker: severity/score thresholds, CVE deny/ignore
  lists, license allow/deny with glob patterns
- Conforma checker: OPA/Rego policy evaluation via ec CLI or
  HTTP server mode
- Four SBOM input modes: local file, upload, by-name, by-ID
- Four output formats: table, JSON, SARIF, JUnit XML
@sourcery-ai

sourcery-ai Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Introduces a new tools/gate Python-based Trustify CI/CD policy gate CLI with a plugin-based checker system, built-in threshold and Conforma checkers, HTTP client, policy/result modeling, multiple output formats, examples, Makefile, and tests for the builtin checker and policy models.

Sequence diagram for gate check command with upload and multiple checkers

sequenceDiagram
  actor CI as CI_pipeline
  participant Gate as gate.py
  participant Client as client.py
  participant Trustify as Trustify_API
  participant Builtin as BuiltinChecker
  participant Conforma as ConformaChecker
  participant EC as Conforma_ec

  CI->>Gate: main() / _cmd_check(check --sbom file --upload)
  Gate->>Gate: load_gate_config() or default_gate_config()
  Gate->>Gate: _build_context()
  Gate->>Gate: _context_from_upload(sbom_path)
  Gate->>Client: upload_sbom(sbom_path)
  Client->>Trustify: POST /api/v3/sbom
  Trustify-->>Client: SBOM id
  Client-->>Gate: upload result
  Gate->>Gate: _context_from_id(sbom_id)
  Gate->>Client: get_sbom_packages(sbom_id)
  Client->>Trustify: GET /api/v3/sbom/{id}/packages
  Gate->>Client: analyze_purls(purls)
  Client->>Trustify: POST /api/v3/vulnerability/analyze
  Gate->>Client: get_sbom_advisories(sbom_id)
  Client->>Trustify: GET /api/v3/sbom/{id}/advisory
  Gate->>Client: get_sbom_license_ids(sbom_id)
  Client->>Trustify: GET /api/v3/sbom/{id}/all-license-ids
  Client-->>Gate: CheckContext populated

  loop for each checker
    Gate->>Builtin: check(ctx, config)
    Builtin-->>Gate: list[Violation]
    Gate->>Conforma: check(ctx, config)
    Conforma->>EC: ec validate input / HTTP POST
    EC-->>Conforma: validation result
    Conforma-->>Gate: list[Violation]
  end

  Gate->>Gate: GateResult(passed, violations)
  Gate->>Gate: format_result(result, format)
  Gate-->>CI: exit code (0 or 1)
Loading

File-Level Changes

Change Details Files
Add main Trustify Gate CLI that resolves SBOM inputs, orchestrates Trustify API calls, runs configured checkers, and emits results in multiple formats with appropriate exit codes.
  • Implement argument parsing with subcommands for check and list-checkers, supporting multiple SBOM input modes and upload behavior.
  • Resolve SBOMs via upload, name, ID, or local parsing and populate a shared CheckContext with SBOM, PURL, vulnerability, advisory, and license data pulled via the client.
  • Iterate over configured checker specs from the gate config, invoke each checker, aggregate violations into a GateResult, and route to formatters, controlling process exit status based on pass/fail.
  • Provide helper functions to extract PURLs from Trustify package responses and to construct contexts for the different SBOM resolution paths.
tools/gate/gate.py
Introduce a plugin-based checker framework and two concrete checkers: a builtin threshold/allow-deny checker and a Conforma OPA/Rego-based policy checker.
  • Define a CheckContext dataclass and Checker protocol plus a registry with registration and lookup helpers, and auto-import builtin and Conforma checkers on package import.
  • Implement BuiltinChecker to evaluate vulnerability severity/score limits, CVE deny/ignore lists, ignore-unfixed behavior, and license allow/deny glob rules using Trustify analysis/advisory/license data.
  • Implement ConformaChecker to build a consolidated policy input document from CheckContext, call Conforma either via ec validate input subprocess or HTTP server, and translate Conforma failures into internal Violations.
  • Provide helper logic to parse vulnerability severity/score from multiple Trustify shapes and glob-match licenses for allow/deny evaluation.
tools/gate/checkers/__init__.py
tools/gate/checkers/builtin.py
tools/gate/checkers/conforma.py
Add Trustify Gate policy and result modeling plus JSON config loading with sane defaults for checker configuration.
  • Define Violation and GateResult dataclasses with severity ranking helpers and convenience counters for critical/high/medium/low tallies.
  • Implement load_gate_config for JSON-based gate configuration with error reporting and process termination on invalid/missing files.
  • Provide default_gate_config that runs only the builtin checker with a default vulnerability severity/score threshold policy.
  • Expose a shared severity rank map used consistently by checkers and formatters.
tools/gate/policy.py
Implement synchronous Trustify REST client and local SBOM PURL extraction to support both uploaded and non-uploaded SBOM workflows.
  • Wrap core Trustify v3 endpoints for SBOM upload, lookup, advisories, packages, license IDs, and PURL vulnerability analysis with httpx-based helpers that inherit URL/token configuration.
  • Provide local JSON parsing for CycloneDX and SPDX SBOMs to extract PURLs without uploading, with a fallback to Trustify’s extract-sbom-purls API.
  • Centralize HTTP configuration including base URL, auth headers, and timeouts to simplify gate logic.
  • Handle content types for uploads and extraction, including JSON/XML SBOMs.
tools/gate/client.py
Add multiple output formatters (table, JSON, SARIF, JUnit) for gate results, suitable for CI pipelines and integrations like GitHub Code Scanning and JUnit consumers.
  • Implement format_result dispatcher and format-specific functions for JSON, human-readable table with ANSI colors, SARIF 2.1.0, and JUnit XML.
  • Map internal violations to SARIF rules/results with severity-to-level mapping and checker/kind/score/purl as properties.
  • Represent overall gate status and each violation as JUnit test cases, treating higher-severity issues as failures and lower ones as system-out messages.
  • Provide basic XML escaping and terminal color handling based on TTY detection.
tools/gate/formatters.py
Configure environment, packaging, and developer tooling for the new gate tool, including Makefile targets and test configuration.
  • Expose TRUSTIFY_URL and TRUSTIFY_TOKEN via a simple config module, defaulting the URL for local development.
  • Define a pyproject for the gate tool with runtime/test dependencies, a console script entrypoint, Ruff configuration, and pytest settings.
  • Add a Makefile with convenience targets for common check modes (local, upload, by name/ID), checker listing, and dependency syncing with uv.
  • Ensure tests can import the gate package by adjusting sys.path in a conftest.py and ignoring local artifacts via .gitignore.
tools/gate/config.py
tools/gate/pyproject.toml
tools/gate/Makefile
tools/gate/tests/conftest.py
tools/gate/.gitignore
Provide documentation, examples, and tests to explain and validate the Trustify Gate behavior.
  • Add a comprehensive README describing usage, SBOM modes, checkers, output formats, CI integration patterns, and repository file layout.
  • Add a DESIGN document that details architecture, plugin system, checkers, SBOM modes, APIs used, and multi-CI examples.
  • Include example gate configuration JSON files for builtin-only, Conforma-only, Conforma server, and full pipeline setups.
  • Add unit tests for the builtin checker across vulnerability and license scenarios plus combined behavior, and for policy models/config loading behavior.
tools/gate/README.md
tools/gate/docs/DESIGN.md
tools/gate/examples/builtin-only.json
tools/gate/examples/conforma-only.json
tools/gate/examples/conforma-server.json
tools/gate/examples/full-pipeline.json
tools/gate/tests/test_builtin.py
tools/gate/tests/test_policy.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

Security issues:

  • Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'. (link)

General comments:

  • Several non-CLI modules (e.g. client.py, policy.load_gate_config, checkers/conforma.py) call sys.exit, which makes them hard to reuse; consider raising domain-specific exceptions instead and let gate.py translate those into exit codes.
  • The pyproject exposes a gate console script while the README and CI examples invoke python gate.py; aligning usage examples with the installed entrypoint (or vice versa) would reduce confusion for users.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Several non-CLI modules (e.g. `client.py`, `policy.load_gate_config`, `checkers/conforma.py`) call `sys.exit`, which makes them hard to reuse; consider raising domain-specific exceptions instead and let `gate.py` translate those into exit codes.
- The pyproject exposes a `gate` console script while the README and CI examples invoke `python gate.py`; aligning usage examples with the installed entrypoint (or vice versa) would reduce confusion for users.

## Individual Comments

### Comment 1
<location path="tools/gate/client.py" line_range="146-153" />
<code_context>
+    return _extract_purls_via_api(sbom_path)
+
+
+def _extract_purls_via_api(sbom_path: Path) -> list[str]:
+    """Use Trustify's extract-sbom-purls endpoint as a fallback."""
+    content = sbom_path.read_bytes()
+    headers = _headers()
+    headers["Content-Type"] = "application/json"
+
+    with httpx.Client(base_url=TRUSTIFY_URL, headers=headers, timeout=TIMEOUT) as c:
+        r = c.post("/api/v3/ui/extract-sbom-purls", content=content)
+        r.raise_for_status()
+        result = r.json()
</code_context>
<issue_to_address>
**issue (bug_risk):** Fallback PURL extraction assumes API returns a list of dicts; list-of-strings responses will break.

In `_extract_purls_via_api`, the loop `for item in result if isinstance(result, list) else result.get("items", [])` calls `item.get(...)` unconditionally when `result` is a list. If `result` is `['pkg:...']` rather than a list of dicts, this will raise `AttributeError`. Please add type-aware handling (e.g., branch on `isinstance(item, dict)` vs `str`) and extract the PURL accordingly, similar to the defensive logic used elsewhere.
</issue_to_address>

### Comment 2
<location path="tools/gate/tests/test_builtin.py" line_range="88-106" />
<code_context>
+        })
+        assert result == []
+
+    def test_ignore_unfixed(self):
+        ctx = _make_ctx(vuln_analysis=[{
+            "purl": "pkg:npm/baz@3.0",
+            "vulnerabilities": [
+                {
+                    "identifier": "CVE-2024-0010",
+                    "severity": "critical",
+                    "score": 9.8,
+                    "status": "not_affected",
+                },
+            ],
+        }])
+        checker = BuiltinChecker()
+        result = checker.check(ctx, {
+            "vulnerabilities": {"max_severity": "low", "ignore_unfixed": True},
+        })
+        assert result == []
+
+    def test_severity_inferred_from_score(self):
</code_context>
<issue_to_address>
**suggestion (testing):** Add a complementary test where `ignore_unfixed` is false to ensure those statuses are still treated as violations

Please also add a test where the same statuses (`not_affected` / `under_investigation`) are checked with `ignore_unfixed` omitted or set to `False`, asserting that a violation is reported. This ensures the default behavior doesn’t silently change and guards against future regressions.

```suggestion
    def test_ignore_unfixed(self):
        ctx = _make_ctx(vuln_analysis=[{
            "purl": "pkg:npm/baz@3.0",
            "vulnerabilities": [
                {
                    "identifier": "CVE-2024-0010",
                    "severity": "critical",
                    "score": 9.8,
                    "status": "not_affected",
                },
            ],
        }])
        checker = BuiltinChecker()
        result = checker.check(ctx, {
            "vulnerabilities": {"max_severity": "low", "ignore_unfixed": True},
        })
        assert result == []

    def test_unfixed_statuses_violate_by_default(self):
        ctx = _make_ctx(vuln_analysis=[{
            "purl": "pkg:npm/baz@3.0",
            "vulnerabilities": [
                {
                    "identifier": "CVE-2024-0011",
                    "severity": "critical",
                    "score": 9.8,
                    "status": "not_affected",
                },
                {
                    "identifier": "CVE-2024-0012",
                    "severity": "critical",
                    "score": 9.0,
                    "status": "under_investigation",
                },
            ],
        }])
        checker = BuiltinChecker()
        result = checker.check(ctx, {
            "vulnerabilities": {"max_severity": "low"},
        })
        assert len(result) == 2

    def test_unfixed_statuses_violate_when_ignore_unfixed_false(self):
        ctx = _make_ctx(vuln_analysis=[{
            "purl": "pkg:npm/baz@3.0",
            "vulnerabilities": [
                {
                    "identifier": "CVE-2024-0013",
                    "severity": "critical",
                    "score": 9.8,
                    "status": "not_affected",
                },
                {
                    "identifier": "CVE-2024-0014",
                    "severity": "critical",
                    "score": 9.0,
                    "status": "under_investigation",
                },
            ],
        }])
        checker = BuiltinChecker()
        result = checker.check(ctx, {
            "vulnerabilities": {"max_severity": "low", "ignore_unfixed": False},
        })
        assert len(result) == 2

    def test_severity_inferred_from_score(self):
```
</issue_to_address>

### Comment 3
<location path="tools/gate/checkers/conforma.py" line_range="127-132" />
<code_context>
        result = subprocess.run(
            cmd,
            capture_output=True,
            text=True,
            timeout=120,
        )
</code_context>
<issue_to_address>
**security (python.lang.security.audit.dangerous-subprocess-use-audit):** Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.

*Source: opengrep*
</issue_to_address>

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

Comment thread tools/gate/client.py
Comment on lines +146 to +153
def _extract_purls_via_api(sbom_path: Path) -> list[str]:
"""Use Trustify's extract-sbom-purls endpoint as a fallback."""
content = sbom_path.read_bytes()
headers = _headers()
headers["Content-Type"] = "application/json"

with httpx.Client(base_url=TRUSTIFY_URL, headers=headers, timeout=TIMEOUT) as c:
r = c.post("/api/v3/ui/extract-sbom-purls", content=content)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

issue (bug_risk): Fallback PURL extraction assumes API returns a list of dicts; list-of-strings responses will break.

In _extract_purls_via_api, the loop for item in result if isinstance(result, list) else result.get("items", []) calls item.get(...) unconditionally when result is a list. If result is ['pkg:...'] rather than a list of dicts, this will raise AttributeError. Please add type-aware handling (e.g., branch on isinstance(item, dict) vs str) and extract the PURL accordingly, similar to the defensive logic used elsewhere.

Comment on lines +88 to +106
def test_ignore_unfixed(self):
ctx = _make_ctx(vuln_analysis=[{
"purl": "pkg:npm/baz@3.0",
"vulnerabilities": [
{
"identifier": "CVE-2024-0010",
"severity": "critical",
"score": 9.8,
"status": "not_affected",
},
],
}])
checker = BuiltinChecker()
result = checker.check(ctx, {
"vulnerabilities": {"max_severity": "low", "ignore_unfixed": True},
})
assert result == []

def test_severity_inferred_from_score(self):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion (testing): Add a complementary test where ignore_unfixed is false to ensure those statuses are still treated as violations

Please also add a test where the same statuses (not_affected / under_investigation) are checked with ignore_unfixed omitted or set to False, asserting that a violation is reported. This ensures the default behavior doesn’t silently change and guards against future regressions.

Suggested change
def test_ignore_unfixed(self):
ctx = _make_ctx(vuln_analysis=[{
"purl": "pkg:npm/baz@3.0",
"vulnerabilities": [
{
"identifier": "CVE-2024-0010",
"severity": "critical",
"score": 9.8,
"status": "not_affected",
},
],
}])
checker = BuiltinChecker()
result = checker.check(ctx, {
"vulnerabilities": {"max_severity": "low", "ignore_unfixed": True},
})
assert result == []
def test_severity_inferred_from_score(self):
def test_ignore_unfixed(self):
ctx = _make_ctx(vuln_analysis=[{
"purl": "pkg:npm/baz@3.0",
"vulnerabilities": [
{
"identifier": "CVE-2024-0010",
"severity": "critical",
"score": 9.8,
"status": "not_affected",
},
],
}])
checker = BuiltinChecker()
result = checker.check(ctx, {
"vulnerabilities": {"max_severity": "low", "ignore_unfixed": True},
})
assert result == []
def test_unfixed_statuses_violate_by_default(self):
ctx = _make_ctx(vuln_analysis=[{
"purl": "pkg:npm/baz@3.0",
"vulnerabilities": [
{
"identifier": "CVE-2024-0011",
"severity": "critical",
"score": 9.8,
"status": "not_affected",
},
{
"identifier": "CVE-2024-0012",
"severity": "critical",
"score": 9.0,
"status": "under_investigation",
},
],
}])
checker = BuiltinChecker()
result = checker.check(ctx, {
"vulnerabilities": {"max_severity": "low"},
})
assert len(result) == 2
def test_unfixed_statuses_violate_when_ignore_unfixed_false(self):
ctx = _make_ctx(vuln_analysis=[{
"purl": "pkg:npm/baz@3.0",
"vulnerabilities": [
{
"identifier": "CVE-2024-0013",
"severity": "critical",
"score": 9.8,
"status": "not_affected",
},
{
"identifier": "CVE-2024-0014",
"severity": "critical",
"score": 9.0,
"status": "under_investigation",
},
],
}])
checker = BuiltinChecker()
result = checker.check(ctx, {
"vulnerabilities": {"max_severity": "low", "ignore_unfixed": False},
})
assert len(result) == 2
def test_severity_inferred_from_score(self):

Comment on lines +127 to +132
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=120,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

security (python.lang.security.audit.dangerous-subprocess-use-audit): Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.

Source: opengrep

@codecov

codecov Bot commented Jul 23, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 72.19%. Comparing base (e47dacf) to head (01654c9).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2535      +/-   ##
==========================================
+ Coverage   72.17%   72.19%   +0.01%     
==========================================
  Files         465      465              
  Lines       28702    28702              
  Branches    28702    28702              
==========================================
+ Hits        20715    20720       +5     
+ Misses       6812     6802      -10     
- Partials     1175     1180       +5     

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

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@rh-jfuller
rh-jfuller marked this pull request as draft July 23, 2026 17:06
@rh-jfuller

Copy link
Copy Markdown
Contributor Author

I think maybe we can emulate the way https://github.com/EmbarkStudios/cargo-deny works as well

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

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

1 participant