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"
|
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
@@ -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/),
|
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,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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}"
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user