howler-api 4.0.0.dev799__tar.gz → 4.0.0.dev803__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/PKG-INFO +1 -1
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/api/socket.py +30 -5
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/api/v2/ingest.py +8 -3
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/helper.py +0 -4
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/howler_data.py +0 -4
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/observable.py +0 -4
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/services/case_service.py +31 -3
- howler_api-4.0.0.dev803/howler/services/viewer_service.py +43 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/utils/socket_utils.py +4 -25
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/pyproject.toml +1 -1
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/README.md +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/__init__.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/actions/__init__.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/actions/add_label.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/actions/add_to_bundle.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/actions/add_to_case.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/actions/change_field.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/actions/demote.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/actions/example_plugin.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/actions/prioritization.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/actions/promote.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/actions/remove_from_bundle.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/actions/remove_label.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/actions/transition.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/api/__init__.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/api/base.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/api/v1/__init__.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/api/v1/action.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/api/v1/analytic.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/api/v1/auth.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/api/v1/clue.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/api/v1/configs.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/api/v1/dossier.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/api/v1/help.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/api/v1/hit.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/api/v1/notebook.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/api/v1/overview.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/api/v1/search.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/api/v1/template.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/api/v1/tool.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/api/v1/user.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/api/v1/utils/__init__.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/api/v1/utils/etag.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/api/v1/view.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/api/v2/__init__.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/api/v2/case.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/api/v2/search.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/app.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/common/README.md +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/common/__init__.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/common/classification.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/common/classification.yml +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/common/exceptions.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/common/loader.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/common/logging/__init__.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/common/logging/audit.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/common/logging/format.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/common/net.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/common/net_static.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/common/random_user.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/common/swagger.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/config.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/cronjobs/__init__.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/cronjobs/retention.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/cronjobs/view_cleanup.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/datastore/README.md +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/datastore/__init__.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/datastore/bulk.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/datastore/collection.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/datastore/constants.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/datastore/exceptions.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/datastore/howler_store.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/datastore/migrations/fix_process.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/datastore/operations.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/datastore/schemas.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/datastore/store.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/datastore/support/__init__.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/datastore/support/build.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/datastore/support/schemas.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/datastore/types.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/error.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/external/README.md +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/external/__init__.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/external/generate_mitre.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/external/generate_sigma_rules.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/external/generate_tlds.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/external/reindex_data.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/external/wipe_databases.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/gunicorn_config.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/healthz.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/helper/__init__.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/helper/azure.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/helper/discover.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/helper/hit.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/helper/oauth.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/helper/search.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/helper/workflow.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/helper/ws.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/README.md +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/__init__.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/base.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/charter.txt +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/constants.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/howler_enum.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/mixins.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/__init__.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/action.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/analytic.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/assemblyline.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/aws.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/azure.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/case.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/cbs.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/clue.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/config.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/dossier.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/__init__.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/agent.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/autonomous_system.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/client.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/cloud.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/code_signature.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/container.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/dns.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/egress.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/elf.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/email.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/error.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/event.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/faas.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/file.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/geo.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/group.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/hash.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/host.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/http.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/ingress.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/interface.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/network.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/observer.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/organization.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/os.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/pe.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/process.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/registry.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/related.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/rule.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/server.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/threat.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/tls.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/url.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/user.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/user_agent.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/ecs/vulnerability.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/gcp.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/hit.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/lead.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/localized_label.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/overview.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/pivot.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/record.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/template.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/user.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/models/view.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/random_data.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/odm/randomizer.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/patched.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/plugins/__init__.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/plugins/config.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/remote/__init__.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/remote/datatypes/README.md +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/remote/datatypes/__init__.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/remote/datatypes/counters.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/remote/datatypes/events.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/remote/datatypes/hash.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/remote/datatypes/lock.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/remote/datatypes/queues/__init__.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/remote/datatypes/queues/comms.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/remote/datatypes/queues/multi.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/remote/datatypes/queues/named.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/remote/datatypes/queues/priority.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/remote/datatypes/set.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/remote/datatypes/user_quota_tracker.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/security/__init__.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/security/socket.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/security/utils.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/services/__init__.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/services/action_service.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/services/analytic_service.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/services/auth_service.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/services/bundle_compat_service.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/services/config_service.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/services/docs_service.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/services/dossier_service.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/services/event_service.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/services/hit_service.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/services/jwt_service.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/services/lucene_service.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/services/notebook_service.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/services/observable_service.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/services/overview_service.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/services/search_service.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/services/template_service.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/services/user_service.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/telemetry.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/utils/__init__.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/utils/annotations.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/utils/chunk.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/utils/compat.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/utils/constants.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/utils/dict_utils.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/utils/isotime.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/utils/list_utils.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/utils/lucene.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/utils/path.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/utils/str_utils.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev803}/howler/utils/uid.py +0 -0
|
@@ -7,11 +7,11 @@ from flask import Blueprint, request
|
|
|
7
7
|
from opentelemetry import trace
|
|
8
8
|
|
|
9
9
|
import howler.services.event_service as event_service
|
|
10
|
+
import howler.services.viewer_service as viewer_service
|
|
10
11
|
from howler.api import ok, unauthorized
|
|
11
12
|
from howler.common.logging import get_logger
|
|
12
|
-
from howler.datastore.operations import OdmHelper
|
|
13
13
|
from howler.helper.ws import ConnectionClosed, Server
|
|
14
|
-
from howler.
|
|
14
|
+
from howler.security import api_login
|
|
15
15
|
from howler.security.socket import websocket_auth, ws_response
|
|
16
16
|
from howler.utils.socket_utils import check_action
|
|
17
17
|
|
|
@@ -24,8 +24,6 @@ socket_api._doc = "Endpoints concerning websocket connectivity between the clien
|
|
|
24
24
|
logger = get_logger(__file__)
|
|
25
25
|
tracer = trace.get_tracer(__name__)
|
|
26
26
|
|
|
27
|
-
hit_helper = OdmHelper(Hit)
|
|
28
|
-
|
|
29
27
|
|
|
30
28
|
@tracer.start_as_current_span(f"{__name__}.emit")
|
|
31
29
|
@socket_api.route("/emit/<event>", methods=["POST"])
|
|
@@ -49,10 +47,25 @@ def emit(event: str):
|
|
|
49
47
|
return ok()
|
|
50
48
|
|
|
51
49
|
|
|
50
|
+
@tracer.start_as_current_span(f"{__name__}.get_viewers")
|
|
51
|
+
@socket_api.route("/viewers/<entity_id>", methods=["GET"])
|
|
52
|
+
@api_login(audit=False, required_priv=["R"])
|
|
53
|
+
def get_viewers(entity_id: str, **kwargs):
|
|
54
|
+
"""Get the list of users currently viewing the specified entity
|
|
55
|
+
|
|
56
|
+
Variables:
|
|
57
|
+
entity_id => The ID of the entity to get viewers for
|
|
58
|
+
|
|
59
|
+
Result Example:
|
|
60
|
+
["user1", "user2"]
|
|
61
|
+
"""
|
|
62
|
+
return ok(viewer_service.get_viewers(entity_id))
|
|
63
|
+
|
|
64
|
+
|
|
52
65
|
@tracer.start_as_current_span(f"{__name__}.connect")
|
|
53
66
|
@socket_api.route("/connect", websocket=True) # type: ignore
|
|
54
67
|
@websocket_auth(required_priv=["R"])
|
|
55
|
-
def connect(ws: Server, *args: Any, ws_id: str, **kwargs):
|
|
68
|
+
def connect(ws: Server, *args: Any, ws_id: str, **kwargs): # noqa: C901
|
|
56
69
|
"""Connect to the server to monitor for updates via websocket
|
|
57
70
|
|
|
58
71
|
Variables:
|
|
@@ -78,10 +91,20 @@ def connect(ws: Server, *args: Any, ws_id: str, **kwargs):
|
|
|
78
91
|
logger.debug("Sending action: %s", data)
|
|
79
92
|
ws.send(ws_response("action", data))
|
|
80
93
|
|
|
94
|
+
def send_case(data: dict[str, Any]):
|
|
95
|
+
logger.debug("Sending case update: %s", data.get("case", {}).get("case_id", "unknown"))
|
|
96
|
+
ws.send(ws_response("cases", data))
|
|
97
|
+
|
|
98
|
+
def send_viewers_update(data: dict[str, Any]):
|
|
99
|
+
logger.debug("Sending viewers update: %s", data.get("id", "unknown"))
|
|
100
|
+
ws.send(ws_response("viewers_update", data))
|
|
101
|
+
|
|
81
102
|
try:
|
|
82
103
|
event_service.on("hits", send_hit)
|
|
83
104
|
event_service.on("broadcast", send_broadcast)
|
|
84
105
|
event_service.on("action", send_action)
|
|
106
|
+
event_service.on("cases", send_case)
|
|
107
|
+
event_service.on("viewers_update", send_viewers_update)
|
|
85
108
|
while ws.connected:
|
|
86
109
|
data = ws.receive(10)
|
|
87
110
|
if data:
|
|
@@ -113,6 +136,8 @@ def connect(ws: Server, *args: Any, ws_id: str, **kwargs):
|
|
|
113
136
|
event_service.off("hits", send_hit)
|
|
114
137
|
event_service.off("broadcast", send_broadcast)
|
|
115
138
|
event_service.off("action", send_action)
|
|
139
|
+
event_service.off("cases", send_case)
|
|
140
|
+
event_service.off("viewers_update", send_viewers_update)
|
|
116
141
|
|
|
117
142
|
for id, action, broadcast in outstanding_actions:
|
|
118
143
|
outstanding_actions = check_action(id, action, broadcast, outstanding_actions=outstanding_actions, **kwargs)
|
|
@@ -120,15 +120,20 @@ def delete(indexes: str, user: User, **kwargs):
|
|
|
120
120
|
"success": True # Deleting the hits succeded
|
|
121
121
|
}
|
|
122
122
|
"""
|
|
123
|
-
|
|
123
|
+
ids = request.json
|
|
124
124
|
|
|
125
|
-
if
|
|
125
|
+
if ids is None:
|
|
126
126
|
return bad_request(err="No hit ids were sent.")
|
|
127
127
|
|
|
128
128
|
if "admin" not in user["type"]:
|
|
129
129
|
return forbidden(err="Cannot delete hit, only administrators are permitted to delete.")
|
|
130
130
|
|
|
131
|
-
index_list = indexes.split(",")
|
|
131
|
+
index_list = indexes.split(",")
|
|
132
|
+
|
|
133
|
+
ds = datastore()
|
|
134
|
+
|
|
135
|
+
if non_existing_hit_ids := [id for id in ids if all(not ds[index].exists(id) for index in index_list)]:
|
|
136
|
+
return not_found(err=f"Record ids [{','.join(non_existing_hit_ids)}] do not exist.")
|
|
132
137
|
|
|
133
138
|
# TODO: Reimplement in a generic function
|
|
134
139
|
# hit_service.delete_hits(hit_ids, indexes=index_list)
|
|
@@ -249,8 +249,6 @@ def generate_useful_hit( # noqa: C901
|
|
|
249
249
|
except IndexError:
|
|
250
250
|
pass
|
|
251
251
|
|
|
252
|
-
hit.howler.viewers = []
|
|
253
|
-
|
|
254
252
|
hit.howler.dossier = [
|
|
255
253
|
Lead(
|
|
256
254
|
{
|
|
@@ -446,8 +444,6 @@ def generate_useful_observable( # noqa: C901
|
|
|
446
444
|
),
|
|
447
445
|
]
|
|
448
446
|
|
|
449
|
-
observable.howler.viewers = []
|
|
450
|
-
|
|
451
447
|
return observable
|
|
452
448
|
|
|
453
449
|
|
|
@@ -293,7 +293,3 @@ class HowlerData(odm.Model):
|
|
|
293
293
|
dossier: list[Lead] = odm.List(
|
|
294
294
|
odm.Compound(Lead), default=[], description="A list of leads forming the dossier associated with this hit"
|
|
295
295
|
)
|
|
296
|
-
viewers: list[str] = odm.List(
|
|
297
|
-
odm.Keyword(description="A list of users currently viewing the hit"),
|
|
298
|
-
default=[],
|
|
299
|
-
)
|
|
@@ -101,10 +101,6 @@ class ObservableData(odm.Model):
|
|
|
101
101
|
default=[],
|
|
102
102
|
description="A list of changes to the observable with timestamps and attribution.",
|
|
103
103
|
)
|
|
104
|
-
viewers: list[str] = odm.List(
|
|
105
|
-
odm.Keyword(description="A list of users currently viewing the observable"),
|
|
106
|
-
default=[],
|
|
107
|
-
)
|
|
108
104
|
|
|
109
105
|
|
|
110
106
|
@odm.model(
|
|
@@ -4,11 +4,11 @@ This module provides functionality for creating, updating, retrieving, and manag
|
|
|
4
4
|
cases - collections of security alerts and investigation data organized by analysts.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from typing import Any,
|
|
7
|
+
from typing import Any, overload
|
|
8
8
|
|
|
9
9
|
from prometheus_client import Counter
|
|
10
10
|
|
|
11
|
-
from howler.common.exceptions import InvalidDataException, NotFoundException
|
|
11
|
+
from howler.common.exceptions import HowlerValueError, InvalidDataException, NotFoundException
|
|
12
12
|
from howler.common.loader import APP_NAME, datastore
|
|
13
13
|
from howler.common.logging import get_logger
|
|
14
14
|
from howler.datastore.exceptions import DataStoreException
|
|
@@ -17,6 +17,7 @@ from howler.odm.models.ecs.related import Related
|
|
|
17
17
|
from howler.odm.models.hit import Hit
|
|
18
18
|
from howler.odm.models.observable import Observable
|
|
19
19
|
from howler.odm.models.user import User
|
|
20
|
+
from howler.services import event_service
|
|
20
21
|
|
|
21
22
|
logger = get_logger(__file__)
|
|
22
23
|
|
|
@@ -51,7 +52,14 @@ def create_case(_case: dict, user: str = None) -> Case: # type: ignore
|
|
|
51
52
|
append_case_item(case.case_id, item=CaseItem(item))
|
|
52
53
|
|
|
53
54
|
if items:
|
|
54
|
-
|
|
55
|
+
updated_case = datastore().case.get(case.case_id)
|
|
56
|
+
|
|
57
|
+
if not updated_case:
|
|
58
|
+
raise HowlerValueError("Error occurred when creating case")
|
|
59
|
+
|
|
60
|
+
case = updated_case
|
|
61
|
+
|
|
62
|
+
event_service.emit("cases", {"case": case.as_primitives()})
|
|
55
63
|
|
|
56
64
|
return case
|
|
57
65
|
|
|
@@ -218,6 +226,8 @@ def update_case(case_id: str, case_data: dict[str, Any], user: User) -> Case:
|
|
|
218
226
|
case.updated = "NOW"
|
|
219
227
|
ds.case.save(case_id, case)
|
|
220
228
|
|
|
229
|
+
event_service.emit("cases", {"case": case.as_primitives()})
|
|
230
|
+
|
|
221
231
|
return case
|
|
222
232
|
|
|
223
233
|
|
|
@@ -329,6 +339,10 @@ def append_hit(case_id: str, item: CaseItem) -> Case:
|
|
|
329
339
|
|
|
330
340
|
_sync_case_metadata(_case.case_id)
|
|
331
341
|
|
|
342
|
+
updated_case = ds.case.get(_case.case_id)
|
|
343
|
+
if updated_case:
|
|
344
|
+
event_service.emit("cases", {"case": updated_case.as_primitives()})
|
|
345
|
+
|
|
332
346
|
return _case
|
|
333
347
|
|
|
334
348
|
|
|
@@ -371,6 +385,10 @@ def append_observable(case_id: str, item: CaseItem) -> Case:
|
|
|
371
385
|
_add_backreference(observable, _case.case_id)
|
|
372
386
|
_sync_case_metadata(case_id)
|
|
373
387
|
|
|
388
|
+
updated_case = ds.case.get(_case.case_id)
|
|
389
|
+
if updated_case:
|
|
390
|
+
event_service.emit("cases", {"case": updated_case.as_primitives()})
|
|
391
|
+
|
|
374
392
|
return _case
|
|
375
393
|
|
|
376
394
|
|
|
@@ -410,6 +428,8 @@ def append_case(case_id: str, item: CaseItem) -> Case:
|
|
|
410
428
|
if not datastore().case.save(_case.case_id, _case):
|
|
411
429
|
raise DataStoreException(f"Failed to save {_case.case_id} with new item {item.value}")
|
|
412
430
|
|
|
431
|
+
event_service.emit("cases", {"case": _case.as_primitives()})
|
|
432
|
+
|
|
413
433
|
return _case
|
|
414
434
|
|
|
415
435
|
|
|
@@ -473,6 +493,8 @@ def append_reference(case_id: str, item: CaseItem) -> Case:
|
|
|
473
493
|
if not datastore().case.save(_case.case_id, _case):
|
|
474
494
|
raise DataStoreException(f"Failed to save {_case.case_id} with new item {item.value}")
|
|
475
495
|
|
|
496
|
+
event_service.emit("cases", {"case": _case.as_primitives()})
|
|
497
|
+
|
|
476
498
|
return _case
|
|
477
499
|
|
|
478
500
|
|
|
@@ -646,6 +668,10 @@ def remove_case_items(case_id: str, values: list[str]):
|
|
|
646
668
|
|
|
647
669
|
_sync_case_metadata(case_id)
|
|
648
670
|
|
|
671
|
+
updated_case = ds.case.get(_case.case_id)
|
|
672
|
+
if updated_case:
|
|
673
|
+
event_service.emit("cases", {"case": updated_case.as_primitives()})
|
|
674
|
+
|
|
649
675
|
return _case
|
|
650
676
|
|
|
651
677
|
|
|
@@ -692,4 +718,6 @@ def rename_case_item(case_id: str, item_value: str, new_path: str) -> Case:
|
|
|
692
718
|
if not ds.case.save(_case.case_id, _case):
|
|
693
719
|
raise DataStoreException("Failed to save case after item rename")
|
|
694
720
|
|
|
721
|
+
event_service.emit("cases", {"case": _case.as_primitives()})
|
|
722
|
+
|
|
695
723
|
return _case
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Viewer service for tracking active viewers of entities (hits, cases, observables) in Redis.
|
|
2
|
+
|
|
3
|
+
Stores viewer presence as ephemeral Redis sets with TTL, replacing the
|
|
4
|
+
previous approach of persisting viewers directly in the ODM/ElasticSearch.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from howler.config import redis
|
|
8
|
+
from howler.remote.datatypes import retry_call
|
|
9
|
+
from howler.services import event_service
|
|
10
|
+
|
|
11
|
+
VIEWER_KEY_PREFIX = "viewers"
|
|
12
|
+
VIEWER_TTL = 3600 # 1 hour
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _key(entity_id: str) -> str:
|
|
16
|
+
return f"{VIEWER_KEY_PREFIX}:{entity_id}"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def add_viewer(entity_id: str, username: str) -> None:
|
|
20
|
+
"""Record that a user is viewing the given entity."""
|
|
21
|
+
key = _key(entity_id)
|
|
22
|
+
retry_call(redis.sadd, key, username)
|
|
23
|
+
retry_call(redis.expire, key, VIEWER_TTL)
|
|
24
|
+
_emit_update(entity_id)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def remove_viewer(entity_id: str, username: str) -> None:
|
|
28
|
+
"""Record that a user has stopped viewing the given entity."""
|
|
29
|
+
retry_call(redis.srem, _key(entity_id), username)
|
|
30
|
+
_emit_update(entity_id)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_viewers(entity_id: str) -> list[str]:
|
|
34
|
+
"""Return the list of usernames currently viewing the given entity."""
|
|
35
|
+
members = retry_call(redis.smembers, _key(entity_id))
|
|
36
|
+
return sorted(m.decode() if isinstance(m, bytes) else m for m in members)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _emit_update(entity_id: str) -> None:
|
|
40
|
+
event_service.emit(
|
|
41
|
+
"viewers_update",
|
|
42
|
+
{"id": entity_id, "viewers": get_viewers(entity_id)},
|
|
43
|
+
)
|
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
from howler.common.logging import get_logger
|
|
2
|
-
from howler.
|
|
3
|
-
from howler.odm.models.hit import Hit
|
|
4
|
-
from howler.services import event_service, hit_service
|
|
2
|
+
from howler.services import event_service, viewer_service
|
|
5
3
|
|
|
6
4
|
logger = get_logger(__file__)
|
|
7
5
|
|
|
8
|
-
hit_helper = OdmHelper(Hit)
|
|
9
|
-
|
|
10
6
|
|
|
11
7
|
def check_action(
|
|
12
8
|
id: str, action: str, broadcast: bool, outstanding_actions: list[tuple[str, str, bool]] = [], **kwargs
|
|
@@ -35,27 +31,10 @@ def check_action(
|
|
|
35
31
|
outstanding_actions = [a for a in outstanding_actions if a[1] != "stop_typing"]
|
|
36
32
|
|
|
37
33
|
elif action == "viewing":
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
hit_service.update_hit(
|
|
41
|
-
id,
|
|
42
|
-
[
|
|
43
|
-
hit_helper.list_add(
|
|
44
|
-
"howler.viewers",
|
|
45
|
-
kwargs["username"],
|
|
46
|
-
silent=True,
|
|
47
|
-
if_missing=True,
|
|
48
|
-
)
|
|
49
|
-
],
|
|
50
|
-
user=kwargs["username"],
|
|
51
|
-
)
|
|
34
|
+
outstanding_actions.append((id, "stop_viewing", False))
|
|
35
|
+
viewer_service.add_viewer(id, kwargs["username"])
|
|
52
36
|
elif action == "stop_viewing":
|
|
53
|
-
|
|
54
|
-
hit_service.update_hit(
|
|
55
|
-
id,
|
|
56
|
-
[hit_helper.list_remove("howler.viewers", kwargs["username"], silent=True)],
|
|
57
|
-
user=kwargs["username"],
|
|
58
|
-
)
|
|
37
|
+
viewer_service.remove_viewer(id, kwargs["username"])
|
|
59
38
|
outstanding_actions = [a for a in outstanding_actions if a[1] != "stop_viewing"]
|
|
60
39
|
|
|
61
40
|
return outstanding_actions
|
|
@@ -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.0.0.
|
|
155
|
+
version = "4.0.0.dev803"
|
|
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
|
|
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
|
|
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.0.0.dev799 → howler_api-4.0.0.dev803}/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
|
|
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
|