def resolve_findings(self, findings): rids = set() for f in findings: for r in f['Resources']: # Security hub invented some new arn format for a few resources... # detect that and normalize to something sane. if r['Id'].startswith( 'AWS') and r['Type'] == 'AwsIamAccessKey': if 'PrincipalName' in r['Details']['AwsIamAccessKey']: label = r['Details']['AwsIamAccessKey'][ 'PrincipalName'] else: label = r['Details']['AwsIamAccessKey']['UserName'] rids.add('arn:{}:iam::{}:user/{}'.format( get_partition(r['Region']), f['AwsAccountId'], label)) elif not r['Id'].startswith('arn'): if r['Type'] == 'AwsEc2Instance': rids.add('arn:{}:ec2:{}:{}:instance/{}'.format( get_partition(r['Region']), r['Region'], f['AwsAccountId'], r['Id'])) else: log.warning("security hub unknown id:%s rtype:%s", r['Id'], r['Type']) else: rids.add(r['Id']) return rids
def format_envelope(self, r): details = {} envelope = filter_empty({ 'Id': self.manager.get_arns([r])[0], 'Region': self.manager.config.region, 'Tags': {t['Key']: t['Value'] for t in r.get('Tags', [])}, 'Partition': get_partition(self.manager.config.region), 'Details': {self.resource_type: details}, 'Type': self.resource_type }) return envelope, details
def get_variables(self, variables=None): """Get runtime variables for policy interpolation. Runtime variables are merged with the passed in variables if any. """ # Global policy variable expansion, we have to carry forward on # various filter/action local vocabularies. Where possible defer # by using a format string. # # See https://github.com/cloud-custodian/cloud-custodian/issues/2330 if not variables: variables = {} partition = utils.get_partition(self.options.region) if 'mode' in self.data: if 'role' in self.data['mode'] and not self.data['mode'][ 'role'].startswith("arn:aws"): self.data['mode']['role'] = "arn:%s:iam::%s:role/%s" % \ (partition, self.options.account_id, self.data['mode']['role']) variables.update({ # standard runtime variables for interpolation 'account': '{account}', 'account_id': self.options.account_id, 'partition': partition, 'region': self.options.region, # non-standard runtime variables from local filter/action vocabularies # # notify action 'policy': self.data, 'event': '{event}', # mark for op action 'op': '{op}', 'action_date': '{action_date}', # tag action pyformat-date handling 'now': utils.FormatDate(datetime.utcnow()), # account increase limit action 'service': '{service}', # s3 set logging action :-( see if we can revisit this one. 'bucket_region': '{bucket_region}', 'bucket_name': '{bucket_name}', 'source_bucket_name': '{source_bucket_name}', 'source_bucket_region': '{source_bucket_region}', 'target_bucket_name': '{target_bucket_name}', 'target_prefix': '{target_prefix}', 'LoadBalancerName': '{LoadBalancerName}' }) return variables
def process(self, resources, event=None): alias = utils.get_account_alias_from_sts( utils.local_session(self.manager.session_factory)) partition = utils.get_partition(self.manager.config.region) message = { 'event': event, 'account_id': self.manager.config.account_id, 'partition': partition, 'account': alias, 'version': version, 'region': self.manager.config.region, 'execution_id': self.manager.ctx.execution_id, 'execution_start': self.manager.ctx.start_time, 'policy': self.manager.data} message['action'] = self.expand_variables(message) for batch in utils.chunks(resources, self.batch_size): message['resources'] = self.prepare_resources(batch) receipt = self.send_data_message(message) self.log.info("sent message:%s policy:%s template:%s count:%s" % ( receipt, self.manager.data['name'], self.data.get('template', 'default'), len(batch)))
def format_resource(self, r): details = {} for k in r: if isinstance(k, (list, dict)): continue details[k] = r[k] for f in self.fields: value = jmespath.search(f['expr'], r) if not value: continue details[f['key']] = value for k, v in details.items(): if isinstance(v, datetime): v = v.isoformat() elif isinstance(v, (list, dict)): v = dumps(v) elif isinstance(v, (int, float, bool)): v = str(v) else: continue details[k] = v[:SECHUB_VALUE_SIZE_LIMIT] details['c7n:resource-type'] = self.manager.type other = { 'Type': self.resource_type, 'Id': self.manager.get_arns([r])[0], 'Region': self.manager.config.region, 'Partition': get_partition(self.manager.config.region), 'Details': { self.resource_type: filter_empty(details) } } tags = {t['Key']: t['Value'] for t in r.get('Tags', [])} if tags: other['Tags'] = tags return other
def get_finding(self, resources, existing_finding_id, created_at, updated_at): policy = self.manager.ctx.policy model = self.manager.resource_type region = self.data.get('region', self.manager.config.region) if existing_finding_id: finding_id = existing_finding_id else: finding_id = '{}/{}/{}/{}'.format( # nosec self.manager.config.region, self.manager.config.account_id, hashlib.md5(json.dumps( # nosemgrep policy.data).encode('utf8')).hexdigest(), hashlib.md5(json.dumps(list(sorted( # nosemgrep [r[model.id] for r in resources]))).encode( 'utf8')).hexdigest()) finding = { "SchemaVersion": self.FindingVersion, "ProductArn": "arn:{}:securityhub:{}::product/cloud-custodian/cloud-custodian".format( get_partition(self.manager.config.region), region ), "AwsAccountId": self.manager.config.account_id, # Long search chain for description values, as this was # made required long after users had policies deployed, so # use explicit description, or policy description, or # explicit title, or policy name, in that order. "Description": self.data.get( "description", policy.data.get( "description", self.data.get('title', policy.name))).strip(), "Title": self.data.get("title", policy.name), 'Id': finding_id, "GeneratorId": policy.name, 'CreatedAt': created_at, 'UpdatedAt': updated_at, "RecordState": "ACTIVE", } severity = {'Product': 0, 'Normalized': 0, 'Label': 'INFORMATIONAL'} if self.data.get("severity") is not None: severity["Product"] = self.data["severity"] if self.data.get("severity_label") is not None: severity["Label"] = self.data["severity_label"] # severity_normalized To be deprecated per https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-findings-format.html#asff-severity # NOQA if self.data.get("severity_normalized") is not None: severity["Normalized"] = self.data["severity_normalized"] if severity: finding["Severity"] = severity recommendation = {} if self.data.get("recommendation"): recommendation["Text"] = self.data["recommendation"] if self.data.get("recommendation_url"): recommendation["Url"] = self.data["recommendation_url"] if recommendation: finding["Remediation"] = {"Recommendation": recommendation} if "confidence" in self.data: finding["Confidence"] = self.data["confidence"] if "criticality" in self.data: finding["Criticality"] = self.data["criticality"] if "compliance_status" in self.data: finding["Compliance"] = {"Status": self.data["compliance_status"]} if "record_state" in self.data: finding["RecordState"] = self.data["record_state"] fields = { 'resource': policy.resource_type, 'ProviderName': 'CloudCustodian', 'ProviderVersion': version } if "fields" in self.data: fields.update(self.data["fields"]) else: tags = {} for t in policy.tags: if ":" in t: k, v = t.split(":", 1) else: k, v = t, "" tags[k] = v fields.update(tags) if fields: finding["ProductFields"] = fields finding_resources = [] for r in resources: finding_resources.append(self.format_resource(r)) finding["Resources"] = finding_resources finding["Types"] = list(self.data["types"]) return filter_empty(finding)