Security Command Center 検出結果で(Open Port なファイアウォールルール)と(Public IP addressをもつインスタンス)があるVPC一覧をgcloudで取得する

## やりたいこと
Google Cloud の Security Command Center を有効化したところ、
複数の "Open SSH Port" や "Open RDP Port" 検出結果があるが、
コンソール上で対象リソースを調べて回るのが面倒なので、コマンドで簡単に抽出したい。

また、攻撃リスクが高い環境を優先して対応すべく、
ファイアウォールルールが Open かつ、Public IP address のあるインスタンスがある環境の一覧が欲しい。

## 前提条件
- Google Cloud 以外のサードパーティ製セキュリティツールは利用しない。
- Connectivity Test や nmap などによる実際のアクセス確認までは行わない。
- Firewall Logging は有効になっておらず、Firewall Insights は利用できない。
    - これが使えれば、不要なファイアウォールルールの特定がより楽になりますが、今回は使用しません。

## コマンド
Cloud Shell で、1 Project 毎の実行を想定しています。
**パフォーマンスは考慮していません。**

(あと、やたら長いコマンドになりましたが、もっと効率の良い書き方があるかも...)

### 1. Project ID 設定
```sh
export PROJECT_ID=プロジェクトID
```
```sh
gcloud config set project $PROJECT_ID
```

### 2. パブリックIP をもつインスタンスと VPC の一覧を出力
検出カテゴリ名 PUBLIC_IP_ADDRESS のインスタンスと、VPC 名をファイル出力します。
(各コマンドで何やってるかは後述)
```sh
gcloud scc findings list projects/$PROJECT_ID \
--filter="state=\"ACTIVE\" AND category=\"PUBLIC_IP_ADDRESS\"" \
--format="value[separator=\" \"](resource.displayName,resource.name)" |
sed -r 's#^([a-z0-9-]+) //compute\.googleapis\.com/projects/[a-z0-9-]+/zones/([a-z0-9-]+)/instances/[0-9]+$#\1 \2#' |
xargs -r echo |
xargs -r -n 2 bash -c 'gcloud compute instances describe ${0} --zone=${1} \
--format="value[separator=\",\"](name,networkInterfaces.network)"' |
sed -r 's#^([a-z0-9-]+),https://www\.googleapis\.com/compute/v1/projects/[a-z0-9-]+/global/networks/([a-z0-9-]+)$#\1,\2#' \
> public-ip.txt
```
出力されるファイルはこんな感じです。(インスタンス名,VPC名)
```text:public-ip.txt
instance-1,vpc-1
instance-2,vpc-2
instance-3,vpc-3
```

### 3. OPEN なファイアウォールルールと VPC の一覧を出力
"OPEN" を含む検出カテゴリで検出されたファイアウォールと、VPC を抽出します。
(同じく、各コマンドで何やってるかは後述)
```sh
gcloud scc findings list projects/$PROJECT_ID \
--filter="state=\"ACTIVE\" AND category:\"OPEN\" AND resource.type=\"google.compute.Firewall\"" \
--format="value[separator=\" \"](finding.category,resource.displayName)" |
xargs -r echo |
xargs -r -n 2 bash -c 'echo "${0},$(gcloud compute firewall-rules list \
--filter="name=${1}" \
--format="value[separator=\",\"](name,network)")"' \
> open-firewall.txt
```

出力されるファイルはこんな感じです。(検出カテゴリ名,ファイアウォールルール名,VPC名)
```text:open-firewall.txt
OPEN_SSH_PORT,vpc-2-allow-ssh,vpc-2
OPEN_RDP_PORT,vpc-1-allow-rdp,vpc-1
OPEN_SSH_PORT,vpc-1-allow-ssh,vpc-1
```

### 4. ２と３の結果を JOIN
```sh
join -a 1 -t, -1 2 -2 3 -o auto -e "NULL" \
<(sort -t, -k2 public-ip.txt) <(sort -t, -k3 open-firewall.txt)
```

出力結果はこんな感じです。
```text
vpc-1,instance-1,OPEN_RDP_PORT,vpc-1-allow-rdp
vpc-1,instance-1,OPEN_SSH_PORT,vpc-1-allow-ssh
vpc-2,instance-2,OPEN_RDP_PORT,vpc-2-allow-ssh
vpc-3,instance-3,NULL,NULL
```
VPC、Public IP をもつインスタンス、同じ VPC に存在する OPEN なファイアウォールルールの組み合わせを抽出することができました。
PUBLIC IP はあるけれどもファイアウォールが OPEN なのは検出されていませんよ、という場合は NULL 表示になります。

あとは、これを元にファイアウォールやアクセス方法の見直しをすれば良さそうです。
(アクセス元IPの絞り込みや、SSH 接続だけならIAP 利用への切り替えなど。)

TODO: ファイアウォールルールは OPEN だが、Public IP をもつインスタンスは無い という情報も欲しいので、FULL JOIN に直す

### 備考
上記で出力されたからといって、必ずしもアクセス可能とは限らず、
他のファイアウォールルールで Deny されている可能性も、
ネットワークタグ指定によりアクセス制限されている可能性も、
インスタンス側でポートが空いていないという可能性もあります。

また、そもそも Public IP が付与される前に防止したい！というのであれば、
Organization Policy (constraints/compute.vmExternalIpAccess) を設定しておく手があります。

## 各コマンドの詳細
各行の詳細をメモしておきます。

参考：
- [Security Command Center の検出カテゴリ一覧](https://cloud.google.com/security-command-center/docs/concepts-vulnerabilities-findings)
- [gcloud リファレンス](https://cloud.google.com/sdk/gcloud)

### 2 のコマンド詳細
まずは、検出カテゴリ PUBLIC_IP_ADDRESS のインスタンスを抽出する。
--format="value" はデフォルトではタブ区切りで各項目を出力するが、xargs でdelimiter 指定が要らないようにスペース区切りに変更している。

```sh
$ gcloud scc findings list projects/$PROJECT_ID \
--filter="state=\"ACTIVE\" AND category=\"PUBLIC_IP_ADDRESS\"" \
--format="value[separator=\" \"](resource.displayName,resource.name)"
instance-1 //compute.googleapis.com/projects/PROJECT_ID/zones/us-central1-a/instances/1234567890123456789
instance-2 //compute.googleapis.com/projects/PROJECT_ID/zones/us-west1-b/instances/1234567890123456789 
```

続いて gcloud compute コマンドに渡して VPC 情報を取得したい...のだが、インスタンス名とゾーン名が必要なので、ゾーン名を前コマンドの resource.name からで抽出する
-r オプションで拡張正規表現使用可能にしておく
```sh
$ echo "instance-1 //compute.googleapis.com/projects/PROJECT_ID/zones/us-central1-a/instances/1234567890123456789" |
sed -r 's#^([a-z0-9-]+) //compute\.googleapis\.com/projects/[a-z0-9-]+/zones/([a-z0-9-]+)/instances/[0-9]+$#\1 \2#'
instance-1 us-central1-a
```
※上記のコマンドそのまま実行しても、PROJECT_ID 部分がマッチしないため抽出されません

続いて gcloud compute instance describe コマンドを各インスタンスについて実行...する前に、xargs で扱いやすいようにで改行を除去する
-r オプションで、入力が空の場合はコマンド実行しない
```sh
$ echo -e "instance-1 us-central1-a\ninstance-2 us-west1-b" |
xargs -r echo
instance-1 us-central1-a instance-2 us-west1-b
```

1行になったので、先頭から2つずつ取り出して gcloud compute instances describe コマンドに渡す
結果はカンマ区切りで、インスタンス名とVPC情報のみ取得する
```sh
$ echo "instance-1 us-central1-a instance-2 us-west1-b" |
xargs -r -n 2 bash -c 'gcloud compute instances describe ${0} --zone=${1} \
--format="value[separator=\",\"](name,networkInterfaces.network)"'
instance-1,https://www.googleapis.com/compute/v1/projects/PROJECT_ID/global/networks/vpc-1
instance-2,https://www.googleapis.com/compute/v1/projects/PROJECT_ID/global/networks/vpc-2
```

めんどくさいことに VPC は URL で返ってくるので、名前だけ抽出する

```sh
$ echo "instance-1,https://www.googleapis.com/compute/v1/projects/PROJECT_ID/global/networks/vpc-1" |
sed -r 's#^([a-z0-9-]+),https://www\.googleapis\.com/compute/v1/projects/[a-z0-9-]+/global/networks/([a-z0-9-]+)$#\1,\2#'
instance-1,vpc-1
```
※上記のコマンドそのまま実行しても、PROJECT_ID 部分がマッチしないため抽出されません

### 3 のコマンド詳細
まずは、SCC から "OPEN ~" として検出されているファイアウォールルールを抽出する
category:OPEN だけだと、OPEN_GROUP_IAM_MEMBER なども引っかかる可能性があるので、resource.type も指定している
uniq は複数の検出結果に表れるファイアウォールルールがあるかもしれないので指定している

```sh
$ gcloud scc findings list projects/$PROJECT_ID \
--filter="state=\"ACTIVE\" AND category:\"OPEN\" AND resource.type=\"google.compute.Firewall\"" \
--format="value[separator=\" \"](finding.category,resource.displayName)" | uniq
OPEN_SSH_PORT,vpc-2-allow-ssh
OPEN_RDP_PORT,vpc-1-allow-rdp
OPEN_SSH_PORT,vpc-1-allow-ssh
```

改行除去する部分は 2 と同様なので割愛

検出カテゴリ名は そのまま echo コマンドへ、
ファイアウォールルール名は gcloud compute firewall-rules list コマンドに渡してから、その結果を echo コマンドへ
```sh 
$ echo "OPEN_SSH_PORT vpc-2-allow-ssh OPEN_RDP_PORT vpc-1-allow-rdp OPEN_SSH_PORT vpc-1-allow-ssh" |
xargs -r -n 2 bash -c 'echo "${0},$(gcloud compute firewall-rules list \
--filter="name=${1}" \
--format="value[separator=\",\"](name,network)")"'
OPEN_SSH_PORT,vpc-2-allow-ssh,vpc-2
OPEN_RDP_PORT,vpc-1-allow-rdp,vpc-1
OPEN_SSH_PORT,vpc-1-allow-ssh,vpc-1
```

### 4 のコマンド詳細
```sh
join -a 1 -t, -1 2 -2 3 -o auto -e "NULL" <(sort -t, -k2 public-ip.txt) <(sort -t, -k3 open-firewall.txt)
```

-a 1 で LEFT JOIN
-t, で区切り文字をカンマに指定
-1 で一番目のファイルのどの列を JOIN に使うかを指定
-2 で二番目のファイルのどの列を JOIN に使うかを指定
-o auto で出力項目を自動に指定 (-e 利用時には -o が必要)
-e "NULL" で、マッチする行がなかった時に表示する文字列を指定

また、事前にソートされていないと join コマンドでエラーになるため、それぞれのファイルを sort している
-t, で区切り文字指定
-k3 でソートに使う列を指定

手順2,3 の時点でソートしておいても良かったが、コマンドがさらに長くなるのでこちらでソートをかけている