howler-api 4.0.0.dev799__tar.gz → 4.0.0.dev841__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.dev841}/PKG-INFO +1 -1
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/api/socket.py +36 -6
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/api/v2/case.py +103 -1
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/api/v2/ingest.py +35 -3
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/app.py +5 -0
- howler_api-4.0.0.dev841/howler/cronjobs/correlation.py +36 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/datastore/collection.py +27 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/helper.py +0 -4
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/case.py +25 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/config.py +14 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/howler_data.py +0 -4
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/observable.py +0 -4
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/random_data.py +29 -3
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/services/case_service.py +273 -63
- howler_api-4.0.0.dev841/howler/services/correlation_service.py +168 -0
- howler_api-4.0.0.dev841/howler/services/event_service.py +134 -0
- howler_api-4.0.0.dev841/howler/services/viewer_service.py +43 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/utils/socket_utils.py +4 -25
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/pyproject.toml +1 -1
- howler_api-4.0.0.dev799/howler/services/event_service.py +0 -96
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/README.md +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/__init__.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/actions/__init__.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/actions/add_label.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/actions/add_to_bundle.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/actions/add_to_case.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/actions/change_field.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/actions/demote.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/actions/example_plugin.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/actions/prioritization.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/actions/promote.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/actions/remove_from_bundle.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/actions/remove_label.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/actions/transition.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/api/__init__.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/api/base.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/api/v1/__init__.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/api/v1/action.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/api/v1/analytic.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/api/v1/auth.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/api/v1/clue.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/api/v1/configs.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/api/v1/dossier.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/api/v1/help.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/api/v1/hit.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/api/v1/notebook.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/api/v1/overview.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/api/v1/search.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/api/v1/template.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/api/v1/tool.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/api/v1/user.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/api/v1/utils/__init__.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/api/v1/utils/etag.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/api/v1/view.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/api/v2/__init__.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/api/v2/search.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/common/README.md +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/common/__init__.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/common/classification.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/common/classification.yml +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/common/exceptions.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/common/loader.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/common/logging/__init__.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/common/logging/audit.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/common/logging/format.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/common/net.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/common/net_static.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/common/random_user.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/common/swagger.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/config.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/cronjobs/__init__.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/cronjobs/retention.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/cronjobs/view_cleanup.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/datastore/README.md +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/datastore/__init__.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/datastore/bulk.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/datastore/constants.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/datastore/exceptions.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/datastore/howler_store.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/datastore/migrations/fix_process.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/datastore/operations.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/datastore/schemas.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/datastore/store.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/datastore/support/__init__.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/datastore/support/build.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/datastore/support/schemas.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/datastore/types.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/error.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/external/README.md +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/external/__init__.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/external/generate_mitre.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/external/generate_sigma_rules.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/external/generate_tlds.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/external/reindex_data.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/external/wipe_databases.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/gunicorn_config.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/healthz.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/helper/__init__.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/helper/azure.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/helper/discover.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/helper/hit.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/helper/oauth.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/helper/search.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/helper/workflow.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/helper/ws.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/README.md +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/__init__.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/base.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/charter.txt +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/constants.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/howler_enum.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/mixins.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/__init__.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/action.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/analytic.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/assemblyline.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/aws.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/azure.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/cbs.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/clue.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/dossier.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/__init__.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/agent.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/autonomous_system.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/client.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/cloud.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/code_signature.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/container.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/dns.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/egress.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/elf.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/email.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/error.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/event.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/faas.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/file.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/geo.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/group.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/hash.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/host.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/http.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/ingress.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/interface.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/network.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/observer.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/organization.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/os.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/pe.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/process.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/registry.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/related.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/rule.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/server.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/threat.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/tls.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/url.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/user.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/user_agent.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/vulnerability.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/gcp.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/hit.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/lead.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/localized_label.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/overview.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/pivot.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/record.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/template.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/user.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/models/view.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/odm/randomizer.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/patched.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/plugins/__init__.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/plugins/config.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/remote/__init__.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/remote/datatypes/README.md +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/remote/datatypes/__init__.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/remote/datatypes/counters.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/remote/datatypes/events.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/remote/datatypes/hash.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/remote/datatypes/lock.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/remote/datatypes/queues/__init__.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/remote/datatypes/queues/comms.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/remote/datatypes/queues/multi.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/remote/datatypes/queues/named.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/remote/datatypes/queues/priority.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/remote/datatypes/set.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/remote/datatypes/user_quota_tracker.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/security/__init__.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/security/socket.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/security/utils.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/services/__init__.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/services/action_service.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/services/analytic_service.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/services/auth_service.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/services/bundle_compat_service.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/services/config_service.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/services/docs_service.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/services/dossier_service.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/services/hit_service.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/services/jwt_service.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/services/lucene_service.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/services/notebook_service.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/services/observable_service.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/services/overview_service.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/services/search_service.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/services/template_service.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/services/user_service.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/telemetry.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/utils/__init__.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/utils/annotations.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/utils/chunk.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/utils/compat.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/utils/constants.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/utils/dict_utils.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/utils/isotime.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/utils/list_utils.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/utils/lucene.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/utils/path.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/howler/utils/str_utils.py +0 -0
- {howler_api-4.0.0.dev799 → howler_api-4.0.0.dev841}/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,13 +24,16 @@ 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"])
|
|
32
30
|
def emit(event: str):
|
|
33
|
-
"""Emit an event to all listening websockets
|
|
31
|
+
"""Emit an event to all listening websockets.
|
|
32
|
+
|
|
33
|
+
.. deprecated::
|
|
34
|
+
This endpoint is deprecated. Events are now propagated via Redis pubsub
|
|
35
|
+
and no longer require a dedicated websocket pod.
|
|
36
|
+
"""
|
|
34
37
|
if "Authorization" not in request.headers:
|
|
35
38
|
return unauthorized(err="Missing authorization header")
|
|
36
39
|
|
|
@@ -49,10 +52,25 @@ def emit(event: str):
|
|
|
49
52
|
return ok()
|
|
50
53
|
|
|
51
54
|
|
|
55
|
+
@tracer.start_as_current_span(f"{__name__}.get_viewers")
|
|
56
|
+
@socket_api.route("/viewers/<entity_id>", methods=["GET"])
|
|
57
|
+
@api_login(audit=False, required_priv=["R"])
|
|
58
|
+
def get_viewers(entity_id: str, **kwargs):
|
|
59
|
+
"""Get the list of users currently viewing the specified entity
|
|
60
|
+
|
|
61
|
+
Variables:
|
|
62
|
+
entity_id => The ID of the entity to get viewers for
|
|
63
|
+
|
|
64
|
+
Result Example:
|
|
65
|
+
["user1", "user2"]
|
|
66
|
+
"""
|
|
67
|
+
return ok(viewer_service.get_viewers(entity_id))
|
|
68
|
+
|
|
69
|
+
|
|
52
70
|
@tracer.start_as_current_span(f"{__name__}.connect")
|
|
53
71
|
@socket_api.route("/connect", websocket=True) # type: ignore
|
|
54
72
|
@websocket_auth(required_priv=["R"])
|
|
55
|
-
def connect(ws: Server, *args: Any, ws_id: str, **kwargs):
|
|
73
|
+
def connect(ws: Server, *args: Any, ws_id: str, **kwargs): # noqa: C901
|
|
56
74
|
"""Connect to the server to monitor for updates via websocket
|
|
57
75
|
|
|
58
76
|
Variables:
|
|
@@ -78,10 +96,20 @@ def connect(ws: Server, *args: Any, ws_id: str, **kwargs):
|
|
|
78
96
|
logger.debug("Sending action: %s", data)
|
|
79
97
|
ws.send(ws_response("action", data))
|
|
80
98
|
|
|
99
|
+
def send_case(data: dict[str, Any]):
|
|
100
|
+
logger.debug("Sending case update: %s", data.get("case", {}).get("case_id", "unknown"))
|
|
101
|
+
ws.send(ws_response("cases", data))
|
|
102
|
+
|
|
103
|
+
def send_viewers_update(data: dict[str, Any]):
|
|
104
|
+
logger.debug("Sending viewers update: %s", data.get("id", "unknown"))
|
|
105
|
+
ws.send(ws_response("viewers_update", data))
|
|
106
|
+
|
|
81
107
|
try:
|
|
82
108
|
event_service.on("hits", send_hit)
|
|
83
109
|
event_service.on("broadcast", send_broadcast)
|
|
84
110
|
event_service.on("action", send_action)
|
|
111
|
+
event_service.on("cases", send_case)
|
|
112
|
+
event_service.on("viewers_update", send_viewers_update)
|
|
85
113
|
while ws.connected:
|
|
86
114
|
data = ws.receive(10)
|
|
87
115
|
if data:
|
|
@@ -113,6 +141,8 @@ def connect(ws: Server, *args: Any, ws_id: str, **kwargs):
|
|
|
113
141
|
event_service.off("hits", send_hit)
|
|
114
142
|
event_service.off("broadcast", send_broadcast)
|
|
115
143
|
event_service.off("action", send_action)
|
|
144
|
+
event_service.off("cases", send_case)
|
|
145
|
+
event_service.off("viewers_update", send_viewers_update)
|
|
116
146
|
|
|
117
147
|
for id, action, broadcast in outstanding_actions:
|
|
118
148
|
outstanding_actions = check_action(id, action, broadcast, outstanding_actions=outstanding_actions, **kwargs)
|
|
@@ -290,7 +290,7 @@ def delete_item(case_id: str, **kwargs):
|
|
|
290
290
|
|
|
291
291
|
|
|
292
292
|
@generate_swagger_docs()
|
|
293
|
-
@case_api.route("/<case_id>/items", methods=["
|
|
293
|
+
@case_api.route("/<case_id>/items", methods=["PUT"])
|
|
294
294
|
@api_login(required_priv=["R", "W"])
|
|
295
295
|
def rename_item(case_id: str, **kwargs):
|
|
296
296
|
"""Rename (re-path) an item within a case
|
|
@@ -331,3 +331,105 @@ def rename_item(case_id: str, **kwargs):
|
|
|
331
331
|
return internal_error(err=str(e))
|
|
332
332
|
except (InvalidDataException, NotFoundException) as e:
|
|
333
333
|
return bad_request(err=str(e))
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
@generate_swagger_docs()
|
|
337
|
+
@case_api.route("/<id>/rules", methods=["POST"])
|
|
338
|
+
@api_login(required_priv=["R", "W"])
|
|
339
|
+
def add_rule(id: str, user: User, **kwargs):
|
|
340
|
+
"""Add a correlation rule to a case
|
|
341
|
+
|
|
342
|
+
Creates a new correlation rule that will match incoming alerts into the case.
|
|
343
|
+
The rule's id and author are generated server-side.
|
|
344
|
+
|
|
345
|
+
Variables:
|
|
346
|
+
id => The id of the case to add a rule to
|
|
347
|
+
|
|
348
|
+
Arguments:
|
|
349
|
+
None
|
|
350
|
+
|
|
351
|
+
Data Block:
|
|
352
|
+
{
|
|
353
|
+
"query": "howler.analytic:Suspicious*",
|
|
354
|
+
"destination": "alerts/{{howler.analytic}}",
|
|
355
|
+
"timeframe": "2026-05-06T00:00:00Z" // optional, null means no expiry
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
Result Example:
|
|
359
|
+
{
|
|
360
|
+
...case # The updated case data
|
|
361
|
+
}
|
|
362
|
+
"""
|
|
363
|
+
body = request.json
|
|
364
|
+
|
|
365
|
+
if not body or not isinstance(body, dict):
|
|
366
|
+
return bad_request(err="Request body must be a JSON object with rule data.")
|
|
367
|
+
|
|
368
|
+
try:
|
|
369
|
+
return ok(case_service.add_case_rule(id, body, user))
|
|
370
|
+
except NotFoundException as e:
|
|
371
|
+
return not_found(err=str(e))
|
|
372
|
+
except InvalidDataException as e:
|
|
373
|
+
return bad_request(err=str(e))
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
@generate_swagger_docs()
|
|
377
|
+
@case_api.route("/<id>/rules/<rule_id>", methods=["DELETE"])
|
|
378
|
+
@api_login(required_priv=["R", "W"])
|
|
379
|
+
def delete_rule(id: str, rule_id: str, user: User, **kwargs):
|
|
380
|
+
"""Delete a correlation rule from a case
|
|
381
|
+
|
|
382
|
+
Variables:
|
|
383
|
+
id => The id of the case
|
|
384
|
+
rule_id => The id of the rule to delete
|
|
385
|
+
|
|
386
|
+
Arguments:
|
|
387
|
+
None
|
|
388
|
+
|
|
389
|
+
Result Example:
|
|
390
|
+
{
|
|
391
|
+
...case # The updated case data
|
|
392
|
+
}
|
|
393
|
+
"""
|
|
394
|
+
try:
|
|
395
|
+
return ok(case_service.remove_case_rule(id, rule_id, user))
|
|
396
|
+
except NotFoundException as e:
|
|
397
|
+
return not_found(err=str(e))
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
@generate_swagger_docs()
|
|
401
|
+
@case_api.route("/<id>/rules/<rule_id>", methods=["PUT"])
|
|
402
|
+
@api_login(required_priv=["R", "W"])
|
|
403
|
+
def update_rule(id: str, rule_id: str, user: User, **kwargs):
|
|
404
|
+
"""Update a correlation rule on a case
|
|
405
|
+
|
|
406
|
+
Allows updating individual fields on a rule: enabled, query, destination, timeframe.
|
|
407
|
+
|
|
408
|
+
Variables:
|
|
409
|
+
id => The id of the case
|
|
410
|
+
rule_id => The id of the rule to update
|
|
411
|
+
|
|
412
|
+
Arguments:
|
|
413
|
+
None
|
|
414
|
+
|
|
415
|
+
Data Block:
|
|
416
|
+
{
|
|
417
|
+
"enabled": false
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
Result Example:
|
|
421
|
+
{
|
|
422
|
+
...case # The updated case data
|
|
423
|
+
}
|
|
424
|
+
"""
|
|
425
|
+
body = request.json
|
|
426
|
+
|
|
427
|
+
if not body or not isinstance(body, dict):
|
|
428
|
+
return bad_request(err="Request body must be a JSON object with fields to update.")
|
|
429
|
+
|
|
430
|
+
try:
|
|
431
|
+
return ok(case_service.update_case_rule(id, rule_id, body, user))
|
|
432
|
+
except NotFoundException as e:
|
|
433
|
+
return not_found(err=str(e))
|
|
434
|
+
except InvalidDataException as e:
|
|
435
|
+
return bad_request(err=str(e))
|
|
@@ -9,6 +9,7 @@ from howler.common.exceptions import HowlerException, HowlerValueError
|
|
|
9
9
|
from howler.common.loader import datastore
|
|
10
10
|
from howler.common.logging import get_logger
|
|
11
11
|
from howler.common.swagger import generate_swagger_docs
|
|
12
|
+
from howler.config import config
|
|
12
13
|
from howler.datastore.collection import ESCollection
|
|
13
14
|
from howler.datastore.exceptions import DataStoreException
|
|
14
15
|
from howler.datastore.howler_store import INDEXES
|
|
@@ -16,6 +17,7 @@ from howler.datastore.operations import OdmHelper, OdmUpdateOperation
|
|
|
16
17
|
from howler.odm.models.hit import Hit
|
|
17
18
|
from howler.odm.models.observable import Observable
|
|
18
19
|
from howler.odm.models.user import User
|
|
20
|
+
from howler.remote.datatypes.queues.named import NamedQueue
|
|
19
21
|
from howler.security import api_login
|
|
20
22
|
from howler.services import hit_service, observable_service
|
|
21
23
|
from howler.utils.dict_utils import flatten
|
|
@@ -32,6 +34,24 @@ logger = get_logger(__file__)
|
|
|
32
34
|
|
|
33
35
|
hit_helper = OdmHelper(Hit)
|
|
34
36
|
|
|
37
|
+
# Persistent queue for the correlation worker to consume newly ingested hit IDs.
|
|
38
|
+
_ingestion_queue: NamedQueue[str] | None = None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _get_ingestion_queue() -> NamedQueue[str]:
|
|
42
|
+
"""Return the shared ingestion queue, creating it on first use."""
|
|
43
|
+
global _ingestion_queue
|
|
44
|
+
|
|
45
|
+
if _ingestion_queue is None:
|
|
46
|
+
_ingestion_queue = NamedQueue(
|
|
47
|
+
"howler.ingestion_queue",
|
|
48
|
+
host=config.core.redis.persistent.host,
|
|
49
|
+
port=config.core.redis.persistent.port,
|
|
50
|
+
private=False,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
return _ingestion_queue
|
|
54
|
+
|
|
35
55
|
|
|
36
56
|
@generate_swagger_docs()
|
|
37
57
|
@ingest_api.route("/<index>", methods=["POST"])
|
|
@@ -93,6 +113,13 @@ def create(index: str, user: User, **kwargs):
|
|
|
93
113
|
logger.exception("Ingestion failed.")
|
|
94
114
|
return bad_request(err=f"Ingestion failure on record at index {i}: {e}")
|
|
95
115
|
|
|
116
|
+
# Enqueue newly created hit IDs for the correlation worker.
|
|
117
|
+
if ids:
|
|
118
|
+
try:
|
|
119
|
+
_get_ingestion_queue().push(*ids)
|
|
120
|
+
except Exception:
|
|
121
|
+
logger.exception("Failed to enqueue hit IDs for correlation")
|
|
122
|
+
|
|
96
123
|
return created(ids, warnings=warnings)
|
|
97
124
|
|
|
98
125
|
|
|
@@ -120,15 +147,20 @@ def delete(indexes: str, user: User, **kwargs):
|
|
|
120
147
|
"success": True # Deleting the hits succeded
|
|
121
148
|
}
|
|
122
149
|
"""
|
|
123
|
-
|
|
150
|
+
ids = request.json
|
|
124
151
|
|
|
125
|
-
if
|
|
152
|
+
if ids is None:
|
|
126
153
|
return bad_request(err="No hit ids were sent.")
|
|
127
154
|
|
|
128
155
|
if "admin" not in user["type"]:
|
|
129
156
|
return forbidden(err="Cannot delete hit, only administrators are permitted to delete.")
|
|
130
157
|
|
|
131
|
-
index_list = indexes.split(",")
|
|
158
|
+
index_list = indexes.split(",")
|
|
159
|
+
|
|
160
|
+
ds = datastore()
|
|
161
|
+
|
|
162
|
+
if non_existing_hit_ids := [id for id in ids if all(not ds[index].exists(id) for index in index_list)]:
|
|
163
|
+
return not_found(err=f"Record ids [{','.join(non_existing_hit_ids)}] do not exist.")
|
|
132
164
|
|
|
133
165
|
# TODO: Reimplement in a generic function
|
|
134
166
|
# hit_service.delete_hits(hit_ids, indexes=index_list)
|
|
@@ -174,6 +174,11 @@ else:
|
|
|
174
174
|
if HWL_USE_WEBSOCKET_API or DEBUG:
|
|
175
175
|
logger.debug("Enabled Websocket API")
|
|
176
176
|
app.register_blueprint(socket_api)
|
|
177
|
+
|
|
178
|
+
# Start the Redis pubsub watcher so this pod receives events from all pods
|
|
179
|
+
import howler.services.event_service as event_service
|
|
180
|
+
|
|
181
|
+
event_service.start_watcher()
|
|
177
182
|
else:
|
|
178
183
|
logger.info("Disabled Websocket API")
|
|
179
184
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Correlation cronjob — starts the correlation worker thread.
|
|
2
|
+
|
|
3
|
+
Auto-discovered by ``howler.cronjobs.setup_jobs`` when ``HWL_USE_JOB_SYSTEM``
|
|
4
|
+
is enabled. Instead of scheduling a periodic APScheduler job, this module
|
|
5
|
+
starts a long-running daemon thread that drains the ingestion queue.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import threading
|
|
9
|
+
|
|
10
|
+
from apscheduler.schedulers.base import BaseScheduler
|
|
11
|
+
|
|
12
|
+
from howler.common.logging import get_logger
|
|
13
|
+
from howler.odm.models.config import config
|
|
14
|
+
|
|
15
|
+
logger = get_logger(__file__)
|
|
16
|
+
|
|
17
|
+
_thread: threading.Thread | None = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def setup_job(sched: BaseScheduler):
|
|
21
|
+
"""Start the correlation worker thread if correlation is enabled."""
|
|
22
|
+
global _thread
|
|
23
|
+
|
|
24
|
+
if not config.system.correlation.enabled:
|
|
25
|
+
logger.info("Correlation worker disabled by configuration")
|
|
26
|
+
return
|
|
27
|
+
|
|
28
|
+
if _thread is not None and _thread.is_alive():
|
|
29
|
+
logger.debug("Correlation worker thread already running")
|
|
30
|
+
return
|
|
31
|
+
|
|
32
|
+
from howler.services.correlation_service import run_worker
|
|
33
|
+
|
|
34
|
+
_thread = threading.Thread(target=run_worker, name="correlation-worker", daemon=True)
|
|
35
|
+
_thread.start()
|
|
36
|
+
logger.info("Correlation worker thread started")
|
|
@@ -1365,6 +1365,7 @@ class ESCollection(Generic[ModelType]):
|
|
|
1365
1365
|
extra_fields["_index"] = result["_index"]
|
|
1366
1366
|
if "*" in fields:
|
|
1367
1367
|
fields = None
|
|
1368
|
+
|
|
1368
1369
|
return self.model_class(source_data, mask=fields, docid=item_id, extra_fields=extra_fields)
|
|
1369
1370
|
else:
|
|
1370
1371
|
source_data = recursive_update(source_data, extra_fields, allow_recursion=False)
|
|
@@ -1692,6 +1693,32 @@ class ESCollection(Generic[ModelType]):
|
|
|
1692
1693
|
|
|
1693
1694
|
return ret_data
|
|
1694
1695
|
|
|
1696
|
+
@overload
|
|
1697
|
+
def stream_search(
|
|
1698
|
+
self,
|
|
1699
|
+
query: str,
|
|
1700
|
+
fl: str | None = None,
|
|
1701
|
+
filters: list[str] | str | None = None,
|
|
1702
|
+
access_control: typing.Any = None,
|
|
1703
|
+
item_buffer_size: int = 200,
|
|
1704
|
+
*,
|
|
1705
|
+
as_obj: Literal[True] = True,
|
|
1706
|
+
use_archive: bool = False,
|
|
1707
|
+
) -> typing.Generator[ModelType, None, None]: ...
|
|
1708
|
+
|
|
1709
|
+
@overload
|
|
1710
|
+
def stream_search(
|
|
1711
|
+
self,
|
|
1712
|
+
query: str,
|
|
1713
|
+
fl: str | None = None,
|
|
1714
|
+
filters: list[str] | str | None = None,
|
|
1715
|
+
access_control: typing.Any = None,
|
|
1716
|
+
item_buffer_size: int = 200,
|
|
1717
|
+
*,
|
|
1718
|
+
as_obj: Literal[False],
|
|
1719
|
+
use_archive: bool = False,
|
|
1720
|
+
) -> typing.Generator[dict[str, typing.Any], None, None]: ...
|
|
1721
|
+
|
|
1695
1722
|
def stream_search(
|
|
1696
1723
|
self,
|
|
1697
1724
|
query,
|
|
@@ -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
|
|
|
@@ -8,6 +8,19 @@ from howler.utils.compat import StrEnum
|
|
|
8
8
|
|
|
9
9
|
CASE_ITEM_TYPES = {"observable", "hit", "case", "lead", "reference"}
|
|
10
10
|
|
|
11
|
+
RULE_INDEX_TYPES = {"hit", "observable"}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class RuleIndexTypes(StrEnum):
|
|
15
|
+
"""Enumeration of valid index types for case rules.
|
|
16
|
+
|
|
17
|
+
Determines which Elasticsearch indexes a case rule query runs against
|
|
18
|
+
during correlation.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
HIT = "hit"
|
|
22
|
+
OBSERVABLE = "observable"
|
|
23
|
+
|
|
11
24
|
|
|
12
25
|
class CaseItemTypes(StrEnum):
|
|
13
26
|
"""Enumeration of valid case item types.
|
|
@@ -69,8 +82,20 @@ class CaseItem(odm.Model):
|
|
|
69
82
|
|
|
70
83
|
@odm.model(index=True, store=True, description="Rule used to place/query data into case paths.")
|
|
71
84
|
class CaseRule(odm.Model):
|
|
85
|
+
rule_id: str = odm.UUID(description="Unique rule identifier.")
|
|
72
86
|
destination: str = odm.Keyword(description="Destination case path template.")
|
|
73
87
|
query: str = odm.Keyword(description="Lucene query used by this rule.")
|
|
88
|
+
author: str = odm.Keyword(description="Username who created the rule.")
|
|
89
|
+
enabled: bool = odm.Boolean(default=True, description="Whether the rule is currently active.")
|
|
90
|
+
timeframe: Optional[str] = odm.Optional(
|
|
91
|
+
odm.Date(description="ISO datetime when rule expires. Null means no expiry."),
|
|
92
|
+
default=None,
|
|
93
|
+
)
|
|
94
|
+
indexes: list[str] = odm.List(
|
|
95
|
+
odm.Enum(values=RuleIndexTypes),
|
|
96
|
+
default=[RuleIndexTypes.HIT],
|
|
97
|
+
description="Indexes to run this rule against (hit, observable, or both).",
|
|
98
|
+
)
|
|
74
99
|
|
|
75
100
|
|
|
76
101
|
@odm.model(index=True, store=True, description="Task associated with a case item path.")
|
|
@@ -353,6 +353,18 @@ class ViewCleanup(BaseModel):
|
|
|
353
353
|
)
|
|
354
354
|
|
|
355
355
|
|
|
356
|
+
class Correlation(BaseModel):
|
|
357
|
+
"""Correlation worker configuration.
|
|
358
|
+
|
|
359
|
+
Controls the background worker that matches newly ingested alerts
|
|
360
|
+
against active case rules.
|
|
361
|
+
"""
|
|
362
|
+
|
|
363
|
+
enabled: bool = Field(default=True, description="Enable the correlation worker?")
|
|
364
|
+
batch_size: int = Field(default=100, description="Max alerts per batch.")
|
|
365
|
+
batch_timeout: int = Field(default=10, description="Seconds to wait before flushing a partial batch.")
|
|
366
|
+
|
|
367
|
+
|
|
356
368
|
class System(BaseModel):
|
|
357
369
|
"""System-level configuration for Howler.
|
|
358
370
|
|
|
@@ -366,6 +378,8 @@ class System(BaseModel):
|
|
|
366
378
|
"Retention Configuration"
|
|
367
379
|
view_cleanup: ViewCleanup = ViewCleanup()
|
|
368
380
|
"View Cleanup Configuration"
|
|
381
|
+
correlation: Correlation = Correlation()
|
|
382
|
+
"Correlation Worker Configuration"
|
|
369
383
|
|
|
370
384
|
|
|
371
385
|
class UI(BaseModel):
|
|
@@ -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(
|
|
@@ -19,7 +19,7 @@ import importlib
|
|
|
19
19
|
import json
|
|
20
20
|
import random
|
|
21
21
|
import textwrap
|
|
22
|
-
from datetime import datetime
|
|
22
|
+
from datetime import datetime, timedelta
|
|
23
23
|
from random import choice, randint, sample
|
|
24
24
|
from typing import Any, Callable, cast
|
|
25
25
|
from uuid import uuid4
|
|
@@ -866,9 +866,35 @@ def create_cases(ds: HowlerDatastore, num_cases: int = 5):
|
|
|
866
866
|
"enrichments": [],
|
|
867
867
|
"rules": [
|
|
868
868
|
{
|
|
869
|
-
"destination":
|
|
870
|
-
|
|
869
|
+
"destination": choice(
|
|
870
|
+
[
|
|
871
|
+
"alerts/{{howler.analytic}}",
|
|
872
|
+
"incoming/{{event.kind}}",
|
|
873
|
+
"alerts/{{howler.analytic}}/{{event.category}}",
|
|
874
|
+
"correlated/{{source.ip}}",
|
|
875
|
+
"triage/{{howler.escalation}}",
|
|
876
|
+
]
|
|
877
|
+
),
|
|
878
|
+
"query": choice(
|
|
879
|
+
[
|
|
880
|
+
f"destination.domain:{choice(threat_pool)}",
|
|
881
|
+
"source.ip:10.0.0.0/8 AND howler.analytic:Suspicious*",
|
|
882
|
+
"event.category:authentication AND event.outcome:failure",
|
|
883
|
+
"howler.escalation:focus OR howler.escalation:crisis",
|
|
884
|
+
f"destination.domain:{choice(threat_pool)} AND event.kind:alert",
|
|
885
|
+
]
|
|
886
|
+
),
|
|
887
|
+
"author": choice(selected_participants or ["admin"]),
|
|
888
|
+
"enabled": choice([True, True, True, False]),
|
|
889
|
+
"timeframe": choice(
|
|
890
|
+
[
|
|
891
|
+
(datetime.now() + timedelta(days=randint(7, 28))).isoformat(),
|
|
892
|
+
(datetime.now() + timedelta(days=randint(7, 28))).isoformat(),
|
|
893
|
+
None,
|
|
894
|
+
]
|
|
895
|
+
),
|
|
871
896
|
}
|
|
897
|
+
for _ in range(randint(1, 3))
|
|
872
898
|
],
|
|
873
899
|
"tasks": tasks,
|
|
874
900
|
}
|