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, 可视化界面, 权限审计, 特权升级检测, 网络流量审计, 请求拦截, 通知系统