diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 7003bdeae..4dc403cb5 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -2,7 +2,7 @@ name: Docker on: push: - branches: [main] + branches: [incident-reports-docx] jobs: build-backend: @@ -27,7 +27,7 @@ jobs: with: context: ./backend push: true - tags: ghcr.io/socfortress/copilot-backend:latest + tags: ghcr.io/socfortress/copilot-backend:lab build-args: | COPILOT_API_KEY=${{ secrets.COPILOT_API_KEY }} @@ -60,7 +60,7 @@ jobs: with: context: ./frontend push: true - tags: ghcr.io/socfortress/copilot-frontend:latest + tags: ghcr.io/socfortress/copilot-frontend:lab - name: Notify Discord uses: appleboy/discord-action@v1.0.0 diff --git a/backend/app/incidents/routes/incident_report.py b/backend/app/incidents/routes/incident_report.py index 4fc2c578e..3f6963faf 100644 --- a/backend/app/incidents/routes/incident_report.py +++ b/backend/app/incidents/routes/incident_report.py @@ -17,7 +17,7 @@ from app.customers.routes.customers import get_customer from app.db.db_session import get_db from app.incidents.models import Alert, Asset -#from app.incidents.services.reports import download_template, create_case_context, save_template_to_tempfile, render_document_with_context, create_file_response, cleanup_temp_files +from app.incidents.services.reports import download_template, create_case_context, save_template_to_tempfile, render_document_with_context, create_file_response, cleanup_temp_files from app.incidents.models import AlertToTag from app.incidents.models import Case from app.incidents.models import CaseAlertLink @@ -168,29 +168,29 @@ async def get_cases_export_customer_route( return response -# @incidents_report_router.post( -# "/generate-report-docx", -# description="Generate a docx report for a case.", -# ) -# async def get_cases_export_docx_route( -# template_name: str, -# case_id: int, -# session: AsyncSession = Depends(get_db), -# ) -> FileResponse: -# case = await fetch_case_by_id(session, case_id) -# if not case: -# raise HTTPException(status_code=404, detail="No cases found") +@incidents_report_router.post( + "/generate-report-docx", + description="Generate a docx report for a case.", +) +async def get_cases_export_docx_route( + template_name: str, + case_id: int, + session: AsyncSession = Depends(get_db), +) -> FileResponse: + case = await fetch_case_by_id(session, case_id) + if not case: + raise HTTPException(status_code=404, detail="No cases found") -# context = create_case_context(case) + context = create_case_context(case) -# template_file_content = await download_template(template_name) -# tmp_template_name = save_template_to_tempfile(template_file_content) + template_file_content = await download_template(template_name) + tmp_template_name = save_template_to_tempfile(template_file_content) -# rendered_file_name = render_document_with_context(tmp_template_name, context) + rendered_file_name = render_document_with_context(tmp_template_name, context) -# response = create_file_response(rendered_file_name) + response = create_file_response(rendered_file_name) -# # Clean up temporary files -# cleanup_temp_files([tmp_template_name]) + # Clean up temporary files + cleanup_temp_files([tmp_template_name]) -# return response + return response diff --git a/backend/app/incidents/services/reports.py b/backend/app/incidents/services/reports.py index 9f51da356..0a5fefbf6 100644 --- a/backend/app/incidents/services/reports.py +++ b/backend/app/incidents/services/reports.py @@ -1,81 +1,81 @@ -# from typing import Dict -# from fastapi.responses import FileResponse -# from tempfile import NamedTemporaryFile -# import os -# from docxtpl import DocxTemplate -# from app.data_store.data_store_operations import download_data_store -# from loguru import logger +from typing import Dict +from fastapi.responses import FileResponse +from tempfile import NamedTemporaryFile +import os +from docxtpl import DocxTemplate +from app.data_store.data_store_operations import download_data_store +from loguru import logger -# async def download_template(template_name: str) -> bytes: -# """Retrieve the template file content from the data store.""" -# return await download_data_store(bucket_name="copilot-case-report-templates", object_name=template_name) +async def download_template(template_name: str) -> bytes: + """Retrieve the template file content from the data store.""" + return await download_data_store(bucket_name="copilot-case-report-templates", object_name=template_name) -# def create_case_context(case) -> Dict[str, Dict[str, str]]: -# """Prepare the context for the Jinja template.""" -# return { -# "case": { -# "name": case.case_name, -# "description": case.case_description, -# "assigned_to": case.assigned_to, -# "case_creation_time": case.case_creation_time, -# "id": case.id, -# "alerts": [ -# { -# "alert_name": alert.alert.alert_name, -# "alert_description": alert.alert.alert_description, -# "status": alert.alert.status, -# "tags": [tag.tag.tag for tag in alert.alert.tags], -# "assets": [ -# { -# "asset_name": asset.asset_name, -# "agent_id": asset.agent_id, -# } -# for asset in alert.alert.assets -# ], -# "comments": [ -# { -# "comment": comment.comment, -# "user_name": comment.user_name, -# "created_at": comment.created_at, -# } -# for comment in alert.alert.comments -# ], -# "context": { -# "source": alert.alert.assets[0].alert_context.source if alert.alert.assets and alert.alert.assets[0].alert_context else None, -# "context": alert.alert.assets[0].alert_context.context if alert.alert.assets and alert.alert.assets[0].alert_context else None, -# } if alert.alert.assets else None -# } -# for alert in case.alerts -# ] -# } -# } +def create_case_context(case) -> Dict[str, Dict[str, str]]: + """Prepare the context for the Jinja template.""" + return { + "case": { + "name": case.case_name, + "description": case.case_description, + "assigned_to": case.assigned_to, + "case_creation_time": case.case_creation_time, + "id": case.id, + "alerts": [ + { + "alert_name": alert.alert.alert_name, + "alert_description": alert.alert.alert_description, + "status": alert.alert.status, + "tags": [tag.tag.tag for tag in alert.alert.tags], + "assets": [ + { + "asset_name": asset.asset_name, + "agent_id": asset.agent_id, + } + for asset in alert.alert.assets + ], + "comments": [ + { + "comment": comment.comment, + "user_name": comment.user_name, + "created_at": comment.created_at, + } + for comment in alert.alert.comments + ], + "context": { + "source": alert.alert.assets[0].alert_context.source if alert.alert.assets and alert.alert.assets[0].alert_context else None, + "context": alert.alert.assets[0].alert_context.context if alert.alert.assets and alert.alert.assets[0].alert_context else None, + } if alert.alert.assets else None + } + for alert in case.alerts + ] + } + } -# def save_template_to_tempfile(template_file_content: bytes) -> str: -# """Save the template content to a temporary file.""" -# with NamedTemporaryFile(delete=False, suffix=".docx") as tmp_template: -# tmp_template.write(template_file_content) -# return tmp_template.name +def save_template_to_tempfile(template_file_content: bytes) -> str: + """Save the template content to a temporary file.""" + with NamedTemporaryFile(delete=False, suffix=".docx") as tmp_template: + tmp_template.write(template_file_content) + return tmp_template.name -# def render_document_with_context(template_path: str, context: Dict[str, Dict[str, str]]) -> str: -# """Load and render the document template with the given context.""" -# doc = DocxTemplate(template_path) -# doc.render(context) -# with NamedTemporaryFile(delete=False, suffix=".docx") as tmp: -# doc.save(tmp.name) -# return tmp.name +def render_document_with_context(template_path: str, context: Dict[str, Dict[str, str]]) -> str: + """Load and render the document template with the given context.""" + doc = DocxTemplate(template_path) + doc.render(context) + with NamedTemporaryFile(delete=False, suffix=".docx") as tmp: + doc.save(tmp.name) + return tmp.name -# def create_file_response(file_path: str) -> FileResponse: -# """Create a FileResponse object for the rendered document.""" -# return FileResponse( -# file_path, -# filename="case_report.docx", -# media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document", -# ) +def create_file_response(file_path: str) -> FileResponse: + """Create a FileResponse object for the rendered document.""" + return FileResponse( + file_path, + filename="case_report.docx", + media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document", + ) -# def cleanup_temp_files(file_paths: list): -# """Clean up the temporary files.""" -# for file_path in file_paths: -# if os.path.exists(file_path): -# os.remove(file_path) +def cleanup_temp_files(file_paths: list): + """Clean up the temporary files.""" + for file_path in file_paths: + if os.path.exists(file_path): + os.remove(file_path) diff --git a/backend/requirements.txt b/backend/requirements.txt index 946c14838..6f159a9f9 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -110,7 +110,7 @@ prompt-toolkit==3.0.39 protobuf==4.24.4 psycopg2-binary==2.9.9 pyasn1==0.5.0 -#docxtpl==0.18.0 Comment out for now +docxtpl==0.18.0 pycountry==22.3.5 pycparser==2.21 # pydantic==1.10.13