mirror of
https://github.com/prompt-security/clawsec.git
synced 2026-06-13 05:28:02 +03:00
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:
@@ -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" .)
|
||||
|
||||
@@ -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
@@ -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())
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user