"use client"; import { Send } from "lucide-react"; import Link from "next/link"; import { useEffect, useState } from "react"; import { getIssue, getIssueFindings } from "@/actions/issues"; import { IssueCompliance } from "@/components/issues/issue-compliance"; import { IssueFindings } from "@/components/issues/issue-findings"; import { IssueGraph } from "@/components/issues/issue-graph"; import { IssueTriage } from "@/components/issues/issue-triage"; import { IssuesLoadError } from "@/components/issues/issues-load-error"; import { Badge } from "@/components/shadcn/badge/badge"; import { Button } from "@/components/shadcn/button/button"; import { Card } from "@/components/shadcn/card/card"; import { CodeSnippet } from "@/components/shadcn/code-snippet"; import { InfoField } from "@/components/shadcn/info-field/info-field"; import { riskFactors } from "@/lib/issues/risk-factors"; import { legBadgeVariant } from "@/lib/issues/verdicts"; import { createJiraDispatchPayload } from "@/lib/jira-dispatch-selection"; import { useJiraDispatchStore } from "@/store/jira-dispatch/store"; import { JIRA_DISPATCH_TARGET } from "@/types/integrations"; import { ATTACKER_START_LABEL, ISSUE_FINDINGS_LIMIT, ISSUE_OUTCOME_LABEL, IssueAttributes, IssueFinding, LegName, LegState, } from "@/types/issues"; const LEG_QUESTIONS: Record = { reachability: "Can an attacker reach it?", access: "Can an attacker authenticate or bypass authentication?", target: "Is there a target worth reaching?", controls: "Are protective controls in place?", verification: "Would the activity be detected?", }; const LEG_LABELS: Record = { reachability: "Reachability", access: "Access", target: "Target", controls: "Controls", verification: "Verification", }; const STATE_LABELS: Record = { yes: "Yes", no: "No", partial: "Partly", unknown: "Unknown", }; const RESOURCE_PAGE_SIZE = 12; // The issue carries resource uids, not ids, so this opens the Resources list // on that one resource rather than its drawer. const resourceHref = (uid: string, providerId?: string) => `/resources?${new URLSearchParams({ "filter[uid]": uid, // A uid is unique within an account, not across them. ...(providerId && { "filter[provider_id__in]": providerId }), })}`; interface IssueDetailProps { id: string; issue: IssueAttributes; scanId?: string; providerId?: string; } type ResourcesState = | { status: "loading" } | { status: "error" } | { status: "loaded"; resources: string[]; findings: IssueFinding[] | undefined; /** `getIssueFindings` capped the preview at ISSUE_FINDINGS_LIMIT. */ findingsTruncated: boolean; scanId?: string; providerId?: string; }; export const IssueDetail = ({ id, issue, scanId, providerId, }: IssueDetailProps) => { const openJiraDispatch = useJiraDispatchStore( (state) => state.openJiraDispatch, ); const [visibleResources, setVisibleResources] = useState(RESOURCE_PAGE_SIZE); const [attempt, setAttempt] = useState(0); // Resource UIDs are only included by the detail endpoint, fetched on expansion. const [state, setState] = useState({ status: "loading" }); useEffect(() => { let cancelled = false; getIssue(id) .then(async (detail) => { if (cancelled) return; const attributes = detail?.data?.attributes; if (!attributes?.resource_uids) { setState({ status: "error" }); return; } const detailScanId = detail?.data.relationships?.scan?.data?.id ?? scanId; const detailProviderId = detail?.data.relationships?.provider?.data?.id ?? providerId; const findingUids = attributes.finding_uids ?? []; const findings = detailScanId ? await getIssueFindings(detailScanId, findingUids) : findingUids.length === 0 ? [] : undefined; if (!cancelled) setState({ status: "loaded", resources: attributes.resource_uids, findings, findingsTruncated: new Set(findingUids).size > ISSUE_FINDINGS_LIMIT, scanId: detailScanId, providerId: detailProviderId, }); }) .catch(() => { if (!cancelled) setState({ status: "error" }); }); return () => { cancelled = true; }; }, [id, scanId, providerId, attempt]); const retry = () => { setState({ status: "loading" }); setAttempt((value) => value + 1); }; const activeScanId = state.status === "loaded" ? state.scanId : scanId; const factors = riskFactors(issue); const uniqueFindings = state.status === "loaded" ? Array.from( new Map( state.findings?.map((finding) => [finding.id, finding]), ).values(), ) : []; const jiraPayload = createJiraDispatchPayload({ findingTitle: issue.title, targetIds: uniqueFindings.map((finding) => finding.id), targetType: JIRA_DISPATCH_TARGET.FINDING_ID, description: `Send ${uniqueFindings.length} of ${issue.findings_count} findings on this attack path.`, }); return (
{/* The conditions that hold at once: the verdict says how bad, these say why, and which part to argue with. */} {factors.length > 0 && (
    {factors.map((factor) => (
  • {factor.label}
  • ))}
)} {state.status === "loaded" && state.findings === undefined && ( )} {state.status === "loaded" && state.findings !== undefined && state.resources.length > 0 && ( )} {issue.outcome && issue.attacker_start && (
{ISSUE_OUTCOME_LABEL[issue.outcome]} {/* Inline, not an InfoTooltip: its trigger has no tab stop. */} {ATTACKER_START_LABEL[issue.attacker_start]}

Where the attacker has to be standing. Whether they also need a credential is the Access leg.

)}

Exploitability assessment

{(Object.keys(LEG_QUESTIONS) as LegName[]).map((name) => { const leg = issue.legs[name]; if (!leg) return null; return (
{LEG_LABELS[name]}
{STATE_LABELS[leg.state]}

{LEG_QUESTIONS[name]}

{leg.text}

{leg.evidence.length > 0 && (
    {leg.evidence.map((line) => (
  • ))}
)}
); })}
{state.status === "loading" ? (

Loading issue details…

) : state.status === "error" ? ( ) : state.resources.length > 0 ? (

Affected resources ({state.resources.length})

    {state.resources.slice(0, visibleResources).map((uid) => (
  • ))}
{visibleResources < state.resources.length && ( )} {visibleResources > RESOURCE_PAGE_SIZE && ( )}
) : null}

Remediation

{issue.fix}

{activeScanId && ( )} {jiraPayload && ( // shortcut: the ticket groups the loaded preview (ISSUE_FINDINGS_LIMIT // findings), not every finding of a larger issue; page through // finding_uids if tickets for big issues need all of them. )}
{state.status === "loaded" && state.findings && state.findings.length > 0 && ( )} {state.status === "loaded" && state.findings && state.findings.length > 0 && ( )}
); };