Пример #1
0
 def test_modes(self):
     report = deprecated.Report(
         "some-policy",
         mode=[
             deprecated.field('foo', 'bar', '2021-06-30'),
             deprecated.field('baz', 'yet', '2021-06-30'),
         ])
     self.assertTrue(report)
     self.assertEqual(
         report.format(),
         dedent("""
         policy 'some-policy'
           mode:
             field 'foo' has been deprecated (replaced by 'bar')
             field 'baz' has been deprecated (replaced by 'yet')
         """)[1:-1])
Пример #2
0
 def test_conditions(self):
     report = deprecated.Report(
         "some-policy",
         conditions=[
             deprecated.field('start', 'value filter in condition block',
                              '2021-06-30'),
             deprecated.field('end', 'value filter in condition block',
                              '2021-06-30'),
         ])
     self.assertTrue(report)
     self.assertEqual(
         report.format(),
         dedent("""
         policy 'some-policy'
           condition:
             field 'start' has been deprecated (replaced by value filter in condition block)
             field 'end' has been deprecated (replaced by value filter in condition block)
         """)[1:-1])
Пример #3
0
 def test_field(self):
     deprecation = deprecated.field('severity_normalized', 'severity_label',
                                    '2021-06-30')
     self.assertTrue(deprecation.check({'severity_normalized': '10'}))
     self.assertFalse(deprecation.check({'no-match': 'ignored'}))
     self.assertEqual(
         str(deprecation),
         "field 'severity_normalized' has been deprecated (replaced by 'severity_label')"
     )
Пример #4
0
 def test_footnotes(self):
     footnotes = deprecated.Footnotes()
     report = deprecated.Report(
         "some-policy",
         mode=[
             deprecated.field('foo', 'bar'),
             deprecated.field('baz', 'yet', '2021-06-30'),
         ],
         actions=[
             deprecated.Context(
                 'mark-for-op:',
                 deprecated.optional_fields(
                     ('hours', 'days'),
                     link="http://docs.example.com/deprecations/foo#time")),
             deprecated.Context(
                 'mark-for-op:',
                 deprecated.optional_field(
                     'tag', '2021-06-30',
                     "http://docs.example.com/deprecations/foo#tag")),
         ])
     self.assertTrue(report)
     self.assertEqual(report.format(footnotes=footnotes),
                      dedent("""
         policy 'some-policy'
           mode:
             field 'foo' has been deprecated (replaced by 'bar')
             field 'baz' has been deprecated (replaced by 'yet') [1]
           actions:
             mark-for-op: optional fields deprecated (one of 'hours' or 'days' must be specified) [2]
             mark-for-op: optional field 'tag' deprecated (must be specified) [3]
         """)[1:-1])  # noqa
     self.assertEqual(footnotes(),
                      dedent("""
         [1] Will be removed after 2021-06-30
         [2] See http://docs.example.com/deprecations/foo#time
         [3] See http://docs.example.com/deprecations/foo#tag, will become an error after 2021-06-30
         """)[1:-1])  # noqa
Пример #5
0
class PostFinding(Action):
    """Report a finding to AWS Security Hub.

    Custodian acts as a finding provider, allowing users to craft
    policies that report to the AWS SecurityHub in the AWS Security Finding Format documented at
    https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-findings-format.html

    For resources that are taggable, we will tag the resource with an identifier
    such that further findings generate updates.

    Example generate a finding for accounts that don't have shield enabled.

    Note with Cloud Custodian (0.9+) you need to enable the Custodian integration
    to post-findings, see Getting Started with :ref:`Security Hub <aws-securityhub>`.

    :example:

    .. code-block:: yaml

      policies:

       - name: account-shield-enabled
         resource: account
         filters:
           - shield-enabled
         actions:
           - type: post-finding
             description: |
                Shield should be enabled on account to allow for DDOS protection (1 time 3k USD Charge).
             severity_label: LOW
             types:
               - "Software and Configuration Checks/Industry and Regulatory Standards/NIST CSF Controls (USA)"
             recommendation: "Enable shield"
             recommendation_url: "https://www.example.com/policies/AntiDDoS.html"
             confidence: 100
             compliance_status: FAILED

    """ # NOQA

    deprecations = (
        deprecated.field('severity_normalized', 'severity_label'),
    )

    FindingVersion = "2018-10-08"

    permissions = ('securityhub:BatchImportFindings',)

    resource_type = ""

    schema_alias = True
    schema = type_schema(
        "post-finding",
        required=["types"],
        title={"type": "string", 'default': 'policy.name'},
        description={'type': 'string', 'default':
            'policy.description, or if not defined in policy then policy.name'},
        severity={"type": "number", 'default': 0},
        severity_normalized={"type": "number", "min": 0, "max": 100, 'default': 0},
        severity_label={
            "type": "string", 'default': 'INFORMATIONAL',
            "enum": ["INFORMATIONAL", "LOW", "MEDIUM", "HIGH", "CRITICAL"],
        },
        confidence={"type": "number", "min": 0, "max": 100},
        criticality={"type": "number", "min": 0, "max": 100},
        # Cross region aggregation
        region={'type': 'string', 'description': 'cross-region aggregation target'},
        recommendation={"type": "string"},
        recommendation_url={"type": "string"},
        fields={"type": "object"},
        batch_size={'type': 'integer', 'minimum': 1, 'maximum': 100, 'default': 1},
        types={
            "type": "array",
            "minItems": 1,
            "items": {"type": "string"},
        },
        compliance_status={
            "type": "string",
            "enum": ["PASSED", "WARNING", "FAILED", "NOT_AVAILABLE"],
        },
        record_state={
            "type": "string", 'default': 'ACTIVE',
            "enum": ["ACTIVE", "ARCHIVED"],
        },
    )

    NEW_FINDING = 'New'

    def validate(self):
        for finding_type in self.data["types"]:
            if finding_type.count('/') > 2 or finding_type.split('/')[0] not in FindingTypes:
                raise PolicyValidationError(
                    "Finding types must be in the format 'namespace/category/classifier'."
                    " Found {}. Valid namespace values are: {}.".format(
                        finding_type, " | ".join([ns for ns in FindingTypes])))

    def get_finding_tag(self, resource):
        finding_tag = None
        tags = resource.get('Tags', [])

        finding_key = '{}:{}'.format('c7n:FindingId',
            self.data.get('title', self.manager.ctx.policy.name))

        # Support Tags as dictionary
        if isinstance(tags, dict):
            return tags.get(finding_key)

        # Support Tags as list of {'Key': 'Value'}
        for t in tags:
            key = t['Key']
            value = t['Value']
            if key == finding_key:
                finding_tag = value
        return finding_tag

    def group_resources(self, resources):
        grouped_resources = {}
        for r in resources:
            finding_tag = self.get_finding_tag(r) or self.NEW_FINDING
            grouped_resources.setdefault(finding_tag, []).append(r)
        return grouped_resources

    def process(self, resources, event=None):
        region_name = self.data.get('region', self.manager.config.region)
        client = local_session(
            self.manager.session_factory).client(
                "securityhub", region_name=region_name)

        now = datetime.now(tzutc()).isoformat()
        # default batch size to one to work around security hub console issue
        # which only shows a single resource in a finding.
        batch_size = self.data.get('batch_size', 1)
        stats = Counter()
        for resource_set in chunks(resources, batch_size):
            findings = []
            for key, grouped_resources in self.group_resources(resource_set).items():
                for resource in grouped_resources:
                    stats['Finding'] += 1
                    if key == self.NEW_FINDING:
                        finding_id = None
                        created_at = now
                        updated_at = now
                    else:
                        finding_id, created_at = self.get_finding_tag(
                            resource).split(':', 1)
                        updated_at = now

                    finding = self.get_finding(
                        [resource], finding_id, created_at, updated_at)
                    findings.append(finding)
                    if key == self.NEW_FINDING:
                        stats['New'] += 1
                        # Tag resources with new finding ids
                        tag_action = self.manager.action_registry.get('tag')
                        if tag_action is None:
                            continue
                        tag_action({
                            'key': '{}:{}'.format(
                                'c7n:FindingId',
                                self.data.get(
                                    'title', self.manager.ctx.policy.name)),
                            'value': '{}:{}'.format(
                                finding['Id'], created_at)},
                            self.manager).process([resource])
                    else:
                        stats['Update'] += 1
            import_response = client.batch_import_findings(
                Findings=findings)
            if import_response['FailedCount'] > 0:
                stats['Failed'] += import_response['FailedCount']
                self.log.error(
                    "import_response=%s" % (import_response))
        self.log.debug(
            "policy:%s securityhub %d findings resources %d new %d updated %d failed",
            self.manager.ctx.policy.name,
            stats['Finding'],
            stats['New'],
            stats['Update'],
            stats['Failed'])

    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)

    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

    filter_empty = staticmethod(filter_empty)

    def format_resource(self, r):
        raise NotImplementedError("subclass responsibility")
Пример #6
0
class Policy:

    log = logging.getLogger('custodian.policy')

    deprecations = (
        deprecated.field('region', 'region in condition block'),
        deprecated.field('start', 'value filter in condition block'),
        deprecated.field('end', 'value filter in condition block'),
    )

    def __init__(self, data, options, session_factory=None):
        self.data = data
        self.options = options
        assert "name" in self.data
        if session_factory is None:
            session_factory = get_session_factory(self.provider_name, options)
        self.session_factory = session_factory
        self.ctx = ExecutionContext(self.session_factory, self, self.options)
        self.resource_manager = self.load_resource_manager()
        self.conditions = PolicyConditions(self, data)

    def __repr__(self):
        return "<Policy resource:%s name:%s region:%s>" % (
            self.resource_type, self.name, self.options.region)

    @property
    def name(self):
        return self.data['name']

    @property
    def resource_type(self):
        return self.data['resource']

    @property
    def provider_name(self):
        if '.' in self.resource_type:
            provider_name, resource_type = self.resource_type.split('.', 1)
        else:
            provider_name = 'aws'
        return provider_name

    def is_runnable(self, event=None):
        return self.conditions.evaluate(event)

    # Runtime circuit breakers
    @property
    def max_resources(self):
        return self.data.get('max-resources')

    @property
    def max_resources_percent(self):
        return self.data.get('max-resources-percent')

    @property
    def tags(self):
        return self.data.get('tags', ())

    def get_cache(self):
        return self.resource_manager._cache

    @property
    def execution_mode(self):
        return self.data.get('mode', {'type': 'pull'})['type']

    def get_execution_mode(self):
        try:
            exec_mode = execution[self.execution_mode]
        except KeyError:
            return None
        return exec_mode(self)

    @property
    def is_lambda(self):
        if 'mode' not in self.data:
            return False
        return True

    def validate(self):
        self.conditions.validate()
        m = self.get_execution_mode()
        if m is None:
            raise PolicyValidationError("Invalid Execution mode in policy %s" %
                                        (self.data, ))
        m.validate()
        self.validate_policy_start_stop()
        self.resource_manager.validate()
        for f in self.resource_manager.filters:
            f.validate()
        for a in self.resource_manager.actions:
            a.validate()

    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 expand_variables(self, variables):
        """Expand variables in policy data.

        Updates the policy data in-place.
        """
        # format string values returns a copy
        updated = utils.format_string_values(self.data, **variables)

        # Several keys should only be expanded at runtime, perserve them.
        if 'member-role' in updated.get('mode', {}):
            updated['mode']['member-role'] = self.data['mode']['member-role']

        # Update ourselves in place
        self.data = updated
        # Reload filters/actions using updated data, we keep a reference
        # for some compatiblity preservation work.
        m = self.resource_manager
        self.resource_manager = self.load_resource_manager()

        # XXX: Compatiblity hack
        # Preserve notify action subject lines which support
        # embedded jinja2 as a passthrough to the mailer.
        for old_a, new_a in zip(m.actions, self.resource_manager.actions):
            if old_a.type == 'notify' and 'subject' in old_a.data:
                new_a.data['subject'] = old_a.data['subject']

    def push(self, event, lambda_ctx=None):
        mode = self.get_execution_mode()
        return mode.run(event, lambda_ctx)

    def provision(self):
        """Provision policy as a lambda function."""
        mode = self.get_execution_mode()
        return mode.provision()

    def poll(self):
        """Query resources and apply policy."""
        mode = self.get_execution_mode()
        return mode.run()

    def get_permissions(self):
        """get permissions needed by this policy"""
        permissions = set()
        permissions.update(self.resource_manager.get_permissions())
        for f in self.resource_manager.filters:
            permissions.update(f.get_permissions())
        for a in self.resource_manager.actions:
            permissions.update(a.get_permissions())
        return permissions

    def _trim_runtime_filters(self):
        from c7n.filters.core import trim_runtime
        trim_runtime(self.conditions.filters)
        trim_runtime(self.resource_manager.filters)

    def __call__(self):
        """Run policy in default mode"""
        mode = self.get_execution_mode()
        if (isinstance(mode, ServerlessExecutionMode) or self.options.dryrun):
            self._trim_runtime_filters()

        if self.options.dryrun:
            resources = PullMode(self).run()
        elif not self.is_runnable():
            resources = []
        elif isinstance(mode, ServerlessExecutionMode):
            resources = mode.provision()
        else:
            resources = mode.run()
        # clear out resource manager post run, to clear cache
        self.resource_manager = self.load_resource_manager()
        return resources

    run = __call__

    def _write_file(self, rel_path, value):
        if isinstance(self.ctx.output, NullBlobOutput):
            return
        with open(os.path.join(self.ctx.log_dir, rel_path), 'w') as fh:
            fh.write(value)

    def load_resource_manager(self):
        factory = get_resource_class(self.data.get('resource'))
        return factory(self.ctx, self.data)

    def validate_policy_start_stop(self):
        policy_name = self.data.get('name')
        policy_tz = self.data.get('tz')
        policy_start = self.data.get('start')
        policy_end = self.data.get('end')

        if policy_tz:
            try:
                p_tz = tzutil.gettz(policy_tz)
            except Exception as e:
                raise PolicyValidationError(
                    "Policy: %s TZ not parsable: %s, %s" %
                    (policy_name, policy_tz, e))

            # Type will be tzwin on windows, but tzwin is null on linux
            if not (isinstance(p_tz, tzutil.tzfile) or
                    (tzutil.tzwin and isinstance(p_tz, tzutil.tzwin))):
                raise PolicyValidationError("Policy: %s TZ not parsable: %s" %
                                            (policy_name, policy_tz))

        for i in [policy_start, policy_end]:
            if i:
                try:
                    parser.parse(i)
                except Exception as e:
                    raise ValueError(
                        "Policy: %s Date/Time not parsable: %s, %s" %
                        (policy_name, i, e))

    def get_deprecations(self):
        """Return any matching deprecations for the policy fields itself."""
        return deprecated.check_deprecations(self, "policy")