diff --git a/.github/workflows/skill-release.yml b/.github/workflows/skill-release.yml index ef3c6c0..fa80028 100644 --- a/.github/workflows/skill-release.yml +++ b/.github/workflows/skill-release.yml @@ -530,6 +530,9 @@ jobs: echo " [Dry-run] Removed test signatures from release staging" fi + # --- Verify staged runtime import closure before archiving --- + python3 scripts/ci/verify_skill_release_import_closure.py "${inner_dir}" + # --- Create zip preserving directory structure --- zip_name="${skill_name}-v${version}.zip" (cd "${staging_dir}" && zip -qr "${OLDPWD}/${out_assets}/${zip_name}" .) @@ -892,6 +895,9 @@ jobs: cp "$SKILL_PATH/skill.json" "$INNER_DIR/skill.json" + # --- Verify staged runtime import closure before archiving --- + python3 scripts/ci/verify_skill_release_import_closure.py "$INNER_DIR" + # --- Create zip preserving directory structure --- ZIP_NAME="${SKILL_NAME}-v${VERSION}.zip" (cd "$STAGING_DIR" && zip -qr "$OLDPWD/release-assets/$ZIP_NAME" .) diff --git a/scripts/ci/test_verify_skill_release_import_closure.py b/scripts/ci/test_verify_skill_release_import_closure.py new file mode 100644 index 0000000..6a4746f --- /dev/null +++ b/scripts/ci/test_verify_skill_release_import_closure.py @@ -0,0 +1,51 @@ +from __future__ import annotations + +import importlib.util +import sys +import tempfile +import unittest +from pathlib import Path + + +def _load_module(): + module_path = Path(__file__).with_name("verify_skill_release_import_closure.py") + spec = importlib.util.spec_from_file_location("verify_skill_release_import_closure", module_path) + if spec is None or spec.loader is None: + raise RuntimeError(f"Unable to load {module_path}") + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + return module + + +class VerifySkillReleaseImportClosureTests(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + cls.module = _load_module() + + def test_empty_directory_does_not_satisfy_relative_import(self) -> None: + with tempfile.TemporaryDirectory() as tmpdir: + root = Path(tmpdir) + (root / "runtime-lib").mkdir() + (root / "main.mjs").write_text("import './runtime-lib';\n", encoding="utf-8") + + failures = self.module.verify_import_closure(root) + + self.assertEqual(len(failures), 1) + self.assertIn("main.mjs imports ./runtime-lib", failures[0]) + + def test_directory_import_requires_index_file(self) -> None: + with tempfile.TemporaryDirectory() as tmpdir: + root = Path(tmpdir) + runtime_lib = root / "runtime-lib" + runtime_lib.mkdir() + (runtime_lib / "index.mjs").write_text("export {};\n", encoding="utf-8") + (root / "main.mjs").write_text("import './runtime-lib';\n", encoding="utf-8") + + failures = self.module.verify_import_closure(root) + + self.assertEqual(failures, []) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/ci/verify_skill_release_import_closure.py b/scripts/ci/verify_skill_release_import_closure.py new file mode 100755 index 0000000..6dce63d --- /dev/null +++ b/scripts/ci/verify_skill_release_import_closure.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +"""Verify staged skill release JS/TS relative imports are self-contained. + +The skill release workflow builds archives from `skill.json.sbom.files`. If a +runtime helper exists in the repo but is omitted from the SBOM, the staged +release can contain files whose relative imports point at missing files. This +script checks the staged payload, not the source tree, so it catches exactly +what would ship. +""" + +from __future__ import annotations + +import argparse +import re +import sys +from pathlib import Path + +IMPORT_RE = re.compile( + r"(?:" + r"\bimport\s+(?:type\s+)?(?:[^'\";]+?\s+from\s+)?" + r"|\bexport\s+(?:type\s+)?[^'\";]+?\s+from\s+" + r"|\bimport\s*\(\s*" + r"|\brequire\s*\(\s*" + r")" + r"['\"](?P\.{1,2}/[^'\"]+)['\"]", + re.MULTILINE, +) + +SOURCE_SUFFIXES = {".js", ".mjs", ".cjs", ".ts", ".mts", ".cts"} +RESOLUTION_SUFFIXES = ["", ".mjs", ".js", ".cjs", ".mts", ".ts", ".cts", ".json"] +INDEX_FILENAMES = ["index.mjs", "index.js", "index.cjs", "index.mts", "index.ts", "index.cts", "index.json"] + + +def candidate_paths(importer: Path, spec: str) -> list[Path]: + base = (importer.parent / spec).resolve() + candidates = [base] + candidates.extend(base.with_suffix(suffix) for suffix in RESOLUTION_SUFFIXES if suffix and base.suffix == "") + candidates.extend(base / name for name in INDEX_FILENAMES) + return candidates + + +def is_within(path: Path, root: Path) -> bool: + try: + path.resolve().relative_to(root) + return True + except ValueError: + return False + + +def is_resolved_file(candidate: Path, root: Path) -> bool: + return candidate.is_file() and is_within(candidate, root) + + +def verify_import_closure(root: Path) -> list[str]: + root = root.resolve() + failures: list[str] = [] + + for source in sorted(p for p in root.rglob("*") if p.is_file() and p.suffix in SOURCE_SUFFIXES): + text = source.read_text(encoding="utf-8", errors="ignore") + for match in IMPORT_RE.finditer(text): + spec = match.group("spec") + candidates = candidate_paths(source, spec) + if any(is_resolved_file(candidate, root) for candidate in candidates): + continue + + rel_source = source.relative_to(root).as_posix() + display_target = (source.parent / spec).resolve() + try: + rel_target = display_target.relative_to(root).as_posix() + except ValueError: + rel_target = str(display_target) + failures.append(f"{rel_source} imports {spec} but {rel_target} is absent from staged release") + + return failures + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("staged_skill_dir", type=Path, help="Staged skill payload directory, e.g. $INNER_DIR") + args = parser.parse_args() + + root = args.staged_skill_dir + if not root.is_dir(): + print(f"error: staged skill directory not found: {root}", file=sys.stderr) + return 2 + + failures = verify_import_closure(root) + if failures: + print("Release import-closure check failed:", file=sys.stderr) + for failure in failures: + print(f" - {failure}", file=sys.stderr) + print("Add the missing runtime file(s) to skill.json sbom.files or remove the stale import.", file=sys.stderr) + return 1 + + print(f"Release import-closure check OK: {root}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/skills/clawsec-suite/CHANGELOG.md b/skills/clawsec-suite/CHANGELOG.md index 8e4ad97..322c991 100644 --- a/skills/clawsec-suite/CHANGELOG.md +++ b/skills/clawsec-suite/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to the ClawSec Suite will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.1.8] - 2026-05-16 + +### Fixed + +- Added the advisory scope and suppression runtime helpers to `skill.json` SBOM metadata so release archives include every file required by the advisory guardian hook. + ## [0.1.7] - 2026-04-16 ### Changed diff --git a/skills/clawsec-suite/SKILL.md b/skills/clawsec-suite/SKILL.md index a4a6a56..bc0cbd4 100644 --- a/skills/clawsec-suite/SKILL.md +++ b/skills/clawsec-suite/SKILL.md @@ -1,6 +1,6 @@ --- name: clawsec-suite -version: 0.1.7 +version: 0.1.8 description: ClawSec suite manager with embedded advisory-feed monitoring, cryptographic signature verification, approval-gated malicious-skill response, and guided setup for additional security skills. homepage: https://clawsec.prompt.security clawdis: diff --git a/skills/clawsec-suite/skill.json b/skills/clawsec-suite/skill.json index 4483fb7..bdc748c 100644 --- a/skills/clawsec-suite/skill.json +++ b/skills/clawsec-suite/skill.json @@ -1,6 +1,6 @@ { "name": "clawsec-suite", - "version": "0.1.7", + "version": "0.1.8", "description": "ClawSec suite manager with embedded advisory-feed monitoring, cryptographic signature verification, approval-gated malicious-skill response, and guided setup for additional security skills.", "author": "prompt-security", "license": "AGPL-3.0-or-later", @@ -85,6 +85,11 @@ "required": true, "description": "Shared semver parsing and version matching logic" }, + { + "path": "hooks/clawsec-advisory-guardian/lib/advisory_scope.mjs", + "required": true, + "description": "Advisory application-scope filtering helper for OpenClaw-facing flows" + }, { "path": "hooks/clawsec-advisory-guardian/lib/feed.mjs", "required": true, @@ -110,6 +115,11 @@ "required": true, "description": "Advisory-to-skill matching and alert message generation" }, + { + "path": "hooks/clawsec-advisory-guardian/lib/suppression.mjs", + "required": true, + "description": "Advisory suppression loading and matching helpers" + }, { "path": "scripts/setup_advisory_hook.mjs", "required": true, diff --git a/skills/hermes-attestation-guardian/CHANGELOG.md b/skills/hermes-attestation-guardian/CHANGELOG.md index 5ee7f32..f8e6b18 100644 --- a/skills/hermes-attestation-guardian/CHANGELOG.md +++ b/skills/hermes-attestation-guardian/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## [0.1.2] - 2026-05-15 + +### Fixed +- Included `lib/semver.mjs` and `lib/cron.mjs` in the release SBOM so signed archives contain every runtime library imported by shipped scripts. + ## [0.1.1] - 2026-05-13 ### Security diff --git a/skills/hermes-attestation-guardian/SKILL.md b/skills/hermes-attestation-guardian/SKILL.md index 53bee5d..87af9b6 100644 --- a/skills/hermes-attestation-guardian/SKILL.md +++ b/skills/hermes-attestation-guardian/SKILL.md @@ -1,6 +1,6 @@ --- name: hermes-attestation-guardian -version: 0.1.1 +version: 0.1.2 description: Hermes-only runtime security attestation and drift detection skill for operator-managed Hermes infrastructure. homepage: https://clawsec.prompt.security hermes: @@ -24,7 +24,7 @@ For standalone installs, verify the signed release manifest before trusting `SKI set -euo pipefail SKILL_NAME="hermes-attestation-guardian" -VERSION="0.1.1" +VERSION="0.1.2" REPO="prompt-security/clawsec" TAG="${SKILL_NAME}-v${VERSION}" BASE="https://github.com/${REPO}/releases/download/${TAG}" diff --git a/skills/hermes-attestation-guardian/skill.json b/skills/hermes-attestation-guardian/skill.json index 6472586..05a8e44 100644 --- a/skills/hermes-attestation-guardian/skill.json +++ b/skills/hermes-attestation-guardian/skill.json @@ -1,6 +1,6 @@ { "name": "hermes-attestation-guardian", - "version": "0.1.1", + "version": "0.1.2", "description": "Hermes-only runtime security attestation and drift detection skill. Generates deterministic posture artifacts, verifies integrity fail-closed, and classifies baseline drift severity.", "author": "prompt-security", "license": "AGPL-3.0-or-later", @@ -46,6 +46,16 @@ "required": true, "description": "Hermes-native advisory feed verification and state helpers" }, + { + "path": "lib/semver.mjs", + "required": true, + "description": "Advisory version-range parsing and matching helpers" + }, + { + "path": "lib/cron.mjs", + "required": true, + "description": "Shared managed cron block and cadence helpers" + }, { "path": "scripts/generate_attestation.mjs", "required": true, diff --git a/skills/openclaw-audit-watchdog/CHANGELOG.md b/skills/openclaw-audit-watchdog/CHANGELOG.md index 1112dbe..7bea9c7 100644 --- a/skills/openclaw-audit-watchdog/CHANGELOG.md +++ b/skills/openclaw-audit-watchdog/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## [0.1.6] - 2026-05-16 + +### Fixed +- Added `scripts/load_suppression_config.mjs` to `skill.json` SBOM metadata so release archives include the helper imported by `scripts/render_report.mjs`. + ## [0.1.5] - 2026-05-14 ### Security diff --git a/skills/openclaw-audit-watchdog/SKILL.md b/skills/openclaw-audit-watchdog/SKILL.md index a245c68..b71f747 100644 --- a/skills/openclaw-audit-watchdog/SKILL.md +++ b/skills/openclaw-audit-watchdog/SKILL.md @@ -1,6 +1,6 @@ --- name: openclaw-audit-watchdog -version: 0.1.5 +version: 0.1.6 description: Automated daily security audits for OpenClaw agents with DM delivery and optional email reporting. Runs deep audits, creates or updates a recurring cron job, and sends formatted reports to configured recipients. homepage: https://clawsec.prompt.security metadata: @@ -74,7 +74,7 @@ For standalone installs, verify the signed release manifest before trusting `SKI set -euo pipefail SKILL_NAME="openclaw-audit-watchdog" -VERSION="0.1.5" +VERSION="0.1.6" REPO="prompt-security/clawsec" TAG="${SKILL_NAME}-v${VERSION}" BASE="https://github.com/${REPO}/releases/download/${TAG}" diff --git a/skills/openclaw-audit-watchdog/skill.json b/skills/openclaw-audit-watchdog/skill.json index a4b5a5a..3b01521 100644 --- a/skills/openclaw-audit-watchdog/skill.json +++ b/skills/openclaw-audit-watchdog/skill.json @@ -1,6 +1,6 @@ { "name": "openclaw-audit-watchdog", - "version": "0.1.5", + "version": "0.1.6", "description": "Automated daily security audits for OpenClaw agents with DM delivery and optional email reporting. Creates or updates an unattended cron job and sends formatted reports to configured recipients.", "author": "prompt-security", "license": "AGPL-3.0-or-later", @@ -52,6 +52,11 @@ "required": false, "description": "SMTP delivery (Node.js)" }, + { + "path": "scripts/load_suppression_config.mjs", + "required": false, + "description": "Suppression configuration loading and path normalization used by report rendering" + }, { "path": "scripts/setup_cron.mjs", "required": false,