【JS体操第3問ヒント③】「疎」な配列を「密」にする12の方法

こんにちは!面白プロデュース事業部のおばらです。
今回は JS体操第3問「Zalgo Text の生成」の問題のヒントにもなるかもしれないシリーズ第3弾。

JavaScript で「疎」な配列を(要素数は変えずに)「密」にする12の方法を

  • コードゴルフ(文字数を短くしたい場合)
  • ワンライナ(1行で書きたい場合)

的な視点も交え、まとめてみます。


JS体操第3問「Zalgo Text の生成」 とこれまでのヒント記事については以下をご覧ください。

JS体操第3問「Zalgo Text の生成」
JS体操第3問「Zalgo Text の生成」編集画面 JS体操第3問「Zalgo Text の生成」編集画面(エラー)

hubspot.kayac.com

techblog.kayac.com

techblog.kayac.com


目次


疎な配列とは

配列の要素の値として undefined でも null でもない概念「空(empty)」があります。

「空(empty)」 の「要素(スロット、slot)」を1つ以上持つ配列は、「疎(sparse)」な配列、「疎配列(sparse array)」と呼ばれます。つまり歯抜けの配列、隙間がある配列です。

一方「空(empty)」の要素を1つも持たない配列、つまり「疎(sparse)」ではない配列は「密(dense)」な配列と呼ばれることもあります。つまりぎっしり詰まった配列です。

developer.mozilla.org


疎な配列の作り方

中身がすべて or 一部「空(empty)」な「疎配列」の作り方の例を以下にいくつか示します。実務では意図して「疎」な配列を作りたい機会はあまり無いかもしれませんが、逆に意図せず配列を「疎」にしてしまう可能性に注意しましょう。

なおサンプルコードでは配列の長さ(length)、要素数は 10 とします。


配列リテラル

配列リテラルで要素の値を指定せずコンマのみで。

const arr = [,,,,,,,,,,];

arr; // => [empty × 10]
const arr = [0,,1,,2,,3,,4,,];

arr; // => [0, empty, 1, empty, 2, empty, 3, empty, 4, empty]


length プロパティの指定

配列リテラルで長さ 0 の配列を生成し、後から length を指定。

const arr = [];

arr.length = 10;
arr; // => [empty × 10]
const arr = [0,1,2,3,4];

arr.length = 10;
arr; // => [0, 1, 2, 3, 4, empty × 5]


添字アクセス

配列リテラルで長さ 0 の配列を生成し、後から添字アクセスで指定。

const arr = [];

arr[9] = 0;
arr; // => [empty × 9, 0]
const arr = [0,1,2,3,4];

arr[9] = 0;
arr; // => [0, 1, 2, 3, 4, empty × 4, 0]


delete で要素を削除

密な配列の要素を delete 演算子で削除。

const arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];

delete arr[0];

arr; // => [empty, 1, 2, 3, 4, 5, 6, 7, 8, 9]
const arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];

delete arr[5];

arr; // => [0, 1, 2, 3, 4, empty, 6, 7, 8, 9]
const arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];

delete arr[9];

arr; // => [0, 1, 2, 3, 4, 5, 6, 7, 8, empty]

developer.mozilla.org


Array() コンストラクタ

Array() コンストラクタの引数で配列の長さ、要素数を指定。

const arr = new Array(10);

arr; // => [empty × 10]


なお new 演算子は省略できます。

const arr = Array(10);

arr; // => [empty × 10]


developer.mozilla.org


他にも「疎」な配列の作り方、意図せず「疎」になってしまう処理があるので注意しましょう。



疎な配列のデメリット

特にコードゴルフやワンライナにおいては「疎」な配列を「密」な配列にしたいことがよくあります。 例えば何らかの処理を任意の回数だけ繰り返したい場合です。

console.log()w と10回だけ出力したいとします。
長さが 10 の配列と forEach() で楽勝!と思いきや、以下のコードではなぜか1回も出力されません。

const arr = Array(10); // => [empty × 10]

arr.forEach(() => console.log('w')); // => 1回も出力されない

forEach() は「空(empty)」の要素に対して処理を行ってくれない、つまり「空」の要素をスキップしてしまうからです。


10回出力されるためには「空」の要素をすべて何らかの値で埋める必要があります。この場合、埋める値は何でも良いので 0 で埋めてみます。

const arr = Array(10).fill(0); // => [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

arr.forEach(() => console.log('w')); // => ちゃんと10回出力される!


このように、特にコードゴルフやワンライナでは「疎」な配列を「密」にしたい、 つまり値はなんでも良いからとにかく「空(empty)」の要素を「空」以外の何かにしたいケースはよくあります。


配列のメソッドにおける「空(empty)」要素の扱いの違い

配列のインスタンスメソッドはメソッドによって「空(empty)」の扱いに違いがあります。

以下の(古めの)メソッドは配列の「空」の要素をスキップしたり(undefined ではなく)「空」のままにしたりします。 (MDNより)


以下の(新しめの)メソッドは「空(empty)」の要素を undefined と同様に扱います。 (MDNより)
join()toLocaleString() などは新しめのメソッドではないですが)


配列のメソッドによって 「空(empty)」の要素の扱いが異なることに注意しましょう。

developer.mozilla.org


「疎」な配列を「密」にする12の方法

さていよいよ本題。コードゴルフにも役立つかもしれない、「疎」な配列を(要素数を変えずに)「密」にする12の方法をご紹介します。

① for ループ

まずは一番シンプルな方法から。

const arr = new Array(10);

for (let i = 0; i < arr.length; ++i) {
  arr[i] = 0;
}

arr; // => [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

for 文は式、値として扱えないのでコードゴルフでは使いづらいですね。

developer.mozilla.org


② fill()

すでにサンプルコードでも登場した fill()。 その名の通り配列の要素を任意の値で埋めるメソッドです。

const arr = new Array(10).fill(0);

arr; // => [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

この方法はよく使われており、コードの意図もわかりやすいです。


fill() の引数を省略すると undefined を指定したことになります。

const arr = new Array(10).fill();

arr; // => [undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined]


developer.mozilla.org


③ スプレッド構文で展開(コピー)

配列は反復可能(iterable)です。 具体的に言うと Array.prototype[Symbol.iterator] が定義・実装されています。

粗配列をスプレッド構文で展開すると「空(empty)」の要素が undefined になります。

const arr = [...Array(10)];

arr; // => [undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined]

配列リテラルとスプレッド構文の組み合わせは、配列を(手軽に)コピーする方法としてもよく使われます。


配列自体が反復可能ですが、あえて「イテレータ(Iterator)」を返すメソッドを挟んでみるとこうなります。

const arr = [...Array(10).keys()];

arr; // => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
const arr = [...Array(10).values()];

arr; // => [undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined]
const arr = [...Array(10).entries()];

arr; // => [Array(2), Array(2), Array(2), Array(2), Array(2), Array(2), Array(2), Array(2), Array(2), Array(2)]


ちなみにスプレッド構文が無かった時代によく用いられていた配列のコピー方法、 concat()slice() を使う方法では「疎」の配列を「密」にすることはできません。 concat()slice() は「空(empty)」を undefined と同等に扱わない(古い)メソッドだからです。

const arr = [].concat(Array(10));

arr; // => [empty × 10]
const arr = [].slice.apply(Array(10));

arr; // => [empty × 10]


developer.mozilla.org

developer.mozilla.org


④ Array() + Array()

Array() を二重に使ってみます。

const arr = Array(...Array(10));

arr; // => [undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined]

developer.mozilla.org


⑤ Array.apply()

const arr = Array.apply(null, Array(10));

arr; // => [undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined]

④ とやっていることはほぼ同じですね。


ちなみに apply() の兄弟、call() を無理やり使うとしたらこう。

const arr = Array.call(null, ...Array(10));

arr; // => [undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined]

apply()call() はクラス記法が登場する前はサブクラスのコンストラクタ関数内でスーパークラスのコンストラクタ関数を実行するときによく使っていました。


developer.mozilla.org

developer.mozilla.org


⑥ Array.of() + スプレッド構文

const arr = Array.of(...Array(10)); 

arr; // => [undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined]

developer.mozilla.org


⑦ Array.from()

Array.from() は「反復可能(iterable)なオブジェクト」 or「array-like なオブジェクト」を(本物の)配列にしてくれるクラスメソッドです。 この場合の array-like とは length プロパティを持つオブジェクトのことです。

Array(10) は反復可能で且つ array-like なオブジェクトです。(本物の配列を array-like と呼ぶもおかしいですが)

const arr = Array.from(Array(10));

arr; // => [undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined]

同時に値も指定したい場合は第2引数に渡す関数で。

const arr = Array.from(Array(10), () => 0);

arr; // => [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
const arr = Array.from(Array(10), (_, i) => i);

arr; // => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]


イテレータからでも。

const arr = Array.from(Array(10).keys());

arr; // => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]


型付き配列からでも。

const arr = Array.from(new Int8Array(10));

arr; // => [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]


ためしに自作の array-like オブジェクト、つまり length プロパティを持つオブジェクトを渡してみます。

const arr = Array.from({ length: 10 });

arr; // => [undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined]
const arr = Array.from({ length: Number(10) });

arr; // => [undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined]
const arr = Array.from({ length: '10' });

arr; // => [undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined]
const arr = Array.from({ length: '    10    ' });

arr; // => [undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined]


ちなみに、Array.from() が「反復可能(iterable)であること」と「array-like であること」のどちらを優先するかは、例えば以下のコードでチェックすることができます。

// 反復可能なオブジェクト
const obj = {
  [Symbol.iterator]() {
    return ['反復可能'][Symbol.iterator]();
  },
};

Array.from(obj); // => ['反復可能']
// array-like なオブジェクト
const obj = {
  0: 'array-like',
  length: 1,
};

Array.from(obj); // => ['array-like']
// 反復可能で且つ array-like なオブジェクト
const obj = {
  0: 'array-like が優先',
  length: 1,
  [Symbol.iterator]() {
    return ['反復可能が優先'][Symbol.iterator]();
  },
};

Array.from(obj); // => ['反復可能が優先']


developer.mozilla.org

tc39.es


⑧ toSorted()

sort() の非破壊版、toSorted() も使えます。 toSorted() は「空(empty)」を undefined と同等に扱ってくれます。

const arr = Array(10).toSorted();

arr; // => [undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined]

developer.mozilla.org


⑨ toReversed()

reverse() の非破壊版、toReversed() も。

const arr = Array(10).toReversed();

arr; // => [undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined]

developer.mozilla.org


⑩ toSpliced()

splice() の非破壊版、toSpliced() も。

const arr = Array(10).toSpliced();

arr; // => [undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined]

developer.mozilla.org


⑪ with()

const arr = Array(10).with();

arr; // => [undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined]

developer.mozilla.org


⑫ JSON.parse() + JSON.stringify()

ちょっと強引ですがこんな方法も。

const arr = JSON.parse(JSON.stringify(Array(10))); 

arr; // => [null, null, null, null, null, null, null, null, null, null]


JSON.parseevalFunction で代替しても良いですね。

const arr = eval(JSON.stringify(Array(10))); 

arr; // => [null, null, null, null, null, null, null, null, null, null]
const arr = Function(`return${JSON.stringify(Array(10))}`)();

arr; // => [null, null, null, null, null, null, null, null, null, null]

developer.mozilla.org developer.mozilla.org


本記事で紹介する方法は以上です。まだまだあるかもしれません。まだまだあるでしょう。
他にもこんな面白い方法がある!という方はぜひ教えてください。



おまけ:「粗」な配列の「空(empty)」の要素のみを取り除く方法

filter()

const arr = [0,,1,,2,,3].filter(() => true);

arr; // => [0, 1, 2, 3]

developer.mozilla.org


flat()

const arr = [0,,1,,2,,3].flat();

arr; // => [0, 1, 2, 3]

developer.mozilla.org


flatMap();

const arr = [0,,1,,2,,3].flatMap(item => item);

arr; // => [0, 1, 2, 3]


なお map() では NG です。

const arr = [0,,1,,2,,3].map(item => item);

arr; // => [0, empty, 1, empty, 2, empty, 3]


developer.mozilla.org developer.mozilla.org



まとめ

最後まで読んでいただき、ありがとうございます。

今回は「疎」な配列を「密」にする12の方法をまとめてみました。 まだまだあると思うので、他にもこんな面白い方法がある!という方はぜひ教えてください。

本記事の内容は、もしかしたら現在開催中のJS体操第3問「Zalgo Textの生成」のヒントにもなるかもしれません。

『JS体操』とは面白法人カヤックが主催する JavaScript のコードゴルフ大会。
その第3問は任意の文字列を「Zalgo Text」に変換する JavaScript の長〜いコードをできるだけ短くする!というお題です。 元々2222文字もあるコードを、なんと115文字以下まで減らせます。(現状の最短記録は114文字です)

みなさまのご参加をお待ちしております!

JS体操第3問「Zalgo Text の生成」

JS体操第3問「Zalgo Text の生成」編集画面 JS体操第3問「Zalgo Text の生成」編集画面(エラー)

hubspot.kayac.com

techblog.kayac.com

techblog.kayac.com

CEL(Common Expression Language)を使ってIAMポリシーを検索する iam-policy-finder

SREチームの藤原です。

今回は CEL(Common Expression Language) を使って、AWSのIAMポリシーを検索するツールを作ったので紹介します。

github.com

3行でまとめ

  • CEL (Common Expression Language)の式を指定してAWS IAMポリシーを検索するツールをOSSとして作りました。GetAccountAuthorizationDetails APIで取得したIAMポリシーをCELで評価して、マッチするものを出力します
  • 例えば「lambda:GetFunctionがあるがlambda:ListTagsがないポリシーを探す」などができます
  • AWSからたびたびやってくる、IAMポリシーに関するお知らせに対応するのに便利です

突然の「Action Required」

ある日、AWSからこんなメールが届きました。

Lambda GetFunction API の認証に変更に際し、お客様のアクションが必要になる可能性があるため、ご連絡しております。

これまで、ListTags API を明示的に使用する場合にのみ、ListTags への権限が必要でした。しかし、GetFunction API 権限を持つプリンシパルにおいても、GetFunction 呼び出しにより出力されたタグ情報にはアクセスできました。2024 年 7 月 27 日以降、Lambda は GetFunction API を呼び出すプリンシパルに ListTags API に対する明示的な許可権限が設定されたポリシーがある場合にのみタグデータを返すようになります。GetFunction API を呼び出すロールに対して、拒否ポリシーが設定されているか、ListTags API へのアクセスを明示的に許可するポリシーがない場合、Lambda は GetFunction API 呼び出しへの応答でタグデータを返さなくなります。

お客様のアカウントには、GetFunction API へのアクセスを許可するロールがあることを確認しましたが、そのポリシーでは ListTags API へのアクセスが許可されていません。引き続き GetFunction API を使用してタグデータを受信する場合は、GetFunction API を呼び出すために使用される AWS Identity and Access Management (IAM) ロールに、明示的な「ListTags API のアクセス許可」をするポリシーを追加する必要があります

要約すると…

  • (これまで) Lambda GetFunction APIでlambda:ListTagsがなくてもタグが取得できていた
  • (これから)lambda:ListTags権限がないとタグが取得できなくなる
  • このアカウントにはlambda:GetFunctionがあるけどlambda:ListTagsがないポリシーがあるよ
  • 期日までにlambda:ListTagsを許可しないとタグが取れなくなるよ!

というお知らせです。なるほど、事情は分かります。おそらくタグの扱いを厳格にしたいのですね。

しかし、そのようなポリシーがあるのが分かっているならどれが該当するのかまで教えてくれればよさそうなものですが、この種のお知らせにおいてはなぜか教えてくれないことが多いのです。

ある条件を持つIAMポリシーを探す方法

どうやってIAMポリシーを洗い出すか、まず考えるのがマネージメントコンソールでの目視です。しかしこれは大変です。なにしろ数が多すぎる (1358) ←

IAMマネージメントコンソール

AWSの人に聞いたところ、aws iam get-account-authorization-detailsを使えばアカウント内の許可一覧を取得できると教えてもらえました。

しかし、これも結局目視でポリシーを探す必要があります。出力はJSONなのでjq芸でなんとかしたくなりますが、これも意外と難しいのです。IAMのポリシーJSONは配列でも文字列でもよい要素(ActionResource)があり、人間が書くには便利なのですが、機械に読ませるにはちょっと面倒な構造をしているためです。

仕方ないので、ちゃっちゃとAWS SDK Go(v2)でコードを書きました。1から書いても30分ぐらいなので悩んでるより早いです。

lambda:GetFunctionがあるのにlambda:ListTagsがないポリシーを見つけるくん · GitHub

やっていることは簡単で、GetAccountAuthorizationDetails APIでアカウント内の許可一覧を取得して、ポリシーの"JSON文字列"に対してGoのコードで評価するだけです。

"lambda:GetFunction"があるが"lambda:ListTags"がないポリシーを探すための評価式はこんな感じです。

func detect(s string) bool {
  d, _ := url.QueryUnescape(s) // APIの結果はURL escapeされている
  l := strings.ToLower(d)      // Actionは大文字小文字を区別しないので小文字に揃える
  return strings.Contains(l, `"lambda:getfunction"`) && !strings.Contains(l, `"lambda:listtags"`)
}

これを、お知らせが来たアカウントで実行して回れば解決です。

果たして実行してみたところ、複数のアカウントでぽろぽろと見つかったのは AWSSupportServiceRolePolicy でした。これはAWSのマネージドポリシーです。こちらでは編集できないので、どうしろという感じですが……

(このポリシーを何かにattachして使っている場合は問題が起きるので通知自体は仕方ないのですが、なんとかならないものでしょうか)

また別の「Action Required」が

しかし、続けてまた別のお知らせが来ました。

[要対応] IAM ポリシーを更新して明示的な ecs:TagResource の「許可」を追加してください

ECSでもecs:CreateCluster時にecs:TagResourceが必要になると言われています。例によって、どのポリシーが該当しているかは教えてもらえません。特定の条件でポリシーを探したいことはこれからも多そうです。

💡 評価式部分を汎用化したら使い回せるのでは?

ここで、評価式をGoのコードに埋め込むのではなくなんらかの形で外部から与えることができれば、IAMポリシーを検索する汎用的なツールになりそうだと思いつきました。

問題は「式評価」を何で実装するか。自作するのは大変です。汎用言語(Lua, JavaScriptなど)を組み込むこともできますが、今回はGoogleが先日発表したCEL(Common Expression Language)を使ってみることにしました。

cel.dev

CELは非チューリング完全で式評価に特化した、高速、安全に実行可能な式評価言語です。Go, Java, C++に組み込み可能で、GoogleCloudのCertificate Authority Service, Kubernetes, Istio などで既に実用されています。

CEL式の例は以下のような感じです。言語の定義はこちら

// 数値の範囲チェック
age >= 18 && age < 65

// 文字列の前方一致や正規表現マッチ
name.startsWith('Foo')
name.matches('^Foo.*')

// リスト内の要素に条件を満たすものがあるか
roles.exists(role, role == 'admin')

// 日付、時間の比較
request.date < timestamp('2023-12-31T23:59:59Z')
timeout >= duration('10s')

値に型があり(例えばbool, int, uint, double, string, timestamp, duration...)、演算子も一通り揃っています。文字列関数もあります。これらを組み合わせて評価した結果を、任意の値(boolだけではなく)で返せるため、今回の用途にはちょうどよさそうです。

iam-policy-finder

ということで作成した、CELを使ってIAMポリシーを検索するツールがこちらです。

github.com

Usage: iam-policy-finder <expr> [flags]

Arguments:
  <expr>    CEL expression string or file name

Flags:
  -h, --help                     Show context-sensitive help.
      --dump                     dump found policy document
  -f, --filter=FILTER,...        filter policy document(User, Group, Role, LocalManagedPolicy,
                                 AWSManagedPolicy)
      --debug                    debug logging
      --skip-evaluation-error    skip evaluation error
      --[no-]progress            show progress dots
      --lc                       convert action to lower case
  1. GetAccountAuthorizationDetails でポリシーを取得
  2. ポリシーをCELで式評価をして真になるものを出力

やっていることはこれだけです。以下は具体的な使い方の例です。

ポリシーの名前で検索する

単純に、ポリシーの名前の一致で検索する例です。

$ iam-policy-finder 'Name == "AmazonEC2FullAccess"'

time=2024-07-18T17:37:13.110+09:00 level=INFO msg="starting scan" expr="Name == \"AmazonEC2FullAccess\"" filter=[]
time=2024-07-18T17:37:26.272+09:00 level=INFO msg=found policy=AmazonEC2FullAccess versions="[v5 v4 v3 v2 v1]" attached=2
time=2024-07-18T17:37:33.377+09:00 level=INFO msg=finished found=5 scanned=975

ポリシーのJSONを文字列比較する

最初に書いた雑ツールの「ポリシー文字列に"lambda:GetFunction"があり"lambda:ListTags"がない」という条件で検索する例をCELにすると以下のようになります。

Document.matches('"lambda:[Gg]et[Ff]unction"')
&&
!Document.matches('"lambda:[Ll]ist[Tt]ags"')

変数DocumentにはポリシーのJSON文字列が入っています。matchesは、文字列に対して正規表現マッチを行うCELの関数です。

IAM policy actionは大文字小文字を区別しないという仕様があります。CELには大文字小文字を変換したり、同一視して比較する方法がデフォルトでは用意されていないため、正規表現の文字クラスを使っています。

正規化したポリシーを評価する

雑な文字列マッチだけでは少々誤検知が多そうなので、少し頑張ってポリシーを正規化してみました。 JSON文字列をパースして、StatementActionResourceが文字列だった場合は全てリストに正規化します。

例えば以下のポリシーは

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": "s3:*",
      "Effect": "Allow",
      "Resource": "*",
      "Sid": "1"
    }
  ]
}

このように正規化されてから処理されます。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": ["s3:*"],
      "Effect": "Allow",
      "Resource": ["*"],
      "Sid": "1"
    }
  ]
}

この正規化したStatementを使って、「s3:*を許可している」ポリシーを検索する式はこのようになります。

Statement.exists(s, (s.Action.exists(a, a == "s3:*") && s.Effect == "Allow"))

exists(要素, 式)はCELで使える、リストの要素を式で評価してどれかが真になれば真になる関数です。

Actionを小文字に正規化して比較

CELには大文字小文字を変換したり無視して比較する方法が(デフォルトでは)ないので、Actionを小文字に正規化して比較するオプションを追加しました。

--lc オプションを付けると、正規化時にAction要素を全部小文字に変換します。

「Actionにlambda:GetFunctionがあるがlambda:ListTagsがない」式を--lcと一緒に使うと以下のように書けます。

Statement.exists(s,
  (
    s.Action.exists(a, a == "lambda:getfunction")
    &&
    !s.Action.exists(a, a == "lambda:listtags")
  )
)

元のポリシーJSON文字列Documentは小文字になりません。

ECSの件を早速調べてみた

さて、よいツールができたので早速ECSの件を調べてみましょう!

ecs:CreateClusterがあるがecs:TagResourceがないもの」を--lcオプション付きで探す式は以下のようになります。

// ecs.cel
Statement.exists(s, (
    s.Action.exists(a, a == "ecs:createcluster")
    &&
    !s.Action.exists(a, a == "ecs:tagresouce")
))
$ iam-policy-finder --dump --lc  ecs.cel

CEL式はコマンドライン引数で直接渡すこともできますが、式をファイルに書いてそのファイル名を引数に渡すこともできます。

--dump は見つかったポリシーJSON文字列も出力するオプションです。

$ iam-policy-finder --dump --lc  ecs.cel
time=2024-07-18T18:02:23.881+09:00 level=INFO msg=found
policy=AmazonEC2ContainerServiceforEC2Role versions="[v7 v6 v5 v4 v3 v2 v1]" attached=1
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ecs:CreateCluster",
        "ecs:DeregisterContainerInstance",
        "ecs:DiscoverPollEndpoint",
        "ecs:Poll",
        "ecs:RegisterContainerInstance",
        "ecs:Submit*"
      ],
      "Resource": "*"
    }
  ]
}

見つかったのは AmazonEC2ContainerServiceforEC2Role ……AWSのマネージドポリシーでした!!!

まとめ

  • CEL (Common Expression Language)の式を指定してAWS IAMポリシーを検索するツールをOSSで作りました。GetAccountAuthorizationDetails APIで取得したIAMポリシーをCELで評価して、マッチするものを出力します
  • 例えば「lambda:GetFunctionがあるがlambda:ListTagsがないポリシーを探す」などができます
  • AWSからたびたびやってくる、IAMポリシーに関するお知らせに対応するのに便利です

どうぞご利用ください。

カヤックではOSSが好きなエンジニアを募集しています!