feat(jira): per-user auth, lifecycle hooks, admin config endpoints
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled

- Add jira_api_token field to User model + migration b042
- Per-user Jira client: user's corporate email + personal Atlassian token
- Admin-configurable Jira URL/project via system_configs (GET/PATCH /system/jira-config + POST /system/jira-test)
- Auto-create Jira ticket when a test is created (non-fatal)
- Push lifecycle comments on every state transition: draft→red_executing→blue_evaluating→in_review→validated/rejected→draft
- Rich ticket descriptions with technique, MITRE ID, priority from severity, labels
- UserOut.jira_token_set (bool) instead of exposing raw token
- PATCH /users/me/preferences now accepts jira_api_token

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
kitos
2026-05-26 15:56:28 +02:00
parent 8bed3abc08
commit c780ad1e78
8 changed files with 631 additions and 46 deletions

View File

@@ -108,12 +108,7 @@ def transition_state(
def start_execution(db: Session, test: Test, user: User) -> Test:
"""Move from ``draft`` → ``red_executing``.
Typically called by a **red_tech** when they begin the attack.
Delegates to :meth:`TestEntity.start_execution` which handles the
state transition and sets ``execution_date`` / ``red_started_at``.
"""
"""Move from ``draft`` → ``red_executing``."""
entity = TestEntity.from_orm(test)
entity.start_execution()
entity.apply_to(test)
@@ -138,6 +133,12 @@ def start_execution(db: Session, test: Test, user: User) -> Test:
except Exception as e:
logger.warning("Notification failed for test %s: %s", test.id, e, exc_info=True)
try:
from app.services.jira_service import push_test_event
push_test_event(db, test, user, "red_executing")
except Exception as e:
logger.warning("Jira push failed for test %s: %s", test.id, e, exc_info=True)
return test
@@ -176,6 +177,13 @@ def submit_red_evidence(db: Session, test: Test, user: User) -> Test:
# Start Blue Team timer
test.blue_started_at = now
test.blue_paused_seconds = 0
try:
from app.services.jira_service import push_test_event
push_test_event(db, test, user, "blue_evaluating")
except Exception as e:
logger.warning("Jira push failed for test %s: %s", test.id, e, exc_info=True)
return test
@@ -210,6 +218,12 @@ def submit_blue_evidence(db: Session, test: Test, user: User) -> Test:
description=f"Blue Team evaluation: {test.name}",
)
try:
from app.services.jira_service import push_test_event
push_test_event(db, test, user, "in_review")
except Exception as e:
logger.warning("Jira push failed for test %s: %s", test.id, e, exc_info=True)
return test
@@ -355,7 +369,7 @@ def validate_as_red_lead(
},
)
_dispatch_dual_validation_effects(db, test, entity)
_dispatch_dual_validation_effects(db, test, entity, actor=user)
return test
@@ -390,7 +404,7 @@ def validate_as_blue_lead(
},
)
_dispatch_dual_validation_effects(db, test, entity)
_dispatch_dual_validation_effects(db, test, entity, actor=user)
return test
@@ -409,9 +423,9 @@ def check_dual_validation(db: Session, test: Test) -> Test:
def _dispatch_dual_validation_effects(
db: Session, test: Test, entity: TestEntity
db: Session, test: Test, entity: TestEntity, actor: User | None = None
) -> None:
"""Dispatch side effects (notifications, cache) based on domain events."""
"""Dispatch side effects (notifications, cache, Jira) based on domain events."""
for event in entity.events:
if event.name == "dual_validation_approved":
try:
@@ -426,6 +440,13 @@ def _dispatch_dual_validation_effects(
"Notification failed for test %s (validated): %s",
test.id, e, exc_info=True,
)
if actor:
try:
from app.services.jira_service import push_test_event
push_test_event(db, test, actor, "validated")
except Exception as e:
logger.warning("Jira push failed for test %s: %s", test.id, e, exc_info=True)
elif event.name == "dual_validation_rejected":
try:
notify_test_state_change(db, test, "rejected")
@@ -434,6 +455,12 @@ def _dispatch_dual_validation_effects(
"Notification failed for test %s (rejected): %s",
test.id, e, exc_info=True,
)
if actor:
try:
from app.services.jira_service import push_test_event
push_test_event(db, test, actor, "rejected")
except Exception as e:
logger.warning("Jira push failed for test %s: %s", test.id, e, exc_info=True)
def handle_remediation_completed(db: Session, test: Test, user: User) -> Test | None:
@@ -588,4 +615,10 @@ def reopen_test(db: Session, test: Test, user: User) -> Test:
test.red_paused_seconds = 0
test.blue_paused_seconds = 0
try:
from app.services.jira_service import push_test_event
push_test_event(db, test, user, "draft")
except Exception as e:
logger.warning("Jira push failed for test %s: %s", test.id, e, exc_info=True)
return test