mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-23 20:42:02 +00:00
feat(findings): Handle muted findings in API and UI (#7378)
Co-authored-by: Pablo Lara <larabjj@gmail.com>
This commit is contained in:
committed by
GitHub
parent
e06a33de84
commit
08690068fc
@@ -11,6 +11,7 @@ All notable changes to the **Prowler API** are documented in this file.
|
||||
- Support for developing new integrations [(#7167)](https://github.com/prowler-cloud/prowler/pull/7167).
|
||||
- HTTP Security Headers [(#7289)](https://github.com/prowler-cloud/prowler/pull/7289).
|
||||
- New endpoint to get the compliance overviews metadata [(#7333)](https://github.com/prowler-cloud/prowler/pull/7333).
|
||||
- Support for muted findings [(#7378)](https://github.com/prowler-cloud/prowler/pull/7378).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -287,6 +287,9 @@ class FindingFilter(FilterSet):
|
||||
status = ChoiceFilter(choices=StatusChoices.choices)
|
||||
severity = ChoiceFilter(choices=SeverityChoices)
|
||||
impact = ChoiceFilter(choices=SeverityChoices)
|
||||
muted = BooleanFilter(
|
||||
help_text="If this filter is not provided, muted and non-muted findings will be returned."
|
||||
)
|
||||
|
||||
resources = UUIDInFilter(field_name="resource__id", lookup_expr="in")
|
||||
|
||||
@@ -614,12 +617,6 @@ class ScanSummaryFilter(FilterSet):
|
||||
field_name="scan__provider__provider", choices=Provider.ProviderChoices.choices
|
||||
)
|
||||
region = CharFilter(field_name="region")
|
||||
muted_findings = BooleanFilter(method="filter_muted_findings")
|
||||
|
||||
def filter_muted_findings(self, queryset, name, value):
|
||||
if not value:
|
||||
return queryset.exclude(muted__gt=0)
|
||||
return queryset
|
||||
|
||||
class Meta:
|
||||
model = ScanSummary
|
||||
@@ -630,8 +627,6 @@ class ScanSummaryFilter(FilterSet):
|
||||
|
||||
|
||||
class ServiceOverviewFilter(ScanSummaryFilter):
|
||||
muted_findings = None
|
||||
|
||||
def is_valid(self):
|
||||
# Check if at least one of the inserted_at filters is present
|
||||
inserted_at_filters = [
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# Generated by Django 5.1.5 on 2025-03-25 11:29
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
import api.db_utils
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("api", "0014_integrations"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="finding",
|
||||
name="muted",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="finding",
|
||||
name="status",
|
||||
field=api.db_utils.StatusEnumField(
|
||||
choices=[("FAIL", "Fail"), ("PASS", "Pass"), ("MANUAL", "Manual")]
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -59,7 +59,6 @@ class StatusChoices(models.TextChoices):
|
||||
FAIL = "FAIL", _("Fail")
|
||||
PASS = "PASS", _("Pass")
|
||||
MANUAL = "MANUAL", _("Manual")
|
||||
MUTED = "MUTED", _("Muted")
|
||||
|
||||
|
||||
class StateChoices(models.TextChoices):
|
||||
@@ -656,6 +655,7 @@ class Finding(PostgresPartitionedModel, RowLevelSecurityProtectedModel):
|
||||
tags = models.JSONField(default=dict, null=True, blank=True)
|
||||
check_id = models.CharField(max_length=100, blank=False, null=False)
|
||||
check_metadata = models.JSONField(default=dict, null=False)
|
||||
muted = models.BooleanField(default=False, null=False)
|
||||
|
||||
# Relationships
|
||||
scan = models.ForeignKey(to=Scan, related_name="findings", on_delete=models.CASCADE)
|
||||
|
||||
@@ -294,6 +294,7 @@ paths:
|
||||
- inserted_at
|
||||
- updated_at
|
||||
- first_seen_at
|
||||
- muted
|
||||
- url
|
||||
- scan
|
||||
- resources
|
||||
@@ -402,6 +403,12 @@ paths:
|
||||
type: string
|
||||
format: date
|
||||
description: Maximum date range is 7 days.
|
||||
- in: query
|
||||
name: filter[muted]
|
||||
schema:
|
||||
type: boolean
|
||||
description: If this filter is not provided, muted and non-muted findings
|
||||
will be returned.
|
||||
- in: query
|
||||
name: filter[provider]
|
||||
schema:
|
||||
@@ -633,13 +640,11 @@ paths:
|
||||
enum:
|
||||
- FAIL
|
||||
- MANUAL
|
||||
- MUTED
|
||||
- PASS
|
||||
description: |-
|
||||
* `FAIL` - Fail
|
||||
* `PASS` - Pass
|
||||
* `MANUAL` - Manual
|
||||
* `MUTED` - Muted
|
||||
- in: query
|
||||
name: filter[status__in]
|
||||
schema:
|
||||
@@ -756,6 +761,7 @@ paths:
|
||||
- inserted_at
|
||||
- updated_at
|
||||
- first_seen_at
|
||||
- muted
|
||||
- url
|
||||
- scan
|
||||
- resources
|
||||
@@ -909,6 +915,12 @@ paths:
|
||||
type: string
|
||||
format: date
|
||||
description: Maximum date range is 7 days.
|
||||
- in: query
|
||||
name: filter[muted]
|
||||
schema:
|
||||
type: boolean
|
||||
description: If this filter is not provided, muted and non-muted findings
|
||||
will be returned.
|
||||
- in: query
|
||||
name: filter[provider]
|
||||
schema:
|
||||
@@ -1140,13 +1152,11 @@ paths:
|
||||
enum:
|
||||
- FAIL
|
||||
- MANUAL
|
||||
- MUTED
|
||||
- PASS
|
||||
description: |-
|
||||
* `FAIL` - Fail
|
||||
* `PASS` - Pass
|
||||
* `MANUAL` - Manual
|
||||
* `MUTED` - Muted
|
||||
- in: query
|
||||
name: filter[status__in]
|
||||
schema:
|
||||
@@ -1338,6 +1348,12 @@ paths:
|
||||
type: string
|
||||
format: date
|
||||
description: Maximum date range is 7 days.
|
||||
- in: query
|
||||
name: filter[muted]
|
||||
schema:
|
||||
type: boolean
|
||||
description: If this filter is not provided, muted and non-muted findings
|
||||
will be returned.
|
||||
- in: query
|
||||
name: filter[provider]
|
||||
schema:
|
||||
@@ -1569,13 +1585,11 @@ paths:
|
||||
enum:
|
||||
- FAIL
|
||||
- MANUAL
|
||||
- MUTED
|
||||
- PASS
|
||||
description: |-
|
||||
* `FAIL` - Fail
|
||||
* `PASS` - Pass
|
||||
* `MANUAL` - Manual
|
||||
* `MUTED` - Muted
|
||||
- in: query
|
||||
name: filter[status__in]
|
||||
schema:
|
||||
@@ -2019,10 +2033,6 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
- in: query
|
||||
name: filter[muted_findings]
|
||||
schema:
|
||||
type: boolean
|
||||
- in: query
|
||||
name: filter[provider_id]
|
||||
schema:
|
||||
@@ -2180,10 +2190,6 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
- in: query
|
||||
name: filter[muted_findings]
|
||||
schema:
|
||||
type: boolean
|
||||
- in: query
|
||||
name: filter[provider_id]
|
||||
schema:
|
||||
@@ -6238,13 +6244,11 @@ components:
|
||||
- FAIL
|
||||
- PASS
|
||||
- MANUAL
|
||||
- MUTED
|
||||
type: string
|
||||
description: |-
|
||||
* `FAIL` - Fail
|
||||
* `PASS` - Pass
|
||||
* `MANUAL` - Manual
|
||||
* `MUTED` - Muted
|
||||
status_extended:
|
||||
type: string
|
||||
nullable: true
|
||||
@@ -6280,6 +6284,8 @@ components:
|
||||
format: date-time
|
||||
readOnly: true
|
||||
nullable: true
|
||||
muted:
|
||||
type: boolean
|
||||
required:
|
||||
- uid
|
||||
- status
|
||||
|
||||
@@ -2693,6 +2693,8 @@ class TestFindingViewSet:
|
||||
# ("resource_tags", "key:value", 2),
|
||||
# ("resource_tags", "not:exists", 0),
|
||||
# ("resource_tags", "not:exists,key:value", 2),
|
||||
("muted", True, 1),
|
||||
("muted", False, 1),
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1091,6 +1091,7 @@ class FindingSerializer(RLSSerializer):
|
||||
"inserted_at",
|
||||
"updated_at",
|
||||
"first_seen_at",
|
||||
"muted",
|
||||
"url",
|
||||
# Relationships
|
||||
"scan",
|
||||
|
||||
@@ -655,6 +655,7 @@ def findings_fixture(scans_fixture, resources_fixture):
|
||||
"Description": "test description orange juice",
|
||||
},
|
||||
first_seen_at="2024-01-02T00:00:00Z",
|
||||
muted=True,
|
||||
)
|
||||
|
||||
finding2.add_resources([resource2])
|
||||
|
||||
@@ -267,6 +267,7 @@ def perform_prowler_scan(
|
||||
check_id=finding.check_id,
|
||||
scan=scan_instance,
|
||||
first_seen_at=last_first_seen_at,
|
||||
muted=finding.muted,
|
||||
)
|
||||
finding_instance.add_resources([resource_instance])
|
||||
|
||||
@@ -402,21 +403,21 @@ def aggregate_findings(tenant_id: str, scan_id: str):
|
||||
).annotate(
|
||||
fail=Sum(
|
||||
Case(
|
||||
When(status="FAIL", then=1),
|
||||
When(status="FAIL", muted=False, then=1),
|
||||
default=0,
|
||||
output_field=IntegerField(),
|
||||
)
|
||||
),
|
||||
_pass=Sum(
|
||||
Case(
|
||||
When(status="PASS", then=1),
|
||||
When(status="PASS", muted=False, then=1),
|
||||
default=0,
|
||||
output_field=IntegerField(),
|
||||
)
|
||||
),
|
||||
muted=Sum(
|
||||
muted_count=Sum(
|
||||
Case(
|
||||
When(status="MUTED", then=1),
|
||||
When(muted=True, then=1),
|
||||
default=0,
|
||||
output_field=IntegerField(),
|
||||
)
|
||||
@@ -424,63 +425,63 @@ def aggregate_findings(tenant_id: str, scan_id: str):
|
||||
total=Count("id"),
|
||||
new=Sum(
|
||||
Case(
|
||||
When(delta="new", then=1),
|
||||
When(delta="new", muted=False, then=1),
|
||||
default=0,
|
||||
output_field=IntegerField(),
|
||||
)
|
||||
),
|
||||
changed=Sum(
|
||||
Case(
|
||||
When(delta="changed", then=1),
|
||||
When(delta="changed", muted=False, then=1),
|
||||
default=0,
|
||||
output_field=IntegerField(),
|
||||
)
|
||||
),
|
||||
unchanged=Sum(
|
||||
Case(
|
||||
When(delta__isnull=True, then=1),
|
||||
When(delta__isnull=True, muted=False, then=1),
|
||||
default=0,
|
||||
output_field=IntegerField(),
|
||||
)
|
||||
),
|
||||
fail_new=Sum(
|
||||
Case(
|
||||
When(delta="new", status="FAIL", then=1),
|
||||
When(delta="new", status="FAIL", muted=False, then=1),
|
||||
default=0,
|
||||
output_field=IntegerField(),
|
||||
)
|
||||
),
|
||||
fail_changed=Sum(
|
||||
Case(
|
||||
When(delta="changed", status="FAIL", then=1),
|
||||
When(delta="changed", status="FAIL", muted=False, then=1),
|
||||
default=0,
|
||||
output_field=IntegerField(),
|
||||
)
|
||||
),
|
||||
pass_new=Sum(
|
||||
Case(
|
||||
When(delta="new", status="PASS", then=1),
|
||||
When(delta="new", status="PASS", muted=False, then=1),
|
||||
default=0,
|
||||
output_field=IntegerField(),
|
||||
)
|
||||
),
|
||||
pass_changed=Sum(
|
||||
Case(
|
||||
When(delta="changed", status="PASS", then=1),
|
||||
When(delta="changed", status="PASS", muted=False, then=1),
|
||||
default=0,
|
||||
output_field=IntegerField(),
|
||||
)
|
||||
),
|
||||
muted_new=Sum(
|
||||
Case(
|
||||
When(delta="new", status="MUTED", then=1),
|
||||
When(delta="new", muted=True, then=1),
|
||||
default=0,
|
||||
output_field=IntegerField(),
|
||||
)
|
||||
),
|
||||
muted_changed=Sum(
|
||||
Case(
|
||||
When(delta="changed", status="MUTED", then=1),
|
||||
When(delta="changed", muted=True, then=1),
|
||||
default=0,
|
||||
output_field=IntegerField(),
|
||||
)
|
||||
@@ -498,7 +499,7 @@ def aggregate_findings(tenant_id: str, scan_id: str):
|
||||
region=agg["resources__region"],
|
||||
fail=agg["fail"],
|
||||
_pass=agg["_pass"],
|
||||
muted=agg["muted"],
|
||||
muted=agg["muted_count"],
|
||||
total=agg["total"],
|
||||
new=agg["new"],
|
||||
changed=agg["changed"],
|
||||
|
||||
@@ -107,6 +107,7 @@ class TestPerformScan:
|
||||
finding.service_name = "service_name"
|
||||
finding.resource_type = "resource_type"
|
||||
finding.resource_tags = {"tag1": "value1", "tag2": "value2"}
|
||||
finding.muted = False
|
||||
finding.raw = {}
|
||||
|
||||
# Mock the ProwlerScan instance
|
||||
|
||||
@@ -12,6 +12,7 @@ All notable changes to the **Prowler UI** are documented in this file.
|
||||
- Added `one-time scan` feature: Adds support for single scan execution. [(#7188)](https://github.com/prowler-cloud/prowler/pull/7188)
|
||||
- Accepted invitations can no longer be edited. [(#7198)](https://github.com/prowler-cloud/prowler/pull/7198)
|
||||
- Added download column in scans table to download reports for completed scans. [(#7353)](https://github.com/prowler-cloud/prowler/pull/7353)
|
||||
- Show muted icon when a finding is muted. [(#7378)](https://github.com/prowler-cloud/prowler/pull/7378)
|
||||
|
||||
#### 🔄 Changed
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ export const filterFindings = [
|
||||
{
|
||||
key: "status__in",
|
||||
labelCheckboxGroup: "Status",
|
||||
values: ["PASS", "FAIL", "MANUAL", "MUTED"],
|
||||
values: ["PASS", "FAIL", "MANUAL"],
|
||||
},
|
||||
{
|
||||
key: "delta__in",
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./muted";
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Tooltip } from "@nextui-org/react";
|
||||
|
||||
import { MutedIcon } from "../icons";
|
||||
|
||||
interface MutedProps {
|
||||
isMuted: boolean;
|
||||
}
|
||||
|
||||
export const Muted = ({ isMuted }: MutedProps) => {
|
||||
if (isMuted === false) return null;
|
||||
|
||||
return (
|
||||
<Tooltip content={"This finding is muted"} className="text-xs">
|
||||
<div className="w-fit rounded-full border border-system-severity-critical/40 p-1">
|
||||
<MutedIcon className="h-4 w-4 text-system-severity-critical" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
@@ -14,6 +14,8 @@ import {
|
||||
} from "@/components/ui/table";
|
||||
import { FindingProps } from "@/types";
|
||||
|
||||
import { Muted } from "../muted";
|
||||
|
||||
const getFindingsData = (row: { original: FindingProps }) => {
|
||||
return row.original;
|
||||
};
|
||||
@@ -91,10 +93,18 @@ export const ColumnFindings: ColumnDef<FindingProps>[] = [
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const { checktitle } = getFindingsMetadata(row);
|
||||
const {
|
||||
attributes: { muted },
|
||||
} = getFindingsData(row);
|
||||
return (
|
||||
<p className="max-w-[450px] whitespace-normal break-words text-small">
|
||||
{checktitle}
|
||||
</p>
|
||||
<div className="relative flex max-w-[410px] flex-row items-center gap-2 3xl:max-w-[660px]">
|
||||
<p className="mr-7 whitespace-normal break-words text-sm">
|
||||
{checktitle}
|
||||
</p>
|
||||
<span className="absolute -right-2 top-1/2 -translate-y-1/2">
|
||||
<Muted isMuted={muted} />
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
@@ -124,7 +134,7 @@ export const ColumnFindings: ColumnDef<FindingProps>[] = [
|
||||
attributes: { status },
|
||||
} = getFindingsData(row);
|
||||
|
||||
return <StatusFindingBadge size="sm" status={status} />;
|
||||
return <StatusFindingBadge status={status} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -169,7 +179,7 @@ export const ColumnFindings: ColumnDef<FindingProps>[] = [
|
||||
const region = getResourceData(row, "region");
|
||||
|
||||
return (
|
||||
<div className="w-[80px]">
|
||||
<div className="w-[80px] text-xs">
|
||||
{typeof region === "string" ? region : "Invalid region"}
|
||||
</div>
|
||||
);
|
||||
@@ -180,7 +190,7 @@ export const ColumnFindings: ColumnDef<FindingProps>[] = [
|
||||
header: "Service",
|
||||
cell: ({ row }) => {
|
||||
const { servicename } = getFindingsMetadata(row);
|
||||
return <p className="max-w-96 truncate text-small">{servicename}</p>;
|
||||
return <p className="max-w-96 truncate text-xs">{servicename}</p>;
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -12,6 +12,8 @@ import {
|
||||
import { SeverityBadge } from "@/components/ui/table/severity-badge";
|
||||
import { FindingProps } from "@/types";
|
||||
|
||||
import { Muted } from "../muted";
|
||||
|
||||
const renderValue = (value: string | null | undefined) => {
|
||||
return value && value.trim() !== "" ? value : "-";
|
||||
};
|
||||
@@ -66,17 +68,20 @@ export const FindingDetail = ({
|
||||
{renderValue(attributes.check_metadata.checktitle)}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="flex items-center gap-x-4">
|
||||
<Muted isMuted={attributes.muted} />
|
||||
|
||||
<div
|
||||
className={`rounded-lg px-3 py-1 text-sm font-semibold ${
|
||||
attributes.status === "PASS"
|
||||
? "bg-green-100 text-green-600"
|
||||
: attributes.status === "MANUAL"
|
||||
? "bg-gray-100 text-gray-600"
|
||||
: "bg-red-100 text-red-600"
|
||||
}`}
|
||||
>
|
||||
{renderValue(attributes.status)}
|
||||
<div
|
||||
className={`rounded-lg px-3 py-1 text-sm font-semibold ${
|
||||
attributes.status === "PASS"
|
||||
? "bg-green-100 text-green-600"
|
||||
: attributes.status === "MANUAL"
|
||||
? "bg-gray-100 text-gray-600"
|
||||
: "bg-red-100 text-system-severity-critical"
|
||||
}`}
|
||||
>
|
||||
{renderValue(attributes.status)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1025,3 +1025,31 @@ export const GCPIcon: React.FC<IconSvgProps> = ({
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const MutedIcon: React.FC<IconSvgProps> = ({
|
||||
size,
|
||||
height,
|
||||
width,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
height={size || height || 24}
|
||||
width={size || width || 24}
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
{...props}
|
||||
>
|
||||
<path d="M10.268 21a2 2 0 0 0 3.464 0" />
|
||||
<path d="M17 17H4a1 1 0 0 1-.74-1.673C4.59 13.956 6 12.499 6 8a6 6 0 0 1 .258-1.742" />
|
||||
<path d="m2 2 20 20" />
|
||||
<path d="M8.668 3.01A6 6 0 0 1 18 8c0 2.687.77 4.653 1.707 6.05" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -10,6 +10,8 @@ import { TriggerSheet } from "@/components/ui/sheet";
|
||||
import { SeverityBadge, StatusFindingBadge } from "@/components/ui/table";
|
||||
import { FindingProps } from "@/types";
|
||||
|
||||
import { Muted } from "../../../findings/muted";
|
||||
|
||||
const getFindingsData = (row: { original: FindingProps }) => {
|
||||
return row.original;
|
||||
};
|
||||
@@ -71,10 +73,18 @@ export const ColumnNewFindingsToDate: ColumnDef<FindingProps>[] = [
|
||||
header: "Finding",
|
||||
cell: ({ row }) => {
|
||||
const { checktitle } = getFindingsMetadata(row);
|
||||
const {
|
||||
attributes: { muted },
|
||||
} = getFindingsData(row);
|
||||
return (
|
||||
<p className="max-w-[450px] whitespace-normal break-words text-small">
|
||||
{checktitle}
|
||||
</p>
|
||||
<div className="relative flex max-w-[410px] flex-row items-center gap-2 3xl:max-w-[660px]">
|
||||
<p className="mr-7 whitespace-normal break-words text-sm">
|
||||
{checktitle}
|
||||
</p>
|
||||
<span className="absolute -right-2 top-1/2 -translate-y-1/2">
|
||||
<Muted isMuted={muted} />
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -45,7 +45,7 @@ export const SeverityBadge = ({ severity }: { severity: Severity }) => {
|
||||
color={color}
|
||||
endContent={getSeverityIcon(severity)}
|
||||
>
|
||||
{severity}
|
||||
<span className="text-xs font-light tracking-wide">{severity}</span>
|
||||
</Chip>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -25,13 +25,15 @@ export const StatusFindingBadge = ({
|
||||
|
||||
return (
|
||||
<Chip
|
||||
className="gap-1 border-none px-2 py-1 capitalize text-default-600"
|
||||
className="border-none px-2 py-0"
|
||||
size={size}
|
||||
variant="flat"
|
||||
color={color}
|
||||
{...props}
|
||||
>
|
||||
{status}
|
||||
<span className="text-xs font-light tracking-wide text-default-600">
|
||||
{status.charAt(0).toUpperCase() + status.slice(1).toLowerCase()}
|
||||
</span>
|
||||
</Chip>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -597,10 +597,11 @@ export interface FindingProps {
|
||||
attributes: {
|
||||
uid: string;
|
||||
delta: "new" | "changed" | null;
|
||||
status: "PASS" | "FAIL" | "MANUAL" | "MUTED";
|
||||
status: "PASS" | "FAIL" | "MANUAL";
|
||||
status_extended: string;
|
||||
severity: "informational" | "low" | "medium" | "high" | "critical";
|
||||
check_id: string;
|
||||
muted: boolean;
|
||||
check_metadata: {
|
||||
risk: string;
|
||||
notes: string;
|
||||
|
||||
Reference in New Issue
Block a user