aegis-devops 0.2.0__tar.gz → 0.3.0__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 (130) hide show
  1. {aegis_devops-0.2.0/src/aegis_devops.egg-info → aegis_devops-0.3.0}/PKG-INFO +6 -5
  2. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/README.md +5 -4
  3. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/pyproject.toml +1 -1
  4. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/__init__.py +1 -1
  5. aegis_devops-0.3.0/src/aegis_core/_examples/__pycache__/__init__.cpython-312.pyc +0 -0
  6. aegis_devops-0.3.0/src/aegis_core/_examples/agents.example.yaml +41 -0
  7. aegis_devops-0.3.0/src/aegis_core/_examples/agents.example.yaml.sig +2 -0
  8. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/_examples/authority.example.yaml +1 -0
  9. aegis_devops-0.3.0/src/aegis_core/_examples/authority.example.yaml.sig +2 -0
  10. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/_examples/constraints.example.yaml +15 -0
  11. aegis_devops-0.3.0/src/aegis_core/_examples/constraints.example.yaml.sig +2 -0
  12. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/_examples/sources/AEGIS-MANIFEST.sig +3 -2
  13. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/_examples/sources/PRINCIPALS.yaml +1 -0
  14. aegis_devops-0.3.0/src/aegis_core/_examples/sources/git-6009.json +15 -0
  15. aegis_devops-0.3.0/src/aegis_core/audit_aws.py +324 -0
  16. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/cli.py +326 -0
  17. aegis_devops-0.3.0/src/aegis_core/compile/__init__.py +81 -0
  18. aegis_devops-0.3.0/src/aegis_core/compile/actions/aws.yaml +339 -0
  19. aegis_devops-0.3.0/src/aegis_core/compile/aws.py +758 -0
  20. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/gitsource.py +3 -0
  21. aegis_devops-0.3.0/src/aegis_core/identity.py +400 -0
  22. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/interceptor.py +26 -10
  23. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/parser.py +133 -20
  24. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/store.py +139 -4
  25. {aegis_devops-0.2.0 → aegis_devops-0.3.0/src/aegis_devops.egg-info}/PKG-INFO +6 -5
  26. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_devops.egg-info/SOURCES.txt +12 -0
  27. aegis_devops-0.3.0/tests/test_audit_aws.py +253 -0
  28. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/tests/test_authority.py +2 -2
  29. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/tests/test_cli.py +9 -9
  30. aegis_devops-0.3.0/tests/test_compile_aws.py +498 -0
  31. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/tests/test_default_rules.py +33 -0
  32. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/tests/test_gitsource.py +24 -0
  33. aegis_devops-0.3.0/tests/test_identity.py +347 -0
  34. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/tests/test_interceptor.py +59 -0
  35. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/tests/test_parser.py +98 -2
  36. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/tests/test_parsers_sql.py +1 -1
  37. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/tests/test_signing.py +2 -1
  38. aegis_devops-0.3.0/tests/test_snapshot.py +264 -0
  39. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/tests/test_store.py +2 -2
  40. aegis_devops-0.2.0/src/aegis_core/_examples/__pycache__/__init__.cpython-312.pyc +0 -0
  41. aegis_devops-0.2.0/src/aegis_core/_examples/authority.example.yaml.sig +0 -2
  42. aegis_devops-0.2.0/src/aegis_core/_examples/constraints.example.yaml.sig +0 -2
  43. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/LICENSE +0 -0
  44. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/MANIFEST.in +0 -0
  45. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/setup.cfg +0 -0
  46. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/_examples/__init__.py +0 -0
  47. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/_examples/environments.example.yaml +0 -0
  48. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/_examples/environments.example.yaml.sig +0 -0
  49. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/_examples/example-signing.key +0 -0
  50. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/_examples/plan_constraints.example.yaml +0 -0
  51. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/_examples/plan_constraints.example.yaml.sig +0 -0
  52. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/_examples/repos.example.yaml +0 -0
  53. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/_examples/repos.example.yaml.sig +0 -0
  54. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/_examples/signers.example.yaml +0 -0
  55. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/_examples/signers.example.yaml.sig +0 -0
  56. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/_examples/sources/git-4001.json +0 -0
  57. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/_examples/sources/git-6001.json +0 -0
  58. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/_examples/sources/git-6002.json +0 -0
  59. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/_examples/sources/git-6003.json +0 -0
  60. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/_examples/sources/git-6004.json +0 -0
  61. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/_examples/sources/git-6005.json +0 -0
  62. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/_examples/sources/git-6006.json +0 -0
  63. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/_examples/sources/git-6007.json +0 -0
  64. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/_examples/sources/git-6008.json +0 -0
  65. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/_examples/sources/git-789abc.json +0 -0
  66. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/_examples/sources/git-abc123.json +0 -0
  67. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/_examples/sources/git-bcd234.json +0 -0
  68. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/_examples/sources/git-def456.json +0 -0
  69. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/_examples/sources/jira-1001.json +0 -0
  70. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/_examples/sources/jira-1002.json +0 -0
  71. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/_examples/sources/jira-2001.json +0 -0
  72. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/_examples/sources/jira-2002.json +0 -0
  73. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/_examples/sources/jira-3001.json +0 -0
  74. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/_examples/sources/jira-3002.json +0 -0
  75. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/_examples/sources/jira-3003.json +0 -0
  76. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/_examples/sources/jira-3004.json +0 -0
  77. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/_examples/sources/jira-3005.json +0 -0
  78. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/_examples/sources/jira-5001.json +0 -0
  79. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/_examples/sources/jira-5002.json +0 -0
  80. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/_examples/sources/jira-5003.json +0 -0
  81. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/_examples/sources/jira-5004.json +0 -0
  82. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/_examples/sources/jira-5006.json +0 -0
  83. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/_examples/sources/plan-1001.json +0 -0
  84. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/_examples/sources/plan-1002.json +0 -0
  85. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/_examples/sources/plan-1003.json +0 -0
  86. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/_examples/sources/plan-1004.json +0 -0
  87. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/_examples/sources/plan-5005.json +0 -0
  88. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/_examples/sources/slack-perma-1.json +0 -0
  89. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/adversarial.py +0 -0
  90. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/authority.py +0 -0
  91. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/baselines/__init__.py +0 -0
  92. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/baselines/base.py +0 -0
  93. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/baselines/external.py +0 -0
  94. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/baselines/llm.py +0 -0
  95. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/baselines/metrics.py +0 -0
  96. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/baselines/opa.py +0 -0
  97. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/config.py +0 -0
  98. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/environments.py +0 -0
  99. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/hook.py +0 -0
  100. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/integrations/opencode-plugin.js +0 -0
  101. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/intent.py +0 -0
  102. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/ledger.py +0 -0
  103. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/parsers/__init__.py +0 -0
  104. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/parsers/pulumi.py +0 -0
  105. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/parsers/sql.py +0 -0
  106. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/plan.py +0 -0
  107. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/provenance.py +0 -0
  108. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/shell.py +0 -0
  109. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_core/signing.py +0 -0
  110. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_devops.egg-info/dependency_links.txt +0 -0
  111. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_devops.egg-info/entry_points.txt +0 -0
  112. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_devops.egg-info/requires.txt +0 -0
  113. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/src/aegis_devops.egg-info/top_level.txt +0 -0
  114. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/tests/test_adversarial.py +0 -0
  115. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/tests/test_baselines.py +0 -0
  116. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/tests/test_benchmark.py +0 -0
  117. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/tests/test_config.py +0 -0
  118. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/tests/test_corpus.py +0 -0
  119. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/tests/test_docs_consistency.py +0 -0
  120. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/tests/test_environments.py +0 -0
  121. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/tests/test_hook.py +0 -0
  122. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/tests/test_intent.py +0 -0
  123. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/tests/test_ledger.py +0 -0
  124. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/tests/test_opencode_plugin.py +0 -0
  125. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/tests/test_parsers_pulumi.py +0 -0
  126. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/tests/test_plan.py +0 -0
  127. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/tests/test_plugins.py +0 -0
  128. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/tests/test_provenance.py +0 -0
  129. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/tests/test_shell.py +0 -0
  130. {aegis_devops-0.2.0 → aegis_devops-0.3.0}/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.0
3
+ Version: 0.3.0
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)](https://pypi.org/project/aegis-devops/)
36
+ [![PyPI](https://img.shields.io/pypi/v/aegis-devops?label=pypi)](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.**
@@ -104,7 +104,7 @@ BLOCK: kubernetes scale deployment/api-server
104
104
  citations: no-scale-prod-peak
105
105
  covered: True latency_ms: 0.20
106
106
  PLAN BLOCK: 1 intent(s)
107
- STORE: loaded=29 quarantined=0 principals=3
107
+ STORE: loaded=30 quarantined=0 principals=3
108
108
  warning: using example signing key
109
109
  ```
110
110
 
@@ -252,7 +252,7 @@ context a rule can arrive from a ticket or a chat message as easily as from you.
252
252
 
253
253
  The engine (constraint store, interceptor, environment mapping, dry-run handling, rate limits,
254
254
  plan-level constraints, and parsers for every tool in "Supported tools") is complete, and
255
- v0.2.0 is on PyPI as an **alpha**. Real LLM baselines have been run: Claude Sonnet 5 through
255
+ v0.3.0 is on PyPI as an **alpha**. Real LLM baselines have been run: Claude Sonnet 5 through
256
256
  the API (cached in `results/llm-external.md`), Haiku through the Claude Code CLI, `gpt-6-astra`
257
257
  through the Codex CLI, and a local `mistral:latest`.
258
258
 
@@ -273,7 +273,8 @@ of anything you care about.
273
273
  | [`docs/agents.md`](docs/agents.md) | Claude Code, Codex, Copilot, VS Code and Cursor: install, what gets blocked |
274
274
  | [`docs/cli.md`](docs/cli.md) | Exit codes, store health, the Claude Code hook, argv forms, compound commands, dry runs, library usage |
275
275
  | [`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 |
276
+ | [`docs/configuration.md`](docs/configuration.md) | Configuration/config-dir discovery, signing, source verification, the identity model (`agents.yaml`), rate limits & ledger |
277
+ | [`docs/server-side.md`](docs/server-side.md) | Preview: compiling the policy to AWS Service Control Policies scoped to agent identities |
277
278
  | [`docs/benchmark.md`](docs/benchmark.md) | Benchmark methodology, full results table, real LLM baselines, agent-harness baselines, corpus, adversarial suite |
278
279
  | [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md) | Adding a new parser or tool |
279
280
  | [`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)](https://pypi.org/project/aegis-devops/)
4
+ [![PyPI](https://img.shields.io/pypi/v/aegis-devops?label=pypi)](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.**
@@ -72,7 +72,7 @@ BLOCK: kubernetes scale deployment/api-server
72
72
  citations: no-scale-prod-peak
73
73
  covered: True latency_ms: 0.20
74
74
  PLAN BLOCK: 1 intent(s)
75
- STORE: loaded=29 quarantined=0 principals=3
75
+ STORE: loaded=30 quarantined=0 principals=3
76
76
  warning: using example signing key
77
77
  ```
78
78
 
@@ -220,7 +220,7 @@ context a rule can arrive from a ticket or a chat message as easily as from you.
220
220
 
221
221
  The engine (constraint store, interceptor, environment mapping, dry-run handling, rate limits,
222
222
  plan-level constraints, and parsers for every tool in "Supported tools") is complete, and
223
- v0.2.0 is on PyPI as an **alpha**. Real LLM baselines have been run: Claude Sonnet 5 through
223
+ v0.3.0 is on PyPI as an **alpha**. Real LLM baselines have been run: Claude Sonnet 5 through
224
224
  the API (cached in `results/llm-external.md`), Haiku through the Claude Code CLI, `gpt-6-astra`
225
225
  through the Codex CLI, and a local `mistral:latest`.
226
226
 
@@ -241,7 +241,8 @@ of anything you care about.
241
241
  | [`docs/agents.md`](docs/agents.md) | Claude Code, Codex, Copilot, VS Code and Cursor: install, what gets blocked |
242
242
  | [`docs/cli.md`](docs/cli.md) | Exit codes, store health, the Claude Code hook, argv forms, compound commands, dry runs, library usage |
243
243
  | [`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 |
244
+ | [`docs/configuration.md`](docs/configuration.md) | Configuration/config-dir discovery, signing, source verification, the identity model (`agents.yaml`), rate limits & ledger |
245
+ | [`docs/server-side.md`](docs/server-side.md) | Preview: compiling the policy to AWS Service Control Policies scoped to agent identities |
245
246
  | [`docs/benchmark.md`](docs/benchmark.md) | Benchmark methodology, full results table, real LLM baselines, agent-harness baselines, corpus, adversarial suite |
246
247
  | [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md) | Adding a new parser or tool |
247
248
  | [`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.0"
7
+ __version__ = "0.3.0"
@@ -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
@@ -451,3 +451,18 @@ constraints:
451
451
  rule_text: Escalate deleting Argo CD applications (with cascade this deletes the
452
452
  deployed resources) for human review.
453
453
  provenance_hash: dc24eb69ac5c8b66279284b6df19095e3f0096303f3ef18da712a53e5e339aeb
454
+ - id: block-kubectl-impersonation
455
+ provider: kubernetes
456
+ resource_pattern: '*'
457
+ actions:
458
+ - impersonate
459
+ scope: {}
460
+ time_window: null
461
+ effect: BLOCK
462
+ constraint_class: configuration
463
+ principal: admin
464
+ source_ref: git-6009
465
+ source_timestamp: '2026-09-28T12:00:00+00:00'
466
+ rule_text: 'Block kubectl impersonation (--as, --as-group, --as-uid): the request
467
+ would run as another identity, outside the controls on the agent''s own.'
468
+ provenance_hash: 3c2382c07e085d7a10be92490d9b8905085e6996c5c4ef1aaf2ba43980a41fd1
@@ -0,0 +1,2 @@
1
+ aegis-sig-v1
2
+ 1b135ef7b7ff50e078b96d0944ce3f771fb541c208a55d41f93f5f0bb65c01ab
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "files": {
3
- "PRINCIPALS.yaml": "e2e7025a2092240acede6a88c7f1a4f22d3cdbf5821772fba9e0f91fbfcbcc8f",
3
+ "PRINCIPALS.yaml": "94a36ee80347de5ff022f103f9b1288a82d008351ce958d9a5764f1edda16ed5",
4
4
  "git-4001.json": "8be2dd6c37ef643b3bc375b15d5d97889b00456285f594e21a3b4d52521e21de",
5
5
  "git-6001.json": "6592ca4568f767d71a431490ff3cacdbc699ad92be966f57aed3b0a485844dc5",
6
6
  "git-6002.json": "e6286c5fb8a0d2b3c34ca0c841ccede2a477a02f4781115caf4650f4f7c019b8",
@@ -10,6 +10,7 @@
10
10
  "git-6006.json": "3f6fbfeb34afb3bcb8075f0864047b3641f0016b8817f67288db7f64afd36b58",
11
11
  "git-6007.json": "eb84394208274b79792e285c5143dde320a2f4755d9b2922ffb55df07e7c5b9e",
12
12
  "git-6008.json": "34d29df94b55a3fa29521246fa17e0966eda93f718a6c41082e6c648569aafa5",
13
+ "git-6009.json": "cadad3135121749b5dc1564e44dd2f97684782f2ebdb1cefbc6e08a319d88337",
13
14
  "git-789abc.json": "48ac500e13d36240ec5c72bf90e96b433a5873d636e9e36f99a73468e7fb5757",
14
15
  "git-abc123.json": "ee3751fdcd64cf1dd374bf9f7b73cda723a4aced5053f32501ba9416de89afc7",
15
16
  "git-bcd234.json": "a0de7b32bde75735c91504aa7156e7b2ba2ef961d0f439218507e40d0bdfd89a",
@@ -36,5 +37,5 @@
36
37
  "slack-perma-1.json": "2c8945a68e413c300ef58fc6a6d3daa8e4d0029a26a454dfad983e2091a188d1"
37
38
  },
38
39
  "header": "aegis-manifest-v1",
39
- "sig": "034812f78b7f502ebcbb9183bfcf88023324ffcd83e82e5d698aee252cd3b824"
40
+ "sig": "5c28ed037b47840ab065c77c790d7a5807ac318a05ffa43393a3e6395d5f71e1"
40
41
  }
@@ -37,3 +37,4 @@ principals:
37
37
  git-6006: admin
38
38
  git-6007: admin
39
39
  git-6008: admin
40
+ git-6009: admin
@@ -0,0 +1,15 @@
1
+ {
2
+ "provider": "kubernetes",
3
+ "resource_pattern": "*",
4
+ "actions": [
5
+ "impersonate"
6
+ ],
7
+ "scope": {},
8
+ "time_window": null,
9
+ "effect": "BLOCK",
10
+ "constraint_class": "configuration",
11
+ "principal": "admin",
12
+ "source_ref": "git-6009",
13
+ "source_timestamp": "2026-09-28T12:00:00+00:00",
14
+ "rule_text": "Block kubectl impersonation (--as, --as-group, --as-uid): the request would run as another identity, outside the controls on the agent's own."
15
+ }
@@ -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