Files
clawsec/skills/hermes-attestation-guardian/scripts/guarded_skill_verify.mjs
T
David Abutbul 26af277afd feat(hermes-attestation-guardian): v0.1.0 release hardening (verify gate + trust policy + .mjs scan context) (#200)
* feat(hermes-attestation-guardian): release v0.0.2 hardening

* docs(wiki): add v0.0.2 hardening update note

* docs: add Hermes support coverage to README and compatibility report

* fix(hermes-attestation-guardian): address baz review on crontab detection and doc dedup

* feat(wiki): add PR-200 skill feature/platform matrix

* docs(wiki): rewrite PR-200 matrix as narrative capability mapping

* docs(readme): add skill feature matrix with requested headers

* docs(readme): replace unknowns with mapped yes/no feature matrix

* docs: move NanoClaw and CI/CD details from README to wiki modules

* docs(readme): remove platform/suite sections and keep wiki module pointers

* docs(readme): refresh project structure to match current repo

* feat(hermes-attestation-guardian): add signed advisory feed verification pipeline

* feat(hermes-attestation-guardian): add advisory-gated guarded skill verification

* feat(hermes-attestation-guardian): add advisory scheduler helper and phase-3 parity docs

* docs(wiki): expand hermes attestation guardian capability coverage

* fix(pr-200): address Baz review findings across Hermes parity rollout

* test(sandbox): extend Hermes regression to cover feed, guarded verify, and advisory scheduler

* fix(pr-200): address Baz semver parsing and feed-state fallback visibility

* fix(ci): suppress shellcheck false positives in sandbox inline docker script

* fix(hermes-attestation-guardian): fail closed on unsupported advisory ranges

* fix(hermes-attestation-guardian): restore safe install verdict in sandbox

* fix(sandbox): capture guarded verify exit under set -e

* fix(semver): fail closed on malformed affected specifiers

* docs(readme): clarify hermes capability matrix wording

* refactor(feed): share signed artifact verification flow

* refactor(cron): share managed block helpers across setup scripts

* fix(feed): require checksum manifest artifacts when enabled

* chore(hermes-skill): relocate sandbox test, refresh docs, and add v0.1.0 release notes

* chore(docs): remove remaining hermes parity plan file

* chore(release): roll hermes-attestation-guardian to v0.1.0

* chore(release): remove standalone v0.1.0 release notes file

* docs(hermes): update README status to v0.1.0

---------

Co-authored-by: David Abutbul <David.a@prompt.security>
2026-04-21 13:56:50 +03:00

203 lines
6.4 KiB
JavaScript

#!/usr/bin/env node
import fs from "node:fs";
import { refreshAdvisoryFeed } from "../lib/feed.mjs";
import { parseAffectedSpecifier, parseVersionSpec, versionMatches } from "../lib/semver.mjs";
const EXIT_CONFIRM_REQUIRED = 42;
function usage() {
process.stdout.write(
[
"Usage: node scripts/guarded_skill_verify.mjs --skill <name> [--version <semver>] [--confirm-advisory] [--allow-unsigned]",
"",
"Verifies advisory feed state using the Hermes feed verification pipeline, then gates",
"a candidate skill by advisory match before install/verification flows continue.",
"",
"Exit codes:",
" 0 no advisory match, or explicit advisory confirmation supplied",
" 42 advisory match found and --confirm-advisory was not provided",
" 1 verification/feed failure or invalid arguments",
"",
].join("\n"),
);
}
function parseArgs(argv) {
const parsed = {
skill: "",
version: "",
confirmAdvisory: false,
allowUnsigned: undefined,
help: false,
};
for (let i = 0; i < argv.length; i += 1) {
const token = argv[i];
if (token === "--skill") {
parsed.skill = String(argv[i + 1] || "").trim();
i += 1;
continue;
}
if (token === "--version") {
parsed.version = String(argv[i + 1] || "").trim();
i += 1;
continue;
}
if (token === "--confirm-advisory") {
parsed.confirmAdvisory = true;
continue;
}
if (token === "--allow-unsigned") {
parsed.allowUnsigned = true;
continue;
}
if (token === "--help" || token === "-h") {
parsed.help = true;
continue;
}
throw new Error(`Unknown argument: ${token}`);
}
if (parsed.help) return parsed;
if (!parsed.skill) {
throw new Error("Missing required argument: --skill");
}
if (!/^[a-z0-9-]+$/.test(parsed.skill)) {
throw new Error("Invalid --skill value. Use lowercase letters, digits, and hyphens only.");
}
if (parsed.version && !/^v?\d+\.\d+\.\d+(?:[-+][0-9a-zA-Z.-]+)?$/.test(parsed.version)) {
throw new Error("Invalid --version value. Expected semver (for example: 1.2.3).");
}
return parsed;
}
function normalizeSkillName(value) {
return String(value || "").trim().toLowerCase();
}
function findAdvisoryMatches(feed, skillName, version = "") {
const advisories = Array.isArray(feed?.advisories) ? feed.advisories : [];
const targetName = normalizeSkillName(skillName);
const matches = [];
for (const advisory of advisories) {
const affected = Array.isArray(advisory?.affected) ? advisory.affected : [];
if (affected.length === 0) continue;
const matchedAffected = [];
const unsupportedSpecs = [];
for (const specifier of affected) {
const parsed = parseAffectedSpecifier(specifier);
if (!parsed) continue;
if (normalizeSkillName(parsed.name) !== targetName) continue;
const parsedSpec = parseVersionSpec(parsed.versionSpec);
if (!parsedSpec.supported) {
// Fail closed: unsupported range syntax is treated as a match to avoid bypass.
matchedAffected.push(specifier);
unsupportedSpecs.push(specifier);
continue;
}
// Conservative default: if operator did not provide --version, any name match gates.
if (!version || versionMatches(version, parsed.versionSpec)) {
matchedAffected.push(specifier);
}
}
if (matchedAffected.length > 0) {
matches.push({ advisory, matchedAffected, unsupportedSpecs });
}
}
return matches;
}
function printMatches(matches, args) {
process.stdout.write("Advisory matches detected for requested candidate.\n");
process.stdout.write(`Target: ${args.skill}${args.version ? `@${args.version}` : ""}\n`);
for (const match of matches) {
const advisory = match.advisory || {};
const severity = String(advisory.severity || "unknown").toUpperCase();
const advisoryId = String(advisory.id || "unknown-id");
const title = String(advisory.title || "Untitled advisory");
process.stdout.write(`- [${severity}] ${advisoryId}: ${title}\n`);
process.stdout.write(` matched: ${match.matchedAffected.join(", ")}\n`);
if (Array.isArray(match.unsupportedSpecs) && match.unsupportedSpecs.length > 0) {
process.stdout.write(
` warning: unsupported advisory version syntax treated as match (fail-closed): ${match.unsupportedSpecs.join(", ")}\n`,
);
}
if (advisory.action) {
process.stdout.write(` action: ${advisory.action}\n`);
}
}
}
async function main() {
const args = parseArgs(process.argv.slice(2));
if (args.help) {
usage();
return;
}
let refreshResult;
try {
refreshResult = await refreshAdvisoryFeed(args.allowUnsigned === true ? { allowUnsigned: true } : {});
} catch (error) {
process.stderr.write(`CRITICAL: advisory feed verification failed (fail-closed): ${error?.message || String(error)}\n`);
process.exit(1);
}
if (refreshResult.status === "unverified") {
const warningSource = args.allowUnsigned === true ? "--allow-unsigned" : "resolved env/config policy";
process.stderr.write(
`WARNING: unsigned advisory bypass enabled via ${warningSource}. This weakens supply-chain guarantees and should be emergency-only.\n`,
);
}
let feed;
try {
feed = JSON.parse(fs.readFileSync(refreshResult.cachedFeedPath, "utf8"));
} catch (error) {
process.stderr.write(
`CRITICAL: cached advisory feed load failed after verification: ${error?.message || String(error)}\n`,
);
process.exit(1);
}
process.stdout.write(`Advisory feed status: ${refreshResult.status} (${refreshResult.source})\n`);
if (!args.version) {
process.stdout.write("No --version provided; applying conservative name-based advisory gate.\n");
}
const matches = findAdvisoryMatches(feed, args.skill, args.version);
if (matches.length === 0) {
process.stdout.write("No advisory matches found for candidate.\n");
return;
}
printMatches(matches, args);
if (!args.confirmAdvisory) {
process.stdout.write("Re-run with --confirm-advisory to proceed with explicit operator acknowledgement.\n");
process.exit(EXIT_CONFIRM_REQUIRED);
}
process.stderr.write(
`WARNING: proceeding despite ${matches.length} advisory match(es) because --confirm-advisory was provided.\n`,
);
}
try {
await main();
} catch (error) {
process.stderr.write(`CRITICAL: ${error?.message || String(error)}\n`);
process.exit(1);
}