From 29c66732e19e9d91346942d67d0bc41a63bccd28 Mon Sep 17 00:00:00 2001 From: Keshav Pradeep <32313895+keshprad@users.noreply.github.com> Date: Tue, 14 Jul 2026 14:20:45 -0700 Subject: [PATCH] Sync OSS release snapshot Refresh the public SkillSpector tree from the internal OSS release branch release/oss-2026-07-14 at a9a92062c299c94fdc078625b3e3cd435fa19e08. The snapshot is generated by scripts/create-oss-release.sh, which removes internal-only files and commits a single orphan-branch tree for public publication. Changes: - Bump public package metadata from 2.3.12 to 2.3.13. - Add development documentation for public GitHub and internal GitLab CI coverage. - Carry the YARA packaged-rule handling fix that compiles decoded rules in memory. - Refresh related YARA and event-loop regression tests. Verification: - scripts/create-oss-release.sh release/oss-2026-07-14 with PYTHONPATH pinned to the fresh clone (includes make test-unit: 1312 passed, 12 skipped, 34 deselected, 6 xfailed). - git diff --check origin/main. Signed-off-by: Keshav Pradeep <32313895+keshprad@users.noreply.github.com> --- docs/DEVELOPMENT.md | 28 ++++++++ pyproject.toml | 2 +- .../nodes/analyzers/static_yara.py | 71 ++++++++----------- tests/nodes/analyzers/test_static_yara.py | 11 ++- tests/unit/test_llm_utils.py | 1 + uv.lock | 2 +- 6 files changed, 65 insertions(+), 50 deletions(-) diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 65bdc9a8..048d0806 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -221,6 +221,34 @@ Optional state keys: `mode`, `model_config`, `output_format`, `use_llm`. The res - **Commands**: `make test`, `make test-cov`. - **Key tests**: [test_graph.py](../tests/integration/test_graph.py) invokes the graph and asserts `findings`, `sarif_report`, `risk_score`, `report_body`; [test_input_handler.py](../tests/unit/test_input_handler.py) covers directory, zip, and single-file resolution; [test_resolve_input.py](../tests/nodes/test_resolve_input.py) covers the resolve_input node; [test_build_context.py](../tests/nodes/test_build_context.py) asserts `component_metadata` and `has_executable_scripts`. +### CI coverage: public GitHub and internal GitLab + +SkillSpector uses its public GitHub Actions workflow as the contributor-facing +quality gate and runs an additional validation pipeline in NVIDIA's internal +GitLab. The two pipelines intentionally share the core checks, while each also +has checks suited to its environment. + +| Check | Public GitHub CI | Internal GitLab CI | +|-------|------------------|--------------------| +| Trigger | Pull requests to `main` and pushes to `main` | Merge requests targeting `main` and pushes to the default branch | +| Runtime | Python 3.12 with `uv` on GitHub-hosted Ubuntu runners | Python 3.12 with `uv` in a container on internal Kubernetes runners | +| Lint and formatting | Ruff lint and format checks | The same Ruff lint and format checks | +| Unit tests | Non-integration, non-provider tests with coverage | The same unit-test set with Cobertura coverage artifacts | +| Integration tests | Not run | Full-graph integration suite; these tests may call configured LLM providers | +| Live provider tests | Not run | Optional manual tests against OpenAI, Anthropic, and NVIDIA Build using masked CI credentials | +| Docker smoke test | Runs when Docker- or application-related files change and uploads smoke reports | Runs for the same categories of changes with Docker-in-Docker and preserves smoke reports | +| Static analysis | OpenSSF Scorecard runs in a separate public workflow | SonarQube runs after unit tests and is currently non-blocking | +| Contribution policy | DCO sign-off check on pull requests | No separate DCO job | +| Automated review | No review bot job is defined in the workflow | CodeRabbit is connected through an external integration/webhook, not a runner job | + +The internal pipeline therefore adds coverage for the full application flow, +live provider connectivity, and SonarQube analysis. Its default-branch pipeline +rechecks the exact commit that landed after a merge. Live provider testing is +manual so it only sends requests when a maintainer chooses to run it; missing +credentials produce a warning, while invalid credentials or provider failures +fail the corresponding test. SonarQube is informational today and does not +block a merge request. + --- ## 8. Data models diff --git a/pyproject.toml b/pyproject.toml index e37a8a51..4ad1c5f7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "skillspector" -version = "2.3.12" +version = "2.3.13" 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" diff --git a/src/skillspector/nodes/analyzers/static_yara.py b/src/skillspector/nodes/analyzers/static_yara.py index 68c0b92f..9d753d5d 100644 --- a/src/skillspector/nodes/analyzers/static_yara.py +++ b/src/skillspector/nodes/analyzers/static_yara.py @@ -26,7 +26,6 @@ import binascii import hashlib from pathlib import Path -from tempfile import TemporaryDirectory import yara @@ -94,63 +93,56 @@ def _rule_namespace(rule_file: Path) -> str: return rule_file.stem -def _materialize_rule_file( - rule_file: Path, temp_dir: Path | None = None, namespace: str | None = None -) -> Path: - """Return a compile-ready rule path, decoding embedded sources when needed.""" +def _read_rule_source(rule_file: Path) -> str: + """Read a YARA rule source, decoding embedded packaged rules when needed.""" if not rule_file.name.endswith(_ENCODED_RULE_SUFFIXES): - return rule_file - if temp_dir is None: - raise ValueError("temp_dir is required for encoded rule files") + return rule_file.read_text(encoding="utf-8") encoded_source = rule_file.read_text(encoding="utf-8") - decoded_source = base64.b64decode("".join(encoded_source.split())).decode("utf-8") - temp_name = (namespace or _rule_namespace(rule_file)).replace("/", "__") - temp_file = temp_dir / f"{temp_name}.yar" - temp_file.write_text(decoded_source, encoding="utf-8") - return temp_file + return base64.b64decode("".join(encoded_source.split())).decode("utf-8") def _build_namespace_map( rule_files: list[Path], temp_dir: Path | None = None ) -> tuple[dict[str, str], int]: - """Build a {namespace: filepath} dict and count malformed encoded files.""" - filepaths: dict[str, str] = {} + """Build a {namespace: source} dict and count malformed rule files.""" + del temp_dir + sources: dict[str, str] = {} skipped = 0 for rf in rule_files: ns = _rule_namespace(rf) - if ns in filepaths: + if ns in sources: ns = f"{rf.parent.name}/{ns}" try: - filepaths[ns] = str(_materialize_rule_file(rf, temp_dir, ns)) + sources[ns] = _read_rule_source(rf) except (binascii.Error, UnicodeDecodeError, ValueError) as exc: skipped += 1 logger.debug("%s: skipping malformed encoded rule %s: %s", ANALYZER_ID, rf, exc) - return filepaths, skipped + return sources, skipped -def _compile_rules(filepaths: dict[str, str]) -> tuple[yara.Rules | None, int]: - """Compile YARA rules from a namespace map. Falls back to per-file compilation on error. +def _compile_rules(sources: dict[str, str]) -> tuple[yara.Rules | None, int]: + """Compile YARA rules from a namespace map. Falls back to per-source compilation on error. Returns (compiled_rules, skipped_count). """ try: - return yara.compile(filepaths=filepaths), 0 + return yara.compile(sources=sources), 0 except yara.SyntaxError: pass - logger.debug("%s: bulk compile failed, falling back to per-file compilation", ANALYZER_ID) + logger.debug("%s: bulk compile failed, falling back to per-source compilation", ANALYZER_ID) good: dict[str, str] = {} skipped = 0 - for ns, fp in filepaths.items(): + for ns, source in sources.items(): try: - yara.compile(filepath=fp) - good[ns] = fp + yara.compile(source=source) + good[ns] = source except (yara.SyntaxError, yara.Error) as exc: skipped += 1 - logger.debug("%s: skipping %s: %s", ANALYZER_ID, fp, exc) + logger.debug("%s: skipping %s: %s", ANALYZER_ID, ns, exc) - compiled = yara.compile(filepaths=good) if good else None + compiled = yara.compile(sources=good) if good else None return compiled, skipped @@ -176,22 +168,19 @@ def _load_rules(extra_dir: Path | None = None) -> yara.Rules | None: if _compiled_rules is not None and _rules_hash == current_hash: return _compiled_rules - with TemporaryDirectory() as temp_dir_name: - temp_dir = Path(temp_dir_name) - filepaths, materialize_skipped = _build_namespace_map(rule_files, temp_dir) + sources, materialize_skipped = _build_namespace_map(rule_files) + compiled, compile_skipped = _compile_rules(sources) + skipped = materialize_skipped + compile_skipped - compiled, compile_skipped = _compile_rules(filepaths) - skipped = materialize_skipped + compile_skipped - - if compiled is None: - logger.warning("%s: failed to compile any YARA rules", ANALYZER_ID) - return None + if compiled is None: + logger.warning("%s: failed to compile any YARA rules", ANALYZER_ID) + return None - _compiled_rules = compiled - _rules_hash = current_hash - loaded = len(filepaths) - compile_skipped - logger.info("%s: compiled %d YARA rule file(s) (%d skipped)", ANALYZER_ID, loaded, skipped) - return compiled + _compiled_rules = compiled + _rules_hash = current_hash + loaded = len(sources) - compile_skipped + logger.info("%s: compiled %d YARA rule file(s) (%d skipped)", ANALYZER_ID, loaded, skipped) + return compiled def _extract_match_strings(match: yara.Match) -> tuple[int, str | None]: diff --git a/tests/nodes/analyzers/test_static_yara.py b/tests/nodes/analyzers/test_static_yara.py index d15826e3..b1472a3c 100644 --- a/tests/nodes/analyzers/test_static_yara.py +++ b/tests/nodes/analyzers/test_static_yara.py @@ -462,8 +462,7 @@ def test_build_namespace_map_decodes_encoded_rules(self, tmp_path): encoded_file = tmp_path / "encoded.yar.b64" encoded_file.write_text(encoded_source) ns_map, skipped = static_yara._build_namespace_map([encoded_file], tmp_path) - decoded_path = Path(ns_map["encoded"]) - assert decoded_path.read_text() == "rule encoded { condition: false }" + assert ns_map["encoded"] == "rule encoded { condition: false }" assert skipped == 0 def test_build_namespace_map_keeps_encoded_namespace_collisions_apart(self, tmp_path): @@ -482,11 +481,9 @@ def test_build_namespace_map_keeps_encoded_namespace_collisions_apart(self, tmp_ [first_file, second_file], materialized_dir ) - first_path = Path(ns_map["malware"]) - second_path = Path(ns_map["extra/malware"]) - assert first_path != second_path - assert first_path.read_text() == "rule first { condition: false }" - assert second_path.read_text() == "rule second { condition: false }" + assert set(ns_map) == {"malware", "extra/malware"} + assert ns_map["malware"] == "rule first { condition: false }" + assert ns_map["extra/malware"] == "rule second { condition: false }" assert skipped == 0 def test_build_namespace_map_skips_malformed_encoded_rules(self, tmp_path): diff --git a/tests/unit/test_llm_utils.py b/tests/unit/test_llm_utils.py index 053315a7..7698d47b 100644 --- a/tests/unit/test_llm_utils.py +++ b/tests/unit/test_llm_utils.py @@ -499,6 +499,7 @@ def test_run_async_without_running_loop(self) -> None: def test_run_async_with_running_loop(self) -> None: """Test run_async works correctly even when there is already a running event loop. + This regression test covers the scenario where SkillSpector is invoked from environments like Jupyter Notebooks, FastAPI, or LangGraph Studio that already have an active event loop. diff --git a/uv.lock b/uv.lock index 40318199..e8355733 100644 --- a/uv.lock +++ b/uv.lock @@ -2660,7 +2660,7 @@ wheels = [ [[package]] name = "skillspector" -version = "2.3.12" +version = "2.3.13" source = { editable = "." } dependencies = [ { name = "boto3" },