feat(compliance): add csa ccm 4.0 for the aws provider (#10018)

This commit is contained in:
Pedro Martín
2026-02-12 13:10:59 +01:00
committed by GitHub
parent 52f98f1704
commit f55983a77d
20 changed files with 8176 additions and 5 deletions

View File

@@ -8,6 +8,7 @@ All notable changes to the **Prowler API** are documented in this file.
- Attack Paths: Queries definition now has short description and attribution [(#9983)](https://github.com/prowler-cloud/prowler/pull/9983) - Attack Paths: Queries definition now has short description and attribution [(#9983)](https://github.com/prowler-cloud/prowler/pull/9983)
- Attack Paths: Internet node is created while scan [(#9992)](https://github.com/prowler-cloud/prowler/pull/9992) - Attack Paths: Internet node is created while scan [(#9992)](https://github.com/prowler-cloud/prowler/pull/9992)
- Support CSA CCM for the AWS provider [(#10018)](https://github.com/prowler-cloud/prowler/pull/10018)
--- ---

View File

@@ -35,6 +35,7 @@ from prowler.lib.outputs.compliance.cis.cis_github import GithubCIS
from prowler.lib.outputs.compliance.cis.cis_kubernetes import KubernetesCIS from prowler.lib.outputs.compliance.cis.cis_kubernetes import KubernetesCIS
from prowler.lib.outputs.compliance.cis.cis_m365 import M365CIS from prowler.lib.outputs.compliance.cis.cis_m365 import M365CIS
from prowler.lib.outputs.compliance.cis.cis_oraclecloud import OracleCloudCIS from prowler.lib.outputs.compliance.cis.cis_oraclecloud import OracleCloudCIS
from prowler.lib.outputs.compliance.csa.csa_aws import AWSCSA
from prowler.lib.outputs.compliance.ens.ens_aws import AWSENS from prowler.lib.outputs.compliance.ens.ens_aws import AWSENS
from prowler.lib.outputs.compliance.ens.ens_azure import AzureENS from prowler.lib.outputs.compliance.ens.ens_azure import AzureENS
from prowler.lib.outputs.compliance.ens.ens_gcp import GCPENS from prowler.lib.outputs.compliance.ens.ens_gcp import GCPENS
@@ -90,6 +91,7 @@ COMPLIANCE_CLASS_MAP = {
(lambda name: name == "prowler_threatscore_aws", ProwlerThreatScoreAWS), (lambda name: name == "prowler_threatscore_aws", ProwlerThreatScoreAWS),
(lambda name: name == "ccc_aws", CCC_AWS), (lambda name: name == "ccc_aws", CCC_AWS),
(lambda name: name.startswith("c5_"), AWSC5), (lambda name: name.startswith("c5_"), AWSC5),
(lambda name: name.startswith("csa_"), AWSCSA),
], ],
"azure": [ "azure": [
(lambda name: name.startswith("cis_"), AzureCIS), (lambda name: name.startswith("cis_"), AzureCIS),

View File

@@ -0,0 +1,31 @@
import warnings
from dashboard.common_methods import get_section_containers_kisa_ismsp
warnings.filterwarnings("ignore")
def get_table(data):
data["REQUIREMENTS_ID"] = (
data["REQUIREMENTS_ID"] + " - " + data["REQUIREMENTS_DESCRIPTION"]
)
data["REQUIREMENTS_ID"] = data["REQUIREMENTS_ID"].apply(
lambda x: x[:150] + "..." if len(str(x)) > 150 else x
)
aux = data[
[
"REQUIREMENTS_ID",
"REQUIREMENTS_ATTRIBUTES_SECTION",
"CHECKID",
"STATUS",
"REGION",
"ACCOUNTID",
"RESOURCEID",
]
].copy()
return get_section_containers_kisa_ismsp(
aux, "REQUIREMENTS_ATTRIBUTES_SECTION", "REQUIREMENTS_ID"
)

View File

@@ -9,6 +9,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
- `defender_safe_attachments_policy_enabled` check for M365 provider [(#9833)](https://github.com/prowler-cloud/prowler/pull/9833) - `defender_safe_attachments_policy_enabled` check for M365 provider [(#9833)](https://github.com/prowler-cloud/prowler/pull/9833)
- `defender_safelinks_policy_enabled` check for M365 provider [(#9832)](https://github.com/prowler-cloud/prowler/pull/9832) - `defender_safelinks_policy_enabled` check for M365 provider [(#9832)](https://github.com/prowler-cloud/prowler/pull/9832)
- AI Skills: Added a skill for creating new Attack Paths queries in openCypher, compatible with Neo4j and Neptune [(#9975)](https://github.com/prowler-cloud/prowler/pull/9975) - AI Skills: Added a skill for creating new Attack Paths queries in openCypher, compatible with Neo4j and Neptune [(#9975)](https://github.com/prowler-cloud/prowler/pull/9975)
- CSA CCM 4.0 for the AWS provider [(#10018)](https://github.com/prowler-cloud/prowler/pull/10018)
### 🔄 Changed ### 🔄 Changed

View File

@@ -65,6 +65,7 @@ from prowler.lib.outputs.compliance.cis.cis_kubernetes import KubernetesCIS
from prowler.lib.outputs.compliance.cis.cis_m365 import M365CIS from prowler.lib.outputs.compliance.cis.cis_m365 import M365CIS
from prowler.lib.outputs.compliance.cis.cis_oraclecloud import OracleCloudCIS from prowler.lib.outputs.compliance.cis.cis_oraclecloud import OracleCloudCIS
from prowler.lib.outputs.compliance.compliance import display_compliance_table from prowler.lib.outputs.compliance.compliance import display_compliance_table
from prowler.lib.outputs.compliance.csa.csa_aws import AWSCSA
from prowler.lib.outputs.compliance.ens.ens_aws import AWSENS from prowler.lib.outputs.compliance.ens.ens_aws import AWSENS
from prowler.lib.outputs.compliance.ens.ens_azure import AzureENS from prowler.lib.outputs.compliance.ens.ens_azure import AzureENS
from prowler.lib.outputs.compliance.ens.ens_gcp import GCPENS from prowler.lib.outputs.compliance.ens.ens_gcp import GCPENS
@@ -626,6 +627,18 @@ def prowler():
) )
generated_outputs["compliance"].append(c5) generated_outputs["compliance"].append(c5)
c5.batch_write_data_to_file() c5.batch_write_data_to_file()
elif compliance_name == "csa_ccm_4.0_aws":
filename = (
f"{output_options.output_directory}/compliance/"
f"{output_options.output_filename}_{compliance_name}.csv"
)
csa_ccm_4_0_aws = AWSCSA(
findings=finding_outputs,
compliance=bulk_compliance_frameworks[compliance_name],
file_path=filename,
)
generated_outputs["compliance"].append(csa_ccm_4_0_aws)
csa_ccm_4_0_aws.batch_write_data_to_file()
else: else:
filename = ( filename = (
f"{output_options.output_directory}/compliance/" f"{output_options.output_directory}/compliance/"

File diff suppressed because it is too large Load Diff

View File

@@ -226,6 +226,18 @@ class C5Germany_Requirement_Attribute(BaseModel):
ComplementaryCriteria: str ComplementaryCriteria: str
# CSA CCM v4 Requirement Attribute
class CSA_CCM_Requirement_Attribute(BaseModel):
"""CSA Cloud Controls Matrix (CCM) v4 Requirement Attribute"""
Section: str
CCMLite: str
IaaS: str
PaaS: str
SaaS: str
ScopeApplicability: list[dict]
# Base Compliance Model # Base Compliance Model
# TODO: move this to compliance folder # TODO: move this to compliance folder
class Compliance_Requirement(BaseModel): class Compliance_Requirement(BaseModel):
@@ -244,6 +256,7 @@ class Compliance_Requirement(BaseModel):
Prowler_ThreatScore_Requirement_Attribute, Prowler_ThreatScore_Requirement_Attribute,
CCC_Requirement_Attribute, CCC_Requirement_Attribute,
C5Germany_Requirement_Attribute, C5Germany_Requirement_Attribute,
CSA_CCM_Requirement_Attribute,
# Generic_Compliance_Requirement_Attribute must be the last one since it is the fallback for generic compliance framework # Generic_Compliance_Requirement_Attribute must be the last one since it is the fallback for generic compliance framework
Generic_Compliance_Requirement_Attribute, Generic_Compliance_Requirement_Attribute,
] ]

View File

@@ -4,6 +4,7 @@ from prowler.lib.check.models import Check_Report
from prowler.lib.logger import logger from prowler.lib.logger import logger
from prowler.lib.outputs.compliance.c5.c5 import get_c5_table from prowler.lib.outputs.compliance.c5.c5 import get_c5_table
from prowler.lib.outputs.compliance.cis.cis import get_cis_table from prowler.lib.outputs.compliance.cis.cis import get_cis_table
from prowler.lib.outputs.compliance.csa.csa import get_csa_table
from prowler.lib.outputs.compliance.ens.ens import get_ens_table from prowler.lib.outputs.compliance.ens.ens import get_ens_table
from prowler.lib.outputs.compliance.generic.generic_table import ( from prowler.lib.outputs.compliance.generic.generic_table import (
get_generic_compliance_table, get_generic_compliance_table,
@@ -85,6 +86,15 @@ def display_compliance_table(
output_directory, output_directory,
compliance_overview, compliance_overview,
) )
elif "csa_ccm_" in compliance_framework:
get_csa_table(
findings,
bulk_checks_metadata,
compliance_framework,
output_filename,
output_directory,
compliance_overview,
)
elif "c5_" in compliance_framework: elif "c5_" in compliance_framework:
get_c5_table( get_c5_table(
findings, findings,

View File

@@ -0,0 +1,101 @@
from colorama import Fore, Style
from tabulate import tabulate
from prowler.config.config import orange_color
def get_csa_table(
findings: list,
bulk_checks_metadata: dict,
compliance_framework: str,
output_filename: str,
output_directory: str,
compliance_overview: bool,
):
section_table = {
"Provider": [],
"Section": [],
"Status": [],
"Muted": [],
}
pass_count = []
fail_count = []
muted_count = []
sections = {}
for index, finding in enumerate(findings):
check = bulk_checks_metadata[finding.check_metadata.CheckID]
check_compliances = check.Compliance
for compliance in check_compliances:
if (
compliance.Framework == "CSA-CCM"
and compliance.Version in compliance_framework
):
for requirement in compliance.Requirements:
for attribute in requirement.Attributes:
section = attribute.Section
if section not in sections:
sections[section] = {"FAIL": 0, "PASS": 0, "Muted": 0}
if finding.muted:
if index not in muted_count:
muted_count.append(index)
sections[section]["Muted"] += 1
else:
if finding.status == "FAIL" and index not in fail_count:
fail_count.append(index)
sections[section]["FAIL"] += 1
elif finding.status == "PASS" and index not in pass_count:
pass_count.append(index)
sections[section]["PASS"] += 1
sections = dict(sorted(sections.items()))
for section in sections:
section_table["Provider"].append(compliance.Provider)
section_table["Section"].append(section)
if sections[section]["FAIL"] > 0:
section_table["Status"].append(
f"{Fore.RED}FAIL({sections[section]['FAIL']}){Style.RESET_ALL}"
)
else:
if sections[section]["PASS"] > 0:
section_table["Status"].append(
f"{Fore.GREEN}PASS({sections[section]['PASS']}){Style.RESET_ALL}"
)
else:
section_table["Status"].append(f"{Fore.GREEN}PASS{Style.RESET_ALL}")
section_table["Muted"].append(
f"{orange_color}{sections[section]['Muted']}{Style.RESET_ALL}"
)
if (
len(fail_count) + len(pass_count) + len(muted_count) > 1
): # If there are no resources, don't print the compliance table
print(
f"\nCompliance Status of {Fore.YELLOW}{compliance_framework.upper()}{Style.RESET_ALL} Framework:"
)
total_findings_count = len(fail_count) + len(pass_count) + len(muted_count)
overview_table = [
[
f"{Fore.RED}{round(len(fail_count) / total_findings_count * 100, 2)}% ({len(fail_count)}) FAIL{Style.RESET_ALL}",
f"{Fore.GREEN}{round(len(pass_count) / total_findings_count * 100, 2)}% ({len(pass_count)}) PASS{Style.RESET_ALL}",
f"{orange_color}{round(len(muted_count) / total_findings_count * 100, 2)}% ({len(muted_count)}) MUTED{Style.RESET_ALL}",
]
]
print(tabulate(overview_table, tablefmt="rounded_grid"))
if not compliance_overview:
if len(fail_count) > 0 and len(section_table["Section"]) > 0:
print(
f"\nFramework {Fore.YELLOW}{compliance_framework.upper()}{Style.RESET_ALL} Results:"
)
print(
tabulate(
section_table,
tablefmt="rounded_grid",
headers="keys",
)
)
print(f"\nDetailed results of {compliance_framework.upper()} are in:")
print(
f" - CSV: {output_directory}/compliance/{output_filename}_{compliance_framework}.csv\n"
)

View File

@@ -0,0 +1,96 @@
from prowler.config.config import timestamp
from prowler.lib.check.compliance_models import Compliance
from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput
from prowler.lib.outputs.compliance.csa.models import AWSCSAModel
from prowler.lib.outputs.finding import Finding
class AWSCSA(ComplianceOutput):
"""
This class represents the AWS CSA compliance output.
Attributes:
- _data (list): A list to store transformed data from findings.
- _file_descriptor (TextIOWrapper): A file descriptor to write data to a file.
Methods:
- transform: Transforms findings into AWS CSA compliance format.
"""
def transform(
self,
findings: list[Finding],
compliance: Compliance,
compliance_name: str,
) -> None:
"""
Transforms a list of findings into AWS CSA compliance format.
Parameters:
- findings (list): A list of findings.
- compliance (Compliance): A compliance model.
- compliance_name (str): The name of the compliance model.
Returns:
- None
"""
for finding in findings:
# Get the compliance requirements for the finding
finding_requirements = finding.compliance.get(compliance_name, [])
for requirement in compliance.Requirements:
if requirement.Id in finding_requirements:
for attribute in requirement.Attributes:
compliance_row = AWSCSAModel(
Provider=finding.provider,
Description=compliance.Description,
AccountId=finding.account_uid,
Region=finding.region,
AssessmentDate=str(timestamp),
Requirements_Id=requirement.Id,
Requirements_Description=requirement.Description,
Requirements_Name=requirement.Name,
Requirements_Attributes_Section=attribute.Section,
Requirements_Attributes_CCMLite=attribute.CCMLite,
Requirements_Attributes_IaaS=attribute.IaaS,
Requirements_Attributes_PaaS=attribute.PaaS,
Requirements_Attributes_SaaS=attribute.SaaS,
Requirements_Attributes_ScopeApplicability=attribute.ScopeApplicability,
Status=finding.status,
StatusExtended=finding.status_extended,
ResourceId=finding.resource_uid,
ResourceName=finding.resource_name,
CheckId=finding.check_id,
Muted=finding.muted,
Framework=compliance.Framework,
Name=compliance.Name,
)
self._data.append(compliance_row)
# Add manual requirements to the compliance output
for requirement in compliance.Requirements:
if not requirement.Checks:
for attribute in requirement.Attributes:
compliance_row = AWSCSAModel(
Provider=compliance.Provider.lower(),
Description=compliance.Description,
AccountId="",
Region="",
AssessmentDate=str(timestamp),
Requirements_Id=requirement.Id,
Requirements_Description=requirement.Description,
Requirements_Name=requirement.Name,
Requirements_Attributes_Section=attribute.Section,
Requirements_Attributes_CCMLite=attribute.CCMLite,
Requirements_Attributes_IaaS=attribute.IaaS,
Requirements_Attributes_PaaS=attribute.PaaS,
Requirements_Attributes_SaaS=attribute.SaaS,
Requirements_Attributes_ScopeApplicability=attribute.ScopeApplicability,
Status="MANUAL",
StatusExtended="Manual check",
ResourceId="manual_check",
ResourceName="Manual check",
CheckId="manual",
Muted=False,
Framework=compliance.Framework,
Name=compliance.Name,
)
self._data.append(compliance_row)

View File

@@ -0,0 +1,30 @@
from pydantic.v1 import BaseModel
class AWSCSAModel(BaseModel):
"""
AWSCSAModel generates a finding's output in CSV CSA format for AWS.
"""
Provider: str
Description: str
AccountId: str
Region: str
AssessmentDate: str
Requirements_Id: str
Requirements_Description: str
Requirements_Name: str
Requirements_Attributes_Section: str
Requirements_Attributes_CCMLite: str
Requirements_Attributes_IaaS: str
Requirements_Attributes_PaaS: str
Requirements_Attributes_SaaS: str
Requirements_Attributes_ScopeApplicability: list[dict]
Status: str
StatusExtended: str
ResourceId: str
CheckId: str
Muted: bool
ResourceName: str
Framework: str
Name: str

View File

@@ -7,6 +7,7 @@ All notable changes to the **Prowler UI** are documented in this file.
### 🔄 Changed ### 🔄 Changed
- Attack Paths: Query list now shows their name and short description, when one is selected it also shows a longer description and an attribution if it has it [(#9983)](https://github.com/prowler-cloud/prowler/pull/9983) - Attack Paths: Query list now shows their name and short description, when one is selected it also shows a longer description and an attribution if it has it [(#9983)](https://github.com/prowler-cloud/prowler/pull/9983)
- CSA CCM detailed view and small fix related with `Top Failed Sections` width [(#10018)](https://github.com/prowler-cloud/prowler/pull/10018)
--- ---

View File

@@ -151,8 +151,9 @@ export default async function ComplianceDetail({
{/* Mobile: each card on own row | Tablet: ThreatScore full row, others share row | Desktop: all 3 in one row */} {/* Mobile: each card on own row | Tablet: ThreatScore full row, others share row | Desktop: all 3 in one row */}
<div <div
className={cn( className={cn(
"grid grid-cols-1 gap-6 md:grid-cols-2", "grid grid-cols-1 gap-6 md:grid-cols-[minmax(280px,400px)_1fr]",
isThreatScore && "xl:grid-cols-[minmax(280px,320px)_auto_1fr]", isThreatScore &&
"xl:grid-cols-[minmax(280px,320px)_minmax(280px,400px)_1fr]",
)} )}
> >
{isThreatScore && ( {isThreatScore && (
@@ -209,7 +210,7 @@ const SSRComplianceContent = async ({
if (!scanId || type === "tasks") { if (!scanId || type === "tasks") {
return ( return (
<div className="flex flex-col gap-8"> <div className="flex flex-col gap-8">
<div className="grid grid-cols-1 gap-6 md:grid-cols-2"> <div className="grid grid-cols-1 gap-6 md:grid-cols-[minmax(280px,400px)_1fr]">
<RequirementsStatusCard pass={0} fail={0} manual={0} /> <RequirementsStatusCard pass={0} fail={0} manual={0} />
<TopFailedSectionsCard sections={[]} /> <TopFailedSectionsCard sections={[]} />
{/* <SectionsFailureRateCard categories={[]} /> */} {/* <SectionsFailureRateCard categories={[]} /> */}
@@ -244,8 +245,9 @@ const SSRComplianceContent = async ({
{/* Mobile: each card on own row | Tablet: ThreatScore full row, others share row | Desktop: all 3 in one row */} {/* Mobile: each card on own row | Tablet: ThreatScore full row, others share row | Desktop: all 3 in one row */}
<div <div
className={cn( className={cn(
"grid grid-cols-1 gap-6 md:grid-cols-2", "grid grid-cols-1 gap-6 md:grid-cols-[minmax(280px,400px)_1fr]",
threatScoreData && "xl:grid-cols-[minmax(280px,320px)_auto_1fr]", threatScoreData &&
"xl:grid-cols-[minmax(280px,320px)_minmax(280px,400px)_1fr]",
)} )}
> >
{threatScoreData && ( {threatScoreData && (

View File

@@ -0,0 +1,93 @@
import { cn } from "@/lib";
import { CSA_MAPPING_SECTIONS } from "@/lib/compliance/csa";
import { Requirement } from "@/types/compliance";
import {
ComplianceBadge,
ComplianceBadgeContainer,
ComplianceDetailContainer,
ComplianceDetailSection,
ComplianceDetailText,
} from "./shared-components";
interface CSADetailsProps {
requirement: Requirement;
}
export const CSACustomDetails = ({ requirement }: CSADetailsProps) => {
const mappingSections = CSA_MAPPING_SECTIONS.map((section) => ({
...section,
data: requirement[section.key] as Array<{
ReferenceId: string;
Identifiers: string[];
}>,
})).filter((section) => section.data && section.data.length > 0);
return (
<ComplianceDetailContainer>
{requirement.description && (
<ComplianceDetailSection title="Description">
<ComplianceDetailText>{requirement.description}</ComplianceDetailText>
</ComplianceDetailSection>
)}
<ComplianceBadgeContainer>
{requirement.ccm_lite && (
<ComplianceBadge
label="CCM Lite"
value={requirement.ccm_lite as string}
color={requirement.ccm_lite === "Yes" ? "green" : "gray"}
/>
)}
{requirement.iaas && (
<ComplianceBadge
label="IaaS"
value={requirement.iaas as string}
color="blue"
/>
)}
{requirement.paas && (
<ComplianceBadge
label="PaaS"
value={requirement.paas as string}
color="blue"
/>
)}
{requirement.saas && (
<ComplianceBadge
label="SaaS"
value={requirement.saas as string}
color="blue"
/>
)}
</ComplianceBadgeContainer>
{mappingSections.map((section) => (
<ComplianceDetailSection key={section.title} title={section.title}>
<div className="flex flex-col gap-3">
{section.data.map((mapping, index) => (
<div key={index} className="flex flex-col gap-1">
<span className="text-muted-foreground text-xs font-medium">
{mapping.ReferenceId}
</span>
<div className="flex flex-wrap gap-2">
{mapping.Identifiers.map((identifier, idx) => (
<span
key={idx}
className={cn(
"inline-flex items-center rounded-md px-2 py-1 text-xs font-medium ring-1 ring-inset",
section.colorClasses,
)}
>
{identifier}
</span>
))}
</div>
</div>
))}
</div>
</ComplianceDetailSection>
))}
</ComplianceDetailContainer>
);
};

View File

@@ -3,6 +3,7 @@ import C5Logo from "./c5.svg";
import CCCLogo from "./ccc.svg"; import CCCLogo from "./ccc.svg";
import CISLogo from "./cis.svg"; import CISLogo from "./cis.svg";
import CISALogo from "./cisa.svg"; import CISALogo from "./cisa.svg";
import CSALogo from "./csa.svg";
import ENSLogo from "./ens.png"; import ENSLogo from "./ens.png";
import FedRAMPLogo from "./fedramp.svg"; import FedRAMPLogo from "./fedramp.svg";
import FFIECLogo from "./ffiec.svg"; import FFIECLogo from "./ffiec.svg";
@@ -40,6 +41,7 @@ const COMPLIANCE_LOGOS = {
nis2: NIS2Logo, nis2: NIS2Logo,
c5: C5Logo, c5: C5Logo,
ccc: CCCLogo, ccc: CCCLogo,
csa: CSALogo,
} as const; } as const;
export const getComplianceIcon = (complianceTitle: string) => { export const getComplianceIcon = (complianceTitle: string) => {

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@@ -4,6 +4,7 @@ import { AWSWellArchitectedCustomDetails } from "@/components/compliance/complia
import { C5CustomDetails } from "@/components/compliance/compliance-custom-details/c5-details"; import { C5CustomDetails } from "@/components/compliance/compliance-custom-details/c5-details";
import { CCCCustomDetails } from "@/components/compliance/compliance-custom-details/ccc-details"; import { CCCCustomDetails } from "@/components/compliance/compliance-custom-details/ccc-details";
import { CISCustomDetails } from "@/components/compliance/compliance-custom-details/cis-details"; import { CISCustomDetails } from "@/components/compliance/compliance-custom-details/cis-details";
import { CSACustomDetails } from "@/components/compliance/compliance-custom-details/csa-details";
import { ENSCustomDetails } from "@/components/compliance/compliance-custom-details/ens-details"; import { ENSCustomDetails } from "@/components/compliance/compliance-custom-details/ens-details";
import { GenericCustomDetails } from "@/components/compliance/compliance-custom-details/generic-details"; import { GenericCustomDetails } from "@/components/compliance/compliance-custom-details/generic-details";
import { ISOCustomDetails } from "@/components/compliance/compliance-custom-details/iso-details"; import { ISOCustomDetails } from "@/components/compliance/compliance-custom-details/iso-details";
@@ -37,6 +38,10 @@ import {
toAccordionItems as toCISAccordionItems, toAccordionItems as toCISAccordionItems,
} from "./cis"; } from "./cis";
import { calculateCategoryHeatmapData, getTopFailedSections } from "./commons"; import { calculateCategoryHeatmapData, getTopFailedSections } from "./commons";
import {
mapComplianceData as mapCSAComplianceData,
toAccordionItems as toCSAAccordionItems,
} from "./csa";
import { import {
mapComplianceData as mapENSComplianceData, mapComplianceData as mapENSComplianceData,
toAccordionItems as toENSAccordionItems, toAccordionItems as toENSAccordionItems,
@@ -179,6 +184,15 @@ const getComplianceMappers = (): Record<string, ComplianceMapper> => ({
getDetailsComponent: (requirement: Requirement) => getDetailsComponent: (requirement: Requirement) =>
createElement(CCCCustomDetails, { requirement }), createElement(CCCCustomDetails, { requirement }),
}, },
"CSA-CCM": {
mapComplianceData: mapCSAComplianceData,
toAccordionItems: toCSAAccordionItems,
getTopFailedSections,
calculateCategoryHeatmapData: (data: Framework[]) =>
calculateCategoryHeatmapData(data),
getDetailsComponent: (requirement: Requirement) =>
createElement(CSACustomDetails, { requirement }),
},
}); });
/** /**

143
ui/lib/compliance/csa.tsx Normal file
View File

@@ -0,0 +1,143 @@
import { ClientAccordionContent } from "@/components/compliance/compliance-accordion/client-accordion-content";
import { ComplianceAccordionRequirementTitle } from "@/components/compliance/compliance-accordion/compliance-accordion-requeriment-title";
import { ComplianceAccordionTitle } from "@/components/compliance/compliance-accordion/compliance-accordion-title";
import { AccordionItemProps } from "@/components/ui/accordion/Accordion";
import { FindingStatus } from "@/components/ui/table/status-finding-badge";
import {
AttributesData,
CSAAttributesMetadata,
Framework,
Requirement,
REQUIREMENT_STATUS,
RequirementsData,
RequirementStatus,
} from "@/types/compliance";
import {
calculateFrameworkCounters,
createRequirementsMap,
findOrCreateCategory,
findOrCreateControl,
findOrCreateFramework,
} from "./commons";
export interface CSAMappingSection {
title: string;
key: keyof Requirement;
colorClasses: string;
}
export const CSA_MAPPING_SECTIONS: CSAMappingSection[] = [
{
title: "Scope Applicability",
key: "scope_applicability",
colorClasses:
"bg-blue-50 text-blue-700 ring-blue-600/10 dark:bg-blue-400/10 dark:text-blue-400 dark:ring-blue-400/20",
},
];
const getStatusCounters = (status: RequirementStatus) => ({
pass: status === REQUIREMENT_STATUS.PASS ? 1 : 0,
fail: status === REQUIREMENT_STATUS.FAIL ? 1 : 0,
manual: status === REQUIREMENT_STATUS.MANUAL ? 1 : 0,
});
export const mapComplianceData = (
attributesData: AttributesData,
requirementsData: RequirementsData,
): Framework[] => {
const attributes = attributesData?.data || [];
const requirementsMap = createRequirementsMap(requirementsData);
const frameworks: Framework[] = [];
for (const attributeItem of attributes) {
const id = attributeItem.id;
const metadataArray = attributeItem.attributes?.attributes
?.metadata as unknown as CSAAttributesMetadata[];
const attrs = metadataArray?.[0];
if (!attrs) continue;
const requirementData = requirementsMap.get(id);
if (!requirementData) continue;
const frameworkName = attributeItem.attributes.framework;
const categoryName = attrs.Section;
const requirementName = attributeItem.attributes.name || "";
const description = attributeItem.attributes.description;
const status = requirementData.attributes.status || "";
const checks = attributeItem.attributes.attributes.check_ids || [];
const framework = findOrCreateFramework(frameworks, frameworkName);
const category = findOrCreateCategory(framework.categories, categoryName);
// Use a single control per category to keep a flat 2-level structure
const control = findOrCreateControl(category.controls, categoryName);
const finalStatus: RequirementStatus = status as RequirementStatus;
const requirement: Requirement = {
name: requirementName ? `${id} - ${requirementName}` : id,
description,
status: finalStatus,
check_ids: checks,
...getStatusCounters(finalStatus),
ccm_lite: attrs.CCMLite,
iaas: attrs.IaaS,
paas: attrs.PaaS,
saas: attrs.SaaS,
scope_applicability: attrs.ScopeApplicability,
};
control.requirements.push(requirement);
}
calculateFrameworkCounters(frameworks);
return frameworks;
};
export const toAccordionItems = (
data: Framework[],
scanId: string | undefined,
): AccordionItemProps[] => {
const safeId = scanId || "";
return data.flatMap((framework) =>
framework.categories.map((category) => ({
key: `${framework.name}-${category.name}`,
title: (
<ComplianceAccordionTitle
label={category.name}
pass={category.pass}
fail={category.fail}
manual={category.manual}
isParentLevel={true}
/>
),
content: "",
// Flatten: requirements are direct children of the section
items: category.controls.flatMap((control) =>
control.requirements.map((requirement, reqIndex) => ({
key: `${framework.name}-${category.name}-req-${reqIndex}`,
title: (
<ComplianceAccordionRequirementTitle
type=""
name={requirement.name}
status={requirement.status as FindingStatus}
/>
),
content: (
<ClientAccordionContent
key={`content-${framework.name}-${category.name}-req-${reqIndex}`}
requirement={requirement}
scanId={safeId}
framework={framework.name}
disableFindings={
requirement.check_ids.length === 0 && requirement.manual === 0
}
/>
),
items: [],
})),
),
})),
);
};

View File

@@ -189,6 +189,18 @@ export interface GenericAttributesMetadata {
Type: string | null; Type: string | null;
} }
export interface CSAAttributesMetadata {
Section: string;
CCMLite: string;
IaaS: string;
PaaS: string;
SaaS: string;
ScopeApplicability: Array<{
ReferenceId: string;
Identifiers: string[];
}>;
}
export interface CCCAttributesMetadata { export interface CCCAttributesMetadata {
FamilyName: string; FamilyName: string;
FamilyDescription: string; FamilyDescription: string;
@@ -227,6 +239,7 @@ export interface AttributesItemData {
| C5AttributesMetadata[] | C5AttributesMetadata[]
| MITREAttributesMetadata[] | MITREAttributesMetadata[]
| CCCAttributesMetadata[] | CCCAttributesMetadata[]
| CSAAttributesMetadata[]
| GenericAttributesMetadata[]; | GenericAttributesMetadata[];
check_ids: string[]; check_ids: string[];
// MITRE structure // MITRE structure