howler-api 3.2.0.dev577__tar.gz → 3.2.0.dev594__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-3.2.0.dev577 → howler_api-3.2.0.dev594}/PKG-INFO +2 -2
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/actions/demote.py +2 -2
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/actions/transition.py +3 -5
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/api/__init__.py +2 -1
- howler_api-3.2.0.dev594/howler/api/v1/__init__.py +44 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/api/v1/action.py +1 -2
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/api/v1/analytic.py +17 -102
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/api/v1/hit.py +11 -215
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/api/v1/tool.py +1 -18
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/api/v1/user.py +1 -1
- howler_api-3.2.0.dev594/howler/api/v2/__init__.py +44 -0
- howler_api-3.2.0.dev594/howler/api/v2/case.py +286 -0
- howler_api-3.2.0.dev594/howler/api/v2/ingest.py +327 -0
- howler_api-3.2.0.dev594/howler/api/v2/search.py +341 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/app.py +10 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/common/loader.py +4 -5
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/datastore/collection.py +8 -9
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/datastore/howler_store.py +29 -14
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/datastore/store.py +4 -1
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/datastore/types.py +2 -6
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/helper/discover.py +11 -7
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/helper/hit.py +4 -4
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/helper/search.py +12 -2
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/odm/base.py +40 -12
- howler_api-3.2.0.dev594/howler/odm/constants.py +20 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/odm/helper.py +115 -5
- howler_api-3.2.0.dev594/howler/odm/models/case.py +179 -0
- howler_api-3.2.0.dev594/howler/odm/models/hit.py +29 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/odm/models/howler_data.py +3 -31
- howler_api-3.2.0.dev594/howler/odm/models/observable.py +129 -0
- howler_api-3.2.0.dev577/howler/odm/models/hit.py → howler_api-3.2.0.dev594/howler/odm/models/record.py +1 -21
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/odm/random_data.py +347 -45
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/remote/datatypes/__init__.py +3 -2
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/remote/datatypes/queues/comms.py +2 -1
- howler_api-3.2.0.dev594/howler/services/case_service.py +558 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/services/config_service.py +3 -3
- howler_api-3.2.0.dev577/howler/api/v1/__init__.py → howler_api-3.2.0.dev594/howler/services/docs_service.py +37 -45
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/services/hit_service.py +90 -168
- howler_api-3.2.0.dev594/howler/services/observable_service.py +128 -0
- howler_api-3.2.0.dev594/howler/services/search_service.py +225 -0
- howler_api-3.2.0.dev594/howler/utils/compat.py +21 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/pyproject.toml +4 -4
- howler_api-3.2.0.dev577/howler/actions/add_to_bundle.py +0 -159
- howler_api-3.2.0.dev577/howler/actions/remove_from_bundle.py +0 -133
- howler_api-3.2.0.dev577/howler/cronjobs/rules.py +0 -274
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/README.md +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/__init__.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/actions/__init__.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/actions/add_label.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/actions/change_field.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/actions/example_plugin.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/actions/prioritization.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/actions/promote.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/actions/remove_label.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/api/base.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/api/socket.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/api/v1/auth.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/api/v1/clue.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/api/v1/configs.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/api/v1/dossier.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/api/v1/help.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/api/v1/notebook.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/api/v1/overview.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/api/v1/search.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/api/v1/template.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/api/v1/utils/__init__.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/api/v1/utils/etag.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/api/v1/view.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/common/README.md +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/common/__init__.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/common/classification.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/common/classification.yml +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/common/exceptions.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/common/logging/__init__.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/common/logging/audit.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/common/logging/format.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/common/net.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/common/net_static.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/common/random_user.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/common/swagger.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/config.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/cronjobs/__init__.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/cronjobs/retention.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/cronjobs/view_cleanup.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/datastore/README.md +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/datastore/__init__.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/datastore/bulk.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/datastore/constants.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/datastore/exceptions.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/datastore/migrations/fix_process.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/datastore/operations.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/datastore/schemas.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/datastore/support/__init__.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/datastore/support/build.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/datastore/support/schemas.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/error.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/external/__init__.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/external/generate_mitre.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/external/generate_sigma_rules.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/external/generate_tlds.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/external/reindex_data.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/external/wipe_databases.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/gunicorn_config.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/healthz.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/helper/__init__.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/helper/azure.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/helper/oauth.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/helper/workflow.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/helper/ws.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/odm/README.md +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/odm/__init__.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/odm/charter.txt +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/odm/howler_enum.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/odm/models/__init__.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/odm/models/action.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/odm/models/analytic.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/odm/models/assemblyline.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/odm/models/aws.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/odm/models/azure.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/odm/models/cbs.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/odm/models/clue.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/odm/models/config.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/odm/models/dossier.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/odm/models/ecs/__init__.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/odm/models/ecs/agent.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/odm/models/ecs/autonomous_system.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/odm/models/ecs/client.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/odm/models/ecs/cloud.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/odm/models/ecs/code_signature.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/odm/models/ecs/container.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/odm/models/ecs/dns.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/odm/models/ecs/egress.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/odm/models/ecs/elf.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/odm/models/ecs/email.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/odm/models/ecs/error.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/odm/models/ecs/event.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/odm/models/ecs/faas.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/odm/models/ecs/file.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/odm/models/ecs/geo.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/odm/models/ecs/group.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/odm/models/ecs/hash.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/odm/models/ecs/host.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/odm/models/ecs/http.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/odm/models/ecs/ingress.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/odm/models/ecs/interface.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/odm/models/ecs/network.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/odm/models/ecs/observer.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/odm/models/ecs/organization.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/odm/models/ecs/os.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/odm/models/ecs/pe.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/odm/models/ecs/process.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/odm/models/ecs/registry.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/odm/models/ecs/related.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/odm/models/ecs/rule.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/odm/models/ecs/server.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/odm/models/ecs/threat.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/odm/models/ecs/tls.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/odm/models/ecs/url.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/odm/models/ecs/user.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/odm/models/ecs/user_agent.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/odm/models/ecs/vulnerability.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/odm/models/gcp.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/odm/models/lead.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/odm/models/localized_label.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/odm/models/overview.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/odm/models/pivot.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/odm/models/template.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/odm/models/user.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/odm/models/view.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/odm/randomizer.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/patched.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/plugins/__init__.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/plugins/config.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/remote/__init__.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/remote/datatypes/README.md +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/remote/datatypes/counters.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/remote/datatypes/events.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/remote/datatypes/hash.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/remote/datatypes/lock.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/remote/datatypes/queues/__init__.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/remote/datatypes/queues/multi.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/remote/datatypes/queues/named.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/remote/datatypes/queues/priority.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/remote/datatypes/set.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/remote/datatypes/user_quota_tracker.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/security/__init__.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/security/socket.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/security/utils.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/services/__init__.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/services/action_service.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/services/analytic_service.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/services/auth_service.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/services/dossier_service.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/services/event_service.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/services/jwt_service.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/services/lucene_service.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/services/notebook_service.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/services/overview_service.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/services/template_service.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/services/user_service.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/utils/__init__.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/utils/annotations.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/utils/chunk.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/utils/dict_utils.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/utils/isotime.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/utils/list_utils.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/utils/lucene.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/utils/path.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/utils/socket_utils.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/utils/str_utils.py +0 -0
- {howler_api-3.2.0.dev577 → howler_api-3.2.0.dev594}/howler/utils/uid.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: howler-api
|
|
3
|
-
Version: 3.2.0.
|
|
3
|
+
Version: 3.2.0.dev594
|
|
4
4
|
Summary: Howler - API server
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: howler,alerting,gc,canada,cse-cst,cse,cst,cyber,cccs
|
|
@@ -29,7 +29,7 @@ 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
31
|
Requires-Dist: flask-caching (==2.3.1)
|
|
32
|
-
Requires-Dist: gevent (
|
|
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)
|
|
35
35
|
Requires-Dist: mergedeep (>=1.3.4,<2.0.0)
|
|
@@ -9,7 +9,7 @@ from howler.odm.models.howler_data import (
|
|
|
9
9
|
Assessment,
|
|
10
10
|
AssessmentEscalationMap,
|
|
11
11
|
Escalation,
|
|
12
|
-
|
|
12
|
+
Status,
|
|
13
13
|
)
|
|
14
14
|
from howler.odm.models.user import User
|
|
15
15
|
from howler.utils.str_utils import sanitize_lucene_query
|
|
@@ -96,7 +96,7 @@ def execute(
|
|
|
96
96
|
"howler.assignment",
|
|
97
97
|
user.get("uname", "automation") if user else "automation",
|
|
98
98
|
),
|
|
99
|
-
odm_helper.update("howler.status",
|
|
99
|
+
odm_helper.update("howler.status", Status.RESOLVED),
|
|
100
100
|
],
|
|
101
101
|
)
|
|
102
102
|
|
|
@@ -8,8 +8,8 @@ from howler.helper.workflow import Workflow, WorkflowException
|
|
|
8
8
|
from howler.odm.models.action import VALID_TRIGGERS
|
|
9
9
|
from howler.odm.models.howler_data import (
|
|
10
10
|
Assessment,
|
|
11
|
-
HitStatus,
|
|
12
11
|
HitStatusTransition,
|
|
12
|
+
Status,
|
|
13
13
|
Vote,
|
|
14
14
|
)
|
|
15
15
|
from howler.odm.models.user import User
|
|
@@ -180,15 +180,13 @@ def specification():
|
|
|
180
180
|
"steps": [
|
|
181
181
|
{
|
|
182
182
|
"args": {"status": []},
|
|
183
|
-
"options": {"status":
|
|
183
|
+
"options": {"status": Status.list()},
|
|
184
184
|
"validation": {"error": {"query": "-howler.status:$status"}},
|
|
185
185
|
},
|
|
186
186
|
{
|
|
187
187
|
"args": {"transition": []},
|
|
188
188
|
"options": {
|
|
189
|
-
"transition": {
|
|
190
|
-
f"status:{status}": hit_service.get_transitions(status) for status in HitStatus.list()
|
|
191
|
-
},
|
|
189
|
+
"transition": {f"status:{status}": hit_service.get_transitions(status) for status in Status.list()},
|
|
192
190
|
},
|
|
193
191
|
},
|
|
194
192
|
{
|
|
@@ -24,7 +24,8 @@ logger = get_logger(__file__)
|
|
|
24
24
|
|
|
25
25
|
def make_subapi_blueprint(name, api_version=1):
|
|
26
26
|
"""Create a flask Blueprint for a subapi in a standard way."""
|
|
27
|
-
|
|
27
|
+
full_name = f"v{api_version}_{name}"
|
|
28
|
+
return Blueprint(full_name, full_name, url_prefix="/".join([API_PREFIX, f"v{api_version}", name]))
|
|
28
29
|
|
|
29
30
|
|
|
30
31
|
def _make_api_response(
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from flask import Blueprint
|
|
2
|
+
|
|
3
|
+
from howler.api import ok
|
|
4
|
+
from howler.security import api_login
|
|
5
|
+
from howler.services import docs_service
|
|
6
|
+
|
|
7
|
+
API_PREFIX = "/api/v1"
|
|
8
|
+
apiv1 = Blueprint("apiv1", __name__, url_prefix=API_PREFIX)
|
|
9
|
+
apiv1._doc = "Api Documentation Version 1" # type: ignore[attr-defined] # type: ignore
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@apiv1.route("/")
|
|
13
|
+
@api_login(audit=False, required_priv=["R", "W"], required_type=["user", "admin"])
|
|
14
|
+
def get_api_documentation(**kwargs):
|
|
15
|
+
"""Full API doc.
|
|
16
|
+
|
|
17
|
+
Loop through all registered API paths and display their documentation.
|
|
18
|
+
Returns a list of API definition.
|
|
19
|
+
|
|
20
|
+
Variables:
|
|
21
|
+
None
|
|
22
|
+
|
|
23
|
+
Arguments:
|
|
24
|
+
None
|
|
25
|
+
|
|
26
|
+
Result Example:
|
|
27
|
+
[
|
|
28
|
+
{
|
|
29
|
+
'name': "Api Doc", # Name of the api
|
|
30
|
+
'path': "/api/path/<variable>/", # API path
|
|
31
|
+
'ui_only': false, # Is UI only API
|
|
32
|
+
'methods': ["GET", "POST"], # Allowed HTTP methods
|
|
33
|
+
'description': "API doc.", # API documentation
|
|
34
|
+
'id': "api_doc", # Unique ID for the API
|
|
35
|
+
'function': "apiv1.api_doc", # Function called in the code
|
|
36
|
+
'protected': False, # Does the API require login?
|
|
37
|
+
'required_type': ['user'], # Type of users allowed to use API
|
|
38
|
+
'complete' : True # Is the API stable?
|
|
39
|
+
},
|
|
40
|
+
]
|
|
41
|
+
"""
|
|
42
|
+
user_types = kwargs["user"]["type"]
|
|
43
|
+
|
|
44
|
+
return ok(docs_service.build_route_docs("v1", user_types))
|
|
@@ -231,8 +231,7 @@ def execute_action(id: str, **kwargs) -> Response:
|
|
|
231
231
|
if not isinstance(execute_req, dict):
|
|
232
232
|
return bad_request(err="Incorrect data structure!")
|
|
233
233
|
|
|
234
|
-
action
|
|
235
|
-
|
|
234
|
+
action = datastore().action.get(id)
|
|
236
235
|
if not action:
|
|
237
236
|
return not_found(err="The specified action does not exist")
|
|
238
237
|
|
|
@@ -15,11 +15,9 @@ from howler.common.exceptions import HowlerException
|
|
|
15
15
|
from howler.common.loader import datastore
|
|
16
16
|
from howler.common.logging import get_logger
|
|
17
17
|
from howler.common.swagger import generate_swagger_docs
|
|
18
|
-
from howler.cronjobs.rules import register_rules
|
|
19
18
|
from howler.datastore.exceptions import DataStoreException
|
|
20
19
|
from howler.datastore.operations import OdmHelper
|
|
21
20
|
from howler.odm.models.analytic import Analytic, Comment, Notebook, TriageOptions
|
|
22
|
-
from howler.odm.models.template import Template
|
|
23
21
|
from howler.odm.models.user import User
|
|
24
22
|
from howler.security import api_login
|
|
25
23
|
from howler.services import analytic_service, user_service
|
|
@@ -104,7 +102,8 @@ def update_analytic(id: str, user: User, **kwargs):
|
|
|
104
102
|
"""
|
|
105
103
|
storage = datastore()
|
|
106
104
|
|
|
107
|
-
|
|
105
|
+
existing_analytic = storage.analytic.get(id)
|
|
106
|
+
if not existing_analytic:
|
|
108
107
|
return not_found(err="This analytic does not exist")
|
|
109
108
|
|
|
110
109
|
new_data = request.json
|
|
@@ -113,8 +112,6 @@ def update_analytic(id: str, user: User, **kwargs):
|
|
|
113
112
|
return bad_request(err="You must provide updated data.")
|
|
114
113
|
|
|
115
114
|
try:
|
|
116
|
-
existing_analytic: Analytic = storage.analytic.get_if_exists(id)
|
|
117
|
-
|
|
118
115
|
existing_analytic.description = new_data.get("description", existing_analytic.description)
|
|
119
116
|
|
|
120
117
|
if existing_analytic.triage_settings is not None:
|
|
@@ -126,21 +123,8 @@ def update_analytic(id: str, user: User, **kwargs):
|
|
|
126
123
|
{**existing_triage_data, **new_data.get("triage_settings", {})}
|
|
127
124
|
)
|
|
128
125
|
|
|
129
|
-
updated_rule = False
|
|
130
|
-
if existing_analytic.rule_type:
|
|
131
|
-
updated_rule = existing_analytic.rule != new_data.get(
|
|
132
|
-
"rule", existing_analytic.rule
|
|
133
|
-
) or existing_analytic.rule_crontab != new_data.get("rule_crontab", existing_analytic.rule_crontab)
|
|
134
|
-
|
|
135
|
-
existing_analytic.rule = new_data.get("rule", existing_analytic.rule)
|
|
136
|
-
existing_analytic.rule_crontab = new_data.get("rule_crontab", existing_analytic.rule_crontab)
|
|
137
|
-
|
|
138
126
|
storage.analytic.save(existing_analytic.analytic_id, existing_analytic)
|
|
139
127
|
|
|
140
|
-
if updated_rule:
|
|
141
|
-
# The registration process automatically deletes and resets the rule cronjob
|
|
142
|
-
register_rules(existing_analytic)
|
|
143
|
-
|
|
144
128
|
return ok(existing_analytic)
|
|
145
129
|
except HowlerException as e:
|
|
146
130
|
return bad_request(err=str(e))
|
|
@@ -169,62 +153,7 @@ def create_rule(user: User, **kwargs):
|
|
|
169
153
|
...analytic # The created analytic rule
|
|
170
154
|
}
|
|
171
155
|
"""
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
new_data: Optional[dict[str, Any]] = request.json
|
|
175
|
-
|
|
176
|
-
if not new_data:
|
|
177
|
-
return bad_request(err="You must provide rule data.")
|
|
178
|
-
|
|
179
|
-
required_keys = {
|
|
180
|
-
"name",
|
|
181
|
-
"description",
|
|
182
|
-
"rule",
|
|
183
|
-
"rule_type",
|
|
184
|
-
"rule_crontab",
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
for key in required_keys:
|
|
188
|
-
if key not in new_data or not new_data[key]:
|
|
189
|
-
return bad_request(err=f"You must provide a {key} for your rule.")
|
|
190
|
-
|
|
191
|
-
extra_keys = set(new_data.keys()) - required_keys
|
|
192
|
-
|
|
193
|
-
if len(extra_keys) > 0:
|
|
194
|
-
return bad_request(err=f"Additional fields ({', '.join(extra_keys)}) are not permitted.")
|
|
195
|
-
|
|
196
|
-
new_analytic = Analytic(
|
|
197
|
-
{
|
|
198
|
-
**new_data,
|
|
199
|
-
"tags": ["rule"],
|
|
200
|
-
"owner": user["uname"],
|
|
201
|
-
"contributors": [user["uname"]],
|
|
202
|
-
"detections": ["Rule"],
|
|
203
|
-
}
|
|
204
|
-
)
|
|
205
|
-
|
|
206
|
-
new_template = Template(
|
|
207
|
-
{
|
|
208
|
-
"analytic": new_data["name"],
|
|
209
|
-
"detection": "Rule",
|
|
210
|
-
"type": "global",
|
|
211
|
-
"owner": user["uname"],
|
|
212
|
-
# TODO: Allow custom keys
|
|
213
|
-
"keys": ["event.kind", "event.module", "event.reason", "event.type"],
|
|
214
|
-
}
|
|
215
|
-
)
|
|
216
|
-
|
|
217
|
-
try:
|
|
218
|
-
storage.analytic.save(new_analytic.analytic_id, new_analytic)
|
|
219
|
-
# Have to commit so the analytic is available during registration
|
|
220
|
-
storage.analytic.commit()
|
|
221
|
-
register_rules(new_analytic)
|
|
222
|
-
|
|
223
|
-
storage.template.save(new_template.template_id, new_template)
|
|
224
|
-
|
|
225
|
-
return ok(new_analytic)
|
|
226
|
-
except HowlerException as e:
|
|
227
|
-
return bad_request(err=str(e))
|
|
156
|
+
raise NotImplementedError()
|
|
228
157
|
|
|
229
158
|
|
|
230
159
|
@generate_swagger_docs()
|
|
@@ -248,23 +177,7 @@ def delete_rule(id: str, user: User, **kwargs):
|
|
|
248
177
|
{
|
|
249
178
|
}
|
|
250
179
|
"""
|
|
251
|
-
|
|
252
|
-
return not_found(err=f"Analytic {id} does not exist")
|
|
253
|
-
|
|
254
|
-
analytic = analytic_service.get_analytic(id, as_odm=True)
|
|
255
|
-
|
|
256
|
-
if not analytic.rule:
|
|
257
|
-
return bad_request(err="This is not a rule analytic, and cannot be deleted.")
|
|
258
|
-
|
|
259
|
-
if user["uname"] != analytic.owner and "admin" not in user["type"]:
|
|
260
|
-
return forbidden(err="You cannot delete this analytic.")
|
|
261
|
-
|
|
262
|
-
try:
|
|
263
|
-
datastore().analytic.delete(analytic.analytic_id)
|
|
264
|
-
except DataStoreException as e:
|
|
265
|
-
return bad_request(err=str(e))
|
|
266
|
-
|
|
267
|
-
return no_content()
|
|
180
|
+
raise NotImplementedError()
|
|
268
181
|
|
|
269
182
|
|
|
270
183
|
@generate_swagger_docs()
|
|
@@ -557,7 +470,7 @@ def set_analytic_owner(id: str, user: dict[str, Any], **kwargs):
|
|
|
557
470
|
@generate_swagger_docs()
|
|
558
471
|
@analytic_api.route("/<id>/favourite", methods=["POST"])
|
|
559
472
|
@api_login(required_priv=["R", "W"])
|
|
560
|
-
def set_as_favourite(id, **kwargs):
|
|
473
|
+
def set_as_favourite(id: str, user: User | None, **kwargs):
|
|
561
474
|
"""Add an analytic to a list of the user's favourites
|
|
562
475
|
|
|
563
476
|
Variables:
|
|
@@ -576,16 +489,17 @@ def set_as_favourite(id, **kwargs):
|
|
|
576
489
|
"""
|
|
577
490
|
storage = datastore()
|
|
578
491
|
|
|
579
|
-
existing_analytic
|
|
492
|
+
existing_analytic = storage.analytic.get(id)
|
|
580
493
|
if not existing_analytic:
|
|
581
494
|
return not_found(err="This analytic does not exist")
|
|
582
495
|
|
|
583
|
-
|
|
584
|
-
|
|
496
|
+
if not user:
|
|
497
|
+
return forbidden(err="User was not found.")
|
|
585
498
|
|
|
586
|
-
|
|
499
|
+
try:
|
|
500
|
+
user.favourite_analytics.append(id)
|
|
587
501
|
|
|
588
|
-
storage.user.save(
|
|
502
|
+
storage.user.save(user.uname, user)
|
|
589
503
|
|
|
590
504
|
return ok()
|
|
591
505
|
except ValueError as e:
|
|
@@ -595,7 +509,7 @@ def set_as_favourite(id, **kwargs):
|
|
|
595
509
|
@generate_swagger_docs()
|
|
596
510
|
@analytic_api.route("/<id>/favourite", methods=["DELETE"])
|
|
597
511
|
@api_login(required_priv=["R", "W"])
|
|
598
|
-
def remove_as_favourite(id, **kwargs):
|
|
512
|
+
def remove_as_favourite(id: str, user: User | None, **kwargs):
|
|
599
513
|
"""Remove an analytic from a list of the user's favourites
|
|
600
514
|
|
|
601
515
|
Variables:
|
|
@@ -614,12 +528,13 @@ def remove_as_favourite(id, **kwargs):
|
|
|
614
528
|
if not storage.analytic.exists(id):
|
|
615
529
|
return not_found(err="This analytic does not exist")
|
|
616
530
|
|
|
617
|
-
|
|
618
|
-
|
|
531
|
+
if not user:
|
|
532
|
+
return forbidden(err="User was not found.")
|
|
619
533
|
|
|
620
|
-
|
|
534
|
+
try:
|
|
535
|
+
user.favourite_analytics = list(filter(lambda f: f != id, user.favourite_analytics))
|
|
621
536
|
|
|
622
|
-
storage.user.save(
|
|
537
|
+
storage.user.save(user.uname, user)
|
|
623
538
|
|
|
624
539
|
return no_content()
|
|
625
540
|
except ValueError as e:
|
|
@@ -147,11 +147,9 @@ def delete_hits(user: User, **kwargs):
|
|
|
147
147
|
None
|
|
148
148
|
|
|
149
149
|
Data Block:
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
]
|
|
154
|
-
}
|
|
150
|
+
[
|
|
151
|
+
hitId, hitId, hitId
|
|
152
|
+
]
|
|
155
153
|
|
|
156
154
|
Result Example:
|
|
157
155
|
{
|
|
@@ -166,17 +164,11 @@ def delete_hits(user: User, **kwargs):
|
|
|
166
164
|
if "admin" not in user["type"]:
|
|
167
165
|
return forbidden(err="Cannot delete hit, only admin is allowed to delete")
|
|
168
166
|
|
|
167
|
+
hit_ids = list(set(hit_ids))
|
|
169
168
|
non_existing_hit_ids = [hit_id for hit_id in hit_ids if not hit_service.exists(hit_id)]
|
|
170
169
|
|
|
171
|
-
if
|
|
172
|
-
return not_found(err=f"Hit id {non_existing_hit_ids
|
|
173
|
-
|
|
174
|
-
if len(non_existing_hit_ids) > 1:
|
|
175
|
-
return not_found(err=f"Hit ids {', '.join(non_existing_hit_ids)} do not exist.")
|
|
176
|
-
|
|
177
|
-
for hit_id in hit_ids:
|
|
178
|
-
if not hit_service.exists(hit_id):
|
|
179
|
-
return not_found(err=f"Hit id {hit_id} does not exist.")
|
|
170
|
+
if non_existing_hit_ids:
|
|
171
|
+
return not_found(err=f"Hit id(s) {', '.join(non_existing_hit_ids)} do not exist.")
|
|
180
172
|
|
|
181
173
|
hit_service.delete_hits(hit_ids)
|
|
182
174
|
|
|
@@ -247,7 +239,7 @@ def validate_hits(**kwargs):
|
|
|
247
239
|
|
|
248
240
|
@generate_swagger_docs()
|
|
249
241
|
@hit_api.route("/<id>", methods=["GET"])
|
|
250
|
-
@api_login(audit=
|
|
242
|
+
@api_login(audit=True, required_priv=["R"])
|
|
251
243
|
@add_etag(getter=hit_service.get_hit)
|
|
252
244
|
def get_hit(id: str, server_version: str, **kwargs):
|
|
253
245
|
"""Get a hit.
|
|
@@ -522,7 +514,7 @@ def add_label(id, label_set, user, **kwargs):
|
|
|
522
514
|
"success": True # Adding the label succeeded
|
|
523
515
|
}
|
|
524
516
|
"""
|
|
525
|
-
if not hit_service.
|
|
517
|
+
if not hit_service.exists(id):
|
|
526
518
|
return not_found(err=f"Hit {id} does not exist")
|
|
527
519
|
|
|
528
520
|
existing_hit: Hit = hit_service.get_hit(id, as_odm=True)
|
|
@@ -586,7 +578,7 @@ def remove_labels(id, label_set, user, **kwargs):
|
|
|
586
578
|
"success": True # Removing the labels succeeded
|
|
587
579
|
}
|
|
588
580
|
"""
|
|
589
|
-
if not hit_service.
|
|
581
|
+
if not hit_service.exists(id):
|
|
590
582
|
return not_found(err=f"Hit {id} does not exist")
|
|
591
583
|
|
|
592
584
|
if f"howler.labels.{label_set}" not in hit_service.get_hit(id, as_odm=True).flat_fields():
|
|
@@ -803,7 +795,7 @@ def edit_comment(id: str, comment_id: str, user: dict[str, Any], **kwargs):
|
|
|
803
795
|
if len(comment_value) > MAX_COMMENT_LEN:
|
|
804
796
|
return bad_request(err="Comment is too long.")
|
|
805
797
|
|
|
806
|
-
if not hit_service.
|
|
798
|
+
if not hit_service.exists(id):
|
|
807
799
|
return not_found(err=f"Hit {id} does not exist")
|
|
808
800
|
|
|
809
801
|
hit: Hit = kwargs["cached_hit"]
|
|
@@ -864,7 +856,7 @@ def delete_comments(id: str, user: User, **kwargs):
|
|
|
864
856
|
...hit # The new data for the hit
|
|
865
857
|
}
|
|
866
858
|
"""
|
|
867
|
-
if not hit_service.
|
|
859
|
+
if not hit_service.exists(id):
|
|
868
860
|
return not_found(err=f"Hit {id} does not exist")
|
|
869
861
|
|
|
870
862
|
comment_ids: list[str] = request.json or []
|
|
@@ -984,199 +976,3 @@ def remove_react_comment(id: str, comment_id: str, user: dict[str, Any], **kwarg
|
|
|
984
976
|
new_hit, version = hit_service.save_hit(hit, version=kwargs.get("server_version"))
|
|
985
977
|
|
|
986
978
|
return ok(new_hit), version
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
@generate_swagger_docs()
|
|
990
|
-
@hit_api.route("/bundle", methods=["POST"])
|
|
991
|
-
@api_login(audit=False, required_priv=["W"])
|
|
992
|
-
def create_bundle(user: User, **kwargs):
|
|
993
|
-
"""Create a new bundle
|
|
994
|
-
|
|
995
|
-
Variables:
|
|
996
|
-
None
|
|
997
|
-
|
|
998
|
-
Arguments:
|
|
999
|
-
None
|
|
1000
|
-
|
|
1001
|
-
Data Block:
|
|
1002
|
-
{
|
|
1003
|
-
"bundle": {
|
|
1004
|
-
...hit # A howler hit that will be used as a template for this new bundle
|
|
1005
|
-
},
|
|
1006
|
-
"hits": [...ids] # A list of existing howler hits to add as children to the new bundle
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
Result Example:
|
|
1010
|
-
{
|
|
1011
|
-
...hit # The created bundle
|
|
1012
|
-
}
|
|
1013
|
-
"""
|
|
1014
|
-
data = request.json
|
|
1015
|
-
if not isinstance(data, dict):
|
|
1016
|
-
return bad_request(err="Invalid data format")
|
|
1017
|
-
|
|
1018
|
-
bundle_hit: Optional[dict[str, Any]] = data.get("bundle")
|
|
1019
|
-
|
|
1020
|
-
if bundle_hit is None:
|
|
1021
|
-
return bad_request(err="You did not provide a bundle hit.")
|
|
1022
|
-
|
|
1023
|
-
try:
|
|
1024
|
-
odm, _ = hit_service.convert_hit(bundle_hit, unique=True)
|
|
1025
|
-
odm.howler.is_bundle = True
|
|
1026
|
-
|
|
1027
|
-
child_hits = data.get("hits", [])
|
|
1028
|
-
|
|
1029
|
-
if len(odm.howler.hits) < 1 and len(child_hits) < 1:
|
|
1030
|
-
return bad_request(err="You did not provide any child hits.")
|
|
1031
|
-
|
|
1032
|
-
for hit_id in child_hits:
|
|
1033
|
-
if hit_id not in odm.howler.hits:
|
|
1034
|
-
odm.howler.hits.append(hit_id)
|
|
1035
|
-
|
|
1036
|
-
hit_service.create_hit(odm.howler.id, odm, user=user.uname)
|
|
1037
|
-
analytic_service.save_from_hit(odm, user)
|
|
1038
|
-
|
|
1039
|
-
for hit_id in odm.howler.hits:
|
|
1040
|
-
child_hit: Hit = hit_service.get_hit(hit_id, as_odm=True)
|
|
1041
|
-
|
|
1042
|
-
if child_hit.howler.is_bundle:
|
|
1043
|
-
return bad_request(
|
|
1044
|
-
err=f"You cannot specify a bundle as a child of another bundle - {child_hit.howler.id} is a bundle."
|
|
1045
|
-
)
|
|
1046
|
-
|
|
1047
|
-
child_hit.howler.bundles.append(odm.howler.id)
|
|
1048
|
-
datastore().hit.save(child_hit.howler.id, child_hit)
|
|
1049
|
-
|
|
1050
|
-
return created(odm)
|
|
1051
|
-
except HowlerException as e:
|
|
1052
|
-
return bad_request(err=str(e))
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
@generate_swagger_docs()
|
|
1056
|
-
@hit_api.route("/bundle/<id>", methods=["PUT"])
|
|
1057
|
-
@api_login(audit=False, required_priv=["W"])
|
|
1058
|
-
@add_etag(getter=hit_service.get_hit, check_if_match=False)
|
|
1059
|
-
def update_bundle(id, **kwargs):
|
|
1060
|
-
"""Update a hit's child hits. Can be used to convert an existing hit into a bundle, or to update an existing bundle.
|
|
1061
|
-
|
|
1062
|
-
Variables:
|
|
1063
|
-
id => The ID of the bundle to update
|
|
1064
|
-
|
|
1065
|
-
Arguments:
|
|
1066
|
-
None
|
|
1067
|
-
|
|
1068
|
-
Data Block:
|
|
1069
|
-
[
|
|
1070
|
-
...ids
|
|
1071
|
-
]
|
|
1072
|
-
|
|
1073
|
-
Result Example:
|
|
1074
|
-
{
|
|
1075
|
-
...hit # The updated bundle
|
|
1076
|
-
}
|
|
1077
|
-
"""
|
|
1078
|
-
bundle_hit: Hit = cast(Hit, kwargs.get("cached_hit", None))
|
|
1079
|
-
if not bundle_hit:
|
|
1080
|
-
return not_found(err="This bundle does not exist.")
|
|
1081
|
-
|
|
1082
|
-
hit_ids = request.json
|
|
1083
|
-
if not isinstance(hit_ids, list):
|
|
1084
|
-
return bad_request(err="Invalid data format")
|
|
1085
|
-
|
|
1086
|
-
new_hit_list = bundle_hit.howler.as_primitives().get("hits", [])
|
|
1087
|
-
if bundle_hit.howler.is_bundle:
|
|
1088
|
-
for hit_id in hit_ids:
|
|
1089
|
-
if hit_id not in new_hit_list:
|
|
1090
|
-
new_hit_list.append(hit_id)
|
|
1091
|
-
else:
|
|
1092
|
-
return conflict(err=f"The hit {hit_id} is already in the bundle {bundle_hit.howler.id}.")
|
|
1093
|
-
else:
|
|
1094
|
-
new_hit_list = hit_ids
|
|
1095
|
-
|
|
1096
|
-
bundle_hit.howler.hits = new_hit_list
|
|
1097
|
-
bundle_hit.howler.is_bundle = True
|
|
1098
|
-
|
|
1099
|
-
try:
|
|
1100
|
-
for hit_id in new_hit_list:
|
|
1101
|
-
child_hit: Hit = hit_service.get_hit(hit_id, as_odm=True)
|
|
1102
|
-
|
|
1103
|
-
if child_hit.howler.is_bundle:
|
|
1104
|
-
return bad_request(
|
|
1105
|
-
err=f"You cannot specify a bundle as a child of another bundle - {child_hit.howler.id} is a bundle."
|
|
1106
|
-
)
|
|
1107
|
-
|
|
1108
|
-
new_bundle_list = child_hit.howler.as_primitives().get("bundles", [])
|
|
1109
|
-
new_bundle_list.append(bundle_hit.howler.id)
|
|
1110
|
-
child_hit.howler.bundles = new_bundle_list
|
|
1111
|
-
datastore().hit.save(child_hit.howler.id, child_hit)
|
|
1112
|
-
|
|
1113
|
-
datastore().hit.save(bundle_hit.howler.id, bundle_hit)
|
|
1114
|
-
|
|
1115
|
-
return ok(bundle_hit)
|
|
1116
|
-
except HowlerException as e:
|
|
1117
|
-
return bad_request(err=str(e))
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
@generate_swagger_docs()
|
|
1121
|
-
@hit_api.route("/bundle/<id>", methods=["DELETE"])
|
|
1122
|
-
@api_login(audit=False, required_priv=["W"])
|
|
1123
|
-
@add_etag(getter=hit_service.get_hit, check_if_match=False)
|
|
1124
|
-
def remove_bundle_children(id, **kwargs):
|
|
1125
|
-
"""Remove a bundle's child hits.
|
|
1126
|
-
|
|
1127
|
-
Can be used to convert an existing bundle back into a normal hit, or to remove a subset of
|
|
1128
|
-
existing hits from the bundle.
|
|
1129
|
-
|
|
1130
|
-
Variables:
|
|
1131
|
-
id => The ID of the bundle to update
|
|
1132
|
-
|
|
1133
|
-
Arguments:
|
|
1134
|
-
None
|
|
1135
|
-
|
|
1136
|
-
Data Block:
|
|
1137
|
-
[
|
|
1138
|
-
...ids OR '*' # A list of ids to remove, or a single '*' to remove all
|
|
1139
|
-
]
|
|
1140
|
-
|
|
1141
|
-
Result Example:
|
|
1142
|
-
{
|
|
1143
|
-
...hit # The updated hit
|
|
1144
|
-
}
|
|
1145
|
-
"""
|
|
1146
|
-
bundle_hit = kwargs.get("cached_hit", None)
|
|
1147
|
-
if not bundle_hit:
|
|
1148
|
-
return not_found(err="This bundle does not exist.")
|
|
1149
|
-
|
|
1150
|
-
hit_ids = request.json
|
|
1151
|
-
if not isinstance(hit_ids, list):
|
|
1152
|
-
return bad_request(err="Invalid data format")
|
|
1153
|
-
|
|
1154
|
-
new_hit_list = bundle_hit.howler.get("hits", [])
|
|
1155
|
-
if bundle_hit.howler.is_bundle:
|
|
1156
|
-
if hit_ids == ["*"]:
|
|
1157
|
-
hit_ids = new_hit_list
|
|
1158
|
-
new_hit_list = []
|
|
1159
|
-
else:
|
|
1160
|
-
new_hit_list = [_id for _id in new_hit_list if _id not in hit_ids]
|
|
1161
|
-
else:
|
|
1162
|
-
return bad_request(err="The specified hit must be a bundle.")
|
|
1163
|
-
|
|
1164
|
-
bundle_hit.howler.hits = new_hit_list
|
|
1165
|
-
bundle_hit.howler.is_bundle = len(new_hit_list) > 0
|
|
1166
|
-
|
|
1167
|
-
try:
|
|
1168
|
-
for hit_id in hit_ids:
|
|
1169
|
-
child_hit: Hit = hit_service.get_hit(hit_id, as_odm=True)
|
|
1170
|
-
|
|
1171
|
-
try:
|
|
1172
|
-
child_hit.howler.bundles.remove(bundle_hit.howler.id)
|
|
1173
|
-
except ValueError:
|
|
1174
|
-
logger.warning("Bundle isn't included in child %s!", bundle_hit.howler.id)
|
|
1175
|
-
|
|
1176
|
-
datastore().hit.save(child_hit.howler.id, child_hit)
|
|
1177
|
-
|
|
1178
|
-
datastore().hit.save(bundle_hit.howler.id, bundle_hit)
|
|
1179
|
-
|
|
1180
|
-
return ok(bundle_hit)
|
|
1181
|
-
except HowlerException as e:
|
|
1182
|
-
return bad_request(err=str(e))
|
|
@@ -94,7 +94,6 @@ def create_one_or_many_hits(tool_name: str, user: User, **kwargs): # noqa: C901
|
|
|
94
94
|
|
|
95
95
|
out: list[dict[str, Any]] = []
|
|
96
96
|
odms = []
|
|
97
|
-
bundle_hit: Optional[Hit] = None
|
|
98
97
|
for hit in hits:
|
|
99
98
|
cur_id = get_random_id()
|
|
100
99
|
cur_time = now_as_iso()
|
|
@@ -138,12 +137,7 @@ def create_one_or_many_hits(tool_name: str, user: User, **kwargs): # noqa: C901
|
|
|
138
137
|
try:
|
|
139
138
|
odm, warns = hit_service.convert_hit(obj, unique=True, ignore_extra_values=ignore_extra_values)
|
|
140
139
|
|
|
141
|
-
|
|
142
|
-
bundle_hit = odm
|
|
143
|
-
elif odm.howler.is_bundle:
|
|
144
|
-
return bad_request(err="You can only specify one bundle hit!")
|
|
145
|
-
else:
|
|
146
|
-
odms.append(odm)
|
|
140
|
+
odms.append(odm)
|
|
147
141
|
|
|
148
142
|
out.append(
|
|
149
143
|
{
|
|
@@ -162,20 +156,9 @@ def create_one_or_many_hits(tool_name: str, user: User, **kwargs): # noqa: C901
|
|
|
162
156
|
return bad_request(out, warnings=warnings, err="No valid hits were provided")
|
|
163
157
|
else:
|
|
164
158
|
for odm in odms:
|
|
165
|
-
if bundle_hit is not None:
|
|
166
|
-
bundle_hit.howler.hits.append(odm.howler.id)
|
|
167
|
-
bundle_hit.howler.bundle_size += 1
|
|
168
|
-
odm.howler.bundles.append(bundle_hit.howler.id)
|
|
169
|
-
|
|
170
159
|
hit_service.create_hit(odm.howler.id, odm, user=user.uname)
|
|
171
|
-
|
|
172
160
|
analytic_service.save_from_hit(odm, user)
|
|
173
161
|
|
|
174
|
-
if bundle_hit:
|
|
175
|
-
hit_service.create_hit(bundle_hit.howler.id, bundle_hit, user=user.uname)
|
|
176
|
-
|
|
177
|
-
analytic_service.save_from_hit(bundle_hit, user)
|
|
178
|
-
|
|
179
162
|
datastore().hit.commit()
|
|
180
163
|
|
|
181
164
|
action_service.bulk_execute_on_query(f"howler.id:({' OR '.join(entry['id'] for entry in out)})", user=user)
|
|
@@ -321,7 +321,7 @@ def get_user_avatar(username, **_):
|
|
|
321
321
|
"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD..."
|
|
322
322
|
"""
|
|
323
323
|
storage = datastore()
|
|
324
|
-
avatar
|
|
324
|
+
avatar = storage.user_avatar.get(username)
|
|
325
325
|
|
|
326
326
|
if avatar:
|
|
327
327
|
resp = ok(avatar)
|