Skip to content

Commit eb4d35f

Browse files
committed
Unify egress allowlist by merging connected and disconnected modes
CI captures egress traffic from both connected and disconnected e2e deployments. However, both jobs run on the same hypervisor with shared caching (pip, Ansible collections), causing the disconnected run to skip traffic already cached by connected. This produces misleading per-mode splits. Merge all captured logs into a single unified allowlist grouped only by caller category (ansible, pip, podman, etc.). Changes: - analyze_egress.py: Accept multiple log files, output complete YAML - update_allowlist.sh: Use find to collect all log files - Workflows: Consolidate artifact downloads, remove mode arguments - Test fixtures: Update to unified format Signed-off-by: Rafa Porres Molina <rporresm@redhat.com> Assisted-by: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent fd2753c commit eb4d35f

7 files changed

Lines changed: 72 additions & 71 deletions

File tree

.github/workflows/egress-capture.yml

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -62,20 +62,12 @@ jobs:
6262
- name: Checkout code
6363
uses: actions/checkout@v4
6464

65-
- name: Download connected egress logs
65+
- name: Download egress logs
6666
uses: actions/download-artifact@v4
6767
with:
68-
pattern: egress-logs-connected-*
69-
path: egress-logs/connected/
70-
merge-multiple: true
71-
continue-on-error: true
72-
73-
- name: Download disconnected egress logs
74-
uses: actions/download-artifact@v4
75-
with:
76-
pattern: egress-logs-disconnected-*
77-
path: egress-logs/disconnected/
78-
merge-multiple: true
68+
pattern: egress-logs-*
69+
path: egress-logs/
70+
merge-multiple: false
7971
continue-on-error: true
8072

8173
- name: Analyze and update allow list

.github/workflows/infra-verify.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ jobs:
173173
if: (inputs.capture-egress == true || steps.egress_trigger.outputs.enabled == 'true') && !cancelled()
174174
run: |
175175
python3 scripts/egress/analyze_egress.py \
176-
artifacts/egress/opensnitch-connections.json.gz infra \
176+
artifacts/egress/opensnitch-connections.json.gz \
177177
> egress-allowlist-infra.yaml
178178
cat egress-allowlist-infra.yaml
179179
echo "## Egress Allow List (smoke test — infra only)" >> $GITHUB_STEP_SUMMARY

docs/EGRESS_ALLOWLIST.yaml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
11
# Auto-generated by scripts/egress/analyze_egress.py
22
# Do not edit manually — updated by the egress-capture CI workflow
3-
egress:
4-
connected: {}
5-
disconnected: {}
3+
egress: {}

scripts/egress/analyze_egress.py

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,11 @@
33
Parse OpenSnitch journald JSON logs and produce a normalized YAML egress allow list.
44
55
Usage:
6-
analyze_egress.py <log_file> <mode>
6+
analyze_egress.py <log_file> [<log_file> ...]
77
8-
log_file Path to journald JSON log file (journalctl -u opensnitchd -o json)
9-
mode Section label in the output YAML (e.g. "connected", "disconnected", "infra")
8+
log_file Path(s) to journald JSON log file(s) (journalctl -u opensnitchd -o json)
109
11-
Output: YAML section to stdout, suitable for inclusion in docs/EGRESS_ALLOWLIST.yaml.
10+
Output: Complete YAML document to stdout (docs/EGRESS_ALLOWLIST.yaml format).
1211
1312
Log format (MESSAGE field from opensnitchd journald entries):
1413
[action] src=<ip>:<port> dst=<ip>:<port> proto=<tcp|udp> ... host=<fqdn>
@@ -234,13 +233,33 @@ def parse_log_file(path: str) -> List[Dict]:
234233
return sorted(entries, key=lambda e: (e["category"], e["dns"], e["protocol"], e["port"]))
235234

236235

237-
def render_yaml(mode: str, entries: List[Dict]) -> str:
238-
"""Render entries as grouped YAML, sorted by category then dns."""
236+
def parse_log_files(paths: List[str]) -> List[Dict]:
237+
"""Parse multiple log files and return merged, deduplicated entries."""
238+
seen: Set[Tuple[str, str, int, str]] = set()
239+
entries: List[Dict] = []
240+
241+
for path in paths:
242+
for entry in parse_log_file(path):
243+
key = (entry["dns"], entry["protocol"], entry["port"], entry["category"])
244+
if key not in seen:
245+
seen.add(key)
246+
entries.append(entry)
247+
248+
return sorted(entries, key=lambda e: (e["category"], e["dns"], e["protocol"], e["port"]))
249+
250+
251+
def render_yaml(entries: List[Dict]) -> str:
252+
"""Render entries as a complete YAML document, grouped by category."""
239253
from collections import defaultdict
240254

241-
lines = [f" {mode}:"]
255+
lines = [
256+
"# Auto-generated by scripts/egress/analyze_egress.py",
257+
"# Do not edit manually — updated by the egress-capture CI workflow",
258+
"egress:"
259+
]
260+
242261
if not entries:
243-
lines.append(" {}")
262+
lines[-1] = "egress: {}"
244263
return "\n".join(lines)
245264

246265
# Group by category
@@ -250,23 +269,23 @@ def render_yaml(mode: str, entries: List[Dict]) -> str:
250269

251270
# Sort categories alphabetically
252271
for category in sorted(by_category.keys()):
253-
lines.append(f" {category}:")
272+
lines.append(f" {category}:")
254273
for e in by_category[category]:
255-
lines.append(f" - dns: {e['dns']}")
256-
lines.append(f" protocol: {e['protocol']}")
257-
lines.append(f" port: {e['port']}")
274+
lines.append(f" - dns: {e['dns']}")
275+
lines.append(f" protocol: {e['protocol']}")
276+
lines.append(f" port: {e['port']}")
258277

259278
return "\n".join(lines)
260279

261280

262281
def main() -> None:
263-
if len(sys.argv) != 3:
264-
print(f"Usage: {sys.argv[0]} <log_file> <mode>", file=sys.stderr)
282+
if len(sys.argv) < 2:
283+
print(f"Usage: {sys.argv[0]} <log_file> [<log_file> ...]", file=sys.stderr)
265284
sys.exit(1)
266285

267-
log_file, mode = sys.argv[1], sys.argv[2]
268-
entries = parse_log_file(log_file)
269-
print(render_yaml(mode, entries))
286+
log_files = sys.argv[1:]
287+
entries = parse_log_files(log_files)
288+
print(render_yaml(entries))
270289

271290

272291
if __name__ == "__main__":

scripts/egress/test_analyze_egress.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ EXPECTED="${SCRIPT_DIR}/testdata/expected.yaml"
1212
ACTUAL=$(mktemp)
1313
trap 'rm -f "$ACTUAL"' EXIT
1414

15-
python3 "$ANALYZER" "$FIXTURE" connected > "$ACTUAL"
15+
python3 "$ANALYZER" "$FIXTURE" > "$ACTUAL"
1616

1717
if diff -u "$EXPECTED" "$ACTUAL"; then
1818
echo "PASS: analyze_egress.py output matches expected"
Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
1-
connected:
2-
ansible:
3-
- dns: github.com
4-
protocol: tcp
5-
port: 443
6-
- dns: mirror.openshift.com
7-
protocol: tcp
8-
port: 443
9-
oc-mirror:
10-
- dns: registry.access.redhat.com
11-
protocol: tcp
12-
port: 443
13-
other:
14-
- dns: dns.google
15-
protocol: udp
16-
port: 53
17-
podman:
18-
- dns: quay.io
19-
protocol: tcp
20-
port: 443
1+
# Auto-generated by scripts/egress/analyze_egress.py
2+
# Do not edit manually — updated by the egress-capture CI workflow
3+
egress:
4+
ansible:
5+
- dns: github.com
6+
protocol: tcp
7+
port: 443
8+
- dns: mirror.openshift.com
9+
protocol: tcp
10+
port: 443
11+
oc-mirror:
12+
- dns: registry.access.redhat.com
13+
protocol: tcp
14+
port: 443
15+
other:
16+
- dns: dns.google
17+
protocol: udp
18+
port: 53
19+
podman:
20+
- dns: quay.io
21+
protocol: tcp
22+
port: 443

scripts/egress/update_allowlist.sh

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,36 +18,26 @@ PR_NUMBER="${PR_NUMBER:-}"
1818

1919
ALLOWLIST="${ENCLAVE_DIR}/docs/EGRESS_ALLOWLIST.yaml"
2020
GENERATED=$(mktemp)
21-
CONNECTED_LOGS="egress-logs/connected/opensnitch-connections.json.gz"
22-
DISCONNECTED_LOGS="egress-logs/disconnected/opensnitch-connections.json.gz"
2321

2422
trap 'rm -f "$GENERATED"' EXIT
2523

2624
# ── Build the generated allow list ────────────────────────────────────────────
2725

28-
{
29-
echo "# Auto-generated by scripts/egress/analyze_egress.py"
30-
echo "# Do not edit manually — updated by the egress-capture CI workflow"
31-
echo "egress:"
32-
} > "$GENERATED"
26+
LOG_FILES=$(find egress-logs/ -name 'opensnitch-connections.json.gz' -type f 2>/dev/null || true)
3327

34-
if [ -f "$CONNECTED_LOGS" ]; then
35-
python3 "${SCRIPT_DIR}/analyze_egress.py" "$CONNECTED_LOGS" connected >> "$GENERATED"
36-
else
37-
printf " connected:\n []\n" >> "$GENERATED"
28+
if [ -z "$LOG_FILES" ]; then
29+
echo "No egress log files found — nothing to analyze." | tee -a "${GITHUB_STEP_SUMMARY:-/dev/null}"
30+
exit 0
3831
fi
3932

40-
if [ -f "$DISCONNECTED_LOGS" ]; then
41-
python3 "${SCRIPT_DIR}/analyze_egress.py" "$DISCONNECTED_LOGS" disconnected >> "$GENERATED"
42-
else
43-
printf " disconnected:\n []\n" >> "$GENERATED"
44-
fi
33+
# shellcheck disable=SC2086
34+
python3 "${SCRIPT_DIR}/analyze_egress.py" $LOG_FILES > "$GENERATED"
4535

4636
# ── Compare with current allow list ───────────────────────────────────────────
4737

4838
if [ ! -f "$ALLOWLIST" ]; then
4939
# First run — treat an empty file as the current state
50-
printf "# Auto-generated by scripts/egress/analyze_egress.py\n# Do not edit manually — updated by the egress-capture CI workflow\negress:\n connected:\n []\n disconnected:\n []\n" > "$ALLOWLIST"
40+
printf "# Auto-generated by scripts/egress/analyze_egress.py\n# Do not edit manually — updated by the egress-capture CI workflow\negress: {}\n" > "$ALLOWLIST"
5141
fi
5242

5343
DIFF=$(diff -u "$ALLOWLIST" "$GENERATED" || true)

0 commit comments

Comments
 (0)