howler-api 4.0.0.dev841__tar.gz → 4.0.0.dev846__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.dev841 → howler_api-4.0.0.dev846}/PKG-INFO +5 -5
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/actions/__init__.py +91 -21
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/actions/add_label.py +3 -1
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/actions/add_to_bundle.py +15 -3
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/actions/change_field.py +1 -1
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/actions/demote.py +3 -1
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/actions/example_plugin.py +1 -1
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/actions/prioritization.py +3 -1
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/actions/promote.py +3 -1
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/actions/remove_from_bundle.py +15 -3
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/actions/remove_label.py +3 -1
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/actions/transition.py +16 -3
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/api/v1/action.py +23 -7
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/api/v1/user.py +2 -4
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/common/classification.py +162 -82
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/common/loader.py +1 -1
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/config.py +1 -1
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/helper/azure.py +7 -2
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/helper/oauth.py +40 -16
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/helper/search.py +1 -1
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/models/config.py +1 -1
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/models/record.py +7 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/models/user.py +2 -1
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/random_data.py +28 -4
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/randomizer.py +6 -4
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/services/jwt_service.py +2 -2
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/services/user_service.py +38 -14
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/pyproject.toml +9 -9
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/README.md +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/__init__.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/actions/add_to_case.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/api/__init__.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/api/base.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/api/socket.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/api/v1/__init__.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/api/v1/analytic.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/api/v1/auth.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/api/v1/clue.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/api/v1/configs.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/api/v1/dossier.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/api/v1/help.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/api/v1/hit.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/api/v1/notebook.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/api/v1/overview.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/api/v1/search.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/api/v1/template.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/api/v1/tool.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/api/v1/utils/__init__.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/api/v1/utils/etag.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/api/v1/view.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/api/v2/__init__.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/api/v2/case.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/api/v2/ingest.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/api/v2/search.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/app.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/common/README.md +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/common/__init__.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/common/classification.yml +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/common/exceptions.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/common/logging/__init__.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/common/logging/audit.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/common/logging/format.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/common/net.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/common/net_static.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/common/random_user.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/common/swagger.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/cronjobs/__init__.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/cronjobs/correlation.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/cronjobs/retention.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/cronjobs/view_cleanup.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/datastore/README.md +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/datastore/__init__.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/datastore/bulk.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/datastore/collection.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/datastore/constants.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/datastore/exceptions.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/datastore/howler_store.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/datastore/migrations/fix_process.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/datastore/operations.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/datastore/schemas.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/datastore/store.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/datastore/support/__init__.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/datastore/support/build.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/datastore/support/schemas.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/datastore/types.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/error.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/external/README.md +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/external/__init__.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/external/generate_mitre.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/external/generate_sigma_rules.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/external/generate_tlds.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/external/reindex_data.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/external/wipe_databases.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/gunicorn_config.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/healthz.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/helper/__init__.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/helper/discover.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/helper/hit.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/helper/workflow.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/helper/ws.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/README.md +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/__init__.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/base.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/charter.txt +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/constants.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/helper.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/howler_enum.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/mixins.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/models/__init__.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/models/action.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/models/analytic.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/models/assemblyline.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/models/aws.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/models/azure.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/models/case.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/models/cbs.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/models/clue.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/models/dossier.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/models/ecs/__init__.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/models/ecs/agent.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/models/ecs/autonomous_system.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/models/ecs/client.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/models/ecs/cloud.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/models/ecs/code_signature.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/models/ecs/container.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/models/ecs/dns.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/models/ecs/egress.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/models/ecs/elf.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/models/ecs/email.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/models/ecs/error.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/models/ecs/event.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/models/ecs/faas.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/models/ecs/file.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/models/ecs/geo.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/models/ecs/group.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/models/ecs/hash.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/models/ecs/host.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/models/ecs/http.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/models/ecs/ingress.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/models/ecs/interface.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/models/ecs/network.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/models/ecs/observer.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/models/ecs/organization.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/models/ecs/os.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/models/ecs/pe.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/models/ecs/process.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/models/ecs/registry.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/models/ecs/related.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/models/ecs/rule.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/models/ecs/server.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/models/ecs/threat.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/models/ecs/tls.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/models/ecs/url.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/models/ecs/user.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/models/ecs/user_agent.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/models/ecs/vulnerability.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/models/gcp.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/models/hit.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/models/howler_data.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/models/lead.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/models/localized_label.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/models/observable.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/models/overview.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/models/pivot.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/models/template.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/odm/models/view.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/patched.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/plugins/__init__.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/plugins/config.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/remote/__init__.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/remote/datatypes/README.md +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/remote/datatypes/__init__.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/remote/datatypes/counters.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/remote/datatypes/events.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/remote/datatypes/hash.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/remote/datatypes/lock.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/remote/datatypes/queues/__init__.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/remote/datatypes/queues/comms.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/remote/datatypes/queues/multi.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/remote/datatypes/queues/named.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/remote/datatypes/queues/priority.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/remote/datatypes/set.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/remote/datatypes/user_quota_tracker.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/security/__init__.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/security/socket.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/security/utils.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/services/__init__.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/services/action_service.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/services/analytic_service.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/services/auth_service.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/services/bundle_compat_service.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/services/case_service.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/services/config_service.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/services/correlation_service.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/services/docs_service.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/services/dossier_service.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/services/event_service.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/services/hit_service.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/services/lucene_service.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/services/notebook_service.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/services/observable_service.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/services/overview_service.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/services/search_service.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/services/template_service.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/services/viewer_service.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/telemetry.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/utils/__init__.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/utils/annotations.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/utils/chunk.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/utils/compat.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/utils/constants.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/utils/dict_utils.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/utils/isotime.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/utils/list_utils.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/utils/lucene.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/utils/path.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/utils/socket_utils.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/howler/utils/str_utils.py +0 -0
- {howler_api-4.0.0.dev841 → howler_api-4.0.0.dev846}/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.dev846
|
|
4
4
|
Summary: Howler - API server
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: howler,alerting,gc,canada,cse-cst,cse,cst,cyber,cccs
|
|
@@ -28,7 +28,7 @@ Requires-Dist: chevron (==0.14.0)
|
|
|
28
28
|
Requires-Dist: elasticsearch (==8.19.3)
|
|
29
29
|
Requires-Dist: flasgger (>=0.9.7.1,<0.10.0.0)
|
|
30
30
|
Requires-Dist: flask (==3.1.3)
|
|
31
|
-
Requires-Dist: flask-caching (==2.
|
|
31
|
+
Requires-Dist: flask-caching (==2.4.0)
|
|
32
32
|
Requires-Dist: gevent (>=25.9.1,<26.0.0)
|
|
33
33
|
Requires-Dist: gunicorn (==23.0.0)
|
|
34
34
|
Requires-Dist: luqum (>=1.0.0,<2.0.0)
|
|
@@ -38,7 +38,7 @@ Requires-Dist: opentelemetry-instrumentation-flask (==0.61b0)
|
|
|
38
38
|
Requires-Dist: opentelemetry-sdk (==1.40.0)
|
|
39
39
|
Requires-Dist: packaging (<25.0)
|
|
40
40
|
Requires-Dist: passlib (==1.7.4)
|
|
41
|
-
Requires-Dist: prometheus-client (==0.
|
|
41
|
+
Requires-Dist: prometheus-client (==0.25.0)
|
|
42
42
|
Requires-Dist: pydantic (>=2.11.4,<3.0.0)
|
|
43
43
|
Requires-Dist: pydantic-settings[yaml] (>=2.9.1,<3.0.0)
|
|
44
44
|
Requires-Dist: pydash (>=8.0.5,<9.0.0)
|
|
@@ -51,10 +51,10 @@ Requires-Dist: python-dotenv (>=1.1.0,<2.0.0)
|
|
|
51
51
|
Requires-Dist: pytz (>=2025.2,<2026.0)
|
|
52
52
|
Requires-Dist: pyyaml (==6.0.3)
|
|
53
53
|
Requires-Dist: redis (==4.6.0)
|
|
54
|
-
Requires-Dist: requests (==2.
|
|
54
|
+
Requires-Dist: requests (==2.33.1)
|
|
55
55
|
Requires-Dist: tzdata (>=2026.1,<2027.0)
|
|
56
56
|
Requires-Dist: validators (>=0.34,<0.36)
|
|
57
|
-
Requires-Dist: wsproto (==1.2
|
|
57
|
+
Requires-Dist: wsproto (==1.3.2)
|
|
58
58
|
Project-URL: Documentation, https://cybercentrecanada.github.io/howler/developer/backend/
|
|
59
59
|
Project-URL: Homepage, https://cybercentrecanada.github.io/howler/
|
|
60
60
|
Project-URL: Repository, https://github.com/CybercentreCanada/howler-api
|
|
@@ -2,8 +2,10 @@ import importlib
|
|
|
2
2
|
import os
|
|
3
3
|
import re
|
|
4
4
|
from pathlib import Path
|
|
5
|
+
from types import ModuleType
|
|
5
6
|
from typing import Any, Optional
|
|
6
7
|
|
|
8
|
+
from howler.common.loader import datastore
|
|
7
9
|
from howler.common.logging import get_logger
|
|
8
10
|
from howler.odm.models.user import User
|
|
9
11
|
from howler.plugins import get_plugins
|
|
@@ -12,6 +14,9 @@ logger = get_logger(__file__)
|
|
|
12
14
|
|
|
13
15
|
PLUGIN_PATH = Path(os.environ.get("HWL_PLUGIN_DIRECTORY", "/etc/howler/plugins"))
|
|
14
16
|
|
|
17
|
+
# Roles that grant advanced hit limits
|
|
18
|
+
ADVANCED_ROLES = {"automation_advanced", "actionrunner_advanced", "admin"}
|
|
19
|
+
|
|
15
20
|
|
|
16
21
|
def __sanitize_specification(spec: dict[str, Any]) -> dict[str, Any]:
|
|
17
22
|
"""Adapt the specification for use in the UI
|
|
@@ -71,6 +76,68 @@ def __sanitize_report(report: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
|
71
76
|
return sanitized
|
|
72
77
|
|
|
73
78
|
|
|
79
|
+
def _get_operation(operation_id: str) -> ModuleType | None:
|
|
80
|
+
"""Find and return an operation module by ID."""
|
|
81
|
+
operation = None
|
|
82
|
+
try:
|
|
83
|
+
operation = importlib.import_module(f"howler.actions.{operation_id}")
|
|
84
|
+
except ImportError:
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
if not operation:
|
|
88
|
+
for plugin in get_plugins():
|
|
89
|
+
if not plugin.modules.operations:
|
|
90
|
+
continue
|
|
91
|
+
|
|
92
|
+
operation = next(
|
|
93
|
+
(operation for operation in plugin.modules.operations if operation.OPERATION_ID == operation_id), None
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
if operation:
|
|
97
|
+
break
|
|
98
|
+
|
|
99
|
+
return operation
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def check_hit_limit(
|
|
103
|
+
query: str, user: User, max_hits_basic: int | None, max_hits_advanced: int | None
|
|
104
|
+
) -> dict[str, Any] | None:
|
|
105
|
+
"""Check if the user exceeds hit count limits. Returns error dict if exceeded, None otherwise.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
query: The query to check hit count for (should be the effective query the operation will execute).
|
|
109
|
+
user: The user executing the action.
|
|
110
|
+
max_hits_basic: Maximum hits allowed for basic users (None for no limit).
|
|
111
|
+
max_hits_advanced: Maximum hits allowed for advanced users (None for no limit).
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
Error dict if limit exceeded, None otherwise.
|
|
115
|
+
"""
|
|
116
|
+
is_advanced = bool(ADVANCED_ROLES & set(user["type"]))
|
|
117
|
+
limit = max_hits_advanced if is_advanced else max_hits_basic
|
|
118
|
+
|
|
119
|
+
if limit is not None:
|
|
120
|
+
hit_count = datastore().hit.search(query, rows=0)["total"]
|
|
121
|
+
if hit_count > limit:
|
|
122
|
+
return {
|
|
123
|
+
"query": query,
|
|
124
|
+
"outcome": "error",
|
|
125
|
+
"title": "Hit limit exceeded",
|
|
126
|
+
"message": (
|
|
127
|
+
f"This action affects {hit_count} hits, but you can only process {limit} at a time. "
|
|
128
|
+
"Contact an administrator for bulk operations."
|
|
129
|
+
),
|
|
130
|
+
}
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _check_hit_limit(operation: ModuleType, query: str, user: User) -> dict[str, Any] | None:
|
|
135
|
+
"""Central hit limit check using raw query. Skipped if operation sets SKIP_CENTRAL_LIMIT."""
|
|
136
|
+
max_hits_basic = getattr(operation, "MAX_HITS_BASIC", None)
|
|
137
|
+
max_hits_advanced = getattr(operation, "MAX_HITS_ADVANCED", None)
|
|
138
|
+
return check_hit_limit(query, user, max_hits_basic, max_hits_advanced)
|
|
139
|
+
|
|
140
|
+
|
|
74
141
|
def execute(
|
|
75
142
|
operation_id: str,
|
|
76
143
|
query: str,
|
|
@@ -89,23 +156,7 @@ def execute(
|
|
|
89
156
|
Returns:
|
|
90
157
|
list[dict[str, Any]]: A report on the execution
|
|
91
158
|
"""
|
|
92
|
-
operation =
|
|
93
|
-
try:
|
|
94
|
-
operation = importlib.import_module(f"howler.actions.{operation_id}")
|
|
95
|
-
except ImportError:
|
|
96
|
-
pass
|
|
97
|
-
|
|
98
|
-
if not operation:
|
|
99
|
-
for plugin in get_plugins():
|
|
100
|
-
if not plugin.modules.operations:
|
|
101
|
-
continue
|
|
102
|
-
|
|
103
|
-
operation = next(
|
|
104
|
-
(operation for operation in plugin.modules.operations if operation.OPERATION_ID == operation_id), None
|
|
105
|
-
)
|
|
106
|
-
|
|
107
|
-
if operation:
|
|
108
|
-
break
|
|
159
|
+
operation = _get_operation(operation_id)
|
|
109
160
|
|
|
110
161
|
if not operation:
|
|
111
162
|
return [
|
|
@@ -117,9 +168,21 @@ def execute(
|
|
|
117
168
|
}
|
|
118
169
|
]
|
|
119
170
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
171
|
+
if not user:
|
|
172
|
+
return [
|
|
173
|
+
{
|
|
174
|
+
"query": query,
|
|
175
|
+
"outcome": "error",
|
|
176
|
+
"title": "Authentication required",
|
|
177
|
+
"message": "You must be logged in to execute actions.",
|
|
178
|
+
}
|
|
179
|
+
]
|
|
180
|
+
|
|
181
|
+
user_roles = set(user["type"])
|
|
182
|
+
is_admin = "admin" in user_roles
|
|
183
|
+
required_roles = set(operation.specification()["roles"])
|
|
184
|
+
has_roles = required_roles & user_roles
|
|
185
|
+
if not is_admin and not has_roles:
|
|
123
186
|
return [
|
|
124
187
|
{
|
|
125
188
|
"query": query,
|
|
@@ -127,11 +190,18 @@ def execute(
|
|
|
127
190
|
"title": "Insufficient permissions",
|
|
128
191
|
"message": (
|
|
129
192
|
f"The operation ID provided ({operation_id}) requires permissions you do not have "
|
|
130
|
-
f"({', '.join(
|
|
193
|
+
f"(missing one of: {', '.join(sorted(required_roles))}). "
|
|
194
|
+
"Contact HOWLER Support for more information."
|
|
131
195
|
),
|
|
132
196
|
}
|
|
133
197
|
]
|
|
134
198
|
|
|
199
|
+
# Skip central limit check if operation handles it locally with transformed query
|
|
200
|
+
if not getattr(operation, "SKIP_CENTRAL_LIMIT", False):
|
|
201
|
+
limit_error = _check_hit_limit(operation, query, user)
|
|
202
|
+
if limit_error:
|
|
203
|
+
return [limit_error]
|
|
204
|
+
|
|
135
205
|
report = operation.execute(query=query, request_id=request_id, user=user, **kwargs)
|
|
136
206
|
|
|
137
207
|
return __sanitize_report(report)
|
|
@@ -10,6 +10,8 @@ from howler.utils.str_utils import sanitize_lucene_query
|
|
|
10
10
|
hit_helper = OdmHelper(Hit)
|
|
11
11
|
|
|
12
12
|
OPERATION_ID = "add_label"
|
|
13
|
+
MAX_HITS_BASIC = 20
|
|
14
|
+
MAX_HITS_ADVANCED = 1000
|
|
13
15
|
|
|
14
16
|
CATEGORIES = list(Label.fields().keys())
|
|
15
17
|
|
|
@@ -99,7 +101,7 @@ def specification():
|
|
|
99
101
|
"short": "Add a label to a hit",
|
|
100
102
|
"long": execute.__doc__,
|
|
101
103
|
},
|
|
102
|
-
"roles": ["automation_basic"],
|
|
104
|
+
"roles": ["automation_basic", "actionrunner_basic"],
|
|
103
105
|
"steps": [
|
|
104
106
|
{
|
|
105
107
|
"args": {"category": [], "label": []},
|
|
@@ -2,16 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
from typing import Optional
|
|
4
4
|
|
|
5
|
+
from howler.actions import check_hit_limit
|
|
5
6
|
from howler.common.exceptions import NotFoundException
|
|
6
7
|
from howler.common.loader import datastore
|
|
7
8
|
from howler.odm.models.action import VALID_TRIGGERS
|
|
9
|
+
from howler.odm.models.user import User
|
|
8
10
|
from howler.services import bundle_compat_service, case_service
|
|
9
11
|
from howler.utils.str_utils import sanitize_lucene_query
|
|
10
12
|
|
|
11
13
|
OPERATION_ID = "add_to_bundle"
|
|
14
|
+
MAX_HITS_BASIC = 10
|
|
15
|
+
MAX_HITS_ADVANCED = 1000
|
|
16
|
+
SKIP_CENTRAL_LIMIT = True # This operation transforms the query, handles limit check locally
|
|
12
17
|
|
|
13
18
|
|
|
14
|
-
def execute(query: str, bundle_id: Optional[str] = None, **kwargs):
|
|
19
|
+
def execute(query: str, bundle_id: Optional[str] = None, user: Optional[User] = None, **kwargs): # noqa: C901
|
|
15
20
|
"""Add a set of hits matching the query to the specified bundle (deprecated — uses cases).
|
|
16
21
|
|
|
17
22
|
Args:
|
|
@@ -44,7 +49,14 @@ def execute(query: str, bundle_id: Optional[str] = None, **kwargs):
|
|
|
44
49
|
return report
|
|
45
50
|
|
|
46
51
|
ds = datastore()
|
|
47
|
-
|
|
52
|
+
|
|
53
|
+
# Check hit limit against the query before searching
|
|
54
|
+
if user:
|
|
55
|
+
limit_error = check_hit_limit(query, user, MAX_HITS_BASIC, MAX_HITS_ADVANCED)
|
|
56
|
+
if limit_error:
|
|
57
|
+
return [limit_error]
|
|
58
|
+
|
|
59
|
+
matching_hits = ds.hit.search(query, rows=MAX_HITS_ADVANCED)["items"]
|
|
48
60
|
|
|
49
61
|
if not matching_hits:
|
|
50
62
|
report.append(
|
|
@@ -125,7 +137,7 @@ def specification():
|
|
|
125
137
|
"short": "Add a set of hits to a bundle (deprecated — uses cases)",
|
|
126
138
|
"long": execute.__doc__,
|
|
127
139
|
},
|
|
128
|
-
"roles": ["automation_basic"],
|
|
140
|
+
"roles": ["automation_basic", "actionrunner_basic"],
|
|
129
141
|
"steps": [
|
|
130
142
|
{
|
|
131
143
|
"args": {"bundle_id": []},
|
|
@@ -65,7 +65,7 @@ def specification():
|
|
|
65
65
|
"short": "Change one of the fields of a hit",
|
|
66
66
|
"long": execute.__doc__,
|
|
67
67
|
},
|
|
68
|
-
"roles": ["automation_advanced", "admin"],
|
|
68
|
+
"roles": ["automation_advanced", "actionrunner_advanced", "admin"],
|
|
69
69
|
"steps": [
|
|
70
70
|
{
|
|
71
71
|
"args": {"field": [], "value": []},
|
|
@@ -15,6 +15,8 @@ from howler.odm.models.user import User
|
|
|
15
15
|
from howler.utils.str_utils import sanitize_lucene_query
|
|
16
16
|
|
|
17
17
|
OPERATION_ID = "demote"
|
|
18
|
+
MAX_HITS_BASIC = 10
|
|
19
|
+
MAX_HITS_ADVANCED = 1000
|
|
18
20
|
|
|
19
21
|
ESCALATIONS = [esc for esc in Escalation.list() if esc != Escalation.EVIDENCE]
|
|
20
22
|
|
|
@@ -131,7 +133,7 @@ def specification():
|
|
|
131
133
|
"short": "Demote a hit",
|
|
132
134
|
"long": execute.__doc__,
|
|
133
135
|
},
|
|
134
|
-
"roles": ["automation_basic"],
|
|
136
|
+
"roles": ["automation_basic", "actionrunner_basic"],
|
|
135
137
|
"steps": [
|
|
136
138
|
{
|
|
137
139
|
"args": {"escalation": []},
|
|
@@ -67,7 +67,7 @@ def specification():
|
|
|
67
67
|
},
|
|
68
68
|
# What roles should be necessary to run this action? In general, automation_basic should always be required,
|
|
69
69
|
# while automation_advanced should be set when this action could be dangerous or costly in terms of resources.
|
|
70
|
-
"roles": ["automation_basic"],
|
|
70
|
+
"roles": ["automation_basic", "actionrunner_basic"],
|
|
71
71
|
# What data should the user be required to provide? This is split intop steps, so arguments can depend on each
|
|
72
72
|
# other, giving basic control flow for specifying arguments.
|
|
73
73
|
"steps": [
|
|
@@ -6,6 +6,8 @@ from howler.odm.models.hit import Hit
|
|
|
6
6
|
hit_helper = OdmHelper(Hit)
|
|
7
7
|
|
|
8
8
|
OPERATION_ID = "prioritization"
|
|
9
|
+
MAX_HITS_BASIC = 10
|
|
10
|
+
MAX_HITS_ADVANCED = 1000
|
|
9
11
|
|
|
10
12
|
VALID_FIELDS = ["reliability", "severity", "volume", "confidence", "score"]
|
|
11
13
|
|
|
@@ -82,7 +84,7 @@ def specification():
|
|
|
82
84
|
"short": "Change one of the prioritization fields of a hit",
|
|
83
85
|
"long": execute.__doc__,
|
|
84
86
|
},
|
|
85
|
-
"roles": ["automation_basic"],
|
|
87
|
+
"roles": ["automation_basic", "actionrunner_basic"],
|
|
86
88
|
"steps": [
|
|
87
89
|
{
|
|
88
90
|
"args": {"field": [], "value": []},
|
|
@@ -13,6 +13,8 @@ from howler.odm.models.howler_data import (
|
|
|
13
13
|
from howler.utils.str_utils import sanitize_lucene_query
|
|
14
14
|
|
|
15
15
|
OPERATION_ID = "promote"
|
|
16
|
+
MAX_HITS_BASIC = 10
|
|
17
|
+
MAX_HITS_ADVANCED = 1000
|
|
16
18
|
|
|
17
19
|
ESCALATIONS = [esc for esc in Escalation.list() if esc != Escalation.MISS]
|
|
18
20
|
|
|
@@ -118,7 +120,7 @@ def specification():
|
|
|
118
120
|
"short": "Promote a hit",
|
|
119
121
|
"long": execute.__doc__,
|
|
120
122
|
},
|
|
121
|
-
"roles": ["automation_basic"],
|
|
123
|
+
"roles": ["automation_basic", "actionrunner_basic"],
|
|
122
124
|
"steps": [
|
|
123
125
|
{
|
|
124
126
|
"args": {"escalation": []},
|
|
@@ -2,16 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
from typing import Optional
|
|
4
4
|
|
|
5
|
+
from howler.actions import check_hit_limit
|
|
5
6
|
from howler.common.exceptions import NotFoundException
|
|
6
7
|
from howler.common.loader import datastore
|
|
7
8
|
from howler.odm.models.action import VALID_TRIGGERS
|
|
9
|
+
from howler.odm.models.user import User
|
|
8
10
|
from howler.services import bundle_compat_service, case_service
|
|
9
11
|
from howler.utils.str_utils import sanitize_lucene_query
|
|
10
12
|
|
|
11
13
|
OPERATION_ID = "remove_from_bundle"
|
|
14
|
+
MAX_HITS_BASIC = 10
|
|
15
|
+
MAX_HITS_ADVANCED = 1000
|
|
16
|
+
SKIP_CENTRAL_LIMIT = True # This operation transforms the query, handles limit check locally
|
|
12
17
|
|
|
13
18
|
|
|
14
|
-
def execute(query: str, bundle_id: Optional[str] = None, **kwargs):
|
|
19
|
+
def execute(query: str, bundle_id: Optional[str] = None, user: Optional[User] = None, **kwargs): # noqa: C901
|
|
15
20
|
"""Remove a set of hits matching the query from the specified bundle (deprecated — uses cases).
|
|
16
21
|
|
|
17
22
|
Args:
|
|
@@ -44,7 +49,14 @@ def execute(query: str, bundle_id: Optional[str] = None, **kwargs):
|
|
|
44
49
|
return report
|
|
45
50
|
|
|
46
51
|
ds = datastore()
|
|
47
|
-
|
|
52
|
+
|
|
53
|
+
# Check hit limit against the query before searching
|
|
54
|
+
if user:
|
|
55
|
+
limit_error = check_hit_limit(query, user, MAX_HITS_BASIC, MAX_HITS_ADVANCED)
|
|
56
|
+
if limit_error:
|
|
57
|
+
return [limit_error]
|
|
58
|
+
|
|
59
|
+
matching_hits = ds.hit.search(query, rows=MAX_HITS_ADVANCED)["items"]
|
|
48
60
|
|
|
49
61
|
if not matching_hits:
|
|
50
62
|
report.append(
|
|
@@ -139,7 +151,7 @@ def specification():
|
|
|
139
151
|
"short": "Remove a set of hits from a bundle (deprecated — uses cases)",
|
|
140
152
|
"long": execute.__doc__,
|
|
141
153
|
},
|
|
142
|
-
"roles": ["automation_basic"],
|
|
154
|
+
"roles": ["automation_basic", "actionrunner_basic"],
|
|
143
155
|
"steps": [
|
|
144
156
|
{
|
|
145
157
|
"args": {"bundle_id": []},
|
|
@@ -10,6 +10,8 @@ from howler.utils.str_utils import sanitize_lucene_query
|
|
|
10
10
|
hit_helper = OdmHelper(Hit)
|
|
11
11
|
|
|
12
12
|
OPERATION_ID = "remove_label"
|
|
13
|
+
MAX_HITS_BASIC = 20
|
|
14
|
+
MAX_HITS_ADVANCED = 1000
|
|
13
15
|
|
|
14
16
|
CATEGORIES = list(Label.fields().keys())
|
|
15
17
|
|
|
@@ -99,7 +101,7 @@ def specification():
|
|
|
99
101
|
"short": "Remove a label from a hit",
|
|
100
102
|
"long": execute.__doc__,
|
|
101
103
|
},
|
|
102
|
-
"roles": ["automation_basic"],
|
|
104
|
+
"roles": ["automation_basic", "actionrunner_basic"],
|
|
103
105
|
"steps": [
|
|
104
106
|
{
|
|
105
107
|
"args": {"category": [], "label": []},
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import inspect
|
|
2
2
|
from typing import Optional, cast
|
|
3
3
|
|
|
4
|
+
from howler.actions import check_hit_limit
|
|
4
5
|
from howler.common.exceptions import InvalidDataException, NotFoundException
|
|
5
6
|
from howler.common.loader import datastore
|
|
6
7
|
from howler.common.logging import get_logger
|
|
@@ -17,6 +18,9 @@ from howler.services import event_service, hit_service
|
|
|
17
18
|
from howler.utils.list_utils import flatten_list
|
|
18
19
|
|
|
19
20
|
OPERATION_ID = "transition"
|
|
21
|
+
MAX_HITS_BASIC = 10
|
|
22
|
+
MAX_HITS_ADVANCED = 1000
|
|
23
|
+
SKIP_CENTRAL_LIMIT = True # This operation transforms the query, handles limit check locally
|
|
20
24
|
|
|
21
25
|
log = get_logger(__file__)
|
|
22
26
|
|
|
@@ -70,8 +74,17 @@ def execute(
|
|
|
70
74
|
status (str): The status from which to transition.
|
|
71
75
|
transition (str): The transition to attempt to execute.
|
|
72
76
|
"""
|
|
73
|
-
|
|
74
|
-
|
|
77
|
+
# Build effective query with status filter
|
|
78
|
+
effective_query = f"({query}) AND howler.status:{status}"
|
|
79
|
+
|
|
80
|
+
# Check hit limit against the effective query (not raw query)
|
|
81
|
+
limit_error = check_hit_limit(effective_query, user, MAX_HITS_BASIC, MAX_HITS_ADVANCED)
|
|
82
|
+
if limit_error:
|
|
83
|
+
return [limit_error]
|
|
84
|
+
|
|
85
|
+
is_advanced = "automation_advanced" in user.type or "actionrunner_advanced" in user.type or "admin" in user.type
|
|
86
|
+
rows = MAX_HITS_ADVANCED if is_advanced else MAX_HITS_BASIC
|
|
87
|
+
hits = datastore().hit.search(effective_query, rows=rows, fl="howler.id")
|
|
75
88
|
|
|
76
89
|
ids = [hit.howler.id for hit in hits["items"]]
|
|
77
90
|
|
|
@@ -176,7 +189,7 @@ def specification():
|
|
|
176
189
|
"short": "Transition a hit",
|
|
177
190
|
"long": execute.__doc__,
|
|
178
191
|
},
|
|
179
|
-
"roles": ["automation_basic"],
|
|
192
|
+
"roles": ["automation_basic", "actionrunner_basic"],
|
|
180
193
|
"steps": [
|
|
181
194
|
{
|
|
182
195
|
"args": {"status": []},
|
|
@@ -23,7 +23,11 @@ action_api._doc = "Endpoints relating to bulk actions and automation" # type: i
|
|
|
23
23
|
|
|
24
24
|
@generate_swagger_docs()
|
|
25
25
|
@action_api.route("/")
|
|
26
|
-
@api_login(
|
|
26
|
+
@api_login(
|
|
27
|
+
audit=False,
|
|
28
|
+
check_xsrf_token=False,
|
|
29
|
+
required_type=["admin", "automation_basic", "automation_advanced", "actionrunner_basic", "actionrunner_advanced"],
|
|
30
|
+
)
|
|
27
31
|
def get_actions(**_) -> Response:
|
|
28
32
|
"""Get a list of existing actions
|
|
29
33
|
|
|
@@ -43,7 +47,7 @@ def get_actions(**_) -> Response:
|
|
|
43
47
|
|
|
44
48
|
@generate_swagger_docs()
|
|
45
49
|
@action_api.route("/", methods=["POST"])
|
|
46
|
-
@api_login(audit=False, check_xsrf_token=False, required_type=["automation_basic"])
|
|
50
|
+
@api_login(audit=False, check_xsrf_token=False, required_type=["admin", "automation_basic", "automation_advanced"])
|
|
47
51
|
def add_action(user: User, **_) -> Response:
|
|
48
52
|
"""Create a new action
|
|
49
53
|
|
|
@@ -97,7 +101,7 @@ def add_action(user: User, **_) -> Response:
|
|
|
97
101
|
@api_login(
|
|
98
102
|
audit=False,
|
|
99
103
|
check_xsrf_token=False,
|
|
100
|
-
required_type=["automation_basic"],
|
|
104
|
+
required_type=["admin", "automation_basic", "automation_advanced"],
|
|
101
105
|
)
|
|
102
106
|
def update_action(id: str, user: User, **_) -> Response:
|
|
103
107
|
"""Update an existing action
|
|
@@ -164,7 +168,7 @@ def update_action(id: str, user: User, **_) -> Response:
|
|
|
164
168
|
|
|
165
169
|
@generate_swagger_docs()
|
|
166
170
|
@action_api.route("/<id>", methods=["DELETE"])
|
|
167
|
-
@api_login(audit=True, check_xsrf_token=False, required_type=["automation_basic"])
|
|
171
|
+
@api_login(audit=True, check_xsrf_token=False, required_type=["admin", "automation_basic", "automation_advanced"])
|
|
168
172
|
def delete_action(id: str, user: User, **kwargs) -> Response:
|
|
169
173
|
"""Delete an existing action
|
|
170
174
|
|
|
@@ -200,7 +204,11 @@ def delete_action(id: str, user: User, **kwargs) -> Response:
|
|
|
200
204
|
|
|
201
205
|
@generate_swagger_docs()
|
|
202
206
|
@action_api.route("/<id>/execute", methods=["POST"])
|
|
203
|
-
@api_login(
|
|
207
|
+
@api_login(
|
|
208
|
+
audit=True,
|
|
209
|
+
check_xsrf_token=False,
|
|
210
|
+
required_type=["admin", "automation_basic", "automation_advanced", "actionrunner_basic", "actionrunner_advanced"],
|
|
211
|
+
)
|
|
204
212
|
def execute_action(id: str, **kwargs) -> Response:
|
|
205
213
|
"""Execute one or more actions on a given query
|
|
206
214
|
|
|
@@ -274,7 +282,11 @@ def execute_action(id: str, **kwargs) -> Response:
|
|
|
274
282
|
|
|
275
283
|
@generate_swagger_docs()
|
|
276
284
|
@action_api.route("/operations")
|
|
277
|
-
@api_login(
|
|
285
|
+
@api_login(
|
|
286
|
+
audit=False,
|
|
287
|
+
check_xsrf_token=False,
|
|
288
|
+
required_type=["admin", "automation_basic", "automation_advanced", "actionrunner_basic", "actionrunner_advanced"],
|
|
289
|
+
)
|
|
278
290
|
def get_operations(**_) -> Response:
|
|
279
291
|
"""Get a list of operations the user can run on a query
|
|
280
292
|
|
|
@@ -294,7 +306,11 @@ def get_operations(**_) -> Response:
|
|
|
294
306
|
|
|
295
307
|
@generate_swagger_docs()
|
|
296
308
|
@action_api.route("/execute", methods=["POST"])
|
|
297
|
-
@api_login(
|
|
309
|
+
@api_login(
|
|
310
|
+
audit=True,
|
|
311
|
+
check_xsrf_token=False,
|
|
312
|
+
required_type=["admin", "automation_basic", "automation_advanced", "actionrunner_basic", "actionrunner_advanced"],
|
|
313
|
+
)
|
|
298
314
|
def execute_operations(**kwargs) -> Response:
|
|
299
315
|
"""Execute one or more operations on a given query
|
|
300
316
|
|
|
@@ -128,9 +128,7 @@ def add_user_account(username, **_):
|
|
|
128
128
|
data["name"] = data["uname"]
|
|
129
129
|
|
|
130
130
|
# Add dynamic classification group
|
|
131
|
-
data["classification"] = user_service.get_dynamic_classification(
|
|
132
|
-
cast(str | None, data.get("classification", None)), data["email"]
|
|
133
|
-
)
|
|
131
|
+
data["classification"] = user_service.get_dynamic_classification(data)
|
|
134
132
|
|
|
135
133
|
# Clear non user account data
|
|
136
134
|
avatar = data.pop("avatar", None)
|
|
@@ -290,7 +288,7 @@ def set_user_account(username: str, **kwargs): # noqa: C901
|
|
|
290
288
|
data.pop("new_pass_confirm", None)
|
|
291
289
|
|
|
292
290
|
# Apply dynamic classification
|
|
293
|
-
data["classification"] = user_service.get_dynamic_classification(data
|
|
291
|
+
data["classification"] = user_service.get_dynamic_classification(data)
|
|
294
292
|
|
|
295
293
|
ret_val = user_service.save_user_account(username, data, kwargs["user"])
|
|
296
294
|
return ok({"success": ret_val})
|