* test(#3514): add failing-first denylist and integrity suites * fix(#3514): deny internal fetch hosts; disclose unverified integrity * docs(#3514): trust-model, glossary, and changeset entries * fix(#3514): scope v6 checks to literals; exact pin kinds in prompt * chore(#3514): backfill changeset pr number --------- Co-authored-by: sim <sim@local>
This commit is contained in:
@@ -616,6 +616,88 @@ test('TV-09: signatureForManifest does NOT vary with missingArtifacts (MISSING a
|
||||
}
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// #3514 (epic #1900 F21c) — unverified-integrity disclosure. The verdict gains
|
||||
// integrityPinned; the Disclosure carries integrityStatus ('pinned' |
|
||||
// 'unverified'); summarizeDisclosure renders it. Deliberately NOT part of the
|
||||
// consent-binding signature — consent's content binding is bundleContentHash
|
||||
// (#1459), and a prompt line must never force a spurious re-consent.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const EXEC_MANIFEST_3514 = {
|
||||
id: 'integrity-status-cap',
|
||||
hooks: [{ event: 'PostToolUse', script: 'hooks/run.js' }],
|
||||
};
|
||||
|
||||
test('#3514: integrityPinned:true verdict carries integrityStatus "pinned" and renders the pinned line', () => {
|
||||
const v = trust.evaluateInstallTrust({
|
||||
parsed: { kind: 'tarball', raw: 'https://example.com/cap.tgz', target: 'https://example.com/cap.tgz' },
|
||||
manifest: EXEC_MANIFEST_3514,
|
||||
hostVersion: '1.6.0',
|
||||
integrityPin: 'sha512',
|
||||
});
|
||||
assert.strictEqual(v.disclosure.integrityStatus, 'pinned');
|
||||
const joined = trust.summarizeDisclosure(v.disclosure).join('\n');
|
||||
assert.match(joined, /pin supplied and verified/i);
|
||||
assert.doesNotMatch(joined, /NO PINNED HASH/i);
|
||||
});
|
||||
|
||||
test('#3514: integrityPinned:false verdict carries integrityStatus "unverified" and renders the unverified line', () => {
|
||||
const v = trust.evaluateInstallTrust({
|
||||
parsed: { kind: 'tarball', raw: 'https://example.com/cap.tgz', target: 'https://example.com/cap.tgz' },
|
||||
manifest: EXEC_MANIFEST_3514,
|
||||
hostVersion: '1.6.0',
|
||||
integrityPin: 'none',
|
||||
});
|
||||
assert.strictEqual(v.disclosure.integrityStatus, 'unverified');
|
||||
const joined = trust.summarizeDisclosure(v.disclosure).join('\n');
|
||||
assert.match(joined, /NO PINNED HASH/i);
|
||||
assert.match(joined, /staged unverified/i);
|
||||
});
|
||||
|
||||
test('#3514: integrityPin git-commit renders the commit-pinned line, never a sha512 claim', () => {
|
||||
const v = trust.evaluateInstallTrust({
|
||||
parsed: { kind: 'git', raw: 'https://example.com/cap.git#sha:0123456789abcdef0123456789abcdef01234567', target: 'https://example.com/cap.git' },
|
||||
manifest: EXEC_MANIFEST_3514,
|
||||
hostVersion: '1.6.0',
|
||||
integrityPin: 'git-commit',
|
||||
});
|
||||
assert.strictEqual(v.disclosure.integrityStatus, 'commit-pinned');
|
||||
const joined = trust.summarizeDisclosure(v.disclosure).join('\n');
|
||||
assert.match(joined, /pinned to a git commit/i);
|
||||
assert.doesNotMatch(joined, /sha512 pin/i, 'a commit pin must not be reported as a sha512 pin');
|
||||
});
|
||||
|
||||
test('#3514: legacy callers (no integrityPin) see no integrity line — byte-identical rendering', () => {
|
||||
const base = {
|
||||
parsed: { kind: 'tarball', raw: 'https://example.com/cap.tgz', target: 'https://example.com/cap.tgz' },
|
||||
manifest: EXEC_MANIFEST_3514,
|
||||
hostVersion: '1.6.0',
|
||||
};
|
||||
const v = trust.evaluateInstallTrust(base);
|
||||
assert.strictEqual(v.disclosure.integrityStatus, undefined, 'absent arg ⇒ absent field');
|
||||
const joined = trust.summarizeDisclosure(v.disclosure).join('\n');
|
||||
assert.doesNotMatch(joined, /PINNED|unverified/i, 'no integrity line for legacy callers');
|
||||
});
|
||||
|
||||
test('#3514: unverified line also renders on the declarative-only disclosure path', () => {
|
||||
const d = trust.discloseExecutableSurfaces({ id: 'x', skills: ['s'] });
|
||||
d.integrityStatus = 'unverified';
|
||||
const joined = trust.summarizeDisclosure(d).join('\n');
|
||||
assert.match(joined, /NO PINNED HASH/i);
|
||||
});
|
||||
|
||||
test('#3514: integrityStatus NEVER enters the consent signature (executableSetChanged stable)', () => {
|
||||
const d1 = trust.discloseExecutableSurfaces(EXEC_MANIFEST_3514);
|
||||
const d2 = trust.discloseExecutableSurfaces(EXEC_MANIFEST_3514);
|
||||
d2.integrityStatus = 'unverified';
|
||||
assert.strictEqual(
|
||||
trust.executableSetChanged(d1, d2),
|
||||
false,
|
||||
'a prompt-only integrity line must not read as a changed executable set'
|
||||
);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// #3515 (epic #1900 F20) — MCP server config is INTENTIONALLY unconfined
|
||||
// (unlike hooks); the consent disclosure must say so. Adopted decision: the
|
||||
|
||||
Reference in New Issue
Block a user