fix(mosaic): bound brain migration and owner lookups
This commit is contained in:
@@ -164,6 +164,38 @@ describe('provider-backed durable owner resolver', (): void => {
|
||||
expect(calls.every((call) => call.authorization === null)).toBe(true);
|
||||
});
|
||||
|
||||
it('refuses provider redirects and configures a bounded no-redirect request', async (): Promise<void> => {
|
||||
const resolver = await loadResolver('MB-REQ-09 owner lookup SSRF boundary');
|
||||
const requests: RequestInit[] = [];
|
||||
|
||||
const result = await resolver.resolveProviderDurableOwner(
|
||||
{
|
||||
estateRegistrySource: estateRegistry(),
|
||||
ownerPolicySource: ownerPolicy(),
|
||||
host: 'git.example.invalid',
|
||||
requestedOwner: 'user:durable-owner',
|
||||
},
|
||||
{
|
||||
fetch: async (_input, init): Promise<Response> => {
|
||||
requests.push(init ?? {});
|
||||
return new Response(JSON.stringify({ message: 'redirect' }), {
|
||||
status: 302,
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
location: 'http://127.0.0.1/internal',
|
||||
},
|
||||
});
|
||||
},
|
||||
absentControlName: (): string => 'generated-absent-control',
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({ verdict: 'not-measured', reasonCode: 'owner-control-invalid' });
|
||||
expect(requests).toHaveLength(1);
|
||||
expect(requests[0]?.redirect).toBe('manual');
|
||||
expect(requests[0]?.signal).toBeInstanceOf(AbortSignal);
|
||||
});
|
||||
|
||||
it('requires the GLPI standing remediation queue in the local estate policy', async (): Promise<void> => {
|
||||
const resolver = await loadResolver('MB-REQ-09 standing process policy');
|
||||
const raw = JSON.parse(ownerPolicy()) as { estates: Array<Record<string, unknown>> };
|
||||
|
||||
@@ -109,6 +109,8 @@ async function readPublicIdentity(
|
||||
Accept: 'application/json',
|
||||
'User-Agent': 'mosaic-brain-owner/1',
|
||||
},
|
||||
redirect: 'manual',
|
||||
signal: AbortSignal.timeout(5_000),
|
||||
});
|
||||
} catch {
|
||||
throw new Error('owner-provider-unavailable');
|
||||
|
||||
@@ -672,6 +672,11 @@ describe('R7 — migration is non-destructive, append-only, and explicit', (): v
|
||||
mkdirSync(join(laneRoot, path, '..'), { recursive: true });
|
||||
writeFileSync(join(laneRoot, path), 'NESTED-SECRET-MARKER\n');
|
||||
}
|
||||
const embeddedSecret = join(laneRoot, 'benign-looking.md');
|
||||
writeFileSync(
|
||||
embeddedSecret,
|
||||
'Authorization: Bearer NESTED-CONTENT-SECRET-MARKER-1234567890\n',
|
||||
);
|
||||
|
||||
const plan = sut.discoverBrainMigration(
|
||||
{ sourceRoot, brainRoot, seat: 'seat-a', lane: 'lane-a', laneActive: true },
|
||||
@@ -680,9 +685,9 @@ describe('R7 — migration is non-destructive, append-only, and explicit', (): v
|
||||
|
||||
expect(plan.status).toBe('ready');
|
||||
expect(plan.candidates).toHaveLength(0);
|
||||
expect(plan.reported).toHaveLength(secretNames.length + nestedSecretPaths.length);
|
||||
expect(plan.reported).toHaveLength(secretNames.length + nestedSecretPaths.length + 1);
|
||||
expect(plan.reported.every((entry) => /secret/i.test(entry.reason))).toBe(true);
|
||||
expect(JSON.stringify(plan)).not.toContain('NESTED-SECRET-MARKER');
|
||||
expect(JSON.stringify(plan)).not.toMatch(/NESTED-(?:CONTENT-)?SECRET-MARKER/);
|
||||
expect(existsSync(brainRoot)).toBe(false);
|
||||
});
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ const GITIGNORE_RULES = [
|
||||
'id_ed25519',
|
||||
] as const;
|
||||
const BRAIN_DIRECTORIES = ['agents', 'lanes', 'board', 'specs', 'methods', 'archives'] as const;
|
||||
const MAX_MIGRATION_FILE_BYTES = 1024 * 1024;
|
||||
const TERMINAL_EXITS = {
|
||||
ok: 0,
|
||||
refused: 10,
|
||||
@@ -615,6 +616,7 @@ function migrationCandidate(
|
||||
lane: string,
|
||||
): MigrationCandidate {
|
||||
const source = stableSourceSnapshot(path);
|
||||
if (sourceContainsSecretMaterial(source.content)) throw new Error('migration-secret-content');
|
||||
const key = relative(sourceRoot, path).split(sep).join('/');
|
||||
const digest = createHash('sha256')
|
||||
.update(key)
|
||||
@@ -639,6 +641,7 @@ function migrationCandidate(
|
||||
function secretShapedName(value: string): boolean {
|
||||
const name = value.toLowerCase();
|
||||
return (
|
||||
['.npmrc', '.netrc', 'auth.json', '.dockerconfigjson'].includes(name) ||
|
||||
name === '.env' ||
|
||||
name.startsWith('.env.') ||
|
||||
name === 'credentials.json' ||
|
||||
@@ -658,6 +661,23 @@ function secretShapedPath(sourceRoot: string, path: string): boolean {
|
||||
return relative(sourceRoot, path).split(sep).filter(Boolean).some(secretShapedName);
|
||||
}
|
||||
|
||||
function sourceContainsSecretMaterial(content: Buffer): boolean {
|
||||
if (content.byteLength > MAX_MIGRATION_FILE_BYTES || content.includes(0)) return true;
|
||||
let source: string;
|
||||
try {
|
||||
source = new TextDecoder('utf-8', { fatal: true }).decode(content);
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
return [
|
||||
/-----BEGIN [^-\r\n]*PRIVATE KEY-----/i,
|
||||
/\bauthorization\s*:\s*(?:bearer|basic)\s+\S+/i,
|
||||
/\b(?:password|passwd|api[_-]?key|access[_-]?token|refresh[_-]?token|client[_-]?secret)\s*[:=]\s*\S+/i,
|
||||
/https?:\/\/[^/\s:@]+:[^/\s@]+@/i,
|
||||
/\b[A-Za-z0-9+/_=-]{40,}\b/,
|
||||
].some((pattern): boolean => pattern.test(source));
|
||||
}
|
||||
|
||||
function reportAll(paths: readonly string[], reason: string): MigrationReport[] {
|
||||
return paths.map((path: string): MigrationReport => ({ path, reason }));
|
||||
}
|
||||
@@ -729,8 +749,14 @@ export function discoverBrainMigration(
|
||||
candidates.push(
|
||||
migrationCandidate(input.sourceRoot, input.brainRoot, path, 'lane', seat, lane),
|
||||
);
|
||||
} catch {
|
||||
reported.push({ path, reason: 'Lane state was not a regular file; retained and reported.' });
|
||||
} catch (error: unknown) {
|
||||
reported.push({
|
||||
path,
|
||||
reason:
|
||||
error instanceof Error && error.message === 'migration-secret-content'
|
||||
? 'Content may contain secret material; retained and reported.'
|
||||
: 'Lane state was not a regular file; retained and reported.',
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const path of seatFiles) {
|
||||
@@ -745,8 +771,14 @@ export function discoverBrainMigration(
|
||||
candidates.push(
|
||||
migrationCandidate(input.sourceRoot, input.brainRoot, path, 'seat', seat, lane),
|
||||
);
|
||||
} catch {
|
||||
reported.push({ path, reason: 'Seat state was not a regular file; retained and reported.' });
|
||||
} catch (error: unknown) {
|
||||
reported.push({
|
||||
path,
|
||||
reason:
|
||||
error instanceof Error && error.message === 'migration-secret-content'
|
||||
? 'Content may contain secret material; retained and reported.'
|
||||
: 'Seat state was not a regular file; retained and reported.',
|
||||
});
|
||||
}
|
||||
}
|
||||
return { status: 'ready', candidates, reported, owner: ownerResolution.principal };
|
||||
|
||||
Reference in New Issue
Block a user