Skip to content
Draft
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
83 changes: 80 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,14 @@ name: CI

on:
pull_request:
branches: ["main"]
push:
branches: ["main"]
workflow_dispatch:
inputs:
change_base_sha:
description: "Full base commit SHA for change detection"
required: true
type: string

# Least privilege: these jobs only read the repo; no write scopes are needed.
permissions:
Expand All @@ -44,12 +49,19 @@ jobs:
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
persist-credentials: false
fetch-depth: 0
- id: filter
env:
BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}
HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
BASE_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event_name == 'workflow_dispatch' && inputs.change_base_sha || github.event.before }}
HEAD_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
run: |
if [[ ! "$BASE_SHA" =~ ^[0-9a-f]{40}$ ]] || \
! git cat-file -e "${BASE_SHA}^{commit}" 2>/dev/null; then
echo "::error::Change base must be an available full commit SHA."
exit 1
fi
if git diff --quiet "$BASE_SHA" "$HEAD_SHA" -- \
.dockerignore .github/workflows/ci.yml .gitlab-ci.yml Dockerfile \
Makefile pyproject.toml uv.lock src tests/docker tests/fixtures/safe_skill; then
Expand All @@ -62,6 +74,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
persist-credentials: false
- name: Set up uv
# Pinned to a full commit SHA (third-party action); comment tracks the tag.
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5
Expand All @@ -78,6 +93,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
persist-credentials: false
- name: Set up uv
# Pinned to a full commit SHA (third-party action); comment tracks the tag.
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5
Expand All @@ -90,12 +108,69 @@ jobs:
- run: uv run skillspector --version
- run: uv run make test-ci

parser-wheel-smoke:
name: Parser wheels (${{ matrix.name }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- name: linux-x86_64-py312
os: ubuntu-latest
python-version: "3.12"
- name: linux-x86_64-py313
os: ubuntu-latest
python-version: "3.13"
- name: linux-x86_64-py314
os: ubuntu-latest
python-version: "3.14"
- name: linux-arm64-py314
os: ubuntu-24.04-arm
python-version: "3.14"
- name: macos-py314
os: macos-latest
python-version: "3.14"
- name: windows-x86_64-py314
os: windows-latest
python-version: "3.14"
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
persist-credentials: false
- name: Set up uv
# Pinned to a full commit SHA (third-party action); comment tracks the tag.
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5
with:
version: ${{ env.UV_VERSION }}
python-version: ${{ matrix.python-version }}
- name: Create clean parser environment
run: uv venv --python ${{ matrix.python-version }} .parser-smoke-venv
- name: Install exact parser wheels only
run: >-
uv pip install --python .parser-smoke-venv --only-binary=:all:
tree-sitter==0.25.2 tree-sitter-bash==0.25.1
- name: Install SkillSpector runtime
run: uv pip install --python .parser-smoke-venv .
- name: Smoke production parser loader
run: >-
uv run --no-project --python .parser-smoke-venv python -c
"import time;
from skillspector.shell_frontend import load_bash_language, parse_bash_source;
tree = parse_bash_source(
b'printf ok\\n', deadline_monotonic=time.monotonic() + 5.0);
assert load_bash_language().abi_version == 15;
assert tree.root_node.type == 'program'"

docker-smoke:
needs: changes
if: needs.changes.outputs.docker == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
persist-credentials: false
- run: docker version
- run: docker info
- run: docker build -t skillspector .
Expand All @@ -116,6 +191,8 @@ jobs:
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
persist-credentials: false
fetch-depth: 0

- name: Verify DCO sign-off on all commits
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ SkillSpector is part of the [NVIDIA Verified Skills pipeline](https://docs.nvidi
- **[Scan agent skills before installation](https://docs.nvidia.com/skills/scanning-agent-skills)** — Hosted guide: when to scan, how to read a report, and how to gate installs.
- **[Development guide](docs/DEVELOPMENT.md)** — Architecture, package layout, and how to extend the analyzer pipeline.
- **[Analysis resource bounds](docs/ANALYSIS_RESOURCE_BOUNDS.md)** — Fail-closed bundle, parser, nested-artifact, ledger, and finding ceilings.
- **[Dependency source redirection](docs/DEPENDENCY_SOURCE_REDIRECTION.md)** — SC10 direct-configuration coverage, evidence, and executable-surface limits.
- **[Dependency source redirection](docs/DEPENDENCY_SOURCE_REDIRECTION.md)** — SC10 direct-configuration and bounded executable-surface coverage, evidence, and limitations.
- **[Pi extension](docs/PI_EXTENSION.md)** — Install SkillSpector as a Pi tool for scanning skills from inside agent sessions.

## Features
Expand Down
12 changes: 12 additions & 0 deletions THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,18 @@ and project URL.
- **Copyright:** Copyright (c) 2017-2021 Ingy dot Net; Copyright (c) 2006-2016 Kirill Simonov
- **URL:** https://github.com/yaml/pyyaml

### tree-sitter (py-tree-sitter)

- **License:** MIT
- **Copyright:** Copyright (c) 2019 Max Brunsfeld, GitHub
- **URL:** https://github.com/tree-sitter/py-tree-sitter

### tree-sitter-bash

- **License:** MIT
- **Copyright:** Copyright (c) 2017 Max Brunsfeld
- **URL:** https://github.com/tree-sitter/tree-sitter-bash

### pydantic

- **License:** MIT
Expand Down
101 changes: 82 additions & 19 deletions docs/DEPENDENCY_SOURCE_REDIRECTION.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# Dependency Source Redirection (SC10)

SC10 reports a deterministic `HIGH` finding when a supported direct configuration file changes
dependency resolution away from that ecosystem's built-in canonical default. The analysis is
local, static-only, and advisory: it reports evidence for review but does not decide whether a
skill should be installed.
SC10 reports a deterministic `HIGH` finding when a supported direct configuration or
syntax-proven executable surface changes dependency resolution away from that ecosystem's built-in
canonical default. The analysis is local, static-only, and advisory: it reports evidence for review
by a human.

## Direct configuration coverage

Expand Down Expand Up @@ -55,25 +55,88 @@ Direct parser limits are shared across the scan where applicable:
| Retained literal bytes | 2,000,000 |
| Emitted source changes | 10,000 |

## Executable and generated configuration boundary
## Syntax-aware executable coverage

SC10 parses canonical raw bytes from these executable units:

- standalone `.sh` and `.bash` files and files with a `bash`, `sh`, or `dash` shebang;
- `bash`, `sh`, `shell-script`, and `console` fenced blocks in `README` Markdown files and
`SKILL.md`; and
- statically literal `bash`/`sh`/`dash` `-c` or `-lc` programs and one-literal-operand `eval`
programs reached from a supported unit.

Parsing uses `tree-sitter==0.25.2` with `tree-sitter-bash==0.25.1`. Each unit is parsed from
immutable bytes through a bounded callable reader. The syntax frontend extracts commands from
lists, conditionals, loops, `case` statements, groups, subshells, pipelines, functions, and
command/process substitutions. It models only bounded literal assignments, exports, command-local
prefix assignments, and conservative control-flow joins. Tree-sitter supplies syntax, not
execution semantics: SC10 does not run shell, expand the host environment, or fall back to a regex,
raw-text, `shlex`, or custom shell parser.

Fixed command adapters recognize these dependency-source sinks:

| Ecosystem | Supported executable forms |
|---|---|
| npm, Yarn, pnpm | Registry/scoped-registry configuration changes and per-invocation registry options |
| pip | `config` index changes and per-invocation index options, including versioned `pip` and `python -m pip` |
| Poetry | Source add and repository configuration forms |
| Cargo | Literal `--config registries.<name>.index=...` overrides |
| uv | Index add, per-invocation index options, and named index values |
| Maven | One literal, unique, bundle-local `-s`/`--settings` reference, parsed with the direct Maven settings parser |

Path-qualified manager names and fixed transparent forms of `env`, `sudo`, `command`, `exec`,
`nohup`, `nice`, `timeout`, `setsid`, and `stdbuf` are supported. `corepack` and `npx` are supported
only when they name a literal recognized downstream manager. Named source environment variables for
the ecosystems above are findings only when an `export` or command-local assignment is proven to
reach the matching manager; a plain persistent shell assignment is state, not an environment
observation.

## Generated configuration

A completed heredoc or here-string can be parsed as generated direct configuration only when its
effective standard input and final standard-output write are structurally proven. Supported writes
use `>`, `>|`, or `>>` to a recognized direct-configuration path, or the fixed literal-output
`tee` form. Unquoted heredoc expansion is limited to modeled literal bindings and preserves a map
back to physical script bytes. A literal Maven settings reference may resolve one uniquely named
bundle-local XML file even when that file has a nonstandard basename.

Dynamic structure, an ambiguous target or wrapper, unsupported option arity, function shadowing,
malformed syntax, data piped into a shell, `xargs`, command-wrapper `env -S`, a heredoc piped to a
downstream writer, unsupported file-descriptor behavior, or an unproven generated-file write
produces a localized `dependency_source_parse_incomplete` limitation rather than a guessed finding
or clean result.
Dockerfile `RUN`, Make recipes, `.zsh`, `.ksh`, `.envrc`, unsupported shebangs, executable-only files
without a supported dialect, indented Markdown code, and shell fences outside `README`/`SKILL.md`
remain explicit `unscanned_executable_content` coverage limitations when recognized.

Shell analysis shares the direct-parser and output budgets and adds these ceilings:

This implementation does not parse commands or generated configuration. It structurally
recognizes executable shell files, executable inventory entries, Dockerfiles containing `RUN`,
Make recipes, and shell-like Markdown fences only to report their affected ranges as
`unscanned_executable_content`. Those ranges are incomplete coverage pending the syntax-aware
parser follow-up; their contents do not produce SC10 findings in this implementation.

The coverage notice does not guess whether a dependency-source command is present. It prevents a
recognized executable surface from being represented as fully analyzed and can raise an otherwise
`SAFE` report to `CAUTION` through the existing completeness policy. It does not change risk
scoring or recommendation policy.
| Resource | Limit |
|---|---:|
| Shell units per file | 256 |
| Parser calls per file | 512 |
| Parsed-byte revisits per file | 2 times the file size |
| Parsed shell bytes across the scan | 6,000,000 |
| CST visits per unit | 12 times unit bytes plus 1,024 |
| Nested literal-program depth | 2 |
| Retained shell IR across the scan | 50,000 |
| Source-map entries per file | 50,000 |
| Retained shell value bytes per file | 2,000,000 |
| Localized shell issues across the scan | 10,000 |

Parser cancellation, parser unavailability, or resource exhaustion is reported as localized
partial or failed work. It never silently converts an applicable unit into complete coverage. The
parser packages are required runtime dependencies; installation therefore requires compatible
distributions for the target Python and platform, and an ABI or semantic-version mismatch is
treated as parser unavailability.

## Security and product boundary

SC10 does not execute project content, commands, package managers, or generated files. It makes no
network, DNS, or reputation requests; maintains no user-managed allow/block/trust lists; and adds
no telemetry, service, or worker. Optional provider analysis may add presentation context, but it
cannot suppress or downgrade the deterministic SC10 evidence.
no telemetry, service, or worker. It is not a general shell interpreter. Optional provider analysis
may add presentation context, but it cannot suppress, downgrade, rewrite, or remove deterministic
SC10 evidence.

The result remains advisory. A `HIGH` finding or incomplete-coverage notice is evidence for the
user's review, not an installation decision or certification.
The result remains advisory. A `HIGH` finding or incomplete-coverage notice is evidence for human
review.
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ dependencies = [
"httpx>=0.28.0",
"packaging>=24.0",
"pyyaml>=6.0.1",
"tree-sitter==0.25.2",
"tree-sitter-bash==0.25.1",
"pydantic>=2.12.0",
"openai>=2.25.0",
"langgraph>=1.0.10",
Expand All @@ -61,6 +63,7 @@ dev = [
"pytest>=9.0.0",
"pytest-asyncio>=1.3.0",
"pytest-cov>=7.0.0",
"pytest-timeout>=2.4.0,<3.0.0",
"ruff>=0.15.0",
"mypy>=1.19.0",
"build>=1.4.0",
Expand Down
Loading
Loading