Skip to content

feat: auto-enrich gh aw audit with firewall policy rule attribution#22756

Merged
pelikhan merged 9 commits intomainfrom
copilot/enhance-gh-aw-audit-firewall-attribution
Mar 25, 2026
Merged

feat: auto-enrich gh aw audit with firewall policy rule attribution#22756
pelikhan merged 9 commits intomainfrom
copilot/enhance-gh-aw-audit-firewall-attribution

Conversation

Copy link
Contributor

Copilot AI commented Mar 24, 2026

When policy-manifest.json and audit.jsonl are present in downloaded firewall artifacts, gh aw audit now automatically enriches the report with policy rule attribution — rule hit counts, denied request details, and summary statistics. Falls back to existing domain-count-only behavior when artifacts are absent. No new flags.

New: pkg/cli/firewall_policy.go

  • Types: PolicyManifest, PolicyRule, AuditLogEntry, EnrichedRequest, RuleHitStats, PolicyAnalysis
  • JSONL parser: parseAuditJSONL() — one JSON object per line, skips malformed lines
  • Domain matching: domainMatchesRule().github.com matches both github.com and *.github.com per AWF conventions; regex-based rules detected via containsRegexMeta() + domainMatchesRegex()
  • Rule evaluation: findMatchingRule() — top-to-bottom by order, first match wins; includes aclName "all" catch-all handling, protocol matching (protocolMatches()/isEntryHTTPS()), and observed-decision validation (isEntryAllowed())
  • Enrichment: enrichWithPolicyRules() — computes rule hits, denied request list, summary stats; filters error:transaction-end-before-headers Squid entries
  • Artifact detection: detectFirewallAuditArtifacts() — discovers files in sandbox/firewall/audit/ or firewall-audit-logs/

Modified

  • audit.go — calls analyzeFirewallPolicy(), wires PolicyAnalysis into ProcessedRun and RunSummary
  • audit_report.go — adds PolicyAnalysis field to AuditData
  • audit_report_render.gorenderPolicyAnalysis() renders rule hit table and denied request details; formatUnixTimestamp() helper
  • logs_models.go — adds PolicyAnalysis to ProcessedRun and RunSummary for cache persistence

Updated: .github/workflows/daily-firewall-report.md

Updated the daily firewall report agentic workflow to leverage the new policy_analysis data from the audit tool:

  • Step 2: Documents new policy_analysis fields returned by the audit tool (rule_hits, denied_requests, policy_summary, etc.)
  • Step 3: Adds policy rule attribution extraction instructions with jq examples for rule hit counts and denied requests
  • Step 4: Adds cross-run aggregation of rule hit counts, denied request grouping by rule/domain, and policy configuration tracking
  • Step 5, Section 4 (new): "Policy Rule Attribution" report section — policy config summary, rule hit table with 🟢/🔴 color-coding, denied requests with rule attribution, and rule effectiveness analysis
  • Step 5, Section 7: Security recommendations now include policy rule suggestions (zero-hit rules, allow rule additions)
  • Section is conditionally included only when policy_analysis data is available; falls back gracefully to domain-count-only reporting

Example console output

═══ Firewall Policy Analysis ═══

  Policy: 12 rules, SSL Bump disabled, DLP disabled

  Policy Rules:

  Rule                  Action  Description                         Hits
  allow-both-plain      allow   Allow HTTP/HTTPS to whitelisted...  47
  deny-blocked-plain    deny    Deny all other HTTP/HTTPS traffic   3

  Denied Requests (3):

  Time      Domain              Rule                Reason
  14:23:01  evil.com:443        deny-blocked-plain  Domain not in allowlist
  14:23:05  tracker.io:443      deny-blocked-plain  Domain not in allowlist

Tests

pkg/cli/firewall_policy_test.go — domain matching, rule matching (including aclName "all", protocol matching, observed-decision validation), regex metacharacter detection, manifest loading with round-trip for HostAccessEnabled/AllowHostPorts, JSONL parsing, enrichment with Squid error filtering, artifact detection, end-to-end analysis, timestamp formatting.


📍 Connect Copilot coding agent with Jira, Azure Boards or Linear to delegate work to Copilot in one click without leaving your project management tool.

Copilot AI and others added 2 commits March 24, 2026 21:17
Add PolicyManifest, PolicyRule Go structs and audit.jsonl JSONL parser.
Port enrichWithPolicyRules/domainMatchesRule from TypeScript to Go.
Auto-detect firewall audit artifacts (policy-manifest.json + audit.jsonl).
Integrate enriched policy analysis into audit console and JSON output.
Add comprehensive unit tests for all new functionality.

Co-authored-by: Mossaka <5447827+Mossaka@users.noreply.github.com>
Agent-Logs-Url: https://github.com/github/gh-aw/sessions/36ac2fc6-303e-452b-b36f-cadde5ac87f8
Copy link
Collaborator

@Mossaka Mossaka left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review — PR #22756

Good first implementation! The overall structure is clean, follows gh-aw patterns well (logger namespace, table-driven tests, struct tags, console.RenderTable, stringutil.Truncate), and the JSONL parser + artifact detection are solid. However, there are several behavioral divergences from the TypeScript source in gh-aw-firewall/src/logs/audit-enricher.ts that will cause incorrect rule attribution. These need fixing before merge.


🔴 Critical: findMatchingRule is missing three key behaviors from the TypeScript version

The TypeScript findMatchingRule() in audit-enricher.ts has three behaviors that the Go port omits:

1. No aclName === "all" handling for default deny rules

The TS version (lines 100-104) has:

if (rule.aclName === 'all') {
  if (expectedAction === 'deny') return rule;
  continue;
}

The Go version has no equivalent. In a real policy-manifest.json, the default deny rule has aclName: "all" with empty domains. Your current code will skip it (empty domains → no match), falling through to (implicit-deny). This means all denied traffic will be attributed to "implicit deny" instead of the actual deny-default rule, making the rule hit table inaccurate.

Your test works around this by giving the deny-all rule Domains: []string{"."}, but "." won't match arbitrary domains in the current matching logic either (confirmed: strings.HasSuffix("evil.com", ".") == false). So even in the test, evil.com falls through to implicit deny.

2. No protocol matching

The TS version checks protocolMatches(rule, entry.isHttps) (lines 78-83, 98). HTTPS-only rules shouldn't match HTTP requests, and vice versa. The Go version ignores protocol entirely.

You'll need to determine isHttps from the audit entry — the method === "CONNECT" check is a reliable signal (CONNECT = HTTPS tunneling).

3. No observed-decision validation

The TS version (line 95, 113) only attributes a rule if its action matches the observed outcome:

const expectedAction = entry.isAllowed ? 'allow' : 'deny';
// ...
if (rule.action === expectedAction) return rule;

The Go version attributes based purely on domain match, so an allow rule could be "credited" for denied traffic if the domain matches but the request was actually blocked (e.g., by a port-based rule).

Suggested fix: Change findMatchingRule to take the full AuditLogEntry (not just host string) so it has access to method/status for protocol and decision matching.


🟡 Important: Missing regex domain matching

The TS domainMatchesRule (lines 44-57) detects regex-based rules (aclName.includes('regex') or regex metacharacters in domains) and applies regex matching. The Go version only handles plain domain patterns. This will cause regex rules (like dst_ipv4 patterns with ^[0-9]+ entries) to never match.

At minimum, add the regex detection + regexp.MustCompile path, or document that regex rules are intentionally unsupported with a comment explaining why.


🟡 Important: Missing fields in Go PolicyManifest

The TypeScript PolicyManifest has two additional fields:

hostAccessEnabled: boolean;
allowHostPorts: string | null;

These should be added to the Go struct for forward compatibility, even if not used in the current rendering. Without them, round-tripping (unmarshal → marshal) loses data, and --json output will be incomplete.

HostAccessEnabled bool    `json:"hostAccessEnabled"`
AllowHostPorts    *string `json:"allowHostPorts"` // nullable → pointer

🟡 Minor: Missing error:transaction-end-before-headers filter

The TS computeRuleStats (line 163) skips entries where url === 'error:transaction-end-before-headers' — these are benign Squid operational entries. The Go enrichWithPolicyRules counts them, which may inflate totals.


✅ What looks good

  • Clean file organization (firewall_policy.go + firewall_policy_test.go)
  • Logger namespace cli:firewall_policy follows convention
  • JSONL parser with proper buffer sizing and malformed-line tolerance
  • Artifact detection with both sandbox/firewall/audit/ and firewall-audit-* fallback paths
  • Good test coverage: domain matching edge cases (suffix collision, case insensitivity, port stripping), manifest loading with sorting, JSONL parsing with empty/malformed lines
  • Proper integration into AuditData pipeline and renderConsole flow
  • --json output automatically works via struct tags
  • formatUnixTimestamp helper is clean

@copilot Please address the critical findMatchingRule issues (aclName "all" handling, protocol matching, and observed-decision validation) — these are needed for correct rule attribution. The regex and missing-fields issues are important but lower priority. Happy to discuss any of these further!

Copilot AI changed the title [WIP] Enhance gh aw audit to auto-enrich with firewall policy attribution feat: auto-enrich gh aw audit with firewall policy rule attribution Mar 24, 2026
Copilot AI requested a review from Mossaka March 24, 2026 21:19
…alidation, regex, missing fields, squid filter

- findMatchingRule now takes full AuditLogEntry instead of just host string
- Add aclName "all" catch-all handling for default deny rules
- Add protocol matching (CONNECT = HTTPS, else HTTP)
- Add observed-decision validation (rule action must match observed outcome)
- Add regex domain matching via aclName "regex" detection or metachar detection
- Add missing PolicyManifest fields (hostAccessEnabled, allowHostPorts)
- Filter error:transaction-end-before-headers Squid operational entries
- Add new tests: TestProtocolMatching, TestIsEntryHTTPS, TestIsEntryAllowed,
  TestContainsRegexMeta, hostAccessEnabled manifest test, squid filter test
- Update existing tests to use full AuditLogEntry with method/status/decision

Co-authored-by: Mossaka <5447827+Mossaka@users.noreply.github.com>
Agent-Logs-Url: https://github.com/github/gh-aw/sessions/8a6f8f24-239c-4326-b935-1553bf8d7487
Copy link
Contributor Author

Copilot AI commented Mar 24, 2026

Code Review — PR #22756

Good first implementation! The overall structure is clean, follows gh-aw patterns well (logger namespace, table-driven tests, struct tags, console.RenderTable, `stringu...

All issues addressed in 89e65a6:

🔴 Critical findMatchingRule fixes:

  • findMatchingRule now takes the full AuditLogEntry (not just host) and implements all 3 TS behaviors:
    • aclName === "all" handling: catch-all rules (e.g., default deny) now match any domain when action matches observed outcome
    • Protocol matching via protocolMatches(): HTTPS-only rules skip HTTP requests and vice versa; CONNECT method = HTTPS
    • Observed-decision validation via isEntryAllowed(): rule only matches when its action matches the observed outcome (allow rule won't be credited for denied traffic)

🟡 Regex domain matching:

  • Added containsRegexMeta() + domainMatchesRegex() — detects regex rules via aclName containing "regex" or regex metacharacters in domain patterns, then applies regexp.Compile matching

🟡 Missing PolicyManifest fields:

  • Added HostAccessEnabled bool and AllowHostPorts *string (nullable pointer) for forward-compatible JSON round-tripping

🟡 Squid filter:

  • enrichWithPolicyRules now skips entries with url === "error:transaction-end-before-headers"

Tests: Added TestProtocolMatching, TestIsEntryHTTPS, TestIsEntryAllowed, TestContainsRegexMeta, manifest round-trip test for new fields, squid error filter test. All existing tests updated to use full AuditLogEntry with method/status/decision.

Copy link
Collaborator

@Mossaka Mossaka left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up Review — Commit 89e65a6

@copilot Great job addressing the review feedback! All critical and important issues have been resolved:

✅ Critical issues fixed

  1. aclName "all" handlingfindMatchingRule now correctly handles catch-all rules with rule.ACLName == "all", matching any domain when the action matches the observed outcome. Tests verify both the match case (denied evil.com → deny-all) and the skip case (allowed unknown.com → nil).

  2. Protocol matchingprotocolMatches() and isEntryHTTPS() are correctly implemented. CONNECT → HTTPS, everything else → HTTP. Tests cover HTTPS-only rule matching/skipping.

  3. Observed-decision validationisEntryAllowed() mirrors the existing firewall log classifier (200/206/304 = allowed, 403/407 = denied, fallback to decision field). findMatchingRule now only attributes when rule.Action == expectedAction. Test "allow rule skipped for denied traffic" confirms this.

✅ Important issues fixed

  1. Regex domain matchingcontainsRegexMeta() + domainMatchesRegex() correctly detect and handle regex-based rules. Tests cover IP pattern and metacharacter detection.

  2. Missing PolicyManifest fieldsHostAccessEnabled bool and AllowHostPorts *string added with proper JSON tags. Test round-trips these fields.

  3. error:transaction-end-before-headers filter — Added in enrichWithPolicyRules with test coverage.

✅ Test coverage is comprehensive

  • TestFindMatchingRule now uses full AuditLogEntry with method/status/decision
  • TestProtocolMatching covers HTTPS-only rules
  • TestIsEntryHTTPS and TestIsEntryAllowed cover classification edge cases
  • TestContainsRegexMeta tests metacharacter detection
  • TestEnrichWithPolicyRules now verifies deny-all attribution and Squid error filtering
  • TestAnalyzeFirewallPolicy full-enrichment test uses proper aclName: "all" deny rule

The implementation now faithfully ports the TypeScript audit-enricher.ts logic. This looks good to merge once CI passes! 👍

@pelikhan
Copy link
Contributor

@copilot update the daily firewall report agentic workflow to leverage this data

Copilot AI and others added 2 commits March 24, 2026 22:14
Copy link
Contributor Author

Copilot AI commented Mar 24, 2026

@copilot update the daily firewall report agentic workflow to leverage this data

Updated the daily firewall report workflow in 1b1d7e5 to leverage policy_analysis data from the audit tool:

  • Step 2: Documents new policy_analysis fields (rule_hits, denied_requests, policy_summary, etc.)
  • Step 3: Adds policy rule attribution extraction with jq examples for rule hit counts and denied requests
  • Step 4: Adds cross-run aggregation of rule hit counts, denied request grouping by rule/domain, and policy config tracking
  • Step 5, Section 4: New "Policy Rule Attribution" report section with policy config summary, rule hit table (🟢/🔴 color-coded), denied requests with rule attribution, and rule effectiveness analysis
  • Step 5, Section 7: Security recommendations now include policy rule suggestions (zero-hit rules, allow rule additions)

The section is conditionally included — only when at least one run has policy_analysis data available. Falls back gracefully to existing domain-count-only reporting when artifacts are absent.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • https://api.github.com/graphql
    • Triggering command: /usr/bin/gh /usr/bin/gh api graphql -f query=query($owner: String!, $name: String!) { repository(owner: $owner, name: $name) { hasDiscussionsEnabled } } -f owner=github -f name=gh-aw (http block)
    • Triggering command: /usr/bin/gh /usr/bin/gh api graphql -f query=query($owner: String!, $name: String!) { repository(owner: $owner, name: $name) { hasDiscussionsEnabled } } -f owner=github -f name=gh-aw -pack /home/REDACTED/work/gh-aw/gh-aw/cmd/gh-aw/main.go (http block)
    • Triggering command: /usr/bin/gh /usr/bin/gh api graphql -f query=query($owner: String!, $name: String!) { repository(owner: $owner, name: $name) { hasDiscussionsEnabled } } -f owner=github -f name=gh-aw i_LhHEB/4maDO63q-1 (http block)
  • https://api.github.com/repos/astral-sh/setup-uv/git/ref/tags/eac588ad8def6316056a12d4907a9d4d84ff7a3b
    • Triggering command: /usr/bin/gh gh api /repos/astral-sh/setup-uv/git/ref/tags/eac588ad8def6316056a12d4907a9d4d84ff7a3b --jq .object.sha (http block)
    • Triggering command: /usr/bin/gh gh api /repos/astral-sh/setup-uv/git/ref/tags/eac588ad8def6316056a12d4907a9d4d84ff7a3b --jq .object.sha g_.a c x_amd64/vet - go1.25.0 -symabis x_amd64/vet -I /opt/hostedtoolc--irreversible-delete ule attribution -U0 x_amd64/vet Z6-ghOWur -Wl,--no-gc-sect-1 -fmessage-lengthxterm-color cyManifest|TestParseAuditJSONL|TestEnrichWithPolicyRules|TestDetectFirewallAuditArtifacts|TestAnowner=github (http block)
  • https://api.github.com/repos/github/gh-aw
    • Triggering command: /usr/bin/gh gh api /repos/github/gh-aw --jq .visibility (http block)
    • Triggering command: /usr/bin/gh gh api /repos/github/gh-aw --jq .visibility g_.a /opt/hostedtoolcache/go/1.25.0/x64/src/runtime/race r: $owner, name: $name) { hasDiscussionsEnabled } } /tmp/go-build356/usr/bin/gh go-sdk/internal/api x86_64-linux-gnugraphql 64/pkg/tool/linu-f -w d -n 10 .cfg 64/pkg/tool/linu-f -c -I /tmp/go-build356inspect 64/pkg/tool/linumcp/notion (http block)
  • https://api.github.com/repos/githubnext/agentics/git/ref/tags/
    • Triggering command: /usr/bin/gh gh api /repos/githubnext/agentics/git/ref/tags/# --jq .object.sha (http block)
    • Triggering command: /usr/bin/gh gh api /repos/githubnext/agentics/git/ref/tags/# --jq .object.sha 0144467/b157/_pkg_.a -I x_amd64/vet --gdwarf-5 --64 -o x_amd64/vet go_.�� -firewall-report.md -I x_amd64/vet Jk0y/wNzk_Rz6x9bgit --64 -o x_amd64/vet (http block)

If you need me to access, download, or install something from one of these locations, you can either:

@pelikhan pelikhan marked this pull request as ready for review March 24, 2026 22:15
Copilot AI review requested due to automatic review settings March 24, 2026 22:15
Copilot AI requested a review from pelikhan March 24, 2026 22:16
@pelikhan
Copy link
Contributor

@copilot Use formatters from console pkg

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds automatic firewall policy rule attribution to gh aw audit when policy-manifest.json and audit.jsonl are present, enriching the report with per-rule hit counts and denied request details while preserving the existing fallback behavior when artifacts are absent.

Changes:

  • Introduces pkg/cli/firewall_policy.go to detect artifacts, parse JSONL, match requests to policy rules, and compute enriched policy analysis.
  • Wires PolicyAnalysis into audit processing, JSON output, console rendering, and cached run summaries.
  • Updates the daily firewall report workflow guide to document the new policy_analysis output shape and suggested aggregations.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
pkg/cli/firewall_policy.go Implements policy manifest loading, audit JSONL parsing, rule matching, and enrichment output (PolicyAnalysis).
pkg/cli/firewall_policy_test.go Adds unit tests for domain/rule matching, parsing, artifact detection, enrichment, and timestamp formatting.
pkg/cli/audit.go Runs policy analysis during audit and attaches it to ProcessedRun / RunSummary.
pkg/cli/audit_report.go Adds policy_analysis to the structured JSON output (AuditData).
pkg/cli/audit_report_render.go Renders a new “Firewall Policy Analysis” console section and formats timestamps.
pkg/cli/logs_models.go Persists PolicyAnalysis in the cached run summary model.
.github/workflows/daily-firewall-report.md Documents how to consume policy_analysis in the daily report process.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +353 to +358
// No matching rule — implicit deny
enriched.RuleID = "(implicit-deny)"
enriched.Action = "deny"
enriched.Reason = "No matching policy rule"
deniedRequests = append(deniedRequests, enriched)
deniedCount++
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When no policy rule matches an entry, the code unconditionally treats the request as an implicit deny (adds it to DeniedRequests and increments deniedCount). This will misclassify requests that were actually allowed but couldn’t be attributed to a specific allow rule (e.g., due to non-domain ACLs, IP-based rules, or gaps in manifest rule parsing). Consider deriving allowed/denied from the observed entry outcome (reusing isEntryAllowed(entry)) and only classifying as "(implicit-deny)" when the entry was actually denied; for unmatched-but-allowed traffic, keep it allowed and attribute it to a separate placeholder like "(unattributed-allow)" (and add a test for this case).

Suggested change
// No matching rule — implicit deny
enriched.RuleID = "(implicit-deny)"
enriched.Action = "deny"
enriched.Reason = "No matching policy rule"
deniedRequests = append(deniedRequests, enriched)
deniedCount++
// No matching rule — derive outcome from entry
if isEntryAllowed(entry) {
// Allowed but cannot be attributed to a specific manifest rule
enriched.RuleID = "(unattributed-allow)"
enriched.Action = "allow"
allowedCount++
} else {
// No matching rule and the entry was actually denied — implicit deny
enriched.RuleID = "(implicit-deny)"
enriched.Action = "deny"
enriched.Reason = "No matching policy rule"
deniedRequests = append(deniedRequests, enriched)
deniedCount++
}

Copilot uses AI. Check for mistakes.
Comment on lines +326 to +330
// Strip port for domain tracking
domain := host
if idx := strings.LastIndex(host, ":"); idx != -1 {
domain = host[:idx]
}
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UniqueDomains is tracked using the raw host-derived domain without normalizing case. Since matching logic lowercases domains, this can over-count unique domains if the audit log contains mixed-case hosts. Consider lowercasing the stripped domain before inserting into uniqueDomains (and ideally share the same host→domain normalization helper used by domainMatchesRule).

Suggested change
// Strip port for domain tracking
domain := host
if idx := strings.LastIndex(host, ":"); idx != -1 {
domain = host[:idx]
}
// Strip port for domain tracking and normalize case
domain := host
if idx := strings.LastIndex(host, ":"); idx != -1 {
domain = host[:idx]
}
domain = strings.ToLower(domain)

Copilot uses AI. Check for mistakes.
Comment on lines +206 to +217
// domainMatchesRegex checks if a domain matches any regex pattern in the list.
func domainMatchesRegex(domain string, patterns []string) bool {
for _, pattern := range patterns {
re, err := regexp.Compile(pattern)
if err != nil {
firewallPolicyLog.Printf("Invalid regex pattern %q: %v", pattern, err)
continue
}
if re.MatchString(domain) {
return true
}
}
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

domainMatchesRegex recompiles each regex pattern on every match attempt. For large audit logs this can become a noticeable hotspot and will also spam logs repeatedly for any invalid pattern. Consider compiling regexes once (e.g., during loadPolicyManifest) into a cached []*regexp.Regexp on the rule (json:"-"), or caching compiled patterns in a map keyed by pattern string, so matching is just re.MatchString().

Copilot uses AI. Check for mistakes.
Copy link
Collaborator

@Mossaka Mossaka left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — Commits 5-6 (Workflow Update)

@copilot The daily firewall report workflow update looks good overall. The documentation accurately reflects the PolicyAnalysis struct fields and provides useful jq examples.

✅ Verified

  • JSON field names in workflow docs match Go struct tags (policy_summary, rule_hits, denied_requests, etc.)
  • Conditional "when policy_analysis is present" phrasing is correct — graceful fallback to firewall_analysis
  • Section numbering correctly updated (4→5→6→7)
  • jq examples are syntactically valid

⚠️ Issues from Copilot Reviewer worth addressing

The copilot-pull-request-reviewer raised 3 points that I agree with:

  1. Implicit deny misclassification (important): In enrichWithPolicyRules, when findMatchingRule returns nil, the code unconditionally classifies it as (implicit-deny). But in the TS source, enrichWithPolicyRules checks entry.isAllowed to determine the fallback reason: "Allowed (rule not identified)" vs "Denied (rule not identified)". The Go version should do the same:
} else {
    // No matching rule — check observed decision
    if isEntryAllowed(entry) {
        enriched.RuleID = "(unattributed-allow)"
        enriched.Action = "allow"
        enriched.Reason = "Allowed (rule not identified)"
        allowedCount++
    } else {
        enriched.RuleID = "(implicit-deny)"
        enriched.Action = "deny"
        enriched.Reason = "No matching policy rule"
        deniedRequests = append(deniedRequests, enriched)
        deniedCount++
    }
}
  1. Unique domains case normalization (minor): uniqueDomains map should use strings.ToLower(domain) since domain matching is case-insensitive.

  2. Regex recompilation (performance, low priority): domainMatchesRegex recompiles patterns on every call. Could cache compiled regexes in loadPolicyManifest. Low priority for Phase 1 since audit logs are typically small.

📋 Pending: pelikhan's "Use formatters from console pkg"

I noticed the summary stats in renderPolicyAnalysis use raw fmt.Fprintf for the counts section. The existing pattern elsewhere is console.RenderStruct for key-value displays — you should switch to that for consistency.

Copy link
Contributor Author

Copilot AI commented Mar 24, 2026

@copilot Use formatters from console pkg

Done in 2b29236. renderPolicyAnalysis now uses:

  • PolicySummaryDisplay struct with console tags → console.RenderStruct() for the key-value summary
  • console.FormatInfoMessage() for the "Policy Rules:" heading
  • console.FormatWarningMessage() for the "Denied Requests (N):" heading

Also fixed a slices.ContainsFunc lint modernize issue in firewall_policy.go.

…alize domain case

Co-authored-by: Mossaka <5447827+Mossaka@users.noreply.github.com>
Agent-Logs-Url: https://github.com/github/gh-aw/sessions/f7144292-b3b2-4f1f-ab3d-d759920e039f
Copy link
Contributor Author

Copilot AI commented Mar 24, 2026

Review — Commits 5-6 (Workflow Update)

@copilot The daily firewall report workflow update looks good overall. The documentation accurately reflects the PolicyAnalysis struct fields and provide...

All three issues addressed in f991b87:

  1. Implicit deny misclassificationenrichWithPolicyRules now checks isEntryAllowed(entry) when no rule matches. Allowed-but-unmatched traffic gets (unattributed-allow) with reason "Allowed (rule not identified)"; only actually denied traffic gets (implicit-deny). Test "unattributed-allow for allowed traffic with no matching rule" covers both paths.

  2. Unique domains case normalization — Domain tracking now uses strings.ToLower(domain) before insertion. Test "unique domains case normalized" verifies API.GITHUB.COM, api.github.com, and Api.GitHub.com count as 1 unique domain.

  3. Regex recompilation — Agreed this is low priority for Phase 1 given typical audit log sizes. Left as-is with a note for future optimization.

Copilot AI requested a review from Mossaka March 24, 2026 22:36
Copy link
Collaborator

@Mossaka Mossaka left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up Review — Commit f991b87

@copilot All three issues resolved correctly:

  1. Unattributed-allow: Matches TS behavior — isEntryAllowed(entry) check on the fallback path
  2. Case normalization: strings.ToLower(domain) before inserting into uniqueDomains
  3. Tests: Both new test cases are well-structured and cover the exact scenarios

The commit 7 (console formatters) also looks clean — PolicySummaryDisplay with console tags and RenderStruct follows the established pattern.

No further issues from me on commits 5-8. The remaining item from the Copilot reviewer (regex caching) is a performance optimization that's fine to skip for Phase 1.

@pelikhan pelikhan merged commit 5dfd614 into main Mar 25, 2026
49 checks passed
@pelikhan pelikhan deleted the copilot/enhance-gh-aw-audit-firewall-attribution branch March 25, 2026 00:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: auto-enrich gh aw audit with firewall policy rule attribution (Phase 1)

4 participants