Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
2f1668b
fix: backport bounded host-language ownership for 2.13.1
mohgupta-ship-it Oct 17, 2026
77d9b7a
fix: backport validated Python quote ownership
mohgupta-ship-it Oct 17, 2026
1fcdb2e
fix: backport prose and report-string inspection correction
mohgupta-ship-it Oct 17, 2026
00b3c5c
test: preserve selected release marker accounting
mohgupta-ship-it Oct 17, 2026
0e96f34
fix: preserve shell uncertainty across Python report flows
mohgupta-ship-it Oct 17, 2026
6e37a9f
release: prepare SkillSpector 2.13.1 patch candidate
mohgupta-ship-it Oct 17, 2026
ec11319
fix: retain shell fallback when import proof serialization exhausts
mohgupta-ship-it Oct 19, 2026
c18d972
fix: bound bundled Python initialization AST traversal
mohgupta-ship-it Oct 19, 2026
ba2f7d8
fix(p2): backport bounded HTML comment matching for 2.13.1 (#452)
rng1995 Oct 19, 2026
0ee5910
backport: fix: report incomplete batch gap-fill analysis (#797)
rng1995 Oct 19, 2026
c2402d5
backport: fix(analyzer): reuse tool-misuse context indexes and honor …
rng1995 Oct 19, 2026
af12ea5
backport: fix: prevent report output symlink races (#798)
rng1995 Oct 19, 2026
2779db5
docs(release): add backported security fixes to 2.13.1 notes
rng1995 Oct 19, 2026
056bd05
backport: Reject unsupported skill options in registry mode (#840)
rng1995 Oct 20, 2026
897aa5d
backport: fix(mp3): recognize descriptive hardware reset states (#837)
rng1995 Oct 20, 2026
6769e5e
backport: fix(tm1): use the final repeated assignment target (#820)
rng1995 Oct 20, 2026
9cd3b6e
backport: fix(cli): reject shipped baselines in recursive multi-skill…
rng1995 Oct 20, 2026
d81a157
backport: fix(report): align recursive risk recommendations (#836)
rng1995 Oct 20, 2026
d69239e
docs(release): add backported CLI, report and analyzer fixes to 2.13.…
rng1995 Oct 20, 2026
b097a15
fix(static): stop code comparisons from reading as tag marker directi…
rng1995 Oct 6, 2026
64432f8
backport: recognize credential disclosure labels (#827)
yashrajp22 Oct 21, 2026
c0f2ce3
docs(release): include remaining customer gate fixes in 2.13.1
yashrajp22 Oct 21, 2026
079af09
release: defer credential labels and include marker prerequisite
yashrajp22 Oct 21, 2026
58b7d0d
fix(output): clear inherited report ACLs before writing
yashrajp22 Oct 21, 2026
1a18b97
docs(release): describe private report ACL protection
yashrajp22 Oct 21, 2026
41b97d2
fix(references): backport quoted version metadata handling from #525
yashrajp22 Oct 22, 2026
008ec15
fix(images): backport passive documentation image handling from #877
yashrajp22 Oct 22, 2026
beff668
fix(python): inspect imported initialization callables
yashrajp22 Oct 21, 2026
ebf6d4c
style(tests): format imported initialization fixtures
yashrajp22 Oct 21, 2026
31d44f2
docs(release): include customer metadata image and import fixes
yashrajp22 Oct 22, 2026
c528e51
docs(release): clarify maintenance fix description
yashrajp22 Oct 22, 2026
03c37f2
fix: preserve Markdown quote ownership and configuration field refere…
yashrajp22 Oct 22, 2026
d7c2856
test: distinguish Markdown delimiters from shell substitutions
yashrajp22 Oct 22, 2026
07dffdc
fix: preserve inline placeholders in list paragraphs
yashrajp22 Oct 22, 2026
7cfdc0f
fix: preserve closing quotes in shell heredoc strings
yashrajp22 Oct 22, 2026
272cd5f
fix: validate workbook printer metadata and inspect large worksheets
yashrajp22 Oct 22, 2026
026f335
test: isolate worksheet expansion budget boundaries
yashrajp22 Oct 22, 2026
9832c43
test: sort workbook regression imports
yashrajp22 Oct 22, 2026
6e06924
perf: bound and reuse security text projection work
yashrajp22 Oct 23, 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ ipython_config.py
# Environments
.env
.venv
.uv-cache/
env/
venv/
ENV/
Expand Down
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,29 @@
### 2.13.1 (Unreleased)
### Fixes
* Preserve quote boundaries in inline JSON examples and inline-code ownership in nested Markdown list paragraphs, avoiding false incomplete inspection while retaining shell-command checks.
* Treat explicitly documented dotted configuration fields as data when no bundled file matches; explicit file references and existing or ambiguous artifact matches retain normal reference accounting.
* Treat quoted version numbers in YAML version fields as metadata while preserving real numeric file references and missing-reference reporting. (#525)
* Avoid format-only AE1 findings for corroborated passive PNG/JPEG documentation images while retaining partial image coverage and genuine security findings. (#877)
* Inspect imported setup callables during the bounded bundled-helper proof so shell execution cannot incorrectly produce a complete/SAFE result. (#897)
* Stop ordinary code comparisons after removal verbs from causing false incomplete inspection while retaining real and ambiguous tag-marker directives. (#777)
* Retain incomplete inspection when a declared-marker instruction exposes an obfuscated curl command, using the minimal command-token prerequisite from #697.
* Preserve Python and Perl host-language ownership when checking embedded shell text, so ordinary regexes, quoted script text, and report strings do not falsely exhaust the shell parser. (#778, #784, #849)
* Recognize possessive Markdown prose without weakening inspection of executable shell text. (#849)
* Retain incomplete inspection when report-shaped strings can reach recognized shell execution, including through bundled helpers or module initialization.
* Retain conservative shell inspection instead of failing the analyzer when a deeply nested bundled Python expression exceeds the optional import proof's serialization limit.
* Report P2 hidden instructions only from text inside a single HTML comment, so visible text between comments or after `--!>`, `<!-->` or `<!--->`, and words such as `target`, `POSTGRES` or `SYSTEMATIC`, no longer count as hidden instructions. Camel-case and run-together directives stay detected. (#452)
* Exclude the release workflow's uv build cache from source distributions.
### Backported security fixes
* Reuse one line index for tool-misuse finding context, so files with thousands of tool-misuse matches are no longer re-read in full for each new finding line, and stop the TM1–TM4 checks and the Perl print helper when the per-file static analysis time limit is reached instead of only after the whole file is scanned. (#790)
* Keep core findings and findings from completed batches when the contributed batch scanner's non-English gap-fill pass fails part-way, and report the skill as incomplete (`gap_fill_status: "incomplete"` with failure reasons, at least a CAUTION recommendation, and batch exit code 2) instead of treating the failed pass as complete with no findings. (#797)
* Write report files (`scan`, `scan --recursive`, `scan --mcp-registry` and the contributed batch scanner's `--output`) and baselines atomically through an already-opened, no-follow parent directory, refusing symlinked or special-file destinations and symlinked parent directories. Report writes preserve effective filesystem write permissions; baselines also enforce their existing write-bit restrictions. Clear inherited access permissions, including macOS ACLs, before writing report content. File output fails before analysis on platforms without these operations, such as Windows; `skillspector baseline -o -` writes the baseline as JSON to stdout instead. (#798)
### Backported fixes
* Use the final value of a repeated Python assignment target in TM1, so a flag that was set true and then reassigned with `enabled, enabled = True, False` no longer makes a later `shell=enabled` call a HIGH finding. The earlier true value is dropped only when the final value is provably false, the right-hand side is passive and earlier bindings are proven safe in a straight-line module or function scope; other cases keep the existing conservative result. (#820)
* Reject `--use-shipped-baseline` with exit code 2, before any report is written, when `scan --recursive` detects one or more child skills, instead of silently ignoring it. Scan each sub-skill separately with `--use-shipped-baseline`; a single-skill scan still accepts it with `--recursive`. (#823)
* Report a complete recursive scan whose highest score is MEDIUM (21–50) as CAUTION, matching the child reports, instead of SAFE in the JSON `risk_recommendation` and the SARIF aggregate `riskAssessment.recommendation`. Exit codes are unchanged. (#836)
* Stop MP3 from reporting descriptions of a clocked hardware or state-machine "reset state" as memory manipulation, while keeping direct agent-reset instructions. Add a HIGH MP3 finding for an explicit instruction to treat or adopt (linked) state as the agent's memory or conversation state, unless that instruction is directly prohibited. (#837)
* Reject `scan --mcp-registry` with exit code 2, before the registry is read or a report is written, when it is combined with `--use-shipped-baseline`, `--transitive`, `--transitive-depth` (including an explicit `--transitive-depth 1`), `--transitive-allow-prefix` or `--transitive-deny-prefix`, instead of silently ignoring those options. (#840)
---
### 2.13.0 (Unreleased)
### Security and blocking fixes
* Selected release based on v2.12.0; includes 55 PRs, with #743 included through #734.
Expand Down
21 changes: 20 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ docker run --rm \
**Write a report to the host filesystem** by writing to the mounted directory:

```bash
docker run --rm \
docker run --rm --user "$(id -u):$(id -g)" \
-v "$PWD:/scan" \
skillspector scan ./my-skill/ --no-llm --format json --output report.json
```
Expand Down Expand Up @@ -196,6 +196,14 @@ See the [contrib guide](contrib/batch_scan/docs/) for details.
> contribute a more universal backend (Ollama, vLLM, or a different provider),
> PRs are very welcome.

### MCP Registry scans

Registry mode (`scan --mcp-registry`) rejects skill-only controls such as
`--use-shipped-baseline`, `--transitive`, `--transitive-depth`,
`--transitive-allow-prefix`, and `--transitive-deny-prefix` with exit code 2,
including an explicitly supplied default depth (`--transitive-depth 1`). Omit
these options for registry scans.

### Suppressing False Positives (baseline)

Suppress known/accepted findings so the risk score reflects only un-triaged
Expand Down Expand Up @@ -677,6 +685,17 @@ Options:
skillspector baseline <path> [-o FILE] [--no-llm] [--reason TEXT]
```

Report files and new baselines use private permissions (0600). Existing baselines
retain their access metadata when safely replaced; updates requiring in-place
writes are refused. All file output uses atomic replacement. The parent directory
must be writable; symlinked
parents and non-regular destinations such as devices are refused. On macOS,
ancestor directories must also be readable. File output requires POSIX no-follow
directory operations and is unavailable on Windows. Unsupported file output fails
before analysis. Omit `--output` for scan/batch stdout, or use
`skillspector baseline <path> -o -` for JSON on stdout, then redirect only to a
trusted destination. Run Docker with your user ID as above so you can read its files.

## Integrating SkillSpector

SkillSpector is built to be driven by other tools (CI pipelines, install gates, editor integrations). Its exit code and JSON output are a stable contract.
Expand Down
3 changes: 2 additions & 1 deletion contrib/batch_scan/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
from .api_pool import ApiKey, ApiKeyPool, PooledChatModel, create_api_key_pool_from_env
from .detection import detect_language, detect_skill_language
from .discovery import discover_skills
from .gap_fill import GapFillAnalyzer, GapFillFinding, GapFillResult, run_gap_fill
from .gap_fill import GapFillAnalyzer, GapFillError, GapFillFinding, GapFillResult, run_gap_fill
from .runner import run_one

__all__ = [
Expand All @@ -60,6 +60,7 @@
"detect_skill_language",
"discover_skills",
"GapFillAnalyzer",
"GapFillError",
"GapFillFinding",
"GapFillResult",
"is_language_compatible",
Expand Down
42 changes: 30 additions & 12 deletions contrib/batch_scan/batch_scan.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
from types import SimpleNamespace
from uuid import uuid4

from skillspector.file_output import require_secure_file_output, write_text_no_follow
from skillspector.logging_config import set_level

from .api_pool import create_api_key_pool_from_env
Expand Down Expand Up @@ -116,6 +117,7 @@ def _scan_skill(
require_llm: bool,
api_pool=None,
verbose: bool = False,
timeout: float = 90,
) -> tuple[dict[str, object], str | None, str]:
"""Scan a single skill through the full pipeline.

Expand All @@ -139,6 +141,7 @@ def _scan_skill(
detected_language=lang,
apply_gap_fill=True,
api_pool=api_pool,
timeout=timeout,
)

return entry, error_msg, rel_name
Expand All @@ -155,8 +158,10 @@ def _kill_worker_group(pid: int) -> None:
try:
subprocess.run(
["taskkill", "/PID", str(pid), "/T", "/F"],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
timeout=5, check=False,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
timeout=5,
check=False,
)
except (OSError, subprocess.TimeoutExpired):
pass
Expand Down Expand Up @@ -201,10 +206,16 @@ def _scan_skill_process(


def _scan_skill_bounded(
skill_dir: Path, root: Path, *, api_pool=None, timeout: float = 90,
startup_timeout: float = 90, **options
skill_dir: Path,
root: Path,
*,
api_pool=None,
timeout: float = 90,
startup_timeout: float = 90,
**options,
) -> tuple[dict[str, object], str | None, str]:
"""Enforce the wall-clock limit on actual work, including local analysis."""
options["timeout"] = timeout
owner = uuid4().hex
if api_pool is not None:
options["api_pool"] = SimpleNamespace(
Expand Down Expand Up @@ -365,6 +376,13 @@ def _print(*args: object, **kwargs: object) -> None:
)
args = parser.parse_args()

if args.output:
try:
require_secure_file_output()
except ValueError as exc:
_print(f"Error: {exc}", markup=False, file=sys.stderr)
sys.exit(2)

if args.verbose:
set_level("DEBUG")

Expand All @@ -389,8 +407,7 @@ def _print(*args: object, **kwargs: object) -> None:

# -- Header --------------------------------------------------------------
pool_note = (
f", [green]{api_pool.keys_configured} keys "
f"({api_pool.total_capacity} slots)[/green]"
f", [green]{api_pool.keys_configured} keys ({api_pool.total_capacity} slots)[/green]"
if api_pool
else ""
)
Expand Down Expand Up @@ -501,13 +518,10 @@ def _print(*args: object, **kwargs: object) -> None:
f"{snap['total_requests_served']} requests served",
]
if snap.get("peak_active_requests", 0) > 0:
_parts.append(
f"peak {snap['peak_active_requests']}/{snap['total_capacity']} slots"
)
_parts.append(f"peak {snap['peak_active_requests']}/{snap['total_capacity']} slots")
if snap.get("rate_limits_hit", 0) > 0:
_parts.append(
f"{snap['rate_limits_hit']} rate-limit(s), "
f"{snap['retry_successes']} retried"
f"{snap['rate_limits_hit']} rate-limit(s), {snap['retry_successes']} retried"
)
_parts.append(f"{snap['keys_configured']} keys")
_print(f"\n[dim]API Pool: {', '.join(_parts)}[/dim]")
Expand All @@ -522,7 +536,11 @@ def _print(*args: object, **kwargs: object) -> None:
report_body = format_markdown(results)

if args.output:
args.output.write_text(report_body, encoding="utf-8")
try:
write_text_no_follow(args.output, report_body)
except (OSError, ValueError) as exc:
_print(f"Error: {exc}", markup=False, file=sys.stderr)
sys.exit(2)
_print(f"\n[green]Batch report saved to:[/green] {display(args.output)}")
else:
if fmt == "terminal":
Expand Down
19 changes: 16 additions & 3 deletions contrib/batch_scan/docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,8 @@ python -m contrib.batch_scan.batch_scan ./tests/fixtures/ -f json -o batch.json

Key differences in batch output:
- `scan_mode: "multilingual-enhanced"` — provenance marker
- `enhancements.gap_fill_applied` — true if LLM gap-fill was used
- `enhancements.gap_fill_applied` — true only if every gap-fill batch completed;
partial passes can still have `gap_fill_findings > 0`
- `enhancements.english_keyword_rules_skipped` — count of static rules bypassed
- `skill.language` — detected language tag

Expand Down Expand Up @@ -313,7 +314,19 @@ python -m contrib.batch_scan.batch_scan ./tests/fixtures/ --no-require-llm --no-
|------|---------|
| 0 | All safe (no HIGH/CRITICAL) |
| 1 | ≥1 skill has HIGH or CRITICAL risk |
| 2 | Scan errors occurred |
| 2 | Scan errors or incomplete gap-fill occurred; `skills[].error` or `enhancements.gap_fill_error` explains the failure |


Gap-fill uses the remaining per-skill budget after core analysis, leaving up to
five seconds for reporting before the worker's 90-second hard limit. Its pooled
calls receive that remaining budget; the provider pool otherwise uses its
caller's timeout. Invalid responses can take up to four attempts within it.
Failed setup and failed batches retain core findings and completed gap-fill
findings. An incomplete pass records `enhancements.gap_fill_status: "incomplete"`,
`gap_fill_error_reasons`, and `enhancements.gap_fill_error`, and exits 2 even when
its ledger outcome is nonfatal. This deadline covers gap-fill only. Core analysis
keeps its own budget and can reach the worker limit even when providers respect
request timeouts. The worker limit also applies if a provider ignores its timeout.

CI usage:

Expand Down Expand Up @@ -415,7 +428,7 @@ tests/
├── test_monkeypatch_fragility.py ← Issue #2: 26 tests (guard verification)
├── tests-pro/
│ ├── test_api_pool.py ← Issue #3: 45 tests (acquire/backoff)
│ ├── test_gap_fill.py ← Issue #3: 41 tests (JSON parsing)
│ ├── test_gap_fill.py ← Issue #3: 29 tests (JSON parsing)
│ ├── test_runner_patches.py ← Issue #3: 24 tests (context manager)
│ └── test_annotation.py ← Issue #3: 10 tests (language compat)
└── docs/
Expand Down
Loading
Loading