-
Notifications
You must be signed in to change notification settings - Fork 354
Expand file tree
/
Copy pathfirewall_blocked_domains.test.cjs
More file actions
352 lines (273 loc) · 15.7 KB
/
firewall_blocked_domains.test.cjs
File metadata and controls
352 lines (273 loc) · 15.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import fs from "fs";
import path from "path";
import os from "os";
describe("firewall_blocked_domains.cjs", () => {
let parseFirewallLogLine;
let isRequestBlocked;
let extractAndSanitizeDomain;
let getBlockedDomains;
let generateBlockedDomainsSection;
let testDir;
beforeEach(async () => {
// Create a temporary directory for test files
testDir = path.join(os.tmpdir(), `gh-aw-test-firewall-${Date.now()}`);
fs.mkdirSync(testDir, { recursive: true });
// Dynamic import to get fresh module state
const module = await import("./firewall_blocked_domains.cjs");
parseFirewallLogLine = module.parseFirewallLogLine;
isRequestBlocked = module.isRequestBlocked;
extractAndSanitizeDomain = module.extractAndSanitizeDomain;
getBlockedDomains = module.getBlockedDomains;
generateBlockedDomainsSection = module.generateBlockedDomainsSection;
});
afterEach(() => {
// Clean up test directory
if (testDir && fs.existsSync(testDir)) {
fs.rmSync(testDir, { recursive: true, force: true });
}
});
describe("parseFirewallLogLine", () => {
it("should parse valid firewall log line with blocked request", () => {
const line = '1761332530.474 172.30.0.20:35288 blocked.example.com:443 140.82.112.22:443 1.1 CONNECT 403 NONE_NONE:HIER_NONE blocked.example.com:443 "-"';
const result = parseFirewallLogLine(line);
expect(result).not.toBeNull();
expect(result.timestamp).toBe("1761332530.474");
expect(result.domain).toBe("blocked.example.com:443");
expect(result.status).toBe("403");
expect(result.decision).toBe("NONE_NONE:HIER_NONE");
});
it("should parse valid firewall log line with allowed request", () => {
const line = '1761332530.474 172.30.0.20:35288 api.github.com:443 140.82.112.22:443 1.1 CONNECT 200 TCP_TUNNEL:HIER_DIRECT api.github.com:443 "-"';
const result = parseFirewallLogLine(line);
expect(result).not.toBeNull();
expect(result.timestamp).toBe("1761332530.474");
expect(result.domain).toBe("api.github.com:443");
expect(result.status).toBe("200");
expect(result.decision).toBe("TCP_TUNNEL:HIER_DIRECT");
});
it("should return null for empty line", () => {
expect(parseFirewallLogLine("")).toBeNull();
expect(parseFirewallLogLine(" ")).toBeNull();
});
it("should return null for comment line", () => {
expect(parseFirewallLogLine("# This is a comment")).toBeNull();
});
it("should return null for invalid timestamp", () => {
const line = 'invalid 172.30.0.20:35288 blocked.example.com:443 140.82.112.22:443 1.1 CONNECT 403 NONE_NONE:HIER_NONE blocked.example.com:443 "-"';
expect(parseFirewallLogLine(line)).toBeNull();
});
it("should return null for lines with fewer than 10 fields", () => {
expect(parseFirewallLogLine("1761332530.474 172.30.0.20:35288 blocked.example.com:443")).toBeNull();
});
});
describe("isRequestBlocked", () => {
it("should identify blocked request with 403 status", () => {
expect(isRequestBlocked("NONE_NONE:HIER_NONE", "403")).toBe(true);
});
it("should identify blocked request with 407 status", () => {
expect(isRequestBlocked("NONE_NONE:HIER_NONE", "407")).toBe(true);
});
it("should identify blocked request with NONE_NONE decision", () => {
expect(isRequestBlocked("NONE_NONE:HIER_NONE", "0")).toBe(true);
});
it("should identify blocked request with TCP_DENIED decision", () => {
expect(isRequestBlocked("TCP_DENIED:HIER_NONE", "0")).toBe(true);
});
it("should identify allowed request with 200 status", () => {
expect(isRequestBlocked("TCP_TUNNEL:HIER_DIRECT", "200")).toBe(false);
});
it("should identify allowed request with TCP_TUNNEL decision", () => {
expect(isRequestBlocked("TCP_TUNNEL:HIER_DIRECT", "200")).toBe(false);
});
it("should identify allowed request with TCP_HIT decision", () => {
expect(isRequestBlocked("TCP_HIT:HIER_DIRECT", "200")).toBe(false);
});
it("should default to blocked for ambiguous requests", () => {
expect(isRequestBlocked("UNKNOWN:UNKNOWN", "999")).toBe(true);
});
});
describe("extractAndSanitizeDomain", () => {
it("should extract and sanitize domain from domain:port format", () => {
expect(extractAndSanitizeDomain("example.com:443")).toBe("example.com");
expect(extractAndSanitizeDomain("api.github.com:443")).toBe("api.github.com");
expect(extractAndSanitizeDomain("sub.domain.example.com:8080")).toBe("sub.domain.example.com");
});
it("should handle placeholder domain", () => {
expect(extractAndSanitizeDomain("-")).toBe("");
});
it("should handle empty or null input", () => {
expect(extractAndSanitizeDomain("")).toBe("");
expect(extractAndSanitizeDomain(null)).toBe("");
});
it("should sanitize special characters in domain", () => {
expect(extractAndSanitizeDomain("ex@mple.com:443")).toBe("exmple.com");
expect(extractAndSanitizeDomain("test_site.com:443")).toBe("testsite.com");
});
it("should handle domain without port", () => {
expect(extractAndSanitizeDomain("example.com")).toBe("example.com");
});
});
describe("getBlockedDomains", () => {
it("should return empty array when logs directory does not exist", () => {
const nonExistentDir = path.join(testDir, "nonexistent");
const result = getBlockedDomains(nonExistentDir);
expect(result).toEqual([]);
});
it("should return empty array when no log files exist", () => {
const emptyDir = path.join(testDir, "empty");
fs.mkdirSync(emptyDir, { recursive: true });
const result = getBlockedDomains(emptyDir);
expect(result).toEqual([]);
});
it("should extract blocked domains from single log file", () => {
const logsDir = path.join(testDir, "logs1");
fs.mkdirSync(logsDir, { recursive: true });
const logContent = [
'1761332530.474 172.30.0.20:35288 blocked1.example.com:443 140.82.112.22:443 1.1 CONNECT 403 NONE_NONE:HIER_NONE blocked1.example.com:443 "-"',
'1761332530.475 172.30.0.20:35289 api.github.com:443 140.82.112.22:443 1.1 CONNECT 200 TCP_TUNNEL:HIER_DIRECT api.github.com:443 "-"',
'1761332530.476 172.30.0.20:35290 blocked2.example.com:443 140.82.112.22:443 1.1 CONNECT 403 NONE_NONE:HIER_NONE blocked2.example.com:443 "-"',
].join("\n");
fs.writeFileSync(path.join(logsDir, "access.log"), logContent);
const result = getBlockedDomains(logsDir);
expect(result).toEqual(["blocked1.example.com", "blocked2.example.com"]);
});
it("should deduplicate blocked domains", () => {
const logsDir = path.join(testDir, "logs2");
fs.mkdirSync(logsDir, { recursive: true });
const logContent = [
'1761332530.474 172.30.0.20:35288 blocked.example.com:443 140.82.112.22:443 1.1 CONNECT 403 NONE_NONE:HIER_NONE blocked.example.com:443 "-"',
'1761332530.475 172.30.0.20:35289 blocked.example.com:443 140.82.112.22:443 1.1 CONNECT 403 NONE_NONE:HIER_NONE blocked.example.com:443 "-"',
'1761332530.476 172.30.0.20:35290 blocked.example.com:443 140.82.112.22:443 1.1 CONNECT 403 NONE_NONE:HIER_NONE blocked.example.com:443 "-"',
].join("\n");
fs.writeFileSync(path.join(logsDir, "access.log"), logContent);
const result = getBlockedDomains(logsDir);
expect(result).toEqual(["blocked.example.com"]);
});
it("should aggregate blocked domains from multiple log files", () => {
const logsDir = path.join(testDir, "logs3");
fs.mkdirSync(logsDir, { recursive: true });
const log1Content = '1761332530.474 172.30.0.20:35288 blocked1.example.com:443 140.82.112.22:443 1.1 CONNECT 403 NONE_NONE:HIER_NONE blocked1.example.com:443 "-"';
const log2Content = '1761332530.475 172.30.0.20:35289 blocked2.example.com:443 140.82.112.22:443 1.1 CONNECT 407 TCP_DENIED:HIER_NONE blocked2.example.com:443 "-"';
fs.writeFileSync(path.join(logsDir, "access1.log"), log1Content);
fs.writeFileSync(path.join(logsDir, "access2.log"), log2Content);
const result = getBlockedDomains(logsDir);
expect(result).toEqual(["blocked1.example.com", "blocked2.example.com"]);
});
it("should sort blocked domains alphabetically", () => {
const logsDir = path.join(testDir, "logs4");
fs.mkdirSync(logsDir, { recursive: true });
const logContent = [
'1761332530.474 172.30.0.20:35288 zebra.example.com:443 140.82.112.22:443 1.1 CONNECT 403 NONE_NONE:HIER_NONE zebra.example.com:443 "-"',
'1761332530.475 172.30.0.20:35289 alpha.example.com:443 140.82.112.22:443 1.1 CONNECT 403 NONE_NONE:HIER_NONE alpha.example.com:443 "-"',
'1761332530.476 172.30.0.20:35290 mike.example.com:443 140.82.112.22:443 1.1 CONNECT 403 NONE_NONE:HIER_NONE mike.example.com:443 "-"',
].join("\n");
fs.writeFileSync(path.join(logsDir, "access.log"), logContent);
const result = getBlockedDomains(logsDir);
expect(result).toEqual(["alpha.example.com", "mike.example.com", "zebra.example.com"]);
});
it("should filter out placeholder domains", () => {
const logsDir = path.join(testDir, "logs5");
fs.mkdirSync(logsDir, { recursive: true });
const logContent = [
'1761332530.474 172.30.0.20:35288 - 140.82.112.22:443 1.1 CONNECT 403 NONE_NONE:HIER_NONE - "-"',
'1761332530.475 172.30.0.20:35289 blocked.example.com:443 140.82.112.22:443 1.1 CONNECT 403 NONE_NONE:HIER_NONE blocked.example.com:443 "-"',
].join("\n");
fs.writeFileSync(path.join(logsDir, "access.log"), logContent);
const result = getBlockedDomains(logsDir);
// The iptables-dropped entry uses destIpPort (140.82.112.22) as fallback
expect(result).toContain("blocked.example.com");
expect(result).toContain("140.82.112.22");
});
it("should use destIpPort as fallback when domain is placeholder", () => {
const logsDir = path.join(testDir, "logs-iptables");
fs.mkdirSync(logsDir, { recursive: true });
// Simulate iptables-dropped traffic: domain="-", destIpPort has actual destination
const logContent = [
'1761332530.474 172.30.0.20:35288 - 8.8.8.8:53 - - 0 NONE_NONE:HIER_NONE - "-"', // iptables-dropped DNS query
'1761332530.475 172.30.0.20:35289 - 1.2.3.4:443 - - 0 NONE_NONE:HIER_NONE - "-"', // iptables-dropped HTTPS
'1761332530.476 172.30.0.20:35290 - - - - 0 NONE_NONE:HIER_NONE - "-"', // truly unknown (both domain and destIpPort are "-")
'1761332530.477 172.30.0.20:35291 allowed.example.com:443 5.5.5.5:443 1.1 CONNECT 200 TCP_TUNNEL:HIER_DIRECT allowed.example.com:443 "-"', // allowed request
].join("\n");
fs.writeFileSync(path.join(logsDir, "access.log"), logContent);
const result = getBlockedDomains(logsDir);
// iptables-dropped entries should use destIpPort as domain identifier
expect(result).toContain("8.8.8.8");
expect(result).toContain("1.2.3.4");
// truly unknown (both domain and destIpPort are "-") should be excluded
expect(result).not.toContain("-");
// allowed domains should not appear
expect(result).not.toContain("allowed.example.com");
});
it("should filter out internal Squid error entries (::1 client, -:- destination)", () => {
const logsDir = path.join(testDir, "logs-squid-internal");
fs.mkdirSync(logsDir, { recursive: true });
// Internal Squid error entries from localhost (::1) should be ignored
const logContent = [
'1773003472.027 ::1:52010 - -:- 0.0 - 0 NONE_NONE:HIER_NONE error:transaction-end-before-headers "-"',
'1773003475.167 172.30.0.30:50232 api.anthropic.com:443 18.64.224.91:443 1.1 CONNECT 200 TCP_TUNNEL:HIER_DIRECT api.anthropic.com:443 "-"',
'1773003477.068 ::1:35712 - -:- 0.0 - 0 NONE_NONE:HIER_NONE error:transaction-end-before-headers "-"',
'1773003480.123 172.30.0.30:50235 blocked.example.com:443 10.0.0.1:443 1.1 CONNECT 403 NONE_NONE:HIER_NONE blocked.example.com:443 "-"',
].join("\n");
fs.writeFileSync(path.join(logsDir, "access.log"), logContent);
const result = getBlockedDomains(logsDir);
// Real blocked domain should appear
expect(result).toContain("blocked.example.com");
// Internal Squid error entries should not appear as "-:-"
expect(result).not.toContain("-:-");
// Allowed domains should not appear
expect(result).not.toContain("api.anthropic.com");
});
it("should handle invalid log lines gracefully", () => {
const logsDir = path.join(testDir, "logs6");
fs.mkdirSync(logsDir, { recursive: true });
const logContent = ["# Comment line", "invalid line", '1761332530.474 172.30.0.20:35288 blocked.example.com:443 140.82.112.22:443 1.1 CONNECT 403 NONE_NONE:HIER_NONE blocked.example.com:443 "-"', "", "short"].join("\n");
fs.writeFileSync(path.join(logsDir, "access.log"), logContent);
const result = getBlockedDomains(logsDir);
expect(result).toEqual(["blocked.example.com"]);
});
});
describe("generateBlockedDomainsSection", () => {
it("should return empty string when no blocked domains", () => {
expect(generateBlockedDomainsSection([])).toBe("");
expect(generateBlockedDomainsSection(null)).toBe("");
expect(generateBlockedDomainsSection(undefined)).toBe("");
});
it("should generate details section for single blocked domain", () => {
const result = generateBlockedDomainsSection(["blocked.example.com"]);
expect(result).toContain("> [!WARNING]");
expect(result).toContain("> **⚠️ Firewall blocked 1 domain**");
expect(result).toContain("> - `blocked.example.com`");
expect(result).toContain("> The following domain was blocked by the firewall during workflow execution:");
expect(result).toContain('> ```yaml\n> network:\n> allowed:\n> - defaults\n> - "blocked.example.com"\n> ```');
expect(result).toContain("> See [Network Configuration](https://github.github.com/gh-aw/reference/network/) for more information.");
});
it("should generate details section for multiple blocked domains", () => {
const domains = ["alpha.example.com", "beta.example.com", "gamma.example.com"];
const result = generateBlockedDomainsSection(domains);
expect(result).toContain("> [!WARNING]");
expect(result).toContain("> **⚠️ Firewall blocked 3 domains**");
expect(result).toContain("> - `alpha.example.com`");
expect(result).toContain("> - `beta.example.com`");
expect(result).toContain("> - `gamma.example.com`");
expect(result).toContain('> ```yaml\n> network:\n> allowed:\n> - defaults\n> - "alpha.example.com"\n> - "beta.example.com"\n> - "gamma.example.com"\n> ```');
expect(result).toContain("> See [Network Configuration](https://github.github.com/gh-aw/reference/network/) for more information.");
});
it("should use correct singular/plural form", () => {
const singleResult = generateBlockedDomainsSection(["single.com"]);
expect(singleResult).toContain("1 domain");
expect(singleResult).toContain("domain was blocked");
const multiResult = generateBlockedDomainsSection(["one.com", "two.com"]);
expect(multiResult).toContain("2 domains");
expect(multiResult).toContain("domains were blocked");
});
it("should format domains with backticks", () => {
const result = generateBlockedDomainsSection(["example.com"]);
expect(result).toMatch(/> - `example\.com`/);
});
it("should start with double newline and warning alert", () => {
const result = generateBlockedDomainsSection(["example.com"]);
expect(result).toMatch(/^\n\n> \[!WARNING\]/);
});
});
});