queryapigate 0.31.0__tar.gz → 0.32.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {queryapigate-0.31.0/queryapigate.egg-info → queryapigate-0.32.0}/PKG-INFO +3 -3
- {queryapigate-0.31.0 → queryapigate-0.32.0}/README.md +2 -2
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/__init__.py +1 -1
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/auditchain.py +49 -10
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/auditstream.py +2 -1
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/cli.py +8 -1
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/experimental.py +2 -10
- {queryapigate-0.31.0 → queryapigate-0.32.0/queryapigate.egg-info}/PKG-INFO +3 -3
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate.egg-info/SOURCES.txt +1 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_audit_chain.py +64 -0
- queryapigate-0.32.0/tests/test_audit_siem.py +246 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_experimental.py +12 -8
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_supply_chain.py +9 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/LICENSE +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/pyproject.toml +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/__main__.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/accesslog.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/adminroles.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/admins.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/agentwrites.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/alerts.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/apikeys.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/app.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/aws.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/broadcast.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/bundle.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/cache.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/collection_admin.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/config.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/configcode.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/console.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/console_dist/assets/AccessMapPage-CFAKvNHS.js +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/console_dist/assets/AdministratorsPage-O9o_8T8Q.js +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/console_dist/assets/AgentsPage-CJL7R9ML.js +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/console_dist/assets/AlertsPage-Rhnx0WE5.js +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/console_dist/assets/ApiKeysPage-LSfT7OFf.js +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/console_dist/assets/ApprovalsPage-CxyRU_om.js +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/console_dist/assets/AuditLogPage-CfozUklt.js +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/console_dist/assets/CachingPage-8TY7yUvl.js +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/console_dist/assets/CodeBox-B8iq2Oop.js +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/console_dist/assets/ConnectionsPage-CrgOBe8i.js +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/console_dist/assets/DesignerPage-C7m2WgjH.js +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/console_dist/assets/DestinationsPage-hjd4ejdt.js +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/console_dist/assets/ExportsPage-D70vuQNl.js +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/console_dist/assets/HelpPage-C7vakDSS.js +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/console_dist/assets/HomePage-BuExjOpj.js +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/console_dist/assets/Managed-DjB6h9Kc.js +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/console_dist/assets/MetricsPage-BAQxP5-4.js +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/console_dist/assets/RepositoryPage-8CBmviv-.js +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/console_dist/assets/Results-DofTMPkj.js +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/console_dist/assets/RolesPage-CH_Dr1DP.js +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/console_dist/assets/SchemaBrowser-D57iE2Kg.js +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/console_dist/assets/SettingsPage-CHmKfYHu.js +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/console_dist/assets/StatTile-BBd2i5mC.js +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/console_dist/assets/Time-CR17Ozn9.js +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/console_dist/assets/forms-C3dw2wG6.js +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/console_dist/assets/forms-DR5CXITG.js +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/console_dist/assets/icons-C-i_90Gs.js +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/console_dist/assets/index-BLd0t6vZ.css +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/console_dist/assets/index-BNMV1syF.js +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/console_dist/assets/maskRules-DyD2pxp7.js +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/console_dist/assets/reach-0Yvn3NAt.js +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/console_dist/assets/shared-BebZOZXs.js +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/console_dist/assets/state-O13cl7cO.js +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/console_dist/assets/useMutation-DCzNFnov.js +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/console_dist/favicon.svg +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/console_dist/index.html +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/cors.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/databases.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/db.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/definitions.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/deprecations.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/descriptions.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/destinations.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/duckfiles.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/engine.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/errors.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/events.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/examples.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/exporting.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/exports.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/formats.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/governance.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/gunicorn_conf.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/history.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/instances.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/jwtauth.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/lib/h2-2.2.224.jar +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/limits.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/logging_setup.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/masking.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/mcp_server.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/metrics.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/mongotools.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/oidc.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/openapi.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/params.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/parquet.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/pool.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/postman.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/quotas.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/ratelimit.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/rediscache.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/rekey.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/runners.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/schema.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/secheaders.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/secretrefs.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/services/__init__.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/services/access.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/services/administrators.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/services/audit.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/services/collections.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/services/connections.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/services/descriptions.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/services/destinations.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/services/exports.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/services/mcp.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/services/queries.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/sqlflow.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/sqltools.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/sso.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/store.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/tableguard.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/tracing.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/usage.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/v1.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/v1_spec.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/vault.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate.egg-info/dependency_links.txt +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate.egg-info/entry_points.txt +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate.egg-info/requires.txt +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate.egg-info/top_level.txt +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/setup.cfg +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_adhoc_history.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_adminroles.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_admins.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_agent_writes.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_alerts.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_api_administrators.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_api_keys.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_api_v1.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_audit_log.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_audit_stream.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_aws.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_backup_restore.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_broadcast.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_cli.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_collection_bundle.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_collection_postman.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_collections.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_configcode.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_connection_encryption.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_console.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_cors_and_rate_limit.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_data_protection.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_databases.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_db.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_dependency_floors.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_deprecations.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_descriptions.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_destinations.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_docs_site.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_duckdb.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_duckdb_files.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_errors.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_events.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_events_server.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_examples.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_export_security.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_exporting.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_exports.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_fips.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_helm.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_history.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_howto.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_instances.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_integration.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_jdbc.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_jwt.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_key_rotation.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_limits.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_masking.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_masking_requests.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_mcp.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_mcp_keycloak.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_mcp_oauth.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_mongotools.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_multinode.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_observability.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_oidc.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_params.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_parquet.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_pool.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_publishing.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_query_window.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_queryapigate.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_quotas.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_rekey.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_release_check.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_response_cache.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_roles.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_rolling_upgrade.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_secretrefs.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_security_headers.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_settings.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_shared_rate_limit.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_sql_guard_fuzz.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_sql_guard_writes.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_sqlflow.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_sso.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_sso_keycloak.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_tableguard.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_tracing.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_upgrades.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_usage.py +0 -0
- {queryapigate-0.31.0 → queryapigate-0.32.0}/tests/test_vault.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: queryapigate
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.32.0
|
|
4
4
|
Summary: Turn SQL into governed REST APIs over MySQL, PostgreSQL, ClickHouse, SQLite, DuckDB, H2, MongoDB, and Parquet/CSV/JSON files - JSON, CSV, Parquet or Excel out, with scoped keys, an admin Console and MCP for AI agents.
|
|
5
5
|
Author-email: Anantha Raju C <arcswdev@gmail.com>
|
|
6
6
|
License-Expression: FSL-1.1-MIT
|
|
@@ -667,8 +667,8 @@ Everything is configured through environment variables (all optional):
|
|
|
667
667
|
| `QUERYAPIGATE_CORS_ORIGINS` | unset | Websites allowed to call the API from a browser: comma-separated origins such as `https://app.example.com`, or `*`. Off by default. |
|
|
668
668
|
| `QUERYAPIGATE_RATE_LIMIT` | unset | Requests allowed per client address, e.g. `60/minute` (also `second`, `hour`, `day`). Off by default; a malformed value stops startup. |
|
|
669
669
|
| `QUERYAPIGATE_TRUST_PROXY` | `0` | Number of reverse proxies in front of the app whose `X-Forwarded-*` headers are trusted. Set it (usually `1`) behind nginx, a load balancer or a platform router, or every client looks like the proxy. |
|
|
670
|
-
| `QUERYAPIGATE_AUDIT_SYSLOG_URL` | *(unset)* | Stream every audit entry to a SIEM over syslog: `tls://host:6514` or `tcp://host:514
|
|
671
|
-
| `QUERYAPIGATE_AUDIT_OTLP` | `false` | Stream every audit entry as OTLP log records to the OTLP logs endpoint
|
|
670
|
+
| `QUERYAPIGATE_AUDIT_SYSLOG_URL` | *(unset)* | Stream every audit entry to a SIEM over syslog: `tls://host:6514` or `tcp://host:514`. With `QUERYAPIGATE_AUDIT_SYSLOG_CA`, and `_CERT`/`_KEY` for mutual TLS. |
|
|
671
|
+
| `QUERYAPIGATE_AUDIT_OTLP` | `false` | Stream every audit entry as OTLP log records to the OTLP logs endpoint. |
|
|
672
672
|
| `QUERYAPIGATE_AUDIT_KEY` | *(unset)* | HMAC key (32+ characters) the audit log's hash chain is signed with, so an edit made with only the store can't be re-signed. Several, comma-separated, while rotating it. Unset: plain SHA-256. |
|
|
673
673
|
| `QUERYAPIGATE_HSTS_MAX_AGE` | *(unset)* | Seconds browsers should use only HTTPS for this host (`Strict-Transport-Security`), sent only on requests that arrived over HTTPS. Unset: not sent - set it once every client reaches the server over HTTPS. |
|
|
674
674
|
| `QUERYAPIGATE_POOL_SIZE` | `5` | Idle connections kept per distinct connection setting. `0` turns pooling off. |
|
|
@@ -589,8 +589,8 @@ Everything is configured through environment variables (all optional):
|
|
|
589
589
|
| `QUERYAPIGATE_CORS_ORIGINS` | unset | Websites allowed to call the API from a browser: comma-separated origins such as `https://app.example.com`, or `*`. Off by default. |
|
|
590
590
|
| `QUERYAPIGATE_RATE_LIMIT` | unset | Requests allowed per client address, e.g. `60/minute` (also `second`, `hour`, `day`). Off by default; a malformed value stops startup. |
|
|
591
591
|
| `QUERYAPIGATE_TRUST_PROXY` | `0` | Number of reverse proxies in front of the app whose `X-Forwarded-*` headers are trusted. Set it (usually `1`) behind nginx, a load balancer or a platform router, or every client looks like the proxy. |
|
|
592
|
-
| `QUERYAPIGATE_AUDIT_SYSLOG_URL` | *(unset)* | Stream every audit entry to a SIEM over syslog: `tls://host:6514` or `tcp://host:514
|
|
593
|
-
| `QUERYAPIGATE_AUDIT_OTLP` | `false` | Stream every audit entry as OTLP log records to the OTLP logs endpoint
|
|
592
|
+
| `QUERYAPIGATE_AUDIT_SYSLOG_URL` | *(unset)* | Stream every audit entry to a SIEM over syslog: `tls://host:6514` or `tcp://host:514`. With `QUERYAPIGATE_AUDIT_SYSLOG_CA`, and `_CERT`/`_KEY` for mutual TLS. |
|
|
593
|
+
| `QUERYAPIGATE_AUDIT_OTLP` | `false` | Stream every audit entry as OTLP log records to the OTLP logs endpoint. |
|
|
594
594
|
| `QUERYAPIGATE_AUDIT_KEY` | *(unset)* | HMAC key (32+ characters) the audit log's hash chain is signed with, so an edit made with only the store can't be re-signed. Several, comma-separated, while rotating it. Unset: plain SHA-256. |
|
|
595
595
|
| `QUERYAPIGATE_HSTS_MAX_AGE` | *(unset)* | Seconds browsers should use only HTTPS for this host (`Strict-Transport-Security`), sent only on requests that arrived over HTTPS. Unset: not sent - set it once every client reaches the server over HTTPS. |
|
|
596
596
|
| `QUERYAPIGATE_POOL_SIZE` | `5` | Idle connections kept per distinct connection setting. `0` turns pooling off. |
|
|
@@ -165,25 +165,64 @@ def verify(conn):
|
|
|
165
165
|
return report
|
|
166
166
|
|
|
167
167
|
|
|
168
|
+
def _entry_in(line):
|
|
169
|
+
"""The JSON object a line holds: the whole line (an export file), or the message at its end - a syslog line, as
|
|
170
|
+
rsyslog or syslog-ng write what the stream sent (`<110>1 ... [meta sequenceId="7"] {...}`). None if it has none."""
|
|
171
|
+
try:
|
|
172
|
+
data = json.loads(line)
|
|
173
|
+
return data if isinstance(data, dict) else None
|
|
174
|
+
except ValueError:
|
|
175
|
+
pass
|
|
176
|
+
decoder = json.JSONDecoder()
|
|
177
|
+
start = line.find('{')
|
|
178
|
+
while start != -1:
|
|
179
|
+
try:
|
|
180
|
+
data, end = decoder.raw_decode(line, start)
|
|
181
|
+
except ValueError:
|
|
182
|
+
start = line.find('{', start + 1)
|
|
183
|
+
continue
|
|
184
|
+
if isinstance(data, dict) and not line[end:].strip():
|
|
185
|
+
return data
|
|
186
|
+
start = line.find('{', start + 1)
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
|
|
168
190
|
def verify_file(path):
|
|
169
|
-
"""
|
|
170
|
-
|
|
171
|
-
|
|
191
|
+
"""A copy of the chain, one entry per line - an export file (QUERYAPIGATE_AUDIT_LOG_EXPORT_FILE), or what a SIEM
|
|
192
|
+
received (ADR 0019): JSON lines, or syslog lines whose message is the entry. Walked in `seq` order from its first
|
|
193
|
+
entry, whose `prev_hash` is taken as given (the copy may have started after the chain did).
|
|
194
|
+
|
|
195
|
+
Delivery is at least once, so a SIEM's copy may hold an entry twice: an exact repeat - the same `seq` and `hash` -
|
|
196
|
+
is dropped, and counted (`repeats`). A repeat whose `hash` differs is never dropped: `conflicting_copies`, naming
|
|
197
|
+
both - one of them isn't what was written."""
|
|
198
|
+
by_seq = {}
|
|
199
|
+
repeats = 0
|
|
172
200
|
with open(path) as f:
|
|
173
201
|
for number, line in enumerate(f, 1):
|
|
174
202
|
if not line.strip():
|
|
175
203
|
continue
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
except ValueError:
|
|
204
|
+
data = _entry_in(line)
|
|
205
|
+
if data is None:
|
|
179
206
|
return {'intact': False, 'break': {'line': number, 'problem': 'not_json'}}
|
|
180
207
|
if 'seq' not in data:
|
|
181
208
|
continue # written before the chain existed
|
|
182
|
-
|
|
183
|
-
|
|
209
|
+
seq, prev_hash, digest = data.pop('seq'), data.pop('prev_hash', None), data.pop('hash', None)
|
|
210
|
+
seen = by_seq.get(seq)
|
|
211
|
+
if seen is None:
|
|
212
|
+
by_seq[seq] = (seq, prev_hash, digest, data, number)
|
|
213
|
+
continue
|
|
214
|
+
if (seen[1], seen[2], seen[3]) == (prev_hash, digest, data):
|
|
215
|
+
repeats += 1
|
|
216
|
+
continue
|
|
217
|
+
return {'intact': False, 'break': {'seq': seq, 'timestamp': data.get('timestamp'),
|
|
218
|
+
'action': data.get('action'), 'problem': 'conflicting_copies',
|
|
219
|
+
'lines': [seen[4], number]}}
|
|
220
|
+
links = [link[:4] for _seq, link in sorted(by_seq.items())]
|
|
184
221
|
if not links:
|
|
185
|
-
return {'intact': True, 'entries': 0}
|
|
186
|
-
|
|
222
|
+
return {'intact': True, 'entries': 0, 'repeats': repeats}
|
|
223
|
+
report = _walk(links, (links[0][0] - 1, links[0][1]))
|
|
224
|
+
report['repeats'] = repeats
|
|
225
|
+
return report
|
|
187
226
|
|
|
188
227
|
|
|
189
228
|
def head_keyed(conn):
|
|
@@ -716,12 +716,17 @@ def _audit_verify(args):
|
|
|
716
716
|
how = ('keyed with QUERYAPIGATE_AUDIT_KEY' if report.get('keyed')
|
|
717
717
|
else 'unkeyed (plain SHA-256: set QUERYAPIGATE_AUDIT_KEY so an edit cannot be re-signed)')
|
|
718
718
|
print(f"Intact: entries {report['first_seq']} to {report['last_seq']}, {how}.")
|
|
719
|
+
if report.get('repeats'):
|
|
720
|
+
print(f" {report['repeats']} repeated entries dropped - the same seq and hash, as at-least-once delivery "
|
|
721
|
+
'can send them')
|
|
719
722
|
if report.get('unchecked_without_key'):
|
|
720
723
|
print(f" {report['unchecked_without_key']} keyed entries not checked - QUERYAPIGATE_AUDIT_KEY is not set")
|
|
721
724
|
return 0
|
|
722
725
|
found = report['break']
|
|
723
726
|
where = f"line {found['line']}" if 'line' in found else (
|
|
724
727
|
f"entry {found['seq']} ({found.get('action')}, {found.get('timestamp')})")
|
|
728
|
+
if found['problem'] == 'conflicting_copies':
|
|
729
|
+
where += f" - lines {found['lines'][0]} and {found['lines'][1]} differ"
|
|
725
730
|
print(f"BROKEN at {where}: {found['problem'].replace('_', ' ')}", file=sys.stderr)
|
|
726
731
|
return 1
|
|
727
732
|
|
|
@@ -915,7 +920,9 @@ def build_parser():
|
|
|
915
920
|
audit_actions = audit_parser.add_subparsers(dest='action')
|
|
916
921
|
audit_verify = audit_actions.add_parser('verify', help='walk the chain and say whether it is intact, or where it '
|
|
917
922
|
'first breaks (exit 0 or 1)')
|
|
918
|
-
audit_verify.add_argument('--file', help='
|
|
923
|
+
audit_verify.add_argument('--file', help='a copy instead of the store: an export file '
|
|
924
|
+
'(QUERYAPIGATE_AUDIT_LOG_EXPORT_FILE), or what a SIEM received - JSON or '
|
|
925
|
+
'syslog lines, repeats allowed')
|
|
919
926
|
audit_verify.set_defaults(func=_audit_verify)
|
|
920
927
|
|
|
921
928
|
history_parser = commands.add_parser('history', help='run history: forget a person (ADR 0016)')
|
|
@@ -9,8 +9,8 @@ docs say so at each feature's own section.
|
|
|
9
9
|
A feature graduates - its entry deleted here, its callout replaced by a note that it's stable and what may still be
|
|
10
10
|
added - when it meets every one of GRADUATION. Alerts, remote files through DuckDB, exports and column masking did in
|
|
11
11
|
0.20; descriptions, agent usage, usage quotas, configuration as code, OAuth for the MCP server, write tools and
|
|
12
|
-
Console sign-in through SSO in 0.30 (ADR 0017), with the Helm chart
|
|
13
|
-
(databases.GRADUATION).
|
|
12
|
+
Console sign-in through SSO in 0.30 (ADR 0017), with the Helm chart; the audit log streamed to a SIEM in 0.32, once
|
|
13
|
+
tested against real receivers (ADR 0019). The connection types have their own terms (databases.GRADUATION).
|
|
14
14
|
"""
|
|
15
15
|
from typing import Any
|
|
16
16
|
|
|
@@ -41,14 +41,6 @@ FEATURES: dict[str, dict[str, Any]] = {
|
|
|
41
41
|
'db_types': ('h2', 'jdbc', 'mongo'),
|
|
42
42
|
'docs': ('documentation/DATABASE_CONNECTION_CONFIGURATION.md', 'Experimental: h2, jdbc and mongo'),
|
|
43
43
|
},
|
|
44
|
-
'audit_stream': {
|
|
45
|
-
'name': 'The audit log streamed to a SIEM',
|
|
46
|
-
'why': 'new in 0.25 - its settings and message format may change as it meets real SIEMs (ADR 0012)',
|
|
47
|
-
'settings': ('QUERYAPIGATE_AUDIT_SYSLOG_URL', 'QUERYAPIGATE_AUDIT_SYSLOG_CA', 'QUERYAPIGATE_AUDIT_SYSLOG_CERT',
|
|
48
|
-
'QUERYAPIGATE_AUDIT_SYSLOG_KEY', 'QUERYAPIGATE_AUDIT_OTLP'),
|
|
49
|
-
'operations': (('get', '/api/v1/audit/delivery'),),
|
|
50
|
-
'docs': ('documentation/API.md', 'Streaming the audit log to a SIEM'),
|
|
51
|
-
},
|
|
52
44
|
'tracing': {
|
|
53
45
|
'name': 'Tracing with OpenTelemetry',
|
|
54
46
|
'why': 'new in 0.23 - span names and attributes may change as the OpenTelemetry conventions for databases and '
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: queryapigate
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.32.0
|
|
4
4
|
Summary: Turn SQL into governed REST APIs over MySQL, PostgreSQL, ClickHouse, SQLite, DuckDB, H2, MongoDB, and Parquet/CSV/JSON files - JSON, CSV, Parquet or Excel out, with scoped keys, an admin Console and MCP for AI agents.
|
|
5
5
|
Author-email: Anantha Raju C <arcswdev@gmail.com>
|
|
6
6
|
License-Expression: FSL-1.1-MIT
|
|
@@ -667,8 +667,8 @@ Everything is configured through environment variables (all optional):
|
|
|
667
667
|
| `QUERYAPIGATE_CORS_ORIGINS` | unset | Websites allowed to call the API from a browser: comma-separated origins such as `https://app.example.com`, or `*`. Off by default. |
|
|
668
668
|
| `QUERYAPIGATE_RATE_LIMIT` | unset | Requests allowed per client address, e.g. `60/minute` (also `second`, `hour`, `day`). Off by default; a malformed value stops startup. |
|
|
669
669
|
| `QUERYAPIGATE_TRUST_PROXY` | `0` | Number of reverse proxies in front of the app whose `X-Forwarded-*` headers are trusted. Set it (usually `1`) behind nginx, a load balancer or a platform router, or every client looks like the proxy. |
|
|
670
|
-
| `QUERYAPIGATE_AUDIT_SYSLOG_URL` | *(unset)* | Stream every audit entry to a SIEM over syslog: `tls://host:6514` or `tcp://host:514
|
|
671
|
-
| `QUERYAPIGATE_AUDIT_OTLP` | `false` | Stream every audit entry as OTLP log records to the OTLP logs endpoint
|
|
670
|
+
| `QUERYAPIGATE_AUDIT_SYSLOG_URL` | *(unset)* | Stream every audit entry to a SIEM over syslog: `tls://host:6514` or `tcp://host:514`. With `QUERYAPIGATE_AUDIT_SYSLOG_CA`, and `_CERT`/`_KEY` for mutual TLS. |
|
|
671
|
+
| `QUERYAPIGATE_AUDIT_OTLP` | `false` | Stream every audit entry as OTLP log records to the OTLP logs endpoint. |
|
|
672
672
|
| `QUERYAPIGATE_AUDIT_KEY` | *(unset)* | HMAC key (32+ characters) the audit log's hash chain is signed with, so an edit made with only the store can't be re-signed. Several, comma-separated, while rotating it. Unset: plain SHA-256. |
|
|
673
673
|
| `QUERYAPIGATE_HSTS_MAX_AGE` | *(unset)* | Seconds browsers should use only HTTPS for this host (`Strict-Transport-Security`), sent only on requests that arrived over HTTPS. Unset: not sent - set it once every client reaches the server over HTTPS. |
|
|
674
674
|
| `QUERYAPIGATE_POOL_SIZE` | `5` | Idle connections kept per distinct connection setting. `0` turns pooling off. |
|
|
@@ -233,6 +233,70 @@ class ExportFileTests(ChainTestCase):
|
|
|
233
233
|
self.assertEqual(auditchain.verify_file(path)['break']['problem'], 'changed')
|
|
234
234
|
|
|
235
235
|
|
|
236
|
+
class SiemCopyTests(ChainTestCase):
|
|
237
|
+
"""What a SIEM received (ADR 0019): delivery is at least once, so its copy may hold an entry twice, and after a
|
|
238
|
+
retry not in order - an untouched copy still verifies; a copy someone edited still doesn't."""
|
|
239
|
+
|
|
240
|
+
def copy(self, lines, name='siem.log'):
|
|
241
|
+
path = os.path.join(self.tmp.name, name)
|
|
242
|
+
with open(path, 'w') as f:
|
|
243
|
+
f.writelines(line + '\n' for line in lines)
|
|
244
|
+
return path
|
|
245
|
+
|
|
246
|
+
def entries(self, n=5):
|
|
247
|
+
path = os.path.join(self.tmp.name, 'audit.jsonl')
|
|
248
|
+
with mock.patch.dict(os.environ, {'QUERYAPIGATE_AUDIT_LOG_EXPORT_FILE': path}):
|
|
249
|
+
self.write(n)
|
|
250
|
+
return [line.rstrip('\n') for line in open(path)]
|
|
251
|
+
|
|
252
|
+
@staticmethod
|
|
253
|
+
def as_syslog(line):
|
|
254
|
+
entry = json.loads(line)
|
|
255
|
+
return (f'2026-10-13T10:00:00+00:00 api-1 queryapigate[42]: <110>1 2026-10-13T10:00:00Z api-1 queryapigate 42 '
|
|
256
|
+
f'{entry["action"]} [meta sequenceId="{entry["seq"]}"] {line}')
|
|
257
|
+
|
|
258
|
+
def test_exact_repeats_are_dropped_and_counted(self):
|
|
259
|
+
lines = self.entries()
|
|
260
|
+
report = auditchain.verify_file(self.copy(lines[:3] + lines[1:3] + lines[3:])) # 2 and 3 sent again
|
|
261
|
+
self.assertTrue(report['intact'], report)
|
|
262
|
+
self.assertEqual((report['first_seq'], report['last_seq'], report['repeats']), (1, 5, 2))
|
|
263
|
+
|
|
264
|
+
def test_entries_are_walked_in_seq_order_whatever_order_they_arrived_in(self):
|
|
265
|
+
lines = self.entries()
|
|
266
|
+
self.assertTrue(auditchain.verify_file(self.copy([lines[0], lines[3], lines[1], lines[2], lines[4]]))['intact'])
|
|
267
|
+
|
|
268
|
+
def test_a_repeat_that_differs_is_never_dropped(self):
|
|
269
|
+
lines = self.entries()
|
|
270
|
+
forged = json.loads(lines[2])
|
|
271
|
+
forged['target'] = 'forged'
|
|
272
|
+
report = auditchain.verify_file(self.copy([*lines, json.dumps(forged)]))
|
|
273
|
+
self.assertFalse(report['intact'])
|
|
274
|
+
self.assertEqual(report['break']['problem'], 'conflicting_copies')
|
|
275
|
+
self.assertEqual((report['break']['seq'], report['break']['lines']), (3, [3, 6]))
|
|
276
|
+
|
|
277
|
+
def test_syslog_lines_as_a_receiver_writes_them(self):
|
|
278
|
+
lines = [self.as_syslog(line) for line in self.entries()]
|
|
279
|
+
self.assertTrue(auditchain.verify_file(self.copy(lines + lines[:1]))['intact'])
|
|
280
|
+
edited = lines[1].replace('"seq": 2', '"seq": 2, "note": "added"')
|
|
281
|
+
self.assertNotEqual(edited, lines[1])
|
|
282
|
+
report = auditchain.verify_file(self.copy([lines[0], edited, *lines[2:]]))
|
|
283
|
+
self.assertEqual(report['break']['problem'], 'changed')
|
|
284
|
+
|
|
285
|
+
def test_a_missing_entry_is_still_missing(self):
|
|
286
|
+
lines = self.entries()
|
|
287
|
+
report = auditchain.verify_file(self.copy(lines[:2] + lines[3:]))
|
|
288
|
+
self.assertEqual((report['break']['problem'], report['break']['seq']), ('missing', 4))
|
|
289
|
+
|
|
290
|
+
def test_the_cli_says_how_many_repeats_it_dropped(self):
|
|
291
|
+
lines = self.entries(3)
|
|
292
|
+
path = self.copy(lines + lines[1:2])
|
|
293
|
+
out, err = io.StringIO(), io.StringIO()
|
|
294
|
+
with contextlib.redirect_stdout(out), contextlib.redirect_stderr(err):
|
|
295
|
+
self.assertEqual(cli.main(['audit', 'verify', '--file', path]), 0)
|
|
296
|
+
self.assertIn('Intact: entries 1 to 3', out.getvalue())
|
|
297
|
+
self.assertIn('1 repeated entries dropped', out.getvalue())
|
|
298
|
+
|
|
299
|
+
|
|
236
300
|
class SurfaceTests(ChainTestCase):
|
|
237
301
|
def run_cli(self, *argv):
|
|
238
302
|
out, err = io.StringIO(), io.StringIO()
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"""The audit log's stream against real receivers (ADR 0019): rsyslog and syslog-ng taking RFC 5425 syslog over mutual
|
|
2
|
+
TLS, and an OpenTelemetry Collector taking OTLP/HTTP logs - started by .github/scripts/siem_receivers.sh, which
|
|
3
|
+
QUERYAPIGATE_TEST_SIEM names the directory of. Each test reads what the receiver itself parsed and wrote, never our own
|
|
4
|
+
parse: MSGID, structured data, the message - and checks that copy with `audit verify --file`, as an auditor would.
|
|
5
|
+
|
|
6
|
+
CI's integration job runs these with every receiver up, and fails if any test skips."""
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import subprocess
|
|
10
|
+
import tempfile
|
|
11
|
+
import time
|
|
12
|
+
import unittest
|
|
13
|
+
import uuid
|
|
14
|
+
from unittest import mock
|
|
15
|
+
|
|
16
|
+
from queryapigate import auditchain, auditstream, db, store
|
|
17
|
+
|
|
18
|
+
SIEM = os.environ.get('QUERYAPIGATE_TEST_SIEM')
|
|
19
|
+
RECEIVERS = json.load(open(os.path.join(SIEM, 'receivers.json'))) if SIEM else {}
|
|
20
|
+
SKIP = 'set QUERYAPIGATE_TEST_SIEM to the directory .github/scripts/siem_receivers.sh started'
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _docker(*args):
|
|
24
|
+
subprocess.run(['docker', *args], check=True, capture_output=True, timeout=120)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class SiemTestCase(unittest.TestCase):
|
|
28
|
+
receiver = None # 'rsyslog', 'syslog-ng' or 'otelcol'
|
|
29
|
+
|
|
30
|
+
def setUp(self):
|
|
31
|
+
self.tmp = tempfile.TemporaryDirectory()
|
|
32
|
+
self.addCleanup(self.tmp.cleanup)
|
|
33
|
+
self.marker = uuid.uuid4().hex[:12] # every receiver's file is shared between tests: ours are marked
|
|
34
|
+
certs = RECEIVERS['certs']
|
|
35
|
+
env = {'QUERYAPIGATE_HOME': self.tmp.name, 'QUERYAPIGATE_AUDIT_KEY': '', 'QUERYAPIGATE_AUDIT_LOG_LIMIT': '500',
|
|
36
|
+
'QUERYAPIGATE_AUDIT_SYSLOG_URL': '', 'QUERYAPIGATE_AUDIT_OTLP': '',
|
|
37
|
+
'QUERYAPIGATE_AUDIT_SYSLOG_CA': f'{certs}/ca.pem',
|
|
38
|
+
'QUERYAPIGATE_AUDIT_SYSLOG_CERT': f'{certs}/client.pem',
|
|
39
|
+
'QUERYAPIGATE_AUDIT_SYSLOG_KEY': f'{certs}/client.key'}
|
|
40
|
+
if self.receiver == 'otelcol':
|
|
41
|
+
env.update(QUERYAPIGATE_AUDIT_OTLP='1', OTEL_EXPORTER_OTLP_ENDPOINT=RECEIVERS['otelcol']['url'],
|
|
42
|
+
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT='', OTEL_EXPORTER_OTLP_HEADERS='')
|
|
43
|
+
else:
|
|
44
|
+
env['QUERYAPIGATE_AUDIT_SYSLOG_URL'] = RECEIVERS[self.receiver]['url']
|
|
45
|
+
patcher = mock.patch.dict(os.environ, env)
|
|
46
|
+
patcher.start()
|
|
47
|
+
self.addCleanup(patcher.stop)
|
|
48
|
+
auditstream.AUTO_START = False
|
|
49
|
+
self.addCleanup(setattr, auditstream, 'AUTO_START', True)
|
|
50
|
+
self.addCleanup(self._close_senders)
|
|
51
|
+
self._close_senders()
|
|
52
|
+
db.init_schema()
|
|
53
|
+
|
|
54
|
+
@staticmethod
|
|
55
|
+
def _close_senders():
|
|
56
|
+
for sender in auditstream._senders.values():
|
|
57
|
+
sender.close()
|
|
58
|
+
auditstream._senders.clear()
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def output(self):
|
|
62
|
+
return 'otlp' if self.receiver == 'otelcol' else 'syslog'
|
|
63
|
+
|
|
64
|
+
def write(self, count, start=0):
|
|
65
|
+
for i in range(start, start + count):
|
|
66
|
+
store.record_audit('alice', 'update_key', f'{self.marker}-key-{i}', {'n': i})
|
|
67
|
+
|
|
68
|
+
def deliver(self):
|
|
69
|
+
return auditstream.deliver_once(self.output)
|
|
70
|
+
|
|
71
|
+
def received(self, count, timeout=30.0):
|
|
72
|
+
"""What the receiver wrote of this test's entries, once it has `count` - [(app, msgid, sd, entry)] for syslog,
|
|
73
|
+
[(record attributes, entry)] for the Collector."""
|
|
74
|
+
deadline = time.monotonic() + timeout
|
|
75
|
+
found = []
|
|
76
|
+
while time.monotonic() < deadline:
|
|
77
|
+
found = self._read()
|
|
78
|
+
if len(found) >= count:
|
|
79
|
+
return found
|
|
80
|
+
time.sleep(0.25)
|
|
81
|
+
self.fail(f'{self.receiver} wrote {len(found)} of {count} entries in {timeout:.0f}s')
|
|
82
|
+
|
|
83
|
+
def _read(self):
|
|
84
|
+
path = RECEIVERS[self.receiver]['out']
|
|
85
|
+
if not os.path.exists(path):
|
|
86
|
+
return []
|
|
87
|
+
with open(path, encoding='utf-8') as f:
|
|
88
|
+
lines = f.read().splitlines()
|
|
89
|
+
if self.receiver == 'otelcol':
|
|
90
|
+
records = []
|
|
91
|
+
for line in lines:
|
|
92
|
+
for resource in json.loads(line)['resourceLogs']:
|
|
93
|
+
for scope in resource['scopeLogs']:
|
|
94
|
+
for record in scope['logRecords']:
|
|
95
|
+
entry = json.loads(record['body']['stringValue'])
|
|
96
|
+
if entry['target'].startswith(self.marker):
|
|
97
|
+
attributes = {a['key']: next(iter(a['value'].values())) for a in record['attributes']}
|
|
98
|
+
records.append((attributes, entry))
|
|
99
|
+
return records
|
|
100
|
+
rows = []
|
|
101
|
+
for line in lines:
|
|
102
|
+
app, msgid, sd, message = line.split('\t', 3)
|
|
103
|
+
entry = json.loads(message.lstrip(' '))
|
|
104
|
+
if entry['target'].startswith(self.marker):
|
|
105
|
+
rows.append((app, msgid, sd, entry))
|
|
106
|
+
return rows
|
|
107
|
+
|
|
108
|
+
def copy_as_received(self, rows, edit=None):
|
|
109
|
+
"""The receiver's copy of this test's entries, as a file `audit verify --file` reads - one entry a line."""
|
|
110
|
+
path = os.path.join(self.tmp.name, f'{self.receiver}-copy.jsonl')
|
|
111
|
+
with open(path, 'w') as f:
|
|
112
|
+
for row in rows:
|
|
113
|
+
entry = dict(row[-1])
|
|
114
|
+
if edit is not None and entry['seq'] == edit:
|
|
115
|
+
entry['changes'] = {'n': 'forged'}
|
|
116
|
+
f.write(json.dumps(entry) + '\n')
|
|
117
|
+
return path
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class SyslogReceiverTests:
|
|
121
|
+
"""For rsyslog and syslog-ng alike."""
|
|
122
|
+
|
|
123
|
+
def test_every_entry_arrives_in_order_parsed_as_meant(self):
|
|
124
|
+
self.write(5)
|
|
125
|
+
self.assertEqual(self.deliver(), 5)
|
|
126
|
+
rows = self.received(5)
|
|
127
|
+
self.assertEqual([r[3]['seq'] for r in rows], [1, 2, 3, 4, 5])
|
|
128
|
+
for app, msgid, sd, entry in rows:
|
|
129
|
+
self.assertEqual(app, 'queryapigate')
|
|
130
|
+
self.assertEqual(msgid, 'update_key') # the action, as the receiver parsed it
|
|
131
|
+
self.assertEqual(sd, f'[meta sequenceId="{entry["seq"]}"]')
|
|
132
|
+
stored = [r[0] for r in db.connection().execute('SELECT hash FROM audit_log ORDER BY seq')]
|
|
133
|
+
self.assertEqual([r[3]['hash'] for r in rows], stored)
|
|
134
|
+
|
|
135
|
+
def test_its_copy_verifies_and_an_edit_in_it_is_found(self):
|
|
136
|
+
self.write(4)
|
|
137
|
+
self.deliver()
|
|
138
|
+
rows = self.received(4)
|
|
139
|
+
report = auditchain.verify_file(self.copy_as_received(rows))
|
|
140
|
+
self.assertEqual((report['intact'], report['first_seq'], report['last_seq']), (True, 1, 4), report)
|
|
141
|
+
broken = auditchain.verify_file(self.copy_as_received(rows, edit=3))
|
|
142
|
+
self.assertEqual((broken['break']['problem'], broken['break']['seq']), ('changed', 3))
|
|
143
|
+
|
|
144
|
+
def test_without_a_client_certificate_it_refuses_and_nothing_is_lost(self):
|
|
145
|
+
with mock.patch.dict(os.environ, {'QUERYAPIGATE_AUDIT_SYSLOG_CERT': '', 'QUERYAPIGATE_AUDIT_SYSLOG_KEY': ''}):
|
|
146
|
+
self.write(2)
|
|
147
|
+
with self.assertRaises(OSError) as caught:
|
|
148
|
+
self.deliver()
|
|
149
|
+
self.assertIn('CERTIFICATE_REQUIRED', str(caught.exception))
|
|
150
|
+
[status] = auditstream.status()
|
|
151
|
+
self.assertEqual((status['delivered_seq'], status['behind']), (0, 2))
|
|
152
|
+
self.assertIn('CERTIFICATE_REQUIRED', status['last_error'])
|
|
153
|
+
self._close_senders()
|
|
154
|
+
self.assertEqual(self.deliver(), 2) # with its certificate again: both, nothing lost
|
|
155
|
+
self.assertEqual([r[3]['seq'] for r in self.received(2)], [1, 2])
|
|
156
|
+
|
|
157
|
+
def test_a_client_certificate_from_another_ca_is_refused(self):
|
|
158
|
+
certs = RECEIVERS['certs']
|
|
159
|
+
with mock.patch.dict(os.environ, {'QUERYAPIGATE_AUDIT_SYSLOG_CERT': f'{certs}/stranger.pem',
|
|
160
|
+
'QUERYAPIGATE_AUDIT_SYSLOG_KEY': f'{certs}/stranger.key'}):
|
|
161
|
+
self.write(1)
|
|
162
|
+
with self.assertRaises(OSError) as caught:
|
|
163
|
+
self.deliver()
|
|
164
|
+
self.assertIn('UNKNOWN_CA', str(caught.exception))
|
|
165
|
+
self.assertEqual(auditstream.status()[0]['delivered_seq'], 0)
|
|
166
|
+
|
|
167
|
+
def test_an_outage_loses_nothing(self):
|
|
168
|
+
container = RECEIVERS[self.receiver]['container']
|
|
169
|
+
self.write(2)
|
|
170
|
+
self.assertEqual(self.deliver(), 2)
|
|
171
|
+
self.received(2)
|
|
172
|
+
_docker('stop', container) # QueryAPIGate's connection to it still open: noticed, not written into
|
|
173
|
+
try:
|
|
174
|
+
self.write(3, start=2)
|
|
175
|
+
with self.assertRaises(OSError):
|
|
176
|
+
self.deliver()
|
|
177
|
+
[status] = auditstream.status()
|
|
178
|
+
self.assertEqual((status['delivered_seq'], status['behind']), (2, 3))
|
|
179
|
+
finally:
|
|
180
|
+
_docker('start', container)
|
|
181
|
+
deadline = time.monotonic() + 60
|
|
182
|
+
while True: # until it listens again
|
|
183
|
+
try:
|
|
184
|
+
delivered = self.deliver()
|
|
185
|
+
break
|
|
186
|
+
except OSError:
|
|
187
|
+
if time.monotonic() > deadline:
|
|
188
|
+
raise
|
|
189
|
+
time.sleep(1)
|
|
190
|
+
self.assertEqual(delivered, 3)
|
|
191
|
+
rows = self.received(5)
|
|
192
|
+
self.assertEqual(sorted({r[3]['seq'] for r in rows}), [1, 2, 3, 4, 5])
|
|
193
|
+
self.assertTrue(auditchain.verify_file(self.copy_as_received(rows))['intact'])
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@unittest.skipUnless(SIEM, SKIP)
|
|
197
|
+
class RsyslogTests(SyslogReceiverTests, SiemTestCase):
|
|
198
|
+
receiver = 'rsyslog'
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@unittest.skipUnless(SIEM, SKIP)
|
|
202
|
+
class SyslogNgTests(SyslogReceiverTests, SiemTestCase):
|
|
203
|
+
receiver = 'syslog-ng'
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@unittest.skipUnless(SIEM, SKIP)
|
|
207
|
+
class OtelCollectorTests(SiemTestCase):
|
|
208
|
+
receiver = 'otelcol'
|
|
209
|
+
|
|
210
|
+
def test_every_entry_arrives_as_a_log_record_with_its_attributes(self):
|
|
211
|
+
self.write(3)
|
|
212
|
+
self.assertEqual(self.deliver(), 3)
|
|
213
|
+
records = self.received(3)
|
|
214
|
+
self.assertEqual([e['seq'] for _a, e in records], [1, 2, 3])
|
|
215
|
+
for attributes, entry in records:
|
|
216
|
+
self.assertEqual(int(attributes['queryapigate.audit.seq']), entry['seq'])
|
|
217
|
+
self.assertEqual(attributes['queryapigate.audit.hash'], entry['hash'])
|
|
218
|
+
self.assertEqual(attributes['queryapigate.audit.action'], 'update_key')
|
|
219
|
+
self.assertEqual(attributes['queryapigate.audit.actor'], 'alice')
|
|
220
|
+
self.assertTrue(auditchain.verify_file(self.copy_as_received(records))['intact'])
|
|
221
|
+
|
|
222
|
+
def test_an_outage_loses_nothing(self):
|
|
223
|
+
container = RECEIVERS['otelcol']['container']
|
|
224
|
+
_docker('stop', container)
|
|
225
|
+
try:
|
|
226
|
+
self.write(2)
|
|
227
|
+
with self.assertRaises(OSError):
|
|
228
|
+
self.deliver()
|
|
229
|
+
self.assertEqual(auditstream.status()[0]['delivered_seq'], 0)
|
|
230
|
+
finally:
|
|
231
|
+
_docker('start', container)
|
|
232
|
+
deadline = time.monotonic() + 60
|
|
233
|
+
while True:
|
|
234
|
+
try:
|
|
235
|
+
delivered = self.deliver()
|
|
236
|
+
break
|
|
237
|
+
except OSError:
|
|
238
|
+
if time.monotonic() > deadline:
|
|
239
|
+
raise
|
|
240
|
+
time.sleep(1)
|
|
241
|
+
self.assertEqual(delivered, 2)
|
|
242
|
+
self.assertEqual([e['seq'] for _a, e in self.received(2)], [1, 2])
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
if __name__ == '__main__':
|
|
246
|
+
unittest.main()
|
|
@@ -65,18 +65,22 @@ class MarkedEverywhereTests(unittest.TestCase):
|
|
|
65
65
|
note = read(path)[match.start():match.start() + 900].replace('\n>', ' ')
|
|
66
66
|
self.assertIn('may still be added', ' '.join(note.split()))
|
|
67
67
|
|
|
68
|
-
def
|
|
69
|
-
"""ADR 0017: each section opens with the promise, what may still be added, and its
|
|
68
|
+
def test_features_graduated_since_0_30_say_what_may_be_added_and_their_limits(self):
|
|
69
|
+
"""ADR 0017 and after: each section opens with the promise and when it began, what may still be added, and its
|
|
70
|
+
limits."""
|
|
70
71
|
api = 'documentation/API.md'
|
|
71
|
-
for path, heading in ((api, 'Descriptions'), (api, 'Agent usage'
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
72
|
+
for path, heading, since in ((api, 'Descriptions', '0.30'), (api, 'Agent usage', '0.30'),
|
|
73
|
+
(api, 'Usage quotas', '0.30'), (api, 'Configuration as code', '0.30'),
|
|
74
|
+
(api, 'Console sign-in through SSO', '0.30'),
|
|
75
|
+
(api, 'Streaming the audit log to a SIEM', '0.32'),
|
|
76
|
+
('documentation/MCP.md', 'Sign-in for AI clients (OAuth)', '0.30'),
|
|
77
|
+
('documentation/MCP.md', 'Write tools: agents that write, approved by a person',
|
|
78
|
+
'0.30'),
|
|
79
|
+
('documentation/DEPLOYMENT.md', 'Kubernetes (Helm)', '0.30')):
|
|
76
80
|
with self.subTest(heading=heading):
|
|
77
81
|
text = read(path)
|
|
78
82
|
match = re.search(rf'^#+ {re.escape(heading)}\n+(.+)', text, re.M)
|
|
79
|
-
self.assertTrue(match.group(1).startswith('> **Stable** since
|
|
83
|
+
self.assertTrue(match.group(1).startswith(f'> **Stable** since {since}'), match.group(1))
|
|
80
84
|
callout = text[match.start(1):].split('\n\n', 1)[0].replace('\n>', ' ')
|
|
81
85
|
self.assertIn('may still be added', ' '.join(callout.split()))
|
|
82
86
|
self.assertIn('Limits:', callout)
|
|
@@ -37,6 +37,15 @@ class PinnedTests(unittest.TestCase):
|
|
|
37
37
|
with self.subTest(image=image):
|
|
38
38
|
self.assertRegex(image, r'@sha256:[0-9a-f]{64}$')
|
|
39
39
|
|
|
40
|
+
def test_the_siem_receivers_ci_tests_against_are_pinned_by_digest(self):
|
|
41
|
+
"""ADR 0019: rsyslog, syslog-ng and the OpenTelemetry Collector, as siem_receivers.sh runs them."""
|
|
42
|
+
with open(os.path.join(ROOT, '.github', 'scripts', 'siem_receivers.sh')) as f:
|
|
43
|
+
images = dict(re.findall(r'^(RSYSLOG|SYSLOG_NG|OTELCOL)=(\S+)$', f.read(), re.M))
|
|
44
|
+
self.assertEqual(set(images), {'RSYSLOG', 'SYSLOG_NG', 'OTELCOL'})
|
|
45
|
+
for name, image in images.items():
|
|
46
|
+
with self.subTest(name=name):
|
|
47
|
+
self.assertRegex(image, r':[\w.-]+@sha256:[0-9a-f]{64}$')
|
|
48
|
+
|
|
40
49
|
def test_dependabot_proposes_the_updates(self):
|
|
41
50
|
with open(os.path.join(ROOT, '.github', 'dependabot.yml')) as f:
|
|
42
51
|
ecosystems = {u['package-ecosystem'] for u in yaml.safe_load(f)['updates']}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/console_dist/assets/AgentsPage-CJL7R9ML.js
RENAMED
|
File without changes
|
{queryapigate-0.31.0 → queryapigate-0.32.0}/queryapigate/console_dist/assets/AlertsPage-Rhnx0WE5.js
RENAMED
|
File without changes
|