Security Sprint M7.1: Complete P1 Security Fixes (#284-#287) #320

Merged
jason.woltje merged 4 commits from fix/284-287-p1-security-fixes into develop 2026-02-04 03:54:02 +00:00
80 changed files with 2551 additions and 35 deletions

View File

@@ -0,0 +1,52 @@
/**
* Sanitize Decorator
*
* Custom class-validator decorator to sanitize string input and prevent XSS.
*/
import { Transform } from "class-transformer";
import { sanitizeString, sanitizeObject } from "../utils/sanitize.util";
/**
* Sanitize decorator for DTO properties
* Automatically sanitizes string values to prevent XSS attacks
*
* Usage:
* ```typescript
* class MyDto {
* @Sanitize()
* @IsString()
* userInput!: string;
* }
* ```
*/
export function Sanitize(): PropertyDecorator {
return Transform(({ value }: { value: unknown }) => {
if (typeof value === "string") {
return sanitizeString(value);
}
return value;
});
}
/**
* SanitizeObject decorator for nested objects
* Recursively sanitizes all string values in an object
*
* Usage:
* ```typescript
* class MyDto {
* @SanitizeObject()
* @IsObject()
* metadata?: Record<string, unknown>;
* }
* ```
*/
export function SanitizeObject(): PropertyDecorator {
return Transform(({ value }: { value: unknown }) => {
if (typeof value === "object" && value !== null) {
return sanitizeObject(value as Record<string, unknown>);
}
return value;
});
}

View File

@@ -0,0 +1,54 @@
/**
* Redis Provider
*
* Provides Redis/Valkey client instance for the application.
*/
import { Logger } from "@nestjs/common";
import type { Provider } from "@nestjs/common";
import Redis from "ioredis";
/**
* Factory function to create Redis client instance
*/
function createRedisClient(): Redis {
const logger = new Logger("RedisProvider");
const valkeyUrl = process.env.VALKEY_URL ?? "redis://localhost:6379";
logger.log(`Connecting to Valkey at ${valkeyUrl}`);
const client = new Redis(valkeyUrl, {
maxRetriesPerRequest: 3,
retryStrategy: (times) => {
const delay = Math.min(times * 50, 2000);
logger.warn(
`Valkey connection retry attempt ${times.toString()}, waiting ${delay.toString()}ms`
);
return delay;
},
reconnectOnError: (err) => {
logger.error("Valkey connection error:", err.message);
return true;
},
});
client.on("connect", () => {
logger.log("Connected to Valkey");
});
client.on("error", (err) => {
logger.error("Valkey error:", err.message);
});
return client;
}
/**
* Redis Client Provider
*
* Provides a singleton Redis client instance for dependency injection.
*/
export const RedisProvider: Provider = {
provide: "REDIS_CLIENT",
useFactory: createRedisClient,
};

View File

@@ -0,0 +1,142 @@
/**
* Redaction Utility Tests
*
* Tests for sensitive data redaction in logs.
*/
import { describe, it, expect } from "vitest";
import { redactSensitiveData, redactUserId, redactInstanceId } from "./redact.util";
describe("Redaction Utilities", () => {
describe("redactUserId", () => {
it("should redact user IDs", () => {
expect(redactUserId("user-12345")).toBe("user-***");
});
it("should handle null/undefined", () => {
expect(redactUserId(null)).toBe("user-***");
expect(redactUserId(undefined)).toBe("user-***");
});
});
describe("redactInstanceId", () => {
it("should redact instance IDs", () => {
expect(redactInstanceId("instance-abc-def")).toBe("instance-***");
});
it("should handle null/undefined", () => {
expect(redactInstanceId(null)).toBe("instance-***");
expect(redactInstanceId(undefined)).toBe("instance-***");
});
});
describe("redactSensitiveData", () => {
it("should redact user IDs", () => {
const data = { userId: "user-123", name: "Test" };
const redacted = redactSensitiveData(data);
expect(redacted.userId).toBe("user-***");
expect(redacted.name).toBe("Test");
});
it("should redact instance IDs", () => {
const data = { instanceId: "instance-456", url: "https://example.com" };
const redacted = redactSensitiveData(data);
expect(redacted.instanceId).toBe("instance-***");
expect(redacted.url).toBe("https://example.com");
});
it("should redact metadata objects", () => {
const data = {
metadata: { secret: "value", public: "data" },
other: "field",
};
const redacted = redactSensitiveData(data);
expect(redacted.metadata).toBe("[REDACTED]");
expect(redacted.other).toBe("field");
});
it("should redact payloads", () => {
const data = {
payload: { command: "execute", args: ["secret"] },
type: "command",
};
const redacted = redactSensitiveData(data);
expect(redacted.payload).toBe("[REDACTED]");
expect(redacted.type).toBe("command");
});
it("should redact private keys", () => {
const data = {
privateKey: "-----BEGIN PRIVATE KEY-----\n...",
publicKey: "-----BEGIN PUBLIC KEY-----\n...",
};
const redacted = redactSensitiveData(data);
expect(redacted.privateKey).toBe("[REDACTED]");
expect(redacted.publicKey).toBe("-----BEGIN PUBLIC KEY-----\n...");
});
it("should redact tokens", () => {
const data = {
token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
oidcToken: "token-value",
};
const redacted = redactSensitiveData(data);
expect(redacted.token).toBe("[REDACTED]");
expect(redacted.oidcToken).toBe("[REDACTED]");
});
it("should handle nested objects", () => {
const data = {
user: {
id: "user-789",
email: "test@example.com",
},
metadata: { nested: "data" },
};
const redacted = redactSensitiveData(data);
expect(redacted.user.id).toBe("user-***");
expect(redacted.user.email).toBe("test@example.com");
expect(redacted.metadata).toBe("[REDACTED]");
});
it("should handle arrays", () => {
const data = {
users: [{ userId: "user-1" }, { userId: "user-2" }],
};
const redacted = redactSensitiveData(data);
expect(redacted.users[0].userId).toBe("user-***");
expect(redacted.users[1].userId).toBe("user-***");
});
it("should preserve non-sensitive data", () => {
const data = {
id: "conn-123",
status: "active",
createdAt: "2024-01-01",
remoteUrl: "https://remote.com",
};
const redacted = redactSensitiveData(data);
expect(redacted).toEqual(data);
});
it("should handle null/undefined", () => {
expect(redactSensitiveData(null)).toBeNull();
expect(redactSensitiveData(undefined)).toBeUndefined();
});
it("should handle primitives", () => {
expect(redactSensitiveData("string")).toBe("string");
expect(redactSensitiveData(123)).toBe(123);
expect(redactSensitiveData(true)).toBe(true);
});
});
});

View File

@@ -0,0 +1,107 @@
/**
* Redaction Utilities
*
* Provides utilities to redact sensitive data from logs to prevent PII leakage.
*/
/**
* Sensitive field names that should be redacted
*/
const SENSITIVE_FIELDS = new Set([
"privateKey",
"token",
"oidcToken",
"accessToken",
"refreshToken",
"password",
"secret",
"apiKey",
"metadata",
"payload",
"signature",
]);
/**
* Redact a user ID to prevent PII leakage
* @param _userId - User ID to redact
* @returns Redacted user ID
*/
export function redactUserId(_userId: string | null | undefined): string {
return "user-***";
}
/**
* Redact an instance ID to prevent system information leakage
* @param _instanceId - Instance ID to redact
* @returns Redacted instance ID
*/
export function redactInstanceId(_instanceId: string | null | undefined): string {
return "instance-***";
}
/**
* Recursively redact sensitive data from an object
* @param data - Data to redact
* @returns Redacted data (creates a new object/array)
*/
export function redactSensitiveData<T>(data: T): T {
// Handle primitives and null/undefined
if (data === null || data === undefined) {
return data;
}
if (typeof data !== "object") {
return data;
}
// Handle arrays
if (Array.isArray(data)) {
const result = data.map((item: unknown) => redactSensitiveData(item));
return result as unknown as T;
}
// Handle objects
const redacted: Record<string, unknown> = {};
for (const [key, value] of Object.entries(data)) {
// Redact sensitive fields
if (SENSITIVE_FIELDS.has(key)) {
redacted[key] = "[REDACTED]";
continue;
}
// Redact user IDs
if (key === "userId" || key === "remoteUserId" || key === "localUserId") {
redacted[key] = redactUserId(value as string);
continue;
}
// Redact instance IDs
if (key === "instanceId" || key === "remoteInstanceId") {
redacted[key] = redactInstanceId(value as string);
continue;
}
// Redact id field within user/instance context
if (key === "id" && typeof value === "string") {
// Check if this might be a user or instance ID based on value format
if (value.startsWith("user-") || value.startsWith("instance-")) {
if (value.startsWith("user-")) {
redacted[key] = redactUserId(value);
} else {
redacted[key] = redactInstanceId(value);
}
continue;
}
}
// Recursively redact nested objects/arrays
if (typeof value === "object" && value !== null) {
redacted[key] = redactSensitiveData(value);
} else {
redacted[key] = value;
}
}
return redacted as T;
}

View File

@@ -0,0 +1,171 @@
/**
* Sanitization Utility Tests
*
* Tests for HTML sanitization and XSS prevention.
*/
import { describe, it, expect } from "vitest";
import { sanitizeString, sanitizeObject, sanitizeArray } from "./sanitize.util";
describe("Sanitization Utilities", () => {
describe("sanitizeString", () => {
it("should remove script tags", () => {
const dirty = '<script>alert("XSS")</script>Hello';
const clean = sanitizeString(dirty);
expect(clean).not.toContain("<script>");
expect(clean).not.toContain("alert");
expect(clean).toContain("Hello");
});
it("should remove javascript: URLs", () => {
const dirty = '<a href="javascript:alert(1)">Click</a>';
const clean = sanitizeString(dirty);
expect(clean).not.toContain("javascript:");
expect(clean).toContain("Click");
});
it("should remove on* event handlers", () => {
const dirty = '<div onclick="alert(1)">Click me</div>';
const clean = sanitizeString(dirty);
expect(clean).not.toContain("onclick");
expect(clean).toContain("Click me");
});
it("should allow safe HTML tags", () => {
const input = "<p>Hello <b>world</b>!</p>";
const clean = sanitizeString(input);
expect(clean).toContain("<p>");
expect(clean).toContain("<b>");
expect(clean).toContain("Hello");
});
it("should handle null and undefined", () => {
expect(sanitizeString(null)).toBe("");
expect(sanitizeString(undefined)).toBe("");
});
it("should handle non-string values", () => {
expect(sanitizeString(123 as any)).toBe("123");
expect(sanitizeString(true as any)).toBe("true");
});
it("should prevent data exfiltration via img tags", () => {
const dirty = '<img src="x" onerror="fetch(\'/steal?data=\'+document.cookie)">';
const clean = sanitizeString(dirty);
expect(clean).not.toContain("onerror");
expect(clean).not.toContain("fetch");
});
it("should remove style tags with malicious CSS", () => {
const dirty = '<style>body { background: url("javascript:alert(1)") }</style>Hello';
const clean = sanitizeString(dirty);
expect(clean).not.toContain("<style>");
expect(clean).not.toContain("javascript:");
});
});
describe("sanitizeObject", () => {
it("should sanitize all string values in an object", () => {
const dirty = {
name: '<script>alert("XSS")</script>John',
description: "Safe text",
nested: {
value: '<img src=x onerror="alert(1)">',
},
};
const clean = sanitizeObject(dirty);
expect(clean.name).not.toContain("<script>");
expect(clean.name).toContain("John");
expect(clean.description).toBe("Safe text");
expect(clean.nested.value).not.toContain("onerror");
});
it("should preserve non-string values", () => {
const input = {
string: "test",
number: 123,
boolean: true,
nullValue: null,
undefinedValue: undefined,
};
const clean = sanitizeObject(input);
expect(clean.number).toBe(123);
expect(clean.boolean).toBe(true);
expect(clean.nullValue).toBeNull();
expect(clean.undefinedValue).toBeUndefined();
});
it("should handle arrays within objects", () => {
const input = {
tags: ["<script>alert(1)</script>safe", "another<script>"],
};
const clean = sanitizeObject(input);
expect(clean.tags[0]).not.toContain("<script>");
expect(clean.tags[0]).toContain("safe");
expect(clean.tags[1]).not.toContain("<script>");
});
it("should handle deeply nested objects", () => {
const input = {
level1: {
level2: {
level3: {
xss: "<script>alert(1)</script>",
},
},
},
};
const clean = sanitizeObject(input);
expect(clean.level1.level2.level3.xss).not.toContain("<script>");
});
});
describe("sanitizeArray", () => {
it("should sanitize all strings in an array", () => {
const dirty = ["<script>alert(1)</script>safe", "clean", '<img src=x onerror="alert(2)">'];
const clean = sanitizeArray(dirty);
expect(clean[0]).not.toContain("<script>");
expect(clean[0]).toContain("safe");
expect(clean[1]).toBe("clean");
expect(clean[2]).not.toContain("onerror");
});
it("should handle arrays with mixed types", () => {
const input = ["<script>test</script>", 123, true, null, { key: "<b>value</b>" }];
const clean = sanitizeArray(input);
expect(clean[0]).not.toContain("<script>");
expect(clean[1]).toBe(123);
expect(clean[2]).toBe(true);
expect(clean[3]).toBeNull();
expect((clean[4] as any).key).toContain("<b>");
});
it("should handle nested arrays", () => {
const input = [["<script>alert(1)</script>", "safe"], ['<img src=x onerror="alert(2)">']];
const clean = sanitizeArray(input);
expect(clean[0][0]).not.toContain("<script>");
expect(clean[0][1]).toBe("safe");
expect(clean[1][0]).not.toContain("onerror");
});
});
});

View File

@@ -0,0 +1,123 @@
/**
* Sanitization Utilities
*
* Provides HTML/XSS sanitization for user-controlled input.
* Uses sanitize-html to prevent XSS attacks.
*/
import sanitizeHtml from "sanitize-html";
/**
* Sanitize options for strict mode (default)
* Allows only safe tags and attributes, removes all scripts and dangerous content
*/
const STRICT_OPTIONS: sanitizeHtml.IOptions = {
allowedTags: ["p", "b", "i", "em", "strong", "a", "br", "ul", "ol", "li"],
allowedAttributes: {
a: ["href"],
},
allowedSchemes: ["http", "https", "mailto"],
disallowedTagsMode: "discard",
};
/**
* Sanitize a string value to prevent XSS attacks
* Removes dangerous HTML tags, scripts, and event handlers
*
* @param value - String to sanitize
* @param options - Optional sanitize-html options (defaults to strict)
* @returns Sanitized string
*/
export function sanitizeString(
value: string | null | undefined,
options: sanitizeHtml.IOptions = STRICT_OPTIONS
): string {
if (value === null || value === undefined) {
return "";
}
// Convert non-strings to strings
const stringValue = typeof value === "string" ? value : String(value);
return sanitizeHtml(stringValue, options);
}
/**
* Sanitize all string values in an object recursively
* Preserves object structure and non-string values
*
* @param obj - Object to sanitize
* @param options - Optional sanitize-html options
* @returns Sanitized object
*/
export function sanitizeObject<T extends Record<string, unknown> | null | undefined>(
obj: T,
options: sanitizeHtml.IOptions = STRICT_OPTIONS
): T {
// Handle null/undefined
if (obj == null) {
return obj;
}
// Handle arrays
if (Array.isArray(obj)) {
return obj.map((item: unknown) => {
if (typeof item === "string") {
return sanitizeString(item, options);
}
if (typeof item === "object" && item !== null) {
return sanitizeObject(item as Record<string, unknown>, options);
}
return item;
}) as unknown as T;
}
// Handle objects
const sanitized: Record<string, unknown> = {};
for (const [key, value] of Object.entries(obj)) {
if (typeof value === "string") {
sanitized[key] = sanitizeString(value, options);
} else if (Array.isArray(value)) {
sanitized[key] = sanitizeArray(value, options);
} else if (typeof value === "object" && value !== null) {
sanitized[key] = sanitizeObject(value as Record<string, unknown>, options);
} else {
sanitized[key] = value;
}
}
return sanitized as T;
}
/**
* Sanitize all string values in an array recursively
* Preserves array structure and non-string values
*
* @param arr - Array to sanitize
* @param options - Optional sanitize-html options
* @returns Sanitized array
*/
export function sanitizeArray<T extends unknown[] = unknown[]>(
arr: T,
options: sanitizeHtml.IOptions = STRICT_OPTIONS
): T {
if (!Array.isArray(arr)) {
return arr;
}
const result = arr.map((item: unknown) => {
if (typeof item === "string") {
return sanitizeString(item, options);
}
if (Array.isArray(item)) {
return sanitizeArray(item as unknown[], options);
}
if (typeof item === "object" && item !== null) {
return sanitizeObject(item as Record<string, unknown>, options);
}
return item;
});
return result as T;
}

View File

@@ -102,7 +102,7 @@ describe("ConnectionService", () => {
provide: SignatureService,
useValue: {
signMessage: vi.fn().mockResolvedValue("mock-signature"),
verifyConnectionRequest: vi.fn().mockReturnValue({ valid: true }),
verifyConnectionRequest: vi.fn().mockResolvedValue({ valid: true }),
},
},
{
@@ -441,7 +441,7 @@ describe("ConnectionService", () => {
});
it("should create pending connection for valid request", async () => {
vi.spyOn(signatureService, "verifyConnectionRequest").mockReturnValue({ valid: true });
vi.spyOn(signatureService, "verifyConnectionRequest").mockResolvedValue({ valid: true });
vi.spyOn(prismaService.federationConnection, "create").mockResolvedValue(mockConnection);
const result = await service.handleIncomingConnectionRequest(mockWorkspaceId, mockRequest);
@@ -451,7 +451,7 @@ describe("ConnectionService", () => {
});
it("should reject request with invalid signature", async () => {
vi.spyOn(signatureService, "verifyConnectionRequest").mockReturnValue({
vi.spyOn(signatureService, "verifyConnectionRequest").mockResolvedValue({
valid: false,
error: "Invalid signature",
});
@@ -462,7 +462,7 @@ describe("ConnectionService", () => {
});
it("should log incoming connection attempt", async () => {
vi.spyOn(signatureService, "verifyConnectionRequest").mockReturnValue({ valid: true });
vi.spyOn(signatureService, "verifyConnectionRequest").mockResolvedValue({ valid: true });
vi.spyOn(prismaService.federationConnection, "create").mockResolvedValue(mockConnection);
const auditSpy = vi.spyOn(auditService, "logIncomingConnectionAttempt");
@@ -477,7 +477,7 @@ describe("ConnectionService", () => {
});
it("should log connection created on success", async () => {
vi.spyOn(signatureService, "verifyConnectionRequest").mockReturnValue({ valid: true });
vi.spyOn(signatureService, "verifyConnectionRequest").mockResolvedValue({ valid: true });
vi.spyOn(prismaService.federationConnection, "create").mockResolvedValue(mockConnection);
const auditSpy = vi.spyOn(auditService, "logIncomingConnectionCreated");
@@ -492,7 +492,7 @@ describe("ConnectionService", () => {
});
it("should log connection rejected on invalid signature", async () => {
vi.spyOn(signatureService, "verifyConnectionRequest").mockReturnValue({
vi.spyOn(signatureService, "verifyConnectionRequest").mockResolvedValue({
valid: false,
error: "Invalid signature",
});

View File

@@ -286,7 +286,7 @@ export class ConnectionService {
});
// Verify signature
const validation = this.signatureService.verifyConnectionRequest(request);
const validation = await this.signatureService.verifyConnectionRequest(request);
if (!validation.valid) {
const errorMsg: string = validation.error ?? "Unknown error";

View File

@@ -6,6 +6,7 @@
import { IsString, IsObject, IsNotEmpty, IsNumber } from "class-validator";
import type { CommandMessage } from "../types/message.types";
import { SanitizeObject } from "../../common/decorators/sanitize.decorator";
/**
* DTO for sending a command to a remote instance
@@ -21,6 +22,7 @@ export class SendCommandDto {
@IsObject()
@IsNotEmpty()
@SanitizeObject()
payload!: Record<string, unknown>;
}
@@ -42,6 +44,7 @@ export class IncomingCommandDto implements CommandMessage {
@IsObject()
@IsNotEmpty()
@SanitizeObject()
payload!: Record<string, unknown>;
@IsNumber()

View File

@@ -5,6 +5,7 @@
*/
import { IsString, IsUrl, IsOptional, IsObject, IsNumber } from "class-validator";
import { Sanitize, SanitizeObject } from "../../common/decorators/sanitize.decorator";
/**
* DTO for initiating a connection
@@ -20,6 +21,7 @@ export class InitiateConnectionDto {
export class AcceptConnectionDto {
@IsOptional()
@IsObject()
@SanitizeObject()
metadata?: Record<string, unknown>;
}
@@ -28,6 +30,7 @@ export class AcceptConnectionDto {
*/
export class RejectConnectionDto {
@IsString()
@Sanitize()
reason!: string;
}
@@ -37,6 +40,7 @@ export class RejectConnectionDto {
export class DisconnectConnectionDto {
@IsOptional()
@IsString()
@Sanitize()
reason?: string;
}

View File

@@ -5,6 +5,7 @@
*/
import { IsString, IsEmail, IsOptional, IsObject, IsArray, IsNumber } from "class-validator";
import { SanitizeObject } from "../../common/decorators/sanitize.decorator";
/**
* DTO for verifying identity from remote instance
@@ -81,6 +82,7 @@ export class CreateIdentityMappingDto {
@IsOptional()
@IsObject()
@SanitizeObject()
metadata?: Record<string, unknown>;
@IsOptional()
@@ -94,5 +96,6 @@ export class CreateIdentityMappingDto {
export class UpdateIdentityMappingDto {
@IsOptional()
@IsObject()
@SanitizeObject()
metadata?: Record<string, unknown>;
}

View File

@@ -0,0 +1,225 @@
/**
* DTO Sanitization Integration Tests
*
* Tests that DTOs properly sanitize XSS attempts.
*/
import { describe, it, expect } from "vitest";
import { plainToInstance } from "class-transformer";
import { validate } from "class-validator";
import {
RejectConnectionDto,
DisconnectConnectionDto,
AcceptConnectionDto,
} from "./connection.dto";
import { CreateIdentityMappingDto, UpdateIdentityMappingDto } from "./identity-linking.dto";
import { SendCommandDto, IncomingCommandDto } from "./command.dto";
describe("DTO Sanitization Integration", () => {
describe("Connection DTOs", () => {
it("should sanitize rejection reason", async () => {
const dirty = {
reason: '<script>alert("XSS")</script>Connection rejected',
};
const dto = plainToInstance(RejectConnectionDto, dirty);
const errors = await validate(dto);
expect(errors).toHaveLength(0);
expect(dto.reason).not.toContain("<script>");
expect(dto.reason).toContain("Connection rejected");
});
it("should sanitize disconnection reason", async () => {
const dirty = {
reason: '<img src=x onerror="alert(1)">Goodbye',
};
const dto = plainToInstance(DisconnectConnectionDto, dirty);
const errors = await validate(dto);
expect(errors).toHaveLength(0);
expect(dto.reason).not.toContain("onerror");
expect(dto.reason).toContain("Goodbye");
});
it("should sanitize acceptance metadata", async () => {
const dirty = {
metadata: {
note: "<script>alert(1)</script>Important",
nested: {
value: "<img src=x onerror=\"fetch('/steal')\">",
},
},
};
const dto = plainToInstance(AcceptConnectionDto, dirty);
const errors = await validate(dto);
expect(errors).toHaveLength(0);
expect(dto.metadata!.note).not.toContain("<script>");
expect(dto.metadata!.note).toContain("Important");
expect((dto.metadata!.nested as any).value).not.toContain("onerror");
});
});
describe("Identity Linking DTOs", () => {
it("should sanitize identity mapping metadata", async () => {
const dirty = {
remoteInstanceId: "instance-123",
remoteUserId: "user-456",
oidcSubject: "sub-789",
email: "test@example.com",
metadata: {
displayName: '<script>alert("XSS")</script>John Doe',
bio: '<img src=x onerror="alert(1)">Developer',
},
};
const dto = plainToInstance(CreateIdentityMappingDto, dirty);
const errors = await validate(dto);
expect(errors).toHaveLength(0);
expect(dto.metadata!.displayName).not.toContain("<script>");
expect(dto.metadata!.displayName).toContain("John Doe");
expect(dto.metadata!.bio).not.toContain("onerror");
});
it("should sanitize update metadata", async () => {
const dirty = {
metadata: {
tags: ["<script>tag1</script>", "<b>tag2</b>"],
},
};
const dto = plainToInstance(UpdateIdentityMappingDto, dirty);
const errors = await validate(dto);
expect(errors).toHaveLength(0);
expect((dto.metadata!.tags as any)[0]).not.toContain("<script>");
expect((dto.metadata!.tags as any)[1]).toContain("<b>");
});
});
describe("Command DTOs", () => {
it("should sanitize command payload", async () => {
const dirty = {
connectionId: "conn-123",
commandType: "execute",
payload: {
script: '<script>alert("XSS")</script>console.log("hello")',
params: {
arg1: '<img src=x onerror="alert(1)">',
},
},
};
const dto = plainToInstance(SendCommandDto, dirty);
const errors = await validate(dto);
expect(errors).toHaveLength(0);
expect(dto.payload.script).not.toContain("<script>");
expect((dto.payload.params as any).arg1).not.toContain("onerror");
});
it("should sanitize incoming command payload", async () => {
const dirty = {
messageId: "msg-123",
instanceId: "instance-456",
commandType: "update",
payload: {
data: '<style>body{background:url("javascript:alert(1)")}</style>Data',
metadata: {
author: "<script>alert(1)</script>Admin",
},
},
timestamp: Date.now(),
signature: "sig-789",
};
const dto = plainToInstance(IncomingCommandDto, dirty);
const errors = await validate(dto);
expect(errors).toHaveLength(0);
expect(dto.payload.data).not.toContain("<style>");
expect(dto.payload.data).not.toContain("javascript:");
expect((dto.payload.metadata as any).author).not.toContain("<script>");
});
it("should handle deeply nested XSS attempts", async () => {
const dirty = {
connectionId: "conn-123",
commandType: "complex",
payload: {
level1: {
level2: {
level3: {
xss: "<script>alert(1)</script>",
array: ['<img src=x onerror="alert(2)">', "safe"],
},
},
},
},
};
const dto = plainToInstance(SendCommandDto, dirty);
const errors = await validate(dto);
expect(errors).toHaveLength(0);
const level3 = (dto.payload.level1 as any).level2.level3;
expect(level3.xss).not.toContain("<script>");
expect(level3.array[0]).not.toContain("onerror");
expect(level3.array[1]).toBe("safe");
});
});
describe("XSS Attack Vectors", () => {
it("should prevent javascript: URL injection", async () => {
const dirty = {
metadata: {
link: '<a href="javascript:alert(1)">Click</a>',
},
};
const dto = plainToInstance(AcceptConnectionDto, dirty);
expect(dto.metadata!.link).not.toContain("javascript:");
});
it("should prevent data exfiltration attempts", async () => {
const dirty = {
reason: "<img src=x onerror=\"fetch('/api/steal?cookie='+document.cookie)\">",
};
const dto = plainToInstance(RejectConnectionDto, dirty);
expect(dto.reason).not.toContain("onerror");
expect(dto.reason).not.toContain("fetch");
expect(dto.reason).not.toContain("document.cookie");
});
it("should prevent CSS-based XSS", async () => {
const dirty = {
metadata: {
style: '<style>@import "http://evil.com/steal.css";</style>',
},
};
const dto = plainToInstance(AcceptConnectionDto, dirty);
expect(dto.metadata!.style).not.toContain("<style>");
expect(dto.metadata!.style).not.toContain("@import");
});
it("should prevent SVG-based XSS", async () => {
const dirty = {
reason: '<svg onload="alert(1)">Test</svg>',
};
const dto = plainToInstance(RejectConnectionDto, dirty);
expect(dto.reason).not.toContain("<svg");
expect(dto.reason).not.toContain("onload");
});
});
});

View File

@@ -12,6 +12,7 @@ import { FederationAuditService } from "./audit.service";
import { ConnectionService } from "./connection.service";
import { AuthGuard } from "../auth/guards/auth.guard";
import { AdminGuard } from "../auth/guards/admin.guard";
import { WorkspaceGuard } from "../common/guards/workspace.guard";
import { CsrfGuard } from "../common/guards/csrf.guard";
import { SkipCsrf } from "../common/decorators/skip-csrf.decorator";
import type { PublicInstanceIdentity } from "./types/instance.types";
@@ -76,11 +77,11 @@ export class FederationController {
/**
* Initiate a connection to a remote instance
* Requires authentication
* Requires authentication and workspace access
* Rate limit: "medium" tier (20 req/min) - authenticated endpoint
*/
@Post("connections/initiate")
@UseGuards(AuthGuard)
@UseGuards(AuthGuard, WorkspaceGuard)
@Throttle({ medium: { limit: 20, ttl: 60000 } })
async initiateConnection(
@Req() req: AuthenticatedRequest,
@@ -99,11 +100,11 @@ export class FederationController {
/**
* Accept a pending connection
* Requires authentication
* Requires authentication and workspace access
* Rate limit: "medium" tier (20 req/min) - authenticated endpoint
*/
@Post("connections/:id/accept")
@UseGuards(AuthGuard)
@UseGuards(AuthGuard, WorkspaceGuard)
@Throttle({ medium: { limit: 20, ttl: 60000 } })
async acceptConnection(
@Req() req: AuthenticatedRequest,
@@ -127,11 +128,11 @@ export class FederationController {
/**
* Reject a pending connection
* Requires authentication
* Requires authentication and workspace access
* Rate limit: "medium" tier (20 req/min) - authenticated endpoint
*/
@Post("connections/:id/reject")
@UseGuards(AuthGuard)
@UseGuards(AuthGuard, WorkspaceGuard)
@Throttle({ medium: { limit: 20, ttl: 60000 } })
async rejectConnection(
@Req() req: AuthenticatedRequest,
@@ -149,11 +150,11 @@ export class FederationController {
/**
* Disconnect an active connection
* Requires authentication
* Requires authentication and workspace access
* Rate limit: "medium" tier (20 req/min) - authenticated endpoint
*/
@Post("connections/:id/disconnect")
@UseGuards(AuthGuard)
@UseGuards(AuthGuard, WorkspaceGuard)
@Throttle({ medium: { limit: 20, ttl: 60000 } })
async disconnectConnection(
@Req() req: AuthenticatedRequest,
@@ -171,11 +172,11 @@ export class FederationController {
/**
* Get all connections for the workspace
* Requires authentication
* Requires authentication and workspace access
* Rate limit: "long" tier (200 req/hour) - read-only endpoint
*/
@Get("connections")
@UseGuards(AuthGuard)
@UseGuards(AuthGuard, WorkspaceGuard)
@Throttle({ long: { limit: 200, ttl: 3600000 } })
async getConnections(
@Req() req: AuthenticatedRequest,
@@ -190,11 +191,11 @@ export class FederationController {
/**
* Get a single connection
* Requires authentication
* Requires authentication and workspace access
* Rate limit: "long" tier (200 req/hour) - read-only endpoint
*/
@Get("connections/:id")
@UseGuards(AuthGuard)
@UseGuards(AuthGuard, WorkspaceGuard)
@Throttle({ long: { limit: 200, ttl: 3600000 } })
async getConnection(
@Req() req: AuthenticatedRequest,

View File

@@ -20,6 +20,7 @@ import { OIDCService } from "./oidc.service";
import { CommandService } from "./command.service";
import { FederationAgentService } from "./federation-agent.service";
import { PrismaModule } from "../prisma/prisma.module";
import { RedisProvider } from "../common/providers/redis.provider";
@Module({
imports: [
@@ -52,6 +53,7 @@ import { PrismaModule } from "../prisma/prisma.module";
],
controllers: [FederationController, FederationAuthController],
providers: [
RedisProvider,
FederationService,
CryptoService,
FederationAuditService,

View File

@@ -9,10 +9,12 @@ import { Test, TestingModule } from "@nestjs/testing";
import { SignatureService } from "./signature.service";
import { FederationService } from "./federation.service";
import { generateKeyPairSync } from "crypto";
import type Redis from "ioredis";
describe("SignatureService", () => {
let service: SignatureService;
let mockFederationService: Partial<FederationService>;
let mockRedis: Partial<Redis>;
// Test keypair
const { publicKey, privateKey } = generateKeyPairSync("rsa", {
@@ -37,6 +39,12 @@ describe("SignatureService", () => {
}),
};
mockRedis = {
get: vi.fn().mockResolvedValue(null),
set: vi.fn().mockResolvedValue("OK"),
setex: vi.fn().mockResolvedValue("OK"),
};
const module: TestingModule = await Test.createTestingModule({
providers: [
SignatureService,
@@ -44,6 +52,10 @@ describe("SignatureService", () => {
provide: FederationService,
useValue: mockFederationService,
},
{
provide: "REDIS_CLIENT",
useValue: mockRedis,
},
],
}).compile();
@@ -168,16 +180,16 @@ describe("SignatureService", () => {
expect(result).toBe(true);
});
it("should accept timestamps within 5 minutes", () => {
const fourMinutesAgo = Date.now() - 4 * 60 * 1000;
const result = service.validateTimestamp(fourMinutesAgo);
it("should accept timestamps within 60 seconds", () => {
const fiftySecondsAgo = Date.now() - 50 * 1000;
const result = service.validateTimestamp(fiftySecondsAgo);
expect(result).toBe(true);
});
it("should reject timestamps older than 5 minutes", () => {
const sixMinutesAgo = Date.now() - 6 * 60 * 1000;
const result = service.validateTimestamp(sixMinutesAgo);
it("should reject timestamps older than 60 seconds", () => {
const twoMinutesAgo = Date.now() - 2 * 60 * 1000;
const result = service.validateTimestamp(twoMinutesAgo);
expect(result).toBe(false);
});
@@ -226,7 +238,7 @@ describe("SignatureService", () => {
});
describe("verifyConnectionRequest", () => {
it("should verify a valid connection request", () => {
it("should verify a valid connection request", async () => {
const timestamp = Date.now();
const request = {
instanceId: "instance-123",
@@ -239,13 +251,14 @@ describe("SignatureService", () => {
const signature = service.sign(request, privateKey);
const signedRequest = { ...request, signature };
const result = service.verifyConnectionRequest(signedRequest);
const result = await service.verifyConnectionRequest(signedRequest);
expect(result.valid).toBe(true);
expect(result.error).toBeUndefined();
expect(mockRedis.setex).toHaveBeenCalled();
});
it("should reject request with invalid signature", () => {
it("should reject request with invalid signature", async () => {
const request = {
instanceId: "instance-123",
instanceUrl: "https://test.example.com",
@@ -255,13 +268,13 @@ describe("SignatureService", () => {
signature: "invalid-signature",
};
const result = service.verifyConnectionRequest(request);
const result = await service.verifyConnectionRequest(request);
expect(result.valid).toBe(false);
expect(result.error).toContain("signature");
});
it("should reject request with expired timestamp", () => {
it("should reject request with expired timestamp", async () => {
const expiredTimestamp = Date.now() - 10 * 60 * 1000; // 10 minutes ago
const request = {
instanceId: "instance-123",
@@ -274,10 +287,92 @@ describe("SignatureService", () => {
const signature = service.sign(request, privateKey);
const signedRequest = { ...request, signature };
const result = service.verifyConnectionRequest(signedRequest);
const result = await service.verifyConnectionRequest(signedRequest);
expect(result.valid).toBe(false);
expect(result.error).toContain("timestamp");
});
});
describe("replay attack prevention", () => {
it("should reject replayed message with same signature", async () => {
const timestamp = Date.now();
const request = {
instanceId: "instance-123",
instanceUrl: "https://test.example.com",
publicKey,
capabilities: {},
timestamp,
};
const signature = service.sign(request, privateKey);
const signedRequest = { ...request, signature };
// First request should succeed
const result1 = await service.verifyConnectionRequest(signedRequest);
expect(result1.valid).toBe(true);
// Mock Redis to indicate nonce was already used
mockRedis.get = vi.fn().mockResolvedValue("1");
// Second request with same signature should be rejected
const result2 = await service.verifyConnectionRequest(signedRequest);
expect(result2.valid).toBe(false);
expect(result2.error).toContain("replay");
});
it("should store nonce with 60 second TTL", async () => {
const timestamp = Date.now();
const request = {
instanceId: "instance-123",
instanceUrl: "https://test.example.com",
publicKey,
capabilities: {},
timestamp,
};
const signature = service.sign(request, privateKey);
const signedRequest = { ...request, signature };
await service.verifyConnectionRequest(signedRequest);
expect(mockRedis.setex).toHaveBeenCalledWith(expect.stringContaining("nonce:"), 60, "1");
});
it("should allow different messages with different signatures", async () => {
const timestamp1 = Date.now();
const request1 = {
instanceId: "instance-123",
instanceUrl: "https://test.example.com",
publicKey,
capabilities: {},
timestamp: timestamp1,
};
const signature1 = service.sign(request1, privateKey);
const signedRequest1 = { ...request1, signature: signature1 };
const result1 = await service.verifyConnectionRequest(signedRequest1);
expect(result1.valid).toBe(true);
// Different timestamp creates different signature
const timestamp2 = Date.now() + 1;
const request2 = {
instanceId: "instance-123",
instanceUrl: "https://test.example.com",
publicKey,
capabilities: {},
timestamp: timestamp2,
};
const signature2 = service.sign(request2, privateKey);
const signedRequest2 = { ...request2, signature: signature2 };
// Reset mock to simulate nonce not found
mockRedis.get = vi.fn().mockResolvedValue(null);
const result2 = await service.verifyConnectionRequest(signedRequest2);
expect(result2.valid).toBe(true);
});
});
});

View File

@@ -4,7 +4,7 @@
* Handles message signing and verification for federation protocol.
*/
import { Injectable, Logger } from "@nestjs/common";
import { Injectable, Logger, Inject } from "@nestjs/common";
import { createSign, createVerify } from "crypto";
import { FederationService } from "./federation.service";
import type {
@@ -12,14 +12,19 @@ import type {
SignatureValidationResult,
ConnectionRequest,
} from "./types/connection.types";
import type Redis from "ioredis";
@Injectable()
export class SignatureService {
private readonly logger = new Logger(SignatureService.name);
private readonly TIMESTAMP_TOLERANCE_MS = 5 * 60 * 1000; // 5 minutes
private readonly TIMESTAMP_TOLERANCE_MS = 60 * 1000; // 60 seconds
private readonly CLOCK_SKEW_TOLERANCE_MS = 60 * 1000; // 1 minute for future timestamps
private readonly NONCE_TTL_SECONDS = 60; // Nonce TTL matches tolerance window
constructor(private readonly federationService: FederationService) {}
constructor(
private readonly federationService: FederationService,
@Inject("REDIS_CLIENT") private readonly redis: Redis
) {}
/**
* Sign a message with a private key
@@ -153,7 +158,7 @@ export class SignatureService {
/**
* Verify a connection request signature
*/
verifyConnectionRequest(request: ConnectionRequest): SignatureValidationResult {
async verifyConnectionRequest(request: ConnectionRequest): Promise<SignatureValidationResult> {
// Extract signature and create message for verification
const { signature, ...message } = request;
@@ -165,14 +170,30 @@ export class SignatureService {
};
}
// Check for replay attack (nonce already used)
const nonceKey = `nonce:${signature}`;
const nonceExists = await this.redis.get(nonceKey);
if (nonceExists) {
this.logger.warn("Replay attack detected: signature already used");
return {
valid: false,
error: "Request rejected: potential replay attack detected",
};
}
// Verify signature using the public key from the request
const result = this.verify(message, signature, request.publicKey);
if (!result.valid) {
const errorMsg = result.error ?? "Unknown error";
this.logger.warn(`Connection request signature verification failed: ${errorMsg}`);
return result;
}
// Store nonce to prevent replay attacks
await this.redis.setex(nonceKey, this.NONCE_TTL_SECONDS, "1");
return result;
}

View File

@@ -0,0 +1,200 @@
/**
* Workspace Access Integration Tests
*
* Tests that workspace-scoped federation endpoints enforce workspace access.
*/
import { describe, it, expect, beforeEach, vi } from "vitest";
import { WorkspaceGuard } from "../common/guards/workspace.guard";
import { PrismaService } from "../prisma/prisma.service";
import { ForbiddenException } from "@nestjs/common";
import type { AuthenticatedRequest } from "../common/types/user.types";
describe("Workspace Access Control - Federation", () => {
let prismaService: PrismaService;
let workspaceGuard: WorkspaceGuard;
beforeEach(() => {
const mockPrismaService = {
workspaceMember: {
findUnique: vi.fn(),
},
};
prismaService = mockPrismaService as unknown as PrismaService;
workspaceGuard = new WorkspaceGuard(prismaService);
});
describe("Workspace membership verification", () => {
it("should allow access when user is workspace member", async () => {
const mockRequest: Partial<AuthenticatedRequest> = {
user: {
id: "user-123",
email: "test@example.com",
workspaceId: "workspace-456",
},
headers: {
"x-workspace-id": "workspace-456",
},
params: {},
body: {},
} as AuthenticatedRequest;
// Mock workspace membership exists
vi.spyOn(prismaService.workspaceMember, "findUnique").mockResolvedValue({
workspaceId: "workspace-456",
userId: "user-123",
role: "ADMIN",
createdAt: new Date(),
updatedAt: new Date(),
} as any);
const canActivate = await workspaceGuard.canActivate({
switchToHttp: () => ({
getRequest: () => mockRequest,
}),
} as any);
expect(canActivate).toBe(true);
});
it("should deny access when user is not workspace member", async () => {
const mockRequest: Partial<AuthenticatedRequest> = {
user: {
id: "user-123",
email: "test@example.com",
},
headers: {
"x-workspace-id": "workspace-999",
},
params: {},
body: {},
} as AuthenticatedRequest;
// Mock workspace membership does not exist
vi.spyOn(prismaService.workspaceMember, "findUnique").mockResolvedValue(null);
await expect(
workspaceGuard.canActivate({
switchToHttp: () => ({
getRequest: () => mockRequest,
}),
} as any)
).rejects.toThrow(ForbiddenException);
});
it("should deny access when workspace ID is missing", async () => {
const mockRequest: Partial<AuthenticatedRequest> = {
user: {
id: "user-123",
email: "test@example.com",
},
headers: {},
params: {},
body: {},
} as AuthenticatedRequest;
await expect(
workspaceGuard.canActivate({
switchToHttp: () => ({
getRequest: () => mockRequest,
}),
} as any)
).rejects.toThrow("Workspace ID is required");
});
it("should check workspace ID from URL parameter", async () => {
const mockRequest: Partial<AuthenticatedRequest> = {
user: {
id: "user-123",
email: "test@example.com",
},
headers: {},
params: {
workspaceId: "workspace-789",
},
} as any;
vi.spyOn(prismaService.workspaceMember, "findUnique").mockResolvedValue({
workspaceId: "workspace-789",
userId: "user-123",
role: "MEMBER",
createdAt: new Date(),
updatedAt: new Date(),
} as any);
const canActivate = await workspaceGuard.canActivate({
switchToHttp: () => ({
getRequest: () => mockRequest,
}),
} as any);
expect(canActivate).toBe(true);
expect(prismaService.workspaceMember.findUnique).toHaveBeenCalledWith({
where: {
workspaceId_userId: {
workspaceId: "workspace-789",
userId: "user-123",
},
},
});
});
it("should check workspace ID from request body", async () => {
const mockRequest: Partial<AuthenticatedRequest> = {
user: {
id: "user-123",
email: "test@example.com",
},
headers: {},
params: {},
body: {
workspaceId: "workspace-111",
},
} as any;
vi.spyOn(prismaService.workspaceMember, "findUnique").mockResolvedValue({
workspaceId: "workspace-111",
userId: "user-123",
role: "ADMIN",
createdAt: new Date(),
updatedAt: new Date(),
} as any);
const canActivate = await workspaceGuard.canActivate({
switchToHttp: () => ({
getRequest: () => mockRequest,
}),
} as any);
expect(canActivate).toBe(true);
});
});
describe("Workspace isolation", () => {
it("should prevent cross-workspace access", async () => {
const mockRequest: Partial<AuthenticatedRequest> = {
user: {
id: "user-123",
email: "test@example.com",
},
headers: {
"x-workspace-id": "workspace-attacker",
},
params: {},
body: {},
} as AuthenticatedRequest;
// User is NOT a member of the requested workspace
vi.spyOn(prismaService.workspaceMember, "findUnique").mockResolvedValue(null);
await expect(
workspaceGuard.canActivate({
switchToHttp: () => ({
getRequest: () => mockRequest,
}),
} as any)
).rejects.toThrow(ForbiddenException);
});
});
});

View File

@@ -0,0 +1,20 @@
# QA Remediation Report
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/common/decorators/sanitize.decorator.ts
**Tool Used:** Write
**Epic:** general
**Iteration:** 1
**Generated:** 2026-02-03 21:44:16
## Status
Pending QA validation
## Next Steps
This report was created by the QA automation hook.
To process this report, run:
```bash
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-common-decorators-sanitize.decorator.ts_20260203-2144_1_remediation_needed.md"
```

View File

@@ -0,0 +1,20 @@
# QA Remediation Report
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/common/decorators/sanitize.decorator.ts
**Tool Used:** Edit
**Epic:** general
**Iteration:** 1
**Generated:** 2026-02-03 21:45:25
## Status
Pending QA validation
## Next Steps
This report was created by the QA automation hook.
To process this report, run:
```bash
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-common-decorators-sanitize.decorator.ts_20260203-2145_1_remediation_needed.md"
```

View File

@@ -0,0 +1,20 @@
# QA Remediation Report
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/common/decorators/sanitize.decorator.ts
**Tool Used:** Edit
**Epic:** general
**Iteration:** 2
**Generated:** 2026-02-03 21:45:31
## Status
Pending QA validation
## Next Steps
This report was created by the QA automation hook.
To process this report, run:
```bash
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-common-decorators-sanitize.decorator.ts_20260203-2145_2_remediation_needed.md"
```

View File

@@ -0,0 +1,20 @@
# QA Remediation Report
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/common/decorators/sanitize.decorator.ts
**Tool Used:** Edit
**Epic:** general
**Iteration:** 1
**Generated:** 2026-02-03 21:46:21
## Status
Pending QA validation
## Next Steps
This report was created by the QA automation hook.
To process this report, run:
```bash
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-common-decorators-sanitize.decorator.ts_20260203-2146_1_remediation_needed.md"
```

View File

@@ -0,0 +1,20 @@
# QA Remediation Report
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/common/providers/redis.provider.ts
**Tool Used:** Write
**Epic:** general
**Iteration:** 1
**Generated:** 2026-02-03 21:40:52
## Status
Pending QA validation
## Next Steps
This report was created by the QA automation hook.
To process this report, run:
```bash
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-common-providers-redis.provider.ts_20260203-2140_1_remediation_needed.md"
```

View File

@@ -0,0 +1,20 @@
# QA Remediation Report
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/common/providers/redis.provider.ts
**Tool Used:** Edit
**Epic:** general
**Iteration:** 1
**Generated:** 2026-02-03 21:42:52
## Status
Pending QA validation
## Next Steps
This report was created by the QA automation hook.
To process this report, run:
```bash
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-common-providers-redis.provider.ts_20260203-2142_1_remediation_needed.md"
```

View File

@@ -0,0 +1,20 @@
# QA Remediation Report
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/common/utils/redact.util.spec.ts
**Tool Used:** Write
**Epic:** general
**Iteration:** 1
**Generated:** 2026-02-03 21:50:53
## Status
Pending QA validation
## Next Steps
This report was created by the QA automation hook.
To process this report, run:
```bash
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-common-utils-redact.util.spec.ts_20260203-2150_1_remediation_needed.md"
```

View File

@@ -0,0 +1,20 @@
# QA Remediation Report
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/common/utils/redact.util.ts
**Tool Used:** Write
**Epic:** general
**Iteration:** 1
**Generated:** 2026-02-03 21:51:08
## Status
Pending QA validation
## Next Steps
This report was created by the QA automation hook.
To process this report, run:
```bash
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-common-utils-redact.util.ts_20260203-2151_1_remediation_needed.md"
```

View File

@@ -0,0 +1,20 @@
# QA Remediation Report
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/common/utils/redact.util.ts
**Tool Used:** Edit
**Epic:** general
**Iteration:** 2
**Generated:** 2026-02-03 21:51:26
## Status
Pending QA validation
## Next Steps
This report was created by the QA automation hook.
To process this report, run:
```bash
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-common-utils-redact.util.ts_20260203-2151_2_remediation_needed.md"
```

View File

@@ -0,0 +1,20 @@
# QA Remediation Report
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/common/utils/redact.util.ts
**Tool Used:** Edit
**Epic:** general
**Iteration:** 3
**Generated:** 2026-02-03 21:51:59
## Status
Pending QA validation
## Next Steps
This report was created by the QA automation hook.
To process this report, run:
```bash
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-common-utils-redact.util.ts_20260203-2151_3_remediation_needed.md"
```

View File

@@ -0,0 +1,20 @@
# QA Remediation Report
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/common/utils/redact.util.ts
**Tool Used:** Edit
**Epic:** general
**Iteration:** 1
**Generated:** 2026-02-03 21:52:03
## Status
Pending QA validation
## Next Steps
This report was created by the QA automation hook.
To process this report, run:
```bash
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-common-utils-redact.util.ts_20260203-2152_1_remediation_needed.md"
```

View File

@@ -0,0 +1,20 @@
# QA Remediation Report
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/common/utils/sanitize.util.spec.ts
**Tool Used:** Write
**Epic:** general
**Iteration:** 1
**Generated:** 2026-02-03 21:43:46
## Status
Pending QA validation
## Next Steps
This report was created by the QA automation hook.
To process this report, run:
```bash
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-common-utils-sanitize.util.spec.ts_20260203-2143_1_remediation_needed.md"
```

View File

@@ -0,0 +1,20 @@
# QA Remediation Report
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/common/utils/sanitize.util.ts
**Tool Used:** Write
**Epic:** general
**Iteration:** 1
**Generated:** 2026-02-03 21:44:05
## Status
Pending QA validation
## Next Steps
This report was created by the QA automation hook.
To process this report, run:
```bash
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-common-utils-sanitize.util.ts_20260203-2144_1_remediation_needed.md"
```

View File

@@ -0,0 +1,20 @@
# QA Remediation Report
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/common/utils/sanitize.util.ts
**Tool Used:** Edit
**Epic:** general
**Iteration:** 1
**Generated:** 2026-02-03 21:46:12
## Status
Pending QA validation
## Next Steps
This report was created by the QA automation hook.
To process this report, run:
```bash
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-common-utils-sanitize.util.ts_20260203-2146_1_remediation_needed.md"
```

View File

@@ -0,0 +1,20 @@
# QA Remediation Report
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/common/utils/sanitize.util.ts
**Tool Used:** Edit
**Epic:** general
**Iteration:** 2
**Generated:** 2026-02-03 21:46:55
## Status
Pending QA validation
## Next Steps
This report was created by the QA automation hook.
To process this report, run:
```bash
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-common-utils-sanitize.util.ts_20260203-2146_2_remediation_needed.md"
```

View File

@@ -0,0 +1,20 @@
# QA Remediation Report
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/common/utils/sanitize.util.ts
**Tool Used:** Edit
**Epic:** general
**Iteration:** 1
**Generated:** 2026-02-03 21:47:02
## Status
Pending QA validation
## Next Steps
This report was created by the QA automation hook.
To process this report, run:
```bash
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-common-utils-sanitize.util.ts_20260203-2147_1_remediation_needed.md"
```

View File

@@ -0,0 +1,20 @@
# QA Remediation Report
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/common/utils/sanitize.util.ts
**Tool Used:** Edit
**Epic:** general
**Iteration:** 2
**Generated:** 2026-02-03 21:47:27
## Status
Pending QA validation
## Next Steps
This report was created by the QA automation hook.
To process this report, run:
```bash
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-common-utils-sanitize.util.ts_20260203-2147_2_remediation_needed.md"
```

View File

@@ -0,0 +1,20 @@
# QA Remediation Report
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/federation/command.service.spec.ts
**Tool Used:** Edit
**Epic:** general
**Iteration:** 1
**Generated:** 2026-02-03 21:32:08
## Status
Pending QA validation
## Next Steps
This report was created by the QA automation hook.
To process this report, run:
```bash
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-federation-command.service.spec.ts_20260203-2132_1_remediation_needed.md"
```

View File

@@ -0,0 +1,20 @@
# QA Remediation Report
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/federation/command.service.spec.ts
**Tool Used:** Edit
**Epic:** general
**Iteration:** 2
**Generated:** 2026-02-03 21:32:14
## Status
Pending QA validation
## Next Steps
This report was created by the QA automation hook.
To process this report, run:
```bash
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-federation-command.service.spec.ts_20260203-2132_2_remediation_needed.md"
```

View File

@@ -0,0 +1,20 @@
# QA Remediation Report
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/federation/command.service.ts
**Tool Used:** Edit
**Epic:** general
**Iteration:** 1
**Generated:** 2026-02-03 21:32:27
## Status
Pending QA validation
## Next Steps
This report was created by the QA automation hook.
To process this report, run:
```bash
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-federation-command.service.ts_20260203-2132_1_remediation_needed.md"
```

View File

@@ -0,0 +1,20 @@
# QA Remediation Report
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/federation/command.service.ts
**Tool Used:** Edit
**Epic:** general
**Iteration:** 2
**Generated:** 2026-02-03 21:32:33
## Status
Pending QA validation
## Next Steps
This report was created by the QA automation hook.
To process this report, run:
```bash
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-federation-command.service.ts_20260203-2132_2_remediation_needed.md"
```

View File

@@ -0,0 +1,20 @@
# QA Remediation Report
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/federation/connection.service.spec.ts
**Tool Used:** Edit
**Epic:** general
**Iteration:** 1
**Generated:** 2026-02-03 21:41:26
## Status
Pending QA validation
## Next Steps
This report was created by the QA automation hook.
To process this report, run:
```bash
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-federation-connection.service.spec.ts_20260203-2141_1_remediation_needed.md"
```

View File

@@ -0,0 +1,20 @@
# QA Remediation Report
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/federation/connection.service.spec.ts
**Tool Used:** Edit
**Epic:** general
**Iteration:** 2
**Generated:** 2026-02-03 21:41:46
## Status
Pending QA validation
## Next Steps
This report was created by the QA automation hook.
To process this report, run:
```bash
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-federation-connection.service.spec.ts_20260203-2141_2_remediation_needed.md"
```

View File

@@ -0,0 +1,20 @@
# QA Remediation Report
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/federation/connection.service.ts
**Tool Used:** Edit
**Epic:** general
**Iteration:** 1
**Generated:** 2026-02-03 21:41:19
## Status
Pending QA validation
## Next Steps
This report was created by the QA automation hook.
To process this report, run:
```bash
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-federation-connection.service.ts_20260203-2141_1_remediation_needed.md"
```

View File

@@ -0,0 +1,20 @@
# QA Remediation Report
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/federation/crypto.service.spec.ts
**Tool Used:** Edit
**Epic:** general
**Iteration:** 1
**Generated:** 2026-02-03 21:34:34
## Status
Pending QA validation
## Next Steps
This report was created by the QA automation hook.
To process this report, run:
```bash
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-federation-crypto.service.spec.ts_20260203-2134_1_remediation_needed.md"
```

View File

@@ -0,0 +1,20 @@
# QA Remediation Report
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/federation/crypto.service.spec.ts
**Tool Used:** Edit
**Epic:** general
**Iteration:** 2
**Generated:** 2026-02-03 21:34:43
## Status
Pending QA validation
## Next Steps
This report was created by the QA automation hook.
To process this report, run:
```bash
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-federation-crypto.service.spec.ts_20260203-2134_2_remediation_needed.md"
```

View File

@@ -0,0 +1,20 @@
# QA Remediation Report
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/federation/crypto.service.spec.ts
**Tool Used:** Edit
**Epic:** general
**Iteration:** 1
**Generated:** 2026-02-03 21:35:03
## Status
Pending QA validation
## Next Steps
This report was created by the QA automation hook.
To process this report, run:
```bash
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-federation-crypto.service.spec.ts_20260203-2135_1_remediation_needed.md"
```

View File

@@ -0,0 +1,20 @@
# QA Remediation Report
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/federation/crypto.service.ts
**Tool Used:** Edit
**Epic:** general
**Iteration:** 1
**Generated:** 2026-02-03 21:34:54
## Status
Pending QA validation
## Next Steps
This report was created by the QA automation hook.
To process this report, run:
```bash
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-federation-crypto.service.ts_20260203-2134_1_remediation_needed.md"
```

View File

@@ -0,0 +1,20 @@
# QA Remediation Report
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/federation/dto/command.dto.ts
**Tool Used:** Edit
**Epic:** general
**Iteration:** 1
**Generated:** 2026-02-03 21:44:42
## Status
Pending QA validation
## Next Steps
This report was created by the QA automation hook.
To process this report, run:
```bash
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-federation-dto-command.dto.ts_20260203-2144_1_remediation_needed.md"
```

View File

@@ -0,0 +1,20 @@
# QA Remediation Report
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/federation/dto/command.dto.ts
**Tool Used:** Edit
**Epic:** general
**Iteration:** 2
**Generated:** 2026-02-03 21:44:50
## Status
Pending QA validation
## Next Steps
This report was created by the QA automation hook.
To process this report, run:
```bash
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-federation-dto-command.dto.ts_20260203-2144_2_remediation_needed.md"
```

View File

@@ -0,0 +1,20 @@
# QA Remediation Report
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/federation/dto/connection.dto.ts
**Tool Used:** Edit
**Epic:** general
**Iteration:** 1
**Generated:** 2026-02-03 21:44:21
## Status
Pending QA validation
## Next Steps
This report was created by the QA automation hook.
To process this report, run:
```bash
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-federation-dto-connection.dto.ts_20260203-2144_1_remediation_needed.md"
```

View File

@@ -0,0 +1,20 @@
# QA Remediation Report
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/federation/dto/connection.dto.ts
**Tool Used:** Edit
**Epic:** general
**Iteration:** 2
**Generated:** 2026-02-03 21:44:26
## Status
Pending QA validation
## Next Steps
This report was created by the QA automation hook.
To process this report, run:
```bash
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-federation-dto-connection.dto.ts_20260203-2144_2_remediation_needed.md"
```

View File

@@ -0,0 +1,20 @@
# QA Remediation Report
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/federation/dto/identity-linking.dto.ts
**Tool Used:** Edit
**Epic:** general
**Iteration:** 1
**Generated:** 2026-02-03 21:44:31
## Status
Pending QA validation
## Next Steps
This report was created by the QA automation hook.
To process this report, run:
```bash
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-federation-dto-identity-linking.dto.ts_20260203-2144_1_remediation_needed.md"
```

View File

@@ -0,0 +1,20 @@
# QA Remediation Report
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/federation/dto/identity-linking.dto.ts
**Tool Used:** Edit
**Epic:** general
**Iteration:** 2
**Generated:** 2026-02-03 21:44:37
## Status
Pending QA validation
## Next Steps
This report was created by the QA automation hook.
To process this report, run:
```bash
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-federation-dto-identity-linking.dto.ts_20260203-2144_2_remediation_needed.md"
```

View File

@@ -0,0 +1,20 @@
# QA Remediation Report
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/federation/dto/sanitization.integration.spec.ts
**Tool Used:** Write
**Epic:** general
**Iteration:** 1
**Generated:** 2026-02-03 21:45:15
## Status
Pending QA validation
## Next Steps
This report was created by the QA automation hook.
To process this report, run:
```bash
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-federation-dto-sanitization.integration.spec.ts_20260203-2145_1_remediation_needed.md"
```

View File

@@ -0,0 +1,20 @@
# QA Remediation Report
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/federation/federation.controller.ts
**Tool Used:** Edit
**Epic:** general
**Iteration:** 1
**Generated:** 2026-02-03 21:49:34
## Status
Pending QA validation
## Next Steps
This report was created by the QA automation hook.
To process this report, run:
```bash
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-federation-federation.controller.ts_20260203-2149_1_remediation_needed.md"
```

View File

@@ -0,0 +1,20 @@
# QA Remediation Report
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/federation/federation.controller.ts
**Tool Used:** Edit
**Epic:** general
**Iteration:** 2
**Generated:** 2026-02-03 21:49:39
## Status
Pending QA validation
## Next Steps
This report was created by the QA automation hook.
To process this report, run:
```bash
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-federation-federation.controller.ts_20260203-2149_2_remediation_needed.md"
```

View File

@@ -0,0 +1,20 @@
# QA Remediation Report
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/federation/federation.controller.ts
**Tool Used:** Edit
**Epic:** general
**Iteration:** 1
**Generated:** 2026-02-03 21:50:03
## Status
Pending QA validation
## Next Steps
This report was created by the QA automation hook.
To process this report, run:
```bash
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-federation-federation.controller.ts_20260203-2150_1_remediation_needed.md"
```

View File

@@ -0,0 +1,20 @@
# QA Remediation Report
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/federation/federation.module.ts
**Tool Used:** Edit
**Epic:** general
**Iteration:** 1
**Generated:** 2026-02-03 21:41:01
## Status
Pending QA validation
## Next Steps
This report was created by the QA automation hook.
To process this report, run:
```bash
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-federation-federation.module.ts_20260203-2141_1_remediation_needed.md"
```

View File

@@ -0,0 +1,20 @@
# QA Remediation Report
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/federation/federation.module.ts
**Tool Used:** Edit
**Epic:** general
**Iteration:** 2
**Generated:** 2026-02-03 21:41:04
## Status
Pending QA validation
## Next Steps
This report was created by the QA automation hook.
To process this report, run:
```bash
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-federation-federation.module.ts_20260203-2141_2_remediation_needed.md"
```

View File

@@ -0,0 +1,20 @@
# QA Remediation Report
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/federation/federation.service.spec.ts
**Tool Used:** Edit
**Epic:** general
**Iteration:** 1
**Generated:** 2026-02-03 21:33:33
## Status
Pending QA validation
## Next Steps
This report was created by the QA automation hook.
To process this report, run:
```bash
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-federation-federation.service.spec.ts_20260203-2133_1_remediation_needed.md"
```

View File

@@ -0,0 +1,20 @@
# QA Remediation Report
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/federation/federation.service.ts
**Tool Used:** Edit
**Epic:** general
**Iteration:** 1
**Generated:** 2026-02-03 21:33:44
## Status
Pending QA validation
## Next Steps
This report was created by the QA automation hook.
To process this report, run:
```bash
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-federation-federation.service.ts_20260203-2133_1_remediation_needed.md"
```

View File

@@ -0,0 +1,20 @@
# QA Remediation Report
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/federation/identity-linking.controller.spec.ts
**Tool Used:** Edit
**Epic:** general
**Iteration:** 1
**Generated:** 2026-02-03 21:36:16
## Status
Pending QA validation
## Next Steps
This report was created by the QA automation hook.
To process this report, run:
```bash
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-federation-identity-linking.controller.spec.ts_20260203-2136_1_remediation_needed.md"
```

View File

@@ -0,0 +1,20 @@
# QA Remediation Report
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/federation/identity-linking.controller.ts
**Tool Used:** Edit
**Epic:** general
**Iteration:** 1
**Generated:** 2026-02-03 21:35:54
## Status
Pending QA validation
## Next Steps
This report was created by the QA automation hook.
To process this report, run:
```bash
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-federation-identity-linking.controller.ts_20260203-2135_1_remediation_needed.md"
```

View File

@@ -0,0 +1,20 @@
# QA Remediation Report
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/federation/identity-linking.controller.ts
**Tool Used:** Edit
**Epic:** general
**Iteration:** 1
**Generated:** 2026-02-03 21:36:00
## Status
Pending QA validation
## Next Steps
This report was created by the QA automation hook.
To process this report, run:
```bash
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-federation-identity-linking.controller.ts_20260203-2136_1_remediation_needed.md"
```

View File

@@ -0,0 +1,20 @@
# QA Remediation Report
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/federation/query.service.spec.ts
**Tool Used:** Edit
**Epic:** general
**Iteration:** 1
**Generated:** 2026-02-03 21:31:21
## Status
Pending QA validation
## Next Steps
This report was created by the QA automation hook.
To process this report, run:
```bash
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-federation-query.service.spec.ts_20260203-2131_1_remediation_needed.md"
```

View File

@@ -0,0 +1,20 @@
# QA Remediation Report
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/federation/query.service.spec.ts
**Tool Used:** Edit
**Epic:** general
**Iteration:** 2
**Generated:** 2026-02-03 21:31:26
## Status
Pending QA validation
## Next Steps
This report was created by the QA automation hook.
To process this report, run:
```bash
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-federation-query.service.spec.ts_20260203-2131_2_remediation_needed.md"
```

View File

@@ -0,0 +1,20 @@
# QA Remediation Report
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/federation/query.service.spec.ts
**Tool Used:** Edit
**Epic:** general
**Iteration:** 3
**Generated:** 2026-02-03 21:31:33
## Status
Pending QA validation
## Next Steps
This report was created by the QA automation hook.
To process this report, run:
```bash
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-federation-query.service.spec.ts_20260203-2131_3_remediation_needed.md"
```

View File

@@ -0,0 +1,20 @@
# QA Remediation Report
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/federation/query.service.ts
**Tool Used:** Edit
**Epic:** general
**Iteration:** 1
**Generated:** 2026-02-03 21:31:53
## Status
Pending QA validation
## Next Steps
This report was created by the QA automation hook.
To process this report, run:
```bash
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-federation-query.service.ts_20260203-2131_1_remediation_needed.md"
```

View File

@@ -0,0 +1,20 @@
# QA Remediation Report
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/federation/query.service.ts
**Tool Used:** Edit
**Epic:** general
**Iteration:** 2
**Generated:** 2026-02-03 21:31:59
## Status
Pending QA validation
## Next Steps
This report was created by the QA automation hook.
To process this report, run:
```bash
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-federation-query.service.ts_20260203-2131_2_remediation_needed.md"
```

View File

@@ -0,0 +1,20 @@
# QA Remediation Report
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/federation/signature.service.spec.ts
**Tool Used:** Edit
**Epic:** general
**Iteration:** 1
**Generated:** 2026-02-03 21:39:35
## Status
Pending QA validation
## Next Steps
This report was created by the QA automation hook.
To process this report, run:
```bash
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-federation-signature.service.spec.ts_20260203-2139_1_remediation_needed.md"
```

View File

@@ -0,0 +1,20 @@
# QA Remediation Report
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/federation/signature.service.spec.ts
**Tool Used:** Edit
**Epic:** general
**Iteration:** 2
**Generated:** 2026-02-03 21:39:40
## Status
Pending QA validation
## Next Steps
This report was created by the QA automation hook.
To process this report, run:
```bash
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-federation-signature.service.spec.ts_20260203-2139_2_remediation_needed.md"
```

View File

@@ -0,0 +1,20 @@
# QA Remediation Report
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/federation/signature.service.spec.ts
**Tool Used:** Edit
**Epic:** general
**Iteration:** 3
**Generated:** 2026-02-03 21:39:49
## Status
Pending QA validation
## Next Steps
This report was created by the QA automation hook.
To process this report, run:
```bash
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-federation-signature.service.spec.ts_20260203-2139_3_remediation_needed.md"
```

View File

@@ -0,0 +1,20 @@
# QA Remediation Report
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/federation/signature.service.spec.ts
**Tool Used:** Edit
**Epic:** general
**Iteration:** 1
**Generated:** 2026-02-03 21:40:07
## Status
Pending QA validation
## Next Steps
This report was created by the QA automation hook.
To process this report, run:
```bash
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-federation-signature.service.spec.ts_20260203-2140_1_remediation_needed.md"
```

View File

@@ -0,0 +1,20 @@
# QA Remediation Report
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/federation/signature.service.ts
**Tool Used:** Edit
**Epic:** general
**Iteration:** 1
**Generated:** 2026-02-03 21:40:34
## Status
Pending QA validation
## Next Steps
This report was created by the QA automation hook.
To process this report, run:
```bash
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-federation-signature.service.ts_20260203-2140_1_remediation_needed.md"
```

View File

@@ -0,0 +1,20 @@
# QA Remediation Report
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/federation/signature.service.ts
**Tool Used:** Edit
**Epic:** general
**Iteration:** 2
**Generated:** 2026-02-03 21:40:44
## Status
Pending QA validation
## Next Steps
This report was created by the QA automation hook.
To process this report, run:
```bash
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-federation-signature.service.ts_20260203-2140_2_remediation_needed.md"
```

View File

@@ -0,0 +1,20 @@
# QA Remediation Report
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/federation/workspace-access.integration.spec.ts
**Tool Used:** Write
**Epic:** general
**Iteration:** 1
**Generated:** 2026-02-03 21:48:25
## Status
Pending QA validation
## Next Steps
This report was created by the QA automation hook.
To process this report, run:
```bash
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-federation-workspace-access.integration.spec.ts_20260203-2148_1_remediation_needed.md"
```

View File

@@ -0,0 +1,20 @@
# QA Remediation Report
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/federation/workspace-access.integration.spec.ts
**Tool Used:** Edit
**Epic:** general
**Iteration:** 2
**Generated:** 2026-02-03 21:48:42
## Status
Pending QA validation
## Next Steps
This report was created by the QA automation hook.
To process this report, run:
```bash
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-federation-workspace-access.integration.spec.ts_20260203-2148_2_remediation_needed.md"
```

View File

@@ -0,0 +1,20 @@
# QA Remediation Report
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/federation/workspace-access.integration.spec.ts
**Tool Used:** Edit
**Epic:** general
**Iteration:** 3
**Generated:** 2026-02-03 21:48:56
## Status
Pending QA validation
## Next Steps
This report was created by the QA automation hook.
To process this report, run:
```bash
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-federation-workspace-access.integration.spec.ts_20260203-2148_3_remediation_needed.md"
```

View File

@@ -0,0 +1,20 @@
# QA Remediation Report
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/federation/workspace-access.integration.spec.ts
**Tool Used:** Edit
**Epic:** general
**Iteration:** 1
**Generated:** 2026-02-03 21:49:01
## Status
Pending QA validation
## Next Steps
This report was created by the QA automation hook.
To process this report, run:
```bash
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-federation-workspace-access.integration.spec.ts_20260203-2149_1_remediation_needed.md"
```

View File

@@ -0,0 +1,20 @@
# QA Remediation Report
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/federation/workspace-access.integration.spec.ts
**Tool Used:** Edit
**Epic:** general
**Iteration:** 2
**Generated:** 2026-02-03 21:49:06
## Status
Pending QA validation
## Next Steps
This report was created by the QA automation hook.
To process this report, run:
```bash
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-federation-workspace-access.integration.spec.ts_20260203-2149_2_remediation_needed.md"
```

View File

@@ -0,0 +1,20 @@
# QA Remediation Report
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/federation/workspace-access.integration.spec.ts
**Tool Used:** Edit
**Epic:** general
**Iteration:** 3
**Generated:** 2026-02-03 21:49:21
## Status
Pending QA validation
## Next Steps
This report was created by the QA automation hook.
To process this report, run:
```bash
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-federation-workspace-access.integration.spec.ts_20260203-2149_3_remediation_needed.md"
```

View File

@@ -0,0 +1,73 @@
# M7.1 P1 Security Fixes (#283-#290)
## Objective
Complete remaining P1 security issues in M7.1 Remediation Sprint
## Issues to Fix
### #283 - Enforce connection status validation in queries
- **Impact**: Authorization gap - operations proceed on non-ACTIVE connections
- **Fix**: Add status check to Prisma queries
- **Files**: command.service.ts, query.service.ts
### #284 - Reduce timestamp validation window
- **Impact**: 5-minute replay attack window
- **Fix**: Reduce to 60s + add nonce tracking with Redis
- **Files**: signature.service.ts
### #285 - Add input sanitization
- **Impact**: XSS risk on user-controlled fields
- **Fix**: Sanitize connection metadata, identity metadata, rejection reasons, command payloads
- **Files**: Multiple DTOs and services
### #286 - Add workspace access validation guard
- **Impact**: Authorization gap - no workspace membership validation
- **Fix**: Create WorkspaceAccessGuard
- **Files**: New guard + controllers
### #287 - Prevent sensitive data in logs
- **Impact**: Data leakage, PII exposure, GDPR violations
- **Fix**: Use appropriate log levels + redact sensitive data
- **Files**: All federation services
### #288 - Upgrade RSA key size
- **Impact**: Future-proofing against quantum computing
- **Fix**: Change from 2048 to 4096 bits
- **Files**: federation.service.ts
### #289 - Prevent private key decryption error leaks
- **Impact**: Sensitive data in error messages
- **Fix**: Don't log error details with potential sensitive data
- **Files**: crypto.service.ts
### #290 - Secure identity verification endpoint
- **Impact**: Public endpoint with no auth
- **Fix**: Add AuthGuard + rate limiting
- **Files**: identity-linking.controller.ts
## Progress
- [ ] #283 - Connection status validation
- [ ] #284 - Timestamp validation window
- [ ] #285 - Input sanitization
- [ ] #286 - Workspace access guard
- [ ] #287 - Sensitive data in logs
- [ ] #288 - RSA key size upgrade
- [ ] #289 - Decryption error leaks
- [ ] #290 - Identity endpoint security
## Testing Strategy
- Minimum 85% coverage for all changes
- TDD approach: write tests first
- Security-focused test cases
- Integration tests for guards and validation