Files
prowler/ui/actions/findings/findings-by-resource.adapter.ts
T

336 lines
11 KiB
TypeScript

import { adaptFindingTriageSummariesResponse } from "@/actions/findings/findings-triage.adapter";
import { getFindingTriageAdapterOptions } from "@/actions/findings/findings-triage.options";
import { createDict } from "@/lib";
import type { ProviderType, Severity } from "@/types";
import type { FindingTriageSummary } from "@/types/findings-triage";
export interface RemediationRecommendation {
text: string;
url: string;
}
export interface RemediationCode {
cli: string;
other: string;
nativeiac: string;
terraform: string;
}
export interface Remediation {
recommendation: RemediationRecommendation;
code: RemediationCode;
}
export interface ScanInfo {
id: string;
name: string;
trigger: string;
state: string;
uniqueResourceCount: number;
progress: number;
duration: number;
startedAt: string | null;
completedAt: string | null;
insertedAt: string | null;
scheduledAt: string | null;
}
/**
* Flattened finding for the resource detail drawer.
* Merges data from the finding attributes, its check_metadata,
* the included resource, and the included scan/provider.
*/
export interface ResourceDrawerFinding {
id: string;
uid: string;
checkId: string;
checkTitle: string;
status: string;
severity: Severity;
delta: string | null;
isMuted: boolean;
mutedReason: string | null;
firstSeenAt: string | null;
updatedAt: string | null;
triage?: FindingTriageSummary;
// Resource
resourceId: string;
resourceUid: string;
resourceName: string;
resourceService: string;
resourceRegion: string;
resourceType: string;
resourceGroup: string;
resourceDetails: string | null;
resourceMetadata: Record<string, unknown> | string | null;
// Provider
providerId: string;
providerType: ProviderType;
providerAlias: string;
providerUid: string;
// Check metadata (flattened)
risk: string;
description: string;
statusExtended: string;
complianceFrameworks: string[];
categories: string[];
remediation: Remediation;
additionalUrls: string[];
// Scan
scan: ScanInfo | null;
}
/**
* Extracts unique compliance framework names from available data.
*
* Supports three shapes:
* 1a. check_metadata.compliance — array of { Framework, Version, ... } objects
* e.g. [{ Framework: "CIS-AWS", Version: "1.4" }, { Framework: "PCI-DSS" }]
* 1b. check_metadata.compliance — dict with framework keys and control arrays
* e.g. {"CIS-1.4": ["1.6"], "GDPR": ["article_25"], "HIPAA": ["164_312_d"]}
* 2. finding.compliance — dict with versioned keys (when API exposes it)
* e.g. {"CIS-AWS-1.4": ["2.1"], "PCI-DSS-3.2": ["6.2"]}
*/
function extractComplianceFrameworks(
metaCompliance: unknown,
findingCompliance: Record<string, string[]> | null | undefined,
): string[] {
const frameworks = new Set<string>();
// Source 1a: check_metadata.compliance — array of objects with Framework field
if (Array.isArray(metaCompliance)) {
for (const entry of metaCompliance) {
if (entry?.Framework || entry?.framework) {
frameworks.add(entry.Framework || entry.framework);
}
}
}
// Source 1b: check_metadata.compliance — dict keyed by framework name
else if (metaCompliance && typeof metaCompliance === "object") {
for (const key of Object.keys(metaCompliance as Record<string, unknown>)) {
const base = key.replace(/-\d+(\.\d+)*$/, "");
frameworks.add(base);
}
}
// Source 2: finding.compliance — dict keys like "CIS-AWS-1.4"
if (findingCompliance && typeof findingCompliance === "object") {
for (const key of Object.keys(findingCompliance)) {
const base = key.replace(/-\d+(\.\d+)*$/, "");
frameworks.add(base);
}
}
return Array.from(frameworks).sort((a, b) =>
a.localeCompare(b, undefined, { sensitivity: "base" }),
);
}
/**
* Internal shape of a finding item returned by the
* `/findings/latest?include=resources,scan.provider` endpoint.
*/
interface FindingApiAttributes {
uid: string;
check_id: string;
status: string;
severity: string;
delta?: string | null;
muted?: boolean;
muted_reason?: string | null;
first_seen_at?: string | null;
updated_at?: string | null;
status_extended?: string;
compliance?: Record<string, string[]>;
check_metadata?: Record<string, unknown>;
}
interface FindingApiItem {
id: string;
attributes: FindingApiAttributes;
relationships?: {
resources?: { data?: Array<{ id: string }> };
scan?: { data?: { id: string } | null };
};
}
/** Shape of an included JSON:API resource/scan/provider entry returned by createDict. */
interface IncludedItem {
id?: string;
attributes?: Record<string, unknown>;
relationships?: Record<string, unknown>;
}
/** Lookup dict returned by createDict(). */
type IncludedDict = Record<string, IncludedItem>;
/**
* Transforms the `/findings/latest?include=resources,scan.provider` response
* into a flat ResourceDrawerFinding array.
*
* Uses createDict to build lookup maps from the JSON:API `included` array,
* then resolves each finding's resource and provider relationships.
*/
interface JsonApiResponse {
data: FindingApiItem | FindingApiItem[];
included?: Record<string, unknown>[];
}
function isJsonApiResponse(value: unknown): value is JsonApiResponse {
const data = (value as { data?: unknown })?.data;
return (
value !== null &&
typeof value === "object" &&
"data" in value &&
(Array.isArray(data) || (data !== null && typeof data === "object"))
);
}
export function adaptFindingsByResourceResponse(
apiResponse: unknown,
): ResourceDrawerFinding[] {
if (!isJsonApiResponse(apiResponse)) {
return [];
}
const resourcesDict = createDict("resources", apiResponse) as IncludedDict;
const scansDict = createDict("scans", apiResponse) as IncludedDict;
const providersDict = createDict("providers", apiResponse) as IncludedDict;
const findings = Array.isArray(apiResponse.data)
? apiResponse.data
: [apiResponse.data];
const triageSummaries = adaptFindingTriageSummariesResponse(
{ ...apiResponse, data: findings },
getFindingTriageAdapterOptions(),
);
return findings.map((item, index) => {
const attrs = item.attributes;
const meta = (attrs.check_metadata || {}) as Record<string, unknown>;
const remediationRaw = meta.remediation as
| Record<string, unknown>
| undefined;
const remediation = remediationRaw || {
recommendation: { text: "", url: "" },
code: { cli: "", other: "", nativeiac: "", terraform: "" },
};
// Resolve resource from included
const resourceRel = item.relationships?.resources?.data?.[0];
const resource: IncludedItem | null = resourceRel
? (resourcesDict[resourceRel.id] ?? null)
: null;
const resourceAttrs = (resource?.attributes || {}) as Record<
string,
unknown
>;
// Resolve provider via scan → provider (include path: scan.provider)
const scanRel = item.relationships?.scan?.data;
const scan: IncludedItem | null = scanRel
? (scansDict[scanRel.id] ?? null)
: null;
const scanRels = scan?.relationships as Record<string, unknown> | undefined;
const providerRelId =
((
(scanRels?.provider as Record<string, unknown> | undefined)?.data as
| Record<string, unknown>
| undefined
)?.id as string | null) ?? null;
const provider: IncludedItem | null = providerRelId
? (providersDict[providerRelId] ?? null)
: null;
const providerAttrs = (provider?.attributes || {}) as Record<
string,
unknown
>;
const remRec = remediation.recommendation as
| Record<string, unknown>
| undefined;
const remCode = remediation.code as Record<string, unknown> | undefined;
return {
id: item.id,
uid: attrs.uid,
checkId: attrs.check_id,
checkTitle: (meta.checktitle as string | undefined) || attrs.check_id,
status: attrs.status,
severity: (attrs.severity || "informational") as Severity,
delta: attrs.delta || null,
isMuted: Boolean(attrs.muted),
mutedReason: attrs.muted_reason || null,
firstSeenAt: attrs.first_seen_at || null,
updatedAt: attrs.updated_at || null,
triage: triageSummaries[index],
// Resource
resourceId: resourceRel?.id || "",
resourceUid: (resourceAttrs.uid as string | undefined) || "-",
resourceName: (resourceAttrs.name as string | undefined) || "-",
resourceService: (resourceAttrs.service as string | undefined) || "-",
resourceRegion: (resourceAttrs.region as string | undefined) || "-",
resourceType: (resourceAttrs.type as string | undefined) || "-",
resourceGroup: (meta.resourcegroup as string | undefined) || "-",
resourceDetails:
(resourceAttrs.details as string | null | undefined) ?? null,
resourceMetadata:
(resourceAttrs.metadata as
| Record<string, unknown>
| string
| null
| undefined) ?? null,
// Provider
providerId: providerRelId ?? "",
providerType: ((providerAttrs.provider as string | undefined) ||
"aws") as ProviderType,
providerAlias: (providerAttrs.alias as string | undefined) || "",
providerUid: (providerAttrs.uid as string | undefined) || "",
// Check metadata
risk: (meta.risk as string | undefined) || "",
description: (meta.description as string | undefined) || "",
statusExtended: attrs.status_extended || "",
complianceFrameworks: extractComplianceFrameworks(
(meta.compliance ?? meta.Compliance) as unknown,
attrs.compliance,
),
categories: (meta.categories as string[] | undefined) || [],
remediation: {
recommendation: {
text: (remRec?.text as string | undefined) || "",
url: (remRec?.url as string | undefined) || "",
},
code: {
cli: (remCode?.cli as string | undefined) || "",
other: (remCode?.other as string | undefined) || "",
nativeiac: (remCode?.nativeiac as string | undefined) || "",
terraform: (remCode?.terraform as string | undefined) || "",
},
},
additionalUrls: (meta.additionalurls as string[] | undefined) || [],
// Scan
scan: scan?.attributes
? {
id: (scan.id as string | undefined) || "",
name: (scan.attributes.name as string | undefined) || "",
trigger: (scan.attributes.trigger as string | undefined) || "",
state: (scan.attributes.state as string | undefined) || "",
uniqueResourceCount:
(scan.attributes.unique_resource_count as number | undefined) ||
0,
progress: (scan.attributes.progress as number | undefined) || 0,
duration: (scan.attributes.duration as number | undefined) || 0,
startedAt:
(scan.attributes.started_at as string | undefined) || null,
completedAt:
(scan.attributes.completed_at as string | undefined) || null,
insertedAt:
(scan.attributes.inserted_at as string | undefined) || null,
scheduledAt:
(scan.attributes.scheduled_at as string | undefined) || null,
}
: null,
};
});
}