fix(attestation): include runtime libs in release sbom (#235)

* fix(attestation): include runtime libs in release sbom

* ci: verify staged skill release import closure

* fix(release): include missing skill runtime sbom files

* fix(release): require files for import closure

---------

Co-authored-by: David Abutbul <David.a@prompt.security>
This commit is contained in:
David Abutbul
2026-05-17 00:40:12 +03:00
committed by GitHub
parent 1e48a955cc
commit 19c5113511
12 changed files with 206 additions and 8 deletions
+6
View File
@@ -530,6 +530,9 @@ jobs:
echo " [Dry-run] Removed test signatures from release staging" echo " [Dry-run] Removed test signatures from release staging"
fi fi
# --- Verify staged runtime import closure before archiving ---
python3 scripts/ci/verify_skill_release_import_closure.py "${inner_dir}"
# --- Create zip preserving directory structure --- # --- Create zip preserving directory structure ---
zip_name="${skill_name}-v${version}.zip" zip_name="${skill_name}-v${version}.zip"
(cd "${staging_dir}" && zip -qr "${OLDPWD}/${out_assets}/${zip_name}" .) (cd "${staging_dir}" && zip -qr "${OLDPWD}/${out_assets}/${zip_name}" .)
@@ -892,6 +895,9 @@ jobs:
cp "$SKILL_PATH/skill.json" "$INNER_DIR/skill.json" 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 --- # --- Create zip preserving directory structure ---
ZIP_NAME="${SKILL_NAME}-v${VERSION}.zip" ZIP_NAME="${SKILL_NAME}-v${VERSION}.zip"
(cd "$STAGING_DIR" && zip -qr "$OLDPWD/release-assets/$ZIP_NAME" .) (cd "$STAGING_DIR" && zip -qr "$OLDPWD/release-assets/$ZIP_NAME" .)
@@ -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()
+100
View File
@@ -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<spec>\.{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())
+6
View File
@@ -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/), 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). 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 ## [0.1.7] - 2026-04-16
### Changed ### Changed
+1 -1
View File
@@ -1,6 +1,6 @@
--- ---
name: clawsec-suite 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. 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 homepage: https://clawsec.prompt.security
clawdis: clawdis:
+11 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "clawsec-suite", "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.", "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", "author": "prompt-security",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
@@ -85,6 +85,11 @@
"required": true, "required": true,
"description": "Shared semver parsing and version matching logic" "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", "path": "hooks/clawsec-advisory-guardian/lib/feed.mjs",
"required": true, "required": true,
@@ -110,6 +115,11 @@
"required": true, "required": true,
"description": "Advisory-to-skill matching and alert message generation" "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", "path": "scripts/setup_advisory_hook.mjs",
"required": true, "required": true,
@@ -1,5 +1,10 @@
# Changelog # 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 ## [0.1.1] - 2026-05-13
### Security ### Security
+2 -2
View File
@@ -1,6 +1,6 @@
--- ---
name: hermes-attestation-guardian 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. description: Hermes-only runtime security attestation and drift detection skill for operator-managed Hermes infrastructure.
homepage: https://clawsec.prompt.security homepage: https://clawsec.prompt.security
hermes: hermes:
@@ -24,7 +24,7 @@ For standalone installs, verify the signed release manifest before trusting `SKI
set -euo pipefail set -euo pipefail
SKILL_NAME="hermes-attestation-guardian" SKILL_NAME="hermes-attestation-guardian"
VERSION="0.1.1" VERSION="0.1.2"
REPO="prompt-security/clawsec" REPO="prompt-security/clawsec"
TAG="${SKILL_NAME}-v${VERSION}" TAG="${SKILL_NAME}-v${VERSION}"
BASE="https://github.com/${REPO}/releases/download/${TAG}" BASE="https://github.com/${REPO}/releases/download/${TAG}"
+11 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "hermes-attestation-guardian", "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.", "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", "author": "prompt-security",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
@@ -46,6 +46,16 @@
"required": true, "required": true,
"description": "Hermes-native advisory feed verification and state helpers" "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", "path": "scripts/generate_attestation.mjs",
"required": true, "required": true,
@@ -1,5 +1,10 @@
# Changelog # 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 ## [0.1.5] - 2026-05-14
### Security ### Security
+2 -2
View File
@@ -1,6 +1,6 @@
--- ---
name: openclaw-audit-watchdog 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. 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 homepage: https://clawsec.prompt.security
metadata: metadata:
@@ -74,7 +74,7 @@ For standalone installs, verify the signed release manifest before trusting `SKI
set -euo pipefail set -euo pipefail
SKILL_NAME="openclaw-audit-watchdog" SKILL_NAME="openclaw-audit-watchdog"
VERSION="0.1.5" VERSION="0.1.6"
REPO="prompt-security/clawsec" REPO="prompt-security/clawsec"
TAG="${SKILL_NAME}-v${VERSION}" TAG="${SKILL_NAME}-v${VERSION}"
BASE="https://github.com/${REPO}/releases/download/${TAG}" BASE="https://github.com/${REPO}/releases/download/${TAG}"
+6 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "openclaw-audit-watchdog", "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.", "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", "author": "prompt-security",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
@@ -52,6 +52,11 @@
"required": false, "required": false,
"description": "SMTP delivery (Node.js)" "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", "path": "scripts/setup_cron.mjs",
"required": false, "required": false,