howler-api 4.0.0.dev596__tar.gz → 4.0.0.dev642__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.dev596 → howler_api-4.0.0.dev642}/PKG-INFO +2 -2
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/api/v1/dossier.py +1 -1
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/api/v1/search.py +2 -1
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/api/v2/case.py +1 -2
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/api/v2/ingest.py +1 -1
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/api/v2/search.py +1 -1
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/case.py +8 -2
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/observable.py +0 -1
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/record.py +8 -1
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/random_data.py +2 -7
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/services/case_service.py +104 -46
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/services/dossier_service.py +4 -2
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/services/hit_service.py +4 -4
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/services/lucene_service.py +5 -3
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/pyproject.toml +3 -3
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/README.md +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/__init__.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/actions/__init__.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/actions/add_label.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/actions/change_field.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/actions/demote.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/actions/example_plugin.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/actions/prioritization.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/actions/promote.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/actions/remove_label.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/actions/transition.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/api/__init__.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/api/base.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/api/socket.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/api/v1/__init__.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/api/v1/action.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/api/v1/analytic.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/api/v1/auth.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/api/v1/clue.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/api/v1/configs.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/api/v1/help.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/api/v1/hit.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/api/v1/notebook.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/api/v1/overview.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/api/v1/template.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/api/v1/tool.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/api/v1/user.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/api/v1/utils/__init__.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/api/v1/utils/etag.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/api/v1/view.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/api/v2/__init__.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/app.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/common/README.md +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/common/__init__.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/common/classification.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/common/classification.yml +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/common/exceptions.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/common/loader.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/common/logging/__init__.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/common/logging/audit.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/common/logging/format.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/common/net.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/common/net_static.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/common/random_user.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/common/swagger.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/config.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/cronjobs/__init__.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/cronjobs/retention.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/cronjobs/view_cleanup.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/datastore/README.md +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/datastore/__init__.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/datastore/bulk.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/datastore/collection.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/datastore/constants.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/datastore/exceptions.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/datastore/howler_store.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/datastore/migrations/fix_process.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/datastore/operations.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/datastore/schemas.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/datastore/store.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/datastore/support/__init__.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/datastore/support/build.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/datastore/support/schemas.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/datastore/types.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/error.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/external/__init__.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/external/generate_mitre.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/external/generate_sigma_rules.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/external/generate_tlds.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/external/reindex_data.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/external/wipe_databases.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/gunicorn_config.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/healthz.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/helper/__init__.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/helper/azure.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/helper/discover.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/helper/hit.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/helper/oauth.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/helper/search.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/helper/workflow.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/helper/ws.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/README.md +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/__init__.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/base.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/charter.txt +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/constants.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/helper.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/howler_enum.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/__init__.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/action.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/analytic.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/assemblyline.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/aws.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/azure.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/cbs.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/clue.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/config.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/dossier.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/__init__.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/agent.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/autonomous_system.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/client.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/cloud.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/code_signature.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/container.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/dns.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/egress.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/elf.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/email.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/error.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/event.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/faas.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/file.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/geo.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/group.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/hash.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/host.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/http.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/ingress.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/interface.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/network.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/observer.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/organization.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/os.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/pe.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/process.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/registry.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/related.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/rule.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/server.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/threat.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/tls.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/url.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/user.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/user_agent.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/ecs/vulnerability.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/gcp.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/hit.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/howler_data.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/lead.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/localized_label.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/overview.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/pivot.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/template.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/user.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/models/view.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/odm/randomizer.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/patched.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/plugins/__init__.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/plugins/config.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/remote/__init__.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/remote/datatypes/README.md +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/remote/datatypes/__init__.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/remote/datatypes/counters.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/remote/datatypes/events.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/remote/datatypes/hash.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/remote/datatypes/lock.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/remote/datatypes/queues/__init__.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/remote/datatypes/queues/comms.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/remote/datatypes/queues/multi.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/remote/datatypes/queues/named.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/remote/datatypes/queues/priority.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/remote/datatypes/set.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/remote/datatypes/user_quota_tracker.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/security/__init__.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/security/socket.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/security/utils.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/services/__init__.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/services/action_service.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/services/analytic_service.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/services/auth_service.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/services/config_service.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/services/docs_service.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/services/event_service.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/services/jwt_service.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/services/notebook_service.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/services/observable_service.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/services/overview_service.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/services/search_service.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/services/template_service.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/services/user_service.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/utils/__init__.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/utils/annotations.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/utils/chunk.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/utils/compat.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/utils/dict_utils.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/utils/isotime.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/utils/list_utils.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/utils/lucene.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/utils/path.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/utils/socket_utils.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/utils/str_utils.py +0 -0
- {howler_api-4.0.0.dev596 → howler_api-4.0.0.dev642}/howler/utils/uid.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: howler-api
|
|
3
|
-
Version: 4.0.0.
|
|
3
|
+
Version: 4.0.0.dev642
|
|
4
4
|
Summary: Howler - API server
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: howler,alerting,gc,canada,cse-cst,cse,cst,cyber,cccs
|
|
@@ -39,7 +39,7 @@ Requires-Dist: prometheus-client (==0.24.1)
|
|
|
39
39
|
Requires-Dist: pydantic (>=2.11.4,<3.0.0)
|
|
40
40
|
Requires-Dist: pydantic-settings[yaml] (>=2.9.1,<3.0.0)
|
|
41
41
|
Requires-Dist: pydash (>=8.0.5,<9.0.0)
|
|
42
|
-
Requires-Dist: pyjwt (==2.
|
|
42
|
+
Requires-Dist: pyjwt (==2.12.1)
|
|
43
43
|
Requires-Dist: pysigma (==0.11.23)
|
|
44
44
|
Requires-Dist: pysigma-backend-elasticsearch (>=1.1.2,<2.0.0)
|
|
45
45
|
Requires-Dist: python-baseconv (==1.2.2)
|
|
@@ -139,7 +139,7 @@ def get_dossier_for_hit(id: str, user: User, **kwargs):
|
|
|
139
139
|
|
|
140
140
|
hit = response["items"][0]
|
|
141
141
|
|
|
142
|
-
return ok(dossier_service.get_matching_dossiers(hit))
|
|
142
|
+
return ok(dossier_service.get_matching_dossiers(hit, username=user.uname))
|
|
143
143
|
except ValueError as e:
|
|
144
144
|
return bad_request(err=str(e))
|
|
145
145
|
|
|
@@ -16,6 +16,7 @@ from howler.common.logging import get_logger
|
|
|
16
16
|
from howler.common.swagger import generate_swagger_docs
|
|
17
17
|
from howler.datastore.exceptions import SearchException
|
|
18
18
|
from howler.helper.search import get_collection, get_default_sort, has_access_control, list_all_fields
|
|
19
|
+
from howler.odm.models.user import User
|
|
19
20
|
from howler.security import api_login
|
|
20
21
|
from howler.services import hit_service, lucene_service
|
|
21
22
|
|
|
@@ -100,7 +101,7 @@ def search(index, **kwargs):
|
|
|
100
101
|
"next_deep_paging_id": "asX3f...342", # ID to pass back for the next page during deep paging
|
|
101
102
|
"items": []} # List of results
|
|
102
103
|
"""
|
|
103
|
-
user = kwargs["user"]
|
|
104
|
+
user: User = kwargs["user"]
|
|
104
105
|
collection = get_collection(index, user)
|
|
105
106
|
default_sort = get_default_sort(index, user)
|
|
106
107
|
|
|
@@ -48,8 +48,7 @@ def create_case(user: User, **kwargs):
|
|
|
48
48
|
return bad_request(err="Request body must be a JSON object with case data.")
|
|
49
49
|
|
|
50
50
|
try:
|
|
51
|
-
|
|
52
|
-
return created(new_case)
|
|
51
|
+
return created(case_service.create_case(case_data, user.uname))
|
|
53
52
|
except InvalidDataException as e:
|
|
54
53
|
return bad_request(err=str(e))
|
|
55
54
|
except ResourceExists as e:
|
|
@@ -279,7 +279,7 @@ def facet(indexes: str, **kwargs):
|
|
|
279
279
|
values where the documents matches the specified queries.
|
|
280
280
|
|
|
281
281
|
Variables:
|
|
282
|
-
|
|
282
|
+
indexes => Comma-separated indexes to search in (hit, user,...)
|
|
283
283
|
|
|
284
284
|
Optional Arguments:
|
|
285
285
|
query => Query to search for
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Optional
|
|
1
|
+
from typing import Any, Optional
|
|
2
2
|
|
|
3
3
|
from howler import odm
|
|
4
4
|
from howler.common.exceptions import HowlerValueError
|
|
@@ -62,7 +62,6 @@ class CaseLog(odm.Model):
|
|
|
62
62
|
class CaseItem(odm.Model):
|
|
63
63
|
path: str = odm.Keyword(description="Path of the item in the case hierarchy.")
|
|
64
64
|
type: str = odm.Enum(values=CaseItemTypes, description="Type of case item.")
|
|
65
|
-
id: str | None = odm.Keyword(description="Identifier for the backing object when available.", optional=True)
|
|
66
65
|
value: str = odm.Keyword(description="String reference value for the item (ID, URL, or token).")
|
|
67
66
|
visible: bool = odm.Boolean(default=True, description="Whether the item is visible/accessible in the frontend.")
|
|
68
67
|
|
|
@@ -177,3 +176,10 @@ class Case(odm.Model):
|
|
|
177
176
|
description="A list of changes to the case with timestamps and attribution.",
|
|
178
177
|
)
|
|
179
178
|
)
|
|
179
|
+
|
|
180
|
+
def as_primitives(self, hidden_fields=False, strip_null=True) -> dict[str, Any]:
|
|
181
|
+
result = super().as_primitives(hidden_fields, strip_null)
|
|
182
|
+
|
|
183
|
+
result["__index"] = self.__class__.__name__.lower()
|
|
184
|
+
|
|
185
|
+
return result
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# mypy: ignore-errors
|
|
2
|
-
from typing import Optional
|
|
2
|
+
from typing import Any, Optional
|
|
3
3
|
|
|
4
4
|
from howler import odm
|
|
5
5
|
from howler.common.logging import get_logger
|
|
@@ -334,3 +334,10 @@ class Record(odm.Model):
|
|
|
334
334
|
reference="https://www.elastic.co/guide/en/ecs/8.5/ecs-vulnerability.html",
|
|
335
335
|
)
|
|
336
336
|
)
|
|
337
|
+
|
|
338
|
+
def as_primitives(self, hidden_fields=False, strip_null=True) -> dict[str, Any]:
|
|
339
|
+
result = super().as_primitives(hidden_fields, strip_null)
|
|
340
|
+
|
|
341
|
+
result["__index"] = self.__class__.__name__.lower()
|
|
342
|
+
|
|
343
|
+
return result
|
|
@@ -717,7 +717,6 @@ def create_cases(ds: HowlerDatastore, num_cases: int = 5):
|
|
|
717
717
|
{
|
|
718
718
|
"path": f"alerts/{hit['howler']['analytic']} ({hit['howler']['id']})",
|
|
719
719
|
"type": "hit",
|
|
720
|
-
"id": hit_id,
|
|
721
720
|
"value": hit_id,
|
|
722
721
|
}
|
|
723
722
|
)
|
|
@@ -731,7 +730,6 @@ def create_cases(ds: HowlerDatastore, num_cases: int = 5):
|
|
|
731
730
|
{
|
|
732
731
|
"path": f"observable/{observable['howler']['id']}",
|
|
733
732
|
"type": "observable",
|
|
734
|
-
"id": observable_id,
|
|
735
733
|
"value": observable_id,
|
|
736
734
|
}
|
|
737
735
|
)
|
|
@@ -747,7 +745,6 @@ def create_cases(ds: HowlerDatastore, num_cases: int = 5):
|
|
|
747
745
|
{
|
|
748
746
|
"path": f"alerts/{get_random_word()}/{get_random_word()}",
|
|
749
747
|
"type": "hit",
|
|
750
|
-
"id": nested_hit_id,
|
|
751
748
|
"value": nested_hit_id,
|
|
752
749
|
}
|
|
753
750
|
)
|
|
@@ -769,7 +766,6 @@ def create_cases(ds: HowlerDatastore, num_cases: int = 5):
|
|
|
769
766
|
{
|
|
770
767
|
"path": f"alerts/{get_random_word()}/{get_random_word()}/{get_random_word()}",
|
|
771
768
|
"type": "observable",
|
|
772
|
-
"id": nested_observable_id,
|
|
773
769
|
"value": nested_observable_id,
|
|
774
770
|
}
|
|
775
771
|
)
|
|
@@ -788,7 +784,6 @@ def create_cases(ds: HowlerDatastore, num_cases: int = 5):
|
|
|
788
784
|
{
|
|
789
785
|
"path": f"cases/Related Case {idx}",
|
|
790
786
|
"type": "case",
|
|
791
|
-
"id": related_case_id,
|
|
792
787
|
"value": related_case_id,
|
|
793
788
|
}
|
|
794
789
|
)
|
|
@@ -883,8 +878,8 @@ def create_cases(ds: HowlerDatastore, num_cases: int = 5):
|
|
|
883
878
|
ds.case.save(case_id, case_data)
|
|
884
879
|
generated_case_ids.append(case_id)
|
|
885
880
|
|
|
886
|
-
case_hit_ids = list({item["
|
|
887
|
-
case_observable_ids = list({item["
|
|
881
|
+
case_hit_ids = list({item["value"] for item in items if item.get("type") == "hit"})
|
|
882
|
+
case_observable_ids = list({item["value"] for item in items if item.get("type") == "observable"})
|
|
888
883
|
|
|
889
884
|
for hit_id in case_hit_ids:
|
|
890
885
|
ds.hit.update(hit_id, [hit_helper.list_add("howler.related", case_id)])
|
|
@@ -4,7 +4,7 @@ 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, overload
|
|
7
|
+
from typing import Any, cast, overload
|
|
8
8
|
|
|
9
9
|
from prometheus_client import Counter
|
|
10
10
|
|
|
@@ -13,6 +13,7 @@ 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
|
|
15
15
|
from howler.odm.models.case import Case, CaseItem, CaseItemTypes, CaseLog
|
|
16
|
+
from howler.odm.models.ecs.related import Related
|
|
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
|
|
@@ -22,7 +23,7 @@ logger = get_logger(__file__)
|
|
|
22
23
|
CREATED_CASES = Counter(f"{APP_NAME.replace('-', '_')}_created_cases_total", "The number of created cases")
|
|
23
24
|
|
|
24
25
|
|
|
25
|
-
def create_case(_case: dict, user: str = None) ->
|
|
26
|
+
def create_case(_case: dict, user: str = None) -> Case: # type: ignore
|
|
26
27
|
"""Create a new case in the datastore.
|
|
27
28
|
|
|
28
29
|
Args:
|
|
@@ -50,8 +51,9 @@ def create_case(_case: dict, user: str = None) -> dict[str, Any]: # type: ignor
|
|
|
50
51
|
append_case_item(case.case_id, item=CaseItem(item))
|
|
51
52
|
|
|
52
53
|
if items:
|
|
53
|
-
return datastore().case.
|
|
54
|
-
|
|
54
|
+
return cast(Case, datastore().case.get(case.case_id))
|
|
55
|
+
|
|
56
|
+
return case
|
|
55
57
|
|
|
56
58
|
|
|
57
59
|
def hide_cases(case_ids: set[str], user: str) -> None:
|
|
@@ -66,19 +68,20 @@ def hide_cases(case_ids: set[str], user: str) -> None:
|
|
|
66
68
|
"""
|
|
67
69
|
ds = datastore()
|
|
68
70
|
|
|
69
|
-
items_query = f"items.
|
|
71
|
+
items_query = f"items.value:({' OR '.join(case_ids)})"
|
|
70
72
|
for case in ds.case.stream_search(items_query, as_obj=False):
|
|
71
73
|
related_case_id = case["case_id"]
|
|
72
74
|
if related_case_id in case_ids:
|
|
73
75
|
continue
|
|
74
76
|
|
|
75
|
-
related_case = ds.case.
|
|
77
|
+
related_case = ds.case.get(related_case_id)
|
|
76
78
|
if related_case:
|
|
77
79
|
hidden_ids: list[str] = []
|
|
78
80
|
for item in related_case.items:
|
|
79
|
-
if item.
|
|
81
|
+
if item.value in case_ids:
|
|
80
82
|
item.visible = False
|
|
81
|
-
hidden_ids.append(item.
|
|
83
|
+
hidden_ids.append(item.value)
|
|
84
|
+
|
|
82
85
|
if hidden_ids:
|
|
83
86
|
related_case.log.append(
|
|
84
87
|
CaseLog(
|
|
@@ -92,7 +95,7 @@ def hide_cases(case_ids: set[str], user: str) -> None:
|
|
|
92
95
|
ds.case.save(related_case_id, related_case)
|
|
93
96
|
|
|
94
97
|
for case_id in case_ids:
|
|
95
|
-
case = ds.case.
|
|
98
|
+
case = ds.case.get(case_id)
|
|
96
99
|
if case:
|
|
97
100
|
case.visible = False
|
|
98
101
|
case.log.append(
|
|
@@ -122,15 +125,15 @@ def delete_cases(case_ids: set[str]) -> bool:
|
|
|
122
125
|
"""
|
|
123
126
|
ds = datastore()
|
|
124
127
|
|
|
125
|
-
items_query = f"items.
|
|
128
|
+
items_query = f"items.value:({' OR '.join(case_ids)})"
|
|
126
129
|
for case in ds.case.stream_search(items_query, as_obj=False):
|
|
127
130
|
related_case_id = case["case_id"]
|
|
128
131
|
if related_case_id in case_ids:
|
|
129
132
|
continue
|
|
130
133
|
|
|
131
|
-
related_case = ds.case.
|
|
134
|
+
related_case = ds.case.get(related_case_id)
|
|
132
135
|
if related_case:
|
|
133
|
-
related_case.items = [item for item in related_case.items if item.
|
|
136
|
+
related_case.items = [item for item in related_case.items if item.value not in case_ids]
|
|
134
137
|
ds.case.save(related_case_id, related_case)
|
|
135
138
|
|
|
136
139
|
return ds.case.delete_by_query(f"case_id:({' OR '.join(case_ids)})")
|
|
@@ -156,7 +159,7 @@ def update_case(case_id: str, case_data: dict[str, Any], user: User) -> Case:
|
|
|
156
159
|
"""
|
|
157
160
|
ds = datastore()
|
|
158
161
|
|
|
159
|
-
case = ds.case.
|
|
162
|
+
case = ds.case.get(case_id)
|
|
160
163
|
if case is None:
|
|
161
164
|
raise NotFoundException(f"Case {case_id} does not exist")
|
|
162
165
|
|
|
@@ -303,7 +306,7 @@ def append_hit(case_id: str, item: CaseItem):
|
|
|
303
306
|
"""
|
|
304
307
|
ds = datastore()
|
|
305
308
|
|
|
306
|
-
case
|
|
309
|
+
case = ds.case.get(key=case_id)
|
|
307
310
|
|
|
308
311
|
if case is None:
|
|
309
312
|
raise NotFoundException(f"Case {case_id} does not exist")
|
|
@@ -311,13 +314,11 @@ def append_hit(case_id: str, item: CaseItem):
|
|
|
311
314
|
if any(item.value == case_item["value"] for case_item in case.items):
|
|
312
315
|
raise InvalidDataException(f"Hit {item.value} already exists in case {case_id}")
|
|
313
316
|
|
|
314
|
-
hit
|
|
317
|
+
hit = ds.hit.get(key=item.value)
|
|
315
318
|
|
|
316
319
|
if hit is None:
|
|
317
320
|
raise NotFoundException(f"Hit {item.value} not found, cannot be added to case")
|
|
318
321
|
|
|
319
|
-
item.id = item.value
|
|
320
|
-
|
|
321
322
|
if item.path == "related/":
|
|
322
323
|
item.path = f"alerts/{hit.howler.analytic} ({hit.howler.id})"
|
|
323
324
|
|
|
@@ -326,7 +327,8 @@ def append_hit(case_id: str, item: CaseItem):
|
|
|
326
327
|
if not datastore().case.save(case.case_id, case):
|
|
327
328
|
raise DataStoreException(f"Failed to save {case.case_id} with new item {item.value}")
|
|
328
329
|
|
|
329
|
-
|
|
330
|
+
_add_backreference(hit, case.case_id)
|
|
331
|
+
_sync_case_metadata(case_id)
|
|
330
332
|
|
|
331
333
|
|
|
332
334
|
def append_observable(case_id: str, item: CaseItem):
|
|
@@ -348,30 +350,29 @@ def append_observable(case_id: str, item: CaseItem):
|
|
|
348
350
|
"""
|
|
349
351
|
ds = datastore()
|
|
350
352
|
|
|
351
|
-
|
|
353
|
+
_case = ds.case.get(key=case_id)
|
|
352
354
|
|
|
353
|
-
if
|
|
355
|
+
if _case is None:
|
|
354
356
|
raise NotFoundException(f"Case {case_id} does not exist")
|
|
355
357
|
|
|
356
|
-
if any(item.value == case_item["value"] for case_item in
|
|
358
|
+
if any(item.value == case_item["value"] for case_item in _case.items):
|
|
357
359
|
raise InvalidDataException(f"Observable {item.value} already exists in case {case_id}")
|
|
358
360
|
|
|
359
|
-
observable
|
|
361
|
+
observable = ds.observable.get(key=item.value)
|
|
360
362
|
|
|
361
363
|
if observable is None:
|
|
362
364
|
raise NotFoundException(f"Observable {item.value} not found, cannot be added to case")
|
|
363
365
|
|
|
364
|
-
item.id = item.value
|
|
365
|
-
|
|
366
366
|
if item.path == "related/":
|
|
367
367
|
item.path = f"observables/{observable.howler.id}"
|
|
368
368
|
|
|
369
|
-
|
|
369
|
+
_case.items.append(item)
|
|
370
370
|
|
|
371
|
-
if not datastore().case.save(
|
|
372
|
-
raise DataStoreException(f"Failed to save {
|
|
371
|
+
if not datastore().case.save(_case.case_id, _case):
|
|
372
|
+
raise DataStoreException(f"Failed to save {_case.case_id} with new item {item.value}")
|
|
373
373
|
|
|
374
|
-
|
|
374
|
+
_add_backreference(observable, _case.case_id)
|
|
375
|
+
_sync_case_metadata(case_id)
|
|
375
376
|
|
|
376
377
|
|
|
377
378
|
def append_case(case_id: str, item: CaseItem):
|
|
@@ -393,30 +394,28 @@ def append_case(case_id: str, item: CaseItem):
|
|
|
393
394
|
"""
|
|
394
395
|
ds = datastore()
|
|
395
396
|
|
|
396
|
-
|
|
397
|
+
_case = ds.case.get(key=case_id)
|
|
397
398
|
|
|
398
|
-
if
|
|
399
|
+
if _case is None:
|
|
399
400
|
raise NotFoundException(f"Case {case_id} does not exist")
|
|
400
401
|
|
|
401
|
-
if any(item.value == case_item["value"] for case_item in
|
|
402
|
+
if any(item.value == case_item["value"] for case_item in _case.items):
|
|
402
403
|
raise InvalidDataException(f"Observable {item.value} already exists in case {case_id}")
|
|
403
404
|
|
|
404
|
-
referenced_case
|
|
405
|
+
referenced_case = ds.case.get(item.value)
|
|
405
406
|
|
|
406
407
|
if referenced_case is None:
|
|
407
408
|
raise NotFoundException(f"Referenced case {item.value} not found, cannot be added to case")
|
|
408
409
|
|
|
409
|
-
item.id = item.value
|
|
410
|
-
|
|
411
410
|
if item.path == "related/":
|
|
412
411
|
item.path = "cases/"
|
|
413
412
|
|
|
414
413
|
item.path += f"{referenced_case.case_id}"
|
|
415
414
|
|
|
416
|
-
|
|
415
|
+
_case.items.append(item)
|
|
417
416
|
|
|
418
|
-
if not datastore().case.save(
|
|
419
|
-
raise DataStoreException(f"Failed to save {
|
|
417
|
+
if not datastore().case.save(_case.case_id, _case):
|
|
418
|
+
raise DataStoreException(f"Failed to save {_case.case_id} with new item {item.value}")
|
|
420
419
|
|
|
421
420
|
|
|
422
421
|
def append_table(case_id: str, item: CaseItem):
|
|
@@ -464,7 +463,67 @@ def append_reference(case_id: str, item: CaseItem):
|
|
|
464
463
|
raise NotImplementedError
|
|
465
464
|
|
|
466
465
|
|
|
467
|
-
def
|
|
466
|
+
def _collect_indicators_from_related(related: Related | None) -> set[str]:
|
|
467
|
+
"""Extract all indicator values from a Related ECS compound object."""
|
|
468
|
+
if related is None:
|
|
469
|
+
return set()
|
|
470
|
+
|
|
471
|
+
indicators: set[str] = set()
|
|
472
|
+
for key in related.fields().keys():
|
|
473
|
+
value = related[key]
|
|
474
|
+
if value:
|
|
475
|
+
indicators.update(str(v) for v in value if v)
|
|
476
|
+
|
|
477
|
+
return indicators
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def _sync_case_metadata(case_id: str) -> None: # noqa: C901
|
|
481
|
+
"""Re-compute and persist threat/target/indicator lists from all case items.
|
|
482
|
+
|
|
483
|
+
Iterates over hit and observable items in the case and re-derives the
|
|
484
|
+
``targets``, ``threats``, and ``indicators`` lists from the backing
|
|
485
|
+
objects' ECS ``related.*`` fields and, for hits, the outline fields.
|
|
486
|
+
"""
|
|
487
|
+
ds = datastore()
|
|
488
|
+
_case = ds.case.get(case_id)
|
|
489
|
+
if _case is None:
|
|
490
|
+
return
|
|
491
|
+
|
|
492
|
+
targets: set[str] = set()
|
|
493
|
+
threats: set[str] = set()
|
|
494
|
+
indicators: set[str] = set()
|
|
495
|
+
|
|
496
|
+
for item in _case.items:
|
|
497
|
+
if item.type == CaseItemTypes.HIT and item.value:
|
|
498
|
+
hit = ds.hit.get(item.value)
|
|
499
|
+
if hit is None:
|
|
500
|
+
continue
|
|
501
|
+
|
|
502
|
+
indicators.update(_collect_indicators_from_related(hit.related))
|
|
503
|
+
|
|
504
|
+
if hit.howler.outline:
|
|
505
|
+
outline = hit.howler.outline
|
|
506
|
+
if outline.threat:
|
|
507
|
+
threats.add(outline.threat)
|
|
508
|
+
if outline.target:
|
|
509
|
+
targets.add(outline.target)
|
|
510
|
+
if outline.indicators:
|
|
511
|
+
indicators.update(str(v) for v in outline.indicators if v)
|
|
512
|
+
|
|
513
|
+
elif item.type == CaseItemTypes.OBSERVABLE and item.value:
|
|
514
|
+
observable = ds.observable.get(item.value)
|
|
515
|
+
if observable is None:
|
|
516
|
+
continue
|
|
517
|
+
|
|
518
|
+
indicators.update(_collect_indicators_from_related(observable.related))
|
|
519
|
+
|
|
520
|
+
_case.targets = sorted(targets)
|
|
521
|
+
_case.threats = sorted(threats)
|
|
522
|
+
_case.indicators = sorted(indicators)
|
|
523
|
+
ds.case.save(case_id, _case)
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def _add_backreference(backing_obj: Hit | Observable | None, case_id: str):
|
|
468
527
|
"""Add a back-reference from a hit or observable to a case.
|
|
469
528
|
|
|
470
529
|
Records the case ID in the backing object's ``howler.related_ids`` set so
|
|
@@ -491,7 +550,7 @@ def add_backreference(backing_obj: Hit | Observable, case_id: str):
|
|
|
491
550
|
datastore()[backing_obj.__class__.__name__.lower()].save(backing_obj.howler.id, backing_obj)
|
|
492
551
|
|
|
493
552
|
|
|
494
|
-
def remove_backreference(backing_obj: Hit | Observable, case_id: str):
|
|
553
|
+
def remove_backreference(backing_obj: Hit | Observable | None, case_id: str):
|
|
495
554
|
"""Remove a back-reference from a hit or observable to a case.
|
|
496
555
|
|
|
497
556
|
Removes the case ID from the backing object's ``howler.related`` list
|
|
@@ -533,7 +592,7 @@ def remove_case_item(case_id: str, item_value: str):
|
|
|
533
592
|
"""
|
|
534
593
|
ds = datastore()
|
|
535
594
|
|
|
536
|
-
_case = ds.case.get(key=case_id
|
|
595
|
+
_case = ds.case.get(key=case_id)
|
|
537
596
|
|
|
538
597
|
if not _case:
|
|
539
598
|
raise NotFoundException(f"Case {case_id} does not exist")
|
|
@@ -543,16 +602,15 @@ def remove_case_item(case_id: str, item_value: str):
|
|
|
543
602
|
raise NotFoundException(f"Case item {item_value} does not exist")
|
|
544
603
|
|
|
545
604
|
backing_obj: Hit | Observable | None = None
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
backing_obj = datastore().hit.get(case_item.id)
|
|
549
|
-
case CaseItemTypes.OBSERVABLE:
|
|
550
|
-
backing_obj = datastore().observable.get(case_item.id)
|
|
605
|
+
if case_item.type in [CaseItemTypes.HIT, CaseItemTypes.OBSERVABLE]:
|
|
606
|
+
backing_obj = ds[case_item.type].get(case_item.value)
|
|
551
607
|
|
|
552
608
|
_case.items.remove(case_item)
|
|
553
609
|
|
|
554
|
-
if not
|
|
610
|
+
if not ds.case.save(_case.case_id, _case):
|
|
555
611
|
raise DataStoreException("Failed to save case after item removal")
|
|
556
612
|
|
|
557
613
|
if backing_obj:
|
|
558
614
|
remove_backreference(backing_obj, _case.case_id)
|
|
615
|
+
|
|
616
|
+
_sync_case_metadata(case_id)
|
|
@@ -235,7 +235,9 @@ def update_dossier(dossier_id: str, dossier_data: dict[str, Any], user: User) ->
|
|
|
235
235
|
raise InvalidDataException("We were unable to update the dossier.", cause=e) from e
|
|
236
236
|
|
|
237
237
|
|
|
238
|
-
def get_matching_dossiers(
|
|
238
|
+
def get_matching_dossiers(
|
|
239
|
+
hit: dict[str, Any], dossiers: list[dict[str, Any]] | None = None, username: str | None = None
|
|
240
|
+
):
|
|
239
241
|
"""Get a list of dossiers that match a specific security alert/hit.
|
|
240
242
|
|
|
241
243
|
This function evaluates each dossier's query against the provided hit data
|
|
@@ -256,7 +258,7 @@ def get_matching_dossiers(hit: dict[str, Any], dossiers: list[dict[str, Any]] |
|
|
|
256
258
|
# Retrieve all dossiers if none provided
|
|
257
259
|
if dossiers is None:
|
|
258
260
|
dossiers = datastore().dossier.search(
|
|
259
|
-
"
|
|
261
|
+
f"type:global OR owner:{username}" if username else "type:global",
|
|
260
262
|
as_obj=False,
|
|
261
263
|
# TODO: Eventually implement caching here
|
|
262
264
|
rows=1000,
|
|
@@ -800,7 +800,7 @@ def __match_metadata(candidates: list[dict[str, Any]], hit: dict[str, Any]) -> O
|
|
|
800
800
|
return sorted(matching_candidates, key=functools.cmp_to_key(__compare_metadata))[0]
|
|
801
801
|
|
|
802
802
|
|
|
803
|
-
def augment_metadata(data: list[dict[str, Any]] | dict[str, Any] | None, metadata: list[str], user:
|
|
803
|
+
def augment_metadata(data: list[dict[str, Any]] | dict[str, Any] | None, metadata: list[str], user: User): # noqa: C901
|
|
804
804
|
"""Augment hit search results with additional metadata.
|
|
805
805
|
|
|
806
806
|
This function enriches hit data by adding related information such as templates,
|
|
@@ -831,7 +831,7 @@ def augment_metadata(data: list[dict[str, Any]] | dict[str, Any] | None, metadat
|
|
|
831
831
|
logger.debug("Augmenting %s hits with %s", len(hits), ",".join(metadata))
|
|
832
832
|
|
|
833
833
|
if "template" in metadata:
|
|
834
|
-
template_candidates = template_service.get_matching_templates(hits, as_odm=False, uname=user
|
|
834
|
+
template_candidates = template_service.get_matching_templates(hits, as_odm=False, uname=user.uname)
|
|
835
835
|
|
|
836
836
|
logger.debug("\tRetrieved %s matching templates", len(template_candidates))
|
|
837
837
|
|
|
@@ -864,11 +864,11 @@ def augment_metadata(data: list[dict[str, Any]] | dict[str, Any] | None, metadat
|
|
|
864
864
|
|
|
865
865
|
if "dossiers" in metadata:
|
|
866
866
|
dossiers: list[dict[str, Any]] = datastore().dossier.search(
|
|
867
|
-
"
|
|
867
|
+
f"type:global OR owner:{user.uname}",
|
|
868
868
|
as_obj=False,
|
|
869
869
|
# TODO: Eventually implement caching here
|
|
870
870
|
rows=1000,
|
|
871
871
|
)["items"]
|
|
872
872
|
|
|
873
873
|
for hit in hits:
|
|
874
|
-
hit["__dossiers"] = dossier_service.get_matching_dossiers(hit, dossiers)
|
|
874
|
+
hit["__dossiers"] = dossier_service.get_matching_dossiers(hit, dossiers, username=user.uname)
|
|
@@ -60,12 +60,14 @@ class LuceneProcessor(TreeVisitor):
|
|
|
60
60
|
for child in node.children:
|
|
61
61
|
child_context = self.child_context(node, child, context)
|
|
62
62
|
for result in self.visit_iter(child, context=child_context):
|
|
63
|
-
# If we run across a MUST or MUST NOT (plus,
|
|
64
|
-
# shortcircuit and return false.
|
|
63
|
+
# If we run across a MUST or MUST NOT (plus, prohibit) object and the value doesn't match, we
|
|
64
|
+
# immediately shortcircuit and return false.
|
|
65
|
+
# NOTE: visit_prohibit already negates the inner result, so a violated MUST NOT arrives here as
|
|
66
|
+
# False (not True). We therefore short-circuit on `not result` rather than `result`.
|
|
65
67
|
if isinstance(child, Plus) and not result:
|
|
66
68
|
yield False
|
|
67
69
|
return
|
|
68
|
-
elif isinstance(child, Prohibit) and result:
|
|
70
|
+
elif isinstance(child, Prohibit) and not result:
|
|
69
71
|
yield False
|
|
70
72
|
return
|
|
71
73
|
|
|
@@ -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.dev642"
|
|
156
156
|
description = "Howler - API server"
|
|
157
157
|
authors = [
|
|
158
158
|
"Canadian Centre for Cyber Security <howler@cyber.gc.ca>",
|
|
@@ -208,7 +208,7 @@ gunicorn = "23.0.0"
|
|
|
208
208
|
packaging = "<25.0"
|
|
209
209
|
passlib = "1.7.4"
|
|
210
210
|
prometheus-client = "0.24.1"
|
|
211
|
-
pyjwt = "2.
|
|
211
|
+
pyjwt = "2.12.1"
|
|
212
212
|
python-baseconv = "1.2.2"
|
|
213
213
|
python-datemath = "3.0.3"
|
|
214
214
|
pyyaml = "6.0.3"
|
|
@@ -233,7 +233,7 @@ bcrypt = "4.3.0"
|
|
|
233
233
|
[tool.poetry.group.dev.dependencies]
|
|
234
234
|
pre-commit = "^3.7.0"
|
|
235
235
|
ruff = ">=0.8,<0.16"
|
|
236
|
-
pyright = {extras = ["nodejs"], version = "^1.1.408"}
|
|
236
|
+
pyright = { extras = ["nodejs"], version = "^1.1.408" }
|
|
237
237
|
mypy = "^1.6.1"
|
|
238
238
|
|
|
239
239
|
[tool.poetry.group.test.dependencies]
|
|
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
|