Skip to content

feat: add OSV.dev dependency vulnerability analyzer (--use-osv) - #135

Merged
vineethsai7 merged 29 commits into
cisco-ai-defense:mainfrom
federicoroncallo-hub:feat/pr4-osv-analyzer
Aug 3, 2026
Merged

feat: add OSV.dev dependency vulnerability analyzer (--use-osv)#135
vineethsai7 merged 29 commits into
cisco-ai-defense:mainfrom
federicoroncallo-hub:feat/pr4-osv-analyzer

Conversation

@federicoroncallo-hub

@federicoroncallo-hub federicoroncallo-hub commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds an optional external analyzer that checks a skill's pinned Python dependencies against the free, open OSV.dev vulnerability database. It follows the existing optional-analyzer pattern (like VirusTotal): opt-in via --use-osv, no API key required, and it fails open on network errors so it never blocks a scan.

  • New core/analyzers/osv_analyzer.py (OSVAnalyzer) — batch-queries api.osv.dev/v1/querybatch, emits SUPPLY_CHAIN_KNOWN_VULNERABILITY (HIGH) with advisory IDs/links.
  • Only exact == pins are queried (an open range has no single version to look up; that risk is covered by the unpinned-dependency check). Pins are collected from requirements*.txt, pyproject.toml, setup.cfg, setup.py (AST), Pipfile, and manifest metadata.
  • Wired through analyzer_factory, CLI (--use-osv), REST API (use_osv), and the interactive wizard.
  • Docs: new osv-analyzer.md deep-dive + CLI/API/configuration reference updates.

Stacked on #133. This branch is based on feat/pr2-unpinned-deps, so the diff currently includes that PR's commits. Once #133 merges, this diff will show only the OSV analyzer. Please review #133 first.

Test plan

  • tests/test_osv_analyzer.py — all HTTP mocked (no live network): pinned parsing, vulnerable/clean packages, unpinned not queried, network-error fail-open, multi-source pin collection (TOML tests skip on <3.11).
  • uv run pytest tests/ passes.
  • uv run pre-commit run --all-files passes.
  • uv run python evals/runners/benchmark_runner.py — 100% precision/recall, no regression.

Made with Cursor

Summary by CodeRabbit

  • New Features

    • Added optional OSV.dev scanning for known vulnerabilities in pinned Python dependencies.
    • Added --use-osv CLI support and a matching API option.
    • Expanded dependency analysis to identify unpinned or loosely specified dependencies across common Python configuration formats.
    • Updated analyzer guidance, configuration, and reference documentation.
  • Bug Fixes

    • Scans continue safely when OSV network requests fail.
    • Dependency checks now account for lockfiles and common dependency declaration formats.

federicoroncallo-hub and others added 7 commits July 2, 2026 11:22
…et/localtunnel.me

ngrok migrated off ngrok.io to ngrok-free.dev / ngrok.app, so exfil to a
current ngrok endpoint slipped past the suspicious-domain checks. Add the
modern ngrok domains plus bore.pub, serveo.net and localtunnel.me to:

- ContextExtractor.SUSPICIOUS_DOMAINS (Python string-literal URL classification)
- tool_chaining_abuse_generic.yara / command_injection_generic.yara exfil dests
- cross_skill_scanner exfil pattern list

Add regression tests for the new domains (YARA true-positives + ContextExtractor).

Co-authored-by: Cursor <cursoragent@cursor.com>
Skill packages are end-user applications, so unpinned dependencies
(requests>=2 or a bare requests) let a later, potentially compromised
release be pulled in at install time -- a supply-chain risk. Only the LLM
prompt mentioned this previously; there was no deterministic check.

Add StaticAnalyzer._check_dependency_pinning() which parses requirements*.txt
files and a manifest metadata "dependencies" list, emitting
SUPPLY_CHAIN_UNPINNED_DEPENDENCY (MEDIUM for open ranges/bare names, LOW for
==x.* wildcard pins). Skips when a lockfile (uv.lock/poetry.lock/...) is
present, since versions are already resolved -- this is why the scanner's own
library pinning policy (ranges in pyproject.toml) is not affected.

Register the rule in the core pack.yaml, document the new pass in the
static-analyzer docs and threat taxonomy, and add unit tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
The suspicious/legitimate domain lists and matching logic previously lived
inside ContextExtractor and only ran over Python AST string literals, so a
tunnel/proxy endpoint hidden in a config value (e.g. base_url in config.yaml)
was never classified.

Refactor (behavior-preserving): extract SUSPICIOUS_DOMAINS/LEGITIMATE_DOMAINS
and the classification into skill_scanner/core/static_analysis/url_classifier.py
(classify_url + extract_urls). ContextExtractor imports from it and keeps the
lists as class attributes for backward compatibility; existing suspicious-URL
behavior is unchanged (covered by refactor-safety tests).

New pass: StaticAnalyzer._scan_config_files() parses config.yaml/.yml/.json,
settings.*, and *.toml (regex fallback on parse failure), runs each URL through
the shared classifier, and emits CONFIG_SUSPICIOUS_URL (HIGH). Registered in
the core pack.yaml.

Docs updated (static-analyzer, behavioral-analyzer). Adds unit tests plus a
labeled config-routed exfil eval sample; benchmark stays at 100% P/R.

Co-authored-by: Cursor <cursoragent@cursor.com>
Collapse the structured YAML/JSON/TOML parse-and-walk in config URL
scanning down to a single raw-text extract_urls() pass. The raw scan is
simpler, drops the json/yaml/tomllib imports and the _iter_string_values
recursion, and additionally catches suspicious URLs hidden in config
comments. classify_url() still only flags known tunnel/exfil domains, so
false-positive risk stays low.

Co-authored-by: Cursor <cursoragent@cursor.com>
Extend the unpinned-dependency check beyond requirements*.txt and
manifest metadata to also read pyproject.toml ([project] dependencies
and optional-dependencies), setup.cfg ([options] install_requires /
extras), setup.py (install_requires literals via AST), and Pipfile
([packages]/[dev-packages]). All sources are normalized to requirement
strings and run through the existing classifier, so a lockfile still
suppresses findings and pinned specs stay clean.

Co-authored-by: Cursor <cursoragent@cursor.com>
Add an opt-in external analyzer that checks a skill's pinned Python
dependencies against the free, open OSV.dev vulnerability database. Follows
the established external-analyzer pattern (like VirusTotal): no API key, and
it fails open so a network error never breaks a scan.

- OSVAnalyzer parses requirements*.txt and manifest metadata dependencies,
  queries only exact (==) pins via OSV querybatch, and emits
  SUPPLY_CHAIN_KNOWN_VULNERABILITY (HIGH) with advisory IDs/links.
- Wire use_osv through analyzer_factory, the CLI (--use-osv), the API router
  (use_osv on JSON + upload endpoints), and the setup wizard.
- Uses httpx (already a dependency): zero new runtime dependencies.

Tests mock all OSV HTTP calls (no live network). Adds a deep-dive doc and
updates the analyzer selection guide, analyzer index, CLI/API references, and
configuration/dependency references.

Co-authored-by: Cursor <cursoragent@cursor.com>
Extend OSV pin collection beyond requirements*.txt and manifest metadata
to also read pyproject.toml, setup.cfg, setup.py (install_requires via
AST), and Pipfile. Only exact == pins are queried against OSV; ranges are
left to the static unpinned-dependency check.

Co-authored-by: Cursor <cursoragent@cursor.com>
@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds an opt-in OSV analyzer for vulnerable pinned Python dependencies. Wires use_osv through CLI, wizard, API, and analyzer construction. Adds static checks for unpinned dependencies across supported manifest formats, with related rule, test, and documentation updates.

Changes

OSV Analyzer and Unpinned Dependency Detection

Layer / File(s) Summary
OSVAnalyzer implementation
skill_scanner/core/analyzers/osv_analyzer.py
Collects exact dependency pins, queries OSV.dev, and emits vulnerability findings with fail-open error handling.
Static dependency-pinning checks
skill_scanner/core/analyzers/static.py, skill_scanner/data/packs/core/pack.yaml
Adds dependency collection, requirement classification, lockfile handling, and SUPPLY_CHAIN_UNPINNED_DEPENDENCY findings.
OSV enablement through scan interfaces
skill_scanner/core/analyzer_factory.py, skill_scanner/cli/*, skill_scanner/api/router.py
Propagates use_osv through analyzer construction, CLI commands, wizard selection, API requests, uploads, and batch scans.
Analyzer validation and registry updates
tests/test_osv_analyzer.py, tests/static_analysis/test_dependency_pinning.py, tests/test_rule_registry.py
Tests OSV queries, dependency parsing, static findings, supported manifests, malformed inputs, and dynamic rule exemptions.
Analyzer and interface documentation
docs/architecture/*, docs/reference/*
Documents OSV scanning, dependency pinning, CLI and API options, configuration, analyzer references, taxonomy mapping, and HTTP client usage.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLIOrAPI
  participant BuildAnalyzers as build_analyzers
  participant OSVAnalyzer
  participant OSVdev as OSV.dev API

  CLIOrAPI->>BuildAnalyzers: build_analyzers(..., use_osv=True)
  BuildAnalyzers->>OSVAnalyzer: instantiate analyzer
  CLIOrAPI->>OSVAnalyzer: analyze(skill)
  OSVAnalyzer->>OSVdev: POST querybatch(name, version pairs)
  OSVdev-->>OSVAnalyzer: vulnerability results
  OSVAnalyzer-->>CLIOrAPI: Finding list
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely identifies the new optional OSV.dev dependency vulnerability analyzer and its --use-osv flag.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 (5)
skill_scanner/core/analyzers/osv_analyzer.py (2)

189-220: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Duplicate pins across manifests produce duplicate findings with identical IDs.

_collect_pinned_dependencies does not dedupe (name, version) pairs. If the same exact pin appears in more than one manifest (e.g. requirements.txt and pyproject.toml, a common redundancy), OSV is queried twice for the same package and two Finding objects are emitted with the exact same id=f"OSV_{name}_{version}" (Line 286) but different file_path/line_number. Downstream consumers that key or dedupe findings by id may silently drop one of the sources, or reports may show inflated/duplicate vulnerability counts.

♻️ Suggested fix: dedupe by (name, version) while keeping all sources
     def _collect_pinned_dependencies(self, skill: Skill) -> list[tuple[str, str, str, int | None]]:
         ...
         collected: list[tuple[str, str, str, int | None]] = []
+        seen: set[tuple[str, str]] = set()
         for source, line_number, raw in self._iter_requirement_strings(skill):
             parsed = self._parse_pinned(raw)
-            if parsed is not None:
+            if parsed is not None and parsed not in seen:
+                seen.add(parsed)
                 collected.append((parsed[0], parsed[1], source, line_number))
         return collected

Also applies to: 279-307

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skill_scanner/core/analyzers/osv_analyzer.py` around lines 189 - 220, The
duplicate OSV findings come from `_collect_pinned_dependencies` returning
repeated `(name, version)` pins from different manifests, which then causes
`_query_osv_batch` and `_create_finding` to emit multiple `Finding` objects with
the same OSV id. Update `_collect_pinned_dependencies` (or the caller in
`OSVAnalyzer.analyze`) to dedupe by `(name, version)` before querying, while
preserving all source/file/line provenance for reporting so a single finding can
represent multiple origins.

172-183: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

httpx.Client is never closed.

OSVAnalyzer.__init__ opens a persistent httpx.Client (with its own connection pool) but the class exposes no close()/__del__/context-manager support, and nothing in the analyzer calls self._client.close(). Since the factory constructs a brand-new OSVAnalyzer (and therefore a new client) on every scan request in the API server (router.py::_build_analyzers is invoked per /scan, /scan-upload, and batch-scan call), sustained traffic will churn many unclosed clients/connection pools, which is a documented source of memory/FD pressure under load for httpx-based services.

♻️ Suggested fix: add explicit lifecycle management
     def __init__(
         self,
         enabled: bool = True,
         ecosystem: str = "PyPI",
         timeout: float = 10.0,
         policy: ScanPolicy | None = None,
     ):
         super().__init__("osv_analyzer", policy=policy)
         self.enabled = enabled
         self.ecosystem = ecosystem
         self.timeout = timeout
         self._client = httpx.Client(timeout=timeout)
+
+    def close(self) -> None:
+        """Release the underlying HTTP connection pool."""
+        self._client.close()
+
+    def __del__(self) -> None:
+        try:
+            self.close()
+        except Exception:
+            pass

Ideally SkillScanner/build_analyzers callers would also invoke close() (or use with) on teardown, but a defensive __del__ prevents unbounded pool growth even if callers forget.

Please confirm whether BaseAnalyzer or SkillScanner already has a teardown hook that closes analyzer resources elsewhere in the codebase.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skill_scanner/core/analyzers/osv_analyzer.py` around lines 172 - 183,
`OSVAnalyzer` creates an `httpx.Client` in `__init__` but never closes it, so
add explicit client lifecycle management to the analyzer. Introduce a
close/cleanup path on `OSVAnalyzer` (and a context-manager or teardown hook if
`BaseAnalyzer`/`SkillScanner` supports one), and make sure the client is closed
when analyzers created by `_build_analyzers` are torn down after scans. If there
is no existing teardown path in `BaseAnalyzer` or `SkillScanner`, add one and
wire it into the API scan flow so `self._client.close()` is always called.
tests/test_rule_registry.py (1)

92-92: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Replace ambiguous en dash with a hyphen.

Ruff flags the (EN DASH) in the comment; using ASCII - avoids lint failures and encoding ambiguity.

🔧 Proposed fix
-    # OSV analyzer: SUPPLY_CHAIN_KNOWN_VULNERABILITY – from external OSV.dev API
+    # OSV analyzer: SUPPLY_CHAIN_KNOWN_VULNERABILITY - from external OSV.dev API
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_rule_registry.py` at line 92, The comment in
tests/test_rule_registry.py uses an ambiguous EN DASH in the “OSV analyzer”
note, which triggers Ruff/encoding issues. Update that comment to use a standard
ASCII hyphen instead of the en dash, keeping the text otherwise unchanged so the
rule registry test comment matches lint expectations.

Source: Linters/SAST tools

skill_scanner/core/analyzers/static.py (2)

752-778: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Dependency pinning check doesn't skip documentation/example manifests.

_collect_requirement_entries matches requirements*.txt, pyproject.toml, setup.cfg, setup.py, and Pipfile purely by basename, regardless of directory. Other checks in this class (e.g. _scan_scripts at line 524, _scan_asset_files at line 1724) use _is_doc_file() to avoid flagging content under doc/example paths. An illustrative docs/examples/requirements.txt snippet in a skill's documentation would be flagged here as a real unpinned dependency, adding noise unrelated to the skill's actual install-time risk.

Uses doc_path_indicators and doc_filename_patterns from the active scan policy to determine if a given relative path belongs to a documentation or example area (e.g. docs/, examples/).

♻️ Proposed fix
         for skill_file in skill.files:
             file_name = Path(skill_file.relative_path).name.lower()
             path = skill_file.relative_path
+            if self._is_doc_file(path):
+                continue
             if file_name.startswith("requirements") and file_name.endswith(".txt"):
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skill_scanner/core/analyzers/static.py` around lines 752 - 778, The
dependency collection in _collect_requirement_entries is treating
documentation/example dependency files as real install inputs because it only
checks basenames like requirements*.txt, pyproject.toml, setup.cfg, setup.py,
and Pipfile. Update this method to skip paths identified as docs/examples using
the same scan-policy-aware logic used by _scan_scripts and _scan_asset_files,
leveraging doc_path_indicators and doc_filename_patterns before adding entries.
Keep the filtering applied consistently for every dependency source path so only
actual skill install-time manifests are scanned.

595-596: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Fix RUF012: mutable class attribute default.

_LOCKFILE_NAMES is a set literal assigned directly in the class body, which Ruff flags because it creates shared mutable state across instances (even though it's never mutated here).

🔧 Proposed fix
-    # Lockfiles whose presence means dependency versions are already resolved/frozen.
-    _LOCKFILE_NAMES = {"uv.lock", "poetry.lock", "pipfile.lock", "requirements.lock"}
+    # Lockfiles whose presence means dependency versions are already resolved/frozen.
+    _LOCKFILE_NAMES = frozenset({"uv.lock", "poetry.lock", "pipfile.lock", "requirements.lock"})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skill_scanner/core/analyzers/static.py` around lines 595 - 596, The
class-level _LOCKFILE_NAMES set in the Static analyzer is a mutable attribute
default that Ruff flags as shared state. Update the Static class definition to
use an immutable container for the lockfile names, or otherwise move the value
to a safe constant pattern so it is not a mutable class attribute. Keep the
existing lockfile checks in the Static analyzer working by referencing the
revised _LOCKFILE_NAMES symbol wherever it is used.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/architecture/analyzers/static-analyzer.md`:
- Around line 21-22: The TL;DR summary in static-analyzer.md is out of date
because the diagram now includes a new detection pass. Update the pass count in
the summary to reflect the added 15th pass, and make sure the wording stays
consistent with the flow shown by the analyzer pipeline entries like the
Dependency pinning checks and Referenced file scanning steps.

---

Nitpick comments:
In `@skill_scanner/core/analyzers/osv_analyzer.py`:
- Around line 189-220: The duplicate OSV findings come from
`_collect_pinned_dependencies` returning repeated `(name, version)` pins from
different manifests, which then causes `_query_osv_batch` and `_create_finding`
to emit multiple `Finding` objects with the same OSV id. Update
`_collect_pinned_dependencies` (or the caller in `OSVAnalyzer.analyze`) to
dedupe by `(name, version)` before querying, while preserving all
source/file/line provenance for reporting so a single finding can represent
multiple origins.
- Around line 172-183: `OSVAnalyzer` creates an `httpx.Client` in `__init__` but
never closes it, so add explicit client lifecycle management to the analyzer.
Introduce a close/cleanup path on `OSVAnalyzer` (and a context-manager or
teardown hook if `BaseAnalyzer`/`SkillScanner` supports one), and make sure the
client is closed when analyzers created by `_build_analyzers` are torn down
after scans. If there is no existing teardown path in `BaseAnalyzer` or
`SkillScanner`, add one and wire it into the API scan flow so
`self._client.close()` is always called.

In `@skill_scanner/core/analyzers/static.py`:
- Around line 752-778: The dependency collection in _collect_requirement_entries
is treating documentation/example dependency files as real install inputs
because it only checks basenames like requirements*.txt, pyproject.toml,
setup.cfg, setup.py, and Pipfile. Update this method to skip paths identified as
docs/examples using the same scan-policy-aware logic used by _scan_scripts and
_scan_asset_files, leveraging doc_path_indicators and doc_filename_patterns
before adding entries. Keep the filtering applied consistently for every
dependency source path so only actual skill install-time manifests are scanned.
- Around line 595-596: The class-level _LOCKFILE_NAMES set in the Static
analyzer is a mutable attribute default that Ruff flags as shared state. Update
the Static class definition to use an immutable container for the lockfile
names, or otherwise move the value to a safe constant pattern so it is not a
mutable class attribute. Keep the existing lockfile checks in the Static
analyzer working by referencing the revised _LOCKFILE_NAMES symbol wherever it
is used.

In `@tests/test_rule_registry.py`:
- Line 92: The comment in tests/test_rule_registry.py uses an ambiguous EN DASH
in the “OSV analyzer” note, which triggers Ruff/encoding issues. Update that
comment to use a standard ASCII hyphen instead of the en dash, keeping the text
otherwise unchanged so the rule registry test comment matches lint expectations.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 732b38da-1029-4f25-ab9f-bab7b235c37c

📥 Commits

Reviewing files that changed from the base of the PR and between 41fec4a and 1d0449b.

📒 Files selected for processing (19)
  • docs/architecture/analyzers/index.md
  • docs/architecture/analyzers/meta-and-external-analyzers.md
  • docs/architecture/analyzers/osv-analyzer.md
  • docs/architecture/analyzers/static-analyzer.md
  • docs/architecture/threat-taxonomy.md
  • docs/reference/api-endpoint-reference.md
  • docs/reference/cli-command-reference.md
  • docs/reference/configuration-reference.md
  • docs/reference/dependencies-and-llm-providers.md
  • skill_scanner/api/router.py
  • skill_scanner/cli/cli.py
  • skill_scanner/cli/wizard.py
  • skill_scanner/core/analyzer_factory.py
  • skill_scanner/core/analyzers/osv_analyzer.py
  • skill_scanner/core/analyzers/static.py
  • skill_scanner/data/packs/core/pack.yaml
  • tests/static_analysis/test_dependency_pinning.py
  • tests/test_osv_analyzer.py
  • tests/test_rule_registry.py

Comment thread docs/architecture/analyzers/static-analyzer.md Outdated
gyrospectre and others added 14 commits July 24, 2026 08:47
ProviderConfig.validate() required a truthy credential for every provider
except Bedrock and Ollama, and the only credential source it checked for
Vertex was GOOGLE_APPLICATION_CREDENTIALS. This blocked ambient auth via
a GCE/Cloud Run attached service account or Workload Identity, even though
LiteLLM/google-auth already fall back to it automatically when no explicit
credential is passed -- the same pattern already supported for Bedrock's
IAM role. Excludes is_vertex from the check, mirroring the Bedrock/Ollama
precedent, and documents the fallback.
- _resolve_api_key() now returns None for Vertex instead of the
  GOOGLE_APPLICATION_CREDENTIALS path, since vertex_ai/gemini-* models
  set both is_vertex and is_gemini, which was causing the file path to
  be written into GEMINI_API_KEY.
- Regenerated configuration-reference.md via generate_reference_docs.py
  instead of hand-editing, and updated the underlying descriptions so
  the doc doesn't drift on next regeneration.

Addresses CodeRabbit review feedbak on cisco-ai-defense#144.
@codecov-commenter

codecov-commenter commented Aug 3, 2026

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 88.42975% with 42 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
skill_scanner/core/analyzers/osv_analyzer.py 90.55% 17 Missing ⚠️
skill_scanner/core/analyzers/static.py 90.53% 16 Missing ⚠️
skill_scanner/core/analyzer_factory.py 16.66% 5 Missing ⚠️
skill_scanner/cli/wizard.py 0.00% 3 Missing ⚠️
skill_scanner/cli/cli.py 66.66% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

# Conflicts:
#	skill_scanner/core/static_analysis/context_extractor.py
# Conflicts:
#	docs/architecture/analyzers/static-analyzer.md
#	skill_scanner/core/analyzers/static.py
#	skill_scanner/data/packs/core/pack.yaml

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
skill_scanner/core/analyzers/static.py (2)

688-707: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Fix: _entries_from_setup_cfg can crash analyze() on a % in install_requires.

The try/except configparser.Error block only wraps parser.read_string(content). parser.get("options", "install_requires") and parser.items("options.extras_require") run outside that block, and configparser's default BasicInterpolation raises configparser.InterpolationSyntaxError (a subclass of configparser.Error) when a value contains an unescaped %. A setup.cfg with a bare % in install_requires (malformed or adversarial input, which this analyzer must tolerate since it scans untrusted skill packages) crashes the whole analyze() call instead of being skipped gracefully like the TOML and setup.py paths.

Disable interpolation, since these values are read as plain strings, not templated.

🛡️ Proposed fix
     def _entries_from_setup_cfg(self, path: str, content: str) -> list[tuple[str, int | None, str]]:
         """``[options] install_requires`` and ``[options.extras_require]``."""
-        parser = configparser.ConfigParser()
+        parser = configparser.ConfigParser(interpolation=None)
         try:
             parser.read_string(content)
         except configparser.Error:
             return []
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skill_scanner/core/analyzers/static.py` around lines 688 - 707, Update
_entries_from_setup_cfg to construct ConfigParser with interpolation disabled,
so install_requires and extras_require values are read as literal strings and
bare '%' characters cannot trigger interpolation errors during analyze().
Preserve the existing parsing and entry extraction behavior.

799-801: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Lockfile-presence check is bypassable and too broad.

any(... in self._LOCKFILE_NAMES for f in skill.files) skips ALL dependency-pinning findings for the entire skill package if a recognized lockfile filename appears anywhere in the file tree, regardless of directory or whether it actually corresponds to the flagged requirements source. A skill author can drop an unrelated or empty poetry.lock anywhere in the package to silence every SUPPLY_CHAIN_UNPINNED_DEPENDENCY finding for an unrelated requirements.txt, pyproject.toml, or Pipfile elsewhere in the tree.

Scope the lockfile check to the directory of the requirement source it's meant to cover, so an unrelated or planted lockfile cannot suppress findings for a different manifest.

🛡️ Proposed fix
         findings: list[Finding] = []

-        # A lockfile freezes the resolved versions, so ranges are intentional.
-        if any(Path(f.relative_path).name.lower() in self._LOCKFILE_NAMES for f in skill.files):
-            return findings
+        # A lockfile freezes the resolved versions, so ranges are intentional.
+        # Scope this to the lockfile's own directory so a lockfile placed
+        # elsewhere in the package cannot suppress findings for an
+        # unrelated, unpinned requirements source.
+        lockfile_dirs = {
+            Path(f.relative_path).parent
+            for f in skill.files
+            if Path(f.relative_path).name.lower() in self._LOCKFILE_NAMES
+        }

         for source_label, line_number, raw in self._collect_requirement_entries(skill):
+            if Path(source_label).parent in lockfile_dirs:
+                continue
             classified = self._classify_requirement(raw)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skill_scanner/core/analyzers/static.py` around lines 799 - 801, Update the
lockfile check in the surrounding analyzer method to evaluate only lockfiles
located in the same directory as the dependency requirement source being
analyzed, rather than anywhere under skill.files. Preserve the existing
recognized-name filtering, and ensure unrelated or empty lockfiles elsewhere
cannot suppress SUPPLY_CHAIN_UNPINNED_DEPENDENCY findings.
🧹 Nitpick comments (1)
skill_scanner/core/analyzers/static.py (1)

709-723: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Consider scoping the ast.walk to list/tuple elements only.

ast.walk(node.value) descends into every nested node of the install_requires expression, not just its list elements. If the value is something like install_requires=filter_deps(read_requirements("requirements.txt")), unrelated string arguments get collected and misclassified as package requirements, producing noisy false-positive findings.

Restrict collection to elements of a literal list/tuple, and skip non-literal expressions.

♻️ Proposed refactor
         for node in ast.walk(tree):
             if not (isinstance(node, ast.keyword) and node.arg == "install_requires"):
                 continue
-            for literal in ast.walk(node.value):
-                if isinstance(literal, ast.Constant) and isinstance(literal.value, str):
-                    line_number = getattr(literal, "lineno", None)
-                    entries.append((path, line_number, literal.value))
+            if not isinstance(node.value, (ast.List, ast.Tuple)):
+                continue
+            for elt in node.value.elts:
+                if isinstance(elt, ast.Constant) and isinstance(elt.value, str):
+                    entries.append((path, getattr(elt, "lineno", None), elt.value))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skill_scanner/core/analyzers/static.py` around lines 709 - 723, Update
_entries_from_setup_py to process install_requires only when node.value is an
ast.List or ast.Tuple, collecting string constants from its direct elements
rather than recursively walking the expression. Skip calls, names, and other
non-literal expressions so unrelated nested strings are not treated as package
requirements.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@skill_scanner/core/analyzers/static.py`:
- Around line 688-707: Update _entries_from_setup_cfg to construct ConfigParser
with interpolation disabled, so install_requires and extras_require values are
read as literal strings and bare '%' characters cannot trigger interpolation
errors during analyze(). Preserve the existing parsing and entry extraction
behavior.
- Around line 799-801: Update the lockfile check in the surrounding analyzer
method to evaluate only lockfiles located in the same directory as the
dependency requirement source being analyzed, rather than anywhere under
skill.files. Preserve the existing recognized-name filtering, and ensure
unrelated or empty lockfiles elsewhere cannot suppress
SUPPLY_CHAIN_UNPINNED_DEPENDENCY findings.

---

Nitpick comments:
In `@skill_scanner/core/analyzers/static.py`:
- Around line 709-723: Update _entries_from_setup_py to process install_requires
only when node.value is an ast.List or ast.Tuple, collecting string constants
from its direct elements rather than recursively walking the expression. Skip
calls, names, and other non-literal expressions so unrelated nested strings are
not treated as package requirements.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8bfedc89-7beb-477f-bb62-71cc0d7bb923

📥 Commits

Reviewing files that changed from the base of the PR and between 6ececb1 and 8c00123.

📒 Files selected for processing (8)
  • docs/architecture/analyzers/static-analyzer.md
  • docs/reference/cli-command-reference.md
  • docs/reference/configuration-reference.md
  • docs/reference/dependencies-and-llm-providers.md
  • skill_scanner/api/router.py
  • skill_scanner/cli/cli.py
  • skill_scanner/core/analyzers/static.py
  • skill_scanner/data/packs/core/pack.yaml
🚧 Files skipped from review as they are similar to previous changes (7)
  • docs/reference/dependencies-and-llm-providers.md
  • docs/architecture/analyzers/static-analyzer.md
  • skill_scanner/data/packs/core/pack.yaml
  • docs/reference/cli-command-reference.md
  • skill_scanner/api/router.py
  • docs/reference/configuration-reference.md
  • skill_scanner/cli/cli.py

@vineethsai7
vineethsai7 merged commit 199fa44 into cisco-ai-defense:main Aug 3, 2026
8 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.

4 participants