fix: prevent operation report from returning null when link paw absent (#3048)#3279
Conversation
…absent (#3048) Three related KeyErrors in c_operation.py could cause Operation.report() to silently return None, which the API then serialised as JSON null and the UI rendered as "Null": 1. `agents_steps[step.paw]` in report() raised KeyError when a link's paw was not in the set of operation agents built at call time (e.g. the agent was removed between operation run and report download). Fixed with `agents_steps.setdefault(step.paw, {'steps': []})`. 2. `abilities_by_agent[link.paw]` in _get_all_possible_abilities_by_agent() had the same pattern — orphan paw not guarded. Fixed with an explicit membership check before the extend. 3. The `except Exception` block in report() logged the error but fell off the end of the function, returning None implicitly. The caller then returned None to web.json_response(), producing the "Null" download. Fixed by re-raising so the framework returns a proper 500 with an error body instead of a silent null payload. Adds a regression test that constructs an operation with a chain link whose paw is not present in operation.agents and asserts report() returns a non-None dict that includes the orphan paw's steps.
There was a problem hiding this comment.
Pull request overview
Fixes operation report generation so downloading an operation report no longer returns JSON null (rendered as “Null”) when the operation chain contains links for agents (paws) that are no longer present in operation.agents at report-download time.
Changes:
- Prevent
KeyErrorinOperation.report()by safely initializing missingagents_stepsentries for orphaned paws. - Prevent
KeyErrorin_get_all_possible_abilities_by_agent()by guarding lookups when a link’s paw is not in the currentabilities_by_agentmap. - Add a regression test covering report generation with an orphaned paw in the chain.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| app/objects/c_operation.py | Makes report generation resilient to orphaned paws and ensures exceptions don’t silently yield None. |
| tests/objects/test_operation.py | Adds regression coverage to ensure report() returns a dict and includes orphan-paw steps. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if step.agent_reported_time: | ||
| step_report['agent_reported_time'] = step.agent_reported_time.strftime(self.TIME_FORMAT) | ||
| agents_steps[step.paw]['steps'].append(step_report) | ||
| agents_steps.setdefault(step.paw, {'steps': []})['steps'].append(step_report) |
| if link.ability.ability_id not in self.adversary.atomic_ordering: | ||
| matching_abilities = await data_svc.locate('abilities', match=dict(ability_id=link.ability.ability_id)) | ||
| abilities_by_agent[link.paw]['all_abilities'].extend(matching_abilities) | ||
| if link.paw in abilities_by_agent: | ||
| abilities_by_agent[link.paw]['all_abilities'].extend(matching_abilities) |
There was a problem hiding this comment.
Pull request overview
Fixes operation report downloads returning JSON null when an operation contains links for agents (paws) that are no longer in operation.agents, and ensures exceptions in Operation.report() propagate properly instead of being swallowed.
Changes:
- Prevent
KeyErrorinOperation.report()by creating missingagents_stepsentries for orphan paws. - Prevent
KeyErrorin_get_all_possible_abilities_by_agent()by guarding missing paws. - Re-raise exceptions from
Operation.report()so callers get an HTTP 500 instead of anullJSON response; add regression test for orphan paw reporting.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| tests/objects/test_operation.py | Adds a regression test ensuring orphan paw links don’t cause report() to return None. |
| app/objects/c_operation.py | Guards report generation and ability aggregation against missing agent paws; ensures exceptions propagate. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| logging.error('Error generating operation report (%s)' % self.name, exc_info=True) | ||
| raise |
| if step.agent_reported_time: | ||
| step_report['agent_reported_time'] = step.agent_reported_time.strftime(self.TIME_FORMAT) | ||
| agents_steps[step.paw]['steps'].append(step_report) | ||
| agents_steps.setdefault(step.paw, {'steps': []})['steps'].append(step_report) |
| if link.paw in abilities_by_agent: | ||
| abilities_by_agent[link.paw]['all_abilities'].extend(matching_abilities) |
There was a problem hiding this comment.
Pull request overview
Fixes operation report generation so it no longer returns None (serialized as JSON null) when the operation contains chain links for paws not present in operation.agents, and adds a regression test for the scenario.
Changes:
- Guarded report step aggregation to tolerate “orphan” paws when building
report['steps']. - Prevented
KeyErrorin ability aggregation by safely handling paws missing fromabilities_by_agent. - Ensured report-generation exceptions don’t silently devolve into
Noneby re-raising after logging; added regression test.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| tests/objects/test_operation.py | Adds regression coverage for orphan-paw links to ensure report() returns a dict and includes steps for missing agents. |
| app/objects/c_operation.py | Hardens report/ability aggregation against missing paws and stops swallowing unexpected exceptions during report generation. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| async def test_report_includes_steps_for_agents_not_in_host_group( | ||
| self, operation_agent, operation_adversary, executor, ability, operation_link, | ||
| encoded_command, parse_datestring, file_svc, data_svc, knowledge_svc, fire_event_mock): | ||
| """Regression test for issue #3048: a link whose paw is absent from operation.agents | ||
| must not cause report() to silently return None (i.e. download as 'Null').""" |
| if step.agent_reported_time: | ||
| step_report['agent_reported_time'] = step.agent_reported_time.strftime(self.TIME_FORMAT) | ||
| agents_steps[step.paw]['steps'].append(step_report) | ||
| agents_steps.setdefault(step.paw, {'steps': []})['steps'].append(step_report) |
| matching_abilities = await data_svc.locate('abilities', match=dict(ability_id=link.ability.ability_id)) | ||
| abilities_by_agent[link.paw]['all_abilities'].extend(matching_abilities) | ||
| entry = abilities_by_agent.get(link.paw) | ||
| if entry: |
|
#3048) (#3279) * fix: prevent operation report from returning null when a link paw is absent (#3048) Three related KeyErrors in c_operation.py could cause Operation.report() to silently return None, which the API then serialised as JSON null and the UI rendered as "Null": 1. `agents_steps[step.paw]` in report() raised KeyError when a link's paw was not in the set of operation agents built at call time (e.g. the agent was removed between operation run and report download). Fixed with `agents_steps.setdefault(step.paw, {'steps': []})`. 2. `abilities_by_agent[link.paw]` in _get_all_possible_abilities_by_agent() had the same pattern — orphan paw not guarded. Fixed with an explicit membership check before the extend. 3. The `except Exception` block in report() logged the error but fell off the end of the function, returning None implicitly. The caller then returned None to web.json_response(), producing the "Null" download. Fixed by re-raising so the framework returns a proper 500 with an error body instead of a silent null payload. Adds a regression test that constructs an operation with a chain link whose paw is not present in operation.agents and asserts report() returns a non-None dict that includes the orphan paw's steps. * style: fix E303 too many blank lines in test_operation.py * refactor: simplify double dict lookup using abilities_by_agent.get()



Summary
Fixes the "Null" download bug on operation reports (#3048). Three related
KeyErrors inc_operation.pycausedOperation.report()to silently returnNone, whichweb.json_response(None)serialised as JSONnulland the UI displayed/downloaded as "Null".Root causes:
agents_steps[step.paw]KeyError inreport()—agents_stepsis pre-populated only with paws fromself.agentsat call time. If an agent was removed between when the operation ran and when the report is downloaded, any link for that agent raisesKeyError. Fixed withagents_steps.setdefault(step.paw, {'steps': []}).abilities_by_agent[link.paw]KeyError in_get_all_possible_abilities_by_agent()— same pattern: orphan paw not guarded. Fixed with an explicitif link.paw in abilities_by_agentcheck.Silent
Nonereturn fromexcept Exception— the broadexceptblock logged the error but fell off the function end without returning anything.Nonepropagated all the way toweb.json_response(None)→ JSONnull→ "Null" download. Fixed by addingraiseso the framework returns a proper HTTP 500 with an error body.Test plan
pytest tests/objects/test_operation.py -v --asyncio-mode=auto— all 27 tests pass, including the new regression test.test_report_includes_steps_for_agents_not_in_host_group— constructs an operation with a chain link whose paw is not inoperation.agents; assertsreport()returns a non-None dict containing the orphan paw's steps.pytest tests/api/v2/handlers/test_operations_api.py -v --asyncio-mode=auto— all 51 tests pass.