howler-api 4.1.0.dev1468__tar.gz → 4.1.0.dev1502__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.dev1468 → howler_api-4.1.0.dev1502}/PKG-INFO +1 -1
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/api/base.py +1 -1
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/api/socket.py +1 -1
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/api/v1/__init__.py +1 -1
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/api/v1/action.py +1 -1
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/api/v1/analytic.py +1 -1
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/api/v1/auth.py +1 -1
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/api/v1/clue.py +1 -1
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/api/v1/dossier.py +1 -1
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/api/v1/help.py +1 -1
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/api/v1/hit.py +1 -1
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/api/v1/notebook.py +1 -1
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/api/v1/overview.py +1 -1
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/api/v1/search.py +1 -1
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/api/v1/template.py +1 -1
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/api/v1/tool.py +1 -1
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/api/v1/user.py +1 -1
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/api/v1/view.py +1 -1
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/api/v2/__init__.py +1 -1
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/api/v2/case.py +1 -1
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/api/v2/fuzzy.py +1 -1
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/api/v2/ingest.py +1 -1
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/api/v2/search.py +1 -1
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/cronjobs/action_queue_worker.py +5 -3
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/config.py +13 -1
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/security/utils.py +10 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/services/action_service.py +59 -9
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/services/auth_service.py +44 -1
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/services/correlation_service.py +1 -1
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/services/hit_service.py +5 -2
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/services/jwt_service.py +21 -0
- howler_api-4.1.0.dev1502/howler/utils/__init__.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/pyproject.toml +3 -3
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/README.md +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/__init__.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/actions/__init__.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/actions/add_label.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/actions/add_to_bundle.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/actions/add_to_case.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/actions/change_field.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/actions/demote.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/actions/example_plugin.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/actions/prioritization.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/actions/promote.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/actions/remove_from_bundle.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/actions/remove_label.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/actions/transition.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/api/__init__.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/api/v1/configs.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/api/v1/utils/__init__.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/api/v1/utils/etag.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/api/v1/utils/params.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/app.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/common/README.md +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/common/__init__.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/common/classification.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/common/classification.yml +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/common/exceptions.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/common/loader.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/common/logging/__init__.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/common/logging/audit.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/common/logging/format.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/common/net.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/common/net_static.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/common/random_user.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/common/swagger.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/config.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/cronjobs/__init__.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/cronjobs/correlation.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/cronjobs/retention.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/cronjobs/view_cleanup.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/datastore/README.md +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/datastore/__init__.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/datastore/bulk.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/datastore/collection.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/datastore/constants.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/datastore/exceptions.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/datastore/howler_store.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/datastore/migrations/fix_process.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/datastore/operations.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/datastore/schemas.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/datastore/store.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/datastore/support/__init__.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/datastore/support/build.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/datastore/support/schemas.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/datastore/types.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/datastore/utils.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/error.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/external/README.md +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/external/__init__.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/external/check_duplicates.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/external/generate_mitre.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/external/generate_sigma_rules.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/external/generate_tlds.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/external/reindex_data.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/external/wipe_databases.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/gunicorn_config.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/healthz.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/helper/__init__.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/helper/azure.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/helper/discover.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/helper/hit.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/helper/oauth.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/helper/search.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/helper/workflow.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/helper/ws.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/README.md +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/__init__.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/base.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/charter.txt +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/constants.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/helper.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/howler_enum.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/mixins.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/__init__.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/action.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/analytic.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/assemblyline.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/aws.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/azure.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/case.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/cbs.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/clue.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/dossier.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/__init__.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/agent.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/autonomous_system.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/client.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/cloud.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/code_signature.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/container.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/dns.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/egress.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/elf.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/email.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/error.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/event.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/faas.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/file.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/geo.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/group.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/hash.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/host.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/http.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/ingress.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/interface.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/network.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/observer.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/organization.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/os.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/pe.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/process.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/registry.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/related.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/rule.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/server.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/threat.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/tls.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/url.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/user.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/user_agent.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/ecs/vulnerability.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/event.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/gcp.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/hit.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/howler_data.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/lead.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/localized_label.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/overview.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/pivot.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/record.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/template.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/user.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/models/view.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/random_data.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/odm/randomizer.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/patched.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/plugins/__init__.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/plugins/config.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/remote/__init__.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/remote/datatypes/README.md +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/remote/datatypes/__init__.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/remote/datatypes/counters.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/remote/datatypes/events.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/remote/datatypes/hash.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/remote/datatypes/lock.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/remote/datatypes/queues/__init__.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/remote/datatypes/queues/comms.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/remote/datatypes/queues/multi.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/remote/datatypes/queues/named.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/remote/datatypes/queues/priority.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/remote/datatypes/set.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/remote/datatypes/user_quota_tracker.py +0 -0
- {howler_api-4.1.0.dev1468/howler/services → howler_api-4.1.0.dev1502/howler/security}/__init__.py +0 -0
- /howler_api-4.1.0.dev1468/howler/security/__init__.py → /howler_api-4.1.0.dev1502/howler/security/login.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/security/socket.py +0 -0
- {howler_api-4.1.0.dev1468/howler/utils → howler_api-4.1.0.dev1502/howler/services}/__init__.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/services/analytic_service.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/services/bundle_compat_service.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/services/case_service.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/services/comms_service.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/services/config_service.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/services/docs_service.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/services/dossier_service.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/services/event_service.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/services/fuzzy_service.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/services/lucene_service.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/services/notebook_service.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/services/overview_service.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/services/search_service.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/services/template_service.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/services/user_service.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/services/viewer_service.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/telemetry.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/utils/annotations.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/utils/chunk.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/utils/compat.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/utils/constants.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/utils/dict_utils.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/utils/indexes.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/utils/isotime.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/utils/list_utils.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/utils/lucene.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/utils/net_utils.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/utils/path.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/utils/socket_utils.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/utils/str_utils.py +0 -0
- {howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/utils/uid.py +0 -0
|
@@ -11,7 +11,7 @@ import howler.services.viewer_service as viewer_service
|
|
|
11
11
|
from howler.api import ok, unauthorized
|
|
12
12
|
from howler.common.logging import get_logger
|
|
13
13
|
from howler.helper.ws import ConnectionClosed, Server
|
|
14
|
-
from howler.security import api_login
|
|
14
|
+
from howler.security.login import api_login
|
|
15
15
|
from howler.security.socket import websocket_auth, ws_response
|
|
16
16
|
from howler.utils.socket_utils import check_action
|
|
17
17
|
|
|
@@ -12,7 +12,7 @@ from howler.common.swagger import generate_swagger_docs
|
|
|
12
12
|
from howler.config import CLASSIFICATION
|
|
13
13
|
from howler.odm.models.action import Action
|
|
14
14
|
from howler.odm.models.user import User
|
|
15
|
-
from howler.security import api_login
|
|
15
|
+
from howler.security.login import api_login
|
|
16
16
|
from howler.services import action_service
|
|
17
17
|
|
|
18
18
|
SUB_API = "action"
|
|
@@ -20,7 +20,7 @@ from howler.datastore.exceptions import DataStoreException
|
|
|
20
20
|
from howler.datastore.operations import OdmHelper
|
|
21
21
|
from howler.odm.models.analytic import Analytic, Comment, Notebook, TriageOptions
|
|
22
22
|
from howler.odm.models.user import User
|
|
23
|
-
from howler.security import api_login
|
|
23
|
+
from howler.security.login import api_login
|
|
24
24
|
from howler.services import analytic_service, user_service
|
|
25
25
|
|
|
26
26
|
MAX_COMMENT_LEN = 5000
|
|
@@ -31,7 +31,7 @@ from howler.common.logging import get_logger
|
|
|
31
31
|
from howler.common.swagger import generate_swagger_docs
|
|
32
32
|
from howler.config import config
|
|
33
33
|
from howler.odm.models.user import ApiKey, User
|
|
34
|
-
from howler.security import api_login
|
|
34
|
+
from howler.security.login import api_login
|
|
35
35
|
from howler.security.utils import generate_random_secret
|
|
36
36
|
from howler.services import jwt_service
|
|
37
37
|
from howler.utils.str_utils import default_string_value
|
|
@@ -10,7 +10,7 @@ from howler.common.logging import get_logger
|
|
|
10
10
|
from howler.common.swagger import generate_swagger_docs
|
|
11
11
|
from howler.config import cache, config
|
|
12
12
|
from howler.plugins import get_plugins
|
|
13
|
-
from howler.security import api_login
|
|
13
|
+
from howler.security.login import api_login
|
|
14
14
|
from howler.utils.constants import TESTING
|
|
15
15
|
|
|
16
16
|
SUB_API = "clue"
|
|
@@ -13,7 +13,7 @@ from howler.common.logging import get_logger
|
|
|
13
13
|
from howler.common.swagger import generate_swagger_docs
|
|
14
14
|
from howler.odm.models.dossier import Dossier
|
|
15
15
|
from howler.odm.models.user import User
|
|
16
|
-
from howler.security import api_login
|
|
16
|
+
from howler.security.login import api_login
|
|
17
17
|
from howler.services import dossier_service
|
|
18
18
|
|
|
19
19
|
SUB_API = "dossier"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from howler.api import make_subapi_blueprint, ok
|
|
2
2
|
from howler.common.swagger import generate_swagger_docs
|
|
3
3
|
from howler.config import CLASSIFICATION
|
|
4
|
-
from howler.security import api_login
|
|
4
|
+
from howler.security.login import api_login
|
|
5
5
|
|
|
6
6
|
SUB_API = "help"
|
|
7
7
|
classification_definition = CLASSIFICATION.get_parsed_classification_definition()
|
|
@@ -29,7 +29,7 @@ from howler.helper.workflow import WorkflowException
|
|
|
29
29
|
from howler.odm.models.hit import Hit
|
|
30
30
|
from howler.odm.models.howler_data import Comment, HitOperationType, HitStatusTransition
|
|
31
31
|
from howler.odm.models.user import User
|
|
32
|
-
from howler.security import api_login
|
|
32
|
+
from howler.security.login import api_login
|
|
33
33
|
from howler.services import action_service, analytic_service, comms_service, correlation_service, hit_service
|
|
34
34
|
from howler.utils.constants import DEBUG_FORCE_REFRESH
|
|
35
35
|
from howler.utils.str_utils import sanitize_lucene_query
|
|
@@ -2,7 +2,7 @@ from flask import request
|
|
|
2
2
|
|
|
3
3
|
from howler.api import bad_request, internal_error, make_subapi_blueprint, ok
|
|
4
4
|
from howler.common.swagger import generate_swagger_docs
|
|
5
|
-
from howler.security import api_login
|
|
5
|
+
from howler.security.login import api_login
|
|
6
6
|
from howler.services import notebook_service
|
|
7
7
|
|
|
8
8
|
SUB_API = "notebook"
|
|
@@ -17,7 +17,7 @@ from howler.common.logging import get_logger
|
|
|
17
17
|
from howler.common.swagger import generate_swagger_docs
|
|
18
18
|
from howler.odm.models.overview import Overview
|
|
19
19
|
from howler.odm.models.user import User
|
|
20
|
-
from howler.security import api_login
|
|
20
|
+
from howler.security.login import api_login
|
|
21
21
|
from howler.utils.str_utils import sanitize_lucene_query
|
|
22
22
|
|
|
23
23
|
SUB_API = "overview"
|
|
@@ -16,7 +16,7 @@ from howler.common.swagger import generate_swagger_docs
|
|
|
16
16
|
from howler.datastore.exceptions import SearchException
|
|
17
17
|
from howler.helper.search import get_collection, get_default_sort, has_access_control, list_all_fields
|
|
18
18
|
from howler.odm.models.user import User
|
|
19
|
-
from howler.security import api_login
|
|
19
|
+
from howler.security.login import api_login
|
|
20
20
|
from howler.services import hit_service, lucene_service
|
|
21
21
|
from howler.utils.net_utils import generate_params
|
|
22
22
|
|
|
@@ -18,7 +18,7 @@ from howler.common.swagger import generate_swagger_docs
|
|
|
18
18
|
from howler.datastore.operations import OdmHelper
|
|
19
19
|
from howler.odm.models.template import Template
|
|
20
20
|
from howler.odm.models.user import User
|
|
21
|
-
from howler.security import api_login
|
|
21
|
+
from howler.security.login import api_login
|
|
22
22
|
from howler.utils.str_utils import sanitize_lucene_query
|
|
23
23
|
|
|
24
24
|
SUB_API = "template"
|
|
@@ -12,7 +12,7 @@ from howler.datastore.operations import OdmHelper
|
|
|
12
12
|
from howler.odm.base import _Field
|
|
13
13
|
from howler.odm.models.hit import Hit
|
|
14
14
|
from howler.odm.models.user import User
|
|
15
|
-
from howler.security import api_login
|
|
15
|
+
from howler.security.login import api_login
|
|
16
16
|
from howler.services import action_service, analytic_service, correlation_service, hit_service
|
|
17
17
|
from howler.utils.constants import DEBUG_FORCE_REFRESH
|
|
18
18
|
from howler.utils.dict_utils import flatten
|
|
@@ -21,7 +21,7 @@ from howler.common.swagger import generate_swagger_docs
|
|
|
21
21
|
from howler.config import config
|
|
22
22
|
from howler.helper.oauth import fetch_groups
|
|
23
23
|
from howler.odm.models.user import User
|
|
24
|
-
from howler.security import api_login
|
|
24
|
+
from howler.security.login import api_login
|
|
25
25
|
from howler.security.utils import check_password_requirements, get_password_hash, get_password_requirement_message
|
|
26
26
|
|
|
27
27
|
SUB_API = "user"
|
|
@@ -12,7 +12,7 @@ from howler.common.swagger import generate_swagger_docs
|
|
|
12
12
|
from howler.datastore.exceptions import SearchException
|
|
13
13
|
from howler.odm.models.user import User
|
|
14
14
|
from howler.odm.models.view import View
|
|
15
|
-
from howler.security import api_login
|
|
15
|
+
from howler.security.login import api_login
|
|
16
16
|
|
|
17
17
|
SUB_API = "view"
|
|
18
18
|
view_api = make_subapi_blueprint(SUB_API, api_version=1)
|
|
@@ -12,7 +12,7 @@ from howler.common.swagger import generate_swagger_docs
|
|
|
12
12
|
from howler.datastore.exceptions import DataStoreException
|
|
13
13
|
from howler.odm.models.case import Case, CaseItem
|
|
14
14
|
from howler.odm.models.user import User
|
|
15
|
-
from howler.security import api_login
|
|
15
|
+
from howler.security.login import api_login
|
|
16
16
|
from howler.services import case_service
|
|
17
17
|
|
|
18
18
|
SUB_API = "case"
|
|
@@ -6,7 +6,7 @@ from howler.common.logging.audit import audit
|
|
|
6
6
|
from howler.common.swagger import generate_swagger_docs
|
|
7
7
|
from howler.datastore.exceptions import SearchException
|
|
8
8
|
from howler.helper.search import has_access_control
|
|
9
|
-
from howler.security import api_login
|
|
9
|
+
from howler.security.login import api_login
|
|
10
10
|
from howler.services import fuzzy_service
|
|
11
11
|
|
|
12
12
|
SUB_API = "fuzzy"
|
|
@@ -18,7 +18,7 @@ from howler.datastore.operations import OdmHelper, OdmUpdateOperation
|
|
|
18
18
|
from howler.odm.models.event import Event
|
|
19
19
|
from howler.odm.models.hit import Hit
|
|
20
20
|
from howler.odm.models.user import User
|
|
21
|
-
from howler.security import api_login
|
|
21
|
+
from howler.security.login import api_login
|
|
22
22
|
from howler.services import correlation_service, event_service, hit_service
|
|
23
23
|
from howler.utils.dict_utils import flatten
|
|
24
24
|
|
|
@@ -15,7 +15,7 @@ from howler.common.swagger import generate_swagger_docs
|
|
|
15
15
|
from howler.datastore.exceptions import SearchException
|
|
16
16
|
from howler.helper.search import get_collection, has_access_control
|
|
17
17
|
from howler.odm.models.user import User
|
|
18
|
-
from howler.security import api_login
|
|
18
|
+
from howler.security.login import api_login
|
|
19
19
|
from howler.services import hit_service, lucene_service, search_service
|
|
20
20
|
from howler.services.search_service import SensitiveUserFieldsException
|
|
21
21
|
from howler.utils.net_utils import generate_params
|
{howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/cronjobs/action_queue_worker.py
RENAMED
|
@@ -50,16 +50,18 @@ def run_worker(trigger: str) -> None: # pragma: no cover – long-running loop,
|
|
|
50
50
|
if item is not None:
|
|
51
51
|
batch.append(item)
|
|
52
52
|
|
|
53
|
-
if len(batch) % 10 == 0 or len(batch) >= BATCH_SIZE:
|
|
53
|
+
if len(batch) > 0 and (len(batch) % 10 == 0 or len(batch) >= BATCH_SIZE):
|
|
54
54
|
logger.info("Batch size: %s", len(batch))
|
|
55
55
|
|
|
56
56
|
if len(batch) >= BATCH_SIZE or (item is None and batch):
|
|
57
57
|
finalized_batch = [*batch]
|
|
58
58
|
batch = []
|
|
59
|
-
logger.debug("Processing action batch of %d item(s) for trigger=%s", len(
|
|
59
|
+
logger.debug("Processing action batch of %d item(s) for trigger=%s", len(finalized_batch), trigger)
|
|
60
60
|
try:
|
|
61
61
|
process_action_batch(trigger, finalized_batch)
|
|
62
|
-
logger.info(
|
|
62
|
+
logger.info(
|
|
63
|
+
"Action batch complete: %d item(s) processed for trigger=%s", len(finalized_batch), trigger
|
|
64
|
+
)
|
|
63
65
|
except Exception:
|
|
64
66
|
logger.exception("Error processing action batch for trigger=%s", trigger)
|
|
65
67
|
except Exception:
|
|
@@ -4,7 +4,7 @@ import os
|
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
from typing import Literal, Optional
|
|
6
6
|
|
|
7
|
-
from pydantic import BaseModel, Field
|
|
7
|
+
from pydantic import BaseModel, Field, field_validator
|
|
8
8
|
from pydantic_settings import BaseSettings, PydanticBaseSettingsSource, SettingsConfigDict, YamlConfigSettingsSource
|
|
9
9
|
|
|
10
10
|
from howler.common.logging.format import HWL_DATE_FORMAT, HWL_LOG_FORMAT
|
|
@@ -476,6 +476,10 @@ class System(BaseModel):
|
|
|
476
476
|
"""
|
|
477
477
|
|
|
478
478
|
type: Literal["production", "staging", "development"] = Field(default="development", description="Type of system")
|
|
479
|
+
encryption_key: str | None = Field(
|
|
480
|
+
default=None,
|
|
481
|
+
description="32-byte secret used to encrypt JWE payloads such as queued authorization tokens.",
|
|
482
|
+
)
|
|
479
483
|
retention: Retention = Retention()
|
|
480
484
|
"Retention Configuration"
|
|
481
485
|
view_cleanup: ViewCleanup = ViewCleanup()
|
|
@@ -485,6 +489,14 @@ class System(BaseModel):
|
|
|
485
489
|
action_queue: ActionQueue = ActionQueue()
|
|
486
490
|
"Action Queue Worker Configuration"
|
|
487
491
|
|
|
492
|
+
@field_validator("encryption_key")
|
|
493
|
+
@classmethod
|
|
494
|
+
def validate_encryption_key(cls, value: str | None) -> str | None:
|
|
495
|
+
if value is not None and len(value.encode("utf-8")) != 32:
|
|
496
|
+
raise ValueError("System encryption_key must be exactly 32 bytes when UTF-8 encoded")
|
|
497
|
+
|
|
498
|
+
return value
|
|
499
|
+
|
|
488
500
|
|
|
489
501
|
class UI(BaseModel):
|
|
490
502
|
"""User interface and web server configuration.
|
|
@@ -7,6 +7,7 @@ from urllib.parse import urlparse
|
|
|
7
7
|
from opentelemetry import trace
|
|
8
8
|
from passlib.hash import bcrypt
|
|
9
9
|
|
|
10
|
+
from howler.common.exceptions import HowlerValueError
|
|
10
11
|
from howler.config import config
|
|
11
12
|
|
|
12
13
|
tracer = trace.get_tracer(__name__)
|
|
@@ -185,3 +186,12 @@ def get_disco_url(host_url: Optional[str]):
|
|
|
185
186
|
return config.discovery.url
|
|
186
187
|
else:
|
|
187
188
|
return config.discovery.url
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def encryption_key() -> bytes:
|
|
192
|
+
"""Return the configured encryption key for JWE payloads."""
|
|
193
|
+
secret_key = config.system.encryption_key
|
|
194
|
+
if secret_key is None:
|
|
195
|
+
raise HowlerValueError("System encryption_key must be configured before encrypting tokens")
|
|
196
|
+
|
|
197
|
+
return secret_key.encode("utf-8")
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import json
|
|
2
2
|
from collections import defaultdict
|
|
3
|
+
from importlib.util import find_spec
|
|
3
4
|
from typing import Any, Optional, TypedDict
|
|
4
5
|
|
|
5
|
-
from flask import Response
|
|
6
|
+
from flask import Response, has_request_context, request
|
|
6
7
|
|
|
7
8
|
from howler import actions
|
|
8
9
|
from howler.api import bad_request
|
|
@@ -14,6 +15,7 @@ from howler.config import config
|
|
|
14
15
|
from howler.odm.models.action import VALID_TRIGGERS, Action
|
|
15
16
|
from howler.odm.models.user import User
|
|
16
17
|
from howler.remote.datatypes.queues.named import NamedQueue
|
|
18
|
+
from howler.services import auth_service
|
|
17
19
|
from howler.utils.constants import TESTING
|
|
18
20
|
from howler.utils.str_utils import sanitize_lucene_query
|
|
19
21
|
|
|
@@ -25,12 +27,30 @@ class TriggeredAction(TypedDict):
|
|
|
25
27
|
|
|
26
28
|
hit_ids: list[str]
|
|
27
29
|
uname: str | None
|
|
30
|
+
auth_token: str | None
|
|
28
31
|
|
|
29
32
|
|
|
30
33
|
# Per-trigger persistent queues for buffering action execution requests.
|
|
31
34
|
_action_queues: dict[str, NamedQueue[TriggeredAction]] = {}
|
|
32
35
|
|
|
33
36
|
|
|
37
|
+
def _is_builtin_operation(operation_id: str) -> bool:
|
|
38
|
+
"""Return whether an operation is implemented by Howler's built-in actions package."""
|
|
39
|
+
return find_spec(f"howler.actions.{operation_id}") is not None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _request_auth_token() -> str | None:
|
|
43
|
+
"""Extract the credential portion of the current request's authorization header."""
|
|
44
|
+
if not has_request_context():
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
authorization = request.headers.get("Authorization")
|
|
48
|
+
if authorization is None or " " not in authorization:
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
return authorization.split(" ", maxsplit=1)[1]
|
|
52
|
+
|
|
53
|
+
|
|
34
54
|
def get_action_queue(trigger: str) -> NamedQueue[TriggeredAction]:
|
|
35
55
|
"""Return the action queue for *trigger*, creating it on first use.
|
|
36
56
|
|
|
@@ -51,7 +71,7 @@ def get_action_queue(trigger: str) -> NamedQueue[TriggeredAction]:
|
|
|
51
71
|
return _action_queues[trigger]
|
|
52
72
|
|
|
53
73
|
|
|
54
|
-
def enqueue_action_execution(hit_ids: list[str], trigger: str = "create", user: Optional[User] = None) -> None:
|
|
74
|
+
def enqueue_action_execution(hit_ids: str | list[str], trigger: str = "create", user: Optional[User] = None) -> None:
|
|
55
75
|
"""Buffer action execution by pushing to a Redis queue.
|
|
56
76
|
|
|
57
77
|
When the action queue is disabled in configuration, falls back to
|
|
@@ -65,25 +85,44 @@ def enqueue_action_execution(hit_ids: list[str], trigger: str = "create", user:
|
|
|
65
85
|
if not hit_ids:
|
|
66
86
|
return
|
|
67
87
|
|
|
88
|
+
if not isinstance(hit_ids, list):
|
|
89
|
+
hit_ids = [hit_ids]
|
|
90
|
+
|
|
68
91
|
if trigger not in VALID_TRIGGERS:
|
|
69
92
|
raise HowlerValueError(f"Invalid trigger {trigger!r}. Must be one of {VALID_TRIGGERS}")
|
|
70
93
|
|
|
94
|
+
auth_token = _request_auth_token()
|
|
95
|
+
|
|
71
96
|
if not config.system.action_queue.enabled:
|
|
97
|
+
logger.debug(
|
|
98
|
+
"Executing actions for trigger %s on hit id(s) [%s] with user %s",
|
|
99
|
+
trigger,
|
|
100
|
+
", ".join(hit_ids),
|
|
101
|
+
user.uname if user else "NO_USER",
|
|
102
|
+
)
|
|
103
|
+
|
|
72
104
|
query = f"howler.id:({' OR '.join(sanitize_lucene_query(h) for h in hit_ids)})"
|
|
73
|
-
bulk_execute_on_query(query, trigger=trigger, user=user)
|
|
105
|
+
bulk_execute_on_query(query, trigger=trigger, user=user, auth_token=auth_token)
|
|
74
106
|
return
|
|
75
107
|
|
|
76
108
|
try:
|
|
109
|
+
logger.debug(
|
|
110
|
+
"Enqueuing actions for trigger %s on hit id(s) [%s] with user %s",
|
|
111
|
+
trigger,
|
|
112
|
+
", ".join(hit_ids),
|
|
113
|
+
user.uname if user else "NO_USER",
|
|
114
|
+
)
|
|
77
115
|
get_action_queue(trigger).push(
|
|
78
116
|
{
|
|
79
117
|
"hit_ids": hit_ids,
|
|
80
118
|
"uname": user.uname if user else None,
|
|
119
|
+
"auth_token": auth_service.encrypt_token(auth_token),
|
|
81
120
|
}
|
|
82
121
|
)
|
|
83
122
|
except Exception:
|
|
84
123
|
logger.exception("Failed to enqueue action execution, falling back to direct execution")
|
|
85
124
|
query = f"howler.id:({' OR '.join(sanitize_lucene_query(h) for h in hit_ids)})"
|
|
86
|
-
bulk_execute_on_query(query, trigger=trigger, user=user)
|
|
125
|
+
bulk_execute_on_query(query, trigger=trigger, user=user, auth_token=auth_token)
|
|
87
126
|
|
|
88
127
|
|
|
89
128
|
def process_action_batch(trigger: str, items: list[TriggeredAction]) -> None:
|
|
@@ -99,21 +138,26 @@ def process_action_batch(trigger: str, items: list[TriggeredAction]) -> None:
|
|
|
99
138
|
if not items:
|
|
100
139
|
return
|
|
101
140
|
|
|
102
|
-
groups: dict[str | None, list[str]] = defaultdict(list)
|
|
141
|
+
groups: dict[tuple[str | None, str | None], list[str]] = defaultdict(list)
|
|
103
142
|
|
|
104
143
|
for item in items:
|
|
105
144
|
uname = item.get("uname")
|
|
145
|
+
try:
|
|
146
|
+
auth_token = auth_service.decrypt_token(item.get("auth_token"))
|
|
147
|
+
except Exception:
|
|
148
|
+
logger.exception("Unable to decrypt queued authorization token for user=%s", uname)
|
|
149
|
+
auth_token = None
|
|
106
150
|
|
|
107
|
-
groups[uname].extend(item["hit_ids"])
|
|
151
|
+
groups[(uname, auth_token)].extend(item["hit_ids"])
|
|
108
152
|
|
|
109
|
-
for uname, hit_ids in groups.items():
|
|
153
|
+
for (uname, auth_token), hit_ids in groups.items():
|
|
110
154
|
# Deduplicate IDs within a batch group
|
|
111
155
|
unique_ids = list(dict.fromkeys(hit_ids))
|
|
112
156
|
query = f"howler.id:({' OR '.join(sanitize_lucene_query(h) for h in unique_ids)})"
|
|
113
157
|
|
|
114
158
|
try:
|
|
115
159
|
user = datastore().user.get(uname)
|
|
116
|
-
bulk_execute_on_query(query, trigger=trigger, user=user)
|
|
160
|
+
bulk_execute_on_query(query, trigger=trigger, user=user, auth_token=auth_token)
|
|
117
161
|
except Exception:
|
|
118
162
|
logger.exception("Error processing action batch for trigger=%s user=%s", trigger, uname)
|
|
119
163
|
|
|
@@ -153,7 +197,9 @@ def validate_action(new_action: Any) -> Optional[Response]: # noqa: C901
|
|
|
153
197
|
return None
|
|
154
198
|
|
|
155
199
|
|
|
156
|
-
def bulk_execute_on_query(
|
|
200
|
+
def bulk_execute_on_query(
|
|
201
|
+
query: str, trigger: str = "create", user: Optional[User] = None, auth_token: str | None = None
|
|
202
|
+
):
|
|
157
203
|
"""Execute the operations specified in registered actions on the given query"""
|
|
158
204
|
storage = datastore()
|
|
159
205
|
|
|
@@ -195,6 +241,10 @@ def bulk_execute_on_query(query: str, trigger: str = "create", user: Optional[Us
|
|
|
195
241
|
if not user:
|
|
196
242
|
raise NotImplementedError("Running actions without a user object is not currently supported")
|
|
197
243
|
|
|
244
|
+
if auth_token is not None and not _is_builtin_operation(operation.operation_id):
|
|
245
|
+
logger.debug("Appending auth token to external operation %s", operation.operation_id)
|
|
246
|
+
parsed_data["auth_token"] = auth_token
|
|
247
|
+
|
|
198
248
|
report = actions.execute(
|
|
199
249
|
operation_id=operation.operation_id,
|
|
200
250
|
query=intersected_query,
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import base64
|
|
2
2
|
import hashlib
|
|
3
3
|
import hmac
|
|
4
|
+
import os
|
|
4
5
|
from datetime import datetime
|
|
5
6
|
from typing import Optional, Union
|
|
6
7
|
|
|
8
|
+
import jwt
|
|
9
|
+
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
7
10
|
from flask import request
|
|
8
11
|
from opentelemetry import trace
|
|
9
12
|
|
|
@@ -22,7 +25,7 @@ from howler.odm.models.user import User
|
|
|
22
25
|
from howler.remote.datatypes import retry_call
|
|
23
26
|
from howler.remote.datatypes.queues.named import NamedQueue
|
|
24
27
|
from howler.remote.datatypes.set import ExpiringSet
|
|
25
|
-
from howler.security.utils import generate_random_secret, verify_password
|
|
28
|
+
from howler.security.utils import encryption_key, generate_random_secret, verify_password
|
|
26
29
|
|
|
27
30
|
logger = get_logger(__file__)
|
|
28
31
|
tracer = trace.get_tracer(__name__)
|
|
@@ -39,6 +42,46 @@ _APIKEY_CACHE_TTL: int = 600 # 10 minutes
|
|
|
39
42
|
# Derive the HMAC key from the configured secret
|
|
40
43
|
_HMAC_KEY: bytes = hashlib.sha256(config.auth.hmac_secret_key.encode()).digest()
|
|
41
44
|
|
|
45
|
+
_NON_JWT_TOKEN_PREFIX = "aesgcm:" # noqa: S105
|
|
46
|
+
_AES_GCM_NONCE_LENGTH = 12
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _is_jwt(token: str) -> bool:
|
|
50
|
+
"""Return whether *token* has the compact three-segment JWT format."""
|
|
51
|
+
if token.count(".") != 2:
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
return "alg" in jwt.get_unverified_header(token)
|
|
56
|
+
except jwt.InvalidTokenError:
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def encrypt_token(token: str | None) -> str | None:
|
|
61
|
+
"""Encrypt an authorization token before it is serialized into Redis."""
|
|
62
|
+
if token is None:
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
if _is_jwt(token):
|
|
66
|
+
return jwt_service.encrypt_token(token)
|
|
67
|
+
|
|
68
|
+
nonce = os.urandom(_AES_GCM_NONCE_LENGTH)
|
|
69
|
+
ciphertext = AESGCM(encryption_key()).encrypt(nonce, token.encode("utf-8"), None)
|
|
70
|
+
return _NON_JWT_TOKEN_PREFIX + base64.urlsafe_b64encode(nonce + ciphertext).decode("ascii")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def decrypt_token(encrypted_token: str | None) -> str | None:
|
|
74
|
+
"""Decrypt a queued authorization token after reading it from Redis."""
|
|
75
|
+
if encrypted_token is None:
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
if not encrypted_token.startswith(_NON_JWT_TOKEN_PREFIX):
|
|
79
|
+
return jwt_service.decrypt_token(encrypted_token)
|
|
80
|
+
|
|
81
|
+
payload = base64.urlsafe_b64decode(encrypted_token.removeprefix(_NON_JWT_TOKEN_PREFIX))
|
|
82
|
+
nonce, ciphertext = payload[:_AES_GCM_NONCE_LENGTH], payload[_AES_GCM_NONCE_LENGTH:]
|
|
83
|
+
return AESGCM(encryption_key()).decrypt(nonce, ciphertext, None).decode("utf-8")
|
|
84
|
+
|
|
42
85
|
|
|
43
86
|
def _apikey_cache_key(username: str, key_name: str) -> str:
|
|
44
87
|
"""Build the Redis key that identifies a cached apikey verification.
|
{howler_api-4.1.0.dev1468 → howler_api-4.1.0.dev1502}/howler/services/correlation_service.py
RENAMED
|
@@ -345,7 +345,7 @@ def run_worker() -> None: # pragma: no cover – long-running loop, tested via
|
|
|
345
345
|
if item is not None:
|
|
346
346
|
batch.append(item)
|
|
347
347
|
|
|
348
|
-
if len(batch) % 10 == 0 or len(batch) >= BATCH_SIZE:
|
|
348
|
+
if len(batch) > 0 and (len(batch) % 10 == 0 or len(batch) >= BATCH_SIZE):
|
|
349
349
|
logger.info("Batch size: %s", len(batch))
|
|
350
350
|
|
|
351
351
|
if len(batch) >= BATCH_SIZE or (item is None and batch):
|
|
@@ -684,7 +684,7 @@ def transition_hit(
|
|
|
684
684
|
workflow: Workflow = get_hit_workflow()
|
|
685
685
|
|
|
686
686
|
# Log hit that will be transitioned
|
|
687
|
-
logger.debug("Transitioning
|
|
687
|
+
logger.debug("Transitioning hit %s with transition %s", hit["howler"]["id"], transition)
|
|
688
688
|
|
|
689
689
|
# Process each hit (primary + children) with the workflow transition
|
|
690
690
|
hit_status = hit["howler"]["status"]
|
|
@@ -727,7 +727,7 @@ def transition_hit(
|
|
|
727
727
|
datastore().hit.commit()
|
|
728
728
|
|
|
729
729
|
# Enqueue action execution for all hits
|
|
730
|
-
action_service.enqueue_action_execution(
|
|
730
|
+
action_service.enqueue_action_execution(hit_id, trigger=trigger, user=user)
|
|
731
731
|
|
|
732
732
|
# Emit events for processed hit to notify other systems
|
|
733
733
|
updated_hit, new_version = datastore().hit.get(hit_id, as_obj=True, version=True)
|
|
@@ -736,6 +736,9 @@ def transition_hit(
|
|
|
736
736
|
)
|
|
737
737
|
|
|
738
738
|
updated_hit, new_version = datastore().hit.get(hit_id, as_obj=True, version=True)
|
|
739
|
+
else:
|
|
740
|
+
logger.debug("Transition %s does not require executing bulk actions", transition)
|
|
741
|
+
|
|
739
742
|
return updated_hit, new_version
|
|
740
743
|
|
|
741
744
|
|
|
@@ -6,14 +6,35 @@ from typing import Any, Optional, cast
|
|
|
6
6
|
|
|
7
7
|
import jwt
|
|
8
8
|
import requests
|
|
9
|
+
from authlib.jose import JsonWebEncryption
|
|
9
10
|
from jwt.api_jwk import PyJWK
|
|
10
11
|
|
|
11
12
|
from howler.common.exceptions import ForbiddenException, HowlerKeyError, HowlerValueError
|
|
12
13
|
from howler.common.logging import get_logger
|
|
13
14
|
from howler.config import cache, config
|
|
15
|
+
from howler.security.utils import encryption_key
|
|
14
16
|
|
|
15
17
|
logger = get_logger(__file__)
|
|
16
18
|
|
|
19
|
+
_jwe = JsonWebEncryption()
|
|
20
|
+
_JWE_HEADER = {"alg": "A256GCMKW", "enc": "A256GCM"}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def encrypt_token(auth_token: str | None) -> str | None:
|
|
24
|
+
"""Encrypt an authorization token before it is serialized into Redis."""
|
|
25
|
+
if auth_token is None:
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
return _jwe.serialize_compact(_JWE_HEADER, auth_token.encode("utf-8"), encryption_key()).decode("ascii")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def decrypt_token(encrypted_auth_token: str | None) -> str | None:
|
|
32
|
+
"""Decrypt a queued authorization token for in-memory action execution."""
|
|
33
|
+
if encrypted_auth_token is None:
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
return _jwe.deserialize_compact(encrypted_auth_token, encryption_key())["payload"].decode("utf-8")
|
|
37
|
+
|
|
17
38
|
|
|
18
39
|
def get_jwk(access_token: str) -> PyJWK:
|
|
19
40
|
"""Get the JSON Web Key associated with the given JWT"""
|
|
File without changes
|
|
@@ -60,7 +60,7 @@ module = ["howler.odm.base", "test", "test.*"]
|
|
|
60
60
|
ignore_errors = true
|
|
61
61
|
|
|
62
62
|
[[tool.mypy.overrides]]
|
|
63
|
-
module = "howler.security"
|
|
63
|
+
module = "howler.security.login"
|
|
64
64
|
disable_error_code = "attr-defined"
|
|
65
65
|
|
|
66
66
|
[[tool.mypy.overrides]]
|
|
@@ -133,7 +133,7 @@ suppress-none-returning = true
|
|
|
133
133
|
"build_scripts/*" = ["D", "ANN", "S", "N818", "T20", "TRY"]
|
|
134
134
|
"howler/app.py" = ["E402"]
|
|
135
135
|
"howler/odm/random*.py" = ["C901", "S105", "S311", "E402"]
|
|
136
|
-
"howler/security/
|
|
136
|
+
"howler/security/login.py" = ["TRY301"]
|
|
137
137
|
"howler/external/*.py" = ["T20"]
|
|
138
138
|
"howler/odm/helper.py" = ["S311"]
|
|
139
139
|
"howler/common/classification.py" = ["D", "ANN", "C901", "TRY"]
|
|
@@ -152,7 +152,7 @@ suppress-none-returning = true
|
|
|
152
152
|
[tool.poetry]
|
|
153
153
|
package-mode = true
|
|
154
154
|
name = "howler-api"
|
|
155
|
-
version = "4.1.0.
|
|
155
|
+
version = "4.1.0.dev1502"
|
|
156
156
|
description = "Howler - API server"
|
|
157
157
|
authors = [
|
|
158
158
|
"Canadian Centre for Cyber Security <howler@cyber.gc.ca>",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|