"""Jira integration router — link, search, sync, create issues.""" import logging from typing import Optional from uuid import UUID from fastapi import APIRouter, Depends, Query from sqlalchemy.orm import Session from app.config import settings from app.database import get_db from app.dependencies.auth import get_current_user, require_role from app.domain.exceptions import EntityNotFoundError from app.models.jira_link import JiraLink, JiraLinkEntityType from app.models.test import Test from app.models.technique import Technique from app.models.campaign import Campaign from app.models.user import User from app.schemas.jira_schema import ( JiraIssueResult, JiraLinkCreate, JiraLinkOut, ) from app.services import jira_service, audit_service logger = logging.getLogger(__name__) router = APIRouter(prefix="/jira", tags=["jira"]) @router.get("/search", response_model=list[JiraIssueResult]) def search_issues( q: str = Query(..., min_length=2), max_results: int = Query(10, le=50), user: User = Depends(get_current_user), ): """Search Jira issues by JQL or free text.""" return jira_service.search_jira_issues(q, max_results) @router.post("/links", response_model=JiraLinkOut, status_code=201) def create_link( body: JiraLinkCreate, db: Session = Depends(get_db), user: User = Depends(get_current_user), ): """Associate an Aegis entity with a Jira issue.""" link = JiraLink( entity_type=body.entity_type, entity_id=body.entity_id, jira_issue_key=body.jira_issue_key, sync_direction=body.sync_direction, created_by=user.id, ) db.add(link) db.flush() # Pull initial data from Jira if enabled if settings.JIRA_ENABLED: try: jira_service.sync_jira_to_aegis(db, link) except Exception as e: logger.warning("Initial Jira sync failed for %s: %s", body.jira_issue_key, e) db.commit() db.refresh(link) audit_service.log_action( db, user_id=user.id, action="jira_link_created", entity_type="jira_link", entity_id=str(link.id), details={ "linked_entity_type": body.entity_type.value, "linked_entity_id": str(body.entity_id), "jira_issue_key": body.jira_issue_key, }, ) return link @router.get("/links", response_model=list[JiraLinkOut]) def list_links( entity_type: Optional[JiraLinkEntityType] = None, entity_id: Optional[UUID] = None, db: Session = Depends(get_db), user: User = Depends(get_current_user), ): """List Jira links, optionally filtered by entity.""" query = db.query(JiraLink) if entity_type: query = query.filter(JiraLink.entity_type == entity_type) if entity_id: query = query.filter(JiraLink.entity_id == entity_id) return query.order_by(JiraLink.created_at.desc()).all() @router.post("/links/{link_id}/sync") def sync_link( link_id: UUID, db: Session = Depends(get_db), user: User = Depends(require_role("admin")), ): """Force bidirectional sync for a specific Jira link.""" link = db.query(JiraLink).filter(JiraLink.id == link_id).first() if not link: raise EntityNotFoundError("JiraLink", str(link_id)) jira_service.sync_jira_to_aegis(db, link) db.commit() return {"message": "Sync completed", "jira_status": link.jira_status} @router.delete("/links/{link_id}", status_code=204) def delete_link( link_id: UUID, db: Session = Depends(get_db), user: User = Depends(get_current_user), ): """Remove a Jira link.""" link = db.query(JiraLink).filter(JiraLink.id == link_id).first() if not link: raise EntityNotFoundError("JiraLink", str(link_id)) db.delete(link) db.commit() audit_service.log_action( db, user_id=user.id, action="jira_link_deleted", entity_type="jira_link", entity_id=str(link_id), details={"jira_issue_key": link.jira_issue_key}, ) @router.post("/create-issue") def create_issue_from_entity( entity_type: JiraLinkEntityType, entity_id: UUID, db: Session = Depends(get_db), user: User = Depends(get_current_user), ): """Auto-create a Jira issue from an Aegis entity and link them.""" summary, description = _build_issue_data(db, entity_type, entity_id) result = jira_service.create_jira_issue( project_key=settings.JIRA_DEFAULT_PROJECT, summary=summary, description=description, labels=["aegis", entity_type.value], ) link = JiraLink( entity_type=entity_type, entity_id=entity_id, jira_issue_key=result["issue_key"], jira_issue_id=result["issue_id"], jira_project_key=settings.JIRA_DEFAULT_PROJECT, created_by=user.id, ) db.add(link) db.commit() return {"issue_key": result["issue_key"], "link_id": str(link.id)} def _build_issue_data( db: Session, entity_type: JiraLinkEntityType, entity_id: UUID, ) -> tuple[str, str]: """Build Jira issue summary + description from an Aegis entity.""" if entity_type == JiraLinkEntityType.test: entity = db.query(Test).filter(Test.id == entity_id).first() if not entity: raise EntityNotFoundError("Test", str(entity_id)) return ( f"[Aegis Test] {entity.name}", f"Test: {entity.name}\n" f"State: {entity.state.value if entity.state else 'draft'}\n" f"Description: {entity.description or 'N/A'}", ) elif entity_type == JiraLinkEntityType.campaign: entity = db.query(Campaign).filter(Campaign.id == entity_id).first() if not entity: raise EntityNotFoundError("Campaign", str(entity_id)) return ( f"[Aegis Campaign] {entity.name}", f"Campaign: {entity.name}\n" f"Type: {entity.type}\nStatus: {entity.status}\n" f"Description: {entity.description or 'N/A'}", ) elif entity_type == JiraLinkEntityType.technique: entity = db.query(Technique).filter(Technique.id == entity_id).first() if not entity: raise EntityNotFoundError("Technique", str(entity_id)) return ( f"[Aegis Technique] {entity.mitre_id} - {entity.name}", f"MITRE ID: {entity.mitre_id}\nName: {entity.name}\n" f"Tactic: {entity.tactic or 'N/A'}\n" f"Description: {entity.description or 'N/A'}", ) else: return f"[Aegis] Entity {entity_id}", f"Entity type: {entity_type.value}"