Skip to content

Commit 10ab27e

Browse files
authored
Refactor generatePolicyManifest into composable policy section builders (#5446)
* Initial plan * refactor: split policy manifest rule builders --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
1 parent c7fabe6 commit 10ab27e

3 files changed

Lines changed: 260 additions & 192 deletions

File tree

src/squid/policy-manifest.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,5 +158,21 @@ describe('generatePolicyManifest - regex pattern rules', () => {
158158
expect(getOrder('allow-http-only-regex')).toBeLessThan(getOrder('allow-https-only-regex'));
159159
expect(getOrder('allow-https-only-regex')).toBeLessThan(getOrder('deny-default'));
160160
});
161+
162+
it('should keep sequential rule numbering across all policy sections', () => {
163+
const manifest = generatePolicyManifest({
164+
domains: ['http://*.insecure.com', 'https://api.secure.com', '*.both.com'],
165+
blockedDomains: ['evil.com', '*.tracking.com'],
166+
enableDlp: true,
167+
apiProxyIp: '172.30.0.30',
168+
port,
169+
});
170+
171+
expect(manifest.rules.map(rule => rule.order)).toEqual(
172+
manifest.rules.map((_, index) => index + 1)
173+
);
174+
expect(manifest.rules[0].id).toBe('deny-unsafe-ports');
175+
expect(manifest.rules[manifest.rules.length - 1].id).toBe('deny-default');
176+
});
161177
});
162178
});

src/squid/policy-manifest.ts

Lines changed: 23 additions & 192 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,17 @@
1-
import type { PolicyManifest, PolicyRule, SquidConfig } from '../types';
1+
import type { PolicyManifest, SquidConfig } from '../types';
22
import { DEFAULT_DNS_SERVERS } from '../dns-resolver';
3-
import { parseDomainList } from '../domain-matchers';
4-
import { formatDomainForSquid, parseDomainConfig } from './domain-acl';
3+
import { parseDomainConfig } from './domain-acl';
4+
import {
5+
addApiProxyAllowRules,
6+
addBlockedDomainRules,
7+
addBothProtocolAllowRules,
8+
addDefaultDenyRule,
9+
addDlpRules,
10+
addPortSafetyRules,
11+
addProtocolAllowRules,
12+
addRawIpBlockRules,
13+
type PolicyRuleState,
14+
} from './policy-rules/section-builders';
515

616
/**
717
* Ports that should never be allowed, even with --allow-host-ports
@@ -49,200 +59,21 @@ export function generatePolicyManifest(config: SquidConfig): PolicyManifest {
4959
// Parse, deduplicate, and group domains by protocol (shared logic with generateSquidConfig)
5060
const { domainsByProto, patternsByProto } = parseDomainConfig(domains);
5161

52-
const rules: PolicyRule[] = [];
53-
let order = 0;
62+
const state: PolicyRuleState = { rules: [], order: 0 };
5463

55-
// --- Port safety rules (evaluated first in Squid) ---
56-
rules.push({
57-
id: 'deny-unsafe-ports',
58-
order: ++order,
59-
action: 'deny',
60-
aclName: '!Safe_ports',
61-
protocol: 'both',
62-
domains: [],
63-
description: 'Deny requests to ports not in Safe_ports ACL (only 80, 443, and user-specified ports allowed)',
64-
});
65-
rules.push({
66-
id: 'deny-connect-unsafe-ports',
67-
order: ++order,
68-
action: 'deny',
69-
aclName: 'CONNECT !Safe_ports',
70-
protocol: 'https',
71-
domains: [],
72-
description: 'Deny CONNECT (HTTPS) to ports not in Safe_ports ACL',
73-
});
74-
75-
// --- api-proxy allow (before raw-IP deny) ---
76-
if (apiProxyIp) {
77-
rules.push({
78-
id: 'allow-api-proxy-ip',
79-
order: ++order,
80-
action: 'allow',
81-
aclName: 'allow_api_proxy_ip',
82-
protocol: 'both',
83-
domains: [apiProxyIp],
84-
description: 'Allow connections to the AWF api-proxy sidecar IP before raw-IP deny rules',
85-
});
86-
rules.push({
87-
id: 'allow-from-api-proxy',
88-
order: ++order,
89-
action: 'allow',
90-
aclName: 'from_api_proxy',
91-
protocol: 'both',
92-
domains: ['*'],
93-
description: 'Allow unrestricted outbound from api-proxy sidecar (trusted AWF component, not subject to agent domain ACL)',
94-
});
95-
}
96-
97-
// --- Raw IP blocking ---
98-
rules.push({
99-
id: 'deny-raw-ipv4',
100-
order: ++order,
101-
action: 'deny',
102-
aclName: 'dst_ipv4',
103-
protocol: 'both',
104-
domains: ['^[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+$'],
105-
description: 'Deny requests to raw IPv4 addresses (bypasses domain filtering)',
106-
});
107-
rules.push({
108-
id: 'deny-raw-ipv6',
109-
order: ++order,
110-
action: 'deny',
111-
aclName: 'dst_ipv6',
112-
protocol: 'both',
113-
domains: ['^\\[?[0-9a-fA-F:]+\\]?$'],
114-
description: 'Deny requests to raw IPv6 addresses (bypasses domain filtering)',
115-
});
116-
117-
// --- DLP rules (if enabled) ---
118-
if (enableDlp) {
119-
rules.push({
120-
id: 'deny-dlp',
121-
order: ++order,
122-
action: 'deny',
123-
aclName: 'dlp_blocked',
124-
protocol: 'both',
125-
domains: [],
126-
description: 'Deny requests containing credential patterns in URLs (DLP)',
127-
});
128-
}
129-
130-
// --- Blocked domains ---
131-
if (blockedDomains && blockedDomains.length > 0) {
132-
const normalizedBlocked = blockedDomains.map(d => d.replace(/^https?:\/\//, '').replace(/\/$/, ''));
133-
const { plainDomains: blockedPlain, patterns: blockedPatterns } = parseDomainList(normalizedBlocked);
134-
135-
if (blockedPlain.length > 0) {
136-
rules.push({
137-
id: 'deny-blocked-plain',
138-
order: ++order,
139-
action: 'deny',
140-
aclName: 'blocked_domains',
141-
protocol: 'both',
142-
domains: blockedPlain.map(e => formatDomainForSquid(e.domain)),
143-
description: 'Deny requests to explicitly blocked domains',
144-
});
145-
}
146-
147-
if (blockedPatterns.length > 0) {
148-
rules.push({
149-
id: 'deny-blocked-regex',
150-
order: ++order,
151-
action: 'deny',
152-
aclName: 'blocked_domains_regex',
153-
protocol: 'both',
154-
domains: blockedPatterns.map(p => p.regex),
155-
description: 'Deny requests to explicitly blocked domain patterns',
156-
});
157-
}
158-
}
159-
160-
// --- Protocol-specific allow rules ---
161-
if (domainsByProto.http.length > 0) {
162-
rules.push({
163-
id: 'allow-http-only-plain',
164-
order: ++order,
165-
action: 'allow',
166-
aclName: 'allowed_http_only',
167-
protocol: 'http',
168-
domains: domainsByProto.http.map(d => formatDomainForSquid(d)),
169-
description: 'Allow HTTP-only traffic to these domains (no HTTPS)',
170-
});
171-
}
172-
if (patternsByProto.http.length > 0) {
173-
rules.push({
174-
id: 'allow-http-only-regex',
175-
order: ++order,
176-
action: 'allow',
177-
aclName: 'allowed_http_only_regex',
178-
protocol: 'http',
179-
domains: patternsByProto.http.map(p => p.regex),
180-
description: 'Allow HTTP-only traffic matching these patterns',
181-
});
182-
}
183-
184-
if (domainsByProto.https.length > 0) {
185-
rules.push({
186-
id: 'allow-https-only-plain',
187-
order: ++order,
188-
action: 'allow',
189-
aclName: 'allowed_https_only',
190-
protocol: 'https',
191-
domains: domainsByProto.https.map(d => formatDomainForSquid(d)),
192-
description: 'Allow HTTPS-only traffic to these domains (no HTTP)',
193-
});
194-
}
195-
if (patternsByProto.https.length > 0) {
196-
rules.push({
197-
id: 'allow-https-only-regex',
198-
order: ++order,
199-
action: 'allow',
200-
aclName: 'allowed_https_only_regex',
201-
protocol: 'https',
202-
domains: patternsByProto.https.map(p => p.regex),
203-
description: 'Allow HTTPS-only traffic matching these patterns',
204-
});
205-
}
206-
207-
// --- Both-protocol allow (used in deny rule logic) ---
208-
if (domainsByProto.both.length > 0) {
209-
rules.push({
210-
id: 'allow-both-plain',
211-
order: ++order,
212-
action: 'allow',
213-
aclName: 'allowed_domains',
214-
protocol: 'both',
215-
domains: domainsByProto.both.map(d => formatDomainForSquid(d)),
216-
description: 'Allow HTTP and HTTPS traffic to these domains',
217-
});
218-
}
219-
if (patternsByProto.both.length > 0) {
220-
rules.push({
221-
id: 'allow-both-regex',
222-
order: ++order,
223-
action: 'allow',
224-
aclName: 'allowed_domains_regex',
225-
protocol: 'both',
226-
domains: patternsByProto.both.map(p => p.regex),
227-
description: 'Allow HTTP and HTTPS traffic matching these patterns',
228-
});
229-
}
230-
231-
// --- Default deny (final rule) ---
232-
rules.push({
233-
id: 'deny-default',
234-
order: ++order,
235-
action: 'deny',
236-
aclName: 'all',
237-
protocol: 'both',
238-
domains: [],
239-
description: 'Deny all traffic not matching any allow rule (default deny)',
240-
});
64+
addPortSafetyRules(state);
65+
addApiProxyAllowRules(state, apiProxyIp);
66+
addRawIpBlockRules(state);
67+
addDlpRules(state, enableDlp);
68+
addBlockedDomainRules(state, blockedDomains);
69+
addProtocolAllowRules(state, domainsByProto, patternsByProto);
70+
addBothProtocolAllowRules(state, domainsByProto, patternsByProto);
71+
addDefaultDenyRule(state);
24172

24273
return {
24374
version: 1,
24475
generatedAt: new Date().toISOString(),
245-
rules,
76+
rules: state.rules,
24677
dangerousPorts: DANGEROUS_PORTS,
24778
dnsServers: dnsServers || DEFAULT_DNS_SERVERS,
24879
sslBumpEnabled: sslBump ?? false,

0 commit comments

Comments
 (0)