mirror of
https://github.com/prompt-security/clawsec.git
synced 2026-06-13 05:28:02 +03:00
600c945fe2
* feat(hermes-attestation-guardian): harden attestation verification and drift controls * docs(wiki): add human-friendly claim mapping for hermes attestation guardian * docs(wiki): expand hermes attestation claim narratives and archive draft * fix(attestation): address Baz review findings for schema and verifier * fix(attestation): reject broken symlink output paths * docs(attestation): pass clean community install guard without force * fix(attestation): harden writes and fail-closed config parsing * feat(ui): add Hermes to rotating platform text * test(attestation): add sandboxed Hermes regression runner script --------- Co-authored-by: David Abutbul <David.a@prompt.security>
250 lines
7.5 KiB
JavaScript
250 lines
7.5 KiB
JavaScript
const SEVERITY_ORDER = ["critical", "high", "medium", "low", "info"];
|
|
|
|
function bumpSummary(summary, severity) {
|
|
if (summary[severity] === undefined) {
|
|
summary[severity] = 0;
|
|
}
|
|
summary[severity] += 1;
|
|
}
|
|
|
|
function compareBooleanFindings({ findings, summary, codeOnEnable, codeOnDisable, path, before, after, enableSeverity = "high" }) {
|
|
if (!!before === !!after) return;
|
|
|
|
if (!before && after) {
|
|
findings.push({
|
|
severity: enableSeverity,
|
|
code: codeOnEnable,
|
|
path,
|
|
message: `${path} changed false -> true`,
|
|
});
|
|
bumpSummary(summary, enableSeverity);
|
|
return;
|
|
}
|
|
|
|
findings.push({
|
|
severity: "info",
|
|
code: codeOnDisable,
|
|
path,
|
|
message: `${path} changed true -> false`,
|
|
});
|
|
bumpSummary(summary, "info");
|
|
}
|
|
|
|
function mapByPath(entries) {
|
|
const out = new Map();
|
|
for (const entry of Array.isArray(entries) ? entries : []) {
|
|
if (!entry || typeof entry.path !== "string") continue;
|
|
out.set(entry.path, entry);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function compareHashedEntries({ findings, summary, beforeEntries, afterEntries, changedCode, missingCode }) {
|
|
const beforeMap = mapByPath(beforeEntries);
|
|
const afterMap = mapByPath(afterEntries);
|
|
|
|
for (const [itemPath, before] of beforeMap.entries()) {
|
|
const after = afterMap.get(itemPath);
|
|
if (!after) {
|
|
findings.push({
|
|
severity: "high",
|
|
code: missingCode,
|
|
path: itemPath,
|
|
message: `${itemPath} missing in current attestation`,
|
|
});
|
|
bumpSummary(summary, "high");
|
|
continue;
|
|
}
|
|
|
|
const beforeHash = before.sha256 || null;
|
|
const afterHash = after.sha256 || null;
|
|
if (beforeHash !== afterHash) {
|
|
findings.push({
|
|
severity: "critical",
|
|
code: changedCode,
|
|
path: itemPath,
|
|
message: `${itemPath} fingerprint changed`,
|
|
});
|
|
bumpSummary(summary, "critical");
|
|
}
|
|
}
|
|
|
|
for (const [itemPath, after] of afterMap.entries()) {
|
|
if (beforeMap.has(itemPath)) continue;
|
|
findings.push({
|
|
severity: "low",
|
|
code: "NEW_INTEGRITY_SCOPE",
|
|
path: itemPath,
|
|
message: `${itemPath} added to integrity tracking scope`,
|
|
details: { exists: !!after.exists },
|
|
});
|
|
bumpSummary(summary, "low");
|
|
}
|
|
}
|
|
|
|
function compareFeedVerification({ findings, summary, baselineFeed, currentFeed }) {
|
|
const beforeStatus = baselineFeed?.status || "unknown";
|
|
const afterStatus = currentFeed?.status || "unknown";
|
|
|
|
if (beforeStatus === afterStatus) return;
|
|
|
|
if (beforeStatus === "verified" && afterStatus !== "verified") {
|
|
findings.push({
|
|
severity: "critical",
|
|
code: "FEED_VERIFICATION_REGRESSION",
|
|
path: "posture.feed_verification.status",
|
|
message: `Feed verification regressed verified -> ${afterStatus}`,
|
|
});
|
|
bumpSummary(summary, "critical");
|
|
return;
|
|
}
|
|
|
|
findings.push({
|
|
severity: "medium",
|
|
code: "FEED_VERIFICATION_CHANGED",
|
|
path: "posture.feed_verification.status",
|
|
message: `Feed verification status changed ${beforeStatus} -> ${afterStatus}`,
|
|
});
|
|
bumpSummary(summary, "medium");
|
|
}
|
|
|
|
function comparePlatform({ findings, summary, baseline, current }) {
|
|
if (baseline.platform === current.platform) return;
|
|
findings.push({
|
|
severity: "critical",
|
|
code: "PLATFORM_MISMATCH",
|
|
path: "platform",
|
|
message: `platform changed ${baseline.platform} -> ${current.platform}`,
|
|
});
|
|
bumpSummary(summary, "critical");
|
|
}
|
|
|
|
function compareSchema({ findings, summary, baseline, current }) {
|
|
if (baseline.schema_version === current.schema_version) return;
|
|
findings.push({
|
|
severity: "high",
|
|
code: "SCHEMA_VERSION_CHANGED",
|
|
path: "schema_version",
|
|
message: `schema_version changed ${baseline.schema_version} -> ${current.schema_version}`,
|
|
});
|
|
bumpSummary(summary, "high");
|
|
}
|
|
|
|
function compareGenerator({ findings, summary, baseline, current }) {
|
|
const before = baseline?.generator?.version || "unknown";
|
|
const after = current?.generator?.version || "unknown";
|
|
if (before === after) return;
|
|
findings.push({
|
|
severity: "info",
|
|
code: "GENERATOR_VERSION_CHANGED",
|
|
path: "generator.version",
|
|
message: `generator.version changed ${before} -> ${after}`,
|
|
});
|
|
bumpSummary(summary, "info");
|
|
}
|
|
|
|
export function diffAttestations(baseline, current) {
|
|
const findings = [];
|
|
const summary = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
|
|
|
|
const baselineSafe = baseline && typeof baseline === "object" ? baseline : {};
|
|
const currentSafe = current && typeof current === "object" ? current : {};
|
|
|
|
comparePlatform({ findings, summary, baseline: baselineSafe, current: currentSafe });
|
|
compareSchema({ findings, summary, baseline: baselineSafe, current: currentSafe });
|
|
compareGenerator({ findings, summary, baseline: baselineSafe, current: currentSafe });
|
|
|
|
const baselineRuntime = baselineSafe?.posture?.runtime || {};
|
|
const currentRuntime = currentSafe?.posture?.runtime || {};
|
|
|
|
compareBooleanFindings({
|
|
findings,
|
|
summary,
|
|
codeOnEnable: "UNSIGNED_MODE_ENABLED",
|
|
codeOnDisable: "UNSIGNED_MODE_DISABLED",
|
|
path: "posture.runtime.risky_toggles.allow_unsigned_mode",
|
|
before: baselineRuntime?.risky_toggles?.allow_unsigned_mode,
|
|
after: currentRuntime?.risky_toggles?.allow_unsigned_mode,
|
|
enableSeverity: "critical",
|
|
});
|
|
|
|
compareBooleanFindings({
|
|
findings,
|
|
summary,
|
|
codeOnEnable: "BYPASS_VERIFICATION_ENABLED",
|
|
codeOnDisable: "BYPASS_VERIFICATION_DISABLED",
|
|
path: "posture.runtime.risky_toggles.bypass_verification",
|
|
before: baselineRuntime?.risky_toggles?.bypass_verification,
|
|
after: currentRuntime?.risky_toggles?.bypass_verification,
|
|
enableSeverity: "critical",
|
|
});
|
|
|
|
for (const gateway of ["telegram", "matrix", "discord"]) {
|
|
compareBooleanFindings({
|
|
findings,
|
|
summary,
|
|
codeOnEnable: "GATEWAY_ENABLED",
|
|
codeOnDisable: "GATEWAY_DISABLED",
|
|
path: `posture.runtime.gateways.${gateway}`,
|
|
before: baselineRuntime?.gateways?.[gateway],
|
|
after: currentRuntime?.gateways?.[gateway],
|
|
enableSeverity: "low",
|
|
});
|
|
}
|
|
|
|
compareFeedVerification({
|
|
findings,
|
|
summary,
|
|
baselineFeed: baselineSafe?.posture?.feed_verification,
|
|
currentFeed: currentSafe?.posture?.feed_verification,
|
|
});
|
|
|
|
compareHashedEntries({
|
|
findings,
|
|
summary,
|
|
beforeEntries: baselineSafe?.posture?.integrity?.trust_anchors,
|
|
afterEntries: currentSafe?.posture?.integrity?.trust_anchors,
|
|
changedCode: "TRUST_ANCHOR_MISMATCH",
|
|
missingCode: "TRUST_ANCHOR_REMOVED",
|
|
});
|
|
|
|
compareHashedEntries({
|
|
findings,
|
|
summary,
|
|
beforeEntries: baselineSafe?.posture?.integrity?.watched_files,
|
|
afterEntries: currentSafe?.posture?.integrity?.watched_files,
|
|
changedCode: "WATCHED_FILE_DRIFT",
|
|
missingCode: "WATCHED_FILE_REMOVED",
|
|
});
|
|
|
|
findings.sort((a, b) => {
|
|
const sev = SEVERITY_ORDER.indexOf(a.severity) - SEVERITY_ORDER.indexOf(b.severity);
|
|
if (sev !== 0) return sev;
|
|
const codeCmp = String(a.code || "").localeCompare(String(b.code || ""));
|
|
if (codeCmp !== 0) return codeCmp;
|
|
return String(a.path || "").localeCompare(String(b.path || ""));
|
|
});
|
|
|
|
return {
|
|
summary,
|
|
findings,
|
|
};
|
|
}
|
|
|
|
export function highestSeverity(findings = []) {
|
|
for (const severity of SEVERITY_ORDER) {
|
|
if (findings.some((finding) => finding?.severity === severity)) {
|
|
return severity;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function severityAtOrAbove(severity, threshold) {
|
|
if (!threshold || threshold === "none") return false;
|
|
const idx = SEVERITY_ORDER.indexOf(severity);
|
|
const thresholdIdx = SEVERITY_ORDER.indexOf(threshold);
|
|
if (idx < 0 || thresholdIdx < 0) return false;
|
|
return idx <= thresholdIdx;
|
|
}
|