Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
0b4d6ce
Fix uv venv py-version
lrecknagel Jun 3, 2026
ffdee3d
feat: adds dockerfile to run it without installing python
jcchavezs Jun 9, 2026
cb26930
Constrain supported Python versions
keshprad Jun 10, 2026
0a7d493
chore: bump version to 2.1.3
keshprad Jun 10, 2026
71ec746
Merge upstream main into PR branch
keshprad Jun 10, 2026
1a7bf02
Merge pull request #13 from lrecknagel/patch-1
keshprad Jun 10, 2026
6a5817f
fix: use official Python Docker base
keshprad Jun 13, 2026
ed1e8ce
docs: simplify Docker usage examples
keshprad Jun 13, 2026
770dff0
chore: add Docker build ignore file
keshprad Jun 13, 2026
986cb30
Merge pull request #18 from jcchavezs/adds_docker
keshprad Jun 13, 2026
5214d0d
Sync OSS release snapshot
keshprad Jun 13, 2026
cff7ecc
Merge pull request #44 from NVIDIA/keshavp/oss-release-2026-06-13
keshprad Jun 13, 2026
a8d284f
Reject invalid skill paths
fallintoplace Jun 13, 2026
09e2ac3
fix(docker): install git for repository scans
medhdj Jun 15, 2026
39dab6e
test: add Docker smoke suite
keshprad Jun 16, 2026
ac9a03c
fix(docker): install git for repository scans
keshprad Jun 16, 2026
afa9529
Sync OSS release snapshot
keshprad Jun 16, 2026
42f1db6
Sync OSS release snapshot - NVIDIA/keshavp/oss-release-2026-06-15
keshprad Jun 16, 2026
e2af5f4
test: preserve default graph invocation
keshprad Jun 16, 2026
0e68afe
Revert "test: preserve default graph invocation"
keshprad Jun 16, 2026
4f9c660
Reject invalid skill paths - Merge pull request #45 from fallintoplac…
keshprad Jun 16, 2026
536c86e
Sync OSS release snapshot
keshprad Jun 16, 2026
e4cb70f
Sync OSS release snapshot - Merge pull request #80 from NVIDIA/keshav…
keshprad Jun 16, 2026
77c55c6
Sync OSS release snapshot
keshprad Jun 16, 2026
a5092dd
Sync OSS release snapshot - Merge pull request #85 from NVIDIA/keshav…
keshprad Jun 16, 2026
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
22 changes: 22 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
.git
.venv
env
venv
__pycache__
*.pyc
.pytest_cache
.ruff_cache
.mypy_cache
.coverage
htmlcov
dist
build
*.egg-info
.env
.env.*
!.env.example
*.log
tmp
temp
.skillspector
.DS_Store
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ venv.bak/
tmp/
temp/
.skillspector/
.provider-test-missing-keys

# API Keys (never commit!)
.env.local
Expand Down
20 changes: 20 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
FROM python:3.12-slim-bookworm AS builder

WORKDIR /app
COPY pyproject.toml README.md ./
COPY src/ src/
RUN python -m venv .venv
RUN .venv/bin/pip install --no-cache-dir .

FROM python:3.12-slim-bookworm

RUN apt-get update \
&& apt-get install --no-install-recommends -y git ca-certificates \
&& rm -rf /var/lib/apt/lists/*

COPY --from=builder /app/.venv /app/.venv

ENV PATH="/app/.venv/bin:$PATH"
WORKDIR /scan

ENTRYPOINT ["skillspector"]
69 changes: 64 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: help install install-dev langgraph-dev test test-unit test-integration test-cov test-ci lint lint-fix format format-check clean build
.PHONY: help install install-dev langgraph-dev test test-unit test-provider openai anthropic nv_build test-integration test-cov test-ci lint lint-fix format format-check clean build docker-build docker-smoke

# Prefer uv if available, else use pip (set when Makefile is parsed)
UV := $(shell command -v uv 2>/dev/null)
Expand All @@ -8,6 +8,24 @@ UV := $(shell command -v uv 2>/dev/null)
# make langgraph-dev LANGGRAPH_STUDIO_URL=https://your-studio.example
LANGGRAPH_STUDIO_URL = https://smith.langchain.com

PROVIDER_TEST_SELECTION := $(filter openai anthropic nv_build,$(MAKECMDGOALS))
ifneq ($(PROVIDER_TEST_SELECTION),)
PROVIDER_TEST_PROVIDERS := $(PROVIDER_TEST_SELECTION)
PROVIDER_TEST_TARGETS :=
ifneq ($(filter openai,$(PROVIDER_TEST_SELECTION)),)
PROVIDER_TEST_TARGETS += tests/provider/test_provider_endpoint.py::test_openai_provider_makes_live_structured_request
endif
ifneq ($(filter anthropic,$(PROVIDER_TEST_SELECTION)),)
PROVIDER_TEST_TARGETS += tests/provider/test_provider_endpoint.py::test_anthropic_provider_makes_live_structured_request
endif
ifneq ($(filter nv_build,$(PROVIDER_TEST_SELECTION)),)
PROVIDER_TEST_TARGETS += tests/provider/test_provider_endpoint.py::test_nv_build_provider_makes_live_structured_request
endif
else
PROVIDER_TEST_PROVIDERS := openai anthropic nv_build
PROVIDER_TEST_TARGETS := tests/provider
endif

# Default target. All targets assume the virtual env is already created and activated.
help:
@echo "Available targets (venv must be created and activated first):"
Expand All @@ -16,6 +34,7 @@ help:
@echo " make langgraph-dev - Run LangGraph dev server (Studio at \$$LANGGRAPH_STUDIO_URL)"
@echo " make test - Run unit + integration tests"
@echo " make test-unit - Run unit tests only (no LLM calls)"
@echo " make test-provider [openai|anthropic|nv_build] - Run live provider tests"
@echo " make test-integration - Run integration tests only (invokes full graph, may call LLMs)"
@echo " make test-cov - Run tests with coverage report"
@echo " make lint - Run linters (ruff only)"
Expand All @@ -24,6 +43,8 @@ help:
@echo " make format-check - Check code formatting with ruff"
@echo " make clean - Remove build artifacts and cache files"
@echo " make build - Build the package"
@echo " make docker-build - Build the Docker image"
@echo " make docker-smoke - Build and smoke test the Docker image"

install:
@if [ -n "$(UV)" ]; then uv sync; else pip install -e .; fi
Expand All @@ -38,21 +59,51 @@ langgraph-dev:
# Run unit + integration tests
test: test-unit test-integration

# Run unit tests only (excludes integration marker)
# Run unit tests only (excludes provider and integration markers)
test-unit:
pytest -m "not integration" tests/
pytest -m "not integration and not provider" tests/

# Run live provider tests (requires provider-specific API keys)
test-provider:
@missing_keys=0; \
if [ -n "$${PROVIDER_TEST_MISSING_KEYS_FILE:-}" ]; then \
rm -f "$$PROVIDER_TEST_MISSING_KEYS_FILE"; \
fi; \
for provider in $(PROVIDER_TEST_PROVIDERS); do \
case "$$provider" in \
openai) env_name=OPENAI_API_KEY; label=OpenAI ;; \
anthropic) env_name=ANTHROPIC_API_KEY; label=Anthropic ;; \
nv_build) env_name=NVIDIA_INFERENCE_KEY; label="NV Build" ;; \
esac; \
eval "value=\$${$${env_name}:-}"; \
if [ -z "$$value" ]; then \
echo "WARNING: $$env_name is not set; $$label provider test will be skipped"; \
missing_keys=1; \
fi; \
done; \
pytest -m provider $(PROVIDER_TEST_TARGETS); \
pytest_status=$$?; \
if [ "$$pytest_status" -ne 0 ]; then \
exit "$$pytest_status"; \
fi; \
if [ "$$missing_keys" -ne 0 ] && [ -n "$${PROVIDER_TEST_MISSING_KEYS_FILE:-}" ]; then \
printf "missing provider keys\n" > "$$PROVIDER_TEST_MISSING_KEYS_FILE"; \
fi

openai anthropic nv_build:
@:

# Run integration tests only (invokes full graph, may call LLMs)
test-integration:
pytest -m integration tests/

# Run tests with coverage
test-cov:
pytest --cov=src/skillspector --cov-report=html --cov-report=term-missing tests/
pytest -m "not integration and not provider" --cov=src/skillspector --cov-report=html --cov-report=term-missing tests/

# Run tests with coverage for CI (Cobertura XML + terminal)
test-ci:
pytest --cov=src/skillspector --cov-report=term-missing --cov-report=xml tests/
pytest -m "not integration and not provider" --cov=src/skillspector --cov-report=term-missing --cov-report=xml tests/

# Run linters (fast: ruff only)
lint:
Expand Down Expand Up @@ -94,3 +145,11 @@ clean:
build: clean
python -m build

# Build the Docker image
docker-build:
docker build -t skillspector .

# Build and smoke test the Docker image
docker-smoke: docker-build
tests/docker/smoke.sh

62 changes: 60 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,64 @@ make install
make install-dev
```

### Docker (no Python required)

Run SkillSpector without installing Python by building it locally from the included [Dockerfile](Dockerfile). The image is based on the Docker Official Python `3.12-slim-bookworm` image.

**Build the image:**

```bash
make docker-build
# or: docker build -t skillspector .
```

**Scan a local directory** by mounting your current directory into `/scan`, the container's working directory:

```bash
docker run --rm -v "$PWD:/scan" skillspector scan ./my-skill/ --no-llm
```

**Scan with LLM analysis** by passing credentials with a local `.env` file:

```bash
cat > .env <<'EOF'
SKILLSPECTOR_PROVIDER=anthropic
ANTHROPIC_API_KEY=sk-ant-...
EOF
```

```bash
docker run --rm \
-v "$PWD:/scan" \
--env-file .env \
skillspector scan ./my-skill/
```

Or pass credentials directly from your shell environment:

```bash
docker run --rm \
-v "$PWD:/scan" \
-e SKILLSPECTOR_PROVIDER=anthropic \
-e ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \
skillspector scan ./my-skill/
```

**Write a report to the host filesystem** by writing to the mounted directory:

```bash
docker run --rm \
-v "$PWD:/scan" \
skillspector scan ./my-skill/ --no-llm --format json --output report.json
```

**Optional alias** for repeated static scans:

```bash
alias skillspector-docker='docker run --rm -v "$PWD:/scan" skillspector'
skillspector-docker scan ./my-skill/ --no-llm
```

### Basic Usage

```bash
Expand Down Expand Up @@ -87,7 +145,7 @@ local OpenAI-compatible servers (Ollama, vLLM, llama.cpp) and managed
inference gateways.

| Provider (`SKILLSPECTOR_PROVIDER`) | Credential env var | Endpoint | Default model |
|----------|----|----|----|
| ---------- | ---- | ---- | ---- |
| `openai` | `OPENAI_API_KEY` (+ optional `OPENAI_BASE_URL`) | api.openai.com (or any OpenAI-compatible URL) | `gpt-5.4` |
| `anthropic` | `ANTHROPIC_API_KEY` | api.anthropic.com | `claude-opus-4-6` |
| `nv_build` | `NVIDIA_INFERENCE_KEY` | build.nvidia.com | `deepseek-ai/deepseek-v4-flash` |
Expand Down Expand Up @@ -344,7 +402,7 @@ Issues (2)
| `OPENAI_BASE_URL` | Override the OpenAI endpoint (e.g. point at Ollama). | Optional |
| `ANTHROPIC_API_KEY` | Credential for the Anthropic provider (`SKILLSPECTOR_PROVIDER=anthropic`). | Required for LLM analysis when `SKILLSPECTOR_PROVIDER=anthropic` |
| `SKILLSPECTOR_MODEL` | Override the active provider's default model. See the LLM Analysis table for each provider's default. | Optional |
| `SKILLSPECTOR_MODEL_REGISTRY` | Override the bundled per-provider YAML registry (`src/skillspector/providers/<provider>.yaml`) with a custom path. | Optional |
| `SKILLSPECTOR_MODEL_REGISTRY` | Override the bundled per-provider YAML registry (`src/skillspector/providers/<provider>/model_registry.yaml`) with a custom path. | Optional |
| `SKILLSPECTOR_LOG_LEVEL` | Log level: `DEBUG`, `INFO`, `WARNING`, `ERROR` (default: `WARNING`). | Optional |

### CLI Options
Expand Down
13 changes: 13 additions & 0 deletions docs/DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,19 @@ Copy [.env.example](../.env.example) to `.env` in the project root and set value
| `ANTHROPIC_API_KEY` | Credential for `SKILLSPECTOR_PROVIDER=anthropic`. | `sk-ant-...` |
| `SKILLSPECTOR_MODEL` | Override the active provider's bundled default model (see [README.md](../README.md) for per-provider defaults). | `gpt-5.2` |

### Live provider tests

The manual `test-provider` CI job and local `make test-provider` target perform live requests against provider default endpoints. Missing provider keys print a `WARNING:` line before pytest runs and skip that provider. In CI, missing keys also make the manual job exit with the configured warning code so GitLab displays the job as passed with warnings; if a key is present but invalid, or the provider request fails, the corresponding test fails.

| Command | Required env var | Default URL | Optional model override |
|---------|------------------|-------------|-------------------------|
| `make test-provider openai` | `OPENAI_API_KEY` | `https://api.openai.com/v1` | `SKILLSPECTOR_OPENAI_TEST_MODEL` |
| `make test-provider anthropic` | `ANTHROPIC_API_KEY` | `https://api.anthropic.com` | `SKILLSPECTOR_ANTHROPIC_TEST_MODEL` |
| `make test-provider nv_build` | `NVIDIA_INFERENCE_KEY` | `https://integrate.api.nvidia.com/v1` | `SKILLSPECTOR_NV_BUILD_TEST_MODEL` |
| `make test-provider` | Any/all of the provider keys above | All provider default URLs above | Any/all provider model overrides above |

Base URL env vars are not needed for live provider tests; the tests intentionally use provider defaults.

### Constants, token budgets, and LLM

- **Constants** ([constants.py](../src/skillspector/constants.py)): `_SKILLSPECTOR_DEFAULT_MODEL`, `MODEL_CONFIG` (per-node model selection), `MAX_INPUT_TOKENS_PCT` (0.75), `DEFAULT_CONTEXT_LENGTH` (128k fallback).
Expand Down
13 changes: 9 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ build-backend = "hatchling.build"

[project]
name = "skillspector"
version = "2.1.2"
version = "2.2.3"
description = "SkillSpector: Security scanner for AI agent skills (Claude Code, Cursor, and similar). Scans skills for vulnerabilities, malicious patterns, and security risks before installation. Supports Git repos, URLs, zips, and local directories; runs static pattern checks and optional LLM semantic analysis; outputs terminal, JSON, and Markdown reports with risk scoring."
readme = "README.md"
license = "Apache-2.0"
requires-python = ">=3.12"
requires-python = ">=3.12,<3.15"
keywords = [
"security",
"ai-agents",
Expand All @@ -25,6 +25,7 @@ classifiers = [
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Topic :: Security",
"Topic :: Software Development :: Quality Assurance",
]
Expand All @@ -38,6 +39,7 @@ dependencies = [
"openai>=2.25.0",
"langgraph>=1.0.10",
"langgraph-cli[inmem]>=0.4.14",
"langchain-anthropic>=1.4.5",
"langchain-core>=1.2.17",
"langchain-openai>=1.1.10",
"langsmith>=0.7.30",
Expand Down Expand Up @@ -97,5 +99,8 @@ source = ["src/skillspector"]
[tool.pytest.ini_options]
testpaths = ["tests"]
asyncio_mode = "auto"
markers = ["integration: end-to-end tests that invoke the full graph (may call LLMs)"]
addopts = "-m 'not integration'"
markers = [
"integration: end-to-end tests that invoke the full graph (may call LLMs)",
"provider: live OpenAI/Anthropic/NVIDIA Build provider endpoint tests",
]
addopts = "-m 'not integration and not provider'"
12 changes: 10 additions & 2 deletions src/skillspector/llm_analyzer_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
from dataclasses import dataclass, field
from typing import Literal

from langchain_core.messages import BaseMessage
from pydantic import BaseModel, Field

from skillspector.llm_utils import get_chat_model
Expand Down Expand Up @@ -191,6 +192,13 @@ def number_lines(content: str, start_line: int = 1) -> str:
return "\n".join(f"L{start_line + i:0>{width}}: {line}" for i, line in enumerate(lines))


def _message_text(response: object) -> str:
"""Extract provider-normalized text from a LangChain chat response."""
if not isinstance(response, BaseMessage):
raise TypeError(f"Expected BaseMessage from chat model, got {type(response).__name__}")
return str(response.text)


BASE_ANALYSIS_PROMPT = """\
{analyzer_prompt}

Expand Down Expand Up @@ -355,7 +363,7 @@ def run_batches(
if self._structured_llm:
response = self._structured_llm.invoke(prompt)
else:
response = self._llm.invoke(prompt).content
response = _message_text(self._llm.invoke(prompt))
logger.debug("LLM response for %s", batch.file_label)
parsed = self.parse_response(response, batch)
results.append((batch, parsed))
Expand Down Expand Up @@ -390,7 +398,7 @@ async def _process(batch: Batch) -> tuple[Batch, list]:
if self._structured_llm:
response = await self._structured_llm.ainvoke(prompt)
else:
response = (await self._llm.ainvoke(prompt)).content
response = _message_text(await self._llm.ainvoke(prompt))
logger.debug("LLM response for %s", batch.file_label)
return (batch, self.parse_response(response, batch))

Expand Down
Loading