Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions packages/agent-mesh/packages/mcp-proxy/policies/enterprise.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,28 @@ rules:
- tool: "run_shell"
action: deny
reason: "Shell execution prohibited per security policy SOC2-CC6.1"
mitigates: ["ASI02", "ASI05"]

- tool: "execute_command"
action: deny
reason: "Command execution prohibited"
mitigates: ["ASI02", "ASI05"]

- tool: "eval"
action: deny
reason: "Dynamic code evaluation prohibited"
mitigates: ["ASI05"]

- tool: "spawn_process"
action: deny
reason: "Process spawning prohibited"
mitigates: ["ASI02", "ASI05", "ASI10"]

# === SENSITIVE DATA PROTECTION ===

- tool: "read_file"
action: allow
mitigates: ["ASI02", "ASI07"]
conditions:
# Block access to sensitive file types
- path_not_contains:
Expand All @@ -50,6 +55,7 @@ rules:

- tool: "write_file"
action: allow
mitigates: ["ASI02"]
conditions:
- path_not_contains:
- ".env"
Expand All @@ -66,11 +72,13 @@ rules:
- tool: "delete_file"
action: deny
reason: "File deletion requires manual approval"
mitigates: ["ASI02"]

# === DATABASE OPERATIONS ===

- tool: "query_database"
action: allow
mitigates: ["ASI02", "ASI05"]
conditions:
# Block destructive queries
- argument_not_matches:
Expand All @@ -83,6 +91,7 @@ rules:

- tool: "http_request"
action: allow
mitigates: ["ASI08", "ASI09"]
rate_limit:
requests: 20
per: minute
Expand Down Expand Up @@ -114,9 +123,11 @@ rate_limits:
global:
requests: 200
per: minute
mitigates: ["ASI08"]

# Audit settings
audit:
mitigates: ["ASI09", "ASI10"]
# Log all tool calls (not just violations)
log_all: true
# Include argument values (may contain sensitive data)
Expand Down
8 changes: 8 additions & 0 deletions packages/agent-mesh/packages/mcp-proxy/policies/standard.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,35 +12,42 @@ rules:
- tool: "run_shell"
action: deny
reason: "Shell execution not permitted"
mitigates: ["ASI02", "ASI05"]

- tool: "execute_command"
action: deny
reason: "Direct command execution not permitted"
mitigates: ["ASI02", "ASI05"]

- tool: "eval"
action: deny
reason: "Code evaluation not permitted"
mitigates: ["ASI05"]

- tool: "spawn_process"
action: deny
reason: "Process spawning not permitted"
mitigates: ["ASI02", "ASI05", "ASI10"]

# === SENSITIVE FILE PROTECTION ===

- tool: "read_file"
action: allow
mitigates: ["ASI02", "ASI07"]
conditions:
- path_not_contains: [".env", ".secret", "id_rsa", ".pem", "credentials"]

- tool: "write_file"
action: allow
mitigates: ["ASI02"]
conditions:
- path_not_contains: [".env", ".secret", "id_rsa", ".pem", "/etc/", "/bin/"]

# === NETWORK (MONITORED) ===

- tool: "http_request"
action: allow
mitigates: ["ASI08", "ASI09"]
rate_limit:
requests: 30
per: minute
Expand All @@ -61,3 +68,4 @@ rate_limits:
global:
requests: 100
per: minute
mitigates: ["ASI08"]
7 changes: 7 additions & 0 deletions packages/agent-mesh/packages/mcp-proxy/policies/strict.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ rules:

- tool: "read_file"
action: allow
mitigates: ["ASI02", "ASI07"]
conditions:
- path_not_contains: [".env", ".secret", "credentials", "password", ".key"]
reason: "Read-only file access permitted"
Expand All @@ -35,22 +36,27 @@ rules:
- tool: "write_file"
action: deny
reason: "Write operations not permitted in strict mode"
mitigates: ["ASI02"]

- tool: "delete_file"
action: deny
reason: "Delete operations not permitted"
mitigates: ["ASI02"]

- tool: "run_shell"
action: deny
reason: "Shell execution blocked"
mitigates: ["ASI02", "ASI05"]

- tool: "execute_command"
action: deny
reason: "Command execution blocked"
mitigates: ["ASI02", "ASI05"]

- tool: "eval"
action: deny
reason: "Code evaluation blocked"
mitigates: ["ASI05"]

# === CATCH-ALL ===

Expand All @@ -63,6 +69,7 @@ rate_limits:
global:
requests: 50
per: minute
mitigates: ["ASI08"]
per_tool:
read_file:
requests: 20
Expand Down
2 changes: 2 additions & 0 deletions packages/agent-mesh/packages/mcp-proxy/src/audit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface AuditEvent {
decision: 'allow' | 'deny';
reason?: string;
rule?: string;
mitigates?: string[];
latency_ms?: number;
}

Expand Down Expand Up @@ -89,6 +90,7 @@ export class AuditLogger {
decision: event.decision,
reason: event.reason,
matched_rule: event.rule,
mitigates: event.mitigates,
latency_ms: event.latency_ms,
},
// Extension attributes for AgentMesh
Expand Down
19 changes: 11 additions & 8 deletions packages/agent-mesh/packages/mcp-proxy/src/policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ export interface PolicyRule {
action: 'allow' | 'deny';
reason?: string;
conditions?: PolicyCondition[];
rate_limit?: { requests: number; per: string };
rate_limit?: { requests: number; per: string };
mitigates?: string[];
}

export interface PolicyCondition {
Expand All @@ -39,7 +40,8 @@ export interface Policy {
export interface PolicyDecision {
allowed: boolean;
reason?: string;
matchedRule?: string;
matchedRule?: string;
mitigatedRisks?: string[];
}

// Built-in policies
Expand All @@ -62,9 +64,9 @@ const BUILTIN_POLICY_DEFS: Record<string, Policy> = {
version: '1.0',
mode: 'enforce',
rules: [
{ tool: 'run_shell', action: 'deny', reason: 'Shell execution blocked' },
{ tool: 'execute_command', action: 'deny', reason: 'Command execution blocked' },
{ tool: 'eval', action: 'deny', reason: 'Eval blocked' },
{ tool: 'run_shell', action: 'deny', reason: 'Shell execution blocked', mitigates: ['ASI02', 'ASI05'] },
{ tool: 'execute_command', action: 'deny', reason: 'Command execution blocked', mitigates: ['ASI02', 'ASI05'] },
{ tool: 'eval', action: 'deny', reason: 'Eval blocked', mitigates: ['ASI05'] },
{ tool: '*', action: 'allow' },
],
},
Expand All @@ -82,8 +84,8 @@ const BUILTIN_POLICY_DEFS: Record<string, Policy> = {
version: '1.0',
mode: 'enforce',
rules: [
{ tool: 'run_shell', action: 'deny', reason: 'Shell execution blocked' },
{ tool: 'execute_command', action: 'deny', reason: 'Command execution blocked' },
{ tool: 'run_shell', action: 'deny', reason: 'Shell execution blocked', mitigates: ['ASI02', 'ASI05'] },
{ tool: 'execute_command', action: 'deny', reason: 'Command execution blocked', mitigates: ['ASI02', 'ASI05'] },
{ tool: 'write_file', action: 'allow', rate_limit: { requests: 10, per: 'minute' } },
{ tool: '*', action: 'allow' },
],
Expand Down Expand Up @@ -150,7 +152,8 @@ export function evaluatePolicy(
return {
allowed: rule.action === 'allow',
reason: rule.reason,
matchedRule: rule.tool,
matchedRule: rule.tool,
mitigatedRisks: rule.mitigates ? [...rule.mitigates] : undefined,
};
}
}
Expand Down
1 change: 1 addition & 0 deletions packages/agent-mesh/packages/mcp-proxy/src/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ export class MCPProxy {
decision: decision.allowed ? 'allow' : 'deny',
reason: decision.reason,
rule: decision.matchedRule,
mitigates: decision.mitigatedRisks,
});

if (!decision.allowed && this.options.mode === 'enforce') {
Expand Down
94 changes: 94 additions & 0 deletions packages/agent-mesh/packages/mcp-proxy/tests/policy-audit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { afterEach, describe, expect, it } from 'vitest';
import { once } from 'events';
import { mkdtempSync, readFileSync, rmSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import { AuditLogger } from '../src/audit.js';
import { evaluatePolicy, Policy } from '../src/policy.js';

const tempDirs: string[] = [];

afterEach(() => {
for (const dir of tempDirs.splice(0)) {
rmSync(dir, { recursive: true, force: true });
}
});

describe('evaluatePolicy', () => {
it('copies mitigates from the matched rule into the decision', () => {
const policy: Policy = {
version: '1.0',
mode: 'enforce',
rules: [
{
tool: 'run_shell',
action: 'deny',
reason: 'blocked',
mitigates: ['ASI02', 'ASI05'],
},
{ tool: '*', action: 'allow' },
],
};

const decision = evaluatePolicy(policy, 'run_shell', {});

expect(decision).toMatchObject({
allowed: false,
matchedRule: 'run_shell',
mitigatedRisks: ['ASI02', 'ASI05'],
});
});

it('leaves mitigatedRisks unset when the matched rule has no annotations', () => {
const policy: Policy = {
version: '1.0',
mode: 'enforce',
rules: [{ tool: '*', action: 'allow' }],
};

const decision = evaluatePolicy(policy, 'read_file', { path: 'README.md' });

expect(decision.allowed).toBe(true);
expect(decision.mitigatedRisks).toBeUndefined();
});
});

describe('AuditLogger', () => {
it('includes mitigates in CloudEvents data only when present', async () => {
const tempDir = mkdtempSync(join(tmpdir(), 'mcp-proxy-audit-'));
tempDirs.push(tempDir);

const logPath = join(tempDir, 'audit.log');
const logger = new AuditLogger({ path: logPath });

logger.log({
type: 'ai.agentmesh.policy.violation',
tool: 'run_shell',
decision: 'deny',
mitigates: ['ASI02', 'ASI05'],
});
logger.log({
type: 'ai.agentmesh.tool.invoked',
tool: 'read_file',
decision: 'allow',
});

logger.close();

const stream = Reflect.get(logger, 'stream');
if (stream) {
await once(stream, 'finish');
}

const [deniedEntry, allowedEntry] = readFileSync(logPath, 'utf-8')
.trim()
.split('\n')
.map((line) => JSON.parse(line) as { data: Record<string, unknown> });

expect(deniedEntry.data.mitigates).toEqual(['ASI02', 'ASI05']);
expect(allowedEntry.data).not.toHaveProperty('mitigates');
});
});
Loading