feat(jira): per-user auth, lifecycle hooks, admin config endpoints
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user