From a9de6babcfd17f1209e61a57ae1fb24b43010af4 Mon Sep 17 00:00:00 2001 From: James McKinney <26463+jpmckinney@users.noreply.github.com> Date: Mon, 21 Sep 2026 12:21:01 -0400 Subject: [PATCH 1/2] fix(docs): Anchor the redirect patterns, so they match one path only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Without a trailing $, a pattern matches at any depth, so every retired path has an infinite URL space beneath it that answers 302 rather than 404. A crawler that resolves a target page's relative links against the requested URL appends a segment, is redirected again, and never terminates. That accounts for 7,388,812 of the 15,569,216 requests in the retained logs, between 333,000 and 689,000 a day, and the share has grown from 45% to 52%. Rate limiting cannot help: the crawler averages 75 requests per 10 seconds, below any threshold that a WordPress admin page load stays under. Of the deeper requests, 137 are not loop artefacts. None is a URL that ever existed: what.md has only been under primer/, and design.md, consortia.md and pre-qualification.md only under guidance/ — never under getting_started/ or implementation/, where they were requested. The optional trailing slash is unchanged, and the anchor is on the URL-path, so query strings still match and are appended to the target. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0148VmvbJ2LcshwztmwuKRoU --- salt/apache/files/sites/docs.conf.include | 120 +++++++++++----------- 1 file changed, 60 insertions(+), 60 deletions(-) diff --git a/salt/apache/files/sites/docs.conf.include b/salt/apache/files/sites/docs.conf.include index 915599fed..51d5cc7c8 100644 --- a/salt/apache/files/sites/docs.conf.include +++ b/salt/apache/files/sites/docs.conf.include @@ -237,50 +237,50 @@ RewriteRule . %1/%2 [R=301,L] #################### # Extensions -RedirectMatch ^/{{ version }}/{{ lang }}/extensions/community/? https://extensions.open-contracting.org/{{ lang }}/extensions/ -RedirectMatch ^/{{ version }}/{{ lang }}/extensions/developing/? https://extensions.open-contracting.org/{{ lang }}/extensions/ -RedirectMatch ^/{{ version }}/{{ lang }}/extensions/party_details/? https://extensions.open-contracting.org/{{ lang }}/extensions/ -RedirectMatch ^/{{ version }}/{{ lang }}/extensions/bids/? https://extensions.open-contracting.org/{{ lang }}/extensions/bids/ -RedirectMatch ^/{{ version }}/{{ lang }}/extensions/enquiries/? https://extensions.open-contracting.org/{{ lang }}/extensions/enquiries/ -RedirectMatch ^/{{ version }}/{{ lang }}/extensions/location/? https://extensions.open-contracting.org/{{ lang }}/extensions/location/ -RedirectMatch ^/{{ version }}/{{ lang }}/extensions/lots/? https://extensions.open-contracting.org/{{ lang }}/extensions/lots/ -RedirectMatch ^/{{ version }}/{{ lang }}/extensions/participation_fee/? https://extensions.open-contracting.org/{{ lang }}/extensions/participation_fee/ -RedirectMatch ^/{{ version }}/{{ lang }}/extensions/process_title/? https://extensions.open-contracting.org/{{ lang }}/extensions/process_title/ +RedirectMatch ^/{{ version }}/{{ lang }}/extensions/community/?$ https://extensions.open-contracting.org/{{ lang }}/extensions/ +RedirectMatch ^/{{ version }}/{{ lang }}/extensions/developing/?$ https://extensions.open-contracting.org/{{ lang }}/extensions/ +RedirectMatch ^/{{ version }}/{{ lang }}/extensions/party_details/?$ https://extensions.open-contracting.org/{{ lang }}/extensions/ +RedirectMatch ^/{{ version }}/{{ lang }}/extensions/bids/?$ https://extensions.open-contracting.org/{{ lang }}/extensions/bids/ +RedirectMatch ^/{{ version }}/{{ lang }}/extensions/enquiries/?$ https://extensions.open-contracting.org/{{ lang }}/extensions/enquiries/ +RedirectMatch ^/{{ version }}/{{ lang }}/extensions/location/?$ https://extensions.open-contracting.org/{{ lang }}/extensions/location/ +RedirectMatch ^/{{ version }}/{{ lang }}/extensions/lots/?$ https://extensions.open-contracting.org/{{ lang }}/extensions/lots/ +RedirectMatch ^/{{ version }}/{{ lang }}/extensions/participation_fee/?$ https://extensions.open-contracting.org/{{ lang }}/extensions/participation_fee/ +RedirectMatch ^/{{ version }}/{{ lang }}/extensions/process_title/?$ https://extensions.open-contracting.org/{{ lang }}/extensions/process_title/ # Iterative improvements from OCDS 1.1.4 and up -RedirectMatch ^/{{ version }}/{{ lang }}/getting_started/building_blocks/? /{{ version }}/{{ lang }}/primer/how/ -RedirectMatch ^/{{ version }}/{{ lang }}/getting_started/contracting_process/? /{{ version }}/{{ lang }}/primer/how/ -RedirectMatch ^/{{ version }}/{{ lang }}/getting_started/publication_patterns/? /{{ version }}/{{ lang }}/guidance/build/hosting/ -RedirectMatch ^/{{ version }}/{{ lang }}/getting_started/quality/? /{{ version }}/{{ lang }}/guidance/publish/quality/ -RedirectMatch ^/{{ version }}/{{ lang }}/getting_started/releases_and_records/? /{{ version }}/{{ lang }}/primer/releases_and_records/ -RedirectMatch ^/{{ version }}/{{ lang }}/getting_started/use_cases/? /{{ version }}/{{ lang }}/guidance/design/user_needs/ -RedirectMatch ^/{{ version }}/{{ lang }}/getting_started/validation/? /{{ version }}/{{ lang }}/guidance/build/#check-your-data -RedirectMatch ^/{{ version }}/{{ lang }}/getting_started/? /{{ version }}/{{ lang }}/primer/ -RedirectMatch ^/{{ version }}/{{ lang }}/guidance/map/awards_contracts_buyers_suppliers/? /{{ version }}/{{ lang }}/guidance/map/awards_contracts/ -RedirectMatch ^/{{ version }}/{{ lang }}/guidance/map/award_notices_decisions/? /{{ version }}/{{ lang }}/guidance/map/awards_contracts/#awards-and-award-notices -RedirectMatch ^/{{ version }}/{{ lang }}/guidance/map/mapping_awards_contracts/? /{{ version }}/{{ lang }}/guidance/map/awards_contracts/#awards-and-contracts -RedirectMatch ^/{{ version }}/{{ lang }}/guidance/map/purchase_orders/? /{{ version }}/{{ lang }}/guidance/map/awards_contracts/#purchase-orders -RedirectMatch ^/{{ version }}/{{ lang }}/guidance/map/consortia/? /{{ version }}/{{ lang }}/guidance/map/buyers_suppliers/#consortia-suppliers -RedirectMatch ^/{{ version }}/{{ lang }}/guidance/map/frameworks/? /{{ version }}/{{ lang }}/guidance/map/framework_agreements/ -RedirectMatch ^/{{ version }}/{{ lang }}/guidance/map/related_processes/? /{{ version }}/{{ lang }}/guidance/map/framework_agreements/ -RedirectMatch ^/{{ version }}/{{ lang }}/guidance/map/unsuccessful_tender/? /{{ version }}/{{ lang }}/guidance/map/unsuccessful_processes/ -RedirectMatch ^/{{ version }}/{{ lang }}/guidance/map/catalogs/? /{{ version }}/{{ lang }}/guidance/map/electronic_catalogues/ -RedirectMatch ^/{{ version }}/{{ lang }}/extensions/? /{{ version }}/{{ lang }}/guidance/map/extensions/ -RedirectMatch ^/{{ version }}/{{ lang }}/implementation/amendments/? /{{ version }}/{{ lang }}/guidance/map/amendments/ -RedirectMatch ^/{{ version }}/{{ lang }}/implementation/hosting/? /{{ version }}/{{ lang }}/guidance/build/hosting/ -RedirectMatch ^/{{ version }}/{{ lang }}/implementation/levels/? /{{ version }}/{{ lang }}/guidance/publish/quality/ -RedirectMatch ^/{{ version }}/{{ lang }}/implementation/licensing/? /{{ version }}/{{ lang }}/guidance/publish/#license-your-data -RedirectMatch ^/{{ version }}/{{ lang }}/implementation/publication_policy/? /{{ version }}/{{ lang }}/guidance/publish/#finalize-your-publication-policy -RedirectMatch ^/{{ version }}/{{ lang }}/implementation/registration/? /{{ version }}/{{ lang }}/guidance/build/#register-an-ocid-prefix -RedirectMatch ^/{{ version }}/{{ lang }}/implementation/related_processes/? /{{ version }}/{{ lang }}/guidance/map/framework_agreements/ -RedirectMatch ^/{{ version }}/{{ lang }}/implementation/serialization/? /{{ version }}/{{ lang }}/guidance/build/serialization/ -RedirectMatch ^/{{ version }}/{{ lang }}/implementation/? /{{ version }}/{{ lang }}/guidance/ -RedirectMatch ^/{{ version }}/{{ lang }}/schema/changelog/? /{{ version }}/{{ lang }}/history/changelog/ -RedirectMatch ^/{{ version }}/{{ lang }}/schema/deprecation/? /{{ version }}/{{ lang }}/governance/deprecation/ -RedirectMatch ^/{{ version }}/{{ lang }}/support/credits/? /{{ version }}/{{ lang }}/history/history_and_development/#appreciation -RedirectMatch ^/{{ version }}/{{ lang }}/support/governance/? /{{ version }}/{{ lang }}/governance/ -RedirectMatch ^/{{ version }}/{{ lang }}/support/history_and_development/? /{{ version }}/{{ lang }}/history/history_and_development/ -RedirectMatch ^/{{ version }}/{{ lang }}/support/tools/? /{{ version }}/{{ lang }}/support/#ocds-tools +RedirectMatch ^/{{ version }}/{{ lang }}/getting_started/building_blocks/?$ /{{ version }}/{{ lang }}/primer/how/ +RedirectMatch ^/{{ version }}/{{ lang }}/getting_started/contracting_process/?$ /{{ version }}/{{ lang }}/primer/how/ +RedirectMatch ^/{{ version }}/{{ lang }}/getting_started/publication_patterns/?$ /{{ version }}/{{ lang }}/guidance/build/hosting/ +RedirectMatch ^/{{ version }}/{{ lang }}/getting_started/quality/?$ /{{ version }}/{{ lang }}/guidance/publish/quality/ +RedirectMatch ^/{{ version }}/{{ lang }}/getting_started/releases_and_records/?$ /{{ version }}/{{ lang }}/primer/releases_and_records/ +RedirectMatch ^/{{ version }}/{{ lang }}/getting_started/use_cases/?$ /{{ version }}/{{ lang }}/guidance/design/user_needs/ +RedirectMatch ^/{{ version }}/{{ lang }}/getting_started/validation/?$ /{{ version }}/{{ lang }}/guidance/build/#check-your-data +RedirectMatch ^/{{ version }}/{{ lang }}/getting_started/?$ /{{ version }}/{{ lang }}/primer/ +RedirectMatch ^/{{ version }}/{{ lang }}/guidance/map/awards_contracts_buyers_suppliers/?$ /{{ version }}/{{ lang }}/guidance/map/awards_contracts/ +RedirectMatch ^/{{ version }}/{{ lang }}/guidance/map/award_notices_decisions/?$ /{{ version }}/{{ lang }}/guidance/map/awards_contracts/#awards-and-award-notices +RedirectMatch ^/{{ version }}/{{ lang }}/guidance/map/mapping_awards_contracts/?$ /{{ version }}/{{ lang }}/guidance/map/awards_contracts/#awards-and-contracts +RedirectMatch ^/{{ version }}/{{ lang }}/guidance/map/purchase_orders/?$ /{{ version }}/{{ lang }}/guidance/map/awards_contracts/#purchase-orders +RedirectMatch ^/{{ version }}/{{ lang }}/guidance/map/consortia/?$ /{{ version }}/{{ lang }}/guidance/map/buyers_suppliers/#consortia-suppliers +RedirectMatch ^/{{ version }}/{{ lang }}/guidance/map/frameworks/?$ /{{ version }}/{{ lang }}/guidance/map/framework_agreements/ +RedirectMatch ^/{{ version }}/{{ lang }}/guidance/map/related_processes/?$ /{{ version }}/{{ lang }}/guidance/map/framework_agreements/ +RedirectMatch ^/{{ version }}/{{ lang }}/guidance/map/unsuccessful_tender/?$ /{{ version }}/{{ lang }}/guidance/map/unsuccessful_processes/ +RedirectMatch ^/{{ version }}/{{ lang }}/guidance/map/catalogs/?$ /{{ version }}/{{ lang }}/guidance/map/electronic_catalogues/ +RedirectMatch ^/{{ version }}/{{ lang }}/extensions/?$ /{{ version }}/{{ lang }}/guidance/map/extensions/ +RedirectMatch ^/{{ version }}/{{ lang }}/implementation/amendments/?$ /{{ version }}/{{ lang }}/guidance/map/amendments/ +RedirectMatch ^/{{ version }}/{{ lang }}/implementation/hosting/?$ /{{ version }}/{{ lang }}/guidance/build/hosting/ +RedirectMatch ^/{{ version }}/{{ lang }}/implementation/levels/?$ /{{ version }}/{{ lang }}/guidance/publish/quality/ +RedirectMatch ^/{{ version }}/{{ lang }}/implementation/licensing/?$ /{{ version }}/{{ lang }}/guidance/publish/#license-your-data +RedirectMatch ^/{{ version }}/{{ lang }}/implementation/publication_policy/?$ /{{ version }}/{{ lang }}/guidance/publish/#finalize-your-publication-policy +RedirectMatch ^/{{ version }}/{{ lang }}/implementation/registration/?$ /{{ version }}/{{ lang }}/guidance/build/#register-an-ocid-prefix +RedirectMatch ^/{{ version }}/{{ lang }}/implementation/related_processes/?$ /{{ version }}/{{ lang }}/guidance/map/framework_agreements/ +RedirectMatch ^/{{ version }}/{{ lang }}/implementation/serialization/?$ /{{ version }}/{{ lang }}/guidance/build/serialization/ +RedirectMatch ^/{{ version }}/{{ lang }}/implementation/?$ /{{ version }}/{{ lang }}/guidance/ +RedirectMatch ^/{{ version }}/{{ lang }}/schema/changelog/?$ /{{ version }}/{{ lang }}/history/changelog/ +RedirectMatch ^/{{ version }}/{{ lang }}/schema/deprecation/?$ /{{ version }}/{{ lang }}/governance/deprecation/ +RedirectMatch ^/{{ version }}/{{ lang }}/support/credits/?$ /{{ version }}/{{ lang }}/history/history_and_development/#appreciation +RedirectMatch ^/{{ version }}/{{ lang }}/support/governance/?$ /{{ version }}/{{ lang }}/governance/ +RedirectMatch ^/{{ version }}/{{ lang }}/support/history_and_development/?$ /{{ version }}/{{ lang }}/history/history_and_development/ +RedirectMatch ^/{{ version }}/{{ lang }}/support/tools/?$ /{{ version }}/{{ lang }}/support/#ocds-tools {%- endfor %} {%- endfor %} @@ -290,24 +290,24 @@ RedirectMatch ^/{{ version }}/{{ lang }}/support/tools/? # PPP {{ lang }} {{ version }} #################### -RedirectMatch /profiles/ppp/{{ version }}/{{ lang }}/extensions/bids/? https://extensions.open-contracting.org/{{ lang }}/extensions/bids/ -RedirectMatch /profiles/ppp/{{ version }}/{{ lang }}/extensions/budget/? https://extensions.open-contracting.org/{{ lang }}/extensions/budget/ -RedirectMatch /profiles/ppp/{{ version }}/{{ lang }}/extensions/budget_project/? https://extensions.open-contracting.org/{{ lang }}/extensions/budget_project/ -RedirectMatch /profiles/ppp/{{ version }}/{{ lang }}/extensions/charges/? https://extensions.open-contracting.org/{{ lang }}/extensions/charges/ -RedirectMatch /profiles/ppp/{{ version }}/{{ lang }}/extensions/documentation_details/? https://extensions.open-contracting.org/{{ lang }}/extensions/documentation_details/ -RedirectMatch /profiles/ppp/{{ version }}/{{ lang }}/extensions/finance/? https://extensions.open-contracting.org/{{ lang }}/extensions/finance/ -RedirectMatch /profiles/ppp/{{ version }}/{{ lang }}/extensions/location/? https://extensions.open-contracting.org/{{ lang }}/extensions/location/ -RedirectMatch /profiles/ppp/{{ version }}/{{ lang }}/extensions/metrics/? https://extensions.open-contracting.org/{{ lang }}/extensions/metrics/ -RedirectMatch /profiles/ppp/{{ version }}/{{ lang }}/extensions/performance_failures/? https://extensions.open-contracting.org/{{ lang }}/extensions/performance_failures/ -RedirectMatch /profiles/ppp/{{ version }}/{{ lang }}/extensions/ppp/? https://extensions.open-contracting.org/{{ lang }}/extensions/ppp/ -RedirectMatch /profiles/ppp/{{ version }}/{{ lang }}/extensions/process_title/? https://extensions.open-contracting.org/{{ lang }}/extensions/process_title/ -RedirectMatch /profiles/ppp/{{ version }}/{{ lang }}/extensions/qualification/? https://extensions.open-contracting.org/{{ lang }}/extensions/qualification/ -RedirectMatch /profiles/ppp/{{ version }}/{{ lang }}/extensions/requirements/? https://extensions.open-contracting.org/{{ lang }}/extensions/requirements/ -RedirectMatch /profiles/ppp/{{ version }}/{{ lang }}/extensions/risk_allocation/? https://extensions.open-contracting.org/{{ lang }}/extensions/risk_allocation/ -RedirectMatch /profiles/ppp/{{ version }}/{{ lang }}/extensions/shareholders/? https://extensions.open-contracting.org/{{ lang }}/extensions/shareholders/ -RedirectMatch /profiles/ppp/{{ version }}/{{ lang }}/extensions/signatories/? https://extensions.open-contracting.org/{{ lang }}/extensions/signatories/ -RedirectMatch /profiles/ppp/{{ version }}/{{ lang }}/extensions/tariffs/? https://extensions.open-contracting.org/{{ lang }}/extensions/tariffs/ -RedirectMatch /profiles/ppp/{{ version }}/{{ lang }}/extensions/transaction_milestones/? https://extensions.open-contracting.org/{{ lang }}/extensions/transaction_milestones/ +RedirectMatch /profiles/ppp/{{ version }}/{{ lang }}/extensions/bids/?$ https://extensions.open-contracting.org/{{ lang }}/extensions/bids/ +RedirectMatch /profiles/ppp/{{ version }}/{{ lang }}/extensions/budget/?$ https://extensions.open-contracting.org/{{ lang }}/extensions/budget/ +RedirectMatch /profiles/ppp/{{ version }}/{{ lang }}/extensions/budget_project/?$ https://extensions.open-contracting.org/{{ lang }}/extensions/budget_project/ +RedirectMatch /profiles/ppp/{{ version }}/{{ lang }}/extensions/charges/?$ https://extensions.open-contracting.org/{{ lang }}/extensions/charges/ +RedirectMatch /profiles/ppp/{{ version }}/{{ lang }}/extensions/documentation_details/?$ https://extensions.open-contracting.org/{{ lang }}/extensions/documentation_details/ +RedirectMatch /profiles/ppp/{{ version }}/{{ lang }}/extensions/finance/?$ https://extensions.open-contracting.org/{{ lang }}/extensions/finance/ +RedirectMatch /profiles/ppp/{{ version }}/{{ lang }}/extensions/location/?$ https://extensions.open-contracting.org/{{ lang }}/extensions/location/ +RedirectMatch /profiles/ppp/{{ version }}/{{ lang }}/extensions/metrics/?$ https://extensions.open-contracting.org/{{ lang }}/extensions/metrics/ +RedirectMatch /profiles/ppp/{{ version }}/{{ lang }}/extensions/performance_failures/?$ https://extensions.open-contracting.org/{{ lang }}/extensions/performance_failures/ +RedirectMatch /profiles/ppp/{{ version }}/{{ lang }}/extensions/ppp/?$ https://extensions.open-contracting.org/{{ lang }}/extensions/ppp/ +RedirectMatch /profiles/ppp/{{ version }}/{{ lang }}/extensions/process_title/?$ https://extensions.open-contracting.org/{{ lang }}/extensions/process_title/ +RedirectMatch /profiles/ppp/{{ version }}/{{ lang }}/extensions/qualification/?$ https://extensions.open-contracting.org/{{ lang }}/extensions/qualification/ +RedirectMatch /profiles/ppp/{{ version }}/{{ lang }}/extensions/requirements/?$ https://extensions.open-contracting.org/{{ lang }}/extensions/requirements/ +RedirectMatch /profiles/ppp/{{ version }}/{{ lang }}/extensions/risk_allocation/?$ https://extensions.open-contracting.org/{{ lang }}/extensions/risk_allocation/ +RedirectMatch /profiles/ppp/{{ version }}/{{ lang }}/extensions/shareholders/?$ https://extensions.open-contracting.org/{{ lang }}/extensions/shareholders/ +RedirectMatch /profiles/ppp/{{ version }}/{{ lang }}/extensions/signatories/?$ https://extensions.open-contracting.org/{{ lang }}/extensions/signatories/ +RedirectMatch /profiles/ppp/{{ version }}/{{ lang }}/extensions/tariffs/?$ https://extensions.open-contracting.org/{{ lang }}/extensions/tariffs/ +RedirectMatch /profiles/ppp/{{ version }}/{{ lang }}/extensions/transaction_milestones/?$ https://extensions.open-contracting.org/{{ lang }}/extensions/transaction_milestones/ {%- endfor %} {%- endfor %} From b4e0dd3087b9cb19247c4cdb1deb6cf33e6c179d Mon Sep 17 00:00:00 2001 From: James McKinney <26463+jpmckinney@users.noreply.github.com> Date: Mon, 21 Sep 2026 12:26:59 -0400 Subject: [PATCH 2/2] feat(cloudflare): Add a command to print firewall events The Free plan keeps 24 hours of Security Events and samples the dashboard's logs, so a longer window has to be collected: this prints JSON lines, one object per event, across every zone, to redirect to a file from cron. Hours beyond 24 return nothing more. It warns rather than paginates when a zone reaches the limit. 10,000 is accepted, and a day's events are about half that. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0148VmvbJ2LcshwztmwuKRoU --- manage.py | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/manage.py b/manage.py index 166d3e726..563757711 100755 --- a/manage.py +++ b/manage.py @@ -10,6 +10,7 @@ import subprocess import sys from collections import defaultdict +from datetime import UTC, datetime, timedelta from email.parser import Parser from email.policy import default from itertools import islice @@ -294,6 +295,57 @@ def zones(api_token, defaults): print_resources(resources, disabled=disabled, default=default) +@cloudflare.command() +@api_token_option +@click.option("--hours", type=int, default=24, help="Hours of events to query") +@click.option("--limit", type=int, default=10000, help="Maximum events per zone") +def events(api_token, hours, limit): + """Print firewall events, one JSON object per line.""" + since = (datetime.now(tz=UTC) - timedelta(hours=hours)).strftime("%Y-%m-%dT%H:%M:%SZ") + fields = ( + "datetime action source ruleId edgeResponseStatus userAgent " + "clientIP clientAsn clientCountryName clientRequestHTTPHost clientRequestHTTPMethodName clientRequestPath" + ) + + for zone in Cloudflare(api_token=api_token).zones.list(): + # https://developers.cloudflare.com/waf/analytics/security-events/#query-using-graphql + query = f"""{{ + viewer {{ + zones(filter: {{zoneTag: "{zone.id}"}}) {{ + firewallEventsAdaptive( + filter: {{datetime_gt: "{since}"}} + limit: {limit} + orderBy: [datetime_DESC] + ) {{ + {fields} + }} + }} + }} + }}""" + + response = requests.post( + "https://api.cloudflare.com/client/v4/graphql", + headers={"Authorization": f"Bearer {api_token}"}, + json={"query": query}, + timeout=10, + ) + response.raise_for_status() + data = response.json() + + # "A 200 response can contain an error." + # https://developers.cloudflare.com/analytics/graphql-api/errors/ + if errors := data.get("errors"): + for error in errors: + click.secho(f"{zone.name}: {error['message']}", fg="red", err=True) + continue + + for result in data["data"]["viewer"]["zones"]: + if len(result["firewallEventsAdaptive"]) == limit: + click.secho(f"{zone.name}: {limit} limit reached (requires pagination code)", fg="yellow", err=True) + for event in result["firewallEventsAdaptive"]: + click.echo(json.dumps({"zone": zone.name, **event})) + + @cloudflare.command() @api_token_option @account_id_option