diff --git a/backend/app/services/jira_service.py b/backend/app/services/jira_service.py index 2e1fb2e..3e1634c 100644 --- a/backend/app/services/jira_service.py +++ b/backend/app/services/jira_service.py @@ -219,9 +219,6 @@ def _build_test_description(test: Test, technique: Optional[Technique]) -> str: "h3. Description", test.description or "_No description provided._", "", - "h3. Proof of Concept", - f"{{code}}{test.procedure_text or 'N/A'}{{code}}", - "", f"*Tool:* {test.tool_used or 'N/A'}", "", "----", diff --git a/backend/app/services/tempo_service.py b/backend/app/services/tempo_service.py index 7c36b69..a7e9568 100644 --- a/backend/app/services/tempo_service.py +++ b/backend/app/services/tempo_service.py @@ -69,21 +69,16 @@ def log_worklog( date: str, time_spent_seconds: int, description: str, - work_type: str | None = None, ) -> dict: """Create a worklog entry in Tempo using *user*'s personal token.""" tempo = get_user_tempo_client(user) - kwargs: dict = { - "accountId": author_account_id, - "issueId": jira_issue_id, - "dateFrom": date, - "timeSpentSeconds": time_spent_seconds, - "description": description, - } - wt = work_type or settings.TEMPO_DEFAULT_WORK_TYPE - if wt: - kwargs["workType"] = wt - return tempo.create_worklog(**kwargs) + return tempo.create_worklog( + accountId=author_account_id, + issueId=jira_issue_id, + dateFrom=date, + timeSpentSeconds=time_spent_seconds, + description=description, + ) def auto_log_test_worklog( diff --git a/backend/app/services/test_crud_service.py b/backend/app/services/test_crud_service.py index a5be075..3e8e65b 100644 --- a/backend/app/services/test_crud_service.py +++ b/backend/app/services/test_crud_service.py @@ -5,6 +5,7 @@ The router is responsible for HTTP concerns, auth, audit logging, and commit. """ import uuid +from datetime import datetime from typing import Any from sqlalchemy.orm import Session, joinedload @@ -78,6 +79,7 @@ def create_test( technique_id=technique_id, created_by=creator_id, state=TestState.draft, + created_at=datetime.utcnow(), # explicit — DB column has no server default **fields, ) db.add(test) @@ -127,6 +129,7 @@ def create_test_from_template( remediation_steps=template.suggested_remediation, created_by=creator_id, state=TestState.draft, + created_at=datetime.utcnow(), # explicit — DB column has no server default ) db.add(test) db.flush() diff --git a/frontend/src/components/TestPhaseTimeline.tsx b/frontend/src/components/TestPhaseTimeline.tsx new file mode 100644 index 0000000..983f1ea --- /dev/null +++ b/frontend/src/components/TestPhaseTimeline.tsx @@ -0,0 +1,246 @@ +/** + * TestPhaseTimeline + * + * Read-only timeline showing the automated phase durations for a test: + * 1. Red Team Execution (red_started_at → blue_started_at, minus paused) + * 2. Blue Queue (blue_started_at → blue_work_started_at) + * 3. Blue Evaluation (blue_work_started_at → …, open-ended until validated) + * 4. Red Lead Validation (red_validated_at + status) + * 5. Blue Lead Validation (blue_validated_at + status) + * + * Only phases with at least a start timestamp are rendered. + */ + +import { Clock, Sword, Shield, CheckCircle, XCircle, Timer } from "lucide-react"; +import type { Test } from "../types/models"; + +// ── Helpers ────────────────────────────────────────────────────────── + +/** Parse a backend datetime string (may lack 'Z') to a JS Date. */ +function parseDate(s: string): Date { + return new Date(s.endsWith("Z") ? s : s + "Z"); +} + +/** Compute duration in seconds between two timestamps, minus any paused seconds. */ +function durationSeconds(start: string, end: string, pausedSecs = 0): number { + const ms = parseDate(end).getTime() - parseDate(start).getTime(); + return Math.max(0, Math.floor(ms / 1000) - pausedSecs); +} + +/** Human-readable duration string. */ +function fmtDuration(secs: number): string { + if (secs < 60) return `${secs}s`; + if (secs < 3600) return `${Math.floor(secs / 60)}m`; + const h = Math.floor(secs / 3600); + const m = Math.floor((secs % 3600) / 60); + return m > 0 ? `${h}h ${m}m` : `${h}h`; +} + +/** Short date + time label. */ +function fmtTs(s: string): string { + return parseDate(s).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + }); +} + +// ── Phase row ───────────────────────────────────────────────────────── + +interface PhaseRowProps { + dotClass: string; + icon: React.ReactNode; + label: string; + badge?: React.ReactNode; + startTs: string | null; + duration?: number | null; // seconds; null → still running / unknown + isLast?: boolean; +} + +function PhaseRow({ dotClass, icon, label, badge, startTs, duration, isLast }: PhaseRowProps) { + return ( +
{fmtTs(startTs)}
+ )} +