howler-api 4.0.0.dev740__tar.gz → 4.0.0.dev799__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.
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/PKG-INFO +6 -3
- howler_api-4.0.0.dev799/howler/actions/add_to_bundle.py +136 -0
- howler_api-4.0.0.dev799/howler/actions/remove_from_bundle.py +150 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/api/__init__.py +3 -1
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/api/socket.py +4 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/api/v1/clue.py +17 -19
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/api/v1/hit.py +134 -1
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/api/v1/search.py +3 -1
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/api/v1/tool.py +45 -5
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/api/v2/ingest.py +4 -9
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/app.py +5 -10
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/datastore/collection.py +26 -16
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/helper/discover.py +4 -4
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/helper/oauth.py +0 -2
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/helper.py +2 -2
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/case.py +1 -1
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/config.py +21 -24
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/random_data.py +8 -7
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/security/__init__.py +2 -10
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/security/utils.py +4 -2
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/services/action_service.py +2 -2
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/services/auth_service.py +6 -4
- howler_api-4.0.0.dev799/howler/services/bundle_compat_service.py +273 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/services/case_service.py +0 -3
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/services/config_service.py +4 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/services/event_service.py +3 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/services/hit_service.py +20 -13
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/services/lucene_service.py +2 -1
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/services/observable_service.py +17 -3
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/services/user_service.py +3 -2
- howler_api-4.0.0.dev799/howler/telemetry.py +65 -0
- howler_api-4.0.0.dev799/howler/utils/constants.py +4 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/pyproject.toml +7 -4
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/README.md +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/__init__.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/actions/__init__.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/actions/add_label.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/actions/add_to_case.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/actions/change_field.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/actions/demote.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/actions/example_plugin.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/actions/prioritization.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/actions/promote.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/actions/remove_label.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/actions/transition.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/api/base.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/api/v1/__init__.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/api/v1/action.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/api/v1/analytic.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/api/v1/auth.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/api/v1/configs.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/api/v1/dossier.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/api/v1/help.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/api/v1/notebook.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/api/v1/overview.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/api/v1/template.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/api/v1/user.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/api/v1/utils/__init__.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/api/v1/utils/etag.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/api/v1/view.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/api/v2/__init__.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/api/v2/case.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/api/v2/search.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/common/README.md +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/common/__init__.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/common/classification.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/common/classification.yml +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/common/exceptions.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/common/loader.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/common/logging/__init__.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/common/logging/audit.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/common/logging/format.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/common/net.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/common/net_static.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/common/random_user.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/common/swagger.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/config.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/cronjobs/__init__.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/cronjobs/retention.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/cronjobs/view_cleanup.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/datastore/README.md +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/datastore/__init__.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/datastore/bulk.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/datastore/constants.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/datastore/exceptions.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/datastore/howler_store.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/datastore/migrations/fix_process.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/datastore/operations.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/datastore/schemas.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/datastore/store.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/datastore/support/__init__.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/datastore/support/build.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/datastore/support/schemas.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/datastore/types.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/error.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/external/README.md +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/external/__init__.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/external/generate_mitre.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/external/generate_sigma_rules.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/external/generate_tlds.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/external/reindex_data.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/external/wipe_databases.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/gunicorn_config.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/healthz.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/helper/__init__.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/helper/azure.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/helper/hit.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/helper/search.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/helper/workflow.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/helper/ws.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/README.md +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/__init__.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/base.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/charter.txt +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/constants.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/howler_enum.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/mixins.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/__init__.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/action.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/analytic.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/assemblyline.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/aws.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/azure.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/cbs.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/clue.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/dossier.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/__init__.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/agent.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/autonomous_system.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/client.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/cloud.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/code_signature.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/container.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/dns.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/egress.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/elf.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/email.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/error.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/event.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/faas.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/file.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/geo.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/group.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/hash.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/host.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/http.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/ingress.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/interface.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/network.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/observer.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/organization.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/os.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/pe.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/process.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/registry.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/related.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/rule.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/server.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/threat.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/tls.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/url.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/user.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/user_agent.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/ecs/vulnerability.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/gcp.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/hit.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/howler_data.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/lead.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/localized_label.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/observable.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/overview.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/pivot.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/record.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/template.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/user.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/models/view.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/odm/randomizer.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/patched.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/plugins/__init__.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/plugins/config.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/remote/__init__.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/remote/datatypes/README.md +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/remote/datatypes/__init__.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/remote/datatypes/counters.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/remote/datatypes/events.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/remote/datatypes/hash.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/remote/datatypes/lock.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/remote/datatypes/queues/__init__.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/remote/datatypes/queues/comms.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/remote/datatypes/queues/multi.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/remote/datatypes/queues/named.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/remote/datatypes/queues/priority.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/remote/datatypes/set.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/remote/datatypes/user_quota_tracker.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/security/socket.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/services/__init__.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/services/analytic_service.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/services/docs_service.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/services/dossier_service.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/services/jwt_service.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/services/notebook_service.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/services/overview_service.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/services/search_service.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/services/template_service.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/utils/__init__.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/utils/annotations.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/utils/chunk.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/utils/compat.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/utils/dict_utils.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/utils/isotime.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/utils/list_utils.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/utils/lucene.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/utils/path.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/utils/socket_utils.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/utils/str_utils.py +0 -0
- {howler_api-4.0.0.dev740 → howler_api-4.0.0.dev799}/howler/utils/uid.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: howler-api
|
|
3
|
-
Version: 4.0.0.
|
|
3
|
+
Version: 4.0.0.dev799
|
|
4
4
|
Summary: Howler - API server
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: howler,alerting,gc,canada,cse-cst,cse,cst,cyber,cccs
|
|
@@ -8,7 +8,7 @@ Author: Canadian Centre for Cyber Security
|
|
|
8
8
|
Author-email: howler@cyber.gc.ca
|
|
9
9
|
Maintainer: Matthew Rafuse
|
|
10
10
|
Maintainer-email: matthew.rafuse@cyber.gc.ca
|
|
11
|
-
Requires-Python: >=3.
|
|
11
|
+
Requires-Python: >=3.10,<4.0
|
|
12
12
|
Classifier: Development Status :: 5 - Production/Stable
|
|
13
13
|
Classifier: Intended Audience :: Developers
|
|
14
14
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -21,10 +21,10 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
21
21
|
Classifier: Topic :: Software Development :: Libraries
|
|
22
22
|
Requires-Dist: apscheduler (==3.11.2)
|
|
23
23
|
Requires-Dist: authlib (>=1.6.0,<2.0.0)
|
|
24
|
+
Requires-Dist: azure-monitor-opentelemetry (>=1.8.7,<2.0.0)
|
|
24
25
|
Requires-Dist: bcrypt (==4.3.0)
|
|
25
26
|
Requires-Dist: chardet (==5.2.0)
|
|
26
27
|
Requires-Dist: chevron (==0.14.0)
|
|
27
|
-
Requires-Dist: elastic-apm[flask] (>=6.22.0,<7.0.0)
|
|
28
28
|
Requires-Dist: elasticsearch (==8.19.3)
|
|
29
29
|
Requires-Dist: flasgger (>=0.9.7.1,<0.10.0.0)
|
|
30
30
|
Requires-Dist: flask (==3.1.3)
|
|
@@ -33,6 +33,9 @@ Requires-Dist: gevent (>=25.9.1,<26.0.0)
|
|
|
33
33
|
Requires-Dist: gunicorn (==23.0.0)
|
|
34
34
|
Requires-Dist: luqum (>=1.0.0,<2.0.0)
|
|
35
35
|
Requires-Dist: mergedeep (>=1.3.4,<2.0.0)
|
|
36
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-http (==1.40.0)
|
|
37
|
+
Requires-Dist: opentelemetry-instrumentation-flask (==0.61b0)
|
|
38
|
+
Requires-Dist: opentelemetry-sdk (==1.40.0)
|
|
36
39
|
Requires-Dist: packaging (<25.0)
|
|
37
40
|
Requires-Dist: passlib (==1.7.4)
|
|
38
41
|
Requires-Dist: prometheus-client (==0.24.1)
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Deprecated add_to_bundle action — delegates to add_to_case via bundle_compat_service."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from howler.common.exceptions import NotFoundException
|
|
6
|
+
from howler.common.loader import datastore
|
|
7
|
+
from howler.odm.models.action import VALID_TRIGGERS
|
|
8
|
+
from howler.services import bundle_compat_service, case_service
|
|
9
|
+
from howler.utils.str_utils import sanitize_lucene_query
|
|
10
|
+
|
|
11
|
+
OPERATION_ID = "add_to_bundle"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def execute(query: str, bundle_id: Optional[str] = None, **kwargs):
|
|
15
|
+
"""Add a set of hits matching the query to the specified bundle (deprecated — uses cases).
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
query (str): The query containing the matching hits
|
|
19
|
+
bundle_id (str): The ``howler.id`` of the bundle to add the hits to.
|
|
20
|
+
"""
|
|
21
|
+
report = []
|
|
22
|
+
|
|
23
|
+
if not bundle_id:
|
|
24
|
+
return [
|
|
25
|
+
{
|
|
26
|
+
"query": query,
|
|
27
|
+
"outcome": "error",
|
|
28
|
+
"title": "Invalid Bundle ID",
|
|
29
|
+
"message": "Bundle ID cannot be empty.",
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
case_id = bundle_compat_service.find_case_for_bundle(bundle_id)
|
|
35
|
+
if case_id is None:
|
|
36
|
+
report.append(
|
|
37
|
+
{
|
|
38
|
+
"query": query,
|
|
39
|
+
"outcome": "error",
|
|
40
|
+
"title": "Invalid Bundle",
|
|
41
|
+
"message": f"Either a hit with ID {bundle_id} does not exist, or it has no associated case.",
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
return report
|
|
45
|
+
|
|
46
|
+
ds = datastore()
|
|
47
|
+
matching_hits = ds.hit.search(query, rows=1000)["items"]
|
|
48
|
+
|
|
49
|
+
if not matching_hits:
|
|
50
|
+
report.append(
|
|
51
|
+
{
|
|
52
|
+
"query": query,
|
|
53
|
+
"outcome": "skipped",
|
|
54
|
+
"title": "No Matching Hits",
|
|
55
|
+
"message": "There were no hits matching this query.",
|
|
56
|
+
}
|
|
57
|
+
)
|
|
58
|
+
return report
|
|
59
|
+
|
|
60
|
+
added = []
|
|
61
|
+
skipped = []
|
|
62
|
+
for hit in matching_hits:
|
|
63
|
+
child_label = f"hits/{hit.howler.analytic} ({hit.howler.id})"
|
|
64
|
+
try:
|
|
65
|
+
case_service.append_case_item(
|
|
66
|
+
case_id,
|
|
67
|
+
item_type="hit",
|
|
68
|
+
item_value=hit.howler.id,
|
|
69
|
+
item_path=child_label,
|
|
70
|
+
)
|
|
71
|
+
added.append(hit.howler.id)
|
|
72
|
+
except Exception:
|
|
73
|
+
skipped.append(hit.howler.id)
|
|
74
|
+
|
|
75
|
+
if skipped:
|
|
76
|
+
report.append(
|
|
77
|
+
{
|
|
78
|
+
"query": f"howler.id:({' OR '.join(sanitize_lucene_query(h) for h in skipped)})",
|
|
79
|
+
"outcome": "skipped",
|
|
80
|
+
"title": "Skipped Hits",
|
|
81
|
+
"message": "These hits could not be added (already present or invalid).",
|
|
82
|
+
}
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
if added:
|
|
86
|
+
report.append(
|
|
87
|
+
{
|
|
88
|
+
"query": f"howler.id:({' OR '.join(sanitize_lucene_query(h) for h in added)})",
|
|
89
|
+
"outcome": "success",
|
|
90
|
+
"title": "Executed Successfully",
|
|
91
|
+
"message": "The specified bundle has had all matching hits added.",
|
|
92
|
+
}
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
except NotFoundException as e:
|
|
96
|
+
report.append(
|
|
97
|
+
{
|
|
98
|
+
"query": query,
|
|
99
|
+
"outcome": "error",
|
|
100
|
+
"title": "Failed to Execute",
|
|
101
|
+
"message": str(e),
|
|
102
|
+
}
|
|
103
|
+
)
|
|
104
|
+
except Exception as e:
|
|
105
|
+
report.append(
|
|
106
|
+
{
|
|
107
|
+
"query": query,
|
|
108
|
+
"outcome": "error",
|
|
109
|
+
"title": "Failed to Execute",
|
|
110
|
+
"message": f"Unknown exception occurred: {str(e)}",
|
|
111
|
+
}
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
return report
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def specification():
|
|
118
|
+
"""Specify various properties of the action, such as title, descriptions, permissions and input steps."""
|
|
119
|
+
return {
|
|
120
|
+
"id": OPERATION_ID,
|
|
121
|
+
"title": "Add to Bundle (Deprecated)",
|
|
122
|
+
"priority": 6,
|
|
123
|
+
"i18nKey": f"operations.{OPERATION_ID}",
|
|
124
|
+
"description": {
|
|
125
|
+
"short": "Add a set of hits to a bundle (deprecated — uses cases)",
|
|
126
|
+
"long": execute.__doc__,
|
|
127
|
+
},
|
|
128
|
+
"roles": ["automation_basic"],
|
|
129
|
+
"steps": [
|
|
130
|
+
{
|
|
131
|
+
"args": {"bundle_id": []},
|
|
132
|
+
"options": {},
|
|
133
|
+
}
|
|
134
|
+
],
|
|
135
|
+
"triggers": VALID_TRIGGERS,
|
|
136
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""Deprecated remove_from_bundle action — delegates to case_service for item removal."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from howler.common.exceptions import NotFoundException
|
|
6
|
+
from howler.common.loader import datastore
|
|
7
|
+
from howler.odm.models.action import VALID_TRIGGERS
|
|
8
|
+
from howler.services import bundle_compat_service, case_service
|
|
9
|
+
from howler.utils.str_utils import sanitize_lucene_query
|
|
10
|
+
|
|
11
|
+
OPERATION_ID = "remove_from_bundle"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def execute(query: str, bundle_id: Optional[str] = None, **kwargs):
|
|
15
|
+
"""Remove a set of hits matching the query from the specified bundle (deprecated — uses cases).
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
query (str): The query containing the matching hits
|
|
19
|
+
bundle_id (str): The ``howler.id`` of the bundle to remove the hits from.
|
|
20
|
+
"""
|
|
21
|
+
report = []
|
|
22
|
+
|
|
23
|
+
if not bundle_id:
|
|
24
|
+
return [
|
|
25
|
+
{
|
|
26
|
+
"query": query,
|
|
27
|
+
"outcome": "error",
|
|
28
|
+
"title": "Invalid Bundle ID",
|
|
29
|
+
"message": "Bundle ID cannot be empty.",
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
case_id = bundle_compat_service.find_case_for_bundle(bundle_id)
|
|
35
|
+
if case_id is None:
|
|
36
|
+
report.append(
|
|
37
|
+
{
|
|
38
|
+
"query": query,
|
|
39
|
+
"outcome": "error",
|
|
40
|
+
"title": "Invalid Bundle",
|
|
41
|
+
"message": f"Either a hit with ID {bundle_id} does not exist, or it has no associated case.",
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
return report
|
|
45
|
+
|
|
46
|
+
ds = datastore()
|
|
47
|
+
matching_hits = ds.hit.search(query, rows=1000)["items"]
|
|
48
|
+
|
|
49
|
+
if not matching_hits:
|
|
50
|
+
report.append(
|
|
51
|
+
{
|
|
52
|
+
"query": query,
|
|
53
|
+
"outcome": "skipped",
|
|
54
|
+
"title": "No Matching Hits",
|
|
55
|
+
"message": "There were no hits matching this query.",
|
|
56
|
+
}
|
|
57
|
+
)
|
|
58
|
+
return report
|
|
59
|
+
|
|
60
|
+
# Get the case to check which hits are actually in it
|
|
61
|
+
case = ds.case.get(case_id)
|
|
62
|
+
if case is None:
|
|
63
|
+
report.append(
|
|
64
|
+
{
|
|
65
|
+
"query": query,
|
|
66
|
+
"outcome": "error",
|
|
67
|
+
"title": "Case Not Found",
|
|
68
|
+
"message": f"Associated case {case_id} no longer exists.",
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
return report
|
|
72
|
+
|
|
73
|
+
case_item_values = {item.value for item in case.items}
|
|
74
|
+
values_to_remove = [h.howler.id for h in matching_hits if h.howler.id in case_item_values]
|
|
75
|
+
skipped_ids = [h.howler.id for h in matching_hits if h.howler.id not in case_item_values]
|
|
76
|
+
|
|
77
|
+
if skipped_ids:
|
|
78
|
+
report.append(
|
|
79
|
+
{
|
|
80
|
+
"query": f"howler.id:({' OR '.join(sanitize_lucene_query(h) for h in skipped_ids)})",
|
|
81
|
+
"outcome": "skipped",
|
|
82
|
+
"title": "Skipped Hits Not in Bundle",
|
|
83
|
+
"message": "These hits are not in the bundle.",
|
|
84
|
+
}
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
if not values_to_remove:
|
|
88
|
+
report.append(
|
|
89
|
+
{
|
|
90
|
+
"query": query,
|
|
91
|
+
"outcome": "skipped",
|
|
92
|
+
"title": "No Matching Hits",
|
|
93
|
+
"message": "None of the matching hits were found in the bundle.",
|
|
94
|
+
}
|
|
95
|
+
)
|
|
96
|
+
return report
|
|
97
|
+
|
|
98
|
+
case_service.remove_case_items(case_id, values_to_remove)
|
|
99
|
+
|
|
100
|
+
report.append(
|
|
101
|
+
{
|
|
102
|
+
"query": query,
|
|
103
|
+
"outcome": "success",
|
|
104
|
+
"title": "Executed Successfully",
|
|
105
|
+
"message": f"Matching hits removed from bundle with id {bundle_id}",
|
|
106
|
+
}
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
except NotFoundException as e:
|
|
110
|
+
report.append(
|
|
111
|
+
{
|
|
112
|
+
"query": query,
|
|
113
|
+
"outcome": "error",
|
|
114
|
+
"title": "Failed to Execute",
|
|
115
|
+
"message": str(e),
|
|
116
|
+
}
|
|
117
|
+
)
|
|
118
|
+
except Exception as e:
|
|
119
|
+
report.append(
|
|
120
|
+
{
|
|
121
|
+
"query": query,
|
|
122
|
+
"outcome": "error",
|
|
123
|
+
"title": "Failed to Execute",
|
|
124
|
+
"message": f"Unknown exception occurred: {str(e)}",
|
|
125
|
+
}
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
return report
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def specification():
|
|
132
|
+
"""Specify various properties of the action, such as title, descriptions, permissions and input steps."""
|
|
133
|
+
return {
|
|
134
|
+
"id": OPERATION_ID,
|
|
135
|
+
"title": "Remove from Bundle (Deprecated)",
|
|
136
|
+
"priority": 5,
|
|
137
|
+
"i18nKey": f"operations.{OPERATION_ID}",
|
|
138
|
+
"description": {
|
|
139
|
+
"short": "Remove a set of hits from a bundle (deprecated — uses cases)",
|
|
140
|
+
"long": execute.__doc__,
|
|
141
|
+
},
|
|
142
|
+
"roles": ["automation_basic"],
|
|
143
|
+
"steps": [
|
|
144
|
+
{
|
|
145
|
+
"args": {"bundle_id": []},
|
|
146
|
+
"options": {},
|
|
147
|
+
}
|
|
148
|
+
],
|
|
149
|
+
"triggers": VALID_TRIGGERS,
|
|
150
|
+
}
|
|
@@ -10,6 +10,7 @@ from howler import odm
|
|
|
10
10
|
from howler.common.loader import APP_NAME
|
|
11
11
|
from howler.common.logging import get_logger, log_with_traceback
|
|
12
12
|
from howler.config import QUOTA_TRACKER, get_version
|
|
13
|
+
from howler.utils.constants import TESTING
|
|
13
14
|
from howler.utils.str_utils import safe_str
|
|
14
15
|
|
|
15
16
|
API_PREFIX = "/api"
|
|
@@ -73,7 +74,8 @@ def _make_api_response(
|
|
|
73
74
|
resp.set_cookie(k, v, secure=True, httponly=True, samesite="Lax")
|
|
74
75
|
|
|
75
76
|
RAW_API_COUNTER.labels(request.method, str(request.url_rule), status_code).inc()
|
|
76
|
-
|
|
77
|
+
if not TESTING:
|
|
78
|
+
logger.info("%s %s - %s", request.method, request.path, status_code)
|
|
77
79
|
|
|
78
80
|
return resp
|
|
79
81
|
|
|
@@ -4,6 +4,7 @@ import os
|
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
6
|
from flask import Blueprint, request
|
|
7
|
+
from opentelemetry import trace
|
|
7
8
|
|
|
8
9
|
import howler.services.event_service as event_service
|
|
9
10
|
from howler.api import ok, unauthorized
|
|
@@ -21,10 +22,12 @@ socket_api = Blueprint("socket", "socket", url_prefix="/socket/v1")
|
|
|
21
22
|
socket_api._doc = "Endpoints concerning websocket connectivity between the client and server" # type: ignore
|
|
22
23
|
|
|
23
24
|
logger = get_logger(__file__)
|
|
25
|
+
tracer = trace.get_tracer(__name__)
|
|
24
26
|
|
|
25
27
|
hit_helper = OdmHelper(Hit)
|
|
26
28
|
|
|
27
29
|
|
|
30
|
+
@tracer.start_as_current_span(f"{__name__}.emit")
|
|
28
31
|
@socket_api.route("/emit/<event>", methods=["POST"])
|
|
29
32
|
def emit(event: str):
|
|
30
33
|
"""Emit an event to all listening websockets"""
|
|
@@ -46,6 +49,7 @@ def emit(event: str):
|
|
|
46
49
|
return ok()
|
|
47
50
|
|
|
48
51
|
|
|
52
|
+
@tracer.start_as_current_span(f"{__name__}.connect")
|
|
49
53
|
@socket_api.route("/connect", websocket=True) # type: ignore
|
|
50
54
|
@websocket_auth(required_priv=["R"])
|
|
51
55
|
def connect(ws: Server, *args: Any, ws_id: str, **kwargs):
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import sys
|
|
2
1
|
import time
|
|
3
2
|
from typing import Callable, Optional
|
|
4
3
|
|
|
5
4
|
import requests
|
|
6
|
-
from elasticapm.traces import capture_span
|
|
7
5
|
from flask import request
|
|
8
6
|
|
|
9
7
|
from howler.api import bad_gateway, make_subapi_blueprint, ok
|
|
@@ -13,6 +11,7 @@ from howler.common.swagger import generate_swagger_docs
|
|
|
13
11
|
from howler.config import cache, config
|
|
14
12
|
from howler.plugins import get_plugins
|
|
15
13
|
from howler.security import api_login
|
|
14
|
+
from howler.utils.constants import TESTING
|
|
16
15
|
|
|
17
16
|
SUB_API = "clue"
|
|
18
17
|
clue_api = make_subapi_blueprint(SUB_API, api_version=1)
|
|
@@ -23,7 +22,7 @@ logger = get_logger(__file__)
|
|
|
23
22
|
|
|
24
23
|
def skip_cache(*args):
|
|
25
24
|
"Function to skip cache in testing mode"
|
|
26
|
-
return
|
|
25
|
+
return TESTING
|
|
27
26
|
|
|
28
27
|
|
|
29
28
|
@cache.memoize(15 * 60, unless=skip_cache)
|
|
@@ -74,22 +73,21 @@ def proxy_to_clue(path, **kwargs):
|
|
|
74
73
|
clue_token = get_token(auth_token)
|
|
75
74
|
|
|
76
75
|
start = time.perf_counter()
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
)
|
|
76
|
+
if request.method.lower() == "get":
|
|
77
|
+
response = requests.get(
|
|
78
|
+
f"{config.core.clue.url}/{path}",
|
|
79
|
+
headers={"Authorization": f"Bearer {clue_token}", "Accept": "application/json"},
|
|
80
|
+
params=request.args.to_dict(),
|
|
81
|
+
timeout=5 * 60,
|
|
82
|
+
)
|
|
83
|
+
else:
|
|
84
|
+
response = requests.post(
|
|
85
|
+
f"{config.core.clue.url}/{path}",
|
|
86
|
+
json=request.json,
|
|
87
|
+
headers={"Authorization": f"Bearer {clue_token}", "Accept": "application/json"},
|
|
88
|
+
params=request.args.to_dict(),
|
|
89
|
+
timeout=5 * 60,
|
|
90
|
+
)
|
|
93
91
|
|
|
94
92
|
logger.debug("Request to clue completed in %s ms", round(time.perf_counter() - start))
|
|
95
93
|
|
|
@@ -17,7 +17,7 @@ from howler.api import (
|
|
|
17
17
|
ok,
|
|
18
18
|
)
|
|
19
19
|
from howler.api.v1.utils.etag import add_etag
|
|
20
|
-
from howler.common.exceptions import HowlerException, HowlerValueError, InvalidDataException
|
|
20
|
+
from howler.common.exceptions import HowlerException, HowlerValueError, InvalidDataException, NotFoundException
|
|
21
21
|
from howler.common.loader import datastore
|
|
22
22
|
from howler.common.logging import get_logger
|
|
23
23
|
from howler.common.swagger import generate_swagger_docs
|
|
@@ -976,3 +976,136 @@ def remove_react_comment(id: str, comment_id: str, user: dict[str, Any], **kwarg
|
|
|
976
976
|
new_hit, version = hit_service.save_hit(hit, version=kwargs.get("server_version"))
|
|
977
977
|
|
|
978
978
|
return ok(new_hit), version
|
|
979
|
+
|
|
980
|
+
|
|
981
|
+
# ---------------------------------------------------------------------------
|
|
982
|
+
# Deprecated bundle shim endpoints
|
|
983
|
+
# ---------------------------------------------------------------------------
|
|
984
|
+
|
|
985
|
+
|
|
986
|
+
def _deprecation_headers(response):
|
|
987
|
+
"""Inject deprecation headers into a Flask Response."""
|
|
988
|
+
response.headers["Deprecation"] = "true"
|
|
989
|
+
response.headers["Sunset"] = "2027-01-01"
|
|
990
|
+
return response
|
|
991
|
+
|
|
992
|
+
|
|
993
|
+
@generate_swagger_docs()
|
|
994
|
+
@hit_api.route("/bundle", methods=["POST"])
|
|
995
|
+
@api_login(audit=False, required_priv=["W"])
|
|
996
|
+
def create_bundle(user: User, **kwargs):
|
|
997
|
+
"""Create a new bundle (deprecated — creates a case instead).
|
|
998
|
+
|
|
999
|
+
Variables:
|
|
1000
|
+
None
|
|
1001
|
+
|
|
1002
|
+
Arguments:
|
|
1003
|
+
None
|
|
1004
|
+
|
|
1005
|
+
Data Block:
|
|
1006
|
+
{
|
|
1007
|
+
"bundle": {
|
|
1008
|
+
...hit # A howler hit that will be used as a template for this new bundle
|
|
1009
|
+
},
|
|
1010
|
+
"hits": [...ids] # A list of existing howler hits to add as children to the new bundle
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
Result Example:
|
|
1014
|
+
{
|
|
1015
|
+
...hit # The created bundle (synthesized from the underlying case)
|
|
1016
|
+
}
|
|
1017
|
+
"""
|
|
1018
|
+
from howler.services import bundle_compat_service
|
|
1019
|
+
|
|
1020
|
+
data = request.json
|
|
1021
|
+
if not isinstance(data, dict):
|
|
1022
|
+
return bad_request(err="Invalid data format")
|
|
1023
|
+
|
|
1024
|
+
bundle_hit: Optional[dict[str, Any]] = data.get("bundle")
|
|
1025
|
+
if bundle_hit is None:
|
|
1026
|
+
return bad_request(err="You did not provide a bundle hit.")
|
|
1027
|
+
|
|
1028
|
+
child_hits: list[str] = data.get("hits", [])
|
|
1029
|
+
|
|
1030
|
+
try:
|
|
1031
|
+
result = bundle_compat_service.create_bundle(bundle_hit, child_hits, user=user.uname)
|
|
1032
|
+
return _deprecation_headers(created(result))
|
|
1033
|
+
except HowlerException as e:
|
|
1034
|
+
return bad_request(err=str(e))
|
|
1035
|
+
|
|
1036
|
+
|
|
1037
|
+
@generate_swagger_docs()
|
|
1038
|
+
@hit_api.route("/bundle/<id>", methods=["PUT"])
|
|
1039
|
+
@api_login(audit=False, required_priv=["W"])
|
|
1040
|
+
def update_bundle(id, **kwargs):
|
|
1041
|
+
"""Add hits to a bundle (deprecated — adds items to the underlying case).
|
|
1042
|
+
|
|
1043
|
+
Variables:
|
|
1044
|
+
id => The ID of the bundle to update
|
|
1045
|
+
|
|
1046
|
+
Arguments:
|
|
1047
|
+
None
|
|
1048
|
+
|
|
1049
|
+
Data Block:
|
|
1050
|
+
[
|
|
1051
|
+
...ids
|
|
1052
|
+
]
|
|
1053
|
+
|
|
1054
|
+
Result Example:
|
|
1055
|
+
{
|
|
1056
|
+
...hit # The updated bundle (synthesized from the underlying case)
|
|
1057
|
+
}
|
|
1058
|
+
"""
|
|
1059
|
+
from howler.services import bundle_compat_service
|
|
1060
|
+
from howler.services.bundle_compat_service import BundleConflictException
|
|
1061
|
+
|
|
1062
|
+
hit_ids = request.json
|
|
1063
|
+
if not isinstance(hit_ids, list):
|
|
1064
|
+
return bad_request(err="Invalid data format")
|
|
1065
|
+
|
|
1066
|
+
try:
|
|
1067
|
+
result = bundle_compat_service.add_to_bundle(id, hit_ids)
|
|
1068
|
+
return _deprecation_headers(ok(result))
|
|
1069
|
+
except BundleConflictException as e:
|
|
1070
|
+
return conflict(err=str(e))
|
|
1071
|
+
except NotFoundException as e:
|
|
1072
|
+
return not_found(err=str(e))
|
|
1073
|
+
except HowlerException as e:
|
|
1074
|
+
return bad_request(err=str(e))
|
|
1075
|
+
|
|
1076
|
+
|
|
1077
|
+
@generate_swagger_docs()
|
|
1078
|
+
@hit_api.route("/bundle/<id>", methods=["DELETE"])
|
|
1079
|
+
@api_login(audit=False, required_priv=["W"])
|
|
1080
|
+
def remove_bundle_children(id, **kwargs):
|
|
1081
|
+
"""Remove hits from a bundle (deprecated — removes items from the underlying case).
|
|
1082
|
+
|
|
1083
|
+
Variables:
|
|
1084
|
+
id => The ID of the bundle to update
|
|
1085
|
+
|
|
1086
|
+
Arguments:
|
|
1087
|
+
None
|
|
1088
|
+
|
|
1089
|
+
Data Block:
|
|
1090
|
+
[
|
|
1091
|
+
...ids OR '*' # A list of ids to remove, or a single '*' to remove all
|
|
1092
|
+
]
|
|
1093
|
+
|
|
1094
|
+
Result Example:
|
|
1095
|
+
{
|
|
1096
|
+
...hit # The updated hit (synthesized from the underlying case)
|
|
1097
|
+
}
|
|
1098
|
+
"""
|
|
1099
|
+
from howler.services import bundle_compat_service
|
|
1100
|
+
|
|
1101
|
+
hit_ids = request.json
|
|
1102
|
+
if not isinstance(hit_ids, list):
|
|
1103
|
+
return bad_request(err="Invalid data format")
|
|
1104
|
+
|
|
1105
|
+
try:
|
|
1106
|
+
result = bundle_compat_service.remove_from_bundle(id, hit_ids)
|
|
1107
|
+
return _deprecation_headers(ok(result))
|
|
1108
|
+
except NotFoundException as e:
|
|
1109
|
+
return not_found(err=str(e))
|
|
1110
|
+
except HowlerException as e:
|
|
1111
|
+
return bad_request(err=str(e))
|
|
@@ -556,11 +556,13 @@ def count(index, **kwargs):
|
|
|
556
556
|
params.update({"access_control": user["access_control"]})
|
|
557
557
|
|
|
558
558
|
query = req_data.get("query", None)
|
|
559
|
+
filters = req_data.get("filters", [])
|
|
560
|
+
|
|
559
561
|
if not query:
|
|
560
562
|
return bad_request(err="There was no search query.")
|
|
561
563
|
|
|
562
564
|
try:
|
|
563
|
-
return ok(collection().count(query, **params))
|
|
565
|
+
return ok(collection().count(query, filters=filters, **params))
|
|
564
566
|
except (SearchException, BadRequestError) as e:
|
|
565
567
|
return bad_request(err=f"SearchException: {e}")
|
|
566
568
|
|
|
@@ -77,8 +77,11 @@ def create_one_or_many_hits(tool_name: str, user: User, **kwargs): # noqa: C901
|
|
|
77
77
|
warnings = []
|
|
78
78
|
# Validate field_map targets
|
|
79
79
|
hit_fields = Hit.flat_fields()
|
|
80
|
+
_bundle_compat_fields = {"howler.is_bundle", "howler.hits", "howler.bundle_size", "howler.bundles"}
|
|
80
81
|
for targets in field_map.values():
|
|
81
82
|
for target in targets:
|
|
83
|
+
if target in _bundle_compat_fields:
|
|
84
|
+
continue
|
|
82
85
|
# This is checking to see if the target matches one of two cases:
|
|
83
86
|
# Simple fields - hit.obj.key of type str (should match)
|
|
84
87
|
# Compound fields - hit.obj of type dict (should also match)
|
|
@@ -93,7 +96,9 @@ def create_one_or_many_hits(tool_name: str, user: User, **kwargs): # noqa: C901
|
|
|
93
96
|
return bad_request(err=warning)
|
|
94
97
|
|
|
95
98
|
out: list[dict[str, Any]] = []
|
|
96
|
-
odms = []
|
|
99
|
+
odms: list[Hit] = []
|
|
100
|
+
bundle_raw: dict[str, Any] | None = None
|
|
101
|
+
bundle_index: int | None = None
|
|
97
102
|
for hit in hits:
|
|
98
103
|
cur_id = get_random_id()
|
|
99
104
|
cur_time = now_as_iso()
|
|
@@ -135,8 +140,19 @@ def create_one_or_many_hits(tool_name: str, user: User, **kwargs): # noqa: C901
|
|
|
135
140
|
obj[target] = _val
|
|
136
141
|
|
|
137
142
|
try:
|
|
143
|
+
is_bundle = obj.pop("howler.is_bundle", False)
|
|
144
|
+
obj.pop("howler.hits", None)
|
|
145
|
+
obj.pop("howler.bundle_size", None)
|
|
146
|
+
obj.pop("howler.bundles", None)
|
|
147
|
+
|
|
138
148
|
odm, warns = hit_service.convert_hit(obj, unique=True, ignore_extra_values=ignore_extra_values)
|
|
139
149
|
|
|
150
|
+
if is_bundle:
|
|
151
|
+
if bundle_raw is not None:
|
|
152
|
+
return bad_request(err="You can only specify one bundle hit!")
|
|
153
|
+
bundle_raw = obj
|
|
154
|
+
bundle_index = len(odms)
|
|
155
|
+
|
|
140
156
|
odms.append(odm)
|
|
141
157
|
|
|
142
158
|
out.append(
|
|
@@ -155,12 +171,36 @@ def create_one_or_many_hits(tool_name: str, user: User, **kwargs): # noqa: C901
|
|
|
155
171
|
if any([obj["error"] for obj in out]):
|
|
156
172
|
return bad_request(out, warnings=warnings, err="No valid hits were provided")
|
|
157
173
|
else:
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
174
|
+
if bundle_index is not None:
|
|
175
|
+
# Route through bundle compat service → creates a case
|
|
176
|
+
from howler.services import bundle_compat_service
|
|
177
|
+
|
|
178
|
+
bundle_odm = odms[bundle_index]
|
|
179
|
+
child_odms = [odm for i, odm in enumerate(odms) if i != bundle_index]
|
|
180
|
+
|
|
181
|
+
for odm in child_odms:
|
|
182
|
+
hit_service.create_hit(odm.howler.id, odm, user=user.uname)
|
|
183
|
+
analytic_service.save_from_hit(odm, user)
|
|
184
|
+
|
|
185
|
+
child_ids = [odm.howler.id for odm in child_odms]
|
|
186
|
+
bundle_data = bundle_odm.as_primitives()
|
|
187
|
+
result = bundle_compat_service.create_bundle(bundle_data, child_ids, user=user.uname)
|
|
188
|
+
warnings.append(bundle_compat_service.DEPRECATION_MESSAGE)
|
|
189
|
+
|
|
190
|
+
# Replace the bundle entry in the output with the created bundle id
|
|
191
|
+
for entry in out:
|
|
192
|
+
if entry.get("id") == bundle_odm.howler.id:
|
|
193
|
+
entry["id"] = result["howler"]["id"]
|
|
194
|
+
entry["_case_id"] = result.get("_case_id")
|
|
195
|
+
else:
|
|
196
|
+
for odm in odms:
|
|
197
|
+
hit_service.create_hit(odm.howler.id, odm, user=user.uname)
|
|
198
|
+
analytic_service.save_from_hit(odm, user)
|
|
161
199
|
|
|
162
200
|
datastore().hit.commit()
|
|
163
201
|
|
|
164
|
-
action_service.bulk_execute_on_query(
|
|
202
|
+
action_service.bulk_execute_on_query(
|
|
203
|
+
f"howler.id:({' OR '.join(entry['id'] for entry in out if entry['id'])})", user=user
|
|
204
|
+
)
|
|
165
205
|
|
|
166
206
|
return created(out, warnings=warnings)
|