Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions .agents/skills/security-audit/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
---
name: security-audit
description: >-
Audits project dependencies and lockfiles for security vulnerabilities, CVEs, and security advisories (e.g. GHSA, RustSec, PyPI) in Python and Rust codebases. Use whenever adding, updating, or modifying dependencies in Cargo.toml, Cargo.lock, pyproject.toml, uv.lock, or requirements.txt, before submitting pull requests, or when security scanning is requested.
---

# Security & Dependency Vulnerability Audit

This skill guides the agent in auditing project dependencies for known security vulnerabilities (CVEs, GHSA advisories, RustSec advisories) across both Rust and Python ecosystems before code is committed or pushed.

## When to Run

Run this audit workflow whenever:
1. Dependencies are added, updated, or upgraded in `Cargo.toml`, `Cargo.lock`, `pyproject.toml`, or `requirements.txt`.
2. A lockfile is regenerated or modified.
3. Preparing a pull request or pushing changes to remote branches.
4. Triaging security alerts reported by GitHub Dependabot, Trivy, or Sourcery.

---

## Audit Procedures

### 1. Fast Batch Audit (Rust & Python)

Run the included multi-ecosystem audit script:
```powershell
python .agents/skills/security-audit/scripts/audit_deps.py
```
This tool:
* Parses all detected `Cargo.lock` files.
* Queries the [OSV.dev](https://osv.dev) database (aggregating RustSec, GitHub Security Advisories [GHSA], CVE, and crates.io security bulletins) in batch via HTTP in ~200ms.
* Runs `uvx pip-audit` to scan Python packages against PyPI / OSV advisory databases.
* Returns exit code `0` on success, or exit code `1` with exact advisory IDs, affected packages, and remediation versions if vulnerabilities are detected.

### 2. Rust-Specific Audit (`cargo-audit`)

If `cargo-audit` is available:
```powershell
cargo audit --file crates/openpiv_rust/Cargo.lock
```
To install `cargo-audit`:
```powershell
cargo install cargo-audit --locked
```

### 3. Python-Specific Audit (`pip-audit`)

Run without installation via `uvx`:
```powershell
uvx pip-audit
```
Or within an active virtualenv:
```powershell
pip-audit
```

---

## Remediation Workflow

When a vulnerability is detected:
1. **Identify the Advisory**: Note the advisory ID (e.g., `GHSA-36hh-v3qg-5jq4` / `RUSTSEC-2026-0176`) and the minimum fixed version.
2. **Update the Manifest**:
- For Rust: Update `Cargo.toml` with the patched version requirement (e.g., `pyo3 = "0.29"`).
- For Python: Update `pyproject.toml` or `dependencies` with `>= <fixed-version>`.
3. **Regenerate Lockfiles**:
- For Rust: Run `cargo update` or `cargo update -p <crate_name>`.
- For Python: Run `uv lock --upgrade-package <package_name>`.
4. **Adapt Breaking Changes**: Check if the dependency upgrade introduces breaking API changes (e.g., PyO3 API renames such as `py.allow_threads` -> `py.detach`), compile, and run the test suite.
5. **Re-run the Audit**: Confirm that `audit_deps.py` reports `0` known vulnerabilities.
189 changes: 189 additions & 0 deletions .agents/skills/security-audit/scripts/audit_deps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
#!/usr/bin/env python3
"""
Security dependency audit tool for Cargo.lock and Python environments.
Audits dependencies against the Open Source Vulnerabilities (OSV.dev) database
(which aggregates RustSec, GitHub Security Advisories [GHSA], CVE, and PyPI).
"""

import argparse
import json
import os
import subprocess
import sys
import urllib.error
import urllib.request
from pathlib import Path
from typing import Dict, List, Tuple

if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
if hasattr(sys.stderr, "reconfigure"):
sys.stderr.reconfigure(encoding="utf-8", errors="replace")


def parse_cargo_lock(lock_path: Path) -> List[Tuple[str, str]]:
"""Parse name and version of third-party crates from Cargo.lock."""
if not lock_path.is_file():
return []

content = lock_path.read_text(encoding="utf-8")
packages = []

for block in content.split("[[package]]")[1:]:
lines = [line.strip() for line in block.splitlines() if line.strip()]
name = None
version = None
source = None

for line in lines:
if line.startswith("name = "):
name = line.split('"')[1]
elif line.startswith("version = "):
version = line.split('"')[1]
elif line.startswith("source = "):
source = line.split('"')[1]

# Only audit packages fetched from crates.io / external registry
if name and version and source:
packages.append((name, version))

return packages


def query_osv_batch(queries: List[Dict]) -> List[Dict]:
"""Query OSV.dev batch API in chunks of 50."""
url = "https://api.osv.dev/v1/querybatch"
all_results = []
chunk_size = 50

for i in range(0, len(queries), chunk_size):
chunk = queries[i : i + chunk_size]
req = urllib.request.Request(
url,
data=json.dumps({"queries": chunk}).encode("utf-8"),
headers={"Content-Type": "application/json", "User-Agent": "antigravity-security-audit/1.0"},
)
try:
with urllib.request.urlopen(req, timeout=30) as resp:
data = json.loads(resp.read().decode("utf-8"))
all_results.extend(data.get("results", []))
except urllib.error.URLError as e:
print(f"[ERROR] Failed to query OSV API: {e}", file=sys.stderr)
raise

return all_results


def audit_cargo_lock(lock_path: Path) -> int:
"""Audit all dependencies in Cargo.lock."""
print(f"\n[INFO] Auditing Rust dependencies from {lock_path}...")
packages = parse_cargo_lock(lock_path)
if not packages:
print(" No external crates found in Cargo.lock.")
return 0

print(f" Found {len(packages)} external crates. Checking OSV/RustSec/GHSA database...")

queries = [
{"package": {"name": name, "ecosystem": "crates.io"}, "version": version}
for name, version in packages
]

try:
results = query_osv_batch(queries)
except Exception as e:
print(f" [WARN] Could not reach OSV database ({e}).")
return 0

vuln_count = 0
for (pkg_name, pkg_ver), res in zip(packages, results):
vulns = res.get("vulns", [])
if vulns:
vuln_count += len(vulns)
print(f"\n[!] VULNERABILITY DETECTED in {pkg_name} {pkg_ver}:")
for v in vulns:
v_id = v.get("id", "UNKNOWN")
summary = v.get("summary", "No summary provided")
aliases = ", ".join(v.get("aliases", []))
alias_str = f" ({aliases})" if aliases else ""
print(f" * {v_id}{alias_str}: {summary}")
for affected in v.get("affected", []):
ranges = affected.get("ranges", [])
for r in ranges:
for event in r.get("events", []):
if "fixed" in event:
print(f" Fixed in: {event['fixed']}")

if vuln_count == 0:
print(f"[OK] All {len(packages)} Rust dependencies are clean! (0 known vulnerabilities)")
return 0
else:
print(f"\n[FAIL] Found {vuln_count} vulnerability advisory/advisories in Rust dependencies!")
return 1


def audit_python_env() -> int:
"""Audit installed Python packages using pip-audit via uvx or pip."""
print("\n[INFO] Auditing Python dependencies...")
try:
res = subprocess.run(
["uvx", "pip-audit"],
capture_output=True,
text=True,
check=False,
)
if res.returncode == 0:
print("[OK] All Python dependencies are clean! (0 known vulnerabilities)")
return 0
else:
print("[FAIL] Python dependency audit failed:")
print(res.stdout)
print(res.stderr)
return res.returncode
except FileNotFoundError:
print(" [INFO] uvx not found; skipping python pip-audit.")
return 0


def main():
parser = argparse.ArgumentParser(description="Antigravity Security & Dependency Audit Tool")
parser.add_argument("--cargo-lock", type=Path, help="Path to Cargo.lock file")
parser.add_argument("--python", action="store_true", help="Audit Python environment using pip-audit")
parser.add_argument("--all", action="store_true", help="Audit all detected Cargo.lock files and Python env")

args = parser.parse_args()

# Default behavior if no flags: check auto-discovered locks and python
audit_cargo = args.cargo_lock is not None or args.all or not args.python
audit_py = args.python or args.all or args.cargo_lock is None

total_failures = 0

if audit_cargo:
lock_paths = []
if args.cargo_lock:
lock_paths.append(args.cargo_lock)
else:
# Auto-discover Cargo.lock files in repository
for p in Path(".").glob("**/Cargo.lock"):
if ".venv" not in p.parts and "target" not in p.parts:
lock_paths.append(p)

for lp in lock_paths:
rc = audit_cargo_lock(lp)
total_failures += rc

if audit_py:
rc = audit_python_env()
total_failures += rc

if total_failures > 0:
print(f"\n[FAIL] Security audit FAILED with {total_failures} issue(s). Please update vulnerable packages.")
sys.exit(1)
else:
print("\n[SUCCESS] All security audits passed successfully!")
sys.exit(0)


if __name__ == "__main__":
main()
32 changes: 0 additions & 32 deletions .github/workflows/build.yml

This file was deleted.

16 changes: 9 additions & 7 deletions .github/workflows/copilot-setup-steps.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,17 @@ jobs:

steps:
- name: Checkout code
uses: actions/checkout@v7
uses: actions/checkout@v4

- name: Set up Python 3.12
uses: actions/setup-python@v7
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
python-version: "3.12"
enable-cache: true

- name: Install Poetry
run: pip install poetry
- name: Set up Python 3.12
run: uv python install 3.12

- name: Install project dependencies
run: poetry install
run: |
uv venv
uv pip install -e .
Loading
Loading