howler-api 4.0.0.dev803__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.dev803 → howler_api-4.0.0.dev841}/PKG-INFO +1 -1
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/api/socket.py +6 -1
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/api/v2/case.py +103 -1
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/api/v2/ingest.py +27 -0
- {howler_api-4.0.0.dev803 → 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.dev803 → howler_api-4.0.0.dev841}/howler/datastore/collection.py +27 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/case.py +25 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/config.py +14 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/random_data.py +29 -3
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/services/case_service.py +242 -60
- 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.dev803 → howler_api-4.0.0.dev841}/pyproject.toml +1 -1
- howler_api-4.0.0.dev803/howler/services/event_service.py +0 -96
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/README.md +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/__init__.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/actions/__init__.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/actions/add_label.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/actions/add_to_bundle.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/actions/add_to_case.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/actions/change_field.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/actions/demote.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/actions/example_plugin.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/actions/prioritization.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/actions/promote.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/actions/remove_from_bundle.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/actions/remove_label.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/actions/transition.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/api/__init__.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/api/base.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/api/v1/__init__.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/api/v1/action.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/api/v1/analytic.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/api/v1/auth.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/api/v1/clue.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/api/v1/configs.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/api/v1/dossier.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/api/v1/help.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/api/v1/hit.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/api/v1/notebook.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/api/v1/overview.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/api/v1/search.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/api/v1/template.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/api/v1/tool.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/api/v1/user.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/api/v1/utils/__init__.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/api/v1/utils/etag.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/api/v1/view.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/api/v2/__init__.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/api/v2/search.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/common/README.md +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/common/__init__.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/common/classification.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/common/classification.yml +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/common/exceptions.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/common/loader.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/common/logging/__init__.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/common/logging/audit.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/common/logging/format.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/common/net.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/common/net_static.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/common/random_user.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/common/swagger.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/config.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/cronjobs/__init__.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/cronjobs/retention.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/cronjobs/view_cleanup.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/datastore/README.md +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/datastore/__init__.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/datastore/bulk.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/datastore/constants.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/datastore/exceptions.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/datastore/howler_store.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/datastore/migrations/fix_process.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/datastore/operations.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/datastore/schemas.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/datastore/store.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/datastore/support/__init__.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/datastore/support/build.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/datastore/support/schemas.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/datastore/types.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/error.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/external/README.md +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/external/__init__.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/external/generate_mitre.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/external/generate_sigma_rules.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/external/generate_tlds.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/external/reindex_data.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/external/wipe_databases.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/gunicorn_config.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/healthz.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/helper/__init__.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/helper/azure.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/helper/discover.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/helper/hit.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/helper/oauth.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/helper/search.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/helper/workflow.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/helper/ws.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/README.md +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/__init__.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/base.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/charter.txt +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/constants.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/helper.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/howler_enum.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/mixins.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/__init__.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/action.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/analytic.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/assemblyline.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/aws.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/azure.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/cbs.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/clue.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/dossier.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/__init__.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/agent.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/autonomous_system.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/client.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/cloud.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/code_signature.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/container.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/dns.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/egress.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/elf.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/email.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/error.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/event.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/faas.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/file.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/geo.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/group.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/hash.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/host.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/http.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/ingress.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/interface.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/network.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/observer.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/organization.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/os.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/pe.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/process.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/registry.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/related.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/rule.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/server.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/threat.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/tls.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/url.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/user.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/user_agent.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/ecs/vulnerability.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/gcp.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/hit.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/howler_data.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/lead.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/localized_label.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/observable.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/overview.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/pivot.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/record.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/template.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/user.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/models/view.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/odm/randomizer.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/patched.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/plugins/__init__.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/plugins/config.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/remote/__init__.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/remote/datatypes/README.md +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/remote/datatypes/__init__.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/remote/datatypes/counters.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/remote/datatypes/events.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/remote/datatypes/hash.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/remote/datatypes/lock.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/remote/datatypes/queues/__init__.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/remote/datatypes/queues/comms.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/remote/datatypes/queues/multi.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/remote/datatypes/queues/named.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/remote/datatypes/queues/priority.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/remote/datatypes/set.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/remote/datatypes/user_quota_tracker.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/security/__init__.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/security/socket.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/security/utils.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/services/__init__.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/services/action_service.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/services/analytic_service.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/services/auth_service.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/services/bundle_compat_service.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/services/config_service.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/services/docs_service.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/services/dossier_service.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/services/hit_service.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/services/jwt_service.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/services/lucene_service.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/services/notebook_service.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/services/observable_service.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/services/overview_service.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/services/search_service.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/services/template_service.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/services/user_service.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/services/viewer_service.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/telemetry.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/utils/__init__.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/utils/annotations.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/utils/chunk.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/utils/compat.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/utils/constants.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/utils/dict_utils.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/utils/isotime.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/utils/list_utils.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/utils/lucene.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/utils/path.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/utils/socket_utils.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/utils/str_utils.py +0 -0
- {howler_api-4.0.0.dev803 → howler_api-4.0.0.dev841}/howler/utils/uid.py +0 -0
|
@@ -28,7 +28,12 @@ tracer = trace.get_tracer(__name__)
|
|
|
28
28
|
@tracer.start_as_current_span(f"{__name__}.emit")
|
|
29
29
|
@socket_api.route("/emit/<event>", methods=["POST"])
|
|
30
30
|
def emit(event: str):
|
|
31
|
-
"""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
|
+
"""
|
|
32
37
|
if "Authorization" not in request.headers:
|
|
33
38
|
return unauthorized(err="Missing authorization header")
|
|
34
39
|
|
|
@@ -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
|
|
|
@@ -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,
|
|
@@ -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):
|
|
@@ -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
|
}
|