mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-09 21:14:22 +00:00
724 lines
21 KiB
TypeScript
724 lines
21 KiB
TypeScript
import {
|
|
ATTACK_PATH_QUERY_KIND,
|
|
type AttackPathGraphData,
|
|
type AttackPathQueryKind,
|
|
type GraphEdge,
|
|
} from "@/types/attack-paths";
|
|
import {
|
|
LIGHTHOUSE_CONTEXT_KIND,
|
|
LIGHTHOUSE_CONTEXT_LIMIT,
|
|
LIGHTHOUSE_CONTEXT_SOURCE,
|
|
type LighthouseAlertContextItem,
|
|
type LighthouseAttackPathContextItem,
|
|
type LighthouseAttackPathParameter,
|
|
type LighthouseComplianceContextItem,
|
|
type LighthouseFindingContextItem,
|
|
type LighthouseProviderContextItem,
|
|
type LighthouseResourceContextItem,
|
|
type LighthouseScanContextItem,
|
|
} from "@/types/lighthouse-context";
|
|
|
|
import type { LighthouseComplianceContextMode } from "./constants";
|
|
import {
|
|
containsSensitiveLighthouseContextValue,
|
|
getLighthouseScopeKey,
|
|
} from "./pages";
|
|
|
|
const ALERTS_SCOPE_KEY = getLighthouseScopeKey("/alerts");
|
|
const FINDINGS_SCOPE_KEY = getLighthouseScopeKey("/findings");
|
|
const RESOURCES_SCOPE_KEY = getLighthouseScopeKey("/resources");
|
|
const SCANS_SCOPE_KEY = getLighthouseScopeKey("/scans");
|
|
const PROVIDERS_SCOPE_KEY = getLighthouseScopeKey("/providers");
|
|
|
|
interface FindingGroupContextInput {
|
|
id: string;
|
|
checkId: string;
|
|
checkTitle: string;
|
|
severity: string;
|
|
status: string;
|
|
}
|
|
|
|
interface FocusedAlertContextInput {
|
|
id: string;
|
|
name?: string;
|
|
trigger?: string;
|
|
enabled?: boolean;
|
|
}
|
|
|
|
interface FindingStatusSummaryContextInput {
|
|
pathname: string;
|
|
passed: number;
|
|
failed: number;
|
|
newPassed?: number;
|
|
newFailed?: number;
|
|
}
|
|
|
|
interface FindingSeveritySummaryContextInput {
|
|
pathname: string;
|
|
severityCounts: Record<string, number>;
|
|
}
|
|
|
|
interface FindingResourceContextInput {
|
|
findingId: string;
|
|
checkId?: string;
|
|
severity?: string;
|
|
status?: string;
|
|
providerUid?: string;
|
|
resourceUid?: string;
|
|
region?: string;
|
|
}
|
|
|
|
interface FocusedFindingContextInput extends FindingResourceContextInput {
|
|
pathname: string;
|
|
}
|
|
|
|
interface ResourceContextAttributes {
|
|
uid: string;
|
|
service: string;
|
|
region: string;
|
|
type: string;
|
|
failed_findings_count: number;
|
|
}
|
|
|
|
interface ResourceContextInput {
|
|
id: string;
|
|
attributes: ResourceContextAttributes;
|
|
providerUid?: string;
|
|
}
|
|
|
|
interface FocusedResourceContextInput extends ResourceContextInput {
|
|
pathname: string;
|
|
}
|
|
|
|
interface ComplianceContextInput {
|
|
pathname: string;
|
|
id: string;
|
|
framework: string;
|
|
version?: string;
|
|
scanId?: string;
|
|
providerUid?: string;
|
|
mode?: LighthouseComplianceContextMode;
|
|
section?: string;
|
|
region?: string;
|
|
score?: number;
|
|
scoreDelta?: number;
|
|
criticalRequirementsCount?: number;
|
|
worstSection?: string;
|
|
worstSectionScore?: number;
|
|
passed?: number;
|
|
failed?: number;
|
|
total?: number;
|
|
}
|
|
|
|
interface AttackPathSelectedNodeInput {
|
|
id: string;
|
|
type?: string;
|
|
}
|
|
|
|
interface AttackPathContextInput {
|
|
pathname: string;
|
|
scanId: string;
|
|
queryId?: string | null;
|
|
queryLabel?: string;
|
|
queryKind?: AttackPathQueryKind;
|
|
parameters?: Record<string, string | number | boolean>;
|
|
graphData?: AttackPathGraphData | null;
|
|
nodeCount?: number;
|
|
edgeCount?: number;
|
|
selectedNode?: AttackPathSelectedNodeInput | null;
|
|
}
|
|
|
|
interface ScanContextInput {
|
|
id: string;
|
|
state?: string;
|
|
providerUid?: string;
|
|
}
|
|
|
|
interface ProviderContextInput {
|
|
id: string;
|
|
uid?: string;
|
|
type?: string;
|
|
}
|
|
|
|
interface FilteredProviderContextInput {
|
|
pathname: string;
|
|
id: string;
|
|
uid?: string;
|
|
type?: string;
|
|
alias?: string;
|
|
}
|
|
|
|
interface ProviderGroupContextInput {
|
|
pathname: string;
|
|
id: string;
|
|
name: string;
|
|
}
|
|
|
|
interface ServiceSummaryContextInput {
|
|
pathname: string;
|
|
service: string;
|
|
failedFindingsCount: number;
|
|
total?: number;
|
|
}
|
|
|
|
export function buildAlertSummaryContext(
|
|
total: number,
|
|
enabledCount?: number,
|
|
): LighthouseAlertContextItem {
|
|
const safeTotal = toSafeCount(total);
|
|
return {
|
|
kind: LIGHTHOUSE_CONTEXT_KIND.ALERT,
|
|
id: "summary",
|
|
source: LIGHTHOUSE_CONTEXT_SOURCE.AUTOMATIC,
|
|
scopeKey: ALERTS_SCOPE_KEY,
|
|
label: `${safeTotal} alert rules`,
|
|
total: safeTotal,
|
|
enabledCount: optionalSafeCount(enabledCount),
|
|
};
|
|
}
|
|
|
|
export function buildFocusedAlertContext(
|
|
input: FocusedAlertContextInput,
|
|
): LighthouseAlertContextItem {
|
|
const safeId = toBoundedString(input.id);
|
|
return {
|
|
kind: LIGHTHOUSE_CONTEXT_KIND.ALERT,
|
|
id: safeId,
|
|
source: LIGHTHOUSE_CONTEXT_SOURCE.FOCUSED,
|
|
scopeKey: ALERTS_SCOPE_KEY,
|
|
label: toBoundedString(input.name || "Edited alert rule"),
|
|
alertId: safeId,
|
|
trigger: optionalBoundedString(input.trigger),
|
|
enabled: input.enabled,
|
|
};
|
|
}
|
|
|
|
export function buildFindingSummaryContext(
|
|
total: number,
|
|
): LighthouseFindingContextItem {
|
|
const safeTotal = toSafeCount(total);
|
|
return {
|
|
kind: LIGHTHOUSE_CONTEXT_KIND.FINDING,
|
|
id: "summary",
|
|
source: LIGHTHOUSE_CONTEXT_SOURCE.AUTOMATIC,
|
|
scopeKey: FINDINGS_SCOPE_KEY,
|
|
label: `${safeTotal} findings`,
|
|
findingId: "summary",
|
|
total: safeTotal,
|
|
};
|
|
}
|
|
|
|
export function buildFindingStatusSummaryContext(
|
|
input: FindingStatusSummaryContextInput,
|
|
): LighthouseFindingContextItem {
|
|
const passed = toSafeCount(input.passed);
|
|
const failed = toSafeCount(input.failed);
|
|
return {
|
|
kind: LIGHTHOUSE_CONTEXT_KIND.FINDING,
|
|
id: "status-summary",
|
|
source: LIGHTHOUSE_CONTEXT_SOURCE.AUTOMATIC,
|
|
scopeKey: getLighthouseScopeKey(input.pathname),
|
|
label: `${failed} failed / ${passed} passed findings`,
|
|
findingId: "status-summary",
|
|
passed,
|
|
failed,
|
|
newPassed: optionalSafeCount(input.newPassed),
|
|
newFailed: optionalSafeCount(input.newFailed),
|
|
};
|
|
}
|
|
|
|
export function buildFindingSeveritySummaryContext(
|
|
input: FindingSeveritySummaryContextInput,
|
|
): LighthouseFindingContextItem {
|
|
const severityCounts = Object.fromEntries(
|
|
Object.entries(input.severityCounts)
|
|
.slice(0, LIGHTHOUSE_CONTEXT_LIMIT.SEVERITY_COUNTS)
|
|
.map(([severity, count]) => [
|
|
toBoundedString(severity),
|
|
toSafeCount(count),
|
|
]),
|
|
);
|
|
return {
|
|
kind: LIGHTHOUSE_CONTEXT_KIND.FINDING,
|
|
id: "severity-summary",
|
|
source: LIGHTHOUSE_CONTEXT_SOURCE.AUTOMATIC,
|
|
scopeKey: getLighthouseScopeKey(input.pathname),
|
|
label: "Failing findings by severity",
|
|
findingId: "severity-summary",
|
|
severityCounts,
|
|
};
|
|
}
|
|
|
|
export function buildFindingGroupContext(
|
|
group: FindingGroupContextInput,
|
|
): LighthouseFindingContextItem {
|
|
return {
|
|
kind: LIGHTHOUSE_CONTEXT_KIND.FINDING,
|
|
id: toBoundedString(group.id),
|
|
source: LIGHTHOUSE_CONTEXT_SOURCE.SELECTION,
|
|
scopeKey: FINDINGS_SCOPE_KEY,
|
|
label: toBoundedString(group.checkTitle),
|
|
findingId: toBoundedString(group.id),
|
|
checkId: toBoundedString(group.checkId),
|
|
severity: toBoundedString(group.severity),
|
|
status: toBoundedString(group.status),
|
|
};
|
|
}
|
|
|
|
export function buildFindingResourceContext(
|
|
finding: FindingResourceContextInput,
|
|
): LighthouseFindingContextItem {
|
|
const safeFindingId = toBoundedString(finding.findingId);
|
|
return {
|
|
kind: LIGHTHOUSE_CONTEXT_KIND.FINDING,
|
|
id: safeFindingId,
|
|
source: LIGHTHOUSE_CONTEXT_SOURCE.SELECTION,
|
|
scopeKey: FINDINGS_SCOPE_KEY,
|
|
label: "Selected finding",
|
|
findingId: safeFindingId,
|
|
checkId: optionalBoundedString(finding.checkId),
|
|
severity: optionalBoundedString(finding.severity),
|
|
status: optionalBoundedString(finding.status),
|
|
providerUid: optionalBoundedString(finding.providerUid),
|
|
resourceUid: optionalBoundedString(finding.resourceUid),
|
|
region: optionalBoundedString(finding.region),
|
|
};
|
|
}
|
|
|
|
export function buildFocusedFindingContext(
|
|
finding: FocusedFindingContextInput,
|
|
): LighthouseFindingContextItem {
|
|
const safeFindingId = toBoundedString(finding.findingId);
|
|
return {
|
|
kind: LIGHTHOUSE_CONTEXT_KIND.FINDING,
|
|
id: safeFindingId,
|
|
source: LIGHTHOUSE_CONTEXT_SOURCE.FOCUSED,
|
|
scopeKey: getLighthouseScopeKey(finding.pathname),
|
|
label: "Focused finding",
|
|
findingId: safeFindingId,
|
|
checkId: optionalBoundedString(finding.checkId),
|
|
severity: optionalBoundedString(finding.severity),
|
|
status: optionalBoundedString(finding.status),
|
|
providerUid: optionalBoundedString(finding.providerUid),
|
|
resourceUid: optionalBoundedString(finding.resourceUid),
|
|
region: optionalBoundedString(finding.region),
|
|
};
|
|
}
|
|
|
|
export function buildResourceSummaryContext(
|
|
total: number,
|
|
): LighthouseResourceContextItem {
|
|
const safeTotal = toSafeCount(total);
|
|
return {
|
|
kind: LIGHTHOUSE_CONTEXT_KIND.RESOURCE,
|
|
id: "summary",
|
|
source: LIGHTHOUSE_CONTEXT_SOURCE.AUTOMATIC,
|
|
scopeKey: RESOURCES_SCOPE_KEY,
|
|
label: `${safeTotal} resources`,
|
|
resourceId: "summary",
|
|
total: safeTotal,
|
|
};
|
|
}
|
|
|
|
export function buildResourceContext(
|
|
resource: ResourceContextInput,
|
|
): LighthouseResourceContextItem {
|
|
return {
|
|
kind: LIGHTHOUSE_CONTEXT_KIND.RESOURCE,
|
|
id: toBoundedString(resource.id),
|
|
source: LIGHTHOUSE_CONTEXT_SOURCE.SELECTION,
|
|
scopeKey: RESOURCES_SCOPE_KEY,
|
|
label: "Selected resource",
|
|
resourceId: toBoundedString(resource.id),
|
|
resourceUid: toBoundedString(resource.attributes.uid),
|
|
providerUid: optionalBoundedString(resource.providerUid),
|
|
service: toBoundedString(resource.attributes.service),
|
|
region: toBoundedString(resource.attributes.region),
|
|
resourceType: toBoundedString(resource.attributes.type),
|
|
failedFindingsCount: toSafeCount(resource.attributes.failed_findings_count),
|
|
};
|
|
}
|
|
|
|
export function buildFocusedResourceContext(
|
|
resource: FocusedResourceContextInput,
|
|
): LighthouseResourceContextItem {
|
|
return {
|
|
kind: LIGHTHOUSE_CONTEXT_KIND.RESOURCE,
|
|
id: toBoundedString(resource.id),
|
|
source: LIGHTHOUSE_CONTEXT_SOURCE.FOCUSED,
|
|
scopeKey: getLighthouseScopeKey(resource.pathname),
|
|
label: "Focused resource",
|
|
resourceId: toBoundedString(resource.id),
|
|
resourceUid: toBoundedString(resource.attributes.uid),
|
|
providerUid: optionalBoundedString(resource.providerUid),
|
|
service: toBoundedString(resource.attributes.service),
|
|
region: toBoundedString(resource.attributes.region),
|
|
resourceType: toBoundedString(resource.attributes.type),
|
|
failedFindingsCount: toSafeCount(resource.attributes.failed_findings_count),
|
|
};
|
|
}
|
|
|
|
export function buildServiceSummaryContext(
|
|
input: ServiceSummaryContextInput,
|
|
): LighthouseResourceContextItem {
|
|
const safeService = toBoundedString(input.service);
|
|
const safeId = toBoundedString(`service-${safeService}`);
|
|
return {
|
|
kind: LIGHTHOUSE_CONTEXT_KIND.RESOURCE,
|
|
id: safeId,
|
|
source: LIGHTHOUSE_CONTEXT_SOURCE.AUTOMATIC,
|
|
scopeKey: getLighthouseScopeKey(input.pathname),
|
|
label: toBoundedString(`Service: ${safeService}`),
|
|
resourceId: safeId,
|
|
service: safeService,
|
|
failedFindingsCount: toSafeCount(input.failedFindingsCount),
|
|
total: optionalSafeCount(input.total),
|
|
};
|
|
}
|
|
|
|
export function buildComplianceContext(
|
|
input: ComplianceContextInput,
|
|
): LighthouseComplianceContextItem {
|
|
const total = optionalSafeCount(input.total);
|
|
const passed = optionalSafeCount(input.passed);
|
|
const failed = optionalSafeCount(input.failed);
|
|
const score =
|
|
input.score !== undefined
|
|
? toSafeScore(input.score)
|
|
: total && passed !== undefined
|
|
? toSafeScore((passed / total) * 100)
|
|
: undefined;
|
|
const hasTotals =
|
|
passed !== undefined || failed !== undefined || total !== undefined;
|
|
|
|
return {
|
|
kind: LIGHTHOUSE_CONTEXT_KIND.COMPLIANCE,
|
|
id: toBoundedString(input.id),
|
|
source: LIGHTHOUSE_CONTEXT_SOURCE.AUTOMATIC,
|
|
scopeKey: getLighthouseScopeKey(input.pathname),
|
|
label: toBoundedString(input.framework),
|
|
framework: toBoundedString(input.framework),
|
|
version: optionalBoundedString(input.version),
|
|
scanId: optionalBoundedString(input.scanId),
|
|
providerUid: optionalBoundedString(input.providerUid),
|
|
mode: input.mode,
|
|
section: optionalBoundedString(input.section),
|
|
region: optionalBoundedString(input.region),
|
|
score,
|
|
scoreDelta: optionalSafeScoreDelta(input.scoreDelta),
|
|
criticalRequirementsCount: optionalSafeCount(
|
|
input.criticalRequirementsCount,
|
|
),
|
|
worstSection: optionalBoundedString(input.worstSection),
|
|
worstSectionScore:
|
|
input.worstSectionScore === undefined
|
|
? undefined
|
|
: toSafeScore(input.worstSectionScore),
|
|
totals: hasTotals ? { passed, failed, total } : undefined,
|
|
};
|
|
}
|
|
|
|
export function buildAttackPathContext(
|
|
input: AttackPathContextInput,
|
|
): LighthouseAttackPathContextItem {
|
|
const { parameters, redactedParameters } = sanitizeAttackPathParameters(
|
|
input.parameters,
|
|
);
|
|
const graphSummary = summarizeAttackPathGraph(input.graphData);
|
|
return {
|
|
kind: LIGHTHOUSE_CONTEXT_KIND.ATTACK_PATH,
|
|
id: input.queryId ? "current-query" : "current-scan",
|
|
source: LIGHTHOUSE_CONTEXT_SOURCE.AUTOMATIC,
|
|
scopeKey: getLighthouseScopeKey(input.pathname),
|
|
label: toBoundedString(input.queryLabel || "Selected attack-path scan"),
|
|
scanId: toBoundedString(input.scanId),
|
|
queryId: optionalBoundedString(input.queryId ?? undefined),
|
|
...(input.queryKind
|
|
? {
|
|
queryKind: input.queryKind,
|
|
canReplayQuery: getCanReplayAttackPathQuery(
|
|
input.queryKind,
|
|
redactedParameters,
|
|
),
|
|
}
|
|
: {}),
|
|
parameters: Object.keys(parameters).length > 0 ? parameters : undefined,
|
|
...(redactedParameters.length > 0 ? { redactedParameters } : {}),
|
|
nodeCount: graphSummary?.nodeCount ?? optionalSafeCount(input.nodeCount),
|
|
edgeCount: graphSummary?.edgeCount ?? optionalSafeCount(input.edgeCount),
|
|
...(graphSummary
|
|
? {
|
|
connectedComponentCount: graphSummary.connectedComponentCount,
|
|
nodeTypeCounts: graphSummary.nodeTypeCounts,
|
|
relationshipTypeCounts: graphSummary.relationshipTypeCounts,
|
|
}
|
|
: {}),
|
|
selectedNodeId: optionalBoundedString(input.selectedNode?.id),
|
|
selectedNodeType: optionalBoundedString(input.selectedNode?.type),
|
|
};
|
|
}
|
|
|
|
export function buildScanSummaryContext(
|
|
total: number,
|
|
state: string,
|
|
): LighthouseScanContextItem {
|
|
const safeTotal = toSafeCount(total);
|
|
const safeState = toBoundedString(state);
|
|
return {
|
|
kind: LIGHTHOUSE_CONTEXT_KIND.SCAN,
|
|
id: "summary",
|
|
source: LIGHTHOUSE_CONTEXT_SOURCE.AUTOMATIC,
|
|
scopeKey: SCANS_SCOPE_KEY,
|
|
label: `${safeTotal} ${safeState} scans`,
|
|
state: safeState,
|
|
total: safeTotal,
|
|
};
|
|
}
|
|
|
|
export function buildScanContext(
|
|
input: ScanContextInput,
|
|
): LighthouseScanContextItem {
|
|
return {
|
|
kind: LIGHTHOUSE_CONTEXT_KIND.SCAN,
|
|
id: toBoundedString(input.id),
|
|
source: LIGHTHOUSE_CONTEXT_SOURCE.SELECTION,
|
|
scopeKey: SCANS_SCOPE_KEY,
|
|
label: "Selected scan",
|
|
scanId: toBoundedString(input.id),
|
|
state: optionalBoundedString(input.state),
|
|
providerUid: optionalBoundedString(input.providerUid),
|
|
};
|
|
}
|
|
|
|
export function buildProviderSummaryContext(
|
|
total: number,
|
|
): LighthouseProviderContextItem {
|
|
const safeTotal = toSafeCount(total);
|
|
return {
|
|
kind: LIGHTHOUSE_CONTEXT_KIND.PROVIDER,
|
|
id: "summary",
|
|
source: LIGHTHOUSE_CONTEXT_SOURCE.AUTOMATIC,
|
|
scopeKey: PROVIDERS_SCOPE_KEY,
|
|
label: `${safeTotal} providers`,
|
|
total: safeTotal,
|
|
};
|
|
}
|
|
|
|
export function buildProviderContext(
|
|
input: ProviderContextInput,
|
|
): LighthouseProviderContextItem {
|
|
return {
|
|
kind: LIGHTHOUSE_CONTEXT_KIND.PROVIDER,
|
|
id: toBoundedString(input.id),
|
|
source: LIGHTHOUSE_CONTEXT_SOURCE.SELECTION,
|
|
scopeKey: PROVIDERS_SCOPE_KEY,
|
|
label: "Selected provider",
|
|
providerId: toBoundedString(input.id),
|
|
providerUid: optionalBoundedString(input.uid),
|
|
providerType: optionalBoundedString(input.type),
|
|
};
|
|
}
|
|
|
|
export function buildFilteredProviderContext(
|
|
input: FilteredProviderContextInput,
|
|
): LighthouseProviderContextItem {
|
|
const safeId = toBoundedString(input.id);
|
|
return {
|
|
kind: LIGHTHOUSE_CONTEXT_KIND.PROVIDER,
|
|
id: safeId,
|
|
source: LIGHTHOUSE_CONTEXT_SOURCE.AUTOMATIC,
|
|
scopeKey: getLighthouseScopeKey(input.pathname),
|
|
label: toBoundedString(
|
|
`Provider: ${input.alias || input.uid || "filtered"}`,
|
|
),
|
|
providerId: safeId,
|
|
providerUid: optionalBoundedString(input.uid),
|
|
providerType: optionalBoundedString(input.type),
|
|
};
|
|
}
|
|
|
|
export function buildProviderGroupContext(
|
|
input: ProviderGroupContextInput,
|
|
): LighthouseProviderContextItem {
|
|
return {
|
|
kind: LIGHTHOUSE_CONTEXT_KIND.PROVIDER,
|
|
id: toBoundedString(`group-${input.id}`),
|
|
source: LIGHTHOUSE_CONTEXT_SOURCE.AUTOMATIC,
|
|
scopeKey: getLighthouseScopeKey(input.pathname),
|
|
label: toBoundedString(`Provider group: ${input.name}`),
|
|
};
|
|
}
|
|
|
|
function toBoundedString(value: string): string {
|
|
return value.slice(0, LIGHTHOUSE_CONTEXT_LIMIT.STRING_LENGTH);
|
|
}
|
|
|
|
function optionalBoundedString(value: string | undefined): string | undefined {
|
|
return value ? toBoundedString(value) : undefined;
|
|
}
|
|
|
|
function toSafeCount(value: number): number {
|
|
return Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
|
|
}
|
|
|
|
function optionalSafeCount(value: number | undefined): number | undefined {
|
|
return value === undefined ? undefined : toSafeCount(value);
|
|
}
|
|
|
|
function toSafeScore(value: number): number {
|
|
if (!Number.isFinite(value)) return 0;
|
|
return Math.min(100, Math.max(0, Math.round(value * 100) / 100));
|
|
}
|
|
|
|
function optionalSafeScoreDelta(value: number | undefined): number | undefined {
|
|
if (value === undefined || !Number.isFinite(value)) return undefined;
|
|
return Math.min(100, Math.max(-100, Math.round(value * 100) / 100));
|
|
}
|
|
|
|
function sanitizeAttackPathParameters(
|
|
parameters: AttackPathContextInput["parameters"],
|
|
): SanitizedAttackPathParameters {
|
|
if (!parameters) return { parameters: {}, redactedParameters: [] };
|
|
|
|
const safeParameters: Record<string, LighthouseAttackPathParameter> = {};
|
|
const redactedParameters: string[] = [];
|
|
|
|
for (const [key, value] of Object.entries(parameters)) {
|
|
if (value === "") continue;
|
|
|
|
const isRedacted =
|
|
/password|secret|token|credential|query/i.test(key) ||
|
|
(typeof value === "string" &&
|
|
containsSensitiveLighthouseContextValue(value));
|
|
if (isRedacted) {
|
|
if (
|
|
redactedParameters.length <
|
|
LIGHTHOUSE_CONTEXT_LIMIT.ATTACK_PATH_REDACTED_PARAMETERS
|
|
) {
|
|
redactedParameters.push(toBoundedString(key));
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (
|
|
Object.keys(safeParameters).length >=
|
|
LIGHTHOUSE_CONTEXT_LIMIT.ATTACK_PATH_PARAMETERS
|
|
) {
|
|
continue;
|
|
}
|
|
safeParameters[toBoundedString(key)] =
|
|
typeof value === "string" ? toBoundedString(value) : value;
|
|
}
|
|
|
|
return {
|
|
parameters: safeParameters,
|
|
redactedParameters: redactedParameters.sort(),
|
|
};
|
|
}
|
|
|
|
interface SanitizedAttackPathParameters {
|
|
parameters: Record<string, LighthouseAttackPathParameter>;
|
|
redactedParameters: string[];
|
|
}
|
|
|
|
interface AttackPathGraphSummary {
|
|
nodeCount: number;
|
|
edgeCount: number;
|
|
connectedComponentCount: number;
|
|
nodeTypeCounts?: Record<string, number>;
|
|
relationshipTypeCounts?: Record<string, number>;
|
|
}
|
|
|
|
function getCanReplayAttackPathQuery(
|
|
queryKind: AttackPathQueryKind | undefined,
|
|
redactedParameters: string[],
|
|
): boolean | undefined {
|
|
if (!queryKind) return undefined;
|
|
return (
|
|
queryKind === ATTACK_PATH_QUERY_KIND.PREDEFINED &&
|
|
redactedParameters.length === 0
|
|
);
|
|
}
|
|
|
|
function summarizeAttackPathGraph(
|
|
graphData: AttackPathGraphData | null | undefined,
|
|
): AttackPathGraphSummary | undefined {
|
|
if (!graphData) return undefined;
|
|
|
|
const edges =
|
|
graphData.edges ??
|
|
graphData.relationships?.map((relationship) => ({
|
|
source: relationship.source,
|
|
target: relationship.target,
|
|
type: relationship.label,
|
|
})) ??
|
|
[];
|
|
|
|
return {
|
|
nodeCount: toSafeCount(graphData.nodes.length),
|
|
edgeCount: toSafeCount(edges.length),
|
|
connectedComponentCount: countConnectedComponents(graphData, edges),
|
|
nodeTypeCounts: buildBoundedTypeCounts(
|
|
graphData.nodes.map((node) => node.labels[0]).filter(Boolean),
|
|
),
|
|
relationshipTypeCounts: buildBoundedTypeCounts(
|
|
edges.map((edge) => edge.type).filter(Boolean),
|
|
),
|
|
};
|
|
}
|
|
|
|
function countConnectedComponents(
|
|
graphData: AttackPathGraphData,
|
|
edges: ReadonlyArray<Pick<GraphEdge, "source" | "target">>,
|
|
): number {
|
|
const nodeIdList = graphData.nodes.map((node) => node.id);
|
|
const nodeIds = new Set(nodeIdList);
|
|
const adjacency = new Map<string, Set<string>>(
|
|
nodeIdList.map((nodeId) => [nodeId, new Set<string>()]),
|
|
);
|
|
|
|
for (const edge of edges) {
|
|
if (!nodeIds.has(edge.source) || !nodeIds.has(edge.target)) continue;
|
|
adjacency.get(edge.source)?.add(edge.target);
|
|
adjacency.get(edge.target)?.add(edge.source);
|
|
}
|
|
|
|
const visited = new Set<string>();
|
|
let componentCount = 0;
|
|
for (const nodeId of nodeIdList) {
|
|
if (visited.has(nodeId)) continue;
|
|
componentCount += 1;
|
|
const pending = [nodeId];
|
|
while (pending.length > 0) {
|
|
const current = pending.pop();
|
|
if (!current || visited.has(current)) continue;
|
|
visited.add(current);
|
|
adjacency.get(current)?.forEach((neighbor) => {
|
|
if (!visited.has(neighbor)) pending.push(neighbor);
|
|
});
|
|
}
|
|
}
|
|
|
|
return componentCount;
|
|
}
|
|
|
|
function buildBoundedTypeCounts(
|
|
values: string[],
|
|
): Record<string, number> | undefined {
|
|
const counts = values.reduce<Record<string, number>>((result, value) => {
|
|
const boundedValue = toBoundedString(value);
|
|
result[boundedValue] = (result[boundedValue] ?? 0) + 1;
|
|
return result;
|
|
}, {});
|
|
const boundedCounts = Object.fromEntries(
|
|
Object.entries(counts)
|
|
.sort(
|
|
([leftLabel, leftCount], [rightLabel, rightCount]) =>
|
|
rightCount - leftCount || leftLabel.localeCompare(rightLabel),
|
|
)
|
|
.slice(0, LIGHTHOUSE_CONTEXT_LIMIT.ATTACK_PATH_TYPE_COUNTS),
|
|
);
|
|
|
|
return Object.keys(boundedCounts).length > 0 ? boundedCounts : undefined;
|
|
}
|