Files
prowler/ui/lib/findings-groups.ts
T
Alan Buscaglia 577aa14acc fix(ui): correct IaC findings counters (#10736)
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
2026-04-17 12:48:57 +02:00

259 lines
6.7 KiB
TypeScript

import {
FINDING_DELTA,
FINDING_STATUS,
type FindingDelta,
type FindingGroupRow,
type FindingStatus,
} from "@/types";
type FindingGroupDeltaState = Pick<
FindingGroupRow,
| "newCount"
| "changedCount"
| "newFailCount"
| "newFailMutedCount"
| "newPassCount"
| "newPassMutedCount"
| "newManualCount"
| "newManualMutedCount"
| "changedFailCount"
| "changedFailMutedCount"
| "changedPassCount"
| "changedPassMutedCount"
| "changedManualCount"
| "changedManualMutedCount"
>;
type FindingGroupDelta = Exclude<FindingDelta, null>;
type FindingGroupStatus = FindingStatus;
const FINDING_GROUP_STATUSES = Object.values(FINDING_STATUS);
export function isFindingGroupMuted(
group: Pick<
FindingGroupRow,
"muted" | "mutedCount" | "resourcesFail" | "resourcesTotal"
>,
): boolean {
if (typeof group.muted === "boolean") {
return group.muted;
}
const mutedCount = group.mutedCount ?? 0;
if (mutedCount === 0) {
return false;
}
return (
mutedCount === group.resourcesFail || mutedCount === group.resourcesTotal
);
}
export function getFindingGroupImpactedCounts(
group: Pick<
FindingGroupRow,
| "resourcesTotal"
| "resourcesFail"
| "passCount"
| "failCount"
| "manualCount"
| "passMutedCount"
| "failMutedCount"
| "manualMutedCount"
| "muted"
| "mutedCount"
>,
): { impacted: number; total: number } {
if (group.resourcesTotal > 0) {
return {
impacted: group.resourcesFail,
total: group.resourcesTotal,
};
}
const total =
(group.passCount ?? 0) + (group.failCount ?? 0) + (group.manualCount ?? 0);
if (!isFindingGroupMuted(group)) {
return {
impacted: group.failCount ?? 0,
total,
};
}
return {
impacted: group.failCount ?? 0,
total:
total +
(group.passMutedCount ?? 0) +
(group.failMutedCount ?? 0) +
(group.manualMutedCount ?? 0),
};
}
export function canDrillDownFindingGroup(
group: Pick<FindingGroupRow, "resourcesTotal">,
): boolean {
return group.resourcesTotal > 0;
}
function getNewDeltaTotal(group: FindingGroupDeltaState): number {
const breakdownTotal =
(group.newFailCount ?? 0) +
(group.newFailMutedCount ?? 0) +
(group.newPassCount ?? 0) +
(group.newPassMutedCount ?? 0) +
(group.newManualCount ?? 0) +
(group.newManualMutedCount ?? 0);
return breakdownTotal > 0 ? breakdownTotal : group.newCount;
}
function getChangedDeltaTotal(group: FindingGroupDeltaState): number {
const breakdownTotal =
(group.changedFailCount ?? 0) +
(group.changedFailMutedCount ?? 0) +
(group.changedPassCount ?? 0) +
(group.changedPassMutedCount ?? 0) +
(group.changedManualCount ?? 0) +
(group.changedManualMutedCount ?? 0);
return breakdownTotal > 0 ? breakdownTotal : group.changedCount;
}
export function getFindingGroupDelta(
group: FindingGroupDeltaState,
): FindingGroupDelta {
if (getNewDeltaTotal(group) > 0) {
return FINDING_DELTA.NEW;
}
if (getChangedDeltaTotal(group) > 0) {
return FINDING_DELTA.CHANGED;
}
return FINDING_DELTA.NONE;
}
type FindingGroupFiltersRecord = Record<string, string | string[] | undefined>;
function parseStatusFilterValue(
rawValue: string | string[] | undefined,
): FindingGroupStatus[] {
if (!rawValue) {
return [];
}
const joined = Array.isArray(rawValue) ? rawValue.join(",") : rawValue;
return joined
.split(",")
.map((status) => status.trim().toUpperCase())
.filter((status): status is FindingGroupStatus =>
(FINDING_GROUP_STATUSES as readonly string[]).includes(status),
);
}
/**
* Returns the set of statuses the user has explicitly narrowed the findings
* view to, or null when no status filter is active (→ all statuses should be
* considered). Supports both `filter[status]` (single value) and
* `filter[status__in]` (comma-separated values).
*/
export function getActiveStatusFilter(
filters: FindingGroupFiltersRecord,
): Set<FindingGroupStatus> | null {
const direct = parseStatusFilterValue(filters["filter[status]"]);
if (direct.length > 0) {
return new Set(direct);
}
const multi = parseStatusFilterValue(filters["filter[status__in]"]);
if (multi.length > 0) {
return new Set(multi);
}
return null;
}
function hasAnyDeltaBreakdown(group: FindingGroupDeltaState): boolean {
return (
(group.newFailCount ?? 0) > 0 ||
(group.newFailMutedCount ?? 0) > 0 ||
(group.newPassCount ?? 0) > 0 ||
(group.newPassMutedCount ?? 0) > 0 ||
(group.newManualCount ?? 0) > 0 ||
(group.newManualMutedCount ?? 0) > 0 ||
(group.changedFailCount ?? 0) > 0 ||
(group.changedFailMutedCount ?? 0) > 0 ||
(group.changedPassCount ?? 0) > 0 ||
(group.changedPassMutedCount ?? 0) > 0 ||
(group.changedManualCount ?? 0) > 0 ||
(group.changedManualMutedCount ?? 0) > 0
);
}
function getNewDeltaForStatuses(
group: FindingGroupDeltaState,
statuses: Set<FindingGroupStatus>,
): number {
let total = 0;
if (statuses.has(FINDING_STATUS.FAIL)) {
total += (group.newFailCount ?? 0) + (group.newFailMutedCount ?? 0);
}
if (statuses.has(FINDING_STATUS.PASS)) {
total += (group.newPassCount ?? 0) + (group.newPassMutedCount ?? 0);
}
if (statuses.has(FINDING_STATUS.MANUAL)) {
total += (group.newManualCount ?? 0) + (group.newManualMutedCount ?? 0);
}
return total;
}
function getChangedDeltaForStatuses(
group: FindingGroupDeltaState,
statuses: Set<FindingGroupStatus>,
): number {
let total = 0;
if (statuses.has(FINDING_STATUS.FAIL)) {
total += (group.changedFailCount ?? 0) + (group.changedFailMutedCount ?? 0);
}
if (statuses.has(FINDING_STATUS.PASS)) {
total += (group.changedPassCount ?? 0) + (group.changedPassMutedCount ?? 0);
}
if (statuses.has(FINDING_STATUS.MANUAL)) {
total +=
(group.changedManualCount ?? 0) + (group.changedManualMutedCount ?? 0);
}
return total;
}
/**
* Filter-aware variant of {@link getFindingGroupDelta}. When a status filter
* is active, only delta counters belonging to the filtered statuses contribute
* to the indicator. When no status filter is active, or when the API response
* lacks breakdown counters (legacy shape), this falls back to the aggregate
* delta so rows still surface deltas correctly.
*/
export function getFilteredFindingGroupDelta(
group: FindingGroupDeltaState,
filters: FindingGroupFiltersRecord,
): FindingGroupDelta {
const activeStatuses = getActiveStatusFilter(filters);
if (!activeStatuses || !hasAnyDeltaBreakdown(group)) {
return getFindingGroupDelta(group);
}
if (getNewDeltaForStatuses(group, activeStatuses) > 0) {
return FINDING_DELTA.NEW;
}
if (getChangedDeltaForStatuses(group, activeStatuses) > 0) {
return FINDING_DELTA.CHANGED;
}
return FINDING_DELTA.NONE;
}