AWS Service Catalog製品をcdk-nagで静的解析する～図解付き～

# はじめに

私は社内向けプラットフォームチームに所属しており、AWS Service Catalog × CDK でセルフサービス型機能の構築・運用を行っています。以下記事を過去に執筆しました。

https://qiita.com/y_matsuo_/items/058a26e18c0e50226d07

> ![service_catalog_outline.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2989026/429a5eed-2d07-4311-75fb-789935ab050b.png)

上記では、CDK の実装ノウハウが中心で、開発フローや少々複雑な ProductStack（L2Construct）の仕様などには触れていませんでした。また、Service Catalog 製品のテストコードを例示していましたが、cdk-nag を適用できない方式でした。

そこで、本記事では以下をご紹介します。

- なぜ Service Catalog 製品に cdk-nag を適用するのか
- Service Catalog 製品テンプレートの生成タイミング
- cdk-nag による Service Catalog 製品の静的解析方法
- cdk-nag のエラーに伴う修正・再テストの流れ

**本記事のポイント**

:::note
Service Catalog 製品のテンプレートを CfnInclude でテスト用スタックに取り込めば、cdk-nag を適用できます。
:::

# 前提

## 想定読者

- Service Catalog をこれから使い始めようとしている人
- AWS CDK における Service Catalog 利用方法に興味がある人
- AWS Constructs Library[^1]、および CDK におけるテスト[^2]の概念を理解している人

[^1]: AWS Constructs Library の解説については、[AWS Black Belt Online Seminar AWS CDK 概要 (Basic #1)](https://pages.awscloud.com/rs/112-TZM-766/images/AWS-Black-Belt_2023_AWS-CDK-Basic-1-Overview_0731_v1.pdf)をご参照ください。

[^2]: CDK のテストについては、[デベロッパーガイド](https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/testing.html)に詳細な解説が記載されています。[AWS CDK Workshop](https://cdkworkshop.com/ja/20-typescript/70-advanced-topics/100-construct-testing.html) では、Construct のテストを体感することができます。

## 実行環境

- CDK v2.113.0
- Jest v29.6.4

# なぜ Service Catalog 製品に cdk-nag を適用するのか

[cdk-nag](https://github.com/cdklabs/cdk-nag) とは、AWS CDK と統合したセキュリティ・コンプライアンスチェックツールです。各社のポリシーに沿った独自ルールパックを生成できます。```cdk deploy```などの CDK CLI 実行時に CDK で生成したテンプレートに対して、cdk-nag のルールパックに沿った静的解析を行います。デプロイする前に、**ルール違反している設定に気付ける**ため、非常に便利です。

一方、Service Catalog 製品とは、配布を目的とした AWS サービス群です。CFn テンプレートなどで定義し、**各社のセキュリティ・コンプライアンスに沿った製品**をセルフサービス型で組織内に展開できます。

2023 年の様々なアップデートにより、CDK でも Service Catalog を扱いやすくなりました。CDK で Service Catalog を構築・運用すると、DevOps のサイクルを円滑に回せます。そのサイクルに Service Catalog 製品の cdk-nag 解析を加えることで、セキュリティ・コンプライアンスに遵守した製品を迅速に配布できます。DevSecOps にも繋がるため、Service Catalog 製品に cdk-nag を適用するのは、非常にオススメです。

# Service Catalog 製品テンプレートの生成タイミング

以下では、Service Catalog 製品テンプレートがどのタイミングで作られるのか、2種類の図で解説します。後述の[サンプルコード](#productstack-を使用したサンプルコード)も併せてご確認ください。

## Service Catalog 製品の開発フロー

以下は、CDK において Service Catalog 製品を開発する際の大まかなフロー図です[^3]。

[^3]: フローがややこしくなるため、製品の cdk-nag 以外に関するテストやタグオプションについては、記載対象外としています。

```plantuml
@startuml
card Service Catalog 製品の開発フロー概要 {
  start
  :要件擦り合わせ;
  :計画;
  :設計;
  group CDK 
    package "ProductStack" {
      :各種 Construct で\n製品リソースを定義;
      note left
        カスタム Construct では
        本段階で cdk-nag 適用可
        （単体テストから実行）
      end note
    }

    package "Stack\n(Service Catalog メタ定義)" {
      split
      :製品情報の定義;
      note left
        製品名や説明、
        開示するバージョンなど
      end note
      split again
      :製品バージョンの定義;
      split again
      :ポートフォリオの定義;
      end split
    }
    
    :Stack を生成し\n製品テンプレート（スナップショット）を出力;
    :製品テンプレートを cdk-nag で静的解析;
    :Stack をデプロイ;
    note left
      ProductStack に関する
      製品テンプレートが
      S3 にアップロードされる
    end note
  end group
  stop
}
@enduml
```

CDK で Service Catalog 製品を定義するには、上記の [ProductStack](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_servicecatalog.ProductStack.html) を使用します。ProductStack の定義内容が製品テンプレートとして出力されます。

:::note 
Stack の生成により、製品テンプレートを出力した後でないと、製品を cdk-nag で静的解析できません。
:::

## CDK × Service Catalog 構成概要

ProductStack と通常の Stack との違いが少々分かりづらいため、イメージ図を以下に掲載します。

```plantuml
@startuml
!define AWSPuml https://raw.githubusercontent.com/awslabs/aws-icons-for-plantuml/v17.0/dist

!include AWSPuml/AWSCommon.puml
!include AWSPuml/AWSSimplified.puml
!include AWSPuml/Groups/AWSCloud.puml
!include AWSPuml/Storage/SimpleStorageService.puml

!include AWSPuml/ManagementGovernance/ServiceCatalog.puml
!include AWSPuml/ManagementGovernance/CloudFormationStack.puml

$AWSGroupColoring(CloudFormationStackGroup, $AWSColor(DeveloperTools))
!define CloudFormationStackGroup(g_alias, g_label="CloudFormation Stack") $AWSDefineGroup(g_alias, g_label, CloudFormationStack, CloudFormationStackGroup)

$AWSGroupColoring(ServiceCatalogGroup, $AWSColor(ManagementGovernance))
!define ServiceCatalogGroup(g_alias, g_label="ServiceCatalog ポートフォリオ") $AWSDefineGroup(g_alias, g_label, ServiceCatalog, ServiceCatalogGroup)

rectangle "CDK Project" #FloralWhite {
  file "ソースコード" {
    frame "Stack (Service Catalog メタ定義)" as cStack #Khaki {
      node "ポートフォリオ" {
        frame 製品 {
          [CloudFormationProduct] #Ivory
          CloudFormationProduct .> [ProductStackHistory] : ProductStackの\n表示するバージョンを指定
        }
      }
    }

    frame ProductStack as pStack #WhiteSmoke {
      [AWS Constructs Library]
      [カスタム Construct]
    }
    [ProductStackHistory] ..> pStack : 現在の定義に\nバージョン名を付与
  }
  
  folder "cdk.out ディレクトリ" as cdkOut {
    artifact CFnテンプレート as cfnTemplate
  }
  cStack --> cfnTemplate : cdk synth などで\n生成 

  folder "任意ディレクトリ" as anyDir {
    collections "過去バージョンの\nCFnテンプレート" as cfnTemplates
    artifact "**CFnテンプレート**" as cfnTemplateToS3 #Orange
  }
  pStack --> cfnTemplateToS3 : cdk synth や\n単体テスト時に\nスナップショットとして生成
  [ProductStackHistory] ..> cfnTemplates :　過去バージョンを\n紐づけ
}



AWSCloudGroup(cloud) {
  SimpleStorageService(S3,"S3", "") 
  CloudFormationStackGroup(CfnStack) {
    ServiceCatalogGroup(portfolio) {
      component "製品情報" as productInfo
    }
    productInfo ..> S3 : 参照
    cfnTemplateToS3 ..> S3 : cdk deploy時に\nアップロード
    cfnTemplate --> CfnStack : cdk\ndeploy
  }
}
@enduml
``` 

本記事で静的解析の対象としているのは、上記の任意ディレクトリに格納された CFn テンプレート（製品テンプレート）です。

:::note warn
ProductStack 単体では、製品テンプレートを出力することはできません。図の通り、ProductStack は他の Stack から呼ばれて使用されます。

もし、単体テストなどで ProductStack を直接生成しようとすると、以下の警告文がでます。
```Product stacks must be defined within scope of another non-product stack```
:::

# ProductStack の問題点

前述の通り、cdk-nag の主な実行タイミングは、CDK CLI 実行時です。しかし、ProductStack で生成した製品テンプレートについては、CDK CLI を実行しても cdk-nag で解析されません。

## cdk-nag 静的解析結果のサンプル

例えば、通常の Stack で S3 バケットをデフォルト設定で定義すると、cdk-nag からエラーが返されます。

```ts: ./bin/app.ts
import * as cdk from 'aws-cdk-lib';
import * as s3 from 'aws-cdk-lib/aws-s3';
import { AwsSolutionsChecks } from 'cdk-nag'

const app = new cdk.App();
cdk.Aspects.of(app).add(new AwsSolutionsChecks());

new s3.Bucket(new cdk.Stack(app, "SampleStack"), 'Bucket');
```

```Bash: cdk synth時cdk-nagの実行結果
[Error at /SampleStack/Bucket/Resource] AwsSolutions-S1: The S3 Bucket has server access logs disabled.

[Error at /SampleStack/Bucket/Resource] AwsSolutions-S10: The S3 Bucket or bucket policy does not require requests to use SSL.
```

## ProductStack を使用したサンプルコード

一方、ProductStack で上記同様に S3 バケットを定義した場合は cdk-nag で解析されず、エラーが返されません。

```ts: ./bin/app.ts
import * as cdk from 'aws-cdk-lib';
import { CdkScStack } from '../lib/cdk_sc-stack';
import { AwsSolutionsChecks } from 'cdk-nag'

const app = new cdk.App();
cdk.Aspects.of(app).add(new AwsSolutionsChecks());

new CdkScStack(app, 'CdkScStack');
```

```ts: ./lib/cdkScStack
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as servicecatalog from 'aws-cdk-lib/aws-servicecatalog';

// 利用者に配布する CFn テンプレートのリソースを ProductStack 配下で定義
class S3BucketProduct extends servicecatalog.ProductStack {
  constructor(scope: Construct, id: string) {
    super(scope, id);

    new s3.Bucket(this, 'Bucket');
  }
}

// Service Catalog 関連のリソースを通常の Stack で定義
export class CdkScStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
    
    // ProductStack のリソース定義にバージョン名を付与して、スナップショットを出力
    const productStackHistory = new servicecatalog.ProductStackHistory(
      this, 'ProductStackHistory', {
        productStack: new S3BucketProduct(this, 'S3BucketProduct'),
        currentVersionName: 'v1',
        currentVersionLocked: true,
    });

    // 利用者に開示する製品を登録
    const product = new servicecatalog.CloudFormationProduct(this, 'MyFirstProduct', {
      productName: "My Product",
      owner: "Product Owner",
      productVersions: [
        productStackHistory.currentVersion(), 
      ],
    });

    // ポートフォリオ(製品の集合)を定義
    const portfolio = new servicecatalog.Portfolio(this, 'Portfolio', {
      displayName: 'MyPortfolio',
      providerName: 'MyTeam',
    });
    portfolio.addProduct(product);
  }
}
```

:::note warn
```cdk synth``` 時は、cdk-nag でエラーが発生せず、CdkScStack に関する CFn テンプレートの生成結果が表示されます。
:::

なお、re:Invent 2023 で発表されたばかりの Amazon Q に聞いてみましたが、上記同様に app レイヤーで cdk-nag を適用する方法が提示されました。

![Amazon Q](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2989026/5f83b7c4-ff09-b17d-01f1-3561ea622a7e.png)

## なぜ解析されない？

CDK で生成される CFn テンプレートは基本的に cdk.out へ出力されます。一方、Service Catalog 製品は、別の CFn テンプレートとして、CDK プロジェクト上の任意ディレクトリに出力されます。

以下のリファレンスによると、Service Catalog 製品の CFn テンプレートについては、```cdk deploy```のコマンドでスタックとして認識されず、Asset として S3 バケットにアップロードされるだけのようです。

> This stack will not be treated as an independent deployment artifact (won't be listed in "cdk list" or deployable through "cdk deploy"), but rather only synthesized as a template and uploaded as an asset to S3.

出典：https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_servicecatalog.ProductStack.html

この辺りを突き詰めていくと根本原因が判明しそうですが、後述の issue にも関わってくる内容なので、本記事では深く触れません。

# 単体テストで cdk-nag を実行する

Stack の単体テストで cdk-nag を実行するという方法が公式ブログで紹介されています[^4]。この方法なら、Stack のリソースを静的解析できそうですが、以下 Issue で ProductStack のバグが報告されています。2023年12月5日時点の CDK 最新バージョン"v2.113.0"においても、本バグは解消されていません。

https://github.com/aws/aws-cdk/issues/24990

[^4]: https://aws.amazon.com/jp/blogs/news/manage-application-security-and-compliance-with-the-aws-cloud-development-kit-and-cdk-nag/

そこで、ProductStack のテストに関する Tips として、過去記事で以下を紹介していました。

> [5. 製品のリソースを CDK でテストする場合、製品のスナップショットを利用する](https://qiita.com/y_matsuo_/items/058a26e18c0e50226d07#5-%E8%A3%BD%E5%93%81%E3%81%AE%E3%83%AA%E3%82%BD%E3%83%BC%E3%82%B9%E3%82%92-cdk-%E3%81%A7%E3%83%86%E3%82%B9%E3%83%88%E3%81%99%E3%82%8B%E5%A0%B4%E5%90%88%E8%A3%BD%E5%93%81%E3%81%AE%E3%82%B9%E3%83%8A%E3%83%83%E3%83%97%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%E3%82%92%E5%88%A9%E7%94%A8%E3%81%99%E3%82%8B)

本 Tips では、Service Catalog 製品スナップショットの CFn テンプレート（JSON ファイル）に対して、readFileSync と fromJSON メソッドを使っていました。

```ts
  beforeAll(() => {
    // JSON 形式のスナップショットを読み取り
    const snapshot = JSON.parse(readFileSync(snapVersionPath, "utf-8"));
    // スナップショットを fromJSON メソッドで Template 化
    template = Template.fromJSON(snapshot);
  });
```

上記サンプルコードでは、JSON ファイルを [Template](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.assertions.Template.html) 形式に変換しており、CDK のテストで一般的に使われる [Stack](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.Stack.html) を生成していません。

しかし、cdk-nag を適用するには、Aspects の [of メソッド](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.Aspects.html#static-ofscope)に Stack などの IConstruct を渡す必要があります[^5]。過去に紹介したサンプルコードでは、Template 形式に変換していたため、cdk-nag で静的解析できませんでした。

> public static of(scope: IConstruct): Aspects

出典：https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.Aspects.html#static-ofscope

[^5]: [前述のサンプルコード](#cdk-nag-静的解析結果のサンプル)では、 ```cdk.Aspects.of(app).add(new AwsSolutionsChecks());```の部分で、cdk.App の IConstruct を渡しています。

## 解決策

前振りが長くなりましたが、解決策はシンプルです。

Service Catalog 製品のテンプレートを [CfnInclude](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.cloudformation_include.CfnInclude.html) でテスト用スタックに取り込めば、cdk-nag を適用できます。コード例は以下の通りです。

```ts: (抜粋)./test/cdkScProduct.test.ts 
  beforeEach(() => {
    // テスト用の仮スタックを定義
    stack = new cdk.Stack(new cdk.App(), "TestStack");
    
    // テスト用の仮スタックに Service Catalog 製品テンプレートをインクルード
    new CfnInclude(stack, "ServiceCatalogProductTemplate", {
      templateFile: snapVersionPath,
    });
    
    // テスト用の仮スタックを cdk-nag で静的解析
    cdk.Aspects.of(stack).add(new AwsSolutionsChecks());

    template = Template.fromStack(stack);
  });
```

:::note
前提として、```cdk synth``` などで、Service Catalog 製品のテンプレートを生成しておく必要があります[^6]。その後、CfnInclude でテスト用の仮スタックに Service Catalog 製品テンプレートを取り込んでいます。この方法ならば、通常のアサーションテストに加えて、cdk-nag の実行も可能です。
:::

[^6]: 当チームでは、Service Catalog Portfolio が含まれているスタックのテストを先に実施して、製品テンプレートを生成しています。サンプルコードでは、CdkScStack が「Service Catalog Portfolio が含まれているスタック」に該当します。

テストコードサンプルの全容は以下の通りです。

<details><summary>テストコードサンプル全容</summary>

```ts: ./test/cdkScProduct.test.ts 
// WARNING: 前提として、製品テンプレートが出力されていること
import { Template, Annotations, Match } from "aws-cdk-lib/assertions";
import * as cdk from "aws-cdk-lib";
import { CfnInclude } from "aws-cdk-lib/cloudformation-include";
import { AwsSolutionsChecks } from 'cdk-nag';
import { SynthesisMessage } from "aws-cdk-lib/cx-api";

// TODO: 製品テンプレート出力に伴い、Service Catalog関連の以下情報を適宜変更
const productSnapshotDirectoryName = "product-stack-snapshots";
const stackName = "CdkScStack";
const productName = "S3BucketProduct";
const productVersion = "v1";

// テスト対象のService Catalog製品テンプレートについてファイルパスを指定
// TODO: ProductStackHistory・productNameサフィックスの文字列は適宜修正
const snapVersionPath = [
  `${productSnapshotDirectoryName}/${stackName}ProductStackHistory11BC36A9`,
  `${stackName}${productName}335B97F3`,
  `${productVersion}.product.template.json`
].join('.');
console.log(snapVersionPath);

describe("ServiceCatalogProductのテスト", () => {
  let template: Template;
  let stack: cdk.Stack;

  beforeEach(() => {
    // テスト用の仮スタックを定義
    stack = new cdk.Stack(new cdk.App(), "TestStack");
    
    // テスト用の仮スタックに Service Catalog 製品テンプレートをインクルード
    new CfnInclude(stack, "ServiceCatalogProductTemplate", {
      templateFile: snapVersionPath,
    });
    
    // テスト用の仮スタックを cdk-nag で静的解析
    cdk.Aspects.of(stack).add(new AwsSolutionsChecks());

    template = Template.fromStack(stack);
  });
  
  describe("cdk-nagの確認", () => {
    test("No unsuppressed Warnings", () => {
      const warnings = Annotations.fromStack(stack).findWarning(
        "*",
        Match.stringLikeRegexp("AwsSolutions-.*"),
      );
      try {
        expect(warnings).toHaveLength(0);
      } catch (e) {
        throw new Error(createCdkNagLog(warnings));
      }
    });

    test("No unsuppressed Errors", () => {
      const errors = Annotations.fromStack(stack).findError(
        "*",
        Match.stringLikeRegexp("AwsSolutions-.*"),
      );
      try {
        expect(errors).toHaveLength(0);
      } catch (e) {
        throw new Error(createCdkNagLog(errors));
      }
    });
  });
  
  test("S3 Bucketの数が1である", () => {
    template.resourceCountIs("AWS::S3::Bucket", 1);
  });
});

function createCdkNagLog(messages: SynthesisMessage[]): string {
  let log = "";

  messages.forEach((message) => {
    switch (message.level) {
      case "info":
        log += "\u001b[34m"; // blue
        break;
      case "warning":
        log += "\u001b[33m"; // yellow
        break;
      case "error":
        log += "\u001b[31m"; // red
        break;
      default:
        log += "\u001b[30m"; // black
        break;
    }
    log += `[${message.level} at ${message.id}] ${message.entry.data as string}\u001b[0m`;
  });

  return log;
}
```

上記の```function createCdkNagLog```は、エラーメッセージを整形するためのメソッドで、以下記事で紹介した内容をそのまま使っています。

[【AWS】cdk-nagで単体テストのメッセージを整形し、コーディングしながら爆速でセキュリティ遵守する！](https://qiita.com/y_matsuo_/items/6ef17964ff3c557f6d1e#%E6%B1%8E%E7%94%A8%E5%8C%96%E3%81%97%E3%81%A6%E3%81%BF%E3%82%8B)

</details>

# Service Catalog 製品の静的解析・修正

以下では、実際に Service Catalog 製品を cdk-nag で解析し、エラー結果を基に修正・再テストするという流れを提示します。

## 単体テストでの cdk-nag 実行結果

[解決策](#解決策)で掲載したテストコードを実行します。

![テスト実行結果1](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2989026/f0bb1049-13b0-e09c-6e01-c1e8f438dc46.png)

[前述の結果](#cdk-nag-静的解析結果のサンプル)と同様に、cdk-nag から S3 バケットの違反に関するエラーが出力されました。

## Service Catalog 製品を修正

cdk-nag のエラー2件に従って、以下方針で Service Catalog 製品の定義を見直します。

- AwsSolutions-S1: The S3 Bucket has server access logs disabled.  
  → ルールを抑制
- AwsSolutions-S10: The S3 Bucket or bucket policy does not require requests to use SSL.  
  → S3バケットで SSL を強制

今回は、既に実環境へ Service Catalog 製品 v1 をリリースしているという想定で、Service Catalog 製品のバージョンを一つ繰り上げます[^7]。コード例は以下の通りです。

[^7]: ```currentVersionLocked: false```で製品スナップショットの上書きを許可している場合、バージョンの繰り上げは必須ではありません。今回のサンプルコードのように```currentVersionLocked: true```で製品スナップショットの上書きを禁止している場合は、バージョンを繰り上げずに既存のスナップショットを消すという方法でも、後続のテストを実施可能です。

```diff_typescript: (抜粋)./lib/cdkScStack
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as servicecatalog from 'aws-cdk-lib/aws-servicecatalog';
+ import { NagSuppressions } from 'cdk-nag';

class S3BucketProduct extends servicecatalog.ProductStack {
  constructor(scope: Construct, id: string) {
    super(scope, id);

-   new s3.Bucket(this, 'Bucket');
+   const bucket = new s3.Bucket(this, 'Bucket', {
+     enforceSSL: true,
+   });
+   NagSuppressions.addResourceSuppressions(bucket, [{
+     id: "AwsSolutions-S1",
+     reason: "Demonstration"
+   }])
  }
}

export class CdkScStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
    
    const productStackHistory = new servicecatalog.ProductStackHistory(this, 'ProductStackHistory', {
      productStack: new S3BucketProduct(this, 'S3BucketProduct'),
-     currentVersionName: 'v1',
+     currentVersionName: 'v2',
      currentVersionLocked: true,
    });
```

上記修正後、```cdk synth``` や　 (CdkScStack)自体のテストなどで Service Catalog 製品テンプレートを出力します。

製品バージョンを繰り上げたので、テストコードの定義も同様に変更します。この変更により、テスト対象の Service Catalog 製品テンプレートを切替できます。

```diff_typescript: (抜粋)./test/cdkScProduct.test.ts 
// Service Catalog 関連の情報を定義
const productSnapDirectoryName = "product-stack-snapshots";
const stackName = "CdkScStack";
const productName = "S3BucketProduct";
- const productVersion = "v1";
+ const productVersion = "v2";
```

## 再テスト

この状態でテストを再度実行します。

![テスト実行結果2](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2989026/58927d23-8e9e-69a8-2896-a7c96fca07fe.png)

無事に cdk-nag のエラーが出力されなくなりました。  
念の為、修正後の Service Catalog 製品テンプレート（スナップショット）を確認してみます。

<details><summary>Service Catalog 製品テンプレート全容</summary>

```json: ./product-stack-snapshots/CdkScStackProductStackHistory11BC36A9.CdkScStackS3BucketProduct335B97F3.v2.product.template.json
{
  "Resources": {
    "Bucket83908E77": {
      "Type": "AWS::S3::Bucket",
      "UpdateReplacePolicy": "Retain",
      "DeletionPolicy": "Retain",
      "Metadata": {
        "aws:cdk:path": "CdkScStack/S3BucketProduct/Bucket/Resource",
        "cdk_nag": {
          "rules_to_suppress": [
            {
              "reason": "Demonstration",
              "id": "AwsSolutions-S1"
            }
          ]
        }
      }
    },
    "BucketPolicyE9A3008A": {
      "Type": "AWS::S3::BucketPolicy",
      "Properties": {
        "Bucket": {
          "Ref": "Bucket83908E77"
        },
        "PolicyDocument": {
          "Statement": [
            {
              "Action": "s3:*",
              "Condition": {
                "Bool": {
                  "aws:SecureTransport": "false"
                }
              },
              "Effect": "Deny",
              "Principal": {
                "AWS": "*"
              },
              "Resource": [
                {
                  "Fn::GetAtt": [
                    "Bucket83908E77",
                    "Arn"
                  ]
                },
                {
                  "Fn::Join": [
                    "",
                    [
                      {
                        "Fn::GetAtt": [
                          "Bucket83908E77",
                          "Arn"
                        ]
                      },
                      "/*"
                    ]
                  ]
                }
              ]
            }
          ],
          "Version": "2012-10-17"
        }
      },
      "Metadata": {
        "aws:cdk:path": "CdkScStack/S3BucketProduct/Bucket/Policy/Resource"
      }
    }
  }
}
```

</details>

rules_to_suppress で AwsSolutions-S1 のルールが抑制され、バケットポリシーで SSL が強制されていますね…！

# おわりに

本記事では、cdk-nag による Service Catalog 製品の静的解析方法を紹介しました。

今回、Service Catalog 製品に特化した事例紹介でしたが、既存の CloudFormation テンプレートに対して整備済みの cdk-nag ルールパックで検査したいといった場合にも、同様のテストコードを流用できそうです。

本記事がどなたかのお役になれば幸いです。

# 参考資料

- [AWS Cloud Development Kit と cdk-nag でアプリケーションのセキュリティとコンプライアンスを管理する](https://aws.amazon.com/jp/blogs/news/manage-application-security-and-compliance-with-the-aws-cloud-development-kit-and-cdk-nag/)
- [【AWS】cdk-nagで単体テストのメッセージを整形し、コーディングしながら爆速でセキュリティ遵守する！](https://qiita.com/y_matsuo_/items/6ef17964ff3c557f6d1e)
- [AWS CDKでService Catalogを構築・運用するための９Tips ～事例と共に～](https://qiita.com/y_matsuo_/items/058a26e18c0e50226d07)
- [aws-cdk-lib/assertions: Cannot run testing against ProductStack objects](https://github.com/aws/aws-cdk/issues/24990)
- [AWS CDK API Reference](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-construct-library.html)