hosted-write-eval-kn-b-security.test.mjs
166 lines 5.3 KB
Raw
sha256:700fafdd1afa490919f9515d660ca6e75456bcd5bb67513abcd8757a634c01f6 docs: record AIP-b SD-21 land (KN #308) Human 10 days ago
1 /**
2 * HOSTED-WRITE-EVAL-KN-b — Tier 7 security: IDOR, forged eval, waiver non-bypass, no secrets in errors.
3 */
4 import { describe, it, beforeEach, afterEach } from 'node:test';
5 import assert from 'node:assert/strict';
6 import fs from 'node:fs';
7 import os from 'node:os';
8 import path from 'node:path';
9 import { fileURLToPath } from 'node:url';
10 import { augmentProposalCreateRequestBody } from '../lib/hub-proposal-create-augment.mjs';
11 import {
12 personalSelfApplyAllowsApprove,
13 applyPersonalSelfApplyEvaluationE1,
14 SCOOLING_REVIEW_TRAY_INTENT,
15 } from '../lib/hub-proposal-personal-self-apply.mjs';
16 import { createProposal } from '../hub/proposals-store.mjs';
17
18 describe('HOSTED-WRITE-EVAL-KN-b security', () => {
19 /** @type {string} */
20 let dataDir;
21 /** @type {string|undefined} */
22 let prevEval;
23
24 beforeEach(() => {
25 dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kt-hwe-sec-'));
26 prevEval = process.env.HUB_PROPOSAL_EVALUATION_REQUIRED;
27 process.env.HUB_PROPOSAL_EVALUATION_REQUIRED = '1';
28 });
29
30 afterEach(() => {
31 if (prevEval === undefined) delete process.env.HUB_PROPOSAL_EVALUATION_REQUIRED;
32 else process.env.HUB_PROPOSAL_EVALUATION_REQUIRED = prevEval;
33 fs.rmSync(dataDir, { recursive: true, force: true });
34 });
35
36 it('IDOR: foreign partition (partitionOwned=false) denied', () => {
37 const proposal = createProposal(dataDir, {
38 path: 'reviewed/idor.md',
39 body: 'secret body TOKEN=abc',
40 intent: SCOOLING_REVIEW_TRAY_INTENT,
41 external_ref: 'scooling.review:idor',
42 evaluationRequired: true,
43 proposed_by: 'user:owner',
44 });
45 assert.equal(
46 personalSelfApplyAllowsApprove({
47 proposal,
48 hasVaultWrite: true,
49 partitionOwned: false,
50 role: 'member',
51 }),
52 false,
53 );
54 });
55
56 it('forged client evaluation_status=passed is overwritten by triggers+E1 rules', () => {
57 // SEC-KN-2: Non-class client forge is stripped; gate assigns pending.
58 const forged = augmentProposalCreateRequestBody(
59 {
60 path: 'inbox/x.md',
61 body: 'x',
62 intent: 'other',
63 external_ref: 'scooling.review:x',
64 evaluation_status: 'passed',
65 evaluated_by: 'attacker',
66 labels: [],
67 },
68 dataDir,
69 { evaluationRequired: true, evaluatedBy: 'attacker' },
70 );
71 assert.equal(forged.evaluation_status, 'pending');
72 assert.equal(Object.hasOwn(forged, 'evaluated_by'), false);
73 assert.equal(
74 personalSelfApplyAllowsApprove({
75 proposal: { ...forged, status: 'proposed' },
76 hasVaultWrite: true,
77 partitionOwned: true,
78 role: 'member',
79 }),
80 false,
81 );
82
83 // Elevated: even with forged passed + matching fingerprint after trigger, E1 must not pass.
84 fs.writeFileSync(
85 path.join(dataDir, 'hub_proposal_review_triggers.json'),
86 JSON.stringify({
87 literal_phrases: [{ match: 'api_key', review_severity: 'elevated' }],
88 path_prefixes: [],
89 label_any: [],
90 }),
91 );
92 const elevated = augmentProposalCreateRequestBody(
93 {
94 path: 'reviewed/elev.md',
95 body: 'api_key leak',
96 intent: SCOOLING_REVIEW_TRAY_INTENT,
97 external_ref: 'scooling.review:elev',
98 evaluation_status: 'passed',
99 labels: [],
100 },
101 dataDir,
102 { evaluationRequired: true, evaluatedBy: 'attacker' },
103 );
104 assert.equal(elevated.review_severity, 'elevated');
105 assert.equal(elevated.evaluation_status, 'pending');
106 });
107
108 it('learner waiver is not required for class; non-class stays blocked without admin path', () => {
109 const ok = applyPersonalSelfApplyEvaluationE1(
110 {
111 status: 'proposed',
112 intent: SCOOLING_REVIEW_TRAY_INTENT,
113 path: 'reviewed/w.md',
114 external_ref: 'scooling.review:w',
115 evaluation_status: 'pending',
116 },
117 { evaluatedBy: 'learner' },
118 );
119 assert.equal(ok.evaluation_status, 'passed');
120 // Non-class pending: self-apply false — waiver is admin escape hatch, not learner bypass for class.
121 assert.equal(
122 personalSelfApplyAllowsApprove({
123 proposal: {
124 status: 'proposed',
125 intent: 'other',
126 path: 'reviewed/w.md',
127 external_ref: 'scooling.review:w',
128 evaluation_status: 'pending',
129 },
130 hasVaultWrite: true,
131 partitionOwned: true,
132 role: 'member',
133 }),
134 false,
135 );
136 });
137
138 it('predicate failures return boolean only — no proposal body / secrets in helper result', () => {
139 const proposal = {
140 status: 'proposed',
141 intent: 'other',
142 path: 'reviewed/s.md',
143 external_ref: 'scooling.review:s',
144 body: 'PRIVATE_KEY=do-not-leak',
145 };
146 const allowed = personalSelfApplyAllowsApprove({
147 proposal,
148 hasVaultWrite: true,
149 partitionOwned: true,
150 role: 'member',
151 });
152 assert.equal(allowed, false);
153 assert.equal(typeof allowed, 'boolean');
154 });
155
156 it('gateway wiring: assertHostedProposalApproveDiscard includes personal self-apply path', () => {
157 const serverPath = path.resolve(
158 path.dirname(fileURLToPath(import.meta.url)),
159 '../hub/gateway/server.mjs',
160 );
161 const src = fs.readFileSync(serverPath, 'utf8');
162 assert.ok(src.includes('personalSelfApplyRefusalReason'));
163 assert.ok(src.includes('fetchHostedProposalForSelfApply'));
164 assert.ok(src.includes('Discard requires admin'));
165 });
166 });
File History 1 commit
sha256:700fafdd1afa490919f9515d660ca6e75456bcd5bb67513abcd8757a634c01f6 docs: record AIP-b SD-21 land (KN #308) Human 10 days ago