|
| 1 | +from datetime import datetime |
| 2 | +from functools import partial |
| 3 | +from typing import ClassVar, Dict, Optional, List, Tuple, Type, Any |
| 4 | + |
| 5 | +from attr import define, field |
| 6 | + |
| 7 | +from fix_plugin_gcp.gcp_client import GcpApiSpec |
| 8 | +from fix_plugin_gcp.resources.base import ( |
| 9 | + GcpRegion, |
| 10 | + GcpResource, |
| 11 | + GcpZone, |
| 12 | + GraphBuilder, |
| 13 | + GcpErrorHandler, |
| 14 | + GcpProject, |
| 15 | + GcpExpectedErrorCodes, |
| 16 | +) |
| 17 | +from fixlib.baseresources import SEVERITY_MAPPING, Finding, Severity |
| 18 | +from fixlib.json_bender import Bender, S, Bend |
| 19 | +from fixlib.types import Json |
| 20 | + |
| 21 | + |
| 22 | +@define(eq=False, slots=False) |
| 23 | +class GcpSourceProperties: |
| 24 | + kind: ClassVar[str] = "gcp_source_properties" |
| 25 | + mapping: ClassVar[Dict[str, Bender]] = { |
| 26 | + "recommendation": S("Recommendation"), |
| 27 | + "explanation": S("Explanation"), |
| 28 | + } |
| 29 | + recommendation: Optional[str] = field(default=None) |
| 30 | + explanation: Optional[str] = field(default=None) |
| 31 | + |
| 32 | + |
| 33 | +@define(eq=False, slots=False) |
| 34 | +class GcpFinding: |
| 35 | + kind: ClassVar[str] = "gcp_finding" |
| 36 | + mapping: ClassVar[Dict[str, Bender]] = { |
| 37 | + "severity": S("severity"), |
| 38 | + "source_properties": S("sourceProperties", default={}) >> Bend(GcpSourceProperties.mapping), |
| 39 | + "description": S("description"), |
| 40 | + "event_time": S("eventTime"), |
| 41 | + "parent_display_name": S("parentDisplayName"), |
| 42 | + # "access": S("access", default={}) >> Bend(GcpAccess.mapping), |
| 43 | + # "application": S("application", default={}) >> Bend(GcpApplication.mapping), |
| 44 | + # "attack_exposure": S("attackExposure", default={}) >> Bend(GcpAttackExposure.mapping), |
| 45 | + # "backup_disaster_recovery": S("backupDisasterRecovery", default={}) >> Bend(GcpBackupDisasterRecovery.mapping), |
| 46 | + # "canonical_name": S("canonicalName"), |
| 47 | + # "category": S("category"), |
| 48 | + # "cloud_armor": S("cloudArmor", default={}) >> Bend(GcpCloudArmor.mapping), |
| 49 | + # "cloud_dlp_data_profile": S("cloudDlpDataProfile", default={}) >> Bend(GcpCloudDlpDataProfile.mapping), |
| 50 | + # "cloud_dlp_inspection": S("cloudDlpInspection", default={}) >> Bend(GcpCloudDlpInspection.mapping), |
| 51 | + # "compliances": S("compliances", default=[]) >> ForallBend(GcpCompliance.mapping), |
| 52 | + # "connections": S("connections", default=[]) >> ForallBend(GcpConnection.mapping), |
| 53 | + # "contacts": S("contacts", default={}) >> MapDict(value_bender=Bend(GcpContactDetails.mapping)), |
| 54 | + # "containers": S("containers", default=[]) >> ForallBend(GcpContainer.mapping), |
| 55 | + # "create_time": S("createTime"), |
| 56 | + # "data_access_events": S("dataAccessEvents", default=[]) >> ForallBend(GcpDataAccessEvent.mapping), |
| 57 | + # "data_flow_events": S("dataFlowEvents", default=[]) >> ForallBend(GcpDataFlowEvent.mapping), |
| 58 | + # "database": S("database", default={}) >> Bend(GcpDatabase.mapping), |
| 59 | + # "exfiltration": S("exfiltration", default={}) >> Bend(GcpExfiltration.mapping), |
| 60 | + # "external_systems": S("externalSystems", default={}) |
| 61 | + # >> MapDict(value_bender=Bend(GcpGoogleCloudSecuritycenterV1ExternalSystem.mapping)), |
| 62 | + # "external_uri": S("externalUri"), |
| 63 | + # "files": S("files", default=[]) >> ForallBend(GcpFile.mapping), |
| 64 | + # "finding_class": S("findingClass"), |
| 65 | + # "group_memberships": S("groupMemberships", default=[]) >> ForallBend(GcpGroupMembership.mapping), |
| 66 | + # "iam_bindings": S("iamBindings", default=[]) >> ForallBend(GcpIamBinding.mapping), |
| 67 | + # "indicator": S("indicator", default={}) >> Bend(GcpIndicator.mapping), |
| 68 | + # "kernel_rootkit": S("kernelRootkit", default={}) >> Bend(GcpKernelRootkit.mapping), |
| 69 | + # "kubernetes": S("kubernetes", default={}) >> Bend(GcpKubernetes.mapping), |
| 70 | + # "load_balancers": S("loadBalancers", default=[]) >> ForallBend(S("name")), |
| 71 | + # "log_entries": S("logEntries", default=[]) >> ForallBend(GcpLogEntry.mapping), |
| 72 | + # "mitre_attack": S("mitreAttack", default={}) >> Bend(GcpMitreAttack.mapping), |
| 73 | + # "module_name": S("moduleName"), |
| 74 | + # "mute": S("mute"), |
| 75 | + # "mute_info": S("muteInfo", default={}) >> Bend(GcpMuteInfo.mapping), |
| 76 | + # "mute_initiator": S("muteInitiator"), |
| 77 | + # "mute_update_time": S("muteUpdateTime"), |
| 78 | + # "name": S("name"), |
| 79 | + # "next_steps": S("nextSteps"), |
| 80 | + # "notebook": S("notebook", default={}) >> Bend(GcpNotebook.mapping), |
| 81 | + # "org_policies": S("orgPolicies", default=[]) >> ForallBend(S("name")), |
| 82 | + # "parent": S("parent"), |
| 83 | + # "processes": S("processes", default=[]) >> ForallBend(GcpProcess.mapping), |
| 84 | + # "resource_name": S("resourceName"), |
| 85 | + # "security_marks": S("securityMarks", default={}) >> Bend(GcpSecurityMarks.mapping), |
| 86 | + # "security_posture": S("securityPosture", default={}) >> Bend(GcpSecurityPosture.mapping), |
| 87 | + # "state": S("state"), |
| 88 | + # "toxic_combination": S("toxicCombination", default={}) >> Bend(GcpToxicCombination.mapping), |
| 89 | + # "vulnerability": S("vulnerability", default={}) >> Bend(GcpVulnerability.mapping), |
| 90 | + } |
| 91 | + description: Optional[str] = field(default=None) |
| 92 | + event_time: Optional[datetime] = field(default=None) |
| 93 | + parent_display_name: Optional[str] = field(default=None) |
| 94 | + severity: Optional[str] = field(default=None) |
| 95 | + source_properties: Optional[GcpSourceProperties] = field(default=None) |
| 96 | + |
| 97 | + |
| 98 | +@define(eq=False, slots=False) |
| 99 | +class GcpFindingResource: |
| 100 | + kind: ClassVar[str] = "gcp_fingding_resource" |
| 101 | + mapping: ClassVar[Dict[str, Bender]] = { |
| 102 | + "cloud_provider": S("cloudProvider"), |
| 103 | + "display_name": S("displayName"), |
| 104 | + "location": S("location"), |
| 105 | + # "aws_metadata": S("awsMetadata", default={}) >> Bend(GcpAwsMetadata.mapping), |
| 106 | + # "azure_metadata": S("azureMetadata", default={}) >> Bend(GcpAzureMetadata.mapping), |
| 107 | + # "folders": S("folders", default=[]) >> ForallBend(GcpFolder.mapping), |
| 108 | + # "name": S("name"), |
| 109 | + # "organization": S("organization"), |
| 110 | + # "parent_display_name": S("parentDisplayName"), |
| 111 | + # "parent_name": S("parentName"), |
| 112 | + # "project_display_name": S("projectDisplayName"), |
| 113 | + # "project_name": S("projectName"), |
| 114 | + # "resource_path": S("resourcePath", default={}) >> Bend(GcpResourcePath.mapping), |
| 115 | + # "resource_path_string": S("resourcePathString"), |
| 116 | + # "service": S("service"), |
| 117 | + # "type": S("type"), |
| 118 | + } |
| 119 | + cloud_provider: Optional[str] = field(default=None) |
| 120 | + display_name: Optional[str] = field(default=None) |
| 121 | + location: Optional[str] = field(default=None) |
| 122 | + |
| 123 | + |
| 124 | +@define(eq=False, slots=False) |
| 125 | +class GcpSccFinding(GcpResource): |
| 126 | + kind: ClassVar[str] = "gcp_scc_finding" |
| 127 | + _model_export: ClassVar[bool] = False |
| 128 | + api_spec: ClassVar[GcpApiSpec] = GcpApiSpec( |
| 129 | + service="securitycenter", |
| 130 | + version="v1", |
| 131 | + accessors=["projects", "sources", "findings"], |
| 132 | + action="list", |
| 133 | + request_parameter={"parent": "projects/{project}/sources/-", "filter": 'state="ACTIVE"'}, |
| 134 | + request_parameter_in={"project"}, |
| 135 | + response_path="listFindingsResults", |
| 136 | + response_regional_sub_path=None, |
| 137 | + ) |
| 138 | + mapping: ClassVar[Dict[str, Bender]] = { |
| 139 | + "id": S("finding", "name"), |
| 140 | + "tags": S("labels", default={}), |
| 141 | + "name": S("finding", "name"), |
| 142 | + "ctime": S("creationTimestamp"), |
| 143 | + "finding_information": S("finding", default={}) >> Bend(GcpFinding.mapping), |
| 144 | + "resource_information": S("resource", default={}) >> Bend(GcpFindingResource.mapping), |
| 145 | + "state_change": S("stateChange"), |
| 146 | + } |
| 147 | + finding_information: Optional[GcpFinding] = field(default=None) |
| 148 | + resource_information: Optional[GcpFindingResource] = field(default=None) |
| 149 | + state_change: Optional[str] = field(default=None) |
| 150 | + |
| 151 | + def parse_finding(self, source: Json) -> Optional[Finding]: |
| 152 | + if finding := self.finding_information: |
| 153 | + description = finding.description |
| 154 | + if finding.source_properties: |
| 155 | + remediation = finding.source_properties.recommendation |
| 156 | + title = finding.source_properties.explanation or "unknown" |
| 157 | + else: |
| 158 | + remediation = None |
| 159 | + title = "unknown" |
| 160 | + source_finding = source.get("finding", {}) |
| 161 | + source_resource = source.get("resource", {}) |
| 162 | + details = source_finding.get("sourceProperties", {}) |
| 163 | + aws_metadata = source_resource.get("awsMetadata", {}) |
| 164 | + azure_metadata = source_resource.get("azureMetadata", {}) |
| 165 | + severity = SEVERITY_MAPPING.get(finding.severity or "") or Severity.medium |
| 166 | + return Finding( |
| 167 | + title, severity, description, remediation, finding.event_time, details | aws_metadata | azure_metadata |
| 168 | + ) |
| 169 | + return None |
| 170 | + |
| 171 | + @classmethod |
| 172 | + def collect_resources(cls, builder: GraphBuilder, **kwargs: Any) -> List[GcpResource]: |
| 173 | + def add_finding( |
| 174 | + provider: str, finding: Finding, clazz: Optional[Type[GcpResource]] = None, **node: Any |
| 175 | + ) -> None: |
| 176 | + if resource := builder.node(clazz=clazz or GcpResource, **node): |
| 177 | + resource.add_finding(provider, finding) |
| 178 | + |
| 179 | + if spec := cls.api_spec: |
| 180 | + with GcpErrorHandler( |
| 181 | + spec.action, |
| 182 | + builder.error_accumulator, |
| 183 | + spec.service, |
| 184 | + builder.region.safe_name if builder.region else None, |
| 185 | + GcpExpectedErrorCodes, |
| 186 | + f" in {builder.project.id} kind {cls.kind}", |
| 187 | + ): |
| 188 | + for item in builder.client.list(spec, **kwargs): |
| 189 | + if finding := GcpSccFinding.from_api(item, builder): |
| 190 | + if (ri := finding.resource_information) and (r_name := ri.display_name): |
| 191 | + provider = ri.cloud_provider or "google_cloud_scc" |
| 192 | + parsed_finding = finding.parse_finding(item) |
| 193 | + if not parsed_finding: |
| 194 | + continue |
| 195 | + if r_name == builder.project.id and ri.location is None: |
| 196 | + builder.after_collect_actions.append( |
| 197 | + partial( |
| 198 | + add_finding, |
| 199 | + provider.lower(), |
| 200 | + parsed_finding, |
| 201 | + GcpProject, |
| 202 | + id=r_name, |
| 203 | + ) |
| 204 | + ) |
| 205 | + |
| 206 | + def resolve_location( |
| 207 | + builder: GraphBuilder, location: str |
| 208 | + ) -> Tuple[Optional[GcpZone], Optional[GcpRegion]]: |
| 209 | + zone = builder.zone_by_name.get(location) |
| 210 | + region = builder.region_by_name.get(location) |
| 211 | + return zone, region |
| 212 | + |
| 213 | + if ri.location: |
| 214 | + zone, region = resolve_location(builder, ri.location) |
| 215 | + if zone: |
| 216 | + builder.after_collect_actions.append( |
| 217 | + partial( |
| 218 | + add_finding, |
| 219 | + provider.lower(), |
| 220 | + parsed_finding, |
| 221 | + GcpResource, |
| 222 | + id=r_name, |
| 223 | + _zone=zone, |
| 224 | + ) |
| 225 | + ) |
| 226 | + elif region: |
| 227 | + builder.after_collect_actions.append( |
| 228 | + partial( |
| 229 | + add_finding, |
| 230 | + provider.lower(), |
| 231 | + parsed_finding, |
| 232 | + GcpResource, |
| 233 | + id=r_name, |
| 234 | + _region=region, |
| 235 | + ) |
| 236 | + ) |
| 237 | + return [] |
| 238 | + |
| 239 | + |
| 240 | +resources: List[Type[GcpResource]] = [GcpSccFinding] |
0 commit comments