Skip to content

feat(api,ui): add howler.triaged to track time assessment is made#490

Merged
cccs-mdr merged 12 commits into
developfrom
add_triage_date_revert_assessor
Jul 21, 2026
Merged

feat(api,ui): add howler.triaged to track time assessment is made#490
cccs-mdr merged 12 commits into
developfrom
add_triage_date_revert_assessor

Conversation

@cccs-hxp

Copy link
Copy Markdown
Contributor

The assessor field was proposed to help with finding all hits assessed by a specific user, but it overlaps with howler.assignment. It is possible to get all hits assessed by a user using the assignment field which is auto-set on assessment, but it is not possible to filter or sort these hits based on time of assessment. These changes add a howler.triaged field which tracks the time that a hit is assessed and adds this field to the available builtin sort fields in the UI.

Changes:

  • Revert howler.assessor changes
  • Add a howler.triaged timestamp field, default to null
  • Set howler.triaged whenever a hit's assessment is changed (set to NOW when a hit is assessed and to None when a hit's assessment is removed)
  • Add howler.triaged to the list of builtin sort fields in the UI search

Copilot AI review requested due to automatic review settings July 14, 2026 19:09
Comment thread ui/src/components/elements/hit/elements/Assigned.tsx Fixed

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning

  • Copilot's review of this pull request may be incomplete because some of the changed files are excluded by your Copilot content exclusion settings. See Excluding content from Copilot for details.

Pull request overview

Adds a new howler.triaged timestamp to record when a hit is assessed (enabling sort/filter by triage time), while reverting the previously introduced howler.assessor field and updating UI/API workflows accordingly.

Changes:

  • API: Introduce howler.triaged on HowlerData and set/reset it as assessment is applied/removed; update transitions/workflow and related automations.
  • UI: Add howler.triaged to the built-in hit sort fields; refactor hit “user chips” UI to remove assessor usage.
  • Tests: Update transition integration test expectations to validate triaged behavior.

Reviewed changes

Copilot reviewed 13 out of 16 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
ui/src/models/entities/generated/Howler.d.ts Removes assessor and adds triaged to the generated Howler type.
ui/src/components/routes/hits/search/shared/HitSort.tsx Allows sorting by howler.triaged.
ui/src/components/elements/hit/HitBanner.tsx Replaces HitUsers with new Assigned component usage.
ui/src/components/elements/hit/grid/HitRow.tsx Replaces HitUsers with Assigned in dense/grid rows.
ui/src/components/elements/hit/elements/HitUsers.tsx Removes combined assignee/assessor/viewers UI (deleted).
ui/src/components/elements/hit/elements/Assigned.tsx New UI component for assignment + viewers rendering.
api/test/integration/api/test_hit_transition.py Updates transition flow checks to validate triaged timestamps.
api/howler/services/hit_service.py Ensures assess transitions also apply assignment updates.
api/howler/odm/random_data.py Updates random hit generation to align with updated assessment helper usage.
api/howler/odm/models/howler_data.py Removes assessor, adds triaged: Optional[datetime].
api/howler/odm/helper.py Initializes triaged to None in helper-generated hits.
api/howler/helper/hit.py Updates assess_hit to write howler.triaged and removes assessor logic.
api/howler/actions/promote.py Clears triaged when clearing assessment/rationale during promote.
api/howler/actions/demote.py Clears triaged when clearing assessment/rationale during demote.
ui/src/locales/en/translation.json Updated (content excluded from review).
ui/src/locales/fr/translation.json Updated (content excluded from review).
Files excluded by content exclusion policy (2)
  • ui/src/locales/en/translation.json
  • ui/src/locales/fr/translation.json

Comment thread api/howler/actions/promote.py
Comment thread api/howler/actions/demote.py
Comment thread api/test/integration/api/test_hit_transition.py Outdated
Comment thread api/howler/helper/hit.py Outdated
Comment thread ui/src/components/elements/hit/grid/HitRow.tsx
Comment thread api/test/integration/api/test_hit_transition.py
@github-actions

github-actions Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 30.16% 9647 / 31979
🔵 Statements 30.16% 9647 / 31979
🔵 Functions 46.17% 404 / 875
🔵 Branches 78.96% 2031 / 2572
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
ui/src/components/elements/hit/elements/Assigned.tsx 98.46% 35.71% 100% 98.46% 30
ui/src/components/routes/hits/search/shared/HitSort.tsx 0% 0% 0% 0% 1-129
Generated in workflow #1281 for commit f40fabc by the Vitest Coverage Report Action

@github-actions

github-actions Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Static Badge

Error Output
___________ TestCorrelationWorker.test_worker_handles_multiple_hits ____________

self = <test.integration.api.test_correlation.TestCorrelationWorker object at 0x7fc667f8a960>
test_case = ('1fbuArEwiRXAdEvfvB3V70', <requests.sessions.Session object at 0x7fc645da5400>, 'http://localhost:5000')
datastore = <howler.datastore.howler_store.HowlerDatastore object at 0x7fc654c40380>

    def test_worker_handles_multiple_hits(self, test_case, datastore: HowlerDatastore):
        """Multiple hits ingested in sequence are each processed by the worker."""
        case_id, session, host = test_case
    
        rule_data = {
            "query": "event.kind:alert",
            "destination": "worker-multi/{{howler.analytic}}",
        }
        get_api_data(
            session,
            f"{host}/api/v2/case/{case_id}/rules",
            method="POST",
            data=json.dumps(rule_data),
        )
        datastore.case.commit()
    
        hit_ids = []
        for i in range(3):
            ingest_resp = get_api_data(
                session,
                f"{host}/api/v2/ingest/hit",
                method="POST",
                data=json.dumps([_make_hit(analytic=f"Multi-{i}", kind="alert")]),
            )
            hit_ids.append(ingest_resp[0])
    
        datastore.hit.commit()
    
>       assert self._wait_for_case_items(datastore, case_id, hit_ids), (
            f"Worker did not add all hits {hit_ids} to case {case_id} within {self.MAX_WAIT}s"
        )
E       AssertionError: Worker did not add all hits ['4xQPMUaDWeq6Sc5TkEvuWp', '5qw82XKtYMNjZcgNTbNxo8', '11pcuXyON6WI04ooHsxBFK'] to case 1fbuArEwiRXAdEvfvB3V70 within 30s
E       assert False
E        +  where False = _wait_for_case_items(<howler.datastore.howler_store.HowlerDatastore object at 0x7fc654c40380>, '1fbuArEwiRXAdEvfvB3V70', ['4xQPMUaDWeq6Sc5TkEvuWp', '5qw82XKtYMNjZcgNTbNxo8', '11pcuXyON6WI04ooHsxBFK'])
E        +    where _wait_for_case_items = <test.integration.api.test_correlation.TestCorrelationWorker object at 0x7fc667f8a960>._wait_for_case_items

test/integration/api/test_correlation.py:385: AssertionError
----------------------------- Captured stderr call -----------------------------
26/07/21 16:59:56 INFO howler.api.services.correlation_service | Correlation batch complete: 1/1 hit(s) added
------------------------------ Captured log call -------------------------------
INFO     howler.api.services.correlation_service:correlation_service.py:213 Correlation batch complete: 1/1 hit(s) added
=============================== warnings summary ===============================
.venv/lib/python3.12/site-packages/passlib/utils/__init__.py:854
  /home/runner/work/howler/howler/api/.venv/lib/python3.12/site-packages/passlib/utils/__init__.py:854: DeprecationWarning: 'crypt' is deprecated and slated for removal in Python 3.13
    from crypt import crypt as _crypt

.venv/lib/python3.12/site-packages/sigma/conditions.py:233
  /home/runner/work/howler/howler/api/.venv/lib/python3.12/site-packages/sigma/conditions.py:233: PyparsingDeprecationWarning: 'setParseAction' deprecated - use 'set_parse_action'
    identifier.setParseAction(ConditionIdentifier.from_parsed)

.venv/lib/python3.12/site-packages/sigma/conditions.py:238
  /home/runner/work/howler/howler/api/.venv/lib/python3.12/site-packages/sigma/conditions.py:238: PyparsingDeprecationWarning: 'setParseAction' deprecated - use 'set_parse_action'
    selector.setParseAction(ConditionSelector.from_parsed)

test/integration/cronjobs/test_retention.py: 8 warnings
test/integration/service/test_action_service.py: 2 warnings
test/integration/test_datastore.py: 5 warnings
test/integration/test_ilm.py: 1 warning
test/unit/actions/test_init.py: 2 warnings
  /home/runner/work/howler/howler/api/howler/datastore/collection.py:343: GeneralAvailabilityWarning: This API is in technical preview and may be changed or removed in a future release. Elastic will work to fix any issues, but features in technical preview are not subject to the support SLA of official GA features.
    ret_val = func(*args, **kwargs)

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html

---------

@cccs-hxp
cccs-hxp requested review from a team and cccs-mdr July 14, 2026 19:34

@cccs-mdr cccs-mdr left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One other minor change to resolve a related issue

Comment thread api/howler/helper/hit.py
odm_helper.update("howler.escalation", escalation),
odm_helper.update("howler.rationale", rationale, silent=True),
odm_helper.update("howler.assessor", assessor_id, silent=True),
odm_helper.update("howler.triaged", triaged_timestamp),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't directly related, but also set howler.status to resolved.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Context: howler.status is set properly when hit is assessed through a transition, but it isn't set when calling assess_hit directly (from promote / demote actions).

To match the the only transition out of resolved, if an assessment is removed by assess_hit we can set howler.status to in progress.

With the current workflow implementation, if an action sets the status, it takes priority over the transition defined destination status. I think it should be the other way around or even fail if values don't match. Personally, if there's a destination defined in the transition states, I expect this to be the behaviour, with any status changes in actions only being a fallback.

Copilot AI review requested due to automatic review settings July 21, 2026 16:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning

  • Copilot's review of this pull request may be incomplete because some of the changed files are excluded by your Copilot content exclusion settings. See Excluding content from Copilot for details.

Pull request overview

Copilot reviewed 12 out of 15 changed files in this pull request and generated 1 comment.

Files excluded by content exclusion policy (2)
  • ui/src/locales/en/translation.json
  • ui/src/locales/fr/translation.json
Comments suppressed due to low confidence (1)

api/test/integration/api/test_hit_transition.py:191

  • transition_data is a module-scoped fixture, but the test loop mutates each dict via pop("check", ...). This makes the fixture stateful and can create order-dependent failures if the fixture is reused (or the test is re-run within the same process). Also, dest is only used for assertions and doesn't need to be sent in the API payload. Prefer building a per-iteration payload without mutating the fixture.
    for data in transition_data:
        checks = data.pop("check", None)
        _, version = datastore.hit.get(HIT_ID, as_obj=False, version=True)
        get_api_data(
            session=session,

Comment thread api/howler/helper/hit.py
Comment on lines +58 to +62
status = Status.IN_PROGRESS
else:
triaged_timestamp = "NOW"
status = Status.RESOLVED

@cccs-mdr
cccs-mdr merged commit f0e4c4a into develop Jul 21, 2026
17 of 18 checks passed
@cccs-mdr
cccs-mdr deleted the add_triage_date_revert_assessor branch July 21, 2026 18:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants