fwxs/aws-iam-grapher

GitHub: fwxs/aws-iam-grapher

将 AWS IAM 权限关系导入 Neo4j 图数据库,帮助安全工程师可视化审计有效权限并检测提权路径。

Stars: 0 | Forks: 0

# aws-iam-grapher 将 AWS IAM 权限收集到 Neo4j 图中,并对其运行安全分析查询。专为需要审计有效权限、检测提权路径以及对比不同时间点权限快照的安全工程师设计。 ## 前置条件 - **Rust 1.82.0 或更高版本** — 通过 [rustup](https://rustup.rs) 安装 - **Docker** — 运行 Neo4j Community Edition 和集成测试套件所必需 - **AWS credentials** — 实时和混合收集模式所必需(`~/.aws/credentials`、环境变量或 IAM role) - **Neo4j Community Edition** — 在本地运行或可通过 Bolt 协议访问 ## 安装说明 ``` git clone https://github.com//aws-iam-grapher cd aws-iam-grapher cargo build --release ``` 编译后的二进制文件位于 `target/release/aws-iam-grapher`。 ## 使用 Docker 运行 Neo4j `docker-compose.yml` 仅运行 Neo4j;`aws-iam-grapher` 二进制文件由 `cargo` 在本地构建并运行。Neo4j 的 `/data` 目录由 命名卷 `neo4j_data` 支撑,因此快照在容器重启后依然存在。 ``` export NEO4J_PASSWORD=changeme # required, no default password # 启动 Neo4j 并等待其变为 healthy docker compose up -d neo4j # 对其运行 binary cargo run --release -- collect \ --mode offline \ --input-file ./data/auth-details.json \ --neo4j-uri bolt://localhost:7687 \ --neo4j-user neo4j ``` **持久化与重置:** ``` docker compose down # stops containers, keeps the neo4j_data volume docker compose up -d neo4j # data from prior collect runs is still there docker volume inspect aws-iam-grapher_neo4j_data # see where Docker stores it docker compose down -v # drops the volume — full reset, all snapshots lost ``` **备份与恢复:** Neo4j Community 不支持在线(热)备份 —— 仅支持离线备份。`scripts/neo4j-backup.sh` 和 `scripts/neo4j-restore.sh` 自动执行离线流程:停止 `neo4j` 容器,将 `aws-iam-grapher_neo4j_data` 卷复制到带时间戳的 tarball 中(或从中恢复),然后重启。在复制期间容器会处于停机状态。 ``` scripts/neo4j-backup.sh # writes ./backups/neo4j-backup-.tar.gz scripts/neo4j-restore.sh -f ./backups/neo4j-backup-.tar.gz ``` **批处理大小与规模扩展:** `--batch-size`(默认 500,适用于每个 `collect` 子命令)控制导入期间每个事务中 Neo4j 提交的写入次数。请参阅 [`docs/limitations.md` § 验证的规模上限](docs/limitations.md#validated-scale-ceiling) 以获取调优指南,以及针对接近 ~10,000 个权限节点实际上限的账号的账户分片策略。 ## 运行测试 ### 单元测试 单元测试没有外部依赖,完全在进程内运行: ``` cargo test --workspace ``` ### 集成测试(需要 Docker) `crates/iam-graph/tests/` 中的集成测试使用 [testcontainers-rs](https://github.com/testcontainers/testcontainers-rs) 启动真实的 Neo4j 容器。它们被标注了 `#[ignore = "requires Docker"]`,因此默认情况下不会运行 —— 你需要显式启用。 #### 在 macOS 上配合 Colima 运行 Colima 在非标准路径暴露 Docker socket,且其默认 profile 限制了容器权限。在运行测试之前,你需要设置两个环境变量: ``` # 将 testcontainers 指向 Colima socket export DOCKER_HOST="unix://${HOME}/.colima/default/docker.sock" # 禁用 ryuk — 它需要 Colima 不允许的 privileged mode export TESTCONTAINERS_RYUK_DISABLED=true ``` 如果你使用的是具名 Colima profile 而不是 `default`,请相应地调整 socket 路径: ``` export DOCKER_HOST="unix://${HOME}/.colima//docker.sock" ``` 变量设置完成后,运行集成测试: ``` # workspace 中所有受 Docker 控制的测试 cargo test --workspace -- --ignored # 仅限 iam-graph 集成测试 cargo test -p iam-graph -- --ignored ``` #### 容器管理方式 每个测试二进制文件(`tests/` 中的每个文件对应一个)都会启动一个共享的 Neo4j 容器。容器初始化一次,schema 也只设置一次,然后该二进制文件中的所有测试都重用同一个正在运行的实例。测试之间保持相互隔离,因为每个测试都在 `IngestConfig` 内部创建自己的 `snapshot_id` —— 一个测试写入的数据对另一个测试是不可见的。 总共会启动四个容器(每个测试二进制文件一个),而不是每个测试函数一个。 #### 验证容器是否正在运行 在测试执行期间,你可以检查哪些容器处于活动状态: ``` DOCKER_HOST="unix://${HOME}/.colima/default/docker.sock" \ docker ps --filter ancestor=neo4j ``` #### 测试运行后清理 由于禁用了 ryuk,测试完成后容器不会被自动移除。完成后请手动移除它们: ``` DOCKER_HOST="unix://${HOME}/.colima/default/docker.sock" \ docker container prune -f ``` ## 快速入门 ### 场景 A — 直接实时访问账号 ``` export NEO4J_PASSWORD=your-password aws-iam-grapher collect \ --mode live \ --profile production \ --account-alias production ``` `--profile` 选择一个本地命名的 AWS profile 进行认证;有关完整的优先级顺序和离线模式行为,请参阅 `aws-iam-grapher collect --help`。 ### 场景 B — 避免 CloudTrail 噪音(离线) ``` # Step 1:导出 IAM 授权详情(单次 API 调用,无需持续访问权限) aws iam get-account-authorization-details \ --output json > account-auth-details.json # Step 2(可选):导出 instance profiles aws iam list-instance-profiles \ --output json > instance-profiles.json # Step 3:ingest 导出的数据,无需任何进一步的 AWS API 调用 export NEO4J_PASSWORD=your-password aws-iam-grapher collect \ --mode offline \ --input-file account-auth-details.json \ --profiles-file instance-profiles.json \ --account-alias production ``` ### 场景 C — 自动模式(尝试实时访问,在拒绝访问时提示提供文件) ``` export NEO4J_PASSWORD=your-password aws-iam-grapher collect --mode hybrid --account-alias production ``` ### 场景 D — 组织范围收集(`collect org`) 枚举 AWS Organization 下的每个账号,通过 assume 一个 jump role 进入每个账号,并将它们全部归档到同一个 `org_collection_run_id` 下: ``` aws-iam-grapher collect org \ --management-profile org-management \ --jump-from-profile default \ --assume-role-name OrganizationAccountAccessRole \ --neo4j-pass "$NEO4J_PASSWORD" ``` `--exclude-ou-id`/`--exclude-ou-name` 和 `--include-ou-name` 用于限定要收集哪些 OU; `--ou-profile-override =` 通过命名的本地 profile 而不是 assume-role 来收集某个子树。如果某个子树在*不同的名称*下(而非 `--assume-role-name`)暴露了跨账号 role,请使用 `--ou-role-override =`(可重复使用)来为该 OU 及其子代下的账号 assume 该 role: ``` aws-iam-grapher collect org \ --management-profile org-management \ --jump-from-profile default \ --assume-role-name OrganizationAccountAccessRole \ --ou-role-override LegacyAcquisition=CrossAccountAuditRole \ --neo4j-pass "$NEO4J_PASSWORD" ``` 所有这些标志都是可重复的,同时匹配 OU id 和显示名称,并且其完整文档 —— 匹配/优先级规则、继承、验证和边缘情况 —— 均记录在 [docs/limitations.md](docs/limitations.md) 中。 ## 组织范围收集(`collect org`) `collect org` 枚举 AWS Organization 中的每个账号,并从每个账号收集 IAM 数据,使用相同的组织收集运行 ID 及其 Organizational Unit 谱系标记每个账号。其中涉及两个独立的身份: - `--management-profile` — 仅用于从管理账号调用 Organizations API(枚举 OU 和 账号)。从不用于 role 假定。 - `--jump-from-profile` — 在每个成员账号中假定(assume) `--assume-role-name` 的源身份。 如果省略,则默认为标准的 AWS 凭证链(`AWS_PROFILE` / `default` profile)。 这是有意保持分开的:如果 `--management-profile` 自身解析为一个假定的 role(一个 SSO profile,或者带有 `role_arn`/`source_profile` 链的 profile),重用其凭证再次调用 `AssumeRole` 将会导致大多数 jump-role 信任策略拒绝的双重跳转假定。 ``` export NEO4J_PASSWORD=your-password aws-iam-grapher collect org \ --management-profile org-management \ --jump-from-profile default \ --assume-role-name OrganizationAccountAccessRole \ --neo4j-pass "$NEO4J_PASSWORD" ``` ### 限定要收集的账号范围 - `--exclude-ou-id ` / `--exclude-ou-name `(可重复使用) — 将某个 OU 及其所有 子代从收集中剔除。 - `--include-ou-name `(可重复使用) — 将收集范围限定为仅限给定的 OU 及其 子代;其他所有账号均被跳过。即使匹配了 include 条件,`--exclude-ou-id`/`--exclude-ou-name` 仍然会将其 剔除。在遍历树时,如果某个 id/name 从未匹配到任何 OU,将以警告形式报告,而不是被默默忽略。 ``` aws-iam-grapher collect org \ --management-profile org-management \ --assume-role-name OrganizationAccountAccessRole \ --exclude-ou-id ou-root1-sandbox \ --include-ou-name Production \ --neo4j-pass "$NEO4J_PASSWORD" ``` ### 混合认证组织(`--ou-profile-override`) 某些账号完全无法从 `--jump-from-profile` 假定 jump role —— 例如,一个需要其自己的 SSO profile 或一组单独的长期静态凭证的隔离 OU。`--ou-profile-override =`(可重复使用)会使匹配的 OU 及其所有子代 OU 下的账号从该命名的本地 profile 而不是 `--jump-from-profile` 假定 `--assume-role-name`。 **这不是绕过 assume-role 的方法** —— 覆盖 profile 仅用于调用 `sts:AssumeRole`,与 `--jump-from-profile` 完全一样,只是范围仅限于该 OU 子树而不是整个运行。每个账号无论从哪个 profile 假定,仍然会调用 `sts:AssumeRole` 进入同名的 role,并落入同一个收集运行中。覆盖 profile 本身只需要具有假定 `--assume-role-name` 的权限 —— 它不需要 `iam:GetAccountAuthorizationDetails`,因为它从不用于直接调用 IAM API。 ``` aws-iam-grapher collect org \ --management-profile org-management \ --jump-from-profile default \ --assume-role-name OrganizationAccountAccessRole \ --ou-profile-override Quarantine=legacy-static-creds \ --ou-profile-override ThirdParty=vendor-sso \ --neo4j-pass "$NEO4J_PASSWORD" ``` 匹配机制映射了 `--exclude-ou-id`/`--exclude-ou-name`:键会同时与 OU 的 id 及其显示名称进行比对,当嵌套的覆盖 OU 发生冲突时,最内层(最近的祖先)覆盖优先。在遍历树时,如果某个覆盖键从未匹配到任何 OU,则属于**致命**的验证错误;如果某个覆盖 profile 的凭证无法解析,同样属于**致命**错误 —— 这两者都会在任何账号被触及之前导致收集失败。详见 [`docs/limitations.md`](docs/limitations.md)。 ### 收集并发(`--concurrency`) `collect org` 以有限的并发数(而不是一次一个)收集成员账号。 `--concurrency `(默认 **4**)限制并行收集的账号数量;超出 `[1, 16]` 范围的值会被报错拒绝,而不是被默默调整。保持保守默认值的原因是 限制因素在于 AWS 端针对每个账号的 IAM 限流和 jump-role STS 信任设置, 而不是本地 CPU。输出(`OrgCollectionResult.accounts`)始终按账号 id 排序,因此无论哪些账号先完成,输出都是确定性的。 ``` aws-iam-grapher collect org \ --management-profile org-management \ --jump-from-profile default \ --assume-role-name OrganizationAccountAccessRole \ --concurrency 8 \ --neo4j-pass "$NEO4J_PASSWORD" ``` ## 日志记录 `collect` 和 `collect org` 通过 `tracing` 在 `info`/`debug` 级别记录其执行的每次 AWS API 调用(区域解析、分页进度、按账号进行的 jump-role 假定等)。 默认显示 `info` 级别的日志;如需更多细节(例如获取的每个分页页面),请设置: ``` RUST_LOG=iam_collector=debug aws-iam-grapher collect --mode live --account-alias production ``` ## 按收集模式划分的数据覆盖范围 下表显示了每种模式下可用的数据。缺失的数据可能会潜移默化地使分析结果产生偏差 —— 在解读图形输出之前,请先查看此表。 | 字段 / 实体 | 实时模式 | 离线模式 | |---|---|---| | 用户 | ✓ 完整 | ✓ 如果存在于 `get-account-authorization-details` 中 | | 角色 | ✓ 完整 | ✓ 如果存在于 `get-account-authorization-details` 中 | | 托管策略 | ✓ 完整 | ✓ 如果存在于 `get-account-authorization-details` 中 | | 内联策略 | ✓ 完整 | ✓ 如果存在于 `get-account-authorization-details` 中 | | 实例配置文件 | ✓ 通过 `list-instance-profiles` | ⚠ 需要 `--profiles-file` | | 通配符展开 | ✓ 通过 awsiamactions.io | ✓ 通过 awsiamactions.io | | 实体的 `create_date` | ✓ | ✓ | | `is_aws_managed` | ✓ 从 ARN/路径派生 | ✓ 从 ARN/路径派生 | | 带有边界的有效权限 | ⚠ 记录了边界,但未进行评估 | ⚠ 记录了边界,但未进行评估 | 有关 V1 分析的局限性,请参阅 [`docs/limitations.md`](docs/limitations.md)。 ## 查询命令 所有查询命令都需要 `--neo4j-pass`(或 `NEO4J_PASSWORD` 环境变量)。如果省略 `--snapshot-id`,则会自动使用该账号最近的快照。 `--account-id` 是可选的。提供该参数时,查询将精确限定于该账号(与之前一样)。当其**被省略**时,`query` 会解析图中至少含有一个快照的每个不同账号,并按账号运行一次查询,每次都正确限定在各自的 `(account_id, snapshot_id)` 范围内 —— 绝不会跨账号合并结果。这适用于 `who-can`、`entity-perms`、`instance-profiles-with`、`privilege-escalation` 和 `list-snapshots`。输出(表格和 JSON)将结果按账号分组,放在 `=== Account: ... ===` 表头(表格)或每个账号的 `account_id`/`snapshot_id`/`results` 信封(JSON)下。如果图中只有一个账号,则退化为单个组。`--snapshot-id` 不能与多账号模式(解析出多个账号)结合使用,因为快照 ID 跨账号会产生歧义 —— 请传递 `--account-id` 以指定单个账号。 当省略 `--account-id` 时,`diff` 会从其两个快照 ID 中派生出账号,如果这两个快照属于不同的账号,则会报错。 `list-accounts` 本质上是跨账号的,从不要求(或使用)`--account-id` —— 使用它来发现图中存在哪些账号,然后再用 `--account-id` 定位特定账号。 添加 `--output-file ` 可将结果作为 JSON 写入文件,而不管 `--output` 如何设置。人类可读的表格仍会打印到 stdout —— 这对于希望获得干净 JSON 产出物而无需抓取 stdout/stderr 的下游工具非常有用: ``` aws-iam-grapher query \ --account-id 123456789012 \ --output-file who-can.json \ who-can s3:DeleteObject ``` `collect` 子命令为其摘要支持相同的 `--output-file` 标志。 ### 谁能执行操作? ``` aws-iam-grapher query \ --account-id 123456789012 \ who-can s3:DeleteObject ``` ``` Entities with permission s3:DeleteObject (snapshot: a3f2c1d0) TYPE ARN RESOURCE ────── ──────────────────────────────────────────────── ───────── Role arn:aws:iam::123456789012:role/DataEngineer * Role arn:aws:iam::123456789012:role/S3AdminRole * User arn:aws:iam::123456789012:user/alice * ``` 添加 `--resource <>` 可将 `Action: "*"`(完全管理员)授权与特定资源进行交集比对,排除那些资源范围不涵盖该特定资源的授权: ``` aws-iam-grapher query \ --account-id 123456789012 \ who-can s3:DeleteObject --resource arn:aws:s3:::my-bucket/object.txt ``` 一个具有 `"Action": "*", "Resource": "arn:aws:s3:::my-bucket"` 的主体在这里会被排除,因为授权范围仅限存储桶,而非对象。请参阅 [`docs/limitations.md`](docs/limitations.md)。 ### 实体的所有权限 ``` aws-iam-grapher query \ --account-id 123456789012 \ entity-perms arn:aws:iam::123456789012:role/DataEngineer ``` ``` Permissions for arn:aws:iam::123456789012:role/DataEngineer EFFECT ACTION RESOURCE ─────── ─────────────── ──────── Allow s3:GetObject * Allow s3:PutObject * Allow s3:DeleteObject arn:aws:s3:::my-bucket/* ``` ### 授予操作的实例配置文件 ``` aws-iam-grapher query \ --account-id 123456789012 \ instance-profiles-with iam:PassRole ``` ``` Instance profiles granting iam:PassRole (snapshot: a3f2c1d0) NAME ARN ───────────── ────────────────────────────────────────────────────────────── EC2DevProfile arn:aws:iam::123456789012:instance-profile/EC2DevProfile ``` ### 提权路径 ``` aws-iam-grapher query \ --account-id 123456789012 \ privilege-escalation ``` ``` Privilege escalation paths (snapshot: a3f2c1d0) ENTITY RISKY ACTIONS ──────────────────────────────────────────────── ────────────────────────────────── arn:aws:iam::123456789012:role/DevRole iam:PassRole, iam:AttachRolePolicy arn:aws:iam::123456789012:user/developer iam:CreatePolicyVersion ``` ### 列出账号 无需 `--account-id` —— 列出当前图中的所有账号。通过 `collect org` 收集的账号会显示其直接的 Organizational Unit ID/名称;通过独立 `collect`(实时/离线/混合)收集的账号则显示空白的 OU 列。 ``` aws-iam-grapher query list-accounts ``` ``` ACCOUNT ID ALIAS OU ID OU NAME ────────────── ───────────── ────────────── ─────────── 111122223333 production 222233334444 staging ou-root1-a1b2 Sandbox ``` ### 列出快照 ``` aws-iam-grapher query \ --account-id 123456789012 \ list-snapshots ``` ``` SNAPSHOT ID ACCOUNT COLLECTED AT STATUS ───────────────────────────────────── ────────────── ───────────────────── ────── a3f2c1d0-4e5b-6c7d-8e9f-0a1b2c3d4e5f 123456789012 2024-01-15T14:32:00Z full b4e3d2c1-5f6a-7b8c-9d0e-1f2a3b4c5d6e 123456789012 2024-01-08T09:15:00Z full ``` ### 两个快照之间的差异 ``` aws-iam-grapher query \ --account-id 123456789012 \ diff a3f2c1d0-... b4e3d2c1-... ``` ``` Permission diff between a3f2c1d0-... and b4e3d2c1-... NEW PERMISSIONS (in b4e3d2c1-..., not in a3f2c1d0-...): [+] Allow s3:DeleteBucket * [+] Allow iam:CreateUser * REMOVED PERMISSIONS (in a3f2c1d0-..., not in b4e3d2c1-...): [-] Allow ec2:TerminateInstances * ``` ### 删除快照 ``` aws-iam-grapher query \ --account-id 123456789012 \ delete-snapshot a3f2c1d0-4e5b-6c7d-8e9f-0a1b2c3d4e5f ``` ## Neo4j Community 设置 Neo4j Community Edition 是免费的,不需要许可证。 ``` docker run \ --name neo4j-iam \ -p 7474:7474 -p 7687:7687 \ -e NEO4J_AUTH=neo4j/your-password \ neo4j:community ``` Neo4j Browser 随后可在 `http://localhost:7474` 访问。此工具使用的 Bolt endpoint 为 `bolt://localhost:7687`。 **注意:** Neo4j Community 仅支持单个数据库。账号隔离是逻辑上的(通过每个节点上的 `account_id` 属性强制执行),而不是物理隔离的。有关此设计的影响,请参阅 [`docs/limitations.md`](docs/limitations.md)。 ## 工作区架构 ``` aws-iam-grapher/ ├── crates/ │ ├── iam-models (lib) ──────────────────────────────┐ │ │ │ │ ├── iam-expander (lib) ──────────────────────────────┤ │ │ │ │ ├── iam-collector (lib) ── uses iam-models │ │ │ ── uses iam-expander │ │ │ │ │ ├── iam-graph (lib) ── uses iam-models │ │ │ ── uses iam-collector │ │ │ │ │ └── iam-grapher (bin) ── uses iam-collector ────────┘ │ ── uses iam-graph │ ── uses iam-models └── ``` ## Crate 参考 | Crate | 类型 | 职责 | |---|---|---| | `iam-models` | lib | 核心 IAM 实体类型:`IamRole`、`IamUser`、`IamPolicy`、`IamGroup`、`IamInstanceProfile`、`PolicyDocument` | | `iam-expander` | lib | 通过 awsiamactions.io 将通配符 IAM 操作(`s3:*`)展开为其完整枚举列表 | | `iam-collector` | lib | 从实时 AWS API、离线 JSON 导出或两者的混合模式收集 IAM 数据 | | `iam-graph` | lib | 将收集的数据导入 Neo4j 并执行 Cypher 分析查询 | | `iam-grapher` | bin | 提供 `collect` 和 `query` 子命令的 CLI 入口点 |
标签:AWS IAM, Neo4j, Rust, 可视化界面, 权限审计, 特权升级检测, 网络流量审计, 请求拦截, 通知系统