From 3509bbe518ab88024dd0b2851a607561d9e9ae14 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 27 Jul 2026 19:58:35 +0000 Subject: [PATCH 1/2] Initial plan From bc0fb6782186523a5c30f97aa4c9a5d8979044de Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 27 Jul 2026 20:16:18 +0000 Subject: [PATCH 2/2] fix: add topologyPeers to policy manifest for audit accuracy --- src/config-writer.ts | 4 ++ src/squid/policy-manifest.test.ts | 72 ++++++++++++++++++++++ src/squid/policy-manifest.ts | 8 ++- src/squid/policy-rules/section-builders.ts | 16 +++++ 4 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/config-writer.ts b/src/config-writer.ts index 02bf25fa7..a0bd124f3 100644 --- a/src/config-writer.ts +++ b/src/config-writer.ts @@ -252,6 +252,10 @@ function writeAuditArtifacts( ...(config.enableApiProxy && networkConfig.proxyIp ? { apiProxyIp: networkConfig.proxyIp, } : {}), + // Include topology peer allow rules so the audit log correctly attributes + // allowed connections to topology-attached containers (e.g. awmg-mcpg:8080) + // rather than misidentifying them as "unknown" or blocked. + topologyPeers: resolveTopologyPeerHosts(config), }); fs.writeFileSync( path.join(auditDir, 'policy-manifest.json'), diff --git a/src/squid/policy-manifest.test.ts b/src/squid/policy-manifest.test.ts index 32669ea67..ec95ade4b 100644 --- a/src/squid/policy-manifest.test.ts +++ b/src/squid/policy-manifest.test.ts @@ -176,3 +176,75 @@ describe('generatePolicyManifest - regex pattern rules', () => { }); }); }); + +describe('generatePolicyManifest - topology peers', () => { + const port = 3128; + + it('emits no topology peer rules when topologyPeers is undefined', () => { + const manifest = generatePolicyManifest({ domains: ['github.com'], port }); + expect(manifest.rules.filter(r => r.id.startsWith('allow-topology-peer-'))).toHaveLength(0); + }); + + it('emits no topology peer rules when topologyPeers is empty', () => { + const manifest = generatePolicyManifest({ domains: ['github.com'], port, topologyPeers: [] }); + expect(manifest.rules.filter(r => r.id.startsWith('allow-topology-peer-'))).toHaveLength(0); + }); + + it('emits an allow rule for each topology peer', () => { + const manifest = generatePolicyManifest({ + domains: ['github.com'], + port, + topologyPeers: ['awmg-mcpg', 'awmg-cli-proxy'], + }); + + const peerRules = manifest.rules.filter(r => r.id.startsWith('allow-topology-peer-')); + expect(peerRules).toHaveLength(2); + + const mcpgRule = peerRules.find(r => r.id === 'allow-topology-peer-awmg-mcpg'); + expect(mcpgRule).toBeDefined(); + expect(mcpgRule!.action).toBe('allow'); + expect(mcpgRule!.protocol).toBe('both'); + expect(mcpgRule!.domains).toContain('.awmg-mcpg'); + + const cliProxyRule = peerRules.find(r => r.id === 'allow-topology-peer-awmg-cli-proxy'); + expect(cliProxyRule).toBeDefined(); + expect(cliProxyRule!.action).toBe('allow'); + expect(cliProxyRule!.domains).toContain('.awmg-cli-proxy'); + }); + + it('places topology peer allow rules before the port-safety deny rules', () => { + const manifest = generatePolicyManifest({ + domains: ['github.com'], + port, + topologyPeers: ['awmg-mcpg'], + }); + + const peerRule = manifest.rules.find(r => r.id === 'allow-topology-peer-awmg-mcpg'); + const portSafetyRule = manifest.rules.find(r => r.id === 'deny-unsafe-ports'); + expect(peerRule).toBeDefined(); + expect(portSafetyRule).toBeDefined(); + expect(peerRule!.order).toBeLessThan(portSafetyRule!.order); + }); + + it('topology peer allow rules are first in the rule list', () => { + const manifest = generatePolicyManifest({ + domains: ['github.com'], + port, + topologyPeers: ['awmg-mcpg'], + }); + + expect(manifest.rules[0].id).toBe('allow-topology-peer-awmg-mcpg'); + }); + + it('keeps sequential rule numbering when topology peers are present', () => { + const manifest = generatePolicyManifest({ + domains: ['github.com'], + port, + topologyPeers: ['awmg-mcpg', 'awmg-cli-proxy'], + }); + + expect(manifest.rules.map(rule => rule.order)).toEqual( + manifest.rules.map((_, index) => index + 1) + ); + }); +}); diff --git a/src/squid/policy-manifest.ts b/src/squid/policy-manifest.ts index e9c729294..c906bc09b 100644 --- a/src/squid/policy-manifest.ts +++ b/src/squid/policy-manifest.ts @@ -11,6 +11,7 @@ import { addPortSafetyRules, addProtocolAllowRules, addRawIpBlockRules, + addTopologyPeerAllowRules, type PolicyRuleState, } from './policy-rules/section-builders'; @@ -55,13 +56,18 @@ export const DANGEROUS_PORTS = [ * enricher skips them and attributes those denials to "unknown". */ export function generatePolicyManifest(config: SquidConfig): PolicyManifest { - const { domains, blockedDomains, sslBump, enableHostAccess, allowHostPorts, enableDlp, dnsServers, apiProxyIp } = config; + const { domains, blockedDomains, sslBump, enableHostAccess, allowHostPorts, enableDlp, dnsServers, apiProxyIp, topologyPeers } = config; // Parse, deduplicate, and group domains by protocol (shared logic with generateSquidConfig) const { domainsByProto, patternsByProto } = parseDomainConfig(domains); const state: PolicyRuleState = { rules: [], order: 0 }; + // Topology peer allow rules must fire BEFORE the port-safety deny rules so + // that proxy clients reaching e.g. awmg-mcpg:8080 through Squid are allowed + // even though port 8080 is not in Safe_ports. This mirrors the rule ordering + // in generateSquidConfig / generateTopologyPeersSection. + addTopologyPeerAllowRules(state, topologyPeers); addPortSafetyRules(state); addApiProxyAllowRules(state, apiProxyIp); addAllowedIpRules(state, domains); diff --git a/src/squid/policy-rules/section-builders.ts b/src/squid/policy-rules/section-builders.ts index bcbca48ed..bc12a77e3 100644 --- a/src/squid/policy-rules/section-builders.ts +++ b/src/squid/policy-rules/section-builders.ts @@ -27,6 +27,22 @@ function pushRule(state: PolicyRuleState, rule: Omit): void }); } +export function addTopologyPeerAllowRules(state: PolicyRuleState, topologyPeers?: string[]): void { + if (!topologyPeers || topologyPeers.length === 0) return; + + for (const peer of topologyPeers) { + const aclName = `topology_peer_${peer.replace(/[^a-zA-Z0-9]/g, '_')}`; + pushRule(state, { + id: `allow-topology-peer-${peer}`, + action: 'allow', + aclName, + protocol: 'both', + domains: [formatDomainForSquid(peer)], + description: `Allow trusted topology peer "${peer}" on any port (network-isolation mode, before Safe_ports deny)`, + }); + } +} + export function addPortSafetyRules(state: PolicyRuleState): void { pushRule(state, { id: 'deny-unsafe-ports',