

本文属于机器翻译版本。若本译文内容与英语原文存在差异，则一律以英文原文为准。

# 定义 Guard 查询和过滤
<a name="query-and-filtering"></a>

本主题介绍编写查询以及在编写 Guard 规则子句时使用筛选。

## 先决条件
<a name="query-filtering-prerequisites"></a>

过滤是一个高级 AWS CloudFormation Guard 概念。我们建议您在学习筛选之前先阅读以下基础主题：
+ [什么是 AWS CloudFormation Guard？](what-is-guard.md)
+ [写作规则、条款](writing-rules.md)

## 定义查询
<a name="defining-queries"></a>

查询表达式是为遍历分层数据而编写的简单点 (`.`) 分隔表达式。查询表达式可以包括筛选表达式来定位值的子集。对查询进行评估时，它们会生成一组值，类似于 SQL 查询返回的结果集。

以下示例查询在 CloudFormation 模板中搜索`AWS::IAM::Role`资源。

```
Resources.*[ Type == 'AWS::IAM::Role' ]
```

查询遵循以下基本原则：
+ 当使用显式关键词时，查询的每个点 (`.`) 部分都会向下遍历层次结构，例如`Resources`或`Properties.Encrypted.`如果查询的任何部分与传入的数据不匹配，Guard 将引发检索错误。
+ 查询中使用通配符的 dot (`.`) 部分`*`会遍历该级别结构的所有值。
+ 查询中使用数组通配符的 dot (`.`) 部分会`[*]`遍历该数组的所有索引。
+ 可以通过在方括号内指定过滤器来筛选所有集合`[]`。可以通过以下方式遇到集合：
  + datum 中自然出现的数组是集合。下面是一些 示例：

    端口：`[20, 21, 110, 190]`

    标签：`[{"Key": "Stage", "Value": "PROD"}, {"Key": "App", "Value": "MyService"}]`
  + 遍历结构的所有值时 `Resources.*`
  + 任何查询结果本身都是一个集合，可以从中进一步筛选值。请参阅以下示例。

    ```
    # Query all resources
    let all_resources = Resource.*
    
    # Filter IAM resources from query results
    let iam_resources = %resources[ Type == /IAM/ ]
    
    # Further refine to get managed policies
    let managed_policies = %iam_resources[ Type == /ManagedPolicy/ ]
    
    # Traverse each managed policy
    %managed_policies {
        # Do something with each policy
    }
    ```

以下是示例 CloudFormation 模板片段。

```
Resources:
  SampleRole:
    Type: AWS::IAM::Role
    ...
  SampleInstance:
    Type: AWS::EC2::Instance
    ...
  SampleVPC:
     Type: AWS::EC2::VPC
    ...
  SampleSubnet1:
    Type: AWS::EC2::Subnet
    ...
  SampleSubnet2:
    Type: AWS::EC2::Subnet
    ...
```

基于此模板，遍历的路径为`SampleRole`，选择的最终值为。`Type: AWS::IAM::Role`

```
Resources:
  SampleRole:
    Type: AWS::IAM::Role
    ...
```

YAML 格式`Resources.*[ Type == 'AWS::IAM::Role' ]`的查询结果值如下例所示。

```
- Type: AWS::IAM::Role
  ...
```

您可以使用查询的一些方法如下：
+ 为变量分配查询，以便可以通过引用这些变量来访问查询结果。
+ 在查询之后使用一个针对每个选定值进行测试的块。
+ 直接将查询与基本子句进行比较。

## 为变量分配查询
<a name="queries-and-filtering-variables"></a>

Guard 支持在给定范围内的一次性变量赋值。有关 Guard 规则中变量的更多信息，请参阅[在 Guard 规则中分配和引用变量](variables.md)。

您可以将查询分配给变量，这样您就可以编写一次查询，然后在 Guard 规则的其他地方引用它们。参见以下变量赋值示例，这些变量赋值演示了本节后面讨论的查询原理。

```
#
# Simple query assignment
#
let resources = Resources.* # All resources

#
# A more complex query here (this will be explained below)
#
let iam_policies_allowing_log_creates = Resources.*[
    Type in [/IAM::Policy/, /IAM::ManagedPolicy/]
    some Properties.PolicyDocument.Statement[*] {
         some Action[*] == 'cloudwatch:CreateLogGroup'
         Effect == 'Allow'
    }
]
```

## 直接遍历分配给查询的变量中的值
<a name="variable-assigned-from-query"></a>

Guard 支持直接根据查询结果运行。在以下示例中，`when`模块针对 CloudFormation 模板中找到的每个`AWS::EC2::Volume`资源的`Encrypted``VolumeType`、和`AvailabilityZone`属性进行测试。

```
let ec2_volumes = Resources.*[ Type == 'AWS::EC2::Volume' ] 

when %ec2_volumes !empty {
    %ec2_volumes {
        Properties {
            Encrypted == true
            VolumeType in ['gp2', 'gp3']
            AvailabilityZone in ['us-west-2b', 'us-west-2c']
        }
    }
}
```

## 直接进行子句级别的比较
<a name="direct-clause-level-comparisons"></a>

Guard 还支持将查询作为直接比较的一部分。例如，请参阅以下内容。

```
let resources = Resources.*
    
    some %resources.Properties.Tags[*].Key == /PROD$/
    some %resources.Properties.Tags[*].Value == /^App/
```

在前面的示例中，以所示形式表示的两个子句（以`some`关键字开头）被视为独立子句，并且是分开计算的。

### 单一条款和区块条款形式
<a name="single-versus-block-clause-form"></a>

总而言之，前一节中显示的两个示例子句并不等同于以下块。

```
let resources = Resources.*

some %resources.Properties.Tags[*] {
    Key == /PROD$/
    Value == /^App/
}
```

此块查询集合中的每个`Tag`值，并将其属性值与预期的属性值进行比较。前一节中子句的组合形式对这两个子句进行独立评估。考虑以下输入。

```
Resources:
  ...
  MyResource:
    ...
    Properties:
      Tags:
        - Key: EndPROD
          Value: NotAppStart
        - Key: NotPRODEnd
          Value: AppStart
```

第一种形式的子句的计算结果为`PASS`。验证第一种形式的第一个子句时，、`Resources``Properties``Tags`、和的以下路径与值`Key`相匹配但`NotPRODEnd`与预期值`PROD`不匹配。

```
Resources:
  ...
  MyResource:
    ...
    Properties:
      Tags:
        - Key: EndPROD
          Value: NotAppStart
        - Key: NotPRODEnd
          Value: AppStart
```

第一种形式的第二个子句也是如此。`Resources`、`Properties``Tags`、和的路径与值`Value`相匹配`AppStart`。结果，第二个子句是独立的。

总体结果是`PASS`.

但是，方块形态的计算结果如下。对于每个`Tags`值，它会比较`Key`和`Value`是否匹配；`NotAppStart`以下示例中的`NotPRODEnd`值不匹配。

```
Resources:
  ...
  MyResource:
    ...
    Properties:
      Tags:
        - Key: EndPROD
          Value: NotAppStart
        - Key: NotPRODEnd
          Value: AppStart
```

由于评估会同时检查和 `Key == /PROD$/``Value == /^App/`，因此匹配未完成。因此，结果是`FAIL`。

**注意**  
使用集合时，我们建议您在要比较集合中每个元素的多个值时使用块子句形式。如果集合是一组标量值，或者只想比较单个属性，则使用单子句形式。

## 查询结果和相关条款
<a name="query-outcomes"></a>

所有查询都会返回一个值列表。遍历的任何部分，例如缺少键、访问所有索引时数组 (`Tags: []`) 的空值，或者遇到空地图 (`Resources: {}`) 时地图的缺失值，都可能导致检索错误。

在根据此类查询评估子句时，所有检索错误都被视为失败。唯一的例外是在查询中使用显式过滤器时。使用筛选器时，会跳过关联的子句。

以下区块失败与正在运行的查询有关。
+ 如果模板不包含资源，则查询的计算结果为`FAIL`，关联的块级子句也计算为`FAIL`。
+ 当模板包含类似的空资源块时`{ "Resources": {} }`，查询的计算结果为`FAIL`，关联的块级子句也计算为`FAIL`。
+ 如果模板包含资源但没有与查询相匹配的资源，则查询将返回空结果，并跳过区块级子句。

## 在查询中使用过滤器
<a name="filtering"></a>

查询中的过滤器实际上是用作选择标准的保护子句。以下是条款的结构。

```
 <query> <operator> [query|value literal] [message] [or|OR]
```

使用筛选器[写作 AWS CloudFormation Guard 规则](writing-rules.md)时，请记住以下要点：
+ 使用[连词普通形式 (CNF](https://en.wikipedia.org/wiki/Conjunctive_normal_form)) 合并子句。
+ 在新行上指定每个连词 (`and`) 子句。
+ 通过在两个子句之间使用`or`关键字来指定分离 (`or`)。

以下示例演示了连词和分离子句。

```
resourceType == 'AWS::EC2::SecurityGroup'
InputParameters.TcpBlockedPorts not empty 

InputParameters.TcpBlockedPorts[*] {
    this in r(100, 400] or 
    this in r(4000, 65535]
}
```

### 使用子句作为选择标准
<a name="selection-criteria"></a>

您可以对任何集合应用筛选。筛选可以直接应用于输入中已经是类似集合的属性`securityGroups: [....]`。您也可以对查询应用筛选，查询始终是值的集合。您可以使用子句的所有功能（包括连词普通形式）进行筛选。

从 CloudFormation 模板中按类型选择资源时，通常使用以下常用查询。

```
Resources.*[ Type == 'AWS::IAM::Role' ]
```

该查询`Resources.*`返回输入`Resources`部分中存在的所有值。对于中的示例模板输入[定义查询](#defining-queries)，查询返回以下内容。

```
- Type: AWS::IAM::Role
  ...
- Type: AWS::EC2::Instance
  ...
- Type: AWS::EC2::VPC
  ...
- Type: AWS::EC2::Subnet
  ...
- Type: AWS::EC2::Subnet
  ...
```

现在，对这个集合应用过滤器。匹配的标准是`Type == AWS::IAM::Role`。以下是应用筛选器后的查询输出。

```
- Type: AWS::IAM::Role
  ...
```

接下来，检查各种`AWS::IAM::Role`资源条款。

```
let all_resources = Resources.*
let all_iam_roles = %all_resources[ Type == 'AWS::IAM::Role' ]
```

以下是选择所有`AWS::IAM::ManagedPolicy`资源`AWS::IAM::Policy`和资源的筛选查询示例。

```
Resources.*[
    Type in [ /IAM::Policy/,
              /IAM::ManagedPolicy/ ]
]
```

以下示例检查这些策略资源是否已指`PolicyDocument`定。

```
Resources.*[ 
    Type in [ /IAM::Policy/,
              /IAM::ManagedPolicy/ ]
    Properties.PolicyDocument exists
]
```

### 构建更复杂的过滤需求
<a name="complex-filtering"></a>

考虑以下入口和出口安全组信息的 AWS Config 配置项目示例。

```
---
resourceType: 'AWS::EC2::SecurityGroup'
configuration:
  ipPermissions:
    - fromPort: 172
      ipProtocol: tcp
      toPort: 172
      ipv4Ranges:
        - cidrIp: 10.0.0.0/24
        - cidrIp: 0.0.0.0/0
    - fromPort: 89
      ipProtocol: tcp
      ipv6Ranges:
        - cidrIpv6: '::/0'
      toPort: 189
      userIdGroupPairs: []
      ipv4Ranges:
        - cidrIp: 1.1.1.1/32
    - fromPort: 89
      ipProtocol: '-1'
      toPort: 189
      userIdGroupPairs: []
      ipv4Ranges:
        - cidrIp: 1.1.1.1/32
  ipPermissionsEgress:
    - ipProtocol: '-1'
      ipv6Ranges: []
      prefixListIds: []
      userIdGroupPairs: []
      ipv4Ranges:
        - cidrIp: 0.0.0.0/0
      ipRanges:
        - 0.0.0.0/0
  tags:
    - key: Name
      value: good-sg-delete-me
  vpcId: vpc-0123abcd
InputParameter:
  TcpBlockedPorts:
    - 3389
    - 20
    - 21
    - 110
    - 143
```

注意以下几点：
+ `ipPermissions`（入口规则）是配置块内的规则集合。
+ 每个规则结构都包含诸如`ipv4Ranges`和之类的属性`ipv6Ranges`，用于指定 CIDR 块的集合。

让我们编写一条规则，选择允许来自任何 IP 地址的连接的所有入口规则，并验证这些规则是否不允许泄露 TCP 阻塞的端口。

从涵盖的查询部分开始 IPv4，如以下示例所示。

```
configuration.ipPermissions[
    #
    # at least one ipv4Ranges equals ANY IPv4
    #
    some ipv4Ranges[*].cidrIp == '0.0.0.0/0'
]
```

在此上下文中，`some`关键字很有用。所有查询都会返回与查询匹配的值的集合。默认情况下，Guard 会评估查询结果返回的所有值都与校验相匹配。但是，这种行为可能并不总是你需要进行检查的。考虑配置项目输入的以下部分。

```
ipv4Ranges: 
  - cidrIp: 10.0.0.0/24
  - cidrIp: 0.0.0.0/0 # any IP allowed
```

存在两个值`ipv4Ranges`。并非所有`ipv4Ranges`值都等于用表示的 `0.0.0.0/0` IP 地址。你想看看是否至少有一个值匹配`0.0.0.0/0`。你告诉 Guard，并非所有从查询返回的结果都需要匹配，但至少有一个结果必须匹配。`some`关键字告诉 Guard 确保结果查询中的一个或多个值与校验相匹配。如果没有匹配的查询结果值，Guard 将引发错误。

接下来 IPv6，添加，如以下示例所示。

```
configuration.ipPermissions[
    #
    # at-least-one ipv4Ranges equals ANY IPv4
    #
    some ipv4Ranges[*].cidrIp == '0.0.0.0/0' or
    #
    # at-least-one ipv6Ranges contains ANY IPv6
    #    
    some ipv6Ranges[*].cidrIpv6 == '::/0'
]
```

最后，在以下示例中，验证协议是否不是`udp`。

```
configuration.ipPermissions[
    #
    # at-least-one ipv4Ranges equals ANY IPv4
    #
    some ipv4Ranges[*].cidrIp == '0.0.0.0/0' or
    #
    # at-least-one ipv6Ranges contains ANY IPv6
    #    
    some ipv6Ranges[*].cidrIpv6 == '::/0'
    
    #
    # and ipProtocol is not udp
    #
    ipProtocol != 'udp' ] 
]
```

以下是完整的规则。

```
rule any_ip_ingress_checks
{

    let ports = InputParameter.TcpBlockedPorts[*]

    let targets = configuration.ipPermissions[
        #
        # if either ipv4 or ipv6 that allows access from any address
        #
        some ipv4Ranges[*].cidrIp == '0.0.0.0/0' or
        some ipv6Ranges[*].cidrIpv6 == '::/0'

        #
        # the ipProtocol is not UDP
        #
        ipProtocol != 'udp' ]
        
    when %targets !empty
    {
        %targets {
            ipProtocol != '-1'
            <<
              result: NON_COMPLIANT
              check_id: HUB_ID_2334
              message: Any IP Protocol is allowed
            >>

            when fromPort exists 
                 toPort exists 
            {
                let each_target = this
                %ports {
                    this < %each_target.fromPort or
                    this > %each_target.toPort
                    <<
                        result: NON_COMPLIANT
                        check_id: HUB_ID_2340
                        message: Blocked TCP port was allowed in range
                    >>
                }
            }

        }       
     }
}
```

### 根据集合包含的类型分隔集合
<a name="splitting-collection"></a>

使用基础设施即代码 (IaC) 配置模板时，您可能会遇到一个集合，其中包含对配置模板中其他实体的引用。以下是描述亚马逊弹性容器服务 (Amazon ECS) 任务的示例 CloudFormation 模板，其中包含本地引用`TaskRoleArn`、引用`TaskArn`和直接字符串引用。

```
Parameters:
  TaskArn:
    Type: String
Resources:
  ecsTask:
    Type: 'AWS::ECS::TaskDefinition'
    Metadata:
      SharedExectionRole: allowed
    Properties:
      TaskRoleArn: 'arn:aws:....'
      ExecutionRoleArn: 'arn:aws:...'
  ecsTask2:
    Type: 'AWS::ECS::TaskDefinition'
    Metadata:
      SharedExectionRole: allowed
    Properties:
      TaskRoleArn:
        'Fn::GetAtt':
          - iamRole
          - Arn
      ExecutionRoleArn: 'arn:aws:...2'
  ecsTask3:
    Type: 'AWS::ECS::TaskDefinition'
    Metadata:
      SharedExectionRole: allowed
    Properties:
      TaskRoleArn:
        Ref: TaskArn
      ExecutionRoleArn: 'arn:aws:...2'
  iamRole:
    Type: 'AWS::IAM::Role'
    Properties:
      PermissionsBoundary: 'arn:aws:...3'
```

请考虑以下查询。

```
let ecs_tasks = Resources.*[ Type == 'AWS::ECS::TaskDefinition' ]
```

此查询返回的值集合包含示例模板中显示的所有三个`AWS::ECS::TaskDefinition`资源。将`ecs_tasks`包含`TaskRoleArn`本地引用的内容与其他引用分开，如以下示例所示。

```
let ecs_tasks = Resources.*[ Type == 'AWS::ECS::TaskDefinition' ]

let ecs_tasks_role_direct_strings = %ecs_tasks[ 
    Properties.TaskRoleArn is_string ]

let ecs_tasks_param_reference = %ecs_tasks[
    Properties.TaskRoleArn.'Ref' exists ]

rule task_role_from_parameter_or_string {
    %ecs_tasks_role_direct_strings !empty or
    %ecs_tasks_param_reference !empty
}

rule disallow_non_local_references {
    # Known issue for rule access: Custom message must start on the same line
    not task_role_from_parameter_or_string 
    <<
        result: NON_COMPLIANT
        message: Task roles are not local to stack definition
    >>
}
```