aegis-devops 0.2.1__tar.gz → 0.3.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. {aegis_devops-0.2.1/src/aegis_devops.egg-info → aegis_devops-0.3.1}/PKG-INFO +9 -5
  2. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/README.md +8 -4
  3. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/pyproject.toml +1 -1
  4. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/__init__.py +1 -1
  5. aegis_devops-0.3.1/src/aegis_core/_examples/__pycache__/__init__.cpython-312.pyc +0 -0
  6. aegis_devops-0.3.1/src/aegis_core/_examples/agents.example.yaml +41 -0
  7. aegis_devops-0.3.1/src/aegis_core/_examples/agents.example.yaml.sig +2 -0
  8. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/_examples/authority.example.yaml +1 -0
  9. aegis_devops-0.3.1/src/aegis_core/_examples/authority.example.yaml.sig +2 -0
  10. aegis_devops-0.3.1/src/aegis_core/audit_aws.py +324 -0
  11. aegis_devops-0.3.1/src/aegis_core/audit_kubernetes.py +312 -0
  12. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/cli.py +468 -0
  13. aegis_devops-0.3.1/src/aegis_core/compile/__init__.py +81 -0
  14. aegis_devops-0.3.1/src/aegis_core/compile/actions/aws.yaml +325 -0
  15. aegis_devops-0.3.1/src/aegis_core/compile/aws.py +758 -0
  16. aegis_devops-0.3.1/src/aegis_core/compile/kubernetes.py +702 -0
  17. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/gitsource.py +3 -0
  18. aegis_devops-0.3.1/src/aegis_core/identity.py +400 -0
  19. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/parser.py +48 -0
  20. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/store.py +139 -4
  21. {aegis_devops-0.2.1 → aegis_devops-0.3.1/src/aegis_devops.egg-info}/PKG-INFO +9 -5
  22. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_devops.egg-info/SOURCES.txt +15 -0
  23. aegis_devops-0.3.1/tests/test_audit_aws.py +253 -0
  24. aegis_devops-0.3.1/tests/test_audit_kubernetes.py +265 -0
  25. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/tests/test_authority.py +2 -2
  26. aegis_devops-0.3.1/tests/test_compile_aws.py +537 -0
  27. aegis_devops-0.3.1/tests/test_compile_kubernetes.py +332 -0
  28. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/tests/test_gitsource.py +24 -0
  29. aegis_devops-0.3.1/tests/test_identity.py +347 -0
  30. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/tests/test_parser.py +54 -0
  31. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/tests/test_signing.py +2 -1
  32. aegis_devops-0.3.1/tests/test_snapshot.py +264 -0
  33. aegis_devops-0.2.1/src/aegis_core/_examples/__pycache__/__init__.cpython-312.pyc +0 -0
  34. aegis_devops-0.2.1/src/aegis_core/_examples/authority.example.yaml.sig +0 -2
  35. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/LICENSE +0 -0
  36. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/MANIFEST.in +0 -0
  37. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/setup.cfg +0 -0
  38. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/_examples/__init__.py +0 -0
  39. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/_examples/constraints.example.yaml +0 -0
  40. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/_examples/constraints.example.yaml.sig +0 -0
  41. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/_examples/environments.example.yaml +0 -0
  42. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/_examples/environments.example.yaml.sig +0 -0
  43. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/_examples/example-signing.key +0 -0
  44. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/_examples/plan_constraints.example.yaml +0 -0
  45. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/_examples/plan_constraints.example.yaml.sig +0 -0
  46. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/_examples/repos.example.yaml +0 -0
  47. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/_examples/repos.example.yaml.sig +0 -0
  48. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/_examples/signers.example.yaml +0 -0
  49. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/_examples/signers.example.yaml.sig +0 -0
  50. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/AEGIS-MANIFEST.sig +0 -0
  51. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/PRINCIPALS.yaml +0 -0
  52. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/git-4001.json +0 -0
  53. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/git-6001.json +0 -0
  54. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/git-6002.json +0 -0
  55. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/git-6003.json +0 -0
  56. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/git-6004.json +0 -0
  57. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/git-6005.json +0 -0
  58. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/git-6006.json +0 -0
  59. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/git-6007.json +0 -0
  60. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/git-6008.json +0 -0
  61. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/git-6009.json +0 -0
  62. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/git-789abc.json +0 -0
  63. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/git-abc123.json +0 -0
  64. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/git-bcd234.json +0 -0
  65. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/git-def456.json +0 -0
  66. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/jira-1001.json +0 -0
  67. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/jira-1002.json +0 -0
  68. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/jira-2001.json +0 -0
  69. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/jira-2002.json +0 -0
  70. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/jira-3001.json +0 -0
  71. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/jira-3002.json +0 -0
  72. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/jira-3003.json +0 -0
  73. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/jira-3004.json +0 -0
  74. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/jira-3005.json +0 -0
  75. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/jira-5001.json +0 -0
  76. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/jira-5002.json +0 -0
  77. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/jira-5003.json +0 -0
  78. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/jira-5004.json +0 -0
  79. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/jira-5006.json +0 -0
  80. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/plan-1001.json +0 -0
  81. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/plan-1002.json +0 -0
  82. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/plan-1003.json +0 -0
  83. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/plan-1004.json +0 -0
  84. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/plan-5005.json +0 -0
  85. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/slack-perma-1.json +0 -0
  86. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/adversarial.py +0 -0
  87. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/authority.py +0 -0
  88. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/baselines/__init__.py +0 -0
  89. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/baselines/base.py +0 -0
  90. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/baselines/external.py +0 -0
  91. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/baselines/llm.py +0 -0
  92. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/baselines/metrics.py +0 -0
  93. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/baselines/opa.py +0 -0
  94. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/config.py +0 -0
  95. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/environments.py +0 -0
  96. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/hook.py +0 -0
  97. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/integrations/opencode-plugin.js +0 -0
  98. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/intent.py +0 -0
  99. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/interceptor.py +0 -0
  100. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/ledger.py +0 -0
  101. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/parsers/__init__.py +0 -0
  102. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/parsers/pulumi.py +0 -0
  103. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/parsers/sql.py +0 -0
  104. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/plan.py +0 -0
  105. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/provenance.py +0 -0
  106. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/shell.py +0 -0
  107. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_core/signing.py +0 -0
  108. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_devops.egg-info/dependency_links.txt +0 -0
  109. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_devops.egg-info/entry_points.txt +0 -0
  110. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_devops.egg-info/requires.txt +0 -0
  111. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/src/aegis_devops.egg-info/top_level.txt +0 -0
  112. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/tests/test_adversarial.py +0 -0
  113. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/tests/test_baselines.py +0 -0
  114. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/tests/test_benchmark.py +0 -0
  115. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/tests/test_cli.py +0 -0
  116. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/tests/test_config.py +0 -0
  117. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/tests/test_corpus.py +0 -0
  118. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/tests/test_default_rules.py +0 -0
  119. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/tests/test_docs_consistency.py +0 -0
  120. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/tests/test_environments.py +0 -0
  121. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/tests/test_hook.py +0 -0
  122. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/tests/test_intent.py +0 -0
  123. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/tests/test_interceptor.py +0 -0
  124. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/tests/test_ledger.py +0 -0
  125. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/tests/test_opencode_plugin.py +0 -0
  126. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/tests/test_parsers_pulumi.py +0 -0
  127. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/tests/test_parsers_sql.py +0 -0
  128. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/tests/test_plan.py +0 -0
  129. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/tests/test_plugins.py +0 -0
  130. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/tests/test_provenance.py +0 -0
  131. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/tests/test_shell.py +0 -0
  132. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/tests/test_store.py +0 -0
  133. {aegis_devops-0.2.1 → aegis_devops-0.3.1}/tests/test_store_match.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aegis-devops
3
- Version: 0.2.1
3
+ Version: 0.3.1
4
4
  Summary: Provenance-backed, authority-aware policy verifier for agentic DevOps actions.
5
5
  License-Expression: Apache-2.0
6
6
  Project-URL: Homepage, https://github.com/moneytool/aegis-devops
@@ -33,7 +33,7 @@ Dynamic: license-file
33
33
  # Aegis-DevOps
34
34
 
35
35
  [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.22950337.svg)](https://doi.org/10.5281/zenodo.22950337)
36
- [![PyPI](https://img.shields.io/pypi/v/aegis-devops?label=pypi)](https://pypi.org/project/aegis-devops/)
36
+ [![PyPI](https://img.shields.io/pypi/v/aegis-devops?label=PyPI&cacheSeconds=3600)](https://pypi.org/project/aegis-devops/)
37
37
 
38
38
  **Stop AI agents from running `kubectl delete`, `terraform destroy` or `DROP TABLE` because a
39
39
  ticket told them to.**
@@ -65,7 +65,10 @@ Codex, GitHub Copilot (CLI and VS Code), Cursor and OpenCode:
65
65
  (see [Coding agents](docs/agents.md)). It only acts in projects with a `.aegis/` policy, and
66
66
  only blocks what that policy blocks. It also works as a CI step
67
67
  (`aegis check terraform plan.json --exit-style ci`) and as a Python library; see
68
- [Quick start](#quick-start).
68
+ [Quick start](#quick-start). In GitHub Actions, the
69
+ [Aegis-DevOps Plan Check](https://github.com/marketplace/actions/aegis-devops-plan-check) Action
70
+ checks a Terraform or OpenTofu plan and comments the verdict on the pull request:
71
+ `uses: moneytool/aegis-devops-action@v1` with `plan: plan.json`.
69
72
 
70
73
  ## What it does
71
74
 
@@ -252,7 +255,7 @@ context a rule can arrive from a ticket or a chat message as easily as from you.
252
255
 
253
256
  The engine (constraint store, interceptor, environment mapping, dry-run handling, rate limits,
254
257
  plan-level constraints, and parsers for every tool in "Supported tools") is complete, and
255
- v0.2.1 is on PyPI as an **alpha**. Real LLM baselines have been run: Claude Sonnet 5 through
258
+ v0.3.1 is on PyPI as an **alpha**. Real LLM baselines have been run: Claude Sonnet 5 through
256
259
  the API (cached in `results/llm-external.md`), Haiku through the Claude Code CLI, `gpt-6-astra`
257
260
  through the Codex CLI, and a local `mistral:latest`.
258
261
 
@@ -273,7 +276,8 @@ of anything you care about.
273
276
  | [`docs/agents.md`](docs/agents.md) | Claude Code, Codex, Copilot, VS Code and Cursor: install, what gets blocked |
274
277
  | [`docs/cli.md`](docs/cli.md) | Exit codes, store health, the Claude Code hook, argv forms, compound commands, dry runs, library usage |
275
278
  | [`docs/constraints.md`](docs/constraints.md) | Writing constraints, metadata vocabulary, authority policy, environment mapping |
276
- | [`docs/configuration.md`](docs/configuration.md) | Configuration/config-dir discovery, signing, source verification, rate limits & ledger |
279
+ | [`docs/configuration.md`](docs/configuration.md) | Configuration/config-dir discovery, signing, source verification, the identity model (`agents.yaml`), rate limits & ledger |
280
+ | [`docs/server-side.md`](docs/server-side.md) | Preview: compiling the policy to AWS Service Control Policies scoped to agent identities |
277
281
  | [`docs/benchmark.md`](docs/benchmark.md) | Benchmark methodology, full results table, real LLM baselines, agent-harness baselines, corpus, adversarial suite |
278
282
  | [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md) | Adding a new parser or tool |
279
283
  | [`docs/dev/PLAN.md`](docs/dev/PLAN.md) | Design plan and open gaps |
@@ -1,7 +1,7 @@
1
1
  # Aegis-DevOps
2
2
 
3
3
  [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.22950337.svg)](https://doi.org/10.5281/zenodo.22950337)
4
- [![PyPI](https://img.shields.io/pypi/v/aegis-devops?label=pypi)](https://pypi.org/project/aegis-devops/)
4
+ [![PyPI](https://img.shields.io/pypi/v/aegis-devops?label=PyPI&cacheSeconds=3600)](https://pypi.org/project/aegis-devops/)
5
5
 
6
6
  **Stop AI agents from running `kubectl delete`, `terraform destroy` or `DROP TABLE` because a
7
7
  ticket told them to.**
@@ -33,7 +33,10 @@ Codex, GitHub Copilot (CLI and VS Code), Cursor and OpenCode:
33
33
  (see [Coding agents](docs/agents.md)). It only acts in projects with a `.aegis/` policy, and
34
34
  only blocks what that policy blocks. It also works as a CI step
35
35
  (`aegis check terraform plan.json --exit-style ci`) and as a Python library; see
36
- [Quick start](#quick-start).
36
+ [Quick start](#quick-start). In GitHub Actions, the
37
+ [Aegis-DevOps Plan Check](https://github.com/marketplace/actions/aegis-devops-plan-check) Action
38
+ checks a Terraform or OpenTofu plan and comments the verdict on the pull request:
39
+ `uses: moneytool/aegis-devops-action@v1` with `plan: plan.json`.
37
40
 
38
41
  ## What it does
39
42
 
@@ -220,7 +223,7 @@ context a rule can arrive from a ticket or a chat message as easily as from you.
220
223
 
221
224
  The engine (constraint store, interceptor, environment mapping, dry-run handling, rate limits,
222
225
  plan-level constraints, and parsers for every tool in "Supported tools") is complete, and
223
- v0.2.1 is on PyPI as an **alpha**. Real LLM baselines have been run: Claude Sonnet 5 through
226
+ v0.3.1 is on PyPI as an **alpha**. Real LLM baselines have been run: Claude Sonnet 5 through
224
227
  the API (cached in `results/llm-external.md`), Haiku through the Claude Code CLI, `gpt-6-astra`
225
228
  through the Codex CLI, and a local `mistral:latest`.
226
229
 
@@ -241,7 +244,8 @@ of anything you care about.
241
244
  | [`docs/agents.md`](docs/agents.md) | Claude Code, Codex, Copilot, VS Code and Cursor: install, what gets blocked |
242
245
  | [`docs/cli.md`](docs/cli.md) | Exit codes, store health, the Claude Code hook, argv forms, compound commands, dry runs, library usage |
243
246
  | [`docs/constraints.md`](docs/constraints.md) | Writing constraints, metadata vocabulary, authority policy, environment mapping |
244
- | [`docs/configuration.md`](docs/configuration.md) | Configuration/config-dir discovery, signing, source verification, rate limits & ledger |
247
+ | [`docs/configuration.md`](docs/configuration.md) | Configuration/config-dir discovery, signing, source verification, the identity model (`agents.yaml`), rate limits & ledger |
248
+ | [`docs/server-side.md`](docs/server-side.md) | Preview: compiling the policy to AWS Service Control Policies scoped to agent identities |
245
249
  | [`docs/benchmark.md`](docs/benchmark.md) | Benchmark methodology, full results table, real LLM baselines, agent-harness baselines, corpus, adversarial suite |
246
250
  | [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md) | Adding a new parser or tool |
247
251
  | [`docs/dev/PLAN.md`](docs/dev/PLAN.md) | Design plan and open gaps |
@@ -62,7 +62,7 @@ version = {attr = "aegis_core.__version__"}
62
62
  where = ["src"]
63
63
 
64
64
  [tool.setuptools.package-data]
65
- aegis_core = ["_examples/**/*", "integrations/*"]
65
+ aegis_core = ["_examples/**/*", "integrations/*", "compile/actions/*"]
66
66
 
67
67
  [tool.ruff]
68
68
  line-length = 100
@@ -4,4 +4,4 @@ infrastructure actions of AI agents.
4
4
  See the README for what this does and, importantly, what it does not yet do.
5
5
  """
6
6
 
7
- __version__ = "0.2.1"
7
+ __version__ = "0.3.1"
@@ -0,0 +1,41 @@
1
+ # The identity model for server-side enforcement (docs/configuration.md#agentsyaml-identity-model).
2
+ # Server-side layers see which identity made an API call, not who typed a command,
3
+ # so compiled policies apply to agent identities and this file says which those are.
4
+ # Copy to agents.yaml, replace every identity with your own, sign it with `aegis sign`,
5
+ # then check it with: aegis agents --pretty
6
+ version: 1
7
+ principal: admin # must hold the 'identity' class in authority.yaml
8
+ mode: deny-by-default # every identity not listed below is treated as an agent
9
+ enforcement: report-only # switch to 'enforce' only after reviewing
10
+ # `aegis audit-identity --would-restrict`
11
+
12
+ # Never restricted, in either mode. Keep one per platform you compile for.
13
+ break_glass:
14
+ - platform: kubernetes
15
+ kind: group
16
+ id: aegis:break-glass
17
+ - platform: aws
18
+ kind: role
19
+ id: "arn:aws:iam::111122223333:role/BreakGlass"
20
+
21
+ # People, CI and controllers that are not agents. Anything unlisted is restricted
22
+ # by the compiled policy, which in practice means its destructive actions.
23
+ trusted:
24
+ - platform: kubernetes
25
+ kind: group
26
+ id: platform-admins
27
+ - platform: kubernetes
28
+ kind: serviceaccount
29
+ id: "argocd:argocd-application-controller"
30
+ note: applies reviewed changes only; agents must not push to the branch it syncs
31
+ - platform: aws
32
+ kind: role
33
+ id: "arn:aws:iam::111122223333:role/Deploy"
34
+ - platform: aws
35
+ kind: source-identity
36
+ id: alice@example.com
37
+ - platform: github
38
+ kind: oidc-subject
39
+ # bound to one reusable workflow at a protected ref, so an agent job in the same
40
+ # repository cannot present it (a plain repo:<org>/<repo>:ref:... subject can)
41
+ id: "repo:example-org/app:job_workflow_ref:example-org/app/.github/workflows/deploy.yml@refs/heads/main"
@@ -0,0 +1,2 @@
1
+ aegis-sig-v1
2
+ 45777052a3635646345db719b154c4182766c620e95f2f069cafc3db385215f9
@@ -5,6 +5,7 @@ principals:
5
5
  - scaling
6
6
  - deletion
7
7
  - configuration
8
+ - identity # may change agents.yaml, the identity model
8
9
  sre_lead:
9
10
  - scaling
10
11
  - configuration
@@ -0,0 +1,2 @@
1
+ aegis-sig-v1
2
+ b0d414e6dcaee5b9ca5d5ed5aa8405f35ed1ea2c7a623124faed7bfe670a104d
@@ -0,0 +1,324 @@
1
+ """``aegis audit-identity aws``: what the identity model means for the
2
+ identities that actually exist in an AWS account (design v0.3, §4.2, §4.3).
3
+
4
+ Read-only. It works on an IAM inventory -- the output of
5
+ ``aws iam get-account-authorization-details`` -- which :func:`collect_inventory`
6
+ fetches with the operator's own credentials, or which the operator saves and
7
+ passes in. The analysis itself is pure, so it is tested offline.
8
+
9
+ * ``would_restrict``: every IAM role and user a compiled policy would restrict.
10
+ In ``deny-by-default`` that is everything not listed as trusted or
11
+ break-glass, so legitimate automation (backup jobs, cleanup functions,
12
+ deploy roles) shows up here and must be added to ``trusted`` before
13
+ ``enforcement: enforce``. Service-linked roles are never restricted (SCPs
14
+ do not apply to them) and are listed separately.
15
+ * ``missing``: identities ``agents.yaml`` lists for this account that do not
16
+ exist. A missing break-glass role means there is no break-glass.
17
+ * ``findings``: trust-policy problems that let an agent become an exempt
18
+ identity without calling ``sts:AssumeRole`` from its own session --
19
+ GitHub OIDC trust that is not bound to a workflow or protected
20
+ environment, OIDC trust with no subject condition at all -- and exempt
21
+ roles that trust a restricted identity directly (closed by the compiled
22
+ self-protection block once enforced, open while report-only).
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import json
28
+ import re
29
+ import subprocess
30
+ from dataclasses import dataclass, field
31
+ from typing import Any
32
+
33
+ from aegis_core.identity import IdentityModel
34
+
35
+ GITHUB_OIDC = "token.actions.githubusercontent.com"
36
+ _SUB_KEY = f"{GITHUB_OIDC}:sub"
37
+ _WORKFLOW_KEYS = (f"{GITHUB_OIDC}:job_workflow_ref", f"{GITHUB_OIDC}:environment")
38
+ _BOUND_MARKERS = ("job_workflow_ref:", ":environment:")
39
+ _ARN_ACCOUNT = re.compile(r"arn:aws[a-z-]*:iam::(\d{12}):")
40
+
41
+
42
+ class InventoryError(ValueError):
43
+ """The inventory could not be read or collected."""
44
+
45
+
46
+ def collect_inventory(profile: str | None = None) -> dict[str, Any]:
47
+ """``aws iam get-account-authorization-details`` for roles and users,
48
+ read-only, with the operator's credentials (the CLI paginates)."""
49
+ argv = ["aws", "iam", "get-account-authorization-details", "--filter", "Role", "User",
50
+ "--output", "json"]
51
+ if profile:
52
+ argv += ["--profile", profile]
53
+ try:
54
+ proc = subprocess.run(argv, capture_output=True, text=True, check=False,
55
+ env=_cli_env())
56
+ except FileNotFoundError:
57
+ raise InventoryError("the aws CLI is not on PATH (or pass --inventory FILE)") from None
58
+ if proc.returncode != 0:
59
+ raise InventoryError(f"aws iam get-account-authorization-details failed: "
60
+ f"{proc.stderr.strip()[:500]}")
61
+ return json.loads(proc.stdout)
62
+
63
+
64
+ def _cli_env() -> dict[str, str]:
65
+ import os
66
+
67
+ return {**os.environ, "AWS_PAGER": ""}
68
+
69
+
70
+ # --- trust policies ----------------------------------------------------------------------
71
+
72
+
73
+ def _as_list(value: Any) -> list[Any]:
74
+ if value is None:
75
+ return []
76
+ return value if isinstance(value, list) else [value]
77
+
78
+
79
+ def _principals(statement: dict[str, Any]) -> dict[str, list[str]]:
80
+ principal = statement.get("Principal", {})
81
+ if principal == "*":
82
+ return {"AWS": ["*"]}
83
+ if not isinstance(principal, dict):
84
+ return {}
85
+ return {k: [str(v) for v in _as_list(vals)] for k, vals in principal.items()}
86
+
87
+
88
+ # Only these operators *require* the claim to match (review of #18): negated
89
+ # operators, ...IfExists (optional), Null and ForAllValues (vacuously true
90
+ # for a missing key) never prove a binding.
91
+ _BINDING_OPERATORS = frozenset({
92
+ "StringEquals", "StringLike", "ForAnyValue:StringEquals", "ForAnyValue:StringLike",
93
+ })
94
+ _GLOB = frozenset("*?")
95
+
96
+
97
+ def _conditions(statement: dict[str, Any]) -> list[tuple[str, str, list[str]]]:
98
+ out = []
99
+ for op, kv in (statement.get("Condition") or {}).items():
100
+ if isinstance(kv, dict):
101
+ for key, vals in kv.items():
102
+ out.append((str(op), str(key).lower(), [str(v) for v in _as_list(vals)]))
103
+ return out
104
+
105
+
106
+ def _binding_value(sub: str) -> str | None:
107
+ """The workflow or environment a ``sub`` value binds to, or None."""
108
+ for marker in _BOUND_MARKERS:
109
+ if marker in sub:
110
+ return sub.split(marker, 1)[1]
111
+ return None
112
+
113
+
114
+ def github_trust_findings(trust: dict[str, Any]) -> list[dict[str, str]]:
115
+ """Problems with ``sts:AssumeRoleWithWebIdentity`` trust for GitHub
116
+ Actions. A statement is bound only when a required (positive) condition
117
+ pins it to one specific reusable workflow at one ref, or one environment:
118
+
119
+ * ``oidc-no-subject``: no required condition on ``sub``, the workflow or
120
+ the environment -- any repository's workflow can assume the role;
121
+ * ``oidc-unsafe-condition``: a negated, optional (``IfExists``), ``Null``
122
+ or ``ForAllValues`` condition on those claims, which never binds;
123
+ * ``oidc-wildcard-subject``: a wildcard in the owner or repository;
124
+ * ``wildcard-binding``: a wildcard in the bound workflow, its ref or the
125
+ environment (``environment:*`` accepts an unprotected environment an
126
+ agent job can name; ``workflows/*`` accepts the agent's own workflow);
127
+ * ``workflow-unbound``: repository/ref-scoped, so every job in that
128
+ repository -- an agent's included -- presents it."""
129
+ findings = []
130
+ claims = (_SUB_KEY.lower(), *(k.lower() for k in _WORKFLOW_KEYS))
131
+ for st in _as_list(trust.get("Statement")):
132
+ if not isinstance(st, dict) or st.get("Effect") != "Allow":
133
+ continue
134
+ federated = _principals(st).get("Federated", [])
135
+ if not any(GITHUB_OIDC in f for f in federated):
136
+ continue
137
+ conds = _conditions(st)
138
+ # conditions are ANDed: a negated or optional one next to a precise
139
+ # required binding only narrows it, so it is reported only when
140
+ # nothing else binds this statement
141
+ unsafe = [{"kind": "oidc-unsafe-condition",
142
+ "detail": f"{op} on {key} {vals!r} does not require that value, so it "
143
+ "binds the role to nothing"}
144
+ for op, key, vals in conds if key in claims and op not in _BINDING_OPERATORS]
145
+ statement: list[dict[str, str]] = []
146
+
147
+ def required(key: str) -> list[str]:
148
+ return [v for op, k, vals in conds if k == key.lower()
149
+ and op in _BINDING_OPERATORS for v in vals]
150
+
151
+ subs = required(_SUB_KEY)
152
+ bindings = [v for k in _WORKFLOW_KEYS for v in required(k)]
153
+ if not subs and not bindings:
154
+ if not unsafe:
155
+ statement.append({"kind": "oidc-no-subject",
156
+ "detail": "GitHub OIDC trust with no required sub, workflow "
157
+ "or environment condition: any repository's "
158
+ "workflow can assume this role"})
159
+ findings += statement + unsafe
160
+ continue
161
+ for value in bindings:
162
+ if _GLOB & set(value):
163
+ statement.append({"kind": "wildcard-binding",
164
+ "detail": f"workflow/environment {value!r} matches more "
165
+ "than one workflow, ref or environment"})
166
+ bound_by_key = any(not (_GLOB & set(v)) for v in bindings)
167
+ for sub in subs:
168
+ repo = sub.split(":", 2)[1] if sub.startswith("repo:") and ":" in sub else ""
169
+ binding = _binding_value(sub)
170
+ if _GLOB & set(repo):
171
+ statement.append({"kind": "oidc-wildcard-subject",
172
+ "detail": f"sub {sub!r} matches more than one repository"})
173
+ elif binding is not None and _GLOB & set(binding):
174
+ statement.append({"kind": "wildcard-binding",
175
+ "detail": f"sub {sub!r} matches more than one workflow, ref "
176
+ "or environment"})
177
+ elif binding is None and not bound_by_key:
178
+ statement.append({"kind": "workflow-unbound",
179
+ "detail": f"sub {sub!r} is repository/ref-scoped: every job "
180
+ "in that repository presents it, an agent's "
181
+ "included"})
182
+ bound = not statement and (bound_by_key or all(
183
+ _binding_value(v) is not None for v in subs))
184
+ findings += statement + ([] if bound else unsafe)
185
+ return findings
186
+
187
+
188
+ def trusted_principals(trust: dict[str, Any]) -> list[str]:
189
+ """The AWS principals an ``Allow`` statement lets assume the role."""
190
+ out = []
191
+ for st in _as_list(trust.get("Statement")):
192
+ if isinstance(st, dict) and st.get("Effect") == "Allow":
193
+ out += _principals(st).get("AWS", [])
194
+ return out
195
+
196
+
197
+ # --- the audit ------------------------------------------------------------------------------
198
+
199
+
200
+ @dataclass
201
+ class AuditResult:
202
+ account: str
203
+ mode: str
204
+ enforcement: str
205
+ would_restrict: list[dict[str, Any]] = field(default_factory=list)
206
+ exempt: list[dict[str, Any]] = field(default_factory=list)
207
+ service_linked: list[str] = field(default_factory=list)
208
+ missing: list[dict[str, str]] = field(default_factory=list)
209
+ findings: list[dict[str, Any]] = field(default_factory=list)
210
+
211
+ @property
212
+ def problems(self) -> bool:
213
+ return bool(self.missing) or any(f["severity"] == "high" for f in self.findings)
214
+
215
+ def to_dict(self) -> dict[str, Any]:
216
+ return {
217
+ "account": self.account, "mode": self.mode, "enforcement": self.enforcement,
218
+ "would_restrict": self.would_restrict, "exempt": self.exempt,
219
+ "service_linked": self.service_linked, "missing": self.missing,
220
+ "findings": self.findings,
221
+ }
222
+
223
+
224
+ def _account_of(inventory: dict[str, Any]) -> str:
225
+ for entry in (*inventory.get("RoleDetailList", []), *inventory.get("UserDetailList", [])):
226
+ m = _ARN_ACCOUNT.match(entry.get("Arn", ""))
227
+ if m:
228
+ return m.group(1)
229
+ raise InventoryError("the inventory has no IAM role or user ARNs")
230
+
231
+
232
+ def _hint(kind: str, path: str, name: str) -> str | None:
233
+ if path.startswith("/aws-reserved/sso.amazonaws.com/"):
234
+ return "IAM Identity Center role: people sign in through it"
235
+ if name == "OrganizationAccountAccessRole":
236
+ return "the management account's way in; usually break-glass"
237
+ return None
238
+
239
+
240
+ def audit(inventory: dict[str, Any], model: IdentityModel) -> AuditResult:
241
+ account = _account_of(inventory)
242
+ result = AuditResult(account, model.mode, model.enforcement)
243
+ present: set[str] = set()
244
+ restricted_arns: set[str] = set()
245
+
246
+ for kind, entries in (("role", inventory.get("RoleDetailList", [])),
247
+ ("user", inventory.get("UserDetailList", []))):
248
+ for entry in entries:
249
+ arn, path = entry["Arn"], entry.get("Path", "/")
250
+ name = entry.get("RoleName") or entry.get("UserName") or arn
251
+ present.add(arn)
252
+ if kind == "role" and path.startswith("/aws-service-role/"):
253
+ result.service_linked.append(arn)
254
+ continue
255
+ last = (entry.get("RoleLastUsed") or {}).get("LastUsedDate") if kind == "role" \
256
+ else entry.get("PasswordLastUsed")
257
+ row = {"arn": arn, "kind": kind, "last_used": last}
258
+ hint = _hint(kind, path, name)
259
+ if hint:
260
+ row["hint"] = hint
261
+ if model.is_agent("aws", [(kind, arn)]):
262
+ result.would_restrict.append(row)
263
+ restricted_arns.add(arn)
264
+ else:
265
+ result.exempt.append(row)
266
+
267
+ exempt_listed = model.exempt("aws")
268
+ for ident in (*exempt_listed, *(model.agents_for("aws") if model.mode == "agents-only"
269
+ else ())):
270
+ if ident.kind not in ("role", "user"):
271
+ continue
272
+ m = _ARN_ACCOUNT.match(ident.id)
273
+ if m and m.group(1) == account and ident.id not in present:
274
+ section = ("break_glass" if ident in model.break_glass else
275
+ "trusted" if ident in model.trusted else "agents")
276
+ result.missing.append({"arn": ident.id, "listed_as": section,
277
+ "detail": "listed in agents.yaml but not in this account"
278
+ + ("; there is no working break-glass for it"
279
+ if section == "break_glass" else "")})
280
+
281
+ exempt_arns = {r["arn"] for r in result.exempt}
282
+ for entry in inventory.get("RoleDetailList", []):
283
+ arn = entry["Arn"]
284
+ if entry.get("Path", "/").startswith("/aws-service-role/"):
285
+ continue
286
+ trust = entry.get("AssumeRolePolicyDocument") or {}
287
+ if isinstance(trust, str):
288
+ from urllib.parse import unquote
289
+
290
+ trust = json.loads(unquote(trust))
291
+ exempt = arn in exempt_arns
292
+ for f in github_trust_findings(trust):
293
+ result.findings.append({
294
+ "role": arn, **f,
295
+ "severity": "high" if exempt else "info",
296
+ "why": ("an exempt role: a GitHub job that is an agent can assume it with "
297
+ "AssumeRoleWithWebIdentity, which the SCP cannot see"
298
+ if exempt else "a restricted role: the policy still applies to it"),
299
+ })
300
+ if exempt:
301
+ for principal in trusted_principals(trust):
302
+ if principal in (f"arn:aws:iam::{account}:root", account):
303
+ result.findings.append({
304
+ "role": arn, "kind": "exempt-trusts-account",
305
+ "detail": "trusts the whole account: any identity in it allowed "
306
+ "sts:AssumeRole can assume it, agents included",
307
+ "severity": "high" if model.enforcement != "enforce" else "info",
308
+ "why": ("blocked for agents by the compiled self-protection block "
309
+ "once enforced; open while report-only. Prefer trusting "
310
+ "named principals"),
311
+ })
312
+ elif principal in restricted_arns:
313
+ result.findings.append({
314
+ "role": arn, "kind": "exempt-trusts-restricted",
315
+ "detail": f"trusts {principal} directly",
316
+ "severity": "high" if model.enforcement != "enforce" else "info",
317
+ "why": ("blocked by the compiled self-protection block (no "
318
+ "sts:AssumeRole into exempt roles) once enforced; open "
319
+ "while report-only"),
320
+ })
321
+ result.would_restrict.sort(key=lambda r: r["arn"])
322
+ result.exempt.sort(key=lambda r: r["arn"])
323
+ result.service_linked.sort()
324
+ return result