"use client"; import { useEffect, useState } from "react"; import { getFindingComplianceFrameworks } from "@/actions/findings/finding-compliance-frameworks"; import { buildComplianceDetailHref, ComplianceFrameworkChip, complianceFrameworkLabel, } from "@/components/compliance/compliance-framework-chip"; import { IssuesLoadError } from "@/components/issues/issues-load-error"; import type { FindingComplianceFramework } from "@/types/compliance-watchlist"; import { IssueFinding } from "@/types/issues"; // shortcut: frameworks are looked up for at most this many distinct checks, // one request each; add an issue-level endpoint if issues fold more checks. export const COMPLIANCE_LOOKUP_LIMIT = 6; type ComplianceState = | { status: "loading" } | { status: "unavailable" } | { status: "loaded"; frameworks: FindingComplianceFramework[]; /** Some lookups failed, so frameworks may be missing. */ partial: boolean; }; interface IssueComplianceProps { findings: IssueFinding[]; scanId?: string; /** `findings` is a capped preview of a larger issue. */ truncated?: boolean; } /** * The frameworks the issue puts at risk, from Prowler's own per-finding * mapping. Frameworks belong to the check, so one finding per check answers * for all of them. */ export const IssueCompliance = ({ findings, scanId, truncated = false, }: IssueComplianceProps) => { const [state, setState] = useState({ status: "loading" }); const [attempt, setAttempt] = useState(0); const checks = new Map(findings.map((f) => [f.checkId, f.id])); const representatives = Array.from(checks.values()).slice( 0, COMPLIANCE_LOOKUP_LIMIT, ); const key = representatives.join(","); useEffect(() => { let cancelled = false; Promise.all( key .split(",") .filter(Boolean) .map((id) => getFindingComplianceFrameworks(id)), ) .then((results) => { if (cancelled) return; if (results.length > 0 && results.every((r) => r.unavailable)) { setState({ status: "unavailable" }); return; } const byId = new Map( results.flatMap((r) => r.frameworks).map((f) => [f.complianceId, f]), ); // Watchlist first: a public bucket maps into dozens of frameworks, and // the ones this organization reports on should not be buried. const frameworks = Array.from(byId.values()).sort( (a, b) => Number(b.inWatchlist) - Number(a.inWatchlist) || complianceFrameworkLabel(a).localeCompare( complianceFrameworkLabel(b), ), ); setState({ status: "loaded", frameworks, partial: results.some((r) => r.unavailable), }); }) .catch(() => { if (!cancelled) setState({ status: "unavailable" }); }); return () => { cancelled = true; }; }, [key, attempt]); const retry = () => { setState({ status: "loading" }); setAttempt((value) => value + 1); }; const incomplete = state.status === "loaded" && (state.partial || truncated || checks.size > COMPLIANCE_LOOKUP_LIMIT); const open = (framework: FindingComplianceFramework) => { if (!scanId) return; window.open( buildComplianceDetailHref({ complianceId: framework.complianceId, framework: framework.framework || framework.name, version: framework.version, scanId, regionFilter: null, }), "_blank", ); }; return (

Compliance affected

{state.status === "loading" ? (

Loading frameworks…

) : state.status === "unavailable" ? ( ) : ( <> {state.frameworks.length === 0 ? (

{incomplete ? "None of the findings checked map to a framework requirement." : "None of the findings behind this issue map to a framework requirement."}

) : (
{state.frameworks.map((framework) => ( ))}
)} {incomplete && (

Not every finding could be checked, so this list may be incomplete.

)} )}
); };