howler-api 4.0.0.dev1176__tar.gz → 4.0.0.dev1182__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.dev1176 → howler_api-4.0.0.dev1182}/PKG-INFO +1 -1
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/actions/add_to_bundle.py +10 -6
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/actions/add_to_case.py +15 -10
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/actions/remove_from_bundle.py +11 -7
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/api/v2/case.py +49 -21
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/datastore/collection.py +7 -4
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/mixins.py +3 -3
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/case.py +28 -5
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/random_data.py +247 -129
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/services/analytic_service.py +6 -2
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/services/bundle_compat_service.py +27 -26
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/services/case_service.py +378 -299
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/services/correlation_service.py +16 -3
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/services/dossier_service.py +11 -2
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/services/hit_service.py +6 -2
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/services/user_service.py +7 -1
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/pyproject.toml +1 -1
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/README.md +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/__init__.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/actions/__init__.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/actions/add_label.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/actions/change_field.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/actions/demote.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/actions/example_plugin.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/actions/prioritization.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/actions/promote.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/actions/remove_label.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/actions/transition.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/api/__init__.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/api/base.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/api/socket.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/api/v1/__init__.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/api/v1/action.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/api/v1/analytic.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/api/v1/auth.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/api/v1/clue.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/api/v1/configs.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/api/v1/dossier.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/api/v1/help.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/api/v1/hit.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/api/v1/notebook.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/api/v1/overview.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/api/v1/search.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/api/v1/template.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/api/v1/tool.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/api/v1/user.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/api/v1/utils/__init__.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/api/v1/utils/etag.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/api/v1/utils/params.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/api/v1/view.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/api/v2/__init__.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/api/v2/fuzzy.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/api/v2/ingest.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/api/v2/search.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/app.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/common/README.md +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/common/__init__.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/common/classification.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/common/classification.yml +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/common/exceptions.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/common/loader.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/common/logging/__init__.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/common/logging/audit.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/common/logging/format.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/common/net.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/common/net_static.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/common/random_user.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/common/swagger.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/config.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/cronjobs/__init__.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/cronjobs/action_queue_worker.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/cronjobs/correlation.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/cronjobs/retention.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/cronjobs/view_cleanup.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/datastore/README.md +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/datastore/__init__.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/datastore/bulk.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/datastore/constants.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/datastore/exceptions.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/datastore/howler_store.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/datastore/migrations/fix_process.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/datastore/operations.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/datastore/schemas.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/datastore/store.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/datastore/support/__init__.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/datastore/support/build.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/datastore/support/schemas.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/datastore/types.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/error.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/external/README.md +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/external/__init__.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/external/generate_mitre.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/external/generate_sigma_rules.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/external/generate_tlds.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/external/reindex_data.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/external/wipe_databases.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/gunicorn_config.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/healthz.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/helper/__init__.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/helper/azure.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/helper/discover.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/helper/hit.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/helper/oauth.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/helper/search.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/helper/workflow.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/helper/ws.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/README.md +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/__init__.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/base.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/charter.txt +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/constants.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/helper.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/howler_enum.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/__init__.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/action.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/analytic.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/assemblyline.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/aws.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/azure.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/cbs.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/clue.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/config.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/dossier.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/__init__.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/agent.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/autonomous_system.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/client.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/cloud.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/code_signature.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/container.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/dns.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/egress.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/elf.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/email.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/error.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/event.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/faas.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/file.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/geo.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/group.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/hash.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/host.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/http.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/ingress.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/interface.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/network.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/observer.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/organization.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/os.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/pe.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/process.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/registry.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/related.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/rule.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/server.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/threat.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/tls.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/url.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/user.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/user_agent.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/ecs/vulnerability.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/event.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/gcp.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/hit.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/howler_data.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/lead.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/localized_label.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/overview.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/pivot.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/record.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/template.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/user.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/models/view.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/odm/randomizer.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/patched.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/plugins/__init__.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/plugins/config.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/remote/__init__.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/remote/datatypes/README.md +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/remote/datatypes/__init__.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/remote/datatypes/counters.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/remote/datatypes/events.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/remote/datatypes/hash.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/remote/datatypes/lock.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/remote/datatypes/queues/__init__.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/remote/datatypes/queues/comms.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/remote/datatypes/queues/multi.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/remote/datatypes/queues/named.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/remote/datatypes/queues/priority.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/remote/datatypes/set.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/remote/datatypes/user_quota_tracker.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/security/__init__.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/security/socket.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/security/utils.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/services/__init__.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/services/action_service.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/services/auth_service.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/services/comms_service.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/services/config_service.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/services/docs_service.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/services/event_service.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/services/fuzzy_service.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/services/jwt_service.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/services/lucene_service.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/services/notebook_service.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/services/overview_service.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/services/search_service.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/services/template_service.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/services/viewer_service.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/telemetry.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/utils/__init__.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/utils/annotations.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/utils/chunk.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/utils/compat.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/utils/constants.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/utils/dict_utils.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/utils/isotime.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/utils/list_utils.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/utils/lucene.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/utils/path.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/utils/socket_utils.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/utils/str_utils.py +0 -0
- {howler_api-4.0.0.dev1176 → howler_api-4.0.0.dev1182}/howler/utils/uid.py +0 -0
|
@@ -6,6 +6,7 @@ from howler.actions import check_hit_limit
|
|
|
6
6
|
from howler.common.exceptions import NotFoundException
|
|
7
7
|
from howler.common.loader import datastore
|
|
8
8
|
from howler.odm.models.action import VALID_TRIGGERS
|
|
9
|
+
from howler.odm.models.case import CaseItemTypes
|
|
9
10
|
from howler.odm.models.user import User
|
|
10
11
|
from howler.services import bundle_compat_service, case_service
|
|
11
12
|
from howler.utils.str_utils import sanitize_lucene_query
|
|
@@ -36,8 +37,8 @@ def execute(query: str, bundle_id: Optional[str] = None, user: Optional[User] =
|
|
|
36
37
|
]
|
|
37
38
|
|
|
38
39
|
try:
|
|
39
|
-
|
|
40
|
-
if
|
|
40
|
+
case = bundle_compat_service.find_case_for_bundle(bundle_id)
|
|
41
|
+
if case is None:
|
|
41
42
|
report.append(
|
|
42
43
|
{
|
|
43
44
|
"query": query,
|
|
@@ -69,16 +70,19 @@ def execute(query: str, bundle_id: Optional[str] = None, user: Optional[User] =
|
|
|
69
70
|
)
|
|
70
71
|
return report
|
|
71
72
|
|
|
73
|
+
folder = case_service.get_parent_from_path(case, "hits", create_if_missing=True)
|
|
74
|
+
|
|
72
75
|
added = []
|
|
73
76
|
skipped = []
|
|
74
77
|
for hit in matching_hits:
|
|
75
|
-
|
|
78
|
+
name = f"{hit.howler.analytic} ({hit.howler.id})"
|
|
76
79
|
try:
|
|
77
80
|
case_service.append_case_item(
|
|
78
|
-
|
|
79
|
-
item_type=
|
|
81
|
+
case,
|
|
82
|
+
item_type=CaseItemTypes.HIT,
|
|
83
|
+
item_name=name,
|
|
80
84
|
item_value=hit.howler.id,
|
|
81
|
-
|
|
85
|
+
item_parent=folder.id if folder else None,
|
|
82
86
|
)
|
|
83
87
|
added.append(hit.howler.id)
|
|
84
88
|
except Exception:
|
|
@@ -10,7 +10,7 @@ from howler.services import case_service
|
|
|
10
10
|
OPERATION_ID = "add_to_case"
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
def execute(
|
|
13
|
+
def execute( # noqa: C901
|
|
14
14
|
query: str,
|
|
15
15
|
case_id: Optional[str] = None,
|
|
16
16
|
path: str = "related",
|
|
@@ -24,7 +24,7 @@ def execute(
|
|
|
24
24
|
case_id (str): The ID of the case to add the alerts to.
|
|
25
25
|
path (str): The path within the case at which to place the alerts. Defaults to "related".
|
|
26
26
|
title_template (str): A Mustache-compatible template string used to generate each item's
|
|
27
|
-
|
|
27
|
+
name. The hit's fields are available as template variables.
|
|
28
28
|
Defaults to "{{howler.analytic}} ({{howler.id}})".
|
|
29
29
|
"""
|
|
30
30
|
if not case_id:
|
|
@@ -39,7 +39,8 @@ def execute(
|
|
|
39
39
|
|
|
40
40
|
ds = datastore()
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
case = ds.case.get(case_id)
|
|
43
|
+
if case is None:
|
|
43
44
|
return [
|
|
44
45
|
{
|
|
45
46
|
"query": query,
|
|
@@ -65,19 +66,23 @@ def execute(
|
|
|
65
66
|
skipped = []
|
|
66
67
|
added = []
|
|
67
68
|
|
|
68
|
-
normalized_path = path.rstrip("/")
|
|
69
|
-
|
|
70
69
|
for hit in hits:
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
title = chevron.render(title_template, hit.as_primitives())
|
|
71
|
+
item_path = f"{path.rstrip('/')}/{title}" if path else title
|
|
72
|
+
try:
|
|
73
|
+
path, name = item_path.rsplit("/", maxsplit=1)
|
|
74
|
+
except ValueError:
|
|
75
|
+
name = item_path
|
|
74
76
|
|
|
75
77
|
try:
|
|
78
|
+
parent = case_service.get_parent_from_path(case, path, create_if_missing=True)
|
|
79
|
+
|
|
76
80
|
case_service.append_case_item(
|
|
77
|
-
|
|
81
|
+
case,
|
|
78
82
|
item_type="hit",
|
|
79
83
|
item_value=hit.howler.id,
|
|
80
|
-
|
|
84
|
+
item_name=name,
|
|
85
|
+
item_parent=parent.id if parent else None,
|
|
81
86
|
)
|
|
82
87
|
added.append(hit.howler.id)
|
|
83
88
|
except InvalidDataException as e:
|
|
@@ -36,8 +36,8 @@ def execute(query: str, bundle_id: Optional[str] = None, user: Optional[User] =
|
|
|
36
36
|
]
|
|
37
37
|
|
|
38
38
|
try:
|
|
39
|
-
|
|
40
|
-
if
|
|
39
|
+
case = bundle_compat_service.find_case_for_bundle(bundle_id)
|
|
40
|
+
if case is None:
|
|
41
41
|
report.append(
|
|
42
42
|
{
|
|
43
43
|
"query": query,
|
|
@@ -69,19 +69,20 @@ def execute(query: str, bundle_id: Optional[str] = None, user: Optional[User] =
|
|
|
69
69
|
)
|
|
70
70
|
return report
|
|
71
71
|
|
|
72
|
-
#
|
|
73
|
-
|
|
74
|
-
if
|
|
72
|
+
# Refresh case from datastore to ensure latest state
|
|
73
|
+
refreshed_case = ds.case.get(case.case_id)
|
|
74
|
+
if refreshed_case is None:
|
|
75
75
|
report.append(
|
|
76
76
|
{
|
|
77
77
|
"query": query,
|
|
78
78
|
"outcome": "error",
|
|
79
79
|
"title": "Case Not Found",
|
|
80
|
-
"message": f"Associated case {case_id} no longer exists.",
|
|
80
|
+
"message": f"Associated case {case.case_id} no longer exists.",
|
|
81
81
|
}
|
|
82
82
|
)
|
|
83
83
|
return report
|
|
84
84
|
|
|
85
|
+
case = refreshed_case
|
|
85
86
|
case_item_values = {item.value for item in case.items}
|
|
86
87
|
values_to_remove = [h.howler.id for h in matching_hits if h.howler.id in case_item_values]
|
|
87
88
|
skipped_ids = [h.howler.id for h in matching_hits if h.howler.id not in case_item_values]
|
|
@@ -107,7 +108,10 @@ def execute(query: str, bundle_id: Optional[str] = None, user: Optional[User] =
|
|
|
107
108
|
)
|
|
108
109
|
return report
|
|
109
110
|
|
|
110
|
-
|
|
111
|
+
# remove_case_items expects item UUIDs (CaseItem.id), not hit value strings.
|
|
112
|
+
value_set = set(values_to_remove)
|
|
113
|
+
item_ids_to_remove = [item.id for item in case.items if item.value in value_set]
|
|
114
|
+
case_service.remove_case_items(case, item_ids_to_remove)
|
|
111
115
|
|
|
112
116
|
report.append(
|
|
113
117
|
{
|
|
@@ -7,7 +7,7 @@ from howler.common.loader import datastore
|
|
|
7
7
|
from howler.common.logging import get_logger
|
|
8
8
|
from howler.common.swagger import generate_swagger_docs
|
|
9
9
|
from howler.datastore.exceptions import DataStoreException
|
|
10
|
-
from howler.odm.models.case import CaseItem
|
|
10
|
+
from howler.odm.models.case import Case, CaseItem
|
|
11
11
|
from howler.odm.models.user import User
|
|
12
12
|
from howler.security import api_login
|
|
13
13
|
from howler.services import case_service
|
|
@@ -221,9 +221,10 @@ def append_item(id: str, user: User, **kwargs): # noqa: C901
|
|
|
221
221
|
|
|
222
222
|
Data Block:
|
|
223
223
|
{
|
|
224
|
-
"type": "hit",
|
|
225
|
-
"value": "item-id-123"
|
|
226
|
-
"
|
|
224
|
+
"type": "hit", # Type of item: hit, event, case, folder, markdown, reference, table, or lead
|
|
225
|
+
"value": "item-id-123", # The ID or reference value for the item
|
|
226
|
+
"parent": "folder-uuid" # Optional: parent folder item ID (null for root)
|
|
227
|
+
"path": "example/path/Title" # Optional: path to create the item at (will ensure path exists)
|
|
227
228
|
}
|
|
228
229
|
|
|
229
230
|
Result Example:
|
|
@@ -236,15 +237,25 @@ def append_item(id: str, user: User, **kwargs): # noqa: C901
|
|
|
236
237
|
except UnsupportedMediaType:
|
|
237
238
|
return bad_request(err="Invalid JSON body")
|
|
238
239
|
|
|
239
|
-
|
|
240
|
+
if not body or not isinstance(body, dict):
|
|
241
|
+
return bad_request(err="Request body must be a JSON object.")
|
|
242
|
+
|
|
243
|
+
for field in ["value", "type", "name"]:
|
|
240
244
|
if field not in body:
|
|
241
245
|
return bad_request(err=f"CaseItem '{field}' is required")
|
|
242
246
|
|
|
243
247
|
try:
|
|
248
|
+
if path := body.pop("path", None):
|
|
249
|
+
parent = case_service.get_parent_from_path(id, path)
|
|
250
|
+
|
|
251
|
+
body["parent"] = parent.id if parent else None
|
|
252
|
+
|
|
244
253
|
return ok(case_service.append_case_item(id, item=CaseItem(body)))
|
|
245
254
|
except DataStoreException as e:
|
|
246
255
|
logger.exception("Save Error")
|
|
247
256
|
return internal_error(err=str(e))
|
|
257
|
+
except NotFoundException as e:
|
|
258
|
+
return not_found(err=str(e))
|
|
248
259
|
except InvalidDataException as e:
|
|
249
260
|
return bad_request(err=str(e))
|
|
250
261
|
|
|
@@ -267,7 +278,8 @@ def delete_item(case_id: str, **kwargs):
|
|
|
267
278
|
|
|
268
279
|
Data Block:
|
|
269
280
|
{
|
|
270
|
-
"
|
|
281
|
+
"ids": ["uuid-1", "uuid-2"], # The UUIDs of the items to delete
|
|
282
|
+
"force": false # Optional: force-delete non-empty folders
|
|
271
283
|
}
|
|
272
284
|
|
|
273
285
|
Result Example:
|
|
@@ -277,15 +289,21 @@ def delete_item(case_id: str, **kwargs):
|
|
|
277
289
|
"""
|
|
278
290
|
body = request.json
|
|
279
291
|
|
|
280
|
-
if not body or not isinstance(body, dict)
|
|
281
|
-
return bad_request(err="Request body must be a JSON object
|
|
292
|
+
if not body or not isinstance(body, dict):
|
|
293
|
+
return bad_request(err="Request body must be a JSON object.")
|
|
282
294
|
|
|
283
|
-
|
|
284
|
-
if not isinstance(
|
|
285
|
-
return bad_request(err="'
|
|
295
|
+
force = body.get("force", False)
|
|
296
|
+
if not isinstance(force, bool):
|
|
297
|
+
return bad_request(err="'force' must be a boolean.")
|
|
298
|
+
|
|
299
|
+
ids = body.get("ids")
|
|
300
|
+
if not ids or not isinstance(ids, list):
|
|
301
|
+
return bad_request(err="'ids' must be a non-empty list.")
|
|
302
|
+
elif not all(isinstance(item_id, str) for item_id in ids):
|
|
303
|
+
return bad_request(err="All items in 'ids' must be strings.")
|
|
286
304
|
|
|
287
305
|
try:
|
|
288
|
-
return ok(case_service.remove_case_items(case_id,
|
|
306
|
+
return ok(case_service.remove_case_items(case_id, ids, force=force))
|
|
289
307
|
except DataStoreException as e:
|
|
290
308
|
logger.exception("Save Error")
|
|
291
309
|
return internal_error(err=str(e))
|
|
@@ -297,10 +315,9 @@ def delete_item(case_id: str, **kwargs):
|
|
|
297
315
|
@case_api.route("/<case_id>/items", methods=["PUT"])
|
|
298
316
|
@api_login(required_priv=["R", "W"])
|
|
299
317
|
def rename_item(case_id: str, **kwargs):
|
|
300
|
-
"""
|
|
318
|
+
"""Move an item within a case
|
|
301
319
|
|
|
302
|
-
Updates the
|
|
303
|
-
not already be used by another item in the case.
|
|
320
|
+
Updates the parent of a single item identified by its id.
|
|
304
321
|
|
|
305
322
|
Variables:
|
|
306
323
|
case_id => The id of the case to modify
|
|
@@ -310,8 +327,9 @@ def rename_item(case_id: str, **kwargs):
|
|
|
310
327
|
|
|
311
328
|
Data Block:
|
|
312
329
|
{
|
|
313
|
-
"
|
|
314
|
-
"
|
|
330
|
+
"id": "uuid-of-item", # The UUID of the item to update
|
|
331
|
+
"parent": "uuid-or-null", # Move: the UUID of the target folder, or null for root
|
|
332
|
+
"name": "Display Name" # Rename: the new display name for the item
|
|
315
333
|
}
|
|
316
334
|
|
|
317
335
|
Result Example:
|
|
@@ -324,12 +342,22 @@ def rename_item(case_id: str, **kwargs):
|
|
|
324
342
|
if not body or not isinstance(body, dict):
|
|
325
343
|
return bad_request(err="Request body must be a JSON object.")
|
|
326
344
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
return bad_request(err=f"'{field}' is required.")
|
|
345
|
+
if "id" not in body:
|
|
346
|
+
return bad_request(err="'id' is required.")
|
|
330
347
|
|
|
348
|
+
item_id = body["id"]
|
|
331
349
|
try:
|
|
332
|
-
|
|
350
|
+
result: Case | None = None
|
|
351
|
+
if "name" in body:
|
|
352
|
+
result = case_service.rename_case_item(case_id, item_id, body["name"])
|
|
353
|
+
|
|
354
|
+
if "parent" in body:
|
|
355
|
+
result = case_service.move_case_item(case_id, item_id, body["parent"])
|
|
356
|
+
|
|
357
|
+
if not result:
|
|
358
|
+
return bad_request(err="At least one of 'name' or 'parent' is required.")
|
|
359
|
+
|
|
360
|
+
return ok(result)
|
|
333
361
|
except DataStoreException as e:
|
|
334
362
|
logger.exception("Save Error")
|
|
335
363
|
return internal_error(err=str(e))
|
|
@@ -836,11 +836,11 @@ class ESCollection(Generic[ModelType]):
|
|
|
836
836
|
alias_actions.append({"remove_index": {"index": new_name}})
|
|
837
837
|
self.with_retries(self.datastore.client.indices.update_aliases, actions=alias_actions)
|
|
838
838
|
|
|
839
|
-
if self.with_retries(self.datastore.client.indices.exists, index=new_name):
|
|
839
|
+
if bool(self.with_retries(self.datastore.client.indices.exists, index=new_name)):
|
|
840
840
|
logger.warning("Deleting reindex target %s", new_name)
|
|
841
841
|
self.with_retries(self.datastore.client.indices.delete, index=new_name)
|
|
842
842
|
finally:
|
|
843
|
-
if self.with_retries(self.datastore.client.indices.exists, index=index):
|
|
843
|
+
if bool(self.with_retries(self.datastore.client.indices.exists, index=index)):
|
|
844
844
|
logger.warning("Unblock writes to the index")
|
|
845
845
|
self.with_retries(
|
|
846
846
|
self.datastore.client.indices.put_settings,
|
|
@@ -848,7 +848,7 @@ class ESCollection(Generic[ModelType]):
|
|
|
848
848
|
settings=write_unblock_settings,
|
|
849
849
|
)
|
|
850
850
|
except Exception:
|
|
851
|
-
if source_writes_blocked and self.with_retries(self.datastore.client.indices.exists, index=index):
|
|
851
|
+
if source_writes_blocked and bool(self.with_retries(self.datastore.client.indices.exists, index=index)):
|
|
852
852
|
logger.warning("Unblock writes to source index %s after failed reindex", index)
|
|
853
853
|
self.with_retries(
|
|
854
854
|
self.datastore.client.indices.put_settings,
|
|
@@ -1233,7 +1233,7 @@ class ESCollection(Generic[ModelType]):
|
|
|
1233
1233
|
return self.normalize(data, as_obj=as_obj), version
|
|
1234
1234
|
return self.normalize(data, as_obj=as_obj)
|
|
1235
1235
|
|
|
1236
|
-
def save(self, key, data, version=None, refresh=None):
|
|
1236
|
+
def save(self, key, data, version=None, refresh: Literal["true", "false", "wait_for"] | None = None):
|
|
1237
1237
|
"""Save to document to the datastore using the key as its document id.
|
|
1238
1238
|
|
|
1239
1239
|
The document data will be normalized before being saved in the datastore.
|
|
@@ -1241,6 +1241,9 @@ class ESCollection(Generic[ModelType]):
|
|
|
1241
1241
|
:param key: ID of the document to save
|
|
1242
1242
|
:param data: raw data or instance of the model class to save as the document
|
|
1243
1243
|
:param version: version of the document to save over, if the version check fails this will raise an exception
|
|
1244
|
+
:param refresh: 'true' | 'false' | 'wait_for' | None
|
|
1245
|
+
Whether to refresh the datastore before returning. 'wait_for' will wait for the change to be visible
|
|
1246
|
+
in search.
|
|
1244
1247
|
:return: True if the document was saved properly
|
|
1245
1248
|
"""
|
|
1246
1249
|
if " " in key:
|
|
@@ -9,7 +9,7 @@ Elasticsearch datastore without boilerplate.
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
11
|
from operator import attrgetter
|
|
12
|
-
from typing import Generic, TypeVar, overload
|
|
12
|
+
from typing import Generic, Literal, TypeVar, overload
|
|
13
13
|
|
|
14
14
|
from howler.common.exceptions import HowlerRuntimeError
|
|
15
15
|
from howler.common.loader import datastore
|
|
@@ -82,7 +82,7 @@ class DatastoreMixin(Generic[ModelType]):
|
|
|
82
82
|
"""
|
|
83
83
|
return datastore()
|
|
84
84
|
|
|
85
|
-
def save(self) -> bool:
|
|
85
|
+
def save(self, refresh: Literal["true", "false", "wait_for"] = "wait_for") -> bool:
|
|
86
86
|
"""Persist the current model instance to the datastore.
|
|
87
87
|
|
|
88
88
|
Determines the target index from the lowercase class name, extracts the
|
|
@@ -94,4 +94,4 @@ class DatastoreMixin(Generic[ModelType]):
|
|
|
94
94
|
id_field = self.__class__._Model__id_field # type: ignore[attr-defined]
|
|
95
95
|
current_id = attrgetter(id_field)(self)
|
|
96
96
|
|
|
97
|
-
return self.ds[index_name].save(current_id, self)
|
|
97
|
+
return self.ds[index_name].save(current_id, self, refresh=refresh)
|
|
@@ -7,7 +7,7 @@ from howler.odm.constants import CaseEscalation, Status
|
|
|
7
7
|
from howler.odm.mixins import DatastoreMixin
|
|
8
8
|
from howler.utils.compat import StrEnum
|
|
9
9
|
|
|
10
|
-
CASE_ITEM_TYPES = {"event", "hit", "case", "lead", "reference"}
|
|
10
|
+
CASE_ITEM_TYPES = {"event", "hit", "case", "lead", "reference", "folder", "markdown"}
|
|
11
11
|
|
|
12
12
|
RULE_INDEX_TYPES = {"hit", "event"}
|
|
13
13
|
|
|
@@ -43,6 +43,8 @@ class CaseItemTypes(StrEnum):
|
|
|
43
43
|
validation by analysts.
|
|
44
44
|
REFERENCE: An external reference such as a URL, document, or resource that
|
|
45
45
|
provides additional context or evidence.
|
|
46
|
+
FOLDER: An organizational folder for grouping case items hierarchically.
|
|
47
|
+
MARKDOWN: A markdown document whose value contains the markdown content directly.
|
|
46
48
|
"""
|
|
47
49
|
|
|
48
50
|
EVENT = "event"
|
|
@@ -51,6 +53,8 @@ class CaseItemTypes(StrEnum):
|
|
|
51
53
|
CASE = "case"
|
|
52
54
|
LEAD = "lead"
|
|
53
55
|
REFERENCE = "reference"
|
|
56
|
+
FOLDER = "folder"
|
|
57
|
+
MARKDOWN = "markdown"
|
|
54
58
|
|
|
55
59
|
|
|
56
60
|
@odm.model(index=True, store=True, description="Log definition.")
|
|
@@ -73,11 +77,19 @@ class CaseLog(odm.Model):
|
|
|
73
77
|
super().__init__(data, *args, **kwargs)
|
|
74
78
|
|
|
75
79
|
|
|
76
|
-
@odm.model(index=True, store=True, description="
|
|
80
|
+
@odm.model(index=True, store=True, description="An item included in a case.")
|
|
77
81
|
class CaseItem(odm.Model):
|
|
78
|
-
|
|
82
|
+
id: str = odm.UUID(description="Unique identifier for this item.")
|
|
83
|
+
parent: Optional[str] = odm.Optional(
|
|
84
|
+
odm.Keyword(description="ID of the parent folder item, or null for root-level items."),
|
|
85
|
+
default=None,
|
|
86
|
+
)
|
|
87
|
+
name: Optional[str] = odm.Optional(
|
|
88
|
+
odm.Keyword(description="Display name for the item. Optional; the UI falls back to value when absent."),
|
|
89
|
+
default=None,
|
|
90
|
+
)
|
|
79
91
|
type: str = odm.Enum(values=CaseItemTypes, description="Type of case item.")
|
|
80
|
-
value: str = odm.Keyword(description="String reference value for the item (ID, URL, or token).")
|
|
92
|
+
value: str = odm.Keyword(description="String reference value for the item (ID, URL, or token), or markdown.")
|
|
81
93
|
visible: bool = odm.Boolean(default=True, description="Whether the item is visible/accessible in the frontend.")
|
|
82
94
|
classification: Optional[str] = odm.Optional(
|
|
83
95
|
odm.Classification(
|
|
@@ -88,6 +100,17 @@ class CaseItem(odm.Model):
|
|
|
88
100
|
)
|
|
89
101
|
)
|
|
90
102
|
|
|
103
|
+
def __init__(self, data: dict = None, *args, **kwargs):
|
|
104
|
+
# Enforce: case items must be root-level (parent=null).
|
|
105
|
+
if data and data.get("type") == CaseItemTypes.CASE and data.get("parent") is not None:
|
|
106
|
+
raise HowlerValueError("Case items must be root-level (parent must be null)")
|
|
107
|
+
|
|
108
|
+
# Set value equal to name for folder items
|
|
109
|
+
if data and data.get("type") == CaseItemTypes.FOLDER:
|
|
110
|
+
data["value"] = data.get("name")
|
|
111
|
+
|
|
112
|
+
super().__init__(data, *args, **kwargs)
|
|
113
|
+
|
|
91
114
|
|
|
92
115
|
@odm.model(index=True, store=True, description="Rule used to place/query data into case paths.")
|
|
93
116
|
class CaseRule(odm.Model):
|
|
@@ -131,7 +154,7 @@ class CaseTask(odm.Model):
|
|
|
131
154
|
complete: bool = odm.Boolean(default=False, description="Whether the task is complete.")
|
|
132
155
|
assignment: str | None = odm.Keyword(description="Assigned discipline or user ID.", optional=True)
|
|
133
156
|
summary: str = odm.Text(description="Task summary.")
|
|
134
|
-
|
|
157
|
+
item: str = odm.Keyword(description="Associated case item id.", optional=True)
|
|
135
158
|
|
|
136
159
|
|
|
137
160
|
@odm.model(index=True, store=True, description="Enrichment annotations associated with a case path.")
|