aegis-devops 0.3.0__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 (132) hide show
  1. {aegis_devops-0.3.0/src/aegis_devops.egg-info → aegis_devops-0.3.1}/PKG-INFO +7 -4
  2. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/README.md +6 -3
  3. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/__init__.py +1 -1
  4. aegis_devops-0.3.1/src/aegis_core/_examples/__pycache__/__init__.cpython-312.pyc +0 -0
  5. aegis_devops-0.3.1/src/aegis_core/audit_kubernetes.py +312 -0
  6. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/cli.py +142 -0
  7. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/compile/actions/aws.yaml +0 -14
  8. aegis_devops-0.3.1/src/aegis_core/compile/kubernetes.py +702 -0
  9. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/parser.py +48 -0
  10. {aegis_devops-0.3.0 → aegis_devops-0.3.1/src/aegis_devops.egg-info}/PKG-INFO +7 -4
  11. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_devops.egg-info/SOURCES.txt +4 -0
  12. aegis_devops-0.3.1/tests/test_audit_kubernetes.py +265 -0
  13. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/tests/test_compile_aws.py +43 -4
  14. aegis_devops-0.3.1/tests/test_compile_kubernetes.py +332 -0
  15. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/tests/test_parser.py +54 -0
  16. aegis_devops-0.3.0/src/aegis_core/_examples/__pycache__/__init__.cpython-312.pyc +0 -0
  17. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/LICENSE +0 -0
  18. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/MANIFEST.in +0 -0
  19. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/pyproject.toml +0 -0
  20. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/setup.cfg +0 -0
  21. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/_examples/__init__.py +0 -0
  22. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/_examples/agents.example.yaml +0 -0
  23. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/_examples/agents.example.yaml.sig +0 -0
  24. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/_examples/authority.example.yaml +0 -0
  25. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/_examples/authority.example.yaml.sig +0 -0
  26. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/_examples/constraints.example.yaml +0 -0
  27. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/_examples/constraints.example.yaml.sig +0 -0
  28. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/_examples/environments.example.yaml +0 -0
  29. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/_examples/environments.example.yaml.sig +0 -0
  30. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/_examples/example-signing.key +0 -0
  31. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/_examples/plan_constraints.example.yaml +0 -0
  32. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/_examples/plan_constraints.example.yaml.sig +0 -0
  33. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/_examples/repos.example.yaml +0 -0
  34. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/_examples/repos.example.yaml.sig +0 -0
  35. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/_examples/signers.example.yaml +0 -0
  36. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/_examples/signers.example.yaml.sig +0 -0
  37. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/AEGIS-MANIFEST.sig +0 -0
  38. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/PRINCIPALS.yaml +0 -0
  39. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/git-4001.json +0 -0
  40. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/git-6001.json +0 -0
  41. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/git-6002.json +0 -0
  42. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/git-6003.json +0 -0
  43. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/git-6004.json +0 -0
  44. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/git-6005.json +0 -0
  45. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/git-6006.json +0 -0
  46. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/git-6007.json +0 -0
  47. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/git-6008.json +0 -0
  48. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/git-6009.json +0 -0
  49. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/git-789abc.json +0 -0
  50. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/git-abc123.json +0 -0
  51. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/git-bcd234.json +0 -0
  52. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/git-def456.json +0 -0
  53. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/jira-1001.json +0 -0
  54. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/jira-1002.json +0 -0
  55. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/jira-2001.json +0 -0
  56. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/jira-2002.json +0 -0
  57. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/jira-3001.json +0 -0
  58. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/jira-3002.json +0 -0
  59. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/jira-3003.json +0 -0
  60. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/jira-3004.json +0 -0
  61. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/jira-3005.json +0 -0
  62. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/jira-5001.json +0 -0
  63. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/jira-5002.json +0 -0
  64. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/jira-5003.json +0 -0
  65. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/jira-5004.json +0 -0
  66. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/jira-5006.json +0 -0
  67. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/plan-1001.json +0 -0
  68. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/plan-1002.json +0 -0
  69. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/plan-1003.json +0 -0
  70. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/plan-1004.json +0 -0
  71. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/plan-5005.json +0 -0
  72. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/_examples/sources/slack-perma-1.json +0 -0
  73. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/adversarial.py +0 -0
  74. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/audit_aws.py +0 -0
  75. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/authority.py +0 -0
  76. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/baselines/__init__.py +0 -0
  77. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/baselines/base.py +0 -0
  78. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/baselines/external.py +0 -0
  79. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/baselines/llm.py +0 -0
  80. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/baselines/metrics.py +0 -0
  81. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/baselines/opa.py +0 -0
  82. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/compile/__init__.py +0 -0
  83. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/compile/aws.py +0 -0
  84. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/config.py +0 -0
  85. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/environments.py +0 -0
  86. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/gitsource.py +0 -0
  87. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/hook.py +0 -0
  88. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/identity.py +0 -0
  89. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/integrations/opencode-plugin.js +0 -0
  90. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/intent.py +0 -0
  91. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/interceptor.py +0 -0
  92. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/ledger.py +0 -0
  93. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/parsers/__init__.py +0 -0
  94. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/parsers/pulumi.py +0 -0
  95. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/parsers/sql.py +0 -0
  96. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/plan.py +0 -0
  97. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/provenance.py +0 -0
  98. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/shell.py +0 -0
  99. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/signing.py +0 -0
  100. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_core/store.py +0 -0
  101. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_devops.egg-info/dependency_links.txt +0 -0
  102. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_devops.egg-info/entry_points.txt +0 -0
  103. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_devops.egg-info/requires.txt +0 -0
  104. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/src/aegis_devops.egg-info/top_level.txt +0 -0
  105. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/tests/test_adversarial.py +0 -0
  106. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/tests/test_audit_aws.py +0 -0
  107. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/tests/test_authority.py +0 -0
  108. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/tests/test_baselines.py +0 -0
  109. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/tests/test_benchmark.py +0 -0
  110. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/tests/test_cli.py +0 -0
  111. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/tests/test_config.py +0 -0
  112. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/tests/test_corpus.py +0 -0
  113. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/tests/test_default_rules.py +0 -0
  114. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/tests/test_docs_consistency.py +0 -0
  115. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/tests/test_environments.py +0 -0
  116. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/tests/test_gitsource.py +0 -0
  117. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/tests/test_hook.py +0 -0
  118. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/tests/test_identity.py +0 -0
  119. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/tests/test_intent.py +0 -0
  120. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/tests/test_interceptor.py +0 -0
  121. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/tests/test_ledger.py +0 -0
  122. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/tests/test_opencode_plugin.py +0 -0
  123. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/tests/test_parsers_pulumi.py +0 -0
  124. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/tests/test_parsers_sql.py +0 -0
  125. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/tests/test_plan.py +0 -0
  126. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/tests/test_plugins.py +0 -0
  127. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/tests/test_provenance.py +0 -0
  128. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/tests/test_shell.py +0 -0
  129. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/tests/test_signing.py +0 -0
  130. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/tests/test_snapshot.py +0 -0
  131. {aegis_devops-0.3.0 → aegis_devops-0.3.1}/tests/test_store.py +0 -0
  132. {aegis_devops-0.3.0 → 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.3.0
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.3.0 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
 
@@ -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.3.0 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
 
@@ -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.3.0"
7
+ __version__ = "0.3.1"
@@ -0,0 +1,312 @@
1
+ """``aegis audit-identity kubernetes``: what the identity model means for the
2
+ identities that exist in a cluster (design v0.3, §4.2, §4.3, §6.6).
3
+
4
+ Read-only. The inventory -- ServiceAccounts, RBAC bindings, which
5
+ ServiceAccounts pods run as, and SubjectAccessReviews for the escape
6
+ permissions -- comes from :func:`collect_inventory` (kubectl with the
7
+ operator's credentials) or a saved copy; :func:`audit` is pure.
8
+
9
+ * ``would_restrict``: every identity the compiled policies would restrict --
10
+ ServiceAccounts, and users/groups named in RBAC bindings (the API has no
11
+ user objects) -- with the control plane exempt as in the compiler.
12
+ * ``missing``: listed ServiceAccounts that do not exist, and a break-glass
13
+ group or user no binding grants anything (break-glass without access).
14
+ * ``findings``: escape permissions an identity the policies restrict still
15
+ holds. Admission cannot see impersonation or protect admission objects, so
16
+ RBAC is the only control there.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import json
22
+ import subprocess
23
+ from concurrent.futures import ThreadPoolExecutor
24
+ from dataclasses import dataclass, field
25
+ from typing import Any
26
+
27
+ from aegis_core.identity import IdentityModel
28
+
29
+ # (id, why). Each check is asked as several SubjectAccessReviews (see
30
+ # _probes): by name where RBAC can grant by name (resourceNames), per
31
+ # namespace where a RoleBinding can grant it, and for every write verb that
32
+ # disables a control (review of #22).
33
+ ESCAPE_CHECKS: tuple[tuple[str, str], ...] = (
34
+ ("impersonate-users", "can act as another user (kubectl --as), a trusted or break-glass "
35
+ "one included; admission then sees that user"),
36
+ ("impersonate-groups", "can claim another group, break-glass included"),
37
+ ("impersonate-serviceaccounts", "can act as another ServiceAccount"),
38
+ ("impersonate-uids", "can claim another identity's UID"),
39
+ ("write-admission-policies", "can update, patch or delete ValidatingAdmissionPolicies or "
40
+ "their bindings (make a rule always pass, or Deny into Warn), "
41
+ "which admission cannot protect"),
42
+ ("write-webhooks", "can update, patch or delete admission webhook configurations"),
43
+ ("create-mutating-admission", "can create a mutating webhook or mutating admission policy "
44
+ "that rewrites requests before validation"),
45
+ ("escalate-roles", "can grant itself permissions it does not hold (escalate)"),
46
+ ("bind-roles", "can bind any cluster role, cluster-admin included"),
47
+ ("write-clusterrolebindings", "can create or change cluster role bindings"),
48
+ )
49
+ _ADM = "admissionregistration.k8s.io"
50
+ _RBAC = "rbac.authorization.k8s.io"
51
+ _SA_PREFIX = "system:serviceaccount:"
52
+ # every identity (or every ServiceAccount) carries these; agents.yaml refuses
53
+ # to trust them, so they are not identities to review
54
+ _UNIVERSAL_GROUPS = ("system:authenticated", "system:unauthenticated", "system:serviceaccounts")
55
+ _HINTS = {
56
+ ("group", "system:masters"): "the super-admin group; usually break-glass",
57
+ ("user", "kube-apiserver-kubelet-client"): "the API server's client to kubelets; trust it",
58
+ }
59
+
60
+
61
+ class InventoryError(ValueError):
62
+ """The cluster inventory could not be collected or read."""
63
+
64
+
65
+ def _is_control_plane(username: str, groups: list[str]) -> bool:
66
+ """The compiler's always-exempt set in deny-by-default."""
67
+ if username.startswith("system:") and not username.startswith(_SA_PREFIX):
68
+ return True
69
+ return "system:nodes" in groups or "system:serviceaccounts:kube-system" in groups
70
+
71
+
72
+ def _sa_groups(namespace: str) -> list[str]:
73
+ return ["system:serviceaccounts", f"system:serviceaccounts:{namespace}",
74
+ "system:authenticated"]
75
+
76
+
77
+ # --- collection ------------------------------------------------------------------------------
78
+
79
+
80
+ def _run(context: str | None, *args: str) -> subprocess.CompletedProcess:
81
+ argv = ["kubectl", *(["--context", context] if context else []), *args]
82
+ try:
83
+ return subprocess.run(argv, capture_output=True, text=True, check=False)
84
+ except FileNotFoundError:
85
+ raise InventoryError("kubectl is not on PATH (or pass --inventory FILE)") from None
86
+
87
+
88
+ def _kubectl(context: str | None, *args: str) -> str:
89
+ proc = _run(context, *args)
90
+ if proc.returncode != 0:
91
+ raise InventoryError(f"kubectl {' '.join(args)} failed: {proc.stderr.strip()[:300]}")
92
+ return proc.stdout
93
+
94
+
95
+ def can_i(context: str | None, args: list[str]) -> bool:
96
+ """One SubjectAccessReview. ``kubectl auth can-i`` exits non-zero for
97
+ "no" too, so only an explicit yes/no answer counts: a failed review (no
98
+ right to impersonate for it, no SubjectAccessReview access, an API error)
99
+ is an error, never a quiet "no" (review of #22)."""
100
+ proc = _run(context, "auth", "can-i", *args)
101
+ lines = proc.stdout.strip().splitlines()
102
+ answer = lines[-1].strip() if lines else ""
103
+ if answer == "yes":
104
+ return True
105
+ if answer == "no":
106
+ return False
107
+ raise InventoryError(f"SubjectAccessReview failed (kubectl auth can-i {' '.join(args)}): "
108
+ f"{(proc.stderr or proc.stdout).strip()[:300] or 'no answer'}")
109
+
110
+
111
+ def _probes(model: IdentityModel, namespaces: list[str]) -> list[tuple[str, list[str], str]]:
112
+ """``(check, can-i arguments, target description)`` for every escape
113
+ check."""
114
+ exempt = model.exempt("kubernetes")
115
+ users = sorted({i.id for i in exempt if i.kind == "user"})
116
+ groups = sorted({i.id for i in exempt if i.kind == "group"} | {"system:masters"})
117
+ sas = sorted({i.id for i in exempt if i.kind == "serviceaccount"})
118
+ out: list[tuple[str, list[str], str]] = [
119
+ ("impersonate-users", ["impersonate", "users"], "any user"),
120
+ ("impersonate-groups", ["impersonate", "groups"], "any group"),
121
+ ("impersonate-uids", ["impersonate", "uids"], "any uid"),
122
+ ]
123
+ out += [("impersonate-users", ["impersonate", f"users/{u}"], f"user {u}") for u in users]
124
+ out += [("impersonate-groups", ["impersonate", f"groups/{g}"], f"group {g}") for g in groups]
125
+ for ns in sorted(set(namespaces)):
126
+ out.append(("impersonate-serviceaccounts", ["impersonate", "serviceaccounts", "-n", ns],
127
+ f"any ServiceAccount in {ns}"))
128
+ for sa in sas:
129
+ ns, name = sa.split(":", 1)
130
+ out.append(("impersonate-serviceaccounts",
131
+ ["impersonate", f"serviceaccounts/{name}", "-n", ns],
132
+ f"ServiceAccount {sa}"))
133
+ for res in ("validatingadmissionpolicies", "validatingadmissionpolicybindings"):
134
+ for verb in ("update", "patch", "delete"):
135
+ out.append(("write-admission-policies", [verb, f"{res}.{_ADM}"], f"{verb} {res}"))
136
+ for res in ("validatingwebhookconfigurations", "mutatingwebhookconfigurations"):
137
+ for verb in ("update", "patch", "delete"):
138
+ out.append(("write-webhooks", [verb, f"{res}.{_ADM}"], f"{verb} {res}"))
139
+ for res in ("mutatingwebhookconfigurations", "mutatingadmissionpolicies",
140
+ "mutatingadmissionpolicybindings"):
141
+ out.append(("create-mutating-admission", ["create", f"{res}.{_ADM}"], f"create {res}"))
142
+ out.append(("escalate-roles", ["escalate", f"clusterroles.{_RBAC}"], "escalate clusterroles"))
143
+ out.append(("bind-roles", ["bind", f"clusterroles.{_RBAC}"], "bind clusterroles"))
144
+ for verb in ("create", "update", "patch"):
145
+ out.append(("write-clusterrolebindings", [verb, f"clusterrolebindings.{_RBAC}"],
146
+ f"{verb} clusterrolebindings"))
147
+ return out
148
+
149
+
150
+ def _identities(sas: list[dict], bindings: list[dict]) -> list[dict[str, Any]]:
151
+ out: dict[tuple[str, str], dict[str, Any]] = {}
152
+ for sa in sas:
153
+ ns, name = sa["namespace"], sa["name"]
154
+ user = f"{_SA_PREFIX}{ns}:{name}"
155
+ out[("serviceaccount", user)] = {"kind": "serviceaccount", "id": f"{ns}:{name}",
156
+ "username": user, "groups": _sa_groups(ns)}
157
+ for b in bindings:
158
+ for s in b.get("subjects") or []:
159
+ if s.get("kind") == "User":
160
+ out.setdefault(("user", s["name"]), {
161
+ "kind": "user", "id": s["name"], "username": s["name"],
162
+ "groups": ["system:authenticated"]})
163
+ elif s.get("kind") == "Group" and not (
164
+ s["name"] in _UNIVERSAL_GROUPS
165
+ or s["name"].startswith("system:serviceaccounts:")):
166
+ out.setdefault(("group", s["name"]), {
167
+ "kind": "group", "id": s["name"], "username": "aegis-audit:probe",
168
+ "groups": [s["name"], "system:authenticated"]})
169
+ return sorted(out.values(), key=lambda i: (i["kind"], i["id"]))
170
+
171
+
172
+ def collect_inventory(context: str | None, model: IdentityModel) -> dict[str, Any]:
173
+ """ServiceAccounts, bindings, pod ServiceAccounts and, for every identity
174
+ the policies would restrict, the escape SubjectAccessReviews."""
175
+ sa_doc = json.loads(_kubectl(context, "get", "serviceaccounts", "-A", "-o", "json"))
176
+ sas = [{"namespace": i["metadata"]["namespace"], "name": i["metadata"]["name"]}
177
+ for i in sa_doc.get("items", [])]
178
+ bindings = []
179
+ for kind in ("clusterrolebindings", "rolebindings"):
180
+ doc = json.loads(_kubectl(context, "get", kind, "-A", "-o", "json"))
181
+ for i in doc.get("items", []):
182
+ bindings.append({"kind": kind, "namespace": i["metadata"].get("namespace", ""),
183
+ "name": i["metadata"]["name"], "role": i["roleRef"]["name"],
184
+ "subjects": i.get("subjects") or []})
185
+ pods = json.loads(_kubectl(context, "get", "pods", "-A", "-o", "json"))
186
+ pod_sas: dict[str, int] = {}
187
+ for p in pods.get("items", []):
188
+ key = f"{p['metadata']['namespace']}:{p['spec'].get('serviceAccountName', 'default')}"
189
+ pod_sas[key] = pod_sas.get(key, 0) + 1
190
+
191
+ inventory = {"serviceaccounts": sas, "bindings": bindings, "pod_serviceaccounts": pod_sas,
192
+ "access": {}}
193
+ restricted = [i for i in _identities(sas, bindings) if _restricted(model, i)]
194
+ probes = _probes(model, [s["namespace"] for s in sas])
195
+
196
+ def ask(job):
197
+ ident, (check, args, target) = job
198
+ who = ["--as", ident["username"]]
199
+ for g in ident["groups"]:
200
+ who += ["--as-group", g]
201
+ return f"{ident['kind']}:{ident['id']}", check, target, can_i(context, [*args, *who])
202
+
203
+ jobs = [(i, p) for i in restricted for p in probes]
204
+ for ident in restricted:
205
+ inventory["access"][f"{ident['kind']}:{ident['id']}"] = {
206
+ c[0]: [] for c in ESCAPE_CHECKS}
207
+ with ThreadPoolExecutor(max_workers=8) as pool:
208
+ for key, check, target, allowed in pool.map(ask, jobs):
209
+ if allowed:
210
+ inventory["access"][key][check].append(target)
211
+ return inventory
212
+
213
+
214
+ # --- analysis ---------------------------------------------------------------------------------
215
+
216
+
217
+ def _restricted(model: IdentityModel, ident: dict[str, Any]) -> bool:
218
+ if model.mode == "deny-by-default" and _is_control_plane(ident["username"],
219
+ ident["groups"]):
220
+ return False
221
+ presented = [("user", ident["username"]), *(("group", g) for g in ident["groups"])]
222
+ if ident["kind"] == "group":
223
+ presented = [("group", ident["id"])]
224
+ return model.is_agent("kubernetes", presented)
225
+
226
+
227
+ @dataclass
228
+ class KubernetesAuditResult:
229
+ context: str
230
+ mode: str
231
+ enforcement: str
232
+ would_restrict: list[dict[str, Any]] = field(default_factory=list)
233
+ exempt: list[dict[str, Any]] = field(default_factory=list)
234
+ control_plane: int = 0
235
+ missing: list[dict[str, str]] = field(default_factory=list)
236
+ findings: list[dict[str, Any]] = field(default_factory=list)
237
+
238
+ @property
239
+ def problems(self) -> bool:
240
+ return bool(self.missing) or any(f["severity"] == "high" for f in self.findings)
241
+
242
+ def to_dict(self) -> dict[str, Any]:
243
+ return {"context": self.context, "mode": self.mode, "enforcement": self.enforcement,
244
+ "would_restrict": self.would_restrict, "exempt": self.exempt,
245
+ "control_plane": self.control_plane, "missing": self.missing,
246
+ "findings": self.findings}
247
+
248
+
249
+ def audit(inventory: dict[str, Any], model: IdentityModel, context: str = "") -> \
250
+ KubernetesAuditResult:
251
+ result = KubernetesAuditResult(context, model.mode, model.enforcement)
252
+ sas, bindings = inventory.get("serviceaccounts", []), inventory.get("bindings", [])
253
+ pod_sas = inventory.get("pod_serviceaccounts", {})
254
+ access = inventory.get("access", {})
255
+ bound: dict[tuple[str, str], list[str]] = {}
256
+ for b in bindings:
257
+ for s in b.get("subjects") or []:
258
+ if s.get("kind") == "ServiceAccount":
259
+ key = ("serviceaccount", f"{s.get('namespace', b.get('namespace', ''))}:"
260
+ f"{s['name']}")
261
+ else:
262
+ key = (str(s.get("kind", "")).lower(), s["name"])
263
+ bound.setdefault(key, []).append(f"{b['kind'][:-1]}/{b['name']} -> {b['role']}")
264
+
265
+ for ident in _identities(sas, bindings):
266
+ if model.mode == "deny-by-default" and _is_control_plane(ident["username"],
267
+ ident["groups"]):
268
+ result.control_plane += 1
269
+ continue
270
+ row = {"kind": ident["kind"], "id": ident["id"],
271
+ "bindings": sorted(bound.get((ident["kind"], ident["id"]), []))}
272
+ if ident["kind"] == "serviceaccount":
273
+ row["pods"] = pod_sas.get(ident["id"], 0)
274
+ hint = _HINTS.get((ident["kind"], ident["id"]))
275
+ if ident["kind"] == "group" and ident["id"].startswith("system:bootstrappers:"):
276
+ hint = "node bootstrap tokens (kubeadm join); trust it"
277
+ if hint:
278
+ row["hint"] = hint
279
+ if _restricted(model, ident):
280
+ result.would_restrict.append(row)
281
+ granted = access.get(f"{ident['kind']}:{ident['id']}", {})
282
+ for check, why in ESCAPE_CHECKS:
283
+ targets = granted.get(check)
284
+ if targets:
285
+ detail = why + (f" ({', '.join(targets)})" if isinstance(targets, list)
286
+ else "")
287
+ result.findings.append({
288
+ "identity": f"{ident['kind']}/{ident['id']}", "kind": check,
289
+ "severity": "high", "detail": detail,
290
+ "why": "an identity the compiled policies restrict; admission cannot "
291
+ "see or stop this, so RBAC must not grant it"})
292
+ else:
293
+ result.exempt.append(row)
294
+
295
+ existing = {f"{s['namespace']}:{s['name']}" for s in sas}
296
+ for ident in (*model.exempt("kubernetes"),
297
+ *(model.agents_for("kubernetes") if model.mode == "agents-only" else ())):
298
+ section = ("break_glass" if ident in model.break_glass else
299
+ "trusted" if ident in model.trusted else "agents")
300
+ if ident.kind == "serviceaccount" and ident.id not in existing:
301
+ result.missing.append({"identity": f"serviceaccount/{ident.id}",
302
+ "listed_as": section,
303
+ "detail": "listed in agents.yaml but not in this cluster"})
304
+ elif section == "break_glass" and ident.kind in ("user", "group") and \
305
+ (ident.kind, ident.id) not in bound:
306
+ result.missing.append({"identity": f"{ident.kind}/{ident.id}",
307
+ "listed_as": section,
308
+ "detail": "no role binding grants it anything, so "
309
+ "break-glass has no access"})
310
+ result.would_restrict.sort(key=lambda r: (r["kind"], r["id"]))
311
+ result.exempt.sort(key=lambda r: (r["kind"], r["id"]))
312
+ return result
@@ -529,6 +529,23 @@ def _build_parser() -> argparse.ArgumentParser:
529
529
  aws_parser.add_argument("--max-policies", type=int, default=4,
530
530
  help="SCPs available on the target (default 4: five may attach, "
531
531
  "one is usually FullAWSAccess)")
532
+ kube_parser = compile_sub.add_parser(
533
+ "kubernetes",
534
+ help="Kubernetes ValidatingAdmissionPolicies scoped to agent identities (agents.yaml), "
535
+ "for one cluster",
536
+ )
537
+ _add_common_options(kube_parser)
538
+ _add_agents_option(kube_parser)
539
+ kube_parser.add_argument("--cluster", required=True,
540
+ help="the cluster (or kube context) name; its environment comes "
541
+ "from environments.yaml (kubernetes.clusters / contexts)")
542
+ kube_out = kube_parser.add_mutually_exclusive_group(required=True)
543
+ kube_out.add_argument("--out", metavar="DIR", help="write the output here")
544
+ kube_out.add_argument("--check", metavar="DIR",
545
+ help="exit 1 if DIR differs from a fresh compile (drift check)")
546
+ kube_parser.add_argument("--escalate", default="deny", choices=("deny", "omit"),
547
+ help="ESCALATE rules: 'deny' (default; admission cannot ask) or "
548
+ "'omit' (left client-only, reported as not enforced)")
532
549
 
533
550
  audit_parser = subparsers.add_parser(
534
551
  "audit-identity",
@@ -547,6 +564,19 @@ def _build_parser() -> argparse.ArgumentParser:
547
564
  "output instead of calling AWS")
548
565
  audit_aws.add_argument("--would-restrict", action="store_true",
549
566
  help="print only the identities the compiled policy would restrict")
567
+ audit_kube = audit_sub.add_parser(
568
+ "kubernetes", help="a cluster's ServiceAccounts, RBAC subjects and escape permissions")
569
+ _add_common_options(audit_kube)
570
+ _add_agents_option(audit_kube)
571
+ audit_kube.add_argument("--context", dest="kube_context", default=None,
572
+ help="kube context to read (read-only: get and SubjectAccessReviews)")
573
+ audit_kube.add_argument("--inventory", default=None, metavar="FILE",
574
+ help="use a saved inventory (JSON) instead of calling the cluster")
575
+ audit_kube.add_argument("--save-inventory", default=None, metavar="FILE",
576
+ help="also write the collected inventory here")
577
+ audit_kube.add_argument("--would-restrict", action="store_true",
578
+ help="print only the identities the compiled policies would "
579
+ "restrict")
550
580
 
551
581
  agents_parser = subparsers.add_parser(
552
582
  "agents",
@@ -1343,6 +1373,53 @@ def _run_audit_aws(args: argparse.Namespace) -> int:
1343
1373
  return 1 if result.problems else 0
1344
1374
 
1345
1375
 
1376
+ def _run_audit_kubernetes(args: argparse.Namespace) -> int:
1377
+ """``aegis audit-identity kubernetes``: read-only; exit 1 on a problem."""
1378
+ from aegis_core.audit_kubernetes import InventoryError, audit, collect_inventory
1379
+
1380
+ model, warnings = _load_identity(args)
1381
+ try:
1382
+ if args.inventory:
1383
+ inventory = json.loads(Path(args.inventory).read_text())
1384
+ else:
1385
+ inventory = collect_inventory(args.kube_context, model)
1386
+ if args.save_inventory:
1387
+ Path(args.save_inventory).write_text(json.dumps(inventory, indent=2) + "\n")
1388
+ result = audit(inventory, model, args.kube_context or "")
1389
+ except InventoryError as exc:
1390
+ raise DataError(f"audit-identity: {exc}") from None
1391
+ doc = result.to_dict()
1392
+ if args.would_restrict:
1393
+ doc = {"context": result.context, "would_restrict": result.would_restrict}
1394
+ if not args.pretty:
1395
+ print(json.dumps({**doc, "warnings": warnings}, sort_keys=True))
1396
+ return 1 if result.problems else 0
1397
+ print(f"AUDIT kubernetes {result.context or '(current context)'} "
1398
+ f"({result.mode}, {result.enforcement})")
1399
+ print(f" would restrict ({len(result.would_restrict)}):")
1400
+ for r in result.would_restrict:
1401
+ pods = f", {r['pods']} pod(s)" if r.get("pods") else ""
1402
+ roles = f"{len(r['bindings'])} binding(s)" if r["bindings"] else "no bindings"
1403
+ hint = f" <- {r['hint']}" if r.get("hint") else ""
1404
+ print(f" {r['kind']:<14} {r['id']} ({roles}{pods}){hint}")
1405
+ if not args.would_restrict:
1406
+ print(f" exempt ({len(result.exempt)}):")
1407
+ for r in result.exempt:
1408
+ print(f" {r['kind']:<14} {r['id']}")
1409
+ print(f" control plane, always exempt: {result.control_plane}")
1410
+ for m in result.missing:
1411
+ print(f" MISSING {m['listed_as']}: {m['identity']} ({m['detail']})")
1412
+ by_identity: dict[str, list[str]] = {}
1413
+ for f in result.findings:
1414
+ by_identity.setdefault(f["identity"], []).append(f["kind"])
1415
+ for identity, kinds in by_identity.items():
1416
+ print(f" PROBLEM {identity} can: {', '.join(kinds)} (admission cannot see or stop "
1417
+ "these; RBAC must not grant them to an identity the policies restrict)")
1418
+ for w in warnings:
1419
+ print(f" warning: {w}")
1420
+ return 1 if result.problems else 0
1421
+
1422
+
1346
1423
  def _run_agents(args: argparse.Namespace) -> int:
1347
1424
  """``aegis agents``: verify ``agents.yaml`` (signature, shape, the
1348
1425
  principal's ``identity`` class) and print the identity model."""
@@ -1399,6 +1476,67 @@ def _build_snapshot(args: argparse.Namespace):
1399
1476
  authority_map, load
1400
1477
 
1401
1478
 
1479
+ def _compile_inputs(args: argparse.Namespace):
1480
+ """``(store, snapshot, model, env_map, identity path)`` for a compile."""
1481
+ store, snap, authority_map, load = _build_snapshot(args)
1482
+ path = _agents_path(args)
1483
+ if not os.path.exists(path):
1484
+ raise DataError(f"compile: no {path}; server-side policy needs the identity model "
1485
+ "(copy agents.example.yaml, see docs/configuration.md)")
1486
+ model = _load_or_data_error(
1487
+ lambda: load_identity_model(path, authority_map=authority_map, **load), path, "agents")
1488
+ env_map = None
1489
+ if args.environments:
1490
+ env_map = _load_or_data_error(lambda: load_environment_map(args.environments, **load),
1491
+ args.environments, "environments")
1492
+ return store, snap, model, env_map, path
1493
+
1494
+
1495
+ def _write_or_check(args: argparse.Namespace, result, snap) -> int | None:
1496
+ from aegis_core.compile import check_outputs, write_outputs
1497
+
1498
+ if args.check:
1499
+ drift = check_outputs(args.check, result.files)
1500
+ for line in drift:
1501
+ print(f"drift: {line}")
1502
+ if not drift:
1503
+ print(f"aegis: {args.check} matches the current policy "
1504
+ f"(snapshot {snap.digest[:16]})")
1505
+ return 1 if drift else 0
1506
+ write_outputs(args.out, result.files)
1507
+ return None
1508
+
1509
+
1510
+ def _run_compile_kubernetes(args: argparse.Namespace) -> int:
1511
+ """``aegis compile kubernetes``: ValidatingAdmissionPolicies for one cluster."""
1512
+ from aegis_core.compile import CompileError
1513
+ from aegis_core.compile.kubernetes import KubernetesTarget, compile_kubernetes
1514
+
1515
+ store, snap, model, env_map, path = _compile_inputs(args)
1516
+ env = None
1517
+ if env_map is not None:
1518
+ env = (env_map.kubernetes_clusters.get(args.cluster)
1519
+ or env_map.kubernetes_contexts.get(args.cluster))
1520
+ try:
1521
+ result = compile_kubernetes(snap, model,
1522
+ KubernetesTarget(cluster=args.cluster, env=env,
1523
+ escalate=args.escalate),
1524
+ identity_sha256=_sha256_file(path))
1525
+ except (CompileError, ValueError) as exc:
1526
+ raise DataError(f"compile: {exc}") from None
1527
+ checked = _write_or_check(args, result, snap)
1528
+ if checked is not None:
1529
+ return checked
1530
+ summary = ", ".join(f"{k}={v}" for k, v in result.coverage["summary"].items())
1531
+ print(f"aegis: wrote {len(result.manifest['policies'])} ValidatingAdmissionPolicies for "
1532
+ f"cluster {args.cluster} to {args.out} (snapshot {snap.digest[:16]}; {summary})")
1533
+ if model.enforcement != "enforce":
1534
+ print("aegis: agents.yaml is report-only: bindings warn and audit instead of denying")
1535
+ for w in [*store.warnings, *(f"agents: {w}" for w in model.warnings)]:
1536
+ print(f" warning: {w}")
1537
+ return 0
1538
+
1539
+
1402
1540
  def _run_compile_aws(args: argparse.Namespace) -> int:
1403
1541
  """``aegis compile aws``: SCPs, coverage and manifest for one account."""
1404
1542
  from aegis_core.compile import CompileError, check_outputs, write_outputs
@@ -1486,8 +1624,12 @@ def _run(argv: list[str]) -> int:
1486
1624
  if args.command == "agents":
1487
1625
  return _run_agents(args)
1488
1626
  if args.command == "compile":
1627
+ if args.target == "kubernetes":
1628
+ return _run_compile_kubernetes(args)
1489
1629
  return _run_compile_aws(args)
1490
1630
  if args.command == "audit-identity":
1631
+ if args.target == "kubernetes":
1632
+ return _run_audit_kubernetes(args)
1491
1633
  return _run_audit_aws(args)
1492
1634
  if args.command == "hook":
1493
1635
  return hook_module.main_hook(
@@ -50,26 +50,22 @@ types:
50
50
  verified: true
51
51
  ec2/volume:
52
52
  arn: "arn:{partition}:ec2:*:{account}:volume/{name}"
53
- names: cli-missing
54
53
  actions:
55
54
  delete: [ec2:DeleteVolume]
56
55
  detach: [ec2:DetachVolume]
57
56
  verified: true
58
57
  ec2/snapshot:
59
58
  arn: "arn:{partition}:ec2:*::snapshot/{name}"
60
- names: cli-missing
61
59
  actions:
62
60
  delete: [ec2:DeleteSnapshot]
63
61
  verified: true
64
62
  ec2/vpc:
65
63
  arn: "arn:{partition}:ec2:*:{account}:vpc/{name}"
66
- names: cli-missing
67
64
  actions:
68
65
  delete: [ec2:DeleteVpc]
69
66
  verified: true
70
67
  ec2/security-group:
71
68
  arn: "arn:{partition}:ec2:*:{account}:security-group/{name}"
72
- names: cli-missing
73
69
  actions:
74
70
  delete: [ec2:DeleteSecurityGroup]
75
71
  verified: true
@@ -114,20 +110,17 @@ types:
114
110
  verified: true
115
111
  rds/db-cluster:
116
112
  arn: "arn:{partition}:rds:*:{account}:cluster:{name}"
117
- names: cli-missing
118
113
  actions:
119
114
  delete: [rds:DeleteDBCluster]
120
115
  update: [rds:ModifyDBCluster]
121
116
  verified: true
122
117
  rds/db-snapshot:
123
118
  arn: "arn:{partition}:rds:*:{account}:snapshot:{name}"
124
- names: cli-missing
125
119
  actions:
126
120
  delete: [rds:DeleteDBSnapshot]
127
121
  verified: true
128
122
  rds/db-cluster-snapshot:
129
123
  arn: "arn:{partition}:rds:*:{account}:cluster-snapshot:{name}"
130
- names: cli-missing
131
124
  actions:
132
125
  delete: [rds:DeleteDBClusterSnapshot]
133
126
  verified: true
@@ -142,14 +135,12 @@ types:
142
135
  verified: true
143
136
  eks/cluster:
144
137
  arn: "arn:{partition}:eks:*:{account}:cluster/{name}"
145
- names: cli-missing
146
138
  actions:
147
139
  delete: [eks:DeleteCluster]
148
140
  update: [eks:UpdateClusterConfig, eks:UpdateClusterVersion]
149
141
  verified: true
150
142
  eks/nodegroup:
151
143
  arn: "arn:{partition}:eks:*:{account}:nodegroup/*/{name}/*"
152
- names: cli-missing
153
144
  actions:
154
145
  delete: [eks:DeleteNodegroup]
155
146
  update: [eks:UpdateNodegroupConfig, eks:UpdateNodegroupVersion]
@@ -265,7 +256,6 @@ types:
265
256
  logs/log-group:
266
257
  arn: ["arn:{partition}:logs:*:{account}:log-group:{name}",
267
258
  "arn:{partition}:logs:*:{account}:log-group:{name}:*"]
268
- names: cli-missing
269
259
  actions:
270
260
  delete: [logs:DeleteLogGroup]
271
261
  verified: true
@@ -288,7 +278,6 @@ types:
288
278
  verified: true
289
279
  ecs/service:
290
280
  arn: "arn:{partition}:ecs:*:{account}:service/*/{name}"
291
- names: cli-missing
292
281
  actions:
293
282
  delete: [ecs:DeleteService]
294
283
  update: [ecs:UpdateService]
@@ -302,13 +291,11 @@ types:
302
291
  route53/hosted-zone:
303
292
  arn: "arn:{partition}:route53:::hostedzone/{name}"
304
293
  regional: false
305
- names: cli-missing
306
294
  actions:
307
295
  delete: [route53:DeleteHostedZone]
308
296
  verified: true
309
297
  efs/file-system:
310
298
  arn: "arn:{partition}:elasticfilesystem:*:{account}:file-system/{name}"
311
- names: cli-missing
312
299
  actions:
313
300
  delete: [elasticfilesystem:DeleteFileSystem]
314
301
  verified: true
@@ -326,7 +313,6 @@ types:
326
313
  verified: true
327
314
  elasticache/cache-cluster:
328
315
  arn: "arn:{partition}:elasticache:*:{account}:cluster:{name}"
329
- names: cli-missing
330
316
  actions:
331
317
  delete: [elasticache:DeleteCacheCluster]
332
318
  verified: true