fix(jira,tests): send real datetime to RT/BT date fields, resolve assignee usernames for operators
Aegis CI / lint-and-test (push) Has been cancelled
Snyk Security Scan / Python vulnerabilities (backend) (push) Has been cancelled
Snyk Security Scan / npm vulnerabilities (frontend) (push) Has been cancelled
Snyk Security Scan / Docker image vulnerabilities (backend) (push) Has been cancelled

- RT/BT Start/End Date are genuine Jira "datetime" custom fields (confirmed
  via issue_editmeta), not plain dates. Pushing a bare "YYYY-MM-DD" string
  made Jira default the time-of-day to midnight, so RT Start Date and RT
  End Date ended up showing the same meaningless midnight timestamp instead
  of the actual execution window. Now sends a full ISO datetime.
- The test detail page showed "RT: Unassigned" for the operator who WAS
  correctly assigned, because GET /users/operators (the only source
  AssigneeControl used to resolve an assignee ID into a username) is
  restricted to leads/managers — a plain operator has no other way to
  resolve their own ID. TestOut now resolves and includes the assignee's
  username directly (red/blue tech + reviewer), so the badge no longer
  depends on a permission-gated list the viewer might not have access to.
This commit is contained in:
kitos
2026-07-13 16:53:31 +02:00
parent 4221d2858b
commit 6d6f87b968
8 changed files with 128 additions and 9 deletions
+25
View File
@@ -330,6 +330,12 @@ class TestOut(BaseModel):
# Assignment fields
red_tech_assignee: uuid.UUID | None = None
blue_tech_assignee: uuid.UUID | None = None
# Resolved usernames — the operator (or lead) viewing a test they're not
# a lead/manager on can't call GET /users/operators (403), so they have
# no other way to resolve who an assignee ID actually is. Populated
# from the ORM relationship, same pattern as technique_name below.
red_tech_assignee_username: str | None = None
blue_tech_assignee_username: str | None = None
# Review assignment fields
red_reviewer_assignee: uuid.UUID | None = None
@@ -340,6 +346,8 @@ class TestOut(BaseModel):
blue_review_by: uuid.UUID | None = None
blue_review_at: datetime | None = None
blue_review_notes: str | None = None
red_reviewer_assignee_username: str | None = None
blue_reviewer_assignee_username: str | None = None
system_gaps: str | None = None
# On-hold fields
@@ -398,6 +406,23 @@ class TestOut(BaseModel):
except Exception: # nosec B110
pass # DetachedInstanceError or similar — leave technique fields None
# Resolved assignee usernames (lazy-load, same as technique above).
# A plain operator/tech viewing their own test can't call
# GET /users/operators (lead/manager-only) to resolve an assignee ID
# into a name, so the API needs to hand it over pre-resolved.
for attr, field in (
("red_tech_assigned_user", "red_tech_assignee_username"),
("blue_tech_assigned_user", "blue_tech_assignee_username"),
("red_reviewer", "red_reviewer_assignee_username"),
("blue_reviewer", "blue_reviewer_assignee_username"),
):
try:
user = getattr(obj, attr, None)
if user is not None:
obj.__dict__[field] = user.username
except Exception: # nosec B110
pass # DetachedInstanceError or similar — leave username None
# Only split evidences when they are already in memory (loaded via joinedload)
raw_evs = obj.__dict__.get("evidences")
if raw_evs is not None:
+18 -4
View File
@@ -1202,6 +1202,20 @@ def _select_field(value: str) -> dict:
return {"value": value}
def _jira_datetime(dt: datetime) -> str:
"""Format a naive-UTC datetime for a Jira ``datetime``-type custom field.
RT/BT Start/End Date are genuine Jira datetime fields (confirmed via
issue_editmeta — schema type "datetime"), not plain dates. Sending a
bare "YYYY-MM-DD" string makes Jira default the time-of-day to
midnight, so RT Start Date and RT End Date ended up showing the same
meaningless midnight timestamp regardless of when the operator
actually started/finished. Jira's REST API expects
``yyyy-MM-dd'T'HH:mm:ss.SSSZ`` for this field type.
"""
return dt.strftime("%Y-%m-%dT%H:%M:%S.000+0000")
def _compute_hours(start: Optional[datetime], end: Optional[datetime]) -> Optional[float]:
"""Return the elapsed time between two timestamps in hours, or None if unavailable."""
if not start or not end:
@@ -1272,10 +1286,10 @@ def push_rt_submitted(db: Session, test: Test) -> None:
else test.execution_start_time
)
if first_start:
fields[JIRA_FIELD_RT_START_DATE] = first_start.strftime("%Y-%m-%d")
fields[JIRA_FIELD_RT_START_DATE] = _jira_datetime(first_start)
if test.execution_end_time:
fields[JIRA_FIELD_RT_END_DATE] = test.execution_end_time.strftime("%Y-%m-%d")
fields[JIRA_FIELD_RT_END_DATE] = _jira_datetime(test.execution_end_time)
attack_success = _enum_value(test.attack_success)
if attack_success in _ATTACK_SUCCESS_TO_JIRA:
@@ -1287,13 +1301,13 @@ def push_rt_submitted(db: Session, test: Test) -> None:
def push_bt_started(db: Session, test: Test) -> None:
"""Set the BT Start Date field — called when Blue picks up the test to evaluate."""
_update_test_fields(db, test, {
JIRA_FIELD_BT_START_DATE: datetime.utcnow().strftime("%Y-%m-%d"),
JIRA_FIELD_BT_START_DATE: _jira_datetime(datetime.utcnow()),
})
def push_bt_submitted(db: Session, test: Test) -> None:
"""Set BT End Date, Attack Detected/Contained, and time-to-detect/contain — Blue submits for review."""
fields: dict = {JIRA_FIELD_BT_END_DATE: datetime.utcnow().strftime("%Y-%m-%d")}
fields: dict = {JIRA_FIELD_BT_END_DATE: _jira_datetime(datetime.utcnow())}
detection_result = _enum_value(test.detection_result)
if detection_result in _DETECTION_TO_JIRA:
@@ -396,6 +396,8 @@ def get_test_detail(db: Session, test_id: uuid.UUID) -> Test:
.options(
joinedload(Test.evidences), joinedload(Test.technique),
joinedload(Test.round_history),
joinedload(Test.red_tech_assigned_user), joinedload(Test.blue_tech_assigned_user),
joinedload(Test.red_reviewer), joinedload(Test.blue_reviewer),
)
.filter(Test.id == test_id)
# Chain .first() call