howler-api 4.0.0.dev1038__tar.gz → 4.0.0.dev1056__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.dev1038 → howler_api-4.0.0.dev1056}/PKG-INFO +2 -2
- howler_api-4.0.0.dev1056/howler/actions/add_to_bundle.py +148 -0
- howler_api-4.0.0.dev1056/howler/actions/add_to_case.py +136 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/actions/demote.py +2 -2
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/actions/remove_from_bundle.py +64 -46
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/actions/transition.py +3 -5
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/api/__init__.py +2 -1
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/api/socket.py +36 -6
- howler_api-4.0.0.dev1056/howler/api/v1/__init__.py +44 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/api/v1/action.py +1 -2
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/api/v1/analytic.py +17 -106
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/api/v1/hit.py +50 -126
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/api/v1/tool.py +47 -22
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/api/v1/user.py +1 -1
- howler_api-4.0.0.dev1056/howler/api/v1/utils/etag.py +106 -0
- howler_api-4.0.0.dev1056/howler/api/v2/__init__.py +44 -0
- howler_api-4.0.0.dev1056/howler/api/v2/case.py +437 -0
- howler_api-4.0.0.dev1056/howler/api/v2/ingest.py +371 -0
- howler_api-4.0.0.dev1056/howler/api/v2/search.py +340 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/app.py +15 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/common/loader.py +4 -5
- howler_api-4.0.0.dev1056/howler/cronjobs/correlation.py +36 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/datastore/collection.py +40 -25
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/datastore/howler_store.py +29 -14
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/datastore/types.py +1 -6
- howler_api-4.0.0.dev1056/howler/healthz.py +47 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/helper/discover.py +12 -8
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/helper/hit.py +4 -4
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/helper/search.py +13 -3
- howler_api-4.0.0.dev1056/howler/odm/constants.py +20 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/odm/helper.py +113 -7
- howler_api-4.0.0.dev1056/howler/odm/mixins.py +97 -0
- howler_api-4.0.0.dev1056/howler/odm/models/case.py +218 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/odm/models/config.py +14 -0
- howler_api-4.0.0.dev1056/howler/odm/models/hit.py +31 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/odm/models/howler_data.py +3 -35
- howler_api-4.0.0.dev1056/howler/odm/models/observable.py +126 -0
- howler_api-4.0.0.dev1038/howler/odm/models/hit.py → howler_api-4.0.0.dev1056/howler/odm/models/record.py +12 -25
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/odm/models/view.py +8 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/odm/random_data.py +372 -50
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/remote/datatypes/__init__.py +3 -2
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/remote/datatypes/queues/comms.py +2 -1
- howler_api-4.0.0.dev1056/howler/services/bundle_compat_service.py +281 -0
- howler_api-4.0.0.dev1056/howler/services/case_service.py +905 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/services/config_service.py +3 -3
- howler_api-4.0.0.dev1056/howler/services/correlation_service.py +168 -0
- howler_api-4.0.0.dev1038/howler/api/v1/__init__.py → howler_api-4.0.0.dev1056/howler/services/docs_service.py +37 -45
- howler_api-4.0.0.dev1056/howler/services/event_service.py +134 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/services/hit_service.py +83 -170
- howler_api-4.0.0.dev1056/howler/services/observable_service.py +142 -0
- howler_api-4.0.0.dev1056/howler/services/search_service.py +229 -0
- howler_api-4.0.0.dev1056/howler/services/viewer_service.py +43 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/utils/socket_utils.py +4 -25
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/pyproject.toml +5 -5
- howler_api-4.0.0.dev1038/howler/actions/add_to_bundle.py +0 -171
- howler_api-4.0.0.dev1038/howler/api/v1/utils/etag.py +0 -84
- howler_api-4.0.0.dev1038/howler/cronjobs/rules.py +0 -279
- howler_api-4.0.0.dev1038/howler/healthz.py +0 -91
- howler_api-4.0.0.dev1038/howler/services/event_service.py +0 -96
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/README.md +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/__init__.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/actions/__init__.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/actions/add_label.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/actions/change_field.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/actions/example_plugin.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/actions/prioritization.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/actions/promote.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/actions/remove_label.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/api/base.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/api/v1/auth.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/api/v1/clue.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/api/v1/configs.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/api/v1/dossier.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/api/v1/help.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/api/v1/notebook.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/api/v1/overview.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/api/v1/search.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/api/v1/template.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/api/v1/utils/__init__.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/api/v1/utils/params.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/api/v1/view.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/common/README.md +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/common/__init__.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/common/classification.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/common/classification.yml +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/common/exceptions.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/common/logging/__init__.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/common/logging/audit.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/common/logging/format.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/common/net.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/common/net_static.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/common/random_user.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/common/swagger.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/config.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/cronjobs/__init__.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/cronjobs/action_queue_worker.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/cronjobs/retention.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/cronjobs/view_cleanup.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/datastore/README.md +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/datastore/__init__.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/datastore/bulk.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/datastore/constants.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/datastore/exceptions.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/datastore/migrations/fix_process.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/datastore/operations.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/datastore/schemas.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/datastore/store.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/datastore/support/__init__.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/datastore/support/build.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/datastore/support/schemas.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/error.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/external/README.md +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/external/__init__.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/external/generate_mitre.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/external/generate_sigma_rules.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/external/generate_tlds.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/external/reindex_data.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/external/wipe_databases.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/gunicorn_config.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/helper/__init__.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/helper/azure.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/helper/oauth.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/helper/workflow.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/helper/ws.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/odm/README.md +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/odm/__init__.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/odm/base.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/odm/charter.txt +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/odm/howler_enum.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/odm/models/__init__.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/odm/models/action.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/odm/models/analytic.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/odm/models/assemblyline.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/odm/models/aws.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/odm/models/azure.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/odm/models/cbs.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/odm/models/clue.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/odm/models/dossier.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/odm/models/ecs/__init__.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/odm/models/ecs/agent.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/odm/models/ecs/autonomous_system.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/odm/models/ecs/client.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/odm/models/ecs/cloud.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/odm/models/ecs/code_signature.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/odm/models/ecs/container.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/odm/models/ecs/dns.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/odm/models/ecs/egress.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/odm/models/ecs/elf.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/odm/models/ecs/email.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/odm/models/ecs/error.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/odm/models/ecs/event.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/odm/models/ecs/faas.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/odm/models/ecs/file.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/odm/models/ecs/geo.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/odm/models/ecs/group.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/odm/models/ecs/hash.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/odm/models/ecs/host.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/odm/models/ecs/http.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/odm/models/ecs/ingress.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/odm/models/ecs/interface.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/odm/models/ecs/network.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/odm/models/ecs/observer.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/odm/models/ecs/organization.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/odm/models/ecs/os.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/odm/models/ecs/pe.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/odm/models/ecs/process.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/odm/models/ecs/registry.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/odm/models/ecs/related.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/odm/models/ecs/rule.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/odm/models/ecs/server.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/odm/models/ecs/threat.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/odm/models/ecs/tls.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/odm/models/ecs/url.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/odm/models/ecs/user.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/odm/models/ecs/user_agent.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/odm/models/ecs/vulnerability.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/odm/models/gcp.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/odm/models/lead.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/odm/models/localized_label.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/odm/models/overview.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/odm/models/pivot.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/odm/models/template.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/odm/models/user.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/odm/randomizer.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/patched.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/plugins/__init__.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/plugins/config.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/remote/__init__.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/remote/datatypes/README.md +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/remote/datatypes/counters.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/remote/datatypes/events.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/remote/datatypes/hash.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/remote/datatypes/lock.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/remote/datatypes/queues/__init__.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/remote/datatypes/queues/multi.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/remote/datatypes/queues/named.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/remote/datatypes/queues/priority.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/remote/datatypes/set.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/remote/datatypes/user_quota_tracker.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/security/__init__.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/security/socket.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/security/utils.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/services/__init__.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/services/action_service.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/services/analytic_service.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/services/auth_service.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/services/dossier_service.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/services/jwt_service.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/services/lucene_service.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/services/notebook_service.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/services/overview_service.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/services/template_service.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/services/user_service.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/telemetry.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/utils/__init__.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/utils/annotations.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/utils/chunk.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/utils/compat.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/utils/constants.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/utils/dict_utils.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/utils/isotime.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/utils/list_utils.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/utils/lucene.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/utils/path.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/howler/utils/str_utils.py +0 -0
- {howler_api-4.0.0.dev1038 → howler_api-4.0.0.dev1056}/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.dev1056
|
|
4
4
|
Summary: Howler - API server
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: howler,alerting,gc,canada,cse-cst,cse,cst,cyber,cccs
|
|
@@ -29,7 +29,7 @@ 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)
|
|
31
31
|
Requires-Dist: flask-caching (==2.4.0)
|
|
32
|
-
Requires-Dist: gevent (
|
|
32
|
+
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)
|
|
@@ -0,0 +1,148 @@
|
|
|
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.actions import check_hit_limit
|
|
6
|
+
from howler.common.exceptions import NotFoundException
|
|
7
|
+
from howler.common.loader import datastore
|
|
8
|
+
from howler.odm.models.action import VALID_TRIGGERS
|
|
9
|
+
from howler.odm.models.user import User
|
|
10
|
+
from howler.services import bundle_compat_service, case_service
|
|
11
|
+
from howler.utils.str_utils import sanitize_lucene_query
|
|
12
|
+
|
|
13
|
+
OPERATION_ID = "add_to_bundle"
|
|
14
|
+
MAX_HITS_BASIC = 10
|
|
15
|
+
MAX_HITS_ADVANCED = 1000
|
|
16
|
+
SKIP_CENTRAL_LIMIT = True # This operation transforms the query, handles limit check locally
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def execute(query: str, bundle_id: Optional[str] = None, user: Optional[User] = None, **kwargs): # noqa: C901
|
|
20
|
+
"""Add a set of hits matching the query to the specified bundle (deprecated — uses cases).
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
query (str): The query containing the matching hits
|
|
24
|
+
bundle_id (str): The ``howler.id`` of the bundle to add the hits to.
|
|
25
|
+
"""
|
|
26
|
+
report = []
|
|
27
|
+
|
|
28
|
+
if not bundle_id:
|
|
29
|
+
return [
|
|
30
|
+
{
|
|
31
|
+
"query": query,
|
|
32
|
+
"outcome": "error",
|
|
33
|
+
"title": "Invalid Bundle ID",
|
|
34
|
+
"message": "Bundle ID cannot be empty.",
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
case_id = bundle_compat_service.find_case_for_bundle(bundle_id)
|
|
40
|
+
if case_id is None:
|
|
41
|
+
report.append(
|
|
42
|
+
{
|
|
43
|
+
"query": query,
|
|
44
|
+
"outcome": "error",
|
|
45
|
+
"title": "Invalid Bundle",
|
|
46
|
+
"message": f"Either a hit with ID {bundle_id} does not exist, or it has no associated case.",
|
|
47
|
+
}
|
|
48
|
+
)
|
|
49
|
+
return report
|
|
50
|
+
|
|
51
|
+
ds = datastore()
|
|
52
|
+
|
|
53
|
+
# Check hit limit against the query before searching
|
|
54
|
+
if user:
|
|
55
|
+
limit_error = check_hit_limit(query, user, MAX_HITS_BASIC, MAX_HITS_ADVANCED)
|
|
56
|
+
if limit_error:
|
|
57
|
+
return [limit_error]
|
|
58
|
+
|
|
59
|
+
matching_hits = ds.hit.search(query, rows=MAX_HITS_ADVANCED)["items"]
|
|
60
|
+
|
|
61
|
+
if not matching_hits:
|
|
62
|
+
report.append(
|
|
63
|
+
{
|
|
64
|
+
"query": query,
|
|
65
|
+
"outcome": "skipped",
|
|
66
|
+
"title": "No Matching Hits",
|
|
67
|
+
"message": "There were no hits matching this query.",
|
|
68
|
+
}
|
|
69
|
+
)
|
|
70
|
+
return report
|
|
71
|
+
|
|
72
|
+
added = []
|
|
73
|
+
skipped = []
|
|
74
|
+
for hit in matching_hits:
|
|
75
|
+
child_label = f"hits/{hit.howler.analytic} ({hit.howler.id})"
|
|
76
|
+
try:
|
|
77
|
+
case_service.append_case_item(
|
|
78
|
+
case_id,
|
|
79
|
+
item_type="hit",
|
|
80
|
+
item_value=hit.howler.id,
|
|
81
|
+
item_path=child_label,
|
|
82
|
+
)
|
|
83
|
+
added.append(hit.howler.id)
|
|
84
|
+
except Exception:
|
|
85
|
+
skipped.append(hit.howler.id)
|
|
86
|
+
|
|
87
|
+
if skipped:
|
|
88
|
+
report.append(
|
|
89
|
+
{
|
|
90
|
+
"query": f"howler.id:({' OR '.join(sanitize_lucene_query(h) for h in skipped)})",
|
|
91
|
+
"outcome": "skipped",
|
|
92
|
+
"title": "Skipped Hits",
|
|
93
|
+
"message": "These hits could not be added (already present or invalid).",
|
|
94
|
+
}
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
if added:
|
|
98
|
+
report.append(
|
|
99
|
+
{
|
|
100
|
+
"query": f"howler.id:({' OR '.join(sanitize_lucene_query(h) for h in added)})",
|
|
101
|
+
"outcome": "success",
|
|
102
|
+
"title": "Executed Successfully",
|
|
103
|
+
"message": "The specified bundle has had all matching hits added.",
|
|
104
|
+
}
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
except NotFoundException as e:
|
|
108
|
+
report.append(
|
|
109
|
+
{
|
|
110
|
+
"query": query,
|
|
111
|
+
"outcome": "error",
|
|
112
|
+
"title": "Failed to Execute",
|
|
113
|
+
"message": str(e),
|
|
114
|
+
}
|
|
115
|
+
)
|
|
116
|
+
except Exception as e:
|
|
117
|
+
report.append(
|
|
118
|
+
{
|
|
119
|
+
"query": query,
|
|
120
|
+
"outcome": "error",
|
|
121
|
+
"title": "Failed to Execute",
|
|
122
|
+
"message": f"Unknown exception occurred: {str(e)}",
|
|
123
|
+
}
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
return report
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def specification():
|
|
130
|
+
"""Specify various properties of the action, such as title, descriptions, permissions and input steps."""
|
|
131
|
+
return {
|
|
132
|
+
"id": OPERATION_ID,
|
|
133
|
+
"title": "Add to Bundle (Deprecated)",
|
|
134
|
+
"priority": 6,
|
|
135
|
+
"i18nKey": f"operations.{OPERATION_ID}",
|
|
136
|
+
"description": {
|
|
137
|
+
"short": "Add a set of hits to a bundle (deprecated — uses cases)",
|
|
138
|
+
"long": execute.__doc__,
|
|
139
|
+
},
|
|
140
|
+
"roles": ["automation_basic", "actionrunner_basic"],
|
|
141
|
+
"steps": [
|
|
142
|
+
{
|
|
143
|
+
"args": {"bundle_id": []},
|
|
144
|
+
"options": {},
|
|
145
|
+
}
|
|
146
|
+
],
|
|
147
|
+
"triggers": VALID_TRIGGERS,
|
|
148
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
import chevron
|
|
4
|
+
|
|
5
|
+
from howler.common.exceptions import InvalidDataException, NotFoundException
|
|
6
|
+
from howler.common.loader import datastore
|
|
7
|
+
from howler.odm.models.action import VALID_TRIGGERS
|
|
8
|
+
from howler.services import case_service
|
|
9
|
+
|
|
10
|
+
OPERATION_ID = "add_to_case"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def execute(
|
|
14
|
+
query: str,
|
|
15
|
+
case_id: Optional[str] = None,
|
|
16
|
+
path: str = "related",
|
|
17
|
+
title_template: str = "{{howler.analytic}} ({{howler.id}})",
|
|
18
|
+
**kwargs,
|
|
19
|
+
):
|
|
20
|
+
"""Add matching alerts to a given case.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
query (str): The query on which to apply this automation.
|
|
24
|
+
case_id (str): The ID of the case to add the alerts to.
|
|
25
|
+
path (str): The path within the case at which to place the alerts. Defaults to "related".
|
|
26
|
+
title_template (str): A Mustache-compatible template string used to generate each item's
|
|
27
|
+
path suffix (title). The hit's fields are available as template variables.
|
|
28
|
+
Defaults to "{{howler.analytic}} ({{howler.id}})".
|
|
29
|
+
"""
|
|
30
|
+
if not case_id:
|
|
31
|
+
return [
|
|
32
|
+
{
|
|
33
|
+
"query": query,
|
|
34
|
+
"outcome": "error",
|
|
35
|
+
"title": "Missing Case ID",
|
|
36
|
+
"message": "A case_id must be provided.",
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
ds = datastore()
|
|
41
|
+
|
|
42
|
+
if ds.case.get(case_id) is None:
|
|
43
|
+
return [
|
|
44
|
+
{
|
|
45
|
+
"query": query,
|
|
46
|
+
"outcome": "error",
|
|
47
|
+
"title": "Case Not Found",
|
|
48
|
+
"message": f"No case with ID '{case_id}' exists.",
|
|
49
|
+
}
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
hits = ds.hit.search(query, rows=1000)["items"]
|
|
53
|
+
|
|
54
|
+
if not hits:
|
|
55
|
+
return [
|
|
56
|
+
{
|
|
57
|
+
"query": query,
|
|
58
|
+
"outcome": "skipped",
|
|
59
|
+
"title": "No Matching Hits",
|
|
60
|
+
"message": "No hits matched the query, so the action was skipped.",
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
report = []
|
|
65
|
+
skipped = []
|
|
66
|
+
added = []
|
|
67
|
+
|
|
68
|
+
normalized_path = path.rstrip("/")
|
|
69
|
+
|
|
70
|
+
for hit in hits:
|
|
71
|
+
hit_data = hit.as_primitives()
|
|
72
|
+
title = chevron.render(title_template, hit_data)
|
|
73
|
+
item_path = f"{normalized_path}/{title}" if normalized_path else title
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
case_service.append_case_item(
|
|
77
|
+
case_id,
|
|
78
|
+
item_type="hit",
|
|
79
|
+
item_value=hit.howler.id,
|
|
80
|
+
item_path=item_path,
|
|
81
|
+
)
|
|
82
|
+
added.append(hit.howler.id)
|
|
83
|
+
except InvalidDataException as e:
|
|
84
|
+
skipped.append(f"{hit.howler.id}: {e}")
|
|
85
|
+
except NotFoundException as e:
|
|
86
|
+
skipped.append(f"{hit.howler.id}: {e}")
|
|
87
|
+
except Exception as e:
|
|
88
|
+
skipped.append(f"{hit.howler.id}: {e}")
|
|
89
|
+
|
|
90
|
+
if added:
|
|
91
|
+
report.append(
|
|
92
|
+
{
|
|
93
|
+
"query": f"howler.id:({' OR '.join(added)})",
|
|
94
|
+
"outcome": "success",
|
|
95
|
+
"title": "Added to Case",
|
|
96
|
+
"message": f"{len(added)} alert(s) successfully added to case '{case_id}'.",
|
|
97
|
+
}
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
if skipped:
|
|
101
|
+
report.append(
|
|
102
|
+
{
|
|
103
|
+
"query": query,
|
|
104
|
+
"outcome": "skipped",
|
|
105
|
+
"title": "Skipped Alerts",
|
|
106
|
+
"message": f"{len(skipped)} alert(s) could not be added: {'; '.join(skipped)}",
|
|
107
|
+
}
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
return report
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def specification():
|
|
114
|
+
"""Specify various properties of the action, such as title, descriptions, permissions and input steps."""
|
|
115
|
+
return {
|
|
116
|
+
"id": OPERATION_ID,
|
|
117
|
+
"title": "Add to Case",
|
|
118
|
+
"priority": 9,
|
|
119
|
+
"i18nKey": f"operations.{OPERATION_ID}",
|
|
120
|
+
"description": {
|
|
121
|
+
"short": "Add matching alerts to a case",
|
|
122
|
+
"long": execute.__doc__,
|
|
123
|
+
},
|
|
124
|
+
"roles": ["automation_basic"],
|
|
125
|
+
"steps": [
|
|
126
|
+
{
|
|
127
|
+
"args": {
|
|
128
|
+
"case_id": [],
|
|
129
|
+
"path": [],
|
|
130
|
+
"title_template": [],
|
|
131
|
+
},
|
|
132
|
+
"options": {},
|
|
133
|
+
}
|
|
134
|
+
],
|
|
135
|
+
"triggers": VALID_TRIGGERS,
|
|
136
|
+
}
|
|
@@ -9,7 +9,7 @@ from howler.odm.models.howler_data import (
|
|
|
9
9
|
Assessment,
|
|
10
10
|
AssessmentEscalationMap,
|
|
11
11
|
Escalation,
|
|
12
|
-
|
|
12
|
+
Status,
|
|
13
13
|
)
|
|
14
14
|
from howler.odm.models.user import User
|
|
15
15
|
from howler.utils.str_utils import sanitize_lucene_query
|
|
@@ -99,7 +99,7 @@ def execute(
|
|
|
99
99
|
"howler.assignment",
|
|
100
100
|
user.get("uname", "automation") if user else "automation",
|
|
101
101
|
),
|
|
102
|
-
odm_helper.update("howler.status",
|
|
102
|
+
odm_helper.update("howler.status", Status.RESOLVED),
|
|
103
103
|
],
|
|
104
104
|
)
|
|
105
105
|
|
|
@@ -1,29 +1,27 @@
|
|
|
1
|
+
"""Deprecated remove_from_bundle action — delegates to case_service for item removal."""
|
|
2
|
+
|
|
1
3
|
from typing import Optional
|
|
2
4
|
|
|
3
5
|
from howler.actions import check_hit_limit
|
|
4
|
-
from howler.common.exceptions import
|
|
6
|
+
from howler.common.exceptions import NotFoundException
|
|
5
7
|
from howler.common.loader import datastore
|
|
6
|
-
from howler.datastore.operations import OdmHelper
|
|
7
8
|
from howler.odm.models.action import VALID_TRIGGERS
|
|
8
|
-
from howler.odm.models.hit import Hit
|
|
9
9
|
from howler.odm.models.user import User
|
|
10
|
-
from howler.services import
|
|
10
|
+
from howler.services import bundle_compat_service, case_service
|
|
11
11
|
from howler.utils.str_utils import sanitize_lucene_query
|
|
12
12
|
|
|
13
|
-
hit_helper = OdmHelper(Hit)
|
|
14
|
-
|
|
15
13
|
OPERATION_ID = "remove_from_bundle"
|
|
16
14
|
MAX_HITS_BASIC = 10
|
|
17
15
|
MAX_HITS_ADVANCED = 1000
|
|
18
16
|
SKIP_CENTRAL_LIMIT = True # This operation transforms the query, handles limit check locally
|
|
19
17
|
|
|
20
18
|
|
|
21
|
-
def execute(query: str, bundle_id: Optional[str] = None, user: Optional[User] = None, **kwargs):
|
|
22
|
-
"""Remove a set of hits matching the query from the specified bundle.
|
|
19
|
+
def execute(query: str, bundle_id: Optional[str] = None, user: Optional[User] = None, **kwargs): # noqa: C901
|
|
20
|
+
"""Remove a set of hits matching the query from the specified bundle (deprecated — uses cases).
|
|
23
21
|
|
|
24
22
|
Args:
|
|
25
23
|
query (str): The query containing the matching hits
|
|
26
|
-
bundle_id (str): The
|
|
24
|
+
bundle_id (str): The ``howler.id`` of the bundle to remove the hits from.
|
|
27
25
|
"""
|
|
28
26
|
report = []
|
|
29
27
|
|
|
@@ -38,67 +36,78 @@ def execute(query: str, bundle_id: Optional[str] = None, user: Optional[User] =
|
|
|
38
36
|
]
|
|
39
37
|
|
|
40
38
|
try:
|
|
41
|
-
|
|
42
|
-
if
|
|
39
|
+
case_id = bundle_compat_service.find_case_for_bundle(bundle_id)
|
|
40
|
+
if case_id is None:
|
|
43
41
|
report.append(
|
|
44
42
|
{
|
|
45
43
|
"query": query,
|
|
46
44
|
"outcome": "error",
|
|
47
45
|
"title": "Invalid Bundle",
|
|
48
|
-
"message": f"Either a hit with ID {bundle_id} does not exist, or it
|
|
46
|
+
"message": f"Either a hit with ID {bundle_id} does not exist, or it has no associated case.",
|
|
49
47
|
}
|
|
50
48
|
)
|
|
51
49
|
return report
|
|
52
50
|
|
|
53
51
|
ds = datastore()
|
|
54
52
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
53
|
+
# Check hit limit against the query before searching
|
|
54
|
+
if user:
|
|
55
|
+
limit_error = check_hit_limit(query, user, MAX_HITS_BASIC, MAX_HITS_ADVANCED)
|
|
56
|
+
if limit_error:
|
|
57
|
+
return [limit_error]
|
|
58
|
+
|
|
59
|
+
matching_hits = ds.hit.search(query, rows=MAX_HITS_ADVANCED)["items"]
|
|
59
60
|
|
|
60
|
-
if
|
|
61
|
+
if not matching_hits:
|
|
61
62
|
report.append(
|
|
62
63
|
{
|
|
63
|
-
"query":
|
|
64
|
+
"query": query,
|
|
64
65
|
"outcome": "skipped",
|
|
65
|
-
"title": "
|
|
66
|
-
"message": "
|
|
66
|
+
"title": "No Matching Hits",
|
|
67
|
+
"message": "There were no hits matching this query.",
|
|
67
68
|
}
|
|
68
69
|
)
|
|
70
|
+
return report
|
|
69
71
|
|
|
70
|
-
|
|
72
|
+
# Get the case to check which hits are actually in it
|
|
73
|
+
case = ds.case.get(case_id)
|
|
74
|
+
if case is None:
|
|
75
|
+
report.append(
|
|
76
|
+
{
|
|
77
|
+
"query": query,
|
|
78
|
+
"outcome": "error",
|
|
79
|
+
"title": "Case Not Found",
|
|
80
|
+
"message": f"Associated case {case_id} no longer exists.",
|
|
81
|
+
}
|
|
82
|
+
)
|
|
83
|
+
return report
|
|
71
84
|
|
|
72
|
-
|
|
73
|
-
if
|
|
74
|
-
|
|
75
|
-
if limit_error:
|
|
76
|
-
return [limit_error]
|
|
85
|
+
case_item_values = {item.value for item in case.items}
|
|
86
|
+
values_to_remove = [h.howler.id for h in matching_hits if h.howler.id in case_item_values]
|
|
87
|
+
skipped_ids = [h.howler.id for h in matching_hits if h.howler.id not in case_item_values]
|
|
77
88
|
|
|
78
|
-
|
|
79
|
-
if len(matching_hits) < 1:
|
|
89
|
+
if skipped_ids:
|
|
80
90
|
report.append(
|
|
81
91
|
{
|
|
82
|
-
"query":
|
|
92
|
+
"query": f"howler.id:({' OR '.join(sanitize_lucene_query(h) for h in skipped_ids)})",
|
|
93
|
+
"outcome": "skipped",
|
|
94
|
+
"title": "Skipped Hits Not in Bundle",
|
|
95
|
+
"message": "These hits are not in the bundle.",
|
|
96
|
+
}
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
if not values_to_remove:
|
|
100
|
+
report.append(
|
|
101
|
+
{
|
|
102
|
+
"query": query,
|
|
83
103
|
"outcome": "skipped",
|
|
84
104
|
"title": "No Matching Hits",
|
|
85
|
-
"message": "
|
|
105
|
+
"message": "None of the matching hits were found in the bundle.",
|
|
86
106
|
}
|
|
87
107
|
)
|
|
88
108
|
return report
|
|
89
109
|
|
|
90
|
-
|
|
91
|
-
safe_query,
|
|
92
|
-
[hit_helper.list_remove("howler.bundles", bundle_id)],
|
|
93
|
-
)
|
|
94
|
-
|
|
95
|
-
hit_service.update_hit(
|
|
96
|
-
bundle_id,
|
|
97
|
-
[hit_helper.list_remove("howler.hits", h["howler"]["id"]) for h in matching_hits],
|
|
98
|
-
)
|
|
99
|
-
|
|
100
|
-
if len(ds.hit.get(bundle_id).howler.hits) < 1:
|
|
101
|
-
hit_service.update_hit(bundle_id, [hit_helper.update("howler.is_bundle", False)])
|
|
110
|
+
case_service.remove_case_items(case_id, values_to_remove)
|
|
102
111
|
|
|
103
112
|
report.append(
|
|
104
113
|
{
|
|
@@ -108,7 +117,17 @@ def execute(query: str, bundle_id: Optional[str] = None, user: Optional[User] =
|
|
|
108
117
|
"message": f"Matching hits removed from bundle with id {bundle_id}",
|
|
109
118
|
}
|
|
110
119
|
)
|
|
111
|
-
|
|
120
|
+
|
|
121
|
+
except NotFoundException as e:
|
|
122
|
+
report.append(
|
|
123
|
+
{
|
|
124
|
+
"query": query,
|
|
125
|
+
"outcome": "error",
|
|
126
|
+
"title": "Failed to Execute",
|
|
127
|
+
"message": str(e),
|
|
128
|
+
}
|
|
129
|
+
)
|
|
130
|
+
except Exception as e:
|
|
112
131
|
report.append(
|
|
113
132
|
{
|
|
114
133
|
"query": query,
|
|
@@ -125,11 +144,11 @@ def specification():
|
|
|
125
144
|
"""Specify various properties of the action, such as title, descriptions, permissions and input steps."""
|
|
126
145
|
return {
|
|
127
146
|
"id": OPERATION_ID,
|
|
128
|
-
"title": "Remove from Bundle",
|
|
147
|
+
"title": "Remove from Bundle (Deprecated)",
|
|
129
148
|
"priority": 5,
|
|
130
149
|
"i18nKey": f"operations.{OPERATION_ID}",
|
|
131
150
|
"description": {
|
|
132
|
-
"short": "Remove a set of hits from a bundle",
|
|
151
|
+
"short": "Remove a set of hits from a bundle (deprecated — uses cases)",
|
|
133
152
|
"long": execute.__doc__,
|
|
134
153
|
},
|
|
135
154
|
"roles": ["automation_basic", "actionrunner_basic"],
|
|
@@ -137,7 +156,6 @@ def specification():
|
|
|
137
156
|
{
|
|
138
157
|
"args": {"bundle_id": []},
|
|
139
158
|
"options": {},
|
|
140
|
-
"validation": {"error": {"query": "-howler.bundles:$bundle_id"}},
|
|
141
159
|
}
|
|
142
160
|
],
|
|
143
161
|
"triggers": VALID_TRIGGERS,
|
|
@@ -9,8 +9,8 @@ from howler.helper.workflow import Workflow, WorkflowException
|
|
|
9
9
|
from howler.odm.models.action import VALID_TRIGGERS
|
|
10
10
|
from howler.odm.models.howler_data import (
|
|
11
11
|
Assessment,
|
|
12
|
-
HitStatus,
|
|
13
12
|
HitStatusTransition,
|
|
13
|
+
Status,
|
|
14
14
|
Vote,
|
|
15
15
|
)
|
|
16
16
|
from howler.odm.models.user import User
|
|
@@ -193,15 +193,13 @@ def specification():
|
|
|
193
193
|
"steps": [
|
|
194
194
|
{
|
|
195
195
|
"args": {"status": []},
|
|
196
|
-
"options": {"status":
|
|
196
|
+
"options": {"status": Status.list()},
|
|
197
197
|
"validation": {"error": {"query": "-howler.status:$status"}},
|
|
198
198
|
},
|
|
199
199
|
{
|
|
200
200
|
"args": {"transition": []},
|
|
201
201
|
"options": {
|
|
202
|
-
"transition": {
|
|
203
|
-
f"status:{status}": hit_service.get_transitions(status) for status in HitStatus.list()
|
|
204
|
-
},
|
|
202
|
+
"transition": {f"status:{status}": hit_service.get_transitions(status) for status in Status.list()},
|
|
205
203
|
},
|
|
206
204
|
},
|
|
207
205
|
{
|
|
@@ -25,7 +25,8 @@ logger = get_logger(__file__)
|
|
|
25
25
|
|
|
26
26
|
def make_subapi_blueprint(name, api_version=1):
|
|
27
27
|
"""Create a flask Blueprint for a subapi in a standard way."""
|
|
28
|
-
|
|
28
|
+
full_name = f"v{api_version}_{name}"
|
|
29
|
+
return Blueprint(full_name, full_name, url_prefix="/".join([API_PREFIX, f"v{api_version}", name]))
|
|
29
30
|
|
|
30
31
|
|
|
31
32
|
def _make_api_response(
|
|
@@ -7,11 +7,11 @@ from flask import Blueprint, request
|
|
|
7
7
|
from opentelemetry import trace
|
|
8
8
|
|
|
9
9
|
import howler.services.event_service as event_service
|
|
10
|
+
import howler.services.viewer_service as viewer_service
|
|
10
11
|
from howler.api import ok, unauthorized
|
|
11
12
|
from howler.common.logging import get_logger
|
|
12
|
-
from howler.datastore.operations import OdmHelper
|
|
13
13
|
from howler.helper.ws import ConnectionClosed, Server
|
|
14
|
-
from howler.
|
|
14
|
+
from howler.security import api_login
|
|
15
15
|
from howler.security.socket import websocket_auth, ws_response
|
|
16
16
|
from howler.utils.socket_utils import check_action
|
|
17
17
|
|
|
@@ -24,13 +24,16 @@ socket_api._doc = "Endpoints concerning websocket connectivity between the clien
|
|
|
24
24
|
logger = get_logger(__file__)
|
|
25
25
|
tracer = trace.get_tracer(__name__)
|
|
26
26
|
|
|
27
|
-
hit_helper = OdmHelper(Hit)
|
|
28
|
-
|
|
29
27
|
|
|
30
28
|
@tracer.start_as_current_span(f"{__name__}.emit")
|
|
31
29
|
@socket_api.route("/emit/<event>", methods=["POST"])
|
|
32
30
|
def emit(event: str):
|
|
33
|
-
"""Emit an event to all listening websockets
|
|
31
|
+
"""Emit an event to all listening websockets.
|
|
32
|
+
|
|
33
|
+
.. deprecated::
|
|
34
|
+
This endpoint is deprecated. Events are now propagated via Redis pubsub
|
|
35
|
+
and no longer require a dedicated websocket pod.
|
|
36
|
+
"""
|
|
34
37
|
if "Authorization" not in request.headers:
|
|
35
38
|
return unauthorized(err="Missing authorization header")
|
|
36
39
|
|
|
@@ -49,10 +52,25 @@ def emit(event: str):
|
|
|
49
52
|
return ok()
|
|
50
53
|
|
|
51
54
|
|
|
55
|
+
@tracer.start_as_current_span(f"{__name__}.get_viewers")
|
|
56
|
+
@socket_api.route("/viewers/<entity_id>", methods=["GET"])
|
|
57
|
+
@api_login(audit=False, required_priv=["R"])
|
|
58
|
+
def get_viewers(entity_id: str, **kwargs):
|
|
59
|
+
"""Get the list of users currently viewing the specified entity
|
|
60
|
+
|
|
61
|
+
Variables:
|
|
62
|
+
entity_id => The ID of the entity to get viewers for
|
|
63
|
+
|
|
64
|
+
Result Example:
|
|
65
|
+
["user1", "user2"]
|
|
66
|
+
"""
|
|
67
|
+
return ok(viewer_service.get_viewers(entity_id))
|
|
68
|
+
|
|
69
|
+
|
|
52
70
|
@tracer.start_as_current_span(f"{__name__}.connect")
|
|
53
71
|
@socket_api.route("/connect", websocket=True) # type: ignore
|
|
54
72
|
@websocket_auth(required_priv=["R"])
|
|
55
|
-
def connect(ws: Server, *args: Any, ws_id: str, **kwargs):
|
|
73
|
+
def connect(ws: Server, *args: Any, ws_id: str, **kwargs): # noqa: C901
|
|
56
74
|
"""Connect to the server to monitor for updates via websocket
|
|
57
75
|
|
|
58
76
|
Variables:
|
|
@@ -78,10 +96,20 @@ def connect(ws: Server, *args: Any, ws_id: str, **kwargs):
|
|
|
78
96
|
logger.debug("Sending action: %s", data)
|
|
79
97
|
ws.send(ws_response("action", data))
|
|
80
98
|
|
|
99
|
+
def send_case(data: dict[str, Any]):
|
|
100
|
+
logger.debug("Sending case update: %s", data.get("case", {}).get("case_id", "unknown"))
|
|
101
|
+
ws.send(ws_response("cases", data))
|
|
102
|
+
|
|
103
|
+
def send_viewers_update(data: dict[str, Any]):
|
|
104
|
+
logger.debug("Sending viewers update: %s", data.get("id", "unknown"))
|
|
105
|
+
ws.send(ws_response("viewers_update", data))
|
|
106
|
+
|
|
81
107
|
try:
|
|
82
108
|
event_service.on("hits", send_hit)
|
|
83
109
|
event_service.on("broadcast", send_broadcast)
|
|
84
110
|
event_service.on("action", send_action)
|
|
111
|
+
event_service.on("cases", send_case)
|
|
112
|
+
event_service.on("viewers_update", send_viewers_update)
|
|
85
113
|
while ws.connected:
|
|
86
114
|
data = ws.receive(10)
|
|
87
115
|
if data:
|
|
@@ -113,6 +141,8 @@ def connect(ws: Server, *args: Any, ws_id: str, **kwargs):
|
|
|
113
141
|
event_service.off("hits", send_hit)
|
|
114
142
|
event_service.off("broadcast", send_broadcast)
|
|
115
143
|
event_service.off("action", send_action)
|
|
144
|
+
event_service.off("cases", send_case)
|
|
145
|
+
event_service.off("viewers_update", send_viewers_update)
|
|
116
146
|
|
|
117
147
|
for id, action, broadcast in outstanding_actions:
|
|
118
148
|
outstanding_actions = check_action(id, action, broadcast, outstanding_actions=outstanding_actions, **kwargs)
|