From 9404cb245f877cdf4736fa9f2ffac70929f7b41d Mon Sep 17 00:00:00 2001 From: liquidsec Date: Mon, 19 Oct 2026 14:16:56 -0400 Subject: [PATCH 1/3] ci: open an issue when the daily signature update fails Adds a workflow_run watcher on Daily Signature Update that opens a labelled issue on failure, comments on it while failures continue, and closes it on the next success. Also count a provider that crashes during update as an error. It was previously dropped with zero errors, so it never counted toward ERROR_THRESHOLD and its data went stale silently. The error list is now written to the job summary for the alert's run link. Closes #303 --- .github/workflows/signature-update-alerts.yml | 74 +++++++++++++++++++ cloudcheck_update/__init__.py | 7 +- cloudcheck_update/cli.py | 15 ++++ cloudcheck_update/test_cloudcheck_update.py | 29 ++++++++ 4 files changed, 122 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/signature-update-alerts.yml diff --git a/.github/workflows/signature-update-alerts.yml b/.github/workflows/signature-update-alerts.yml new file mode 100644 index 00000000..2e7e47a8 --- /dev/null +++ b/.github/workflows/signature-update-alerts.yml @@ -0,0 +1,74 @@ +name: Signature Update Alerts + +# Opens a GitHub issue when the daily signature update fails, comments on it if it keeps failing, +# and closes it when the update next succeeds. +# The name below must match `name:` in daily-update.yml exactly, or alerting silently stops. +# workflow_run only fires from this file on the default branch (stable). +on: + workflow_run: + workflows: [Daily Signature Update] + types: [completed] + +permissions: + actions: read + issues: write + +concurrency: + group: signature-update-alerts + cancel-in-progress: false + +jobs: + alert: + runs-on: ubuntu-24.04 + if: contains(fromJSON('["failure", "timed_out", "startup_failure", "success"]'), github.event.workflow_run.conclusion) + steps: + - name: Open, update, or close the alert issue + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + CONCLUSION: ${{ github.event.workflow_run.conclusion }} + RUN_ID: ${{ github.event.workflow_run.id }} + RUN_URL: ${{ github.event.workflow_run.html_url }} + RUN_EVENT: ${{ github.event.workflow_run.event }} + HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }} + HEAD_SHA: ${{ github.event.workflow_run.head_sha }} + run: | + TITLE="Daily signature update failing" + LABEL="signature-update-failure" + + ISSUE=$(gh issue list --repo "$REPO" --label "$LABEL" --state open --json number,title \ + | jq -r --arg t "$TITLE" '[.[] | select(.title == $t)][0].number // empty') + + if [ "$CONCLUSION" = "success" ]; then + if [ -n "$ISSUE" ]; then + gh issue comment "$ISSUE" --repo "$REPO" --body "Recovered in $RUN_URL" + gh issue close "$ISSUE" --repo "$REPO" + fi + exit 0 + fi + + FAILED=$(gh api "repos/$REPO/actions/runs/$RUN_ID/jobs" --paginate \ + --jq '.jobs[] | select(.conclusion == "failure") | "- `\(.name)`: \([.steps[]? | select(.conclusion == "failure") | .name] | join(", "))"') + FAILED=${FAILED:-"- (no failed job details available)"} + + BODY=$(cat < threshold: print(f"Error count ({error_count}) exceeds threshold ({threshold})") return 1 @@ -17,5 +18,19 @@ def main(): return 0 +def write_step_summary(errors, threshold): + """Write the error list to the GitHub Actions job summary, if running in Actions.""" + summary_path = os.environ.get("GITHUB_STEP_SUMMARY") + if not summary_path: + return + lines = [f"## Signature update: {len(errors)} errors (threshold {threshold})", ""] + for error in errors: + # errors can carry multi-line tracebacks; keep only the first line in the summary + first_line = (str(error).strip().splitlines() or [""])[0] + lines.append(f"- {first_line}") + with open(summary_path, "a") as f: + f.write("\n".join(lines) + "\n") + + if __name__ == "__main__": sys.exit(main()) diff --git a/cloudcheck_update/test_cloudcheck_update.py b/cloudcheck_update/test_cloudcheck_update.py index fbf5408a..b2eae368 100644 --- a/cloudcheck_update/test_cloudcheck_update.py +++ b/cloudcheck_update/test_cloudcheck_update.py @@ -281,3 +281,32 @@ def test_cidr_defragmentation_mixed_ipv4_ipv6(): ) print("Mixed IPv4/IPv6 CIDR defragmentation test passed!") + + +def test_provider_crash_counts_as_error(): + """A provider whose update() raises must be reported as an error, not dropped silently.""" + from cloudcheck_update import _update_provider + + class CrashingProvider(Amazon): + def update(self): + raise RuntimeError("source unreachable") + + name, provider, errors = _update_provider(CrashingProvider) + assert name is None + assert provider is None + assert len(errors) == 1 + assert "CrashingProvider crashed: source unreachable" in errors[0] + + +def test_step_summary(tmp_path, monkeypatch): + """Errors are written to the GitHub Actions job summary, one line each.""" + from cloudcheck_update.cli import write_step_summary + + summary = tmp_path / "summary.md" + monkeypatch.setenv("GITHUB_STEP_SUMMARY", str(summary)) + write_step_summary(["first error\nTraceback line", "second error"], threshold=10) + content = summary.read_text() + assert "2 errors (threshold 10)" in content + assert "- first error\n" in content + assert "Traceback line" not in content + assert "- second error\n" in content From 2d44f6660455db227746e45a331679b37bce6bd9 Mon Sep 17 00:00:00 2001 From: liquidsec Date: Mon, 19 Oct 2026 14:19:32 -0400 Subject: [PATCH 2/3] fix(zscaler): skip non-cloud keys in the CENR feed The feed gained a top-level "svpnIPs" list next to the per-cloud dicts. Calling .items() on it raised every day, so Zscaler has had no CIDRs (0 stored). The listed IPs are all inside the published ranges, so they are skipped rather than added. --- cloudcheck/providers/zscaler.py | 3 +++ cloudcheck_update/test_cloudcheck_update.py | 24 +++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/cloudcheck/providers/zscaler.py b/cloudcheck/providers/zscaler.py index 2714c0ab..11a11f97 100644 --- a/cloudcheck/providers/zscaler.py +++ b/cloudcheck/providers/zscaler.py @@ -16,6 +16,9 @@ def fetch_cidrs(self): response_json = response.json() if isinstance(response_json, dict): for domain, data in response_json.items(): + # skip non-cloud keys like "svpnIPs" (a flat list of IPs already covered by the ranges) + if not isinstance(data, dict): + continue for continent, cities in data.items(): for city, ranges_list in cities.items(): for range_data in ranges_list: diff --git a/cloudcheck_update/test_cloudcheck_update.py b/cloudcheck_update/test_cloudcheck_update.py index b2eae368..774aa769 100644 --- a/cloudcheck_update/test_cloudcheck_update.py +++ b/cloudcheck_update/test_cloudcheck_update.py @@ -310,3 +310,27 @@ def test_step_summary(tmp_path, monkeypatch): assert "- first error\n" in content assert "Traceback line" not in content assert "- second error\n" in content + + +def test_zscaler_skips_non_cloud_keys(monkeypatch): + """Zscaler's feed has a top-level "svpnIPs" list next to the cloud dicts; it must not break parsing.""" + from cloudcheck.providers.zscaler import Zscaler + + class FakeResponse: + status_code = 200 + + def json(self): + return { + "zscaler.net": { + "continent : EMEA": { + "city : Abu Dhabi II": [ + {"range": "159.254.250.0/23"}, + {"range": ""}, + ] + } + }, + "svpnIPs": ["165.225.240.56"], + } + + monkeypatch.setattr(Zscaler, "request", lambda self, *a, **kw: FakeResponse()) + assert Zscaler().fetch_cidrs() == ["159.254.250.0/23"] From aa93136e0a3b1c5c95f474eb6d2bdead8d1b6f60 Mon Sep 17 00:00:00 2001 From: liquidsec Date: Mon, 19 Oct 2026 14:35:44 -0400 Subject: [PATCH 3/3] ci: pin Python 3.14 where the dev group is installed setup-python's 3.x now resolves to 3.15 on some runners, and blasthttp 0.10.0 has no cp315 wheels, so uv falls back to a source build that fails in openssl-sys. Pin Pipeline Tests and the PyPI publish job, the two places that install the dev group, until blasthttp ships 3.15 wheels. --- .github/workflows/pipeline-tests.yml | 2 +- .github/workflows/python-tests.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pipeline-tests.yml b/.github/workflows/pipeline-tests.yml index 0b9476f5..fae8dbdb 100644 --- a/.github/workflows/pipeline-tests.yml +++ b/.github/workflows/pipeline-tests.yml @@ -19,7 +19,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v7 with: - python-version: 3.x + python-version: "3.14" - name: Set up uv uses: astral-sh/setup-uv@v7 diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index 956195bd..871c067f 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -63,7 +63,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v7 with: - python-version: "3.x" + python-version: "3.14" - name: Set up Rust uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # 2026-09-03, no release tag - name: Set up uv