howler-api 4.1.0.dev1623__tar.gz → 4.1.0.dev1624__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.1.0.dev1623 → howler_api-4.1.0.dev1624}/PKG-INFO +1 -1
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/api/v1/dossier.py +20 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/base.py +14 -4
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/helper.py +11 -11
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/models/pivot.py +33 -3
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/random_data.py +51 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/services/dossier_service.py +63 -14
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/pyproject.toml +1 -1
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/README.md +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/__init__.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/actions/__init__.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/actions/add_label.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/actions/add_to_bundle.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/actions/add_to_case.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/actions/change_field.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/actions/demote.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/actions/example_plugin.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/actions/prioritization.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/actions/promote.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/actions/remove_from_bundle.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/actions/remove_label.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/actions/transition.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/api/__init__.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/api/base.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/api/socket.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/api/v1/__init__.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/api/v1/action.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/api/v1/analytic.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/api/v1/auth.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/api/v1/clue.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/api/v1/configs.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/api/v1/help.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/api/v1/hit.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/api/v1/notebook.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/api/v1/overview.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/api/v1/search.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/api/v1/template.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/api/v1/tool.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/api/v1/user.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/api/v1/utils/__init__.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/api/v1/utils/etag.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/api/v1/utils/params.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/api/v1/utils/permission_helper.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/api/v1/view.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/api/v2/__init__.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/api/v2/case.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/api/v2/fuzzy.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/api/v2/ingest.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/api/v2/search.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/app.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/common/README.md +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/common/__init__.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/common/classification.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/common/classification.yml +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/common/exceptions.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/common/loader.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/common/logging/__init__.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/common/logging/audit.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/common/logging/format.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/common/net.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/common/net_static.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/common/random_user.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/common/swagger.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/config.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/cronjobs/__init__.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/cronjobs/action_queue_worker.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/cronjobs/correlation.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/cronjobs/retention.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/cronjobs/view_cleanup.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/datastore/README.md +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/datastore/__init__.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/datastore/bulk.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/datastore/collection.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/datastore/constants.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/datastore/exceptions.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/datastore/howler_store.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/datastore/migrations/__init__.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/datastore/migrations/action_owner.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/datastore/migrations/base.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/datastore/migrations/fix_process.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/datastore/migrations/runner.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/datastore/operations.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/datastore/schemas.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/datastore/store.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/datastore/support/__init__.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/datastore/support/build.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/datastore/support/schemas.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/datastore/types.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/datastore/utils.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/error.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/external/README.md +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/external/__init__.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/external/check_duplicates.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/external/generate_mitre.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/external/generate_sigma_rules.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/external/generate_tlds.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/external/reindex_data.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/external/run_migrations.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/external/wipe_databases.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/gunicorn_config.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/healthz.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/helper/__init__.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/helper/azure.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/helper/discover.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/helper/hit.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/helper/oauth.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/helper/search.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/helper/workflow.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/helper/ws.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/README.md +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/__init__.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/charter.txt +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/constants.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/howler_enum.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/mixins.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/models/__init__.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/models/action.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/models/analytic.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/models/assemblyline.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/models/aws.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/models/azure.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/models/case.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/models/cbs.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/models/clue.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/models/config.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/models/dossier.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/__init__.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/agent.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/autonomous_system.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/client.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/cloud.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/code_signature.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/container.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/dns.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/egress.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/elf.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/email.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/error.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/event.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/faas.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/file.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/geo.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/group.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/hash.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/host.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/http.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/ingress.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/interface.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/network.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/observer.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/organization.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/os.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/pe.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/process.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/registry.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/related.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/rule.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/server.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/threat.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/tls.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/url.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/user.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/user_agent.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/models/ecs/vulnerability.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/models/event.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/models/gcp.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/models/hit.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/models/howler_data.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/models/lead.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/models/localized_label.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/models/migration.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/models/overview.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/models/ownership.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/models/permission_request.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/models/record.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/models/template.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/models/user.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/models/view.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/odm/randomizer.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/patched.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/plugins/__init__.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/plugins/config.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/remote/__init__.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/remote/datatypes/README.md +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/remote/datatypes/__init__.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/remote/datatypes/counters.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/remote/datatypes/events.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/remote/datatypes/hash.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/remote/datatypes/lock.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/remote/datatypes/queues/__init__.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/remote/datatypes/queues/comms.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/remote/datatypes/queues/multi.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/remote/datatypes/queues/named.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/remote/datatypes/queues/priority.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/remote/datatypes/set.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/remote/datatypes/user_quota_tracker.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/security/__init__.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/security/login.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/security/socket.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/security/utils.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/services/__init__.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/services/action_service.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/services/analytic_service.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/services/auth_service.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/services/bundle_compat_service.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/services/case_service.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/services/comms_service.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/services/config_service.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/services/correlation_service.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/services/docs_service.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/services/event_service.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/services/fuzzy_service.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/services/hit_service.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/services/jwt_service.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/services/lucene_service.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/services/notebook_service.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/services/overview_service.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/services/permission_service.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/services/search_service.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/services/template_service.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/services/user_service.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/services/viewer_service.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/telemetry.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/utils/__init__.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/utils/annotations.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/utils/chunk.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/utils/compat.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/utils/constants.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/utils/dict_utils.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/utils/indexes.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/utils/isotime.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/utils/list_utils.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/utils/lucene.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/utils/net_utils.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/utils/path.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/utils/socket_utils.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/utils/str_utils.py +0 -0
- {howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/utils/uid.py +0 -0
|
@@ -93,6 +93,26 @@ def create_dossier(**kwargs):
|
|
|
93
93
|
return internal_error(err="An unknown error occured when creating the dossier.")
|
|
94
94
|
|
|
95
95
|
|
|
96
|
+
@generate_swagger_docs()
|
|
97
|
+
@dossier_api.route("/groups", methods=["GET"])
|
|
98
|
+
@api_login(required_priv=["R"])
|
|
99
|
+
def get_pivot_groups(user: User, **kwargs):
|
|
100
|
+
"""Suggest existing pivot group paths matching a prefix
|
|
101
|
+
|
|
102
|
+
Variables:
|
|
103
|
+
None
|
|
104
|
+
|
|
105
|
+
Optional Arguments:
|
|
106
|
+
prefix => A case-insensitive prefix to filter suggestions by
|
|
107
|
+
|
|
108
|
+
Result Example:
|
|
109
|
+
[
|
|
110
|
+
"network/dns" # Up to 25 matching group paths, used to autocomplete the pivot group field
|
|
111
|
+
]
|
|
112
|
+
"""
|
|
113
|
+
return ok(dossier_service.get_pivot_groups(request.args.get("prefix", "", type=str), username=user.uname))
|
|
114
|
+
|
|
115
|
+
|
|
96
116
|
@generate_swagger_docs()
|
|
97
117
|
@dossier_api.route("/<id>", methods=["GET"])
|
|
98
118
|
@api_login(required_priv=["R"])
|
|
@@ -32,6 +32,7 @@ from howler.common import loader
|
|
|
32
32
|
from howler.common.exceptions import HowlerKeyError, HowlerNotImplementedError, HowlerTypeError, HowlerValueError
|
|
33
33
|
from howler.common.net import is_valid_domain, is_valid_ip
|
|
34
34
|
from howler.odm.howler_enum import HowlerEnum
|
|
35
|
+
from howler.utils.compat import Self
|
|
35
36
|
from howler.utils.compat import StrEnum as PyStrEnum
|
|
36
37
|
from howler.utils.dict_utils import flatten, recursive_update
|
|
37
38
|
from howler.utils.isotime import now_as_iso
|
|
@@ -284,12 +285,21 @@ class Keyword(_Field):
|
|
|
284
285
|
Examples: file hashes, service names, document ids
|
|
285
286
|
"""
|
|
286
287
|
|
|
287
|
-
def
|
|
288
|
+
def __init__(self, *args, coerce=True, **kwargs):
|
|
289
|
+
super().__init__(*args, **kwargs)
|
|
290
|
+
self.coerce = coerce
|
|
291
|
+
|
|
292
|
+
def check(self: Self, value: Any | None, context=[], **kwargs):
|
|
288
293
|
# We have a special case for bytes here due to how often strings and bytes
|
|
289
294
|
# get mixed up in python apis
|
|
290
295
|
if self.optional and value is None:
|
|
291
296
|
return None
|
|
292
297
|
|
|
298
|
+
if not (self.coerce or isinstance(value, str)):
|
|
299
|
+
raise HowlerValueError(
|
|
300
|
+
f"[{'.'.join(context) or self.name}] Provided value is not a string, and coerce is set to false"
|
|
301
|
+
)
|
|
302
|
+
|
|
293
303
|
if isinstance(value, bytes):
|
|
294
304
|
raise HowlerValueError(f"[{'.'.join(context) or self.name}] Keyword doesn't accept bytes values")
|
|
295
305
|
|
|
@@ -422,7 +432,7 @@ class IP(Keyword):
|
|
|
422
432
|
super().__init__(*args, **kwargs)
|
|
423
433
|
self.validation_regex = re.compile(IP_ONLY_REGEX)
|
|
424
434
|
|
|
425
|
-
def check(self, value, context=[], **kwargs):
|
|
435
|
+
def check(self: Self, value: str, context: list[str] = [], **kwargs):
|
|
426
436
|
if not value:
|
|
427
437
|
return None
|
|
428
438
|
|
|
@@ -465,7 +475,7 @@ class Email(Keyword):
|
|
|
465
475
|
super().__init__(*args, **kwargs)
|
|
466
476
|
self.validation_regex = re.compile(EMAIL_REGEX)
|
|
467
477
|
|
|
468
|
-
def check(self, value, context=[], **kwargs):
|
|
478
|
+
def check(self: Self, value: str, context: list[str] = [], **kwargs):
|
|
469
479
|
if not value:
|
|
470
480
|
return None
|
|
471
481
|
|
|
@@ -489,7 +499,7 @@ class URI(Keyword):
|
|
|
489
499
|
super().__init__(*args, **kwargs)
|
|
490
500
|
self.validation_regex = re.compile(FULL_URI)
|
|
491
501
|
|
|
492
|
-
def check(self, value, context=[], **kwargs):
|
|
502
|
+
def check(self: Self, value: str, context: list[str] = [], **kwargs):
|
|
493
503
|
if not value:
|
|
494
504
|
return None
|
|
495
505
|
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import json
|
|
2
|
-
import random
|
|
3
2
|
from datetime import datetime, timedelta
|
|
4
3
|
from hashlib import md5
|
|
5
4
|
from math import ceil
|
|
6
|
-
from random import choice, randint, sample
|
|
5
|
+
from random import choice, choices, gauss, randint, random, sample
|
|
7
6
|
from typing import Any, cast
|
|
8
7
|
|
|
9
8
|
from howler.common.logging import get_logger
|
|
@@ -55,13 +54,13 @@ def generate_useful_hit( # noqa: C901
|
|
|
55
54
|
if CLASSIFICATION.enforce:
|
|
56
55
|
hit.classification = CLASSIFICATION.UNRESTRICTED
|
|
57
56
|
|
|
58
|
-
rand_seed = random
|
|
57
|
+
rand_seed = random()
|
|
59
58
|
|
|
60
59
|
timestamp = datetime.now() - timedelta(
|
|
61
60
|
days=round(rand_seed * 30),
|
|
62
|
-
hours=min(max(round(
|
|
63
|
-
minutes=
|
|
64
|
-
seconds=
|
|
61
|
+
hours=min(max(round(gauss(14, 3)), 0), 23),
|
|
62
|
+
minutes=randint(0, 59),
|
|
63
|
+
seconds=randint(0, 59),
|
|
65
64
|
)
|
|
66
65
|
|
|
67
66
|
hit.event.created = timestamp.isoformat() + "Z"
|
|
@@ -251,7 +250,7 @@ def generate_useful_hit( # noqa: C901
|
|
|
251
250
|
"icon": app["name"],
|
|
252
251
|
}
|
|
253
252
|
)
|
|
254
|
-
for app in
|
|
253
|
+
for app in choices(APPS, k=5)
|
|
255
254
|
)
|
|
256
255
|
except IndexError:
|
|
257
256
|
pass
|
|
@@ -362,13 +361,13 @@ def generate_useful_event( # noqa: C901
|
|
|
362
361
|
"Create a random, useful/cogent event for synthetic data"
|
|
363
362
|
event: Event = random_model_obj(cast(Model, Event))
|
|
364
363
|
|
|
365
|
-
rand_seed = random
|
|
364
|
+
rand_seed = random()
|
|
366
365
|
|
|
367
366
|
timestamp = datetime.now() - timedelta(
|
|
368
367
|
days=round(rand_seed * 30),
|
|
369
|
-
hours=min(max(round(
|
|
370
|
-
minutes=
|
|
371
|
-
seconds=
|
|
368
|
+
hours=min(max(round(gauss(14, 3)), 0), 23),
|
|
369
|
+
minutes=randint(0, 59),
|
|
370
|
+
seconds=randint(0, 59),
|
|
372
371
|
)
|
|
373
372
|
|
|
374
373
|
event.event.created = timestamp.isoformat() + "Z"
|
|
@@ -518,6 +517,7 @@ def generate_useful_dossier(users: list[User]) -> Dossier:
|
|
|
518
517
|
"label": {"en": get_random_word(), "fr": get_random_word()},
|
|
519
518
|
"value": f"https://google.com/search?q={{{{test{i}}}}} and {{{{custom_value}}}}",
|
|
520
519
|
"format": "link",
|
|
520
|
+
"group": choice([None, "parent/child", "test/group"]),
|
|
521
521
|
"mappings": [
|
|
522
522
|
{"key": f"test{i}", "field": choice(list(Hit.flat_fields().keys()))},
|
|
523
523
|
{"key": "custom_value", "field": "custom", "custom_value": get_random_word()},
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# mypy: ignore-errors
|
|
2
|
-
from typing import Optional
|
|
2
|
+
from typing import Any, Optional
|
|
3
3
|
|
|
4
4
|
from howler import odm
|
|
5
|
+
from howler.common.exceptions import HowlerValueError
|
|
5
6
|
from howler.odm.models.localized_label import LocalizedLabel
|
|
6
7
|
|
|
7
8
|
|
|
@@ -21,14 +22,35 @@ class Mapping(odm.Model):
|
|
|
21
22
|
)
|
|
22
23
|
|
|
23
24
|
|
|
25
|
+
def validate_group(group: str | Any) -> None:
|
|
26
|
+
"""Validate a slash-separated pivot group path."""
|
|
27
|
+
if group is None:
|
|
28
|
+
return
|
|
29
|
+
|
|
30
|
+
if group == "":
|
|
31
|
+
raise HowlerValueError("Group cannot be an empty string")
|
|
32
|
+
|
|
33
|
+
if not isinstance(group, str):
|
|
34
|
+
raise HowlerValueError('Data "group" should be a slash-separated string.')
|
|
35
|
+
|
|
36
|
+
if any(not section for section in group.split("/")):
|
|
37
|
+
raise HowlerValueError("A group must consist of a relative path with no empty segments (e.g. a/b/c)")
|
|
38
|
+
|
|
39
|
+
|
|
24
40
|
@odm.model(
|
|
25
41
|
index=False,
|
|
26
42
|
store=True,
|
|
27
43
|
description="The dossier object stores individual tabs/fields for a given alert.",
|
|
28
44
|
)
|
|
29
45
|
class Pivot(odm.Model):
|
|
30
|
-
icon:
|
|
31
|
-
description="An optional icon to use in the tab display for this dossier.",
|
|
46
|
+
icon: str = odm.Text(
|
|
47
|
+
description="An optional icon to use in the tab display for this dossier.", default="material-symbols:link"
|
|
48
|
+
)
|
|
49
|
+
group: str | None = odm.Keyword(
|
|
50
|
+
description="The folder path this pivot should be presented in when viewed in the UI.",
|
|
51
|
+
optional=True,
|
|
52
|
+
index=True,
|
|
53
|
+
coerce=False,
|
|
32
54
|
)
|
|
33
55
|
label: LocalizedLabel = odm.Compound(LocalizedLabel, description="Labels for the pivot in the UI.")
|
|
34
56
|
value: str = odm.Keyword(description="The link/plugin information to pivot on.")
|
|
@@ -38,3 +60,11 @@ class Pivot(odm.Model):
|
|
|
38
60
|
default=[],
|
|
39
61
|
description="A list of the mappings to use when activating a pivot.",
|
|
40
62
|
)
|
|
63
|
+
|
|
64
|
+
def __init__(self, data: dict = None, *args, **kwargs):
|
|
65
|
+
super().__init__(data, *args, **kwargs)
|
|
66
|
+
|
|
67
|
+
validate_group(self.group)
|
|
68
|
+
|
|
69
|
+
if len(self.mappings) != len({mapping.key for mapping in self.mappings}):
|
|
70
|
+
raise HowlerValueError("One of your pivots has duplicate keys set.")
|
|
@@ -36,10 +36,12 @@ from howler.odm.base import Keyword
|
|
|
36
36
|
from howler.odm.helper import generate_useful_case, generate_useful_dossier, generate_useful_event, generate_useful_hit
|
|
37
37
|
from howler.odm.models.action import Action
|
|
38
38
|
from howler.odm.models.analytic import Analytic, Comment, Notebook, TriageOptions
|
|
39
|
+
from howler.odm.models.dossier import Dossier
|
|
39
40
|
from howler.odm.models.ecs.event import EVENT_CATEGORIES
|
|
40
41
|
from howler.odm.models.hit import Hit
|
|
41
42
|
from howler.odm.models.howler_data import Assessment, Escalation, Scrutiny, Status
|
|
42
43
|
from howler.odm.models.overview import Overview
|
|
44
|
+
from howler.odm.models.pivot import Pivot
|
|
43
45
|
from howler.odm.models.template import Template
|
|
44
46
|
from howler.odm.models.user import User
|
|
45
47
|
from howler.odm.models.view import View
|
|
@@ -881,6 +883,7 @@ def wipe_actions(ds: HowlerDatastore):
|
|
|
881
883
|
def create_dossiers(ds: HowlerDatastore, num_dossiers: int = 5):
|
|
882
884
|
"Create random dossiers"
|
|
883
885
|
users = ds.user.search("*:*")["items"]
|
|
886
|
+
grouped_paths = ("network", "network/dns", "network/http")
|
|
884
887
|
for index in range(num_dossiers):
|
|
885
888
|
dossier = generate_useful_dossier(users)
|
|
886
889
|
ownership_data = _get_ownership_data(users, dossier.owner)
|
|
@@ -890,8 +893,56 @@ def create_dossiers(ds: HowlerDatastore, num_dossiers: int = 5):
|
|
|
890
893
|
dossier.type = "personal"
|
|
891
894
|
elif index == 1:
|
|
892
895
|
dossier.type = "global"
|
|
896
|
+
|
|
897
|
+
# Keep one dossier's random pivot data intact, while ensuring the mock data
|
|
898
|
+
# always includes shared and nested groups for the grouped-pivot UI.
|
|
899
|
+
if index > 0:
|
|
900
|
+
grouped_path = grouped_paths[(index - 1) % len(grouped_paths)]
|
|
901
|
+
for pivot in dossier.pivots:
|
|
902
|
+
pivot.group = grouped_path
|
|
903
|
+
|
|
893
904
|
ds.dossier.save(dossier.dossier_id, dossier)
|
|
894
905
|
|
|
906
|
+
password_checker_dossier = Dossier(
|
|
907
|
+
{
|
|
908
|
+
"title": "Password Checker Pivot Folders",
|
|
909
|
+
"query": 'howler.analytic:"Password Checker"',
|
|
910
|
+
"type": "global",
|
|
911
|
+
**_get_ownership_data(users, "admin"),
|
|
912
|
+
"leads": [],
|
|
913
|
+
}
|
|
914
|
+
)
|
|
915
|
+
pivot_groups = [
|
|
916
|
+
None,
|
|
917
|
+
"credentials",
|
|
918
|
+
"credentials/hash",
|
|
919
|
+
"credentials/metadata",
|
|
920
|
+
"validation",
|
|
921
|
+
"validation/length",
|
|
922
|
+
"validation/complexity",
|
|
923
|
+
"validation/reuse",
|
|
924
|
+
"exposure",
|
|
925
|
+
"exposure/breach",
|
|
926
|
+
"exposure/darkweb",
|
|
927
|
+
"exposure/thirdparty",
|
|
928
|
+
"recommendations",
|
|
929
|
+
"recommendations/rotation",
|
|
930
|
+
"recommendations/monitoring",
|
|
931
|
+
]
|
|
932
|
+
for index, pivot_group in enumerate(pivot_groups, start=1):
|
|
933
|
+
password_checker_dossier.pivots.append(
|
|
934
|
+
Pivot(
|
|
935
|
+
{
|
|
936
|
+
"label": {"en": f"Password Check {index}", "fr": f"Verification mot de passe {index}"},
|
|
937
|
+
"value": "https://password-checker.example.test/{{pivot_value}}",
|
|
938
|
+
"format": "link",
|
|
939
|
+
"group": pivot_group,
|
|
940
|
+
"mappings": [{"key": "pivot_value", "field": "howler.id"}],
|
|
941
|
+
}
|
|
942
|
+
)
|
|
943
|
+
)
|
|
944
|
+
|
|
945
|
+
ds.dossier.save(password_checker_dossier.dossier_id, password_checker_dossier)
|
|
895
946
|
ds.dossier.commit()
|
|
896
947
|
|
|
897
948
|
|
|
@@ -9,13 +9,20 @@ from typing import Any, Literal, Optional, cast, overload
|
|
|
9
9
|
|
|
10
10
|
from mergedeep.mergedeep import merge
|
|
11
11
|
|
|
12
|
-
from howler.common.exceptions import
|
|
12
|
+
from howler.common.exceptions import (
|
|
13
|
+
ForbiddenException,
|
|
14
|
+
HowlerException,
|
|
15
|
+
HowlerValueError,
|
|
16
|
+
InvalidDataException,
|
|
17
|
+
NotFoundException,
|
|
18
|
+
)
|
|
13
19
|
from howler.common.loader import datastore
|
|
14
20
|
from howler.common.logging import get_logger
|
|
15
21
|
from howler.datastore.exceptions import SearchException
|
|
16
22
|
from howler.odm.models.dossier import Dossier
|
|
17
23
|
from howler.odm.models.user import User
|
|
18
24
|
from howler.services import lucene_service
|
|
25
|
+
from howler.utils.str_utils import sanitize_lucene_query
|
|
19
26
|
|
|
20
27
|
logger = get_logger(__file__)
|
|
21
28
|
|
|
@@ -28,6 +35,12 @@ PERMITTED_KEYS = {
|
|
|
28
35
|
"type",
|
|
29
36
|
}
|
|
30
37
|
|
|
38
|
+
# Cap the number of group suggestions returned, so the endpoint can't be used to dump the entire group list
|
|
39
|
+
MAX_GROUP_SUGGESTIONS = 25
|
|
40
|
+
GROUP_SUGGESTION_PAGE_SIZE = 100
|
|
41
|
+
# Group suggestions are a convenience feature, so bound aggregation work for broad prefixes.
|
|
42
|
+
MAX_GROUP_SUGGESTION_PAGES = 5
|
|
43
|
+
|
|
31
44
|
|
|
32
45
|
def exists(dossier_id: str) -> bool:
|
|
33
46
|
"""Check if a dossier exists in the datastore.
|
|
@@ -140,11 +153,6 @@ def create_dossier( # noqa: C901
|
|
|
140
153
|
|
|
141
154
|
dossier = Dossier(dossier_data)
|
|
142
155
|
|
|
143
|
-
# Validate pivot configurations to ensure no duplicate mapping keys
|
|
144
|
-
for pivot in dossier.pivots:
|
|
145
|
-
if len(pivot.mappings) != len(set(mapping.key for mapping in pivot.mappings)):
|
|
146
|
-
raise InvalidDataException("One of your pivots has duplicate keys set.")
|
|
147
|
-
|
|
148
156
|
# Ensure the owner is set to the current user.
|
|
149
157
|
dossier.owner = username
|
|
150
158
|
|
|
@@ -208,14 +216,6 @@ def update_dossier( # noqa: C901
|
|
|
208
216
|
"You cannot update a dossier that is not owned by you, or you are not an administrator of."
|
|
209
217
|
)
|
|
210
218
|
|
|
211
|
-
# Validate pivot configurations if they're being updated
|
|
212
|
-
# Ensure no duplicate mapping keys exist within any pivot
|
|
213
|
-
if "pivots" in dossier_data:
|
|
214
|
-
for pivot in dossier_data["pivots"]:
|
|
215
|
-
mappings = pivot.get("mappings") or []
|
|
216
|
-
if len(mappings) != len(set(mapping.get("key") for mapping in mappings)):
|
|
217
|
-
raise InvalidDataException("One of your pivots has duplicate keys set.")
|
|
218
|
-
|
|
219
219
|
try:
|
|
220
220
|
# Validate the Lucene query if it's being updated
|
|
221
221
|
if "query" in dossier_data:
|
|
@@ -231,6 +231,8 @@ def update_dossier( # noqa: C901
|
|
|
231
231
|
except SearchException:
|
|
232
232
|
# Handle invalid Lucene query syntax
|
|
233
233
|
raise InvalidDataException("You must use a valid query when updating a dossier.")
|
|
234
|
+
except HowlerValueError as e:
|
|
235
|
+
raise InvalidDataException(str(e), cause=e) from e
|
|
234
236
|
except (HowlerException, TypeError) as e:
|
|
235
237
|
# Log the error for debugging purposes
|
|
236
238
|
logger.exception("Error when updating dossier.")
|
|
@@ -238,6 +240,53 @@ def update_dossier( # noqa: C901
|
|
|
238
240
|
raise InvalidDataException("We were unable to update the dossier.", cause=e) from e
|
|
239
241
|
|
|
240
242
|
|
|
243
|
+
def get_pivot_groups(prefix: str, username: str) -> list[str]:
|
|
244
|
+
"""Suggest existing pivot group paths matching a prefix, so users can reuse groups already in use.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
prefix: Case-insensitive prefix to filter suggestions by.
|
|
248
|
+
username: The requesting user, used to scope which dossiers are visible.
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
Up to MAX_GROUP_SUGGESTIONS unique matching group paths from the visible dossier search, sorted alphabetically.
|
|
252
|
+
Suggestions are best-effort and scan up to MAX_GROUP_SUGGESTION_PAGES matching aggregation pages.
|
|
253
|
+
"""
|
|
254
|
+
groups: set[str] = set()
|
|
255
|
+
lowered_prefix: str = (prefix or "").lower()
|
|
256
|
+
after: dict[str, str] | None = None
|
|
257
|
+
|
|
258
|
+
for _ in range(MAX_GROUP_SUGGESTION_PAGES):
|
|
259
|
+
composite: dict[str, Any] = {
|
|
260
|
+
"size": GROUP_SUGGESTION_PAGE_SIZE,
|
|
261
|
+
"sources": [{"group": {"terms": {"field": "pivots.group"}}}],
|
|
262
|
+
}
|
|
263
|
+
if after:
|
|
264
|
+
composite["after"] = after
|
|
265
|
+
|
|
266
|
+
group_filter: dict[str, Any] = {"match_all": {}}
|
|
267
|
+
if prefix:
|
|
268
|
+
group_filter = {"prefix": {"pivots.group": {"value": prefix, "case_insensitive": True}}}
|
|
269
|
+
|
|
270
|
+
result = datastore().dossier.search(
|
|
271
|
+
f'(type:global OR owner:("{sanitize_lucene_query(username)}" OR none))',
|
|
272
|
+
rows=0,
|
|
273
|
+
aggregations=[("pivot_groups", {"filter": group_filter, "aggs": {"groups": {"composite": composite}}})],
|
|
274
|
+
)
|
|
275
|
+
aggregation = result["aggregations"]["pivot_groups"]["groups"]
|
|
276
|
+
|
|
277
|
+
for bucket in aggregation["buckets"]:
|
|
278
|
+
group = bucket["key"]["group"]
|
|
279
|
+
if not group.lower().startswith(lowered_prefix):
|
|
280
|
+
continue
|
|
281
|
+
groups.add(group)
|
|
282
|
+
|
|
283
|
+
after = aggregation.get("after_key")
|
|
284
|
+
if not after:
|
|
285
|
+
break
|
|
286
|
+
|
|
287
|
+
return sorted(groups)[:MAX_GROUP_SUGGESTIONS]
|
|
288
|
+
|
|
289
|
+
|
|
241
290
|
def get_matching_dossiers(
|
|
242
291
|
hit: dict[str, Any], dossiers: list[dict[str, Any]] | None = None, username: str | None = None
|
|
243
292
|
):
|
|
@@ -244,7 +244,7 @@ suppress-none-returning = true
|
|
|
244
244
|
[tool.poetry]
|
|
245
245
|
package-mode = true
|
|
246
246
|
name = "howler-api"
|
|
247
|
-
version = "4.1.0.
|
|
247
|
+
version = "4.1.0.dev1624"
|
|
248
248
|
description = "Howler - API server"
|
|
249
249
|
authors = [
|
|
250
250
|
"Canadian Centre for Cyber Security <howler@cyber.gc.ca>",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/api/v1/utils/permission_helper.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/cronjobs/action_queue_worker.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/datastore/migrations/__init__.py
RENAMED
|
File without changes
|
{howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/datastore/migrations/action_owner.py
RENAMED
|
File without changes
|
|
File without changes
|
{howler_api-4.1.0.dev1623 → howler_api-4.1.0.dev1624}/howler/datastore/migrations/fix_process.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|