howler-api 2.11.0.dev185__tar.gz → 2.11.0.dev217__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-2.11.0.dev185 → howler_api-2.11.0.dev217}/PKG-INFO +1 -1
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/api/v1/auth.py +1 -1
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/api/v1/dossier.py +4 -28
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/api/v1/hit.py +11 -7
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/api/v1/search.py +18 -9
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/api/v1/user.py +2 -2
- howler_api-2.11.0.dev217/howler/api/v1/utils/etag.py +84 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/helper.py +4 -1
- howler_api-2.11.0.dev217/howler/services/dossier_service.py +252 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/services/hit_service.py +296 -70
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/services/lucene_service.py +14 -7
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/utils/lucene.py +22 -2
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/pyproject.toml +1 -1
- howler_api-2.11.0.dev185/howler/api/v1/utils/etag.py +0 -46
- howler_api-2.11.0.dev185/howler/services/dossier_service.py +0 -119
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/README.md +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/__init__.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/actions/__init__.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/actions/add_label.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/actions/add_to_bundle.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/actions/change_field.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/actions/demote.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/actions/example_plugin.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/actions/prioritization.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/actions/promote.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/actions/remove_from_bundle.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/actions/remove_label.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/actions/transition.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/api/__init__.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/api/base.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/api/socket.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/api/v1/__init__.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/api/v1/action.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/api/v1/analytic.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/api/v1/borealis.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/api/v1/configs.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/api/v1/help.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/api/v1/notebook.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/api/v1/overview.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/api/v1/template.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/api/v1/tool.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/api/v1/utils/__init__.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/api/v1/view.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/app.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/common/README.md +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/common/__init__.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/common/classification.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/common/classification.yml +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/common/exceptions.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/common/hexdump.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/common/iprange.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/common/loader.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/common/logging/__init__.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/common/logging/audit.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/common/logging/format.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/common/net.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/common/net_static.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/common/random_user.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/common/swagger.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/config.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/cronjobs/__init__.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/cronjobs/retention.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/cronjobs/rules.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/datastore/README.md +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/datastore/__init__.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/datastore/bulk.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/datastore/collection.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/datastore/constants.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/datastore/exceptions.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/datastore/howler_store.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/datastore/migrations/fix_process.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/datastore/operations.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/datastore/schemas.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/datastore/store.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/datastore/support/__init__.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/datastore/support/build.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/datastore/support/schemas.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/datastore/types.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/error.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/external/__init__.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/external/generate_mitre.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/external/generate_sigma_rules.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/external/generate_tlds.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/external/reindex_data.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/external/wipe_databases.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/gunicorn_config.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/healthz.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/helper/__init__.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/helper/azure.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/helper/discover.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/helper/hit.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/helper/oauth.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/helper/search.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/helper/workflow.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/helper/ws.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/README.md +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/__init__.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/base.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/charter.txt +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/howler_enum.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/__init__.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/action.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/analytic.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/assemblyline.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/aws.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/azure.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/cbs.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/config.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/dossier.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/__init__.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/agent.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/autonomous_system.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/client.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/cloud.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/code_signature.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/container.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/dns.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/egress.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/elf.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/email.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/error.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/event.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/faas.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/file.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/geo.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/group.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/hash.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/host.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/http.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/ingress.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/interface.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/network.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/observer.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/organization.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/os.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/pe.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/process.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/registry.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/related.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/rule.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/server.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/threat.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/tls.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/url.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/user.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/user_agent.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/ecs/vulnerability.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/gcp.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/hit.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/howler_data.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/lead.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/localized_label.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/overview.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/pivot.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/template.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/user.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/models/view.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/random_data.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/odm/randomizer.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/patched.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/plugins/__init__.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/plugins/config.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/remote/__init__.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/remote/datatypes/README.md +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/remote/datatypes/__init__.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/remote/datatypes/counters.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/remote/datatypes/events.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/remote/datatypes/hash.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/remote/datatypes/lock.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/remote/datatypes/queues/__init__.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/remote/datatypes/queues/comms.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/remote/datatypes/queues/multi.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/remote/datatypes/queues/named.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/remote/datatypes/queues/priority.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/remote/datatypes/set.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/remote/datatypes/user_quota_tracker.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/security/__init__.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/security/socket.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/security/utils.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/services/__init__.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/services/action_service.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/services/analytic_service.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/services/auth_service.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/services/config_service.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/services/event_service.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/services/jwt_service.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/services/notebook_service.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/services/user_service.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/utils/__init__.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/utils/annotations.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/utils/chunk.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/utils/dict_utils.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/utils/isotime.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/utils/list_utils.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/utils/path.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/utils/socket_utils.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/utils/str_utils.py +0 -0
- {howler_api-2.11.0.dev185 → howler_api-2.11.0.dev217}/howler/utils/uid.py +0 -0
|
@@ -131,7 +131,7 @@ def add_apikey(**kwargs): # noqa: C901
|
|
|
131
131
|
key_name = apikey_data["name"] if "I" not in privs else f"impersonate_{apikey_data['name']}"
|
|
132
132
|
|
|
133
133
|
new_key = {
|
|
134
|
-
"password": bcrypt.
|
|
134
|
+
"password": bcrypt.hash(random_pass),
|
|
135
135
|
"agents": apikey_data.get("agents", []),
|
|
136
136
|
"acl": privs,
|
|
137
137
|
}
|
|
@@ -1,15 +1,6 @@
|
|
|
1
1
|
from flask import request
|
|
2
2
|
|
|
3
|
-
from howler.api import
|
|
4
|
-
bad_request,
|
|
5
|
-
created,
|
|
6
|
-
forbidden,
|
|
7
|
-
internal_error,
|
|
8
|
-
make_subapi_blueprint,
|
|
9
|
-
no_content,
|
|
10
|
-
not_found,
|
|
11
|
-
ok,
|
|
12
|
-
)
|
|
3
|
+
from howler.api import bad_request, created, forbidden, internal_error, make_subapi_blueprint, no_content, not_found, ok
|
|
13
4
|
from howler.common.exceptions import ForbiddenException, HowlerException, InvalidDataException, NotFoundException
|
|
14
5
|
from howler.common.loader import datastore
|
|
15
6
|
from howler.common.logging import get_logger
|
|
@@ -17,7 +8,7 @@ from howler.common.swagger import generate_swagger_docs
|
|
|
17
8
|
from howler.odm.models.dossier import Dossier
|
|
18
9
|
from howler.odm.models.user import User
|
|
19
10
|
from howler.security import api_login
|
|
20
|
-
from howler.services import dossier_service
|
|
11
|
+
from howler.services import dossier_service
|
|
21
12
|
|
|
22
13
|
SUB_API = "dossier"
|
|
23
14
|
dossier_api = make_subapi_blueprint(SUB_API, api_version=1)
|
|
@@ -141,29 +132,14 @@ def get_dossier_for_hit(id: str, user: User, **kwargs):
|
|
|
141
132
|
"""
|
|
142
133
|
storage = datastore()
|
|
143
134
|
try:
|
|
144
|
-
response = storage.hit.search(f"howler.id:{id}", rows=1)
|
|
135
|
+
response = storage.hit.search(f"howler.id:{id}", rows=1, as_obj=False)
|
|
145
136
|
|
|
146
137
|
if response["total"] < 1:
|
|
147
138
|
return not_found(err="Hit does not exist.")
|
|
148
139
|
|
|
149
140
|
hit = response["items"][0]
|
|
150
141
|
|
|
151
|
-
|
|
152
|
-
"dossier_id:*",
|
|
153
|
-
as_obj=True,
|
|
154
|
-
rows=1000,
|
|
155
|
-
)["items"]
|
|
156
|
-
|
|
157
|
-
matching_dossiers: list[Dossier] = []
|
|
158
|
-
for dossier in results:
|
|
159
|
-
if dossier.query is None:
|
|
160
|
-
matching_dossiers.append(dossier)
|
|
161
|
-
continue
|
|
162
|
-
|
|
163
|
-
if lucene_service.match(dossier.query, hit.as_primitives()):
|
|
164
|
-
matching_dossiers.append(dossier)
|
|
165
|
-
|
|
166
|
-
return ok(matching_dossiers)
|
|
142
|
+
return ok(dossier_service.get_matching_dossiers(hit))
|
|
167
143
|
except ValueError as e:
|
|
168
144
|
return bad_request(err=str(e))
|
|
169
145
|
|
|
@@ -17,11 +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
|
|
21
|
-
HowlerException,
|
|
22
|
-
HowlerValueError,
|
|
23
|
-
InvalidDataException,
|
|
24
|
-
)
|
|
20
|
+
from howler.common.exceptions import HowlerException, HowlerValueError, InvalidDataException
|
|
25
21
|
from howler.common.loader import datastore
|
|
26
22
|
from howler.common.logging import get_logger
|
|
27
23
|
from howler.common.swagger import generate_swagger_docs
|
|
@@ -252,7 +248,7 @@ def validate_hits(**kwargs):
|
|
|
252
248
|
@generate_swagger_docs()
|
|
253
249
|
@hit_api.route("/<id>", methods=["GET"])
|
|
254
250
|
@api_login(audit=False, required_priv=["R"])
|
|
255
|
-
@add_etag(getter=hit_service.get_hit
|
|
251
|
+
@add_etag(getter=hit_service.get_hit)
|
|
256
252
|
def get_hit(id: str, server_version: str, **kwargs):
|
|
257
253
|
"""Get a hit.
|
|
258
254
|
|
|
@@ -265,11 +261,19 @@ def get_hit(id: str, server_version: str, **kwargs):
|
|
|
265
261
|
Result Example:
|
|
266
262
|
https://github.com/CybercentreCanada/howler-api/blob/main/howler/odm/models/hit.py
|
|
267
263
|
"""
|
|
268
|
-
hit = cast(Optional[
|
|
264
|
+
hit = cast(Optional[Any], kwargs.get("cached_hit"))
|
|
269
265
|
|
|
270
266
|
if not hit:
|
|
271
267
|
return not_found(err="Hit %s does not exist" % id)
|
|
272
268
|
|
|
269
|
+
if "metadata" in request.args:
|
|
270
|
+
metadata = (request.args.get("metadata", type=str) or "").split(",")
|
|
271
|
+
|
|
272
|
+
hit = hit.as_primitives()
|
|
273
|
+
|
|
274
|
+
if len(metadata) > 0:
|
|
275
|
+
hit_service.augment_metadata(hit, metadata, kwargs["user"])
|
|
276
|
+
|
|
273
277
|
return ok(hit), server_version
|
|
274
278
|
|
|
275
279
|
|
|
@@ -14,6 +14,7 @@ from howler.common.swagger import generate_swagger_docs
|
|
|
14
14
|
from howler.datastore.exceptions import SearchException
|
|
15
15
|
from howler.helper.search import get_collection, get_default_sort, has_access_control, list_all_fields
|
|
16
16
|
from howler.security import api_login
|
|
17
|
+
from howler.services import hit_service
|
|
17
18
|
|
|
18
19
|
SUB_API = "search"
|
|
19
20
|
search_api = make_subapi_blueprint(SUB_API, api_version=1)
|
|
@@ -73,16 +74,18 @@ def search(index, **kwargs):
|
|
|
73
74
|
timeout => Maximum execution time (ms)
|
|
74
75
|
use_archive => Allow access to the datastore achive (Default: False)
|
|
75
76
|
track_total_hits => Track the total number of query matches, instead of stopping at 10000 (Default: False)
|
|
77
|
+
metadata => A list of additional features to be added to the result alongside the raw results
|
|
76
78
|
|
|
77
79
|
Data Block:
|
|
78
80
|
# Note that the data block is for POST requests only!
|
|
79
|
-
{"query": "query",
|
|
80
|
-
"offset": 0,
|
|
81
|
-
"rows": 100,
|
|
82
|
-
"sort": "field asc",
|
|
83
|
-
"fl": "id,score",
|
|
84
|
-
"timeout": 1000,
|
|
85
|
-
"filters": ['fq']
|
|
81
|
+
{"query": "query", # Query to search for
|
|
82
|
+
"offset": 0, # Offset in the results
|
|
83
|
+
"rows": 100, # Max number of results
|
|
84
|
+
"sort": "field asc", # How to sort the results
|
|
85
|
+
"fl": "id,score", # List of fields to return
|
|
86
|
+
"timeout": 1000, # Maximum execution time (ms)
|
|
87
|
+
"filters": ['fq'], # List of additional filter queries limit the data
|
|
88
|
+
"metadata": ["dossiers"]} # List of additional features to add to the search
|
|
86
89
|
|
|
87
90
|
|
|
88
91
|
Result Example:
|
|
@@ -108,7 +111,7 @@ def search(index, **kwargs):
|
|
|
108
111
|
"deep_paging_id",
|
|
109
112
|
"track_total_hits",
|
|
110
113
|
]
|
|
111
|
-
multi_fields = ["filters"]
|
|
114
|
+
multi_fields = ["filters", "metadata"]
|
|
112
115
|
boolean_fields = ["use_archive"]
|
|
113
116
|
|
|
114
117
|
params, req_data = generate_params(request, fields, multi_fields)
|
|
@@ -132,7 +135,13 @@ def search(index, **kwargs):
|
|
|
132
135
|
return bad_request(err="There was no search query.")
|
|
133
136
|
|
|
134
137
|
try:
|
|
135
|
-
|
|
138
|
+
metadata = params.pop("metadata", [])
|
|
139
|
+
result = collection().search(query, **params)
|
|
140
|
+
|
|
141
|
+
if index == "hit" and len(metadata) > 0:
|
|
142
|
+
hit_service.augment_metadata(result["items"], metadata, user)
|
|
143
|
+
|
|
144
|
+
return ok(result)
|
|
136
145
|
except (SearchException, BadRequestError) as e:
|
|
137
146
|
return bad_request(err=f"SearchException: {e}")
|
|
138
147
|
|
|
@@ -145,7 +145,7 @@ def add_user_account(username, **_):
|
|
|
145
145
|
@generate_swagger_docs()
|
|
146
146
|
@user_api.route("/<username>", methods=["GET"])
|
|
147
147
|
@api_login(audit=False, required_priv=["R"])
|
|
148
|
-
@add_etag(getter=user_service.get_user, check_if_match=
|
|
148
|
+
@add_etag(getter=user_service.get_user, check_if_match=True)
|
|
149
149
|
def get_user_account(username: str, server_version: Optional[str] = None, **kwargs):
|
|
150
150
|
"""Load the user account information.
|
|
151
151
|
|
|
@@ -327,7 +327,7 @@ def get_user_avatar(username, **_):
|
|
|
327
327
|
resp.headers["ETag"] = sha256(avatar.encode("utf-8")).hexdigest()
|
|
328
328
|
return resp
|
|
329
329
|
else:
|
|
330
|
-
return
|
|
330
|
+
return no_content()
|
|
331
331
|
|
|
332
332
|
|
|
333
333
|
@generate_swagger_docs()
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""ETag utility module for handling HTTP ETags in Flask responses.
|
|
2
|
+
|
|
3
|
+
ETags (Entity Tags) are HTTP headers used for web cache validation and conditional requests.
|
|
4
|
+
They help optimize performance by allowing clients to cache responses and only fetch
|
|
5
|
+
new data when the resource has actually changed.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import functools
|
|
9
|
+
import re
|
|
10
|
+
|
|
11
|
+
from flask import Response, request
|
|
12
|
+
|
|
13
|
+
from howler.api import not_modified
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def add_etag(getter, check_if_match=True):
|
|
17
|
+
"""Decorator to add ETag handling to a Flask response.
|
|
18
|
+
|
|
19
|
+
This decorator implements HTTP ETag functionality for API endpoints, enabling:
|
|
20
|
+
- Conditional requests using If-Match headers
|
|
21
|
+
- Cache validation to prevent unnecessary data transfers
|
|
22
|
+
- Version tracking for resources
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
getter: Function that retrieves the object and its version
|
|
26
|
+
check_if_match (bool): Whether to check If-Match headers for conditional requests
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
Decorated function with ETag support
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def wrapper(f):
|
|
33
|
+
"""Inner wrapper function that applies ETag functionality to the decorated function."""
|
|
34
|
+
|
|
35
|
+
@functools.wraps(f)
|
|
36
|
+
def generate_etag(*args, **kwargs):
|
|
37
|
+
"""Generate and handle ETags for the HTTP response."""
|
|
38
|
+
# Retrieve the object and its version using the provided getter function
|
|
39
|
+
# The getter should return (object, version) tuple
|
|
40
|
+
obj, version = getter(
|
|
41
|
+
kwargs.get("id", kwargs.get("username", None)),
|
|
42
|
+
as_odm=True,
|
|
43
|
+
version=True,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Handle conditional requests with If-Match header
|
|
47
|
+
# If the client's version matches the current version and it's a GET request
|
|
48
|
+
# without metadata parameter, return 304 Not Modified to save bandwidth
|
|
49
|
+
if (
|
|
50
|
+
check_if_match
|
|
51
|
+
and "If-Match" in request.headers
|
|
52
|
+
and request.headers["If-Match"] == version
|
|
53
|
+
and request.method == "GET"
|
|
54
|
+
and "metadata" not in request.args
|
|
55
|
+
):
|
|
56
|
+
return not_modified()
|
|
57
|
+
|
|
58
|
+
# Extract the resource type from the API path and create a cache key
|
|
59
|
+
# e.g., "/api/v1/users/123" becomes "cached_users"
|
|
60
|
+
key = re.sub(r"^\/api\/v\d+\/(\w+)\/.+$", r"cached_\1", request.path)
|
|
61
|
+
kwargs[key] = obj
|
|
62
|
+
|
|
63
|
+
# Call the original function with the cached object and version
|
|
64
|
+
values = f(*args, server_version=version, **kwargs)
|
|
65
|
+
|
|
66
|
+
# Handle different return value formats from the decorated function
|
|
67
|
+
# If there is only one return, it's just the response
|
|
68
|
+
if isinstance(values, Response):
|
|
69
|
+
# Only add ETag header for successful responses (not 409 Conflict or 400 Bad Request)
|
|
70
|
+
if values.status_code != 409 and values.status_code != 400:
|
|
71
|
+
values.headers["ETag"] = version
|
|
72
|
+
return values
|
|
73
|
+
|
|
74
|
+
# If there are two returns, it's the response and the new version
|
|
75
|
+
# This happens when the function modifies the resource and returns an updated version
|
|
76
|
+
else:
|
|
77
|
+
if values[0].status_code != 409 and values[0].status_code != 400:
|
|
78
|
+
# Add the new ETag version to successful responses
|
|
79
|
+
values[0].headers["ETag"] = values[1]
|
|
80
|
+
return values[0]
|
|
81
|
+
|
|
82
|
+
return generate_etag
|
|
83
|
+
|
|
84
|
+
return wrapper
|
|
@@ -143,8 +143,9 @@ def generate_useful_hit(lookups: dict[str, dict[str, Any]], users: list[User], p
|
|
|
143
143
|
hit.howler.labels.mitigation = []
|
|
144
144
|
hit.howler.labels.operation = []
|
|
145
145
|
hit.howler.labels.threat = []
|
|
146
|
+
hit.howler.labels.tuning = []
|
|
146
147
|
|
|
147
|
-
label_type = ceil(rand_seed *
|
|
148
|
+
label_type = ceil(rand_seed * 7)
|
|
148
149
|
if label_type == 1:
|
|
149
150
|
hit.howler.labels.campaign = ["Bad event 2023-07"]
|
|
150
151
|
elif label_type == 2:
|
|
@@ -155,6 +156,8 @@ def generate_useful_hit(lookups: dict[str, dict[str, Any]], users: list[User], p
|
|
|
155
156
|
hit.howler.labels.mitigation = ["Blocked: google.com"]
|
|
156
157
|
elif label_type == 5:
|
|
157
158
|
hit.howler.labels.operation = ["OP_HOWLER"]
|
|
159
|
+
elif label_type == 6:
|
|
160
|
+
hit.howler.labels.tuning = ["Tune example"]
|
|
158
161
|
else:
|
|
159
162
|
hit.howler.labels.threat = ["Bad Mojo"]
|
|
160
163
|
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"""Dossier service module for managing security investigation dossiers.
|
|
2
|
+
|
|
3
|
+
This module provides functionality for creating, updating, retrieving, and managing
|
|
4
|
+
dossiers - collections of security alerts and investigation data organized by analysts.
|
|
5
|
+
Dossiers can be personal (private to the creator) or global (shared with the team).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any, Optional, cast
|
|
9
|
+
|
|
10
|
+
from mergedeep.mergedeep import merge
|
|
11
|
+
|
|
12
|
+
from howler.common.exceptions import ForbiddenException, HowlerException, InvalidDataException, NotFoundException
|
|
13
|
+
from howler.common.loader import datastore
|
|
14
|
+
from howler.common.logging import get_logger
|
|
15
|
+
from howler.datastore.exceptions import SearchException
|
|
16
|
+
from howler.odm.models.dossier import Dossier
|
|
17
|
+
from howler.odm.models.user import User
|
|
18
|
+
from howler.services import lucene_service
|
|
19
|
+
|
|
20
|
+
logger = get_logger(__file__)
|
|
21
|
+
|
|
22
|
+
# Define which fields are allowed to be updated in a dossier, preventing unauthorized modification of sensitive fields
|
|
23
|
+
PERMITTED_KEYS = {
|
|
24
|
+
"title",
|
|
25
|
+
"query",
|
|
26
|
+
"leads",
|
|
27
|
+
"pivots",
|
|
28
|
+
"type",
|
|
29
|
+
"owner",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def exists(dossier_id: str) -> bool:
|
|
34
|
+
"""Check if a dossier exists in the datastore.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
dossier_id: Unique identifier for the dossier
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
True if the dossier exists, False otherwise
|
|
41
|
+
"""
|
|
42
|
+
return datastore().dossier.exists(dossier_id)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_dossier(
|
|
46
|
+
id: str,
|
|
47
|
+
as_odm: bool = False,
|
|
48
|
+
version: bool = False,
|
|
49
|
+
) -> Dossier:
|
|
50
|
+
"""Retrieve a dossier from the datastore.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
id: Unique identifier for the dossier
|
|
54
|
+
as_odm: Whether to return as ODM object (True) or dictionary (False)
|
|
55
|
+
version: Whether to include version information in the response
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Dossier object or dictionary containing dossier data
|
|
59
|
+
|
|
60
|
+
Raises:
|
|
61
|
+
NotFoundException: If the dossier doesn't exist
|
|
62
|
+
"""
|
|
63
|
+
return datastore().dossier.get_if_exists(key=id, as_obj=as_odm, version=version)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def create_dossier(dossier_data: Optional[Any], username: str) -> Dossier: # noqa: C901
|
|
67
|
+
"""Create a new dossier in the datastore.
|
|
68
|
+
|
|
69
|
+
This function validates the input data, ensures the query is valid by testing it
|
|
70
|
+
against the hit collection, and creates a new dossier with the specified parameters.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
dossier_data: Dictionary containing dossier configuration data
|
|
74
|
+
username: Username of the user creating the dossier
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Newly created Dossier object
|
|
78
|
+
|
|
79
|
+
Raises:
|
|
80
|
+
InvalidDataException: If data format is invalid, required fields are missing,
|
|
81
|
+
or the query is invalid
|
|
82
|
+
HowlerException: If there's an error during dossier creation
|
|
83
|
+
"""
|
|
84
|
+
# Validate input data format
|
|
85
|
+
if not isinstance(dossier_data, dict):
|
|
86
|
+
raise InvalidDataException("Invalid data format")
|
|
87
|
+
|
|
88
|
+
# Validate required fields for dossier creation
|
|
89
|
+
if "title" not in dossier_data:
|
|
90
|
+
raise InvalidDataException("You must specify a title when creating a dossier.")
|
|
91
|
+
|
|
92
|
+
if "query" not in dossier_data:
|
|
93
|
+
raise InvalidDataException("You must specify a query when creating a dossier.")
|
|
94
|
+
|
|
95
|
+
if "type" not in dossier_data:
|
|
96
|
+
raise InvalidDataException("You must specify a type when creating a dossier.")
|
|
97
|
+
|
|
98
|
+
storage = datastore()
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
# Validate the Lucene query by attempting to search with it
|
|
102
|
+
# This ensures the query syntax is correct before saving the dossier
|
|
103
|
+
if query := dossier_data.get("query", None):
|
|
104
|
+
storage.hit.search(query)
|
|
105
|
+
|
|
106
|
+
if "owner" not in dossier_data:
|
|
107
|
+
dossier_data["owner"] = username
|
|
108
|
+
|
|
109
|
+
dossier = Dossier(dossier_data)
|
|
110
|
+
|
|
111
|
+
# Validate pivot configurations to ensure no duplicate mapping keys
|
|
112
|
+
for pivot in dossier.pivots:
|
|
113
|
+
if len(pivot.mappings) != len(set(mapping.key for mapping in pivot.mappings)):
|
|
114
|
+
raise InvalidDataException("One of your pivots has duplicate keys set.")
|
|
115
|
+
|
|
116
|
+
# Ensure the owner is set to the current user (security measure)
|
|
117
|
+
dossier.owner = username
|
|
118
|
+
|
|
119
|
+
# Save the dossier to the datastore
|
|
120
|
+
storage.dossier.save(dossier.dossier_id, dossier)
|
|
121
|
+
|
|
122
|
+
# Commit the transaction to persist changes
|
|
123
|
+
storage.dossier.commit()
|
|
124
|
+
|
|
125
|
+
return dossier
|
|
126
|
+
except SearchException:
|
|
127
|
+
# Handle invalid Lucene query syntax
|
|
128
|
+
raise InvalidDataException("You must use a valid query when creating a dossier.")
|
|
129
|
+
except HowlerException as e:
|
|
130
|
+
# Handle other application-specific errors
|
|
131
|
+
raise InvalidDataException(str(e))
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def update_dossier(dossier_id: str, dossier_data: dict[str, Any], user: User) -> Dossier: # noqa: C901
|
|
135
|
+
"""Update one or more properties of a dossier in the database.
|
|
136
|
+
|
|
137
|
+
This function enforces access control rules and validates data before updating.
|
|
138
|
+
Personal dossiers can only be updated by their owners or admins.
|
|
139
|
+
Global dossiers can only be updated by their owners or admins.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
dossier_id: Unique identifier of the dossier to update
|
|
143
|
+
dossier_data: Dictionary containing fields to update
|
|
144
|
+
user: User object representing the requesting user
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
Updated Dossier object
|
|
148
|
+
|
|
149
|
+
Raises:
|
|
150
|
+
NotFoundException: If the dossier doesn't exist
|
|
151
|
+
InvalidDataException: If invalid fields are provided or data is malformed
|
|
152
|
+
ForbiddenException: If user lacks permission to update the dossier
|
|
153
|
+
"""
|
|
154
|
+
# Verify the dossier exists before attempting to update
|
|
155
|
+
if not exists(dossier_id):
|
|
156
|
+
raise NotFoundException(f"Dossier with id '{dossier_id}' does not exist.")
|
|
157
|
+
|
|
158
|
+
# Validate that only permitted fields are being updated
|
|
159
|
+
# This prevents unauthorized modification of sensitive fields
|
|
160
|
+
if set(dossier_data.keys()) - PERMITTED_KEYS:
|
|
161
|
+
raise InvalidDataException(f"Only {', '.join(PERMITTED_KEYS)} can be updated.")
|
|
162
|
+
|
|
163
|
+
storage = datastore()
|
|
164
|
+
|
|
165
|
+
# Retrieve the existing dossier for access control checks
|
|
166
|
+
existing_dossier: Dossier = get_dossier(dossier_id, as_odm=True)
|
|
167
|
+
|
|
168
|
+
# Enforce access control for personal dossiers
|
|
169
|
+
# Only the owner or admin users can modify personal dossiers
|
|
170
|
+
if existing_dossier.type == "personal" and existing_dossier.owner != user.uname and "admin" not in user.type:
|
|
171
|
+
raise ForbiddenException("You cannot update a personal dossier that is not owned by you.")
|
|
172
|
+
|
|
173
|
+
# Enforce access control for global dossiers
|
|
174
|
+
# Only the owner or admin users can modify global dossiers
|
|
175
|
+
if existing_dossier.type == "global" and existing_dossier.owner != user.uname and "admin" not in user.type:
|
|
176
|
+
raise ForbiddenException("Only the owner of a dossier and administrators can edit a global dossier.")
|
|
177
|
+
|
|
178
|
+
# Validate pivot configurations if they're being updated
|
|
179
|
+
# Ensure no duplicate mapping keys exist within any pivot
|
|
180
|
+
if "pivots" in dossier_data:
|
|
181
|
+
for pivot in dossier_data["pivots"]:
|
|
182
|
+
if len(pivot["mappings"]) != len(set(mapping["key"] for mapping in pivot["mappings"])):
|
|
183
|
+
raise InvalidDataException("One of your pivots has duplicate keys set.")
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
# Validate the Lucene query if it's being updated
|
|
187
|
+
if "query" in dossier_data:
|
|
188
|
+
# Test the query against the hit index to ensure it's valid
|
|
189
|
+
storage.hit.search(dossier_data["query"])
|
|
190
|
+
|
|
191
|
+
# Merge the new data with existing dossier data
|
|
192
|
+
new_data = Dossier(merge({}, existing_dossier.as_primitives(), dossier_data))
|
|
193
|
+
|
|
194
|
+
storage.dossier.save(dossier_id, new_data)
|
|
195
|
+
|
|
196
|
+
# Commit the transaction to persist changes
|
|
197
|
+
storage.dossier.commit()
|
|
198
|
+
|
|
199
|
+
return new_data
|
|
200
|
+
except SearchException:
|
|
201
|
+
# Handle invalid Lucene query syntax
|
|
202
|
+
raise InvalidDataException("You must use a valid query when updating a dossier.")
|
|
203
|
+
except (HowlerException, TypeError) as e:
|
|
204
|
+
# Log the error for debugging purposes
|
|
205
|
+
logger.exception("Error when updating dossier.")
|
|
206
|
+
# Provide a user-friendly error message while preserving the original exception
|
|
207
|
+
raise InvalidDataException("We were unable to update the dossier.", cause=e) from e
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def get_matching_dossiers(hit: dict[str, Any], dossiers: Optional[list[dict[str, Any]]] = None):
|
|
211
|
+
"""Get a list of dossiers that match a specific security alert/hit.
|
|
212
|
+
|
|
213
|
+
This function evaluates each dossier's query against the provided hit data
|
|
214
|
+
to determine which dossiers are relevant to the security event.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
hit: Dictionary containing security alert/hit data to match against
|
|
218
|
+
dossiers: Optional list of dossiers to check. If None, all dossiers
|
|
219
|
+
will be retrieved from the datastore
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
List of dossier dictionaries that match the provided hit
|
|
223
|
+
|
|
224
|
+
Note:
|
|
225
|
+
This function uses Lucene query matching to determine relevance.
|
|
226
|
+
Dossiers with no query are assumed to match all hits.
|
|
227
|
+
"""
|
|
228
|
+
# Retrieve all dossiers if none provided
|
|
229
|
+
if dossiers is None:
|
|
230
|
+
dossiers: list[dict[str, Any]] = datastore().dossier.search(
|
|
231
|
+
"dossier_id:*",
|
|
232
|
+
as_obj=False,
|
|
233
|
+
# TODO: Eventually implement caching here
|
|
234
|
+
rows=1000,
|
|
235
|
+
)["items"]
|
|
236
|
+
|
|
237
|
+
matching_dossiers: list[dict[str, Any]] = []
|
|
238
|
+
|
|
239
|
+
# Evaluate each dossier against the hit data
|
|
240
|
+
for dossier in cast(list[dict[str, Any]], dossiers):
|
|
241
|
+
# Dossiers without queries match all hits by default
|
|
242
|
+
# This allows for catch-all dossiers that collect all security events
|
|
243
|
+
if "query" not in dossier or dossier["query"] is None:
|
|
244
|
+
matching_dossiers.append(dossier)
|
|
245
|
+
continue
|
|
246
|
+
|
|
247
|
+
# Use Lucene service to check if the hit matches the dossier's query
|
|
248
|
+
# This determines if the security event is relevant to this investigation
|
|
249
|
+
if lucene_service.match(dossier["query"], hit):
|
|
250
|
+
matching_dossiers.append(dossier)
|
|
251
|
+
|
|
252
|
+
return matching_dossiers
|