Skip to content

Commit 5482918

Browse files
authored
Fix firewall analysis showing "-" instead of actual blocked domains for iptables-dropped traffic (#20016)
1 parent 35d0c88 commit 5482918

8 files changed

Lines changed: 137 additions & 15 deletions

actions/setup/js/firewall_blocked_domains.cjs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,10 @@ function getBlockedDomains(logsDir) {
155155
// Check if request was blocked
156156
const isBlocked = isRequestBlocked(entry.decision, entry.status);
157157
if (isBlocked) {
158-
const sanitizedDomain = extractAndSanitizeDomain(entry.domain);
158+
// When domain is "-" (iptables-dropped traffic not visible to Squid),
159+
// fall back to dest IP:port so blocked requests show their actual destination instead of "-"
160+
const domainField = entry.domain !== "-" ? entry.domain : entry.destIpPort;
161+
const sanitizedDomain = extractAndSanitizeDomain(domainField);
159162
if (sanitizedDomain && sanitizedDomain !== "-") {
160163
blockedDomainsSet.add(sanitizedDomain);
161164
}

actions/setup/js/firewall_blocked_domains.test.cjs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,34 @@ describe("firewall_blocked_domains.cjs", () => {
230230

231231
const result = getBlockedDomains(logsDir);
232232

233-
expect(result).toEqual(["blocked.example.com"]);
233+
// The iptables-dropped entry uses destIpPort (140.82.112.22) as fallback
234+
expect(result).toContain("blocked.example.com");
235+
expect(result).toContain("140.82.112.22");
236+
});
237+
238+
it("should use destIpPort as fallback when domain is placeholder", () => {
239+
const logsDir = path.join(testDir, "logs-iptables");
240+
fs.mkdirSync(logsDir, { recursive: true });
241+
242+
// Simulate iptables-dropped traffic: domain="-", destIpPort has actual destination
243+
const logContent = [
244+
'1761332530.474 172.30.0.20:35288 - 8.8.8.8:53 - - 0 NONE_NONE:HIER_NONE - "-"', // iptables-dropped DNS query
245+
'1761332530.475 172.30.0.20:35289 - 1.2.3.4:443 - - 0 NONE_NONE:HIER_NONE - "-"', // iptables-dropped HTTPS
246+
'1761332530.476 172.30.0.20:35290 - - - - 0 NONE_NONE:HIER_NONE - "-"', // truly unknown (both domain and destIpPort are "-")
247+
'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
248+
].join("\n");
249+
250+
fs.writeFileSync(path.join(logsDir, "access.log"), logContent);
251+
252+
const result = getBlockedDomains(logsDir);
253+
254+
// iptables-dropped entries should use destIpPort as domain identifier
255+
expect(result).toContain("8.8.8.8");
256+
expect(result).toContain("1.2.3.4");
257+
// truly unknown (both domain and destIpPort are "-") should be excluded
258+
expect(result).not.toContain("-");
259+
// allowed domains should not appear
260+
expect(result).not.toContain("allowed.example.com");
234261
});
235262

236263
it("should handle invalid log lines gracefully", () => {

actions/setup/js/git_patch_integration.test.cjs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,22 @@
1111
* These tests require git to be installed and create temporary git repos.
1212
*/
1313

14-
import { describe, it, expect, beforeEach, afterEach } from "vitest";
14+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
1515
import fs from "fs";
1616
import path from "path";
1717
import { spawnSync } from "child_process";
1818
import os from "os";
1919
import { generateGitPatch } from "./generate_git_patch.cjs";
2020

21+
// generateGitPatch uses execGitSync from git_helpers.cjs which calls core.debug / core.error
22+
// as GitHub Actions globals. Provide a no-op mock so these tests work outside of Actions.
23+
global.core = {
24+
debug: vi.fn(),
25+
error: vi.fn(),
26+
info: vi.fn(),
27+
warning: vi.fn(),
28+
};
29+
2130
/**
2231
* Execute git command safely with args array
2332
*/

actions/setup/js/parse_firewall_logs.cjs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,19 +57,23 @@ async function main() {
5757
// Determine if request was allowed or blocked
5858
const isAllowed = isRequestAllowed(entry.decision, entry.status);
5959

60+
// When domain is "-" (iptables-dropped traffic not visible to Squid),
61+
// fall back to dest IP:port so blocked requests show their actual destination instead of "-"
62+
const domainKey = entry.domain !== "-" ? entry.domain : entry.destIpPort !== "-" ? entry.destIpPort : "-";
63+
6064
if (isAllowed) {
6165
allowedRequests++;
62-
allowedDomains.add(entry.domain);
66+
allowedDomains.add(domainKey);
6367
} else {
6468
blockedRequests++;
65-
blockedDomains.add(entry.domain);
69+
blockedDomains.add(domainKey);
6670
}
6771

6872
// Track request count per domain
69-
if (!requestsByDomain.has(entry.domain)) {
70-
requestsByDomain.set(entry.domain, { allowed: 0, blocked: 0 });
73+
if (!requestsByDomain.has(domainKey)) {
74+
requestsByDomain.set(domainKey, { allowed: 0, blocked: 0 });
7175
}
72-
const domainStats = requestsByDomain.get(entry.domain);
76+
const domainStats = requestsByDomain.get(domainKey);
7377
if (isAllowed) {
7478
domainStats.allowed++;
7579
} else {

cmd/gh-aw/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ Examples:
216216

217217
var compileCmd = &cobra.Command{
218218
Use: "compile [workflow]...",
219-
Short: "Compile workflow markdown files (.md) into GitHub Actions workflows (.lock.yml)",
219+
Short: "Compile workflow Markdown files (.md) into GitHub Actions workflows (.lock.yml)",
220220
Long: `Compile one or more agentic workflows to YAML workflows.
221221
222222
If no workflows are specified, all Markdown files in .github/workflows will be compiled.

pkg/cli/firewall_log.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,8 +252,12 @@ func parseFirewallLog(logPath string, verbose bool) (*FirewallAnalysis, error) {
252252
// Determine if request was allowed or blocked
253253
isAllowed := isRequestAllowed(entry.Decision, entry.Status)
254254

255-
// Extract domain (remove port)
255+
// Extract domain - when domain is "-" (iptables-dropped traffic not visible to Squid),
256+
// fall back to dest IP:port so blocked requests show their actual destination instead of "-"
256257
domain := entry.Domain
258+
if domain == "-" && entry.DestIPPort != "-" {
259+
domain = entry.DestIPPort
260+
}
257261

258262
if isAllowed {
259263
analysis.AllowedRequests++

pkg/cli/firewall_log_test.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,76 @@ func TestParseFirewallLogPartialMissingFields(t *testing.T) {
453453
}
454454
}
455455

456+
func TestParseFirewallLogIptablesDropped(t *testing.T) {
457+
// Create a temporary directory for the test
458+
tempDir := testutil.TempDir(t, "test-*")
459+
460+
// Simulate iptables-dropped traffic: domain="-" but destIPPort has the actual destination.
461+
// This occurs when iptables drops packets before they reach the Squid proxy, so Squid
462+
// only sees the IP layer info and logs domain as "-".
463+
testLogContent := `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 "-"
464+
1761332531.123 172.30.0.20:35289 - 8.8.8.8:53 - - 0 NONE_NONE:HIER_NONE - "-"
465+
1761332532.456 172.30.0.20:35290 - 1.2.3.4:443 - - 0 NONE_NONE:HIER_NONE - "-"
466+
1761332533.789 172.30.0.20:35291 - 1.2.3.4:443 - - 0 NONE_NONE:HIER_NONE - "-"
467+
1761332534.012 172.30.0.20:35292 - - - - 0 NONE_NONE:HIER_NONE - "-"
468+
`
469+
470+
// Write test log file
471+
logPath := filepath.Join(tempDir, "firewall.log")
472+
err := os.WriteFile(logPath, []byte(testLogContent), 0644)
473+
if err != nil {
474+
t.Fatalf("Failed to create test firewall.log: %v", err)
475+
}
476+
477+
// Test parsing
478+
analysis, err := parseFirewallLog(logPath, false)
479+
if err != nil {
480+
t.Fatalf("Failed to parse firewall log: %v", err)
481+
}
482+
483+
if analysis.TotalRequests != 5 {
484+
t.Errorf("TotalRequests: got %d, want 5", analysis.TotalRequests)
485+
}
486+
if analysis.AllowedRequests != 1 {
487+
t.Errorf("AllowedRequests: got %d, want 1", analysis.AllowedRequests)
488+
}
489+
if analysis.BlockedRequests != 4 {
490+
t.Errorf("BlockedRequests: got %d, want 4", analysis.BlockedRequests)
491+
}
492+
493+
// Iptables-dropped entries with destIPPort should use destIPPort as the key
494+
if stats, ok := analysis.RequestsByDomain["8.8.8.8:53"]; !ok {
495+
t.Error("8.8.8.8:53 should be in RequestsByDomain (iptables-dropped fallback)")
496+
} else if stats.Blocked != 1 {
497+
t.Errorf("8.8.8.8:53 Blocked: got %d, want 1", stats.Blocked)
498+
}
499+
500+
if stats, ok := analysis.RequestsByDomain["1.2.3.4:443"]; !ok {
501+
t.Error("1.2.3.4:443 should be in RequestsByDomain (iptables-dropped fallback)")
502+
} else if stats.Blocked != 2 {
503+
t.Errorf("1.2.3.4:443 Blocked: got %d, want 2", stats.Blocked)
504+
}
505+
506+
// "-" should only appear for entries where both domain and destIPPort are "-"
507+
if stats, ok := analysis.RequestsByDomain["-"]; !ok {
508+
t.Error("\"-\" should be in RequestsByDomain for truly-unknown entries")
509+
} else if stats.Blocked != 1 {
510+
t.Errorf("\"-\" Blocked: got %d, want 1", stats.Blocked)
511+
}
512+
513+
// BlockedDomains should include the real IPs, not just "-"
514+
blockedSet := make(map[string]bool)
515+
for _, d := range analysis.BlockedDomains {
516+
blockedSet[d] = true
517+
}
518+
if !blockedSet["8.8.8.8:53"] {
519+
t.Error("BlockedDomains should contain 8.8.8.8:53 (iptables-dropped fallback)")
520+
}
521+
if !blockedSet["1.2.3.4:443"] {
522+
t.Error("BlockedDomains should contain 1.2.3.4:443 (iptables-dropped fallback)")
523+
}
524+
}
525+
456526
func TestAnalyzeMultipleFirewallLogs(t *testing.T) {
457527
// Create a temporary directory for the test
458528
tempDir := testutil.TempDir(t, "test-*")

pkg/cli/logs_parsing_firewall.go

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -163,19 +163,24 @@ const originalMain = function() {
163163
// Determine if request was allowed or blocked
164164
const isAllowed = isRequestAllowed(entry.decision, entry.status);
165165
166+
// When domain is "-" (iptables-dropped traffic not visible to Squid),
167+
// fall back to dest IP:port so blocked requests show their actual destination instead of "-"
168+
const domainKey =
169+
entry.domain !== "-" ? entry.domain : entry.destIpPort !== "-" ? entry.destIpPort : "-";
170+
166171
if (isAllowed) {
167172
allowedRequests++;
168-
allowedDomains.add(entry.domain);
173+
allowedDomains.add(domainKey);
169174
} else {
170175
blockedRequests++;
171-
blockedDomains.add(entry.domain);
176+
blockedDomains.add(domainKey);
172177
}
173178
174179
// Track request count per domain
175-
if (!requestsByDomain.has(entry.domain)) {
176-
requestsByDomain.set(entry.domain, { allowed: 0, blocked: 0 });
180+
if (!requestsByDomain.has(domainKey)) {
181+
requestsByDomain.set(domainKey, { allowed: 0, blocked: 0 });
177182
}
178-
const domainStats = requestsByDomain.get(entry.domain);
183+
const domainStats = requestsByDomain.get(domainKey);
179184
if (isAllowed) {
180185
domainStats.allowed++;
181186
} else {

0 commit comments

Comments
 (0)