Repository: cloud-custodian/cloud-custodian

Issue:
Add resource support for AWS Serverless Application Support
SAR allows customers to share/publish serverless functions as well as access public function code.  We would like to put some controls around this to detect if our customers are publishing things and maybe even limit the customers to consume the verified AWS customer functions.

Discussion:

Patch:
diff --git a/c7n/commands.py b/c7n/commands.py
index 2a29bf925..77c82b936 100644
--- a/c7n/commands.py
+++ b/c7n/commands.py
@@ -276,17 +276,21 @@ def run(options, policies):
             log.exception("Unable to assume role %s", options.assume_role)
             sys.exit(1)
 
+    errored_policies = []
     for policy in policies:
         try:
             policy()
         except Exception:
             exit_code = 2
+            errored_policies.append(policy.name)
             if options.debug:
                 raise
             log.exception(
                 "Error while executing policy %s, continuing" % (
                     policy.name))
     if exit_code != 0:
+        log.error("The following policies had errors while executing\n - %s" % (
+            "\n - ".join(errored_policies)))
         sys.exit(exit_code)
 
 
diff --git a/c7n/filters/iamaccess.py b/c7n/filters/iamaccess.py
index 8519fca7b..3c5f2b159 100644
--- a/c7n/filters/iamaccess.py
+++ b/c7n/filters/iamaccess.py
@@ -249,7 +249,7 @@ class PolicyChecker:
 
     def handle_aws_principalorgid(self, s, c):
         if not self.allowed_orgid:
-            return False
+            return True
         return bool(set(map(_account, c['values'])).difference(self.allowed_orgid))
 
 
diff --git a/c7n/loader.py b/c7n/loader.py
index 108b2516a..ed419ce99 100644
--- a/c7n/loader.py
+++ b/c7n/loader.py
@@ -130,9 +130,9 @@ class PolicyLoader:
         if missing:
             self._handle_missing_resources(policy_data, missing)
 
-        if validate is not False or (
+        if schema and (validate is not False or (
                 validate is None and
-                self.default_schema_validate):
+                self.default_schema_validate)):
             errors = self.validator.validate(policy_data, tuple(rtypes))
             if errors:
                 raise PolicyValidationError(
diff --git a/c7n/manager.py b/c7n/manager.py
index b0995227c..513eb9b29 100644
--- a/c7n/manager.py
+++ b/c7n/manager.py
@@ -111,8 +111,8 @@ class ResourceManager:
         original = len(resources)
         if event and event.get('debug', False):
             self.log.info(
-                "Filtering resources with %s", self.filters)
-        for f in self.filters:
+                "Filtering resources using %d filters", len(self.filters))
+        for idx, f in enumerate(self.filters, start=1):
             if not resources:
                 break
             rcount = len(resources)
@@ -122,7 +122,8 @@ class ResourceManager:
 
             if event and event.get('debug', False):
                 self.log.debug(
-                    "applied filter %s %d->%d", f, rcount, len(resources))
+                    "Filter #%d applied %d->%d filter: %s",
+                    idx, rcount, len(resources), dumps(f.data, indent=None))
         self.log.debug("Filtered from %d to %d %s" % (
             original, len(resources), self.__class__.__name__.lower()))
         return resources
diff --git a/c7n/resources/glue.py b/c7n/resources/glue.py
index 059c50440..e985ebc6d 100644
--- a/c7n/resources/glue.py
+++ b/c7n/resources/glue.py
@@ -11,12 +11,14 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
+import json
 from botocore.exceptions import ClientError
 from concurrent.futures import as_completed
 from c7n.manager import resources, ResourceManager
 from c7n.query import QueryResourceManager, TypeInfo
 from c7n.utils import local_session, chunks, type_schema
-from c7n.actions import BaseAction, ActionRegistry
+from c7n.actions import BaseAction, ActionRegistry, RemovePolicyBase
+from c7n.exceptions import PolicyValidationError
 from c7n.filters.vpc import SubnetFilter, SecurityGroupFilter
 from c7n.filters.related import RelatedResourceFilter
 from c7n.tags import universal_augment
@@ -637,3 +639,46 @@ class GlueCatalogCrossAccount(CrossAccountAccessFilter):
             policy = {}
         r[self.policy_annotation] = policy
         return policy
+
+
+@GlueDataCatalog.action_registry.register('remove-statements')
+class RemovePolicyStatement(RemovePolicyBase):
+    """Action to remove policy statements from Glue Data Catalog
+
+    :example:
+
+    .. code-block:: yaml
+
+           policies:
+              - name: remove-glue-catalog-cross-account
+                resource: aws.glue-catalog
+                filters:
+                  - type: cross-account
+                actions:
+                  - type: remove-statements
+                    statement_ids: matched
+    """
+    permissions = ('glue:PutResourcePolicy',)
+    policy_annotation = "c7n:AccessPolicy"
+
+    def validate(self):
+        for f in self.manager.iter_filters():
+            if isinstance(f, GlueCatalogCrossAccount):
+                return self
+        raise PolicyValidationError(
+            '`remove-statements` may only be used in '
+            'conjunction with `cross-account` filter on %s' % (self.manager.data,))
+
+    def process(self, resources):
+        resource = resources[0]
+        client = local_session(self.manager.session_factory).client('glue')
+        if resource.get(self.policy_annotation):
+            p = json.loads(resource[self.policy_annotation])
+            statements, found = self.process_policy(
+                p, resource, CrossAccountAccessFilter.annotation_key)
+            if not found:
+                return
+            if statements:
+                client.put_resource_policy(PolicyInJson=json.dumps(p))
+            else:
+                client.delete_resource_policy()
diff --git a/c7n/resources/resource_map.py b/c7n/resources/resource_map.py
index 2a56519dd..04b466b87 100644
--- a/c7n/resources/resource_map.py
+++ b/c7n/resources/resource_map.py
@@ -146,6 +146,7 @@ ResourceMap = {
     "aws.sagemaker-transform-job": "c7n.resources.sagemaker.SagemakerTransformJob",
     "aws.secrets-manager": "c7n.resources.secretsmanager.SecretsManager",
     "aws.security-group": "c7n.resources.vpc.SecurityGroup",
+    "aws.serverless-app": "c7n.resources.sar.ServerlessApp",
     "aws.shield-attack": "c7n.resources.shield.ShieldAttack",
     "aws.shield-protection": "c7n.resources.shield.ShieldProtection",
     "aws.simpledb": "c7n.resources.simpledb.SimpleDB",
diff --git a/c7n/resources/sar.py b/c7n/resources/sar.py
new file mode 100644
index 000000000..98c91d77d
--- /dev/null
+++ b/c7n/resources/sar.py
@@ -0,0 +1,87 @@
+# Copyright 2020 Kapil Thangavelu
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+from c7n.actions import Action
+from c7n.filters import CrossAccountAccessFilter
+from c7n.query import QueryResourceManager, TypeInfo
+from c7n.manager import resources
+from c7n.utils import type_schema, local_session
+
+
+@resources.register('serverless-app')
+class ServerlessApp(QueryResourceManager):
+
+    class resource_type(TypeInfo):
+        service = 'serverlessrepo'
+        arn = id = 'ApplicationId'
+        name = 'Name'
+        enum_spec = ('list_applications', 'Applications', None)
+        cfn_type = 'AWS::Serverless::Application'
+        default_report_fields = [
+            'ApplicationId', 'Name', 'CreationTime', 'SpdxLicenseId', 'Author']
+
+
+@ServerlessApp.action_registry.register('delete')
+class Delete(Action):
+
+    permissions = ('serverlessrepo:DeleteApplication',)
+    schema = type_schema('delete')
+
+    def process(self, resources):
+        client = local_session(
+            self.manager.session_factory).client('serverlessrepo')
+        for r in resources:
+            self.manager.retry(
+                client.delete_application,
+                ApplicationId=r['ApplicationId'])
+
+
+@ServerlessApp.filter_registry.register('cross-account')
+class CrossAccount(CrossAccountAccessFilter):
+
+    permissions = ('serverlessrepo:GetApplicationPolicy',)
+    policy_attribute = 'c7n:Policy'
+
+    def process(self, resources, event=None):
+        client = local_session(
+            self.manager.session_factory).client('serverlessrepo')
+        for r in resources:
+            if self.policy_attribute not in r:
+                r[self.policy_attribute] = p = client.get_application_policy(
+                    ApplicationId=r['ApplicationId'])
+                p.pop('ResponseMetadata', None)
+                self.transform_policy(p)
+        return super().process(resources)
+
+    def transform_policy(self, policy):
+        """Serverless Application repositories policies aren't valid iam policies.
+
+        Its a service specific spelling that violates basic constraints of the iam
+        schema. We attempt to normalize it to normal IAM spelling.
+        """
+        policy['Statement'] = policy.pop('Statements')
+        for s in policy['Statement']:
+            actions = ['serverlessrepo:%s' % a for a in s['Actions']]
+            s['Actions'] = actions
+            if 'Effect' not in s:
+                s['Effect'] = 'Allow'
+            if 'Principals' in s:
+                s['Principal'] = {'AWS': s.pop('Principals')}
+            if 'PrincipalOrgIDs' in s:
+                org_ids = s.pop('PrincipalOrgIDs')
+                if org_ids:
+                    s['Condition'] = {
+                        'StringEquals': {'aws:PrincipalOrgID': org_ids}}
+        return policy
diff --git a/tools/dev/cfntypedb.py b/tools/dev/cfntypedb.py
new file mode 100644
index 000000000..6caf4ab39
--- /dev/null
+++ b/tools/dev/cfntypedb.py
@@ -0,0 +1,32 @@
+import boto3
+import json
+import jmespath
+
+from botocore.paginate import Paginator
+
+
+def main():
+
+    client = boto3.client('cloudformation')
+
+    paginator = Paginator(
+        client.list_types,
+        {'input_token': 'NextToken',
+         'output_token': 'NextToken',
+         'result_key': 'TypeSummaries'},
+        client.meta.service_model.operation_model('ListTypes'))
+
+    results = paginator.paginate(Visibility='PUBLIC').build_full_result()
+    type_names = jmespath.search('TypeSummaries[].TypeName', results)
+
+    # manually add the ones missing
+    missing = (
+        'AWS::Serverless::Application',)
+    for m in missing:
+        if m not in type_names:
+            type_names.append(m)
+    print(json.dumps(sorted(type_names), indent=2))
+
+
+if __name__ == '__main__':
+    main()


Test patch:
diff --git a/tests/data/cfn-types.json b/tests/data/cfn-types.json
index 55aad97cc..32e60797c 100644
--- a/tests/data/cfn-types.json
+++ b/tests/data/cfn-types.json
@@ -4,7 +4,9 @@
   "AWS::ACMPCA::CertificateAuthorityActivation",
   "AWS::AccessAnalyzer::Analyzer",
   "AWS::AmazonMQ::Broker",
+  "AWS::AmazonMQ::Configuration",
   "AWS::AmazonMQ::ConfigurationAssociation",
+  "AWS::Amplify::App",
   "AWS::Amplify::Branch",
   "AWS::Amplify::Domain",
   "AWS::ApiGateway::Account",
@@ -37,6 +39,11 @@
   "AWS::ApiGatewayV2::Route",
   "AWS::ApiGatewayV2::RouteResponse",
   "AWS::ApiGatewayV2::Stage",
+  "AWS::AppConfig::Application",
+  "AWS::AppConfig::ConfigurationProfile",
+  "AWS::AppConfig::Deployment",
+  "AWS::AppConfig::DeploymentStrategy",
+  "AWS::AppConfig::Environment",
   "AWS::AppMesh::Mesh",
   "AWS::AppMesh::Route",
   "AWS::AppMesh::VirtualNode",
@@ -49,6 +56,7 @@
   "AWS::AppStream::StackFleetAssociation",
   "AWS::AppStream::StackUserAssociation",
   "AWS::AppStream::User",
+  "AWS::AppSync::ApiCache",
   "AWS::AppSync::ApiKey",
   "AWS::AppSync::DataSource",
   "AWS::AppSync::FunctionConfiguration",
@@ -91,7 +99,9 @@
   "AWS::CloudWatch::AnomalyDetector",
   "AWS::CloudWatch::CompositeAlarm",
   "AWS::CloudWatch::Dashboard",
+  "AWS::CloudWatch::InsightRule",
   "AWS::CodeBuild::Project",
+  "AWS::CodeBuild::ReportGroup",
   "AWS::CodeBuild::SourceCredential",
   "AWS::CodeCommit::Repository",
   "AWS::CodeDeploy::Application",
@@ -103,9 +113,10 @@
   "AWS::CodePipeline::Webhook",
   "AWS::CodeStar::GitHubRepository",
   "AWS::CodeStarConnections::Connection",
+  "AWS::CodeStarNotifications::NotificationRule",
   "AWS::Cognito::IdentityPool",
-    "AWS::Cognito::IdentityPoolRoleAttachment",
-    "AWS::Cognito::UserPool",    
+  "AWS::Cognito::IdentityPoolRoleAttachment",
+  "AWS::Cognito::UserPool",
   "AWS::Cognito::UserPoolClient",
   "AWS::Cognito::UserPoolDomain",
   "AWS::Cognito::UserPoolGroup",
@@ -134,6 +145,7 @@
   "AWS::DMS::ReplicationInstance",
   "AWS::DMS::ReplicationSubnetGroup",
   "AWS::DMS::ReplicationTask",
+  "AWS::DataPipeline::Pipeline",
   "AWS::Detective::Graph",
   "AWS::Detective::MemberInvitation",
   "AWS::DirectoryService::MicrosoftAD",
@@ -203,15 +215,18 @@
   "AWS::EC2::VPNGateway",
   "AWS::EC2::VPNGatewayRoutePropagation",
   "AWS::EC2::Volume",
+  "AWS::EC2::VolumeAttachment",
   "AWS::ECR::Repository",
   "AWS::ECS::Cluster",
   "AWS::ECS::PrimaryTaskSet",
   "AWS::ECS::Service",
   "AWS::ECS::TaskDefinition",
   "AWS::ECS::TaskSet",
+  "AWS::EFS::AccessPoint",
   "AWS::EFS::FileSystem",
   "AWS::EFS::MountTarget",
   "AWS::EKS::Cluster",
+  "AWS::EKS::Nodegroup",
   "AWS::EMR::Cluster",
   "AWS::EMR::InstanceFleetConfig",
   "AWS::EMR::InstanceGroupConfig",
@@ -234,6 +249,10 @@
   "AWS::ElasticLoadBalancingV2::LoadBalancer",
   "AWS::ElasticLoadBalancingV2::TargetGroup",
   "AWS::Elasticsearch::Domain",
+  "AWS::EventSchemas::Discoverer",
+  "AWS::EventSchemas::Registry",
+  "AWS::EventSchemas::RegistryPolicy",
+  "AWS::EventSchemas::Schema",
   "AWS::Events::EventBus",
   "AWS::Events::EventBusPolicy",
   "AWS::Events::Rule",
@@ -243,6 +262,13 @@
   "AWS::GameLift::Alias",
   "AWS::GameLift::Build",
   "AWS::GameLift::Fleet",
+  "AWS::GameLift::GameSessionQueue",
+  "AWS::GameLift::MatchmakingConfiguration",
+  "AWS::GameLift::MatchmakingRuleSet",
+  "AWS::GameLift::Script",
+  "AWS::GlobalAccelerator::Accelerator",
+  "AWS::GlobalAccelerator::EndpointGroup",
+  "AWS::GlobalAccelerator::Listener",
   "AWS::Glue::Classifier",
   "AWS::Glue::Connection",
   "AWS::Glue::Crawler",
@@ -289,6 +315,7 @@
   "AWS::IAM::UserToGroupAddition",
   "AWS::ImageBuilder::Component",
   "AWS::ImageBuilder::DistributionConfiguration",
+  "AWS::ImageBuilder::Image",
   "AWS::ImageBuilder::ImagePipeline",
   "AWS::ImageBuilder::ImageRecipe",
   "AWS::ImageBuilder::InfrastructureConfiguration",
@@ -327,20 +354,27 @@
   "AWS::LakeFormation::Permissions",
   "AWS::LakeFormation::Resource",
   "AWS::Lambda::Alias",
+  "AWS::Lambda::EventInvokeConfig",
   "AWS::Lambda::EventSourceMapping",
   "AWS::Lambda::Function",
   "AWS::Lambda::LayerVersion",
   "AWS::Lambda::LayerVersionPermission",
   "AWS::Lambda::Permission",
-    "AWS::Lambda::Version",
-    "AWS::Logs::LogGroup",
+  "AWS::Lambda::Version",
   "AWS::Logs::Destination",
+  "AWS::Logs::LogGroup",
   "AWS::Logs::LogStream",
   "AWS::Logs::MetricFilter",
   "AWS::Logs::SubscriptionFilter",
   "AWS::MSK::Cluster",
+  "AWS::Macie::CustomDataIdentifier",
+  "AWS::Macie::FindingsFilter",
+  "AWS::Macie::Session",
   "AWS::ManagedBlockchain::Member",
   "AWS::ManagedBlockchain::Node",
+  "AWS::MediaConvert::JobTemplate",
+  "AWS::MediaConvert::Preset",
+  "AWS::MediaConvert::Queue",
   "AWS::MediaLive::Channel",
   "AWS::MediaLive::Input",
   "AWS::MediaLive::InputSecurityGroup",
@@ -452,6 +486,7 @@
   "AWS::SecretsManager::Secret",
   "AWS::SecretsManager::SecretTargetAttachment",
   "AWS::SecurityHub::Hub",
+  "AWS::Serverless::Application",
   "AWS::ServiceCatalog::AcceptedPortfolioShare",
   "AWS::ServiceCatalog::CloudFormationProduct",
   "AWS::ServiceCatalog::CloudFormationProvisionedProduct",
diff --git a/tests/data/placebo/test_catalog_remove_matched/glue.GetDataCatalogEncryptionSettings_1.json b/tests/data/placebo/test_catalog_remove_matched/glue.GetDataCatalogEncryptionSettings_1.json
new file mode 100644
index 000000000..8a71abba2
--- /dev/null
+++ b/tests/data/placebo/test_catalog_remove_matched/glue.GetDataCatalogEncryptionSettings_1.json
@@ -0,0 +1,16 @@
+{
+    "status_code": 200,
+    "data": {
+        "DataCatalogEncryptionSettings": {
+            "EncryptionAtRest": {
+                "CatalogEncryptionMode": "SSE-KMS",
+                "SseAwsKmsKeyId": "arn:aws:kms:us-east-1:644160558196:key/ec3b9189-1247-4ecc-8bfd-c492f07621db"
+            },
+            "ConnectionPasswordEncryption": {
+                "ReturnConnectionPasswordEncrypted": true,
+                "AwsKmsKeyId": "arn:aws:kms:us-east-1:644160558196:key/358f7699-4ea5-455a-9c78-1c868301e5a8"
+            }
+        },
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_catalog_remove_matched/glue.GetResourcePolicy_1.json b/tests/data/placebo/test_catalog_remove_matched/glue.GetResourcePolicy_1.json
new file mode 100644
index 000000000..509e9b365
--- /dev/null
+++ b/tests/data/placebo/test_catalog_remove_matched/glue.GetResourcePolicy_1.json
@@ -0,0 +1,28 @@
+{
+    "status_code": 200,
+    "data": {
+        "PolicyInJson": "{\n  \"Version\" : \"2012-10-17\",\n  \"Statement\" : [ {\n    \"Sid\" : \"SpecificAllow\",\n    \"Effect\" : \"Allow\",\n    \"Principal\" : {\n      \"AWS\" : \"arn:aws:iam::644160558196:root\"\n    },\n    \"Action\" : \"glue:GetDatabase\",\n    \"Resource\" : \"arn:aws:glue:us-east-1:644160558196:catalog\"\n  }, {\n    \"Sid\" : \"CrossAccount\",\n    \"Effect\" : \"Allow\",\n    \"Principal\" : {\n      \"AWS\" : \"arn:aws:iam::123456789123:root\"\n    },\n    \"Action\" : \"glue:GetDatabase\",\n    \"Resource\" : \"arn:aws:glue:us-east-1:644160558196:catalog\"\n  } ]\n}",
+        "PolicyHash": "GA378QJBD16utk5xXRuSAw==",
+        "CreateTime": {
+            "__class__": "datetime",
+            "year": 2020,
+            "month": 5,
+            "day": 27,
+            "hour": 23,
+            "minute": 23,
+            "second": 42,
+            "microsecond": 470000
+        },
+        "UpdateTime": {
+            "__class__": "datetime",
+            "year": 2020,
+            "month": 5,
+            "day": 27,
+            "hour": 23,
+            "minute": 23,
+            "second": 42,
+            "microsecond": 470000
+        },
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_catalog_remove_matched/glue.GetResourcePolicy_2.json b/tests/data/placebo/test_catalog_remove_matched/glue.GetResourcePolicy_2.json
new file mode 100644
index 000000000..05a561141
--- /dev/null
+++ b/tests/data/placebo/test_catalog_remove_matched/glue.GetResourcePolicy_2.json
@@ -0,0 +1,28 @@
+{
+    "status_code": 200,
+    "data": {
+        "PolicyInJson": "{\n  \"Version\" : \"2012-10-17\",\n  \"Statement\" : [ {\n    \"Sid\" : \"SpecificAllow\",\n    \"Effect\" : \"Allow\",\n    \"Principal\" : {\n      \"AWS\" : \"arn:aws:iam::644160558196:root\"\n    },\n    \"Action\" : \"glue:GetDatabase\",\n    \"Resource\" : \"arn:aws:glue:us-east-1:644160558196:catalog\"\n  } ]\n}",
+        "PolicyHash": "5V9sCbFtrjJ6N1gmoX+K7Q==",
+        "CreateTime": {
+            "__class__": "datetime",
+            "year": 2020,
+            "month": 5,
+            "day": 27,
+            "hour": 23,
+            "minute": 23,
+            "second": 42,
+            "microsecond": 470000
+        },
+        "UpdateTime": {
+            "__class__": "datetime",
+            "year": 2020,
+            "month": 5,
+            "day": 27,
+            "hour": 23,
+            "minute": 23,
+            "second": 45,
+            "microsecond": 47000
+        },
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_catalog_remove_matched/glue.PutResourcePolicy_1.json b/tests/data/placebo/test_catalog_remove_matched/glue.PutResourcePolicy_1.json
new file mode 100644
index 000000000..b79241e8b
--- /dev/null
+++ b/tests/data/placebo/test_catalog_remove_matched/glue.PutResourcePolicy_1.json
@@ -0,0 +1,7 @@
+{
+    "status_code": 200,
+    "data": {
+        "PolicyHash": "GA378QJBD16utk5xXRuSAw==",
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_catalog_remove_matched/glue.PutResourcePolicy_2.json b/tests/data/placebo/test_catalog_remove_matched/glue.PutResourcePolicy_2.json
new file mode 100644
index 000000000..226db9174
--- /dev/null
+++ b/tests/data/placebo/test_catalog_remove_matched/glue.PutResourcePolicy_2.json
@@ -0,0 +1,7 @@
+{
+    "status_code": 200,
+    "data": {
+        "PolicyHash": "5V9sCbFtrjJ6N1gmoX+K7Q==",
+        "ResponseMetadata": {}
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_sar_cross_account/serverlessrepo.GetApplicationPolicy_1.json b/tests/data/placebo/test_sar_cross_account/serverlessrepo.GetApplicationPolicy_1.json
new file mode 100644
index 000000000..bedd5a866
--- /dev/null
+++ b/tests/data/placebo/test_sar_cross_account/serverlessrepo.GetApplicationPolicy_1.json
@@ -0,0 +1,31 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {},
+        "Statements": [
+            {
+                "Actions": [
+                    "Deploy",
+                    "UnshareApplication"
+                ],
+                "PrincipalOrgIDs": [
+                    "o-4adkskbcff"
+                ],
+                "Principals": [
+                    "*"
+                ],
+                "StatementId": "cab89702-05f0-4751-818e-ced6e98ef5f9"
+            },
+            {
+                "Actions": [
+                    "Deploy"
+                ],
+                "PrincipalOrgIDs": [],
+                "Principals": [
+                    "112233445566"
+                ],
+                "StatementId": "b364d84f-62d2-411c-9787-3636b2b1975c"
+            }
+        ]
+    }
+}
diff --git a/tests/data/placebo/test_sar_cross_account/serverlessrepo.ListApplications_1.json b/tests/data/placebo/test_sar_cross_account/serverlessrepo.ListApplications_1.json
new file mode 100644
index 000000000..60c699ae9
--- /dev/null
+++ b/tests/data/placebo/test_sar_cross_account/serverlessrepo.ListApplications_1.json
@@ -0,0 +1,18 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {},
+        "Applications": [
+            {
+                "ApplicationId": "arn:aws:serverlessrepo:us-east-1:644160558196:applications/GitterArchive",
+                "Author": "Kapil Thangavelu",
+                "CreationTime": "2020-05-30T15:33:25.878Z",
+                "Description": "archive gitter stuff",
+                "HomePageUrl": "https://github.com/kapilt",
+                "Labels": [],
+                "Name": "GitterArchive",
+                "SpdxLicenseId": "Apache-2.0"
+            }
+        ]
+    }
+}
\ No newline at end of file
diff --git a/tests/data/placebo/test_sar_query_app/serverlessrepo.ListApplications_1.json b/tests/data/placebo/test_sar_query_app/serverlessrepo.ListApplications_1.json
new file mode 100644
index 000000000..60c699ae9
--- /dev/null
+++ b/tests/data/placebo/test_sar_query_app/serverlessrepo.ListApplications_1.json
@@ -0,0 +1,18 @@
+{
+    "status_code": 200,
+    "data": {
+        "ResponseMetadata": {},
+        "Applications": [
+            {
+                "ApplicationId": "arn:aws:serverlessrepo:us-east-1:644160558196:applications/GitterArchive",
+                "Author": "Kapil Thangavelu",
+                "CreationTime": "2020-05-30T15:33:25.878Z",
+                "Description": "archive gitter stuff",
+                "HomePageUrl": "https://github.com/kapilt",
+                "Labels": [],
+                "Name": "GitterArchive",
+                "SpdxLicenseId": "Apache-2.0"
+            }
+        ]
+    }
+}
\ No newline at end of file
diff --git a/tests/test_glue.py b/tests/test_glue.py
index 41896ff17..5057a3215 100644
--- a/tests/test_glue.py
+++ b/tests/test_glue.py
@@ -13,6 +13,8 @@
 # limitations under the License.
 from .common import BaseTest
 import time
+import json
+from c7n.exceptions import PolicyValidationError
 
 
 class TestGlueConnections(BaseTest):
@@ -536,3 +538,52 @@ class TestGlueDataCatalog(BaseTest):
         )
         resources = p.run()
         self.assertEqual(len(resources), 1)
+
+    def test_catalog_remove_matched(self):
+        session_factory = self.replay_flight_data("test_catalog_remove_matched")
+        client = session_factory().client("glue")
+        client.put_resource_policy(PolicyInJson=json.dumps(
+            {
+                "Version": "2012-10-17",
+                "Statement": [
+                    {
+                        "Sid": "SpecificAllow",
+                        "Effect": "Allow",
+                        "Principal": {"AWS": "arn:aws:iam::644160558196:root"},
+                        "Action": "glue:GetDatabase",
+                        "Resource": "arn:aws:glue:us-east-1:644160558196:catalog"
+                    },
+                    {
+                        "Sid": "CrossAccount",
+                        "Effect": "Allow",
+                        "Principal": {"AWS": "arn:aws:iam::123456789123:root"},
+                        "Action": "glue:GetDatabase",
+                        "Resource": "arn:aws:glue:us-east-1:644160558196:catalog"
+                    },
+                ]
+            }))
+        p = self.load_policy(
+            {
+                "name": "glue-catalog-rm-matched",
+                "resource": "glue-catalog",
+                "filters": [{"type": "cross-account"}],
+                "actions": [{"type": "remove-statements", "statement_ids": "matched"}],
+            },
+            session_factory=session_factory,
+        )
+        resources = p.run()
+        self.assertEqual(len(resources), 1)
+        data = json.loads(client.get_resource_policy().get("PolicyInJson"))
+        self.assertEqual(len(data.get('Statement')), 1)
+        self.assertEqual([s['Sid'] for s in data.get('Statement')], ["SpecificAllow"])
+
+    def test_remove_statements_validation_error(self):
+        self.assertRaises(
+            PolicyValidationError,
+            self.load_policy,
+            {
+                "name": "glue-catalog-remove-matched",
+                "resource": "glue-catalog",
+                "actions": [{"type": "remove-statements", "statement_ids": "matched"}],
+            }
+        )
diff --git a/tests/test_iam.py b/tests/test_iam.py
index 47b9ad821..fa06bb5e4 100644
--- a/tests/test_iam.py
+++ b/tests/test_iam.py
@@ -1582,6 +1582,33 @@ class CrossAccountChecker(TestCase):
             violations = checker.check(p)
             self.assertEqual(bool(violations), expected)
 
+    def test_principal_org_id(self):
+        statements = [
+            {'Actions': ['Deploy', 'UnshareApplication'],
+             'Principal': ['*'],
+             'StatementId': 'cab89702-05f0-4751-818e-ced6e98ef5f9',
+             'Effect': 'Allow',
+             'Condition': {
+                 'StringEquals': {
+                     'aws:PrincipalOrgID': ['o-4pmkskbcf9']}}},
+            {'Actions': ['Deploy'],
+             'Principal': ['619193117841'],
+             'StatementId': 'b364d84f-62d2-411c-9787-3636b2b1975c',
+             'Effect': 'Allow'}
+        ]
+
+        checker = PolicyChecker({
+            'allowed_orgid': ['o-4pmkskbcf9']})
+
+        for statement, expected in zip(statements, [False, True]):
+            self.assertEqual(
+                bool(checker.handle_statement(statement)), expected)
+
+        checker = PolicyChecker({})
+        for statement, expected in zip(statements, [True, True]):
+            self.assertEqual(
+                bool(checker.handle_statement(statement)), expected)
+
     def test_s3_policies(self):
         policies = load_data("iam/s3-policies.json")
         checker = PolicyChecker(
diff --git a/tests/test_policy.py b/tests/test_policy.py
index 7896183fb..823f55817 100644
--- a/tests/test_policy.py
+++ b/tests/test_policy.py
@@ -206,8 +206,13 @@ class PolicyMetaLint(BaseTest):
             if rtype is not None:
                 resource_cfn_types.add(rtype)
         cfn_types = set(load_data('cfn-types.json'))
+        missing = set()
         for rtype in resource_cfn_types:
-            assert rtype in cfn_types, "invalid cfn %s" % rtype
+            if rtype not in cfn_types:
+                missing.add(rtype)
+        if missing:
+            raise AssertionError("Bad cfn types:\n %s" % (
+                "\n".join(sorted(missing))))
 
     def test_securityhub_resource_support(self):
         session = fake_session()._session
diff --git a/tests/test_sar.py b/tests/test_sar.py
new file mode 100644
index 000000000..19e3c7e8c
--- /dev/null
+++ b/tests/test_sar.py
@@ -0,0 +1,50 @@
+# Copyright 2020 Kapil Thangavelu
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+from .common import BaseTest
+
+
+class SARTest(BaseTest):
+
+    def test_query(self):
+        factory = self.replay_flight_data('test_sar_query_app')
+        p = self.load_policy({
+            'name': 'test-sar',
+            'resource': 'aws.serverless-app'},
+            session_factory=factory)
+        resources = p.run()
+        self.assertEqual(len(resources), 1)
+        self.assertEqual(resources[0]['Name'], 'GitterArchive')
+
+    def test_cross_account(self):
+        factory = self.replay_flight_data('test_sar_cross_account')
+        p = self.load_policy({
+            'name': 'test-sar',
+            'resource': 'aws.serverless-app',
+            'filters': [{
+                'type': 'cross-account',
+                'whitelist_orgids': ['o-4adkskbcff']
+            }]},
+            session_factory=factory)
+        resources = p.run()
+        self.assertEqual(len(resources), 1)
+        self.maxDiff = None
+        self.assertEqual(
+            resources[0]['CrossAccountViolations'], [
+                {'Actions': ['serverlessrepo:Deploy'],
+                 'Effect': 'Allow',
+                 'Principal': {'AWS': ['112233445566']},
+                 'StatementId': 'b364d84f-62d2-411c-9787-3636b2b1975c'}
+            ])
diff --git a/tests/test_vpc.py b/tests/test_vpc.py
index a7c1e072f..7c6814c1a 100644
--- a/tests/test_vpc.py
+++ b/tests/test_vpc.py
@@ -2564,7 +2564,8 @@ class EndpointTest(BaseTest):
                 'name': 'vpc-endpoint-cross-account',
                 'resource': 'vpc-endpoint',
                 'filters': [
-                    {'type': 'cross-account'}
+                    {'type': 'cross-account',
+                     'whitelist_orgids': ['o-4amkskbcf1']}
                 ]
             },
             session_factory=session_factory