queryapigate 0.24.0__tar.gz → 0.26.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.24.0/queryapigate.egg-info → queryapigate-0.26.0}/PKG-INFO +4 -1
- {queryapigate-0.24.0 → queryapigate-0.26.0}/README.md +3 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/__init__.py +1 -1
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/adminroles.py +1 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/alerts.py +48 -4
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/app.py +23 -0
- queryapigate-0.26.0/queryapigate/auditchain.py +215 -0
- queryapigate-0.26.0/queryapigate/auditstream.py +391 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/cli.py +42 -1
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/config.py +26 -1
- queryapigate-0.24.0/queryapigate/console_dist/assets/AccessMapPage-BOXNtPNM.js → queryapigate-0.26.0/queryapigate/console_dist/assets/AccessMapPage-rRSiv2mm.js +3 -3
- queryapigate-0.24.0/queryapigate/console_dist/assets/AdministratorsPage-Bg2F2ifQ.js → queryapigate-0.26.0/queryapigate/console_dist/assets/AdministratorsPage-DoeYjW1i.js +1 -1
- queryapigate-0.24.0/queryapigate/console_dist/assets/AgentsPage-DH0U29qp.js → queryapigate-0.26.0/queryapigate/console_dist/assets/AgentsPage-DerF6ZBb.js +1 -1
- queryapigate-0.26.0/queryapigate/console_dist/assets/AlertsPage-BWROcwNn.js +1 -0
- queryapigate-0.24.0/queryapigate/console_dist/assets/ApiKeysPage-BVbnuD7h.js → queryapigate-0.26.0/queryapigate/console_dist/assets/ApiKeysPage-DxEySu_Z.js +1 -1
- queryapigate-0.24.0/queryapigate/console_dist/assets/ApprovalsPage-CjaEgkFM.js → queryapigate-0.26.0/queryapigate/console_dist/assets/ApprovalsPage-DSKisl9D.js +1 -1
- queryapigate-0.24.0/queryapigate/console_dist/assets/AuditLogPage-DfTKaJQG.js → queryapigate-0.26.0/queryapigate/console_dist/assets/AuditLogPage-Etti--Y7.js +1 -1
- queryapigate-0.24.0/queryapigate/console_dist/assets/CachingPage-Cv5lzIeL.js → queryapigate-0.26.0/queryapigate/console_dist/assets/CachingPage-BZ61rPfT.js +1 -1
- queryapigate-0.24.0/queryapigate/console_dist/assets/CodeBox-CqMdAPPV.js → queryapigate-0.26.0/queryapigate/console_dist/assets/CodeBox-GGv4fKoR.js +1 -1
- queryapigate-0.24.0/queryapigate/console_dist/assets/ConnectionsPage-BaqJAfe0.js → queryapigate-0.26.0/queryapigate/console_dist/assets/ConnectionsPage-CFCKdJ9-.js +1 -1
- queryapigate-0.24.0/queryapigate/console_dist/assets/DesignerPage-DEFkQkts.js → queryapigate-0.26.0/queryapigate/console_dist/assets/DesignerPage-DL1ysNb9.js +1 -1
- queryapigate-0.24.0/queryapigate/console_dist/assets/DestinationsPage-DfwBrup7.js → queryapigate-0.26.0/queryapigate/console_dist/assets/DestinationsPage-Du2l56ln.js +1 -1
- queryapigate-0.24.0/queryapigate/console_dist/assets/Experimental-DpcpMNdn.js → queryapigate-0.26.0/queryapigate/console_dist/assets/Experimental-BB0g6F3s.js +1 -1
- queryapigate-0.24.0/queryapigate/console_dist/assets/ExportsPage-pg6lMLMv.js → queryapigate-0.26.0/queryapigate/console_dist/assets/ExportsPage-Cs2BpNim.js +1 -1
- queryapigate-0.24.0/queryapigate/console_dist/assets/HelpPage-CU0O7CwF.js → queryapigate-0.26.0/queryapigate/console_dist/assets/HelpPage-CUTTBoH6.js +2 -2
- queryapigate-0.24.0/queryapigate/console_dist/assets/HomePage-B3aYeZ_k.js → queryapigate-0.26.0/queryapigate/console_dist/assets/HomePage-DtOzlurj.js +2 -2
- queryapigate-0.24.0/queryapigate/console_dist/assets/MetricsPage-CvzheLYM.js → queryapigate-0.26.0/queryapigate/console_dist/assets/MetricsPage-o3kSXmOq.js +1 -1
- queryapigate-0.24.0/queryapigate/console_dist/assets/RepositoryPage-CJiYt-Z6.js → queryapigate-0.26.0/queryapigate/console_dist/assets/RepositoryPage-Cooj3jby.js +1 -1
- queryapigate-0.24.0/queryapigate/console_dist/assets/Results-CfCQ5C70.js → queryapigate-0.26.0/queryapigate/console_dist/assets/Results-xBStmWdp.js +1 -1
- queryapigate-0.24.0/queryapigate/console_dist/assets/RolesPage-BDNKrh_d.js → queryapigate-0.26.0/queryapigate/console_dist/assets/RolesPage-CcR_ttpB.js +1 -1
- queryapigate-0.24.0/queryapigate/console_dist/assets/SchemaBrowser-CGVt26yi.js → queryapigate-0.26.0/queryapigate/console_dist/assets/SchemaBrowser-DykbAObL.js +1 -1
- queryapigate-0.24.0/queryapigate/console_dist/assets/SettingsPage-BhXAvC-M.js → queryapigate-0.26.0/queryapigate/console_dist/assets/SettingsPage-mw196Ecu.js +1 -1
- queryapigate-0.24.0/queryapigate/console_dist/assets/StatTile-B5pXPl7t.js → queryapigate-0.26.0/queryapigate/console_dist/assets/StatTile-5XA5PKWp.js +1 -1
- queryapigate-0.24.0/queryapigate/console_dist/assets/Time-CAvdTclw.js → queryapigate-0.26.0/queryapigate/console_dist/assets/Time-zMnS1kSm.js +1 -1
- queryapigate-0.24.0/queryapigate/console_dist/assets/forms-Bu4qxNu4.js → queryapigate-0.26.0/queryapigate/console_dist/assets/forms-C5L_fKDm.js +1 -1
- queryapigate-0.24.0/queryapigate/console_dist/assets/forms-CuksvC9e.js → queryapigate-0.26.0/queryapigate/console_dist/assets/forms-Cl7JC3vt.js +1 -1
- queryapigate-0.24.0/queryapigate/console_dist/assets/icons-Dt5TMrqo.js → queryapigate-0.26.0/queryapigate/console_dist/assets/icons-B_wbfO4Z.js +1 -1
- queryapigate-0.24.0/queryapigate/console_dist/assets/index-Ct_sQPYd.js → queryapigate-0.26.0/queryapigate/console_dist/assets/index-D0zbyWWM.js +2 -2
- queryapigate-0.24.0/queryapigate/console_dist/assets/maskRules-BCy4YI8n.js → queryapigate-0.26.0/queryapigate/console_dist/assets/maskRules-Cw-l_UG8.js +1 -1
- queryapigate-0.24.0/queryapigate/console_dist/assets/reach-BZdwltHz.js → queryapigate-0.26.0/queryapigate/console_dist/assets/reach-DFqBXYli.js +1 -1
- queryapigate-0.24.0/queryapigate/console_dist/assets/shared-Pk-tTLpm.js → queryapigate-0.26.0/queryapigate/console_dist/assets/shared-DygTbsGs.js +1 -1
- queryapigate-0.24.0/queryapigate/console_dist/assets/useMutation-ZWXymgLt.js → queryapigate-0.26.0/queryapigate/console_dist/assets/useMutation-BCy3Br67.js +1 -1
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/console_dist/index.html +1 -1
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/db.py +60 -6
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/examples.py +9 -6
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/experimental.py +8 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/services/audit.py +1 -1
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/store.py +15 -8
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/v1.py +14 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/v1_spec.py +41 -1
- {queryapigate-0.24.0 → queryapigate-0.26.0/queryapigate.egg-info}/PKG-INFO +4 -1
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate.egg-info/SOURCES.txt +37 -32
- queryapigate-0.26.0/tests/test_audit_chain.py +265 -0
- queryapigate-0.26.0/tests/test_audit_stream.py +361 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_examples.py +10 -0
- queryapigate-0.26.0/tests/test_helm.py +192 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_settings.py +1 -1
- queryapigate-0.24.0/queryapigate/console_dist/assets/AlertsPage-CNGsUzAe.js +0 -1
- {queryapigate-0.24.0 → queryapigate-0.26.0}/LICENSE +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/pyproject.toml +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/__main__.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/admins.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/agentwrites.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/apikeys.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/broadcast.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/bundle.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/cache.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/collection_admin.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/console.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/console_dist/assets/index-D9oV4vVP.css +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/console_dist/assets/state-O13cl7cO.js +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/console_dist/favicon.svg +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/cors.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/databases.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/definitions.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/deprecations.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/descriptions.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/destinations.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/duckfiles.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/engine.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/errors.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/events.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/exporting.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/exports.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/formats.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/governance.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/history.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/instances.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/jwtauth.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/lib/h2-2.2.224.jar +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/limits.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/logging_setup.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/masking.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/mcp_server.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/metrics.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/mongotools.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/oidc.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/openapi.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/params.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/parquet.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/pool.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/postman.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/quotas.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/ratelimit.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/rediscache.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/rekey.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/runners.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/schema.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/secheaders.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/services/__init__.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/services/access.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/services/administrators.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/services/collections.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/services/connections.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/services/descriptions.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/services/destinations.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/services/exports.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/services/mcp.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/services/queries.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/sqlflow.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/sqltools.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/sso.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/tableguard.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/tracing.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate/usage.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate.egg-info/dependency_links.txt +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate.egg-info/entry_points.txt +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate.egg-info/requires.txt +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/queryapigate.egg-info/top_level.txt +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/setup.cfg +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_adhoc_history.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_adminroles.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_admins.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_agent_writes.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_alerts.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_api_administrators.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_api_keys.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_api_v1.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_audit_log.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_backup_restore.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_broadcast.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_cli.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_collection_bundle.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_collection_postman.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_collections.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_connection_encryption.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_console.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_cors_and_rate_limit.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_databases.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_db.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_dependency_floors.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_deprecations.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_descriptions.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_destinations.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_duckdb.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_duckdb_files.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_errors.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_events.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_events_server.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_experimental.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_export_security.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_exporting.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_exports.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_history.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_howto.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_instances.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_integration.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_jdbc.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_jwt.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_key_rotation.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_limits.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_masking.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_masking_requests.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_mcp.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_mcp_keycloak.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_mcp_oauth.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_mongotools.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_multinode.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_observability.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_oidc.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_params.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_parquet.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_pool.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_publishing.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_query_window.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_queryapigate.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_quotas.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_rekey.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_release_check.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_response_cache.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_roles.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_rolling_upgrade.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_security_headers.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_shared_rate_limit.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_sql_guard_fuzz.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_sql_guard_writes.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_sqlflow.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_sso.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_sso_keycloak.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_tableguard.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_tracing.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_upgrades.py +0 -0
- {queryapigate-0.24.0 → queryapigate-0.26.0}/tests/test_usage.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: queryapigate
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.26.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
|
|
@@ -663,6 +663,9 @@ Everything is configured through environment variables (all optional):
|
|
|
663
663
|
| `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. |
|
|
664
664
|
| `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. |
|
|
665
665
|
| `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. |
|
|
666
|
+
| `QUERYAPIGATE_AUDIT_SYSLOG_URL` | *(unset)* | Stream every audit entry to a SIEM over syslog: `tls://host:6514` or `tcp://host:514` (experimental). With `QUERYAPIGATE_AUDIT_SYSLOG_CA`, and `_CERT`/`_KEY` for mutual TLS. |
|
|
667
|
+
| `QUERYAPIGATE_AUDIT_OTLP` | `false` | Stream every audit entry as OTLP log records to the OTLP logs endpoint (experimental). |
|
|
668
|
+
| `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. |
|
|
666
669
|
| `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. |
|
|
667
670
|
| `QUERYAPIGATE_POOL_SIZE` | `5` | Idle connections kept per distinct connection setting. `0` turns pooling off. |
|
|
668
671
|
| `QUERYAPIGATE_POOL_IDLE_TIMEOUT` | `300` | Seconds an idle pooled connection is kept before it is closed. |
|
|
@@ -589,6 +589,9 @@ 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` (experimental). 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 (experimental). |
|
|
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. |
|
|
592
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. |
|
|
593
596
|
| `QUERYAPIGATE_POOL_SIZE` | `5` | Idle connections kept per distinct connection setting. `0` turns pooling off. |
|
|
594
597
|
| `QUERYAPIGATE_POOL_IDLE_TIMEOUT` | `300` | Seconds an idle pooled connection is kept before it is closed. |
|
|
@@ -71,6 +71,7 @@ OPERATIONS = {
|
|
|
71
71
|
'v1.clear_cache': 'cache.write', 'v1.delete_cache_entry': 'cache.write',
|
|
72
72
|
'v1.load_examples': 'examples.write', 'v1.unload_examples': 'examples.write',
|
|
73
73
|
'v1.list_alerts': 'observe', 'v1.list_audit': 'observe', 'v1.search_history': 'observe',
|
|
74
|
+
'v1.verify_audit': 'observe', 'v1.audit_delivery': 'observe',
|
|
74
75
|
'v1.list_instances': 'observe', 'v1.agent_usage': 'observe', 'v1.quota_usage': 'observe',
|
|
75
76
|
'v1.list_write_requests': 'writes.approve', 'v1.get_write_request': 'writes.approve',
|
|
76
77
|
'v1.approve_write_request': 'writes.approve', 'v1.reject_write_request': 'writes.approve',
|
|
@@ -58,7 +58,7 @@ KINDS = ('open_server', 'break_glass_used', 'connection_failing', 'key_expired',
|
|
|
58
58
|
'query_timeouts', 'query_slow', 'key_rate_limited', 'client_rate_limited', 'history_failed', 'history_dropped',
|
|
59
59
|
'instances_not_shared', 'instances_versions_differ', 'rate_limits_not_shared', 'export_failing', 'key_unused',
|
|
60
60
|
'agent_unusual_volume', 'sso_unreachable', 'writes_awaiting_approval', 'encryption_key_rotation_incomplete',
|
|
61
|
-
'quota_nearly_used')
|
|
61
|
+
'quota_nearly_used', 'audit_chain_broken', 'audit_delivery_behind')
|
|
62
62
|
|
|
63
63
|
|
|
64
64
|
def _alert(severity, kind, subject, title, detail, target=None, since=None):
|
|
@@ -75,9 +75,8 @@ def _plural(n, word):
|
|
|
75
75
|
def collect(now=None):
|
|
76
76
|
"""Every alert that holds now, most severe first."""
|
|
77
77
|
now = now or datetime.now()
|
|
78
|
-
found = [*_server(now), *_sso(), *_encryption(), *
|
|
79
|
-
*_rate_limited(),
|
|
80
|
-
*_history_health()]
|
|
78
|
+
found = [*_server(now), *_sso(), *_encryption(), *_audit_chain(), *_audit_delivery(now), *_writes(),
|
|
79
|
+
*_quotas(), *_keys(now), *_runs(now), *_rate_limited(), *_history_health()]
|
|
81
80
|
return sorted(found, key=lambda a: (SEVERITY_ORDER[a['severity']], a['kind'], a['id']))
|
|
82
81
|
|
|
83
82
|
|
|
@@ -121,6 +120,51 @@ def _encryption():
|
|
|
121
120
|
'the first. Remove the others from every instance to finish the rotation.', target)]
|
|
122
121
|
|
|
123
122
|
|
|
123
|
+
def _audit_chain():
|
|
124
|
+
"""The audit log's hash chain doesn't verify (ADR 0012): an entry changed, removed, inserted or reordered."""
|
|
125
|
+
from . import auditchain
|
|
126
|
+
try:
|
|
127
|
+
report = auditchain.cached_verify(db.connection())
|
|
128
|
+
except db.Error:
|
|
129
|
+
return []
|
|
130
|
+
if report['intact']:
|
|
131
|
+
return []
|
|
132
|
+
found = report['break']
|
|
133
|
+
where = f"entry {found.get('seq')} ({found.get('action')}, {found.get('timestamp')})"
|
|
134
|
+
problem = _CHAIN_PROBLEMS.get(found.get('problem'), found.get('problem'))
|
|
135
|
+
return [_alert('critical', 'audit_chain_broken', 'audit_log', 'The audit log has been tampered with',
|
|
136
|
+
f"Its hash chain breaks at {where}: {problem}. Compare it with your SIEM's copy, or the export "
|
|
137
|
+
'file (queryapigate audit verify --file).', {'type': 'audit', 'name': 'verify'},
|
|
138
|
+
found.get('timestamp'))]
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _audit_delivery(now):
|
|
142
|
+
"""A SIEM output more than five minutes behind, or failing (ADR 0012)."""
|
|
143
|
+
from . import auditstream
|
|
144
|
+
if not auditstream.outputs():
|
|
145
|
+
return []
|
|
146
|
+
found = []
|
|
147
|
+
for out in auditstream.status():
|
|
148
|
+
oldest = out['oldest_undelivered_at'] if out['behind'] else None
|
|
149
|
+
late = oldest is not None and oldest < (now - timedelta(minutes=5)).strftime('%Y-%m-%d %H:%M:%S')
|
|
150
|
+
if not late and not (out['behind'] and out['last_error']):
|
|
151
|
+
continue
|
|
152
|
+
severity = 'critical' if late and out['last_error'] else 'warning'
|
|
153
|
+
detail = (f"{out['behind']} audit entries are waiting, the oldest since {oldest}. "
|
|
154
|
+
+ (f"Last error: {out['last_error']} ({out['last_error_at']}). " if out['last_error'] else '')
|
|
155
|
+
+ 'They are kept, past the rolling cap, until delivered.')
|
|
156
|
+
found.append(_alert(severity, 'audit_delivery_behind', out['output'],
|
|
157
|
+
f"The audit log isn't reaching {out['target'] or out['output']}", detail,
|
|
158
|
+
{'type': 'audit', 'name': 'delivery'}, oldest))
|
|
159
|
+
return found
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
_CHAIN_PROBLEMS = {'changed': 'it was changed after it was written',
|
|
163
|
+
'changed_before': 'the entry before it was changed or removed',
|
|
164
|
+
'missing': 'entries before it are missing', 'out_of_order': 'it is out of order',
|
|
165
|
+
'unkeyed_after_keyed': 'it was re-written without the audit key'}
|
|
166
|
+
|
|
167
|
+
|
|
124
168
|
def _quotas():
|
|
125
169
|
"""Keys that have used 80% of a usage quota this period (warning), or all of it (critical) - ADR 0011."""
|
|
126
170
|
from . import quotas
|
|
@@ -187,6 +187,9 @@ def create_app():
|
|
|
187
187
|
"environment variable instead - it reads the same way but keeps the secret out of the file, "
|
|
188
188
|
"or set QUERYAPIGATE_SECRET_KEY to encrypt it at rest automatically.",
|
|
189
189
|
', '.join(plaintext), config.db_file())
|
|
190
|
+
_note_audit_key_rotation()
|
|
191
|
+
from . import auditstream
|
|
192
|
+
auditstream.start() # the audit log to a SIEM, when an output is set (ADR 0012)
|
|
190
193
|
if config.load_examples():
|
|
191
194
|
load_examples_at_startup()
|
|
192
195
|
app.json = JSONProvider(app)
|
|
@@ -313,6 +316,26 @@ def create_app():
|
|
|
313
316
|
return app
|
|
314
317
|
|
|
315
318
|
|
|
319
|
+
def _note_audit_key_rotation():
|
|
320
|
+
"""Put a change of QUERYAPIGATE_AUDIT_KEY in the chain (ADR 0012): `audit_key_set` when one is set and the newest
|
|
321
|
+
entry isn't keyed yet - so a key set means the newest entry is keyed, which verify() holds the chain to - and
|
|
322
|
+
`audit_key_rotated` when the first key changed since the newest entry was signed."""
|
|
323
|
+
from . import auditchain
|
|
324
|
+
if not auditchain.keys():
|
|
325
|
+
return
|
|
326
|
+
try:
|
|
327
|
+
conn = db.connection()
|
|
328
|
+
if not auditchain.head_keyed(conn): # a key set for the first time: from here on, every entry is keyed
|
|
329
|
+
# How many came before, unkeyed: expected once, when the key is first set. Again later - after a key was
|
|
330
|
+
# set long ago - it means the keyed history was replaced, which the SIEM's copy shows (ADR 0012).
|
|
331
|
+
unkeyed = conn.execute('SELECT COUNT(*) FROM audit_log').fetchone()[0]
|
|
332
|
+
store.record_audit('startup', 'audit_key_set', 'audit_log', {'entries_before_unkeyed': unkeyed})
|
|
333
|
+
elif auditchain.head_signed_by(conn) not in (None, 0):
|
|
334
|
+
store.record_audit('startup', 'audit_key_rotated', 'audit_log', {'keys': len(auditchain.keys())})
|
|
335
|
+
except db.Error:
|
|
336
|
+
log.warning('Could not check the audit key against the audit log', exc_info=True)
|
|
337
|
+
|
|
338
|
+
|
|
316
339
|
def error_body(message, status, extra=None, code=None):
|
|
317
340
|
"""The JSON body of every error response (errors.py, BACKLOG #69): the message, a stable `code` (the raiser's
|
|
318
341
|
own, or the one for the status), whatever extras the raiser attached, and the `request_id` that finds the
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""The audit log as a hash chain (ADR 0012, BACKLOG #96): each entry carries `seq`, the previous entry's `hash` as its
|
|
2
|
+
`prev_hash`, and its own `hash` over both and the entry - so changing, removing, inserting or reordering an entry
|
|
3
|
+
breaks every hash after it.
|
|
4
|
+
|
|
5
|
+
The hash is HMAC-SHA256 under QUERYAPIGATE_AUDIT_KEY when that is set ("hmac-sha256:<hex>"): someone who can write to
|
|
6
|
+
the store but hasn't the key can't re-sign an edit. Without it, plain SHA-256 ("sha256:<hex>"): consistent, but anyone
|
|
7
|
+
who knows to can recompute it - a SIEM copy outside the store is what contradicts that. Several keys,
|
|
8
|
+
comma-separated, while rotating: the first signs, every one verifies. Once the chain is keyed, an unkeyed entry after
|
|
9
|
+
a keyed one is a break, not a change of mind.
|
|
10
|
+
|
|
11
|
+
Entries are appended inside the insert's transaction, which holds the store's write lock (db.transaction()), so
|
|
12
|
+
instances writing at once still make one chain. The rolling cap (QUERYAPIGATE_AUDIT_LOG_LIMIT) trims the oldest; the
|
|
13
|
+
anchor (audit_chain) remembers the newest entry trimmed, so the chain's first kept entry must follow it - a deletion
|
|
14
|
+
from the head looks different from a trim.
|
|
15
|
+
"""
|
|
16
|
+
import hashlib
|
|
17
|
+
import hmac
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
|
|
21
|
+
GENESIS = '0' * 64
|
|
22
|
+
ANCHOR = 'anchor'
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def keys():
|
|
26
|
+
"""QUERYAPIGATE_AUDIT_KEY's keys, first to last - empty when unset."""
|
|
27
|
+
return [k.strip() for k in os.environ.get('QUERYAPIGATE_AUDIT_KEY', '').split(',') if k.strip()]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def check_keys():
|
|
31
|
+
"""At startup: each key long enough to be one (32 characters or more), none twice."""
|
|
32
|
+
found = keys()
|
|
33
|
+
for i, key in enumerate(found):
|
|
34
|
+
which = ('QUERYAPIGATE_AUDIT_KEY' if len(found) == 1
|
|
35
|
+
else f'Key {i + 1} of {len(found)} in QUERYAPIGATE_AUDIT_KEY')
|
|
36
|
+
if len(key) < 32:
|
|
37
|
+
raise ValueError(f'{which} must be at least 32 characters - e.g. from '
|
|
38
|
+
'`python -c "import secrets; print(secrets.token_hex(32))"`')
|
|
39
|
+
if key in found[:i]:
|
|
40
|
+
raise ValueError(f'{which} is the same as key {found.index(key) + 1} - list each key once')
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def canonical(entry):
|
|
44
|
+
"""The entry as hashed: sorted keys, no whitespace - the same bytes from the dict written and the JSON read back."""
|
|
45
|
+
return json.dumps(entry, sort_keys=True, separators=(',', ':'), default=str, ensure_ascii=False)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _message(prev_hash, seq, entry):
|
|
49
|
+
return f'{prev_hash}\n{seq}\n{canonical(entry)}'.encode()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def compute(prev_hash, seq, entry, key=None):
|
|
53
|
+
if key is None:
|
|
54
|
+
return 'sha256:' + hashlib.sha256(_message(prev_hash, seq, entry)).hexdigest()
|
|
55
|
+
return 'hmac-sha256:' + hmac.new(key.encode(), _message(prev_hash, seq, entry), hashlib.sha256).hexdigest()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _matches(stored, prev_hash, seq, entry, listed):
|
|
59
|
+
"""Whether `stored` is this entry's hash - under any listed key, or unkeyed. Returns (ok, keyed, known): `known`
|
|
60
|
+
False for a keyed hash with no key to check it against."""
|
|
61
|
+
if stored.startswith('sha256:'):
|
|
62
|
+
return hmac.compare_digest(stored, compute(prev_hash, seq, entry)), False, True
|
|
63
|
+
if stored.startswith('hmac-sha256:'):
|
|
64
|
+
if not listed:
|
|
65
|
+
return True, True, False
|
|
66
|
+
return any(hmac.compare_digest(stored, compute(prev_hash, seq, entry, k)) for k in listed), True, True
|
|
67
|
+
return False, False, True
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _head(conn):
|
|
71
|
+
row = conn.execute('SELECT seq, hash FROM audit_log WHERE seq IS NOT NULL ORDER BY seq DESC LIMIT 1').fetchone()
|
|
72
|
+
if row is not None:
|
|
73
|
+
return row[0], row[1]
|
|
74
|
+
anchor = conn.execute('SELECT seq, hash FROM audit_chain WHERE name = ?', (ANCHOR,)).fetchone()
|
|
75
|
+
return (anchor[0], anchor[1]) if anchor is not None else (0, GENESIS)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def append(conn, entry, limit):
|
|
79
|
+
"""Insert `entry` as the chain's next link, inside the caller's transaction, and trim to `limit`, moving the anchor.
|
|
80
|
+
Returns (seq, prev_hash, hash). Entries written meanwhile without a link - by an instance still on 0.24, during a
|
|
81
|
+
rolling upgrade - are linked in first, in the order they were written."""
|
|
82
|
+
chain_existing(conn)
|
|
83
|
+
seq, prev_hash = _head(conn)
|
|
84
|
+
seq += 1
|
|
85
|
+
listed = keys()
|
|
86
|
+
digest = compute(prev_hash, seq, entry, listed[0] if listed else None)
|
|
87
|
+
conn.execute('INSERT INTO audit_log (timestamp, entry_json, seq, prev_hash, hash) VALUES (?, ?, ?, ?, ?)',
|
|
88
|
+
(entry['timestamp'], json.dumps(entry, default=str), seq, prev_hash, digest))
|
|
89
|
+
cut = seq - limit # keep the newest `limit`: everything up to `cut` goes, and `cut` becomes the anchor
|
|
90
|
+
from . import auditstream
|
|
91
|
+
floor = auditstream.delivery_floor(conn)
|
|
92
|
+
if floor is not None and floor < cut: # not delivered to a SIEM yet: kept - up to ten times the cap
|
|
93
|
+
cut = max(floor, seq - limit * auditstream.MAX_BACKLOG_FACTOR)
|
|
94
|
+
if cut > 0:
|
|
95
|
+
row = conn.execute('SELECT seq, hash FROM audit_log WHERE seq = ?', (cut,)).fetchone()
|
|
96
|
+
if row is not None:
|
|
97
|
+
_set_anchor(conn, row[0], row[1])
|
|
98
|
+
conn.execute('DELETE FROM audit_log WHERE seq <= ?', (cut,))
|
|
99
|
+
return seq, prev_hash, digest
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _set_anchor(conn, seq, digest):
|
|
103
|
+
if conn.execute('SELECT 1 FROM audit_chain WHERE name = ?', (ANCHOR,)).fetchone() is None:
|
|
104
|
+
conn.execute('INSERT INTO audit_chain (name, seq, hash) VALUES (?, ?, ?)', (ANCHOR, seq, digest))
|
|
105
|
+
else:
|
|
106
|
+
conn.execute('UPDATE audit_chain SET seq = ?, hash = ? WHERE name = ?', (seq, digest, ANCHOR))
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def chain_existing(conn):
|
|
110
|
+
"""Schema 14: link the entries already stored, oldest first, as they are now - the chain proves nothing about them
|
|
111
|
+
before this moment, which the `audit_chain_started` entry that follows records."""
|
|
112
|
+
rows = conn.execute('SELECT id, entry_json FROM audit_log WHERE seq IS NULL ORDER BY id').fetchall()
|
|
113
|
+
seq, prev_hash = _head(conn)
|
|
114
|
+
listed = keys()
|
|
115
|
+
for row in rows:
|
|
116
|
+
seq += 1
|
|
117
|
+
entry = json.loads(row['entry_json'])
|
|
118
|
+
digest = compute(prev_hash, seq, entry, listed[0] if listed else None)
|
|
119
|
+
conn.execute('UPDATE audit_log SET seq = ?, prev_hash = ?, hash = ? WHERE id = ?',
|
|
120
|
+
(seq, prev_hash, digest, row['id']))
|
|
121
|
+
prev_hash = digest
|
|
122
|
+
return len(rows)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _walk(links, anchor):
|
|
126
|
+
"""Verify `links` - (seq, prev_hash, hash, entry), in seq order - from `anchor` (seq, hash). Returns the report."""
|
|
127
|
+
listed = keys()
|
|
128
|
+
expected_seq, expected_prev = anchor
|
|
129
|
+
keyed_seen = unchecked = 0
|
|
130
|
+
for checked, (seq, prev_hash, stored, entry) in enumerate(links):
|
|
131
|
+
where = {'seq': seq, 'timestamp': entry.get('timestamp'), 'action': entry.get('action')}
|
|
132
|
+
if seq != expected_seq + 1:
|
|
133
|
+
problem = 'missing' if seq > expected_seq + 1 else 'out_of_order'
|
|
134
|
+
return {'intact': False, 'checked': checked, 'break': {**where, 'problem': problem,
|
|
135
|
+
'expected_seq': expected_seq + 1}}
|
|
136
|
+
if prev_hash != expected_prev:
|
|
137
|
+
return {'intact': False, 'checked': checked, 'break': {**where, 'problem': 'changed_before'}}
|
|
138
|
+
ok, keyed, known = _matches(stored or '', prev_hash, seq, entry, listed)
|
|
139
|
+
if not ok:
|
|
140
|
+
return {'intact': False, 'checked': checked, 'break': {**where, 'problem': 'changed'}}
|
|
141
|
+
if keyed:
|
|
142
|
+
keyed_seen += 1
|
|
143
|
+
unchecked += 0 if known else 1
|
|
144
|
+
elif keyed_seen: # unkeyed after keyed: re-written without the key
|
|
145
|
+
return {'intact': False, 'checked': checked, 'break': {**where, 'problem': 'unkeyed_after_keyed'}}
|
|
146
|
+
expected_seq, expected_prev = seq, stored
|
|
147
|
+
if listed and links and not (links[-1][2] or '').startswith('hmac-sha256:'):
|
|
148
|
+
# With a key set the server signs every entry, and says so when it starts (audit_key_set) - so a newest entry
|
|
149
|
+
# that isn't keyed means the chain was re-written without the key, from the start if need be.
|
|
150
|
+
seq, _prev, _stored, entry = links[-1]
|
|
151
|
+
return {'intact': False, 'checked': len(links), 'break': {'seq': seq, 'timestamp': entry.get('timestamp'),
|
|
152
|
+
'action': entry.get('action'),
|
|
153
|
+
'problem': 'unkeyed_after_keyed'}}
|
|
154
|
+
return {'intact': True, 'entries': expected_seq - anchor[0], 'first_seq': anchor[0] + 1, 'last_seq': expected_seq,
|
|
155
|
+
'keyed': bool(keyed_seen), 'unchecked_without_key': unchecked}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def verify(conn):
|
|
159
|
+
"""The stored chain, from the anchor: {'intact': True, ...} or {'intact': False, 'break': {...}}."""
|
|
160
|
+
anchor = conn.execute('SELECT seq, hash FROM audit_chain WHERE name = ?', (ANCHOR,)).fetchone()
|
|
161
|
+
rows = conn.execute('SELECT seq, prev_hash, hash, entry_json FROM audit_log ORDER BY seq').fetchall()
|
|
162
|
+
links = [(r['seq'], r['prev_hash'], r['hash'], json.loads(r['entry_json'])) for r in rows]
|
|
163
|
+
report = _walk(links, (anchor[0], anchor[1]) if anchor is not None else (0, GENESIS))
|
|
164
|
+
report['entries_stored'] = len(links)
|
|
165
|
+
return report
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def verify_file(path):
|
|
169
|
+
"""An export file (QUERYAPIGATE_AUDIT_LOG_EXPORT_FILE), one entry per line with its chain fields - from its first
|
|
170
|
+
line, whose `prev_hash` is taken as given (the file may have started after the chain did)."""
|
|
171
|
+
links = []
|
|
172
|
+
with open(path) as f:
|
|
173
|
+
for number, line in enumerate(f, 1):
|
|
174
|
+
if not line.strip():
|
|
175
|
+
continue
|
|
176
|
+
try:
|
|
177
|
+
data = json.loads(line)
|
|
178
|
+
except ValueError:
|
|
179
|
+
return {'intact': False, 'break': {'line': number, 'problem': 'not_json'}}
|
|
180
|
+
if 'seq' not in data:
|
|
181
|
+
continue # written before the chain existed
|
|
182
|
+
chain = (data.pop('seq'), data.pop('prev_hash', None), data.pop('hash', None))
|
|
183
|
+
links.append((*chain, data))
|
|
184
|
+
if not links:
|
|
185
|
+
return {'intact': True, 'entries': 0}
|
|
186
|
+
return _walk(links, (links[0][0] - 1, links[0][1]))
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def head_keyed(conn):
|
|
190
|
+
"""Whether the newest entry is signed with a key at all (False for none, or an empty log)."""
|
|
191
|
+
row = conn.execute('SELECT hash FROM audit_log ORDER BY seq DESC LIMIT 1').fetchone()
|
|
192
|
+
return row is not None and (row['hash'] or '').startswith('hmac-sha256:')
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def head_signed_by(conn):
|
|
196
|
+
"""Which listed key signed the newest entry - 0 for the first, None for none (unkeyed, or no key verifies it)."""
|
|
197
|
+
row = conn.execute('SELECT seq, prev_hash, hash, entry_json FROM audit_log ORDER BY seq DESC LIMIT 1').fetchone()
|
|
198
|
+
if row is None or not (row['hash'] or '').startswith('hmac-sha256:'):
|
|
199
|
+
return None
|
|
200
|
+
entry = json.loads(row['entry_json'])
|
|
201
|
+
for i, key in enumerate(keys()):
|
|
202
|
+
if hmac.compare_digest(row['hash'], compute(row['prev_hash'], row['seq'], entry, key)):
|
|
203
|
+
return i
|
|
204
|
+
return None
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
_checked = {'at': 0.0, 'report': None}
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def cached_verify(conn, max_age=300.0):
|
|
211
|
+
"""verify(), at most once every `max_age` seconds per process - for the alert, which the Console asks for often."""
|
|
212
|
+
import time
|
|
213
|
+
if _checked['report'] is None or time.monotonic() - _checked['at'] >= max_age:
|
|
214
|
+
_checked['report'], _checked['at'] = verify(conn), time.monotonic()
|
|
215
|
+
return _checked['report']
|