aegis-devops 0.1.6__tar.gz → 0.2.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.
- {aegis_devops-0.1.6/src/aegis_devops.egg-info → aegis_devops-0.2.0}/PKG-INFO +8 -7
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/README.md +7 -6
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/__init__.py +1 -1
- aegis_devops-0.2.0/src/aegis_core/_examples/__pycache__/__init__.cpython-312.pyc +0 -0
- aegis_devops-0.2.0/src/aegis_core/_examples/repos.example.yaml +11 -0
- aegis_devops-0.2.0/src/aegis_core/_examples/repos.example.yaml.sig +2 -0
- aegis_devops-0.2.0/src/aegis_core/_examples/signers.example.yaml +13 -0
- aegis_devops-0.2.0/src/aegis_core/_examples/signers.example.yaml.sig +2 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/cli.py +132 -4
- aegis_devops-0.2.0/src/aegis_core/gitsource.py +631 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/store.py +13 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0/src/aegis_devops.egg-info}/PKG-INFO +8 -7
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_devops.egg-info/SOURCES.txt +6 -0
- aegis_devops-0.2.0/tests/test_gitsource.py +729 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/tests/test_signing.py +3 -1
- aegis_devops-0.1.6/src/aegis_core/_examples/__pycache__/__init__.cpython-312.pyc +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/LICENSE +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/MANIFEST.in +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/pyproject.toml +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/setup.cfg +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/_examples/__init__.py +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/_examples/authority.example.yaml +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/_examples/authority.example.yaml.sig +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/_examples/constraints.example.yaml +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/_examples/constraints.example.yaml.sig +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/_examples/environments.example.yaml +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/_examples/environments.example.yaml.sig +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/_examples/example-signing.key +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/_examples/plan_constraints.example.yaml +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/_examples/plan_constraints.example.yaml.sig +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/_examples/sources/AEGIS-MANIFEST.sig +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/_examples/sources/PRINCIPALS.yaml +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/_examples/sources/git-4001.json +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/_examples/sources/git-6001.json +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/_examples/sources/git-6002.json +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/_examples/sources/git-6003.json +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/_examples/sources/git-6004.json +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/_examples/sources/git-6005.json +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/_examples/sources/git-6006.json +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/_examples/sources/git-6007.json +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/_examples/sources/git-6008.json +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/_examples/sources/git-789abc.json +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/_examples/sources/git-abc123.json +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/_examples/sources/git-bcd234.json +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/_examples/sources/git-def456.json +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/_examples/sources/jira-1001.json +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/_examples/sources/jira-1002.json +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/_examples/sources/jira-2001.json +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/_examples/sources/jira-2002.json +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/_examples/sources/jira-3001.json +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/_examples/sources/jira-3002.json +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/_examples/sources/jira-3003.json +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/_examples/sources/jira-3004.json +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/_examples/sources/jira-3005.json +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/_examples/sources/jira-5001.json +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/_examples/sources/jira-5002.json +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/_examples/sources/jira-5003.json +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/_examples/sources/jira-5004.json +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/_examples/sources/jira-5006.json +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/_examples/sources/plan-1001.json +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/_examples/sources/plan-1002.json +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/_examples/sources/plan-1003.json +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/_examples/sources/plan-1004.json +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/_examples/sources/plan-5005.json +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/_examples/sources/slack-perma-1.json +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/adversarial.py +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/authority.py +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/baselines/__init__.py +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/baselines/base.py +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/baselines/external.py +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/baselines/llm.py +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/baselines/metrics.py +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/baselines/opa.py +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/config.py +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/environments.py +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/hook.py +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/integrations/opencode-plugin.js +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/intent.py +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/interceptor.py +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/ledger.py +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/parser.py +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/parsers/__init__.py +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/parsers/pulumi.py +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/parsers/sql.py +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/plan.py +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/provenance.py +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/shell.py +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_core/signing.py +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_devops.egg-info/dependency_links.txt +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_devops.egg-info/entry_points.txt +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_devops.egg-info/requires.txt +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/src/aegis_devops.egg-info/top_level.txt +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/tests/test_adversarial.py +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/tests/test_authority.py +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/tests/test_baselines.py +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/tests/test_benchmark.py +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/tests/test_cli.py +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/tests/test_config.py +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/tests/test_corpus.py +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/tests/test_default_rules.py +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/tests/test_docs_consistency.py +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/tests/test_environments.py +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/tests/test_hook.py +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/tests/test_intent.py +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/tests/test_interceptor.py +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/tests/test_ledger.py +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/tests/test_opencode_plugin.py +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/tests/test_parser.py +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/tests/test_parsers_pulumi.py +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/tests/test_parsers_sql.py +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/tests/test_plan.py +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/tests/test_plugins.py +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/tests/test_provenance.py +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/tests/test_shell.py +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.0}/tests/test_store.py +0 -0
- {aegis_devops-0.1.6 → aegis_devops-0.2.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.
|
|
3
|
+
Version: 0.2.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
|
|
@@ -252,16 +252,17 @@ 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.
|
|
255
|
+
v0.2.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
|
|
|
259
259
|
What is *not* done is the part the threat model leans on hardest. See `docs/dev/PLAN.md §8`, but in
|
|
260
|
-
short:
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
260
|
+
short: a rule may now cite a signed commit in a policy repository, whose verified signer
|
|
261
|
+
becomes its principal ([Git sources](docs/configuration.md#git-sources-signed-commits), SSH
|
|
262
|
+
or GPG signatures), but file sources, the key-to-principal list and the other policy
|
|
263
|
+
files still rest on a shared signing secret rather than per-principal public keys, and there
|
|
264
|
+
are no Slack/Jira connectors. Until those land, Aegis demonstrates that the *decision
|
|
265
|
+
procedure* is sound; it proves the identities feeding it only for Git-sourced rules. Resource matching is also case-sensitive on names. It is not
|
|
265
266
|
production-ready: read `docs/dev/PLAN.md §8` for the full list of open gaps before putting it in front
|
|
266
267
|
of anything you care about.
|
|
267
268
|
|
|
@@ -220,16 +220,17 @@ 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.
|
|
223
|
+
v0.2.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
|
|
|
227
227
|
What is *not* done is the part the threat model leans on hardest. See `docs/dev/PLAN.md §8`, but in
|
|
228
|
-
short:
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
228
|
+
short: a rule may now cite a signed commit in a policy repository, whose verified signer
|
|
229
|
+
becomes its principal ([Git sources](docs/configuration.md#git-sources-signed-commits), SSH
|
|
230
|
+
or GPG signatures), but file sources, the key-to-principal list and the other policy
|
|
231
|
+
files still rest on a shared signing secret rather than per-principal public keys, and there
|
|
232
|
+
are no Slack/Jira connectors. Until those land, Aegis demonstrates that the *decision
|
|
233
|
+
procedure* is sound; it proves the identities feeding it only for Git-sourced rules. Resource matching is also case-sensitive on names. It is not
|
|
233
234
|
production-ready: read `docs/dev/PLAN.md §8` for the full list of open gaps before putting it in front
|
|
234
235
|
of anything you care about.
|
|
235
236
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Git sources (docs/configuration.md#git-sources-signed-commits).
|
|
2
|
+
# Copy to repos.yaml to enable: a constraint may then cite
|
|
3
|
+
# source_ref: "git:<repo-id>@<full commit sha>:<path>"
|
|
4
|
+
# and its principal becomes the verified signer of that commit.
|
|
5
|
+
# Aegis never fetches: keep each clone up to date with your own `git fetch`,
|
|
6
|
+
# and use full clones (shallow clones and grafts are refused).
|
|
7
|
+
repos:
|
|
8
|
+
platform-policy:
|
|
9
|
+
path: /srv/aegis/platform-policy
|
|
10
|
+
ref: refs/remotes/origin/main # the ref that defines "current"
|
|
11
|
+
rule_glob: rules/*.yaml # one rule per file
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Which signing key speaks for which principal (the names authority.yaml uses).
|
|
2
|
+
# Copy to signers.yaml alongside repos.yaml, then sign both with `aegis sign`.
|
|
3
|
+
# Never list GitHub's web-flow key (it signs merges made in the GitHub UI):
|
|
4
|
+
# it would make anyone who can press "Merge" any principal.
|
|
5
|
+
signers:
|
|
6
|
+
- principal: admin
|
|
7
|
+
keys:
|
|
8
|
+
- type: ssh
|
|
9
|
+
key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExampleOnlyReplaceWithARealPublicKey0000 alice@example.com"
|
|
10
|
+
- principal: sre_lead
|
|
11
|
+
keys:
|
|
12
|
+
- type: ssh
|
|
13
|
+
key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAnotherExampleKeyReplaceWithARealOne000 bob@example.com"
|
|
@@ -95,6 +95,15 @@ from aegis_core.environments import (
|
|
|
95
95
|
load_environment_map,
|
|
96
96
|
resolve_current_context,
|
|
97
97
|
)
|
|
98
|
+
from aegis_core.gitsource import (
|
|
99
|
+
GIT_PREFIX,
|
|
100
|
+
REPOS_FILE,
|
|
101
|
+
SIGNERS_FILE,
|
|
102
|
+
DispatchingSourceFetcher,
|
|
103
|
+
GitSourceFetcher,
|
|
104
|
+
load_repos,
|
|
105
|
+
load_signers,
|
|
106
|
+
)
|
|
98
107
|
from aegis_core.intent import InfrastructureIntent
|
|
99
108
|
from aegis_core.interceptor import AegisInterceptor, Decision
|
|
100
109
|
from aegis_core.ledger import DecisionLedger, JsonlLedger, SqliteLedger, parse_window
|
|
@@ -284,6 +293,28 @@ def _add_common_options(subparser: argparse.ArgumentParser) -> None:
|
|
|
284
293
|
"when it exists; pass '' to disable); constraints whose cited source does not back "
|
|
285
294
|
"them are quarantined as forged at load time",
|
|
286
295
|
)
|
|
296
|
+
subparser.add_argument(
|
|
297
|
+
"--repos",
|
|
298
|
+
default=None,
|
|
299
|
+
help="repos.yaml configuring the local clones of policy repositories that "
|
|
300
|
+
"constraints may cite as git:<repo>@<sha>:<path> (default: <dir of --constraints>/"
|
|
301
|
+
"repos.yaml when it exists); needs --signers",
|
|
302
|
+
)
|
|
303
|
+
subparser.add_argument(
|
|
304
|
+
"--signers",
|
|
305
|
+
default=None,
|
|
306
|
+
help="signers.yaml mapping SSH signing keys to principals; a git-sourced "
|
|
307
|
+
"constraint's principal is the verified signer of the commit it cites (default: "
|
|
308
|
+
"<dir of --constraints>/signers.yaml)",
|
|
309
|
+
)
|
|
310
|
+
subparser.add_argument(
|
|
311
|
+
"--max-source-age",
|
|
312
|
+
type=float,
|
|
313
|
+
default=None,
|
|
314
|
+
metavar="HOURS",
|
|
315
|
+
help="quarantine git-sourced constraints from a repository whose tracked ref has no "
|
|
316
|
+
"commit newer than this (default: only warn after 24h)",
|
|
317
|
+
)
|
|
287
318
|
_add_key_options(subparser)
|
|
288
319
|
subparser.add_argument(
|
|
289
320
|
"--insecure",
|
|
@@ -450,6 +481,13 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
450
481
|
"--out", default="aegis-signing.key", help="path to write the key to"
|
|
451
482
|
)
|
|
452
483
|
|
|
484
|
+
sources_parser = subparsers.add_parser(
|
|
485
|
+
"sources",
|
|
486
|
+
help="Report every constraint's source verification: which transport (git, file), "
|
|
487
|
+
"the verified principal, or why it is quarantined; exit 1 if any is",
|
|
488
|
+
)
|
|
489
|
+
_add_common_options(sources_parser)
|
|
490
|
+
|
|
453
491
|
hook_parser = subparsers.add_parser(
|
|
454
492
|
"hook",
|
|
455
493
|
help="Run as a coding agent's pre-tool hook: read its JSON payload on stdin, answer "
|
|
@@ -591,6 +629,30 @@ def _resolve_config_paths(args: argparse.Namespace) -> None:
|
|
|
591
629
|
)
|
|
592
630
|
|
|
593
631
|
|
|
632
|
+
def _git_source_fetcher(args: argparse.Namespace, load: dict) -> GitSourceFetcher | None:
|
|
633
|
+
"""A :class:`GitSourceFetcher` when a ``repos.yaml`` is given or sits
|
|
634
|
+
next to the constraints file; ``None`` otherwise. ``repos.yaml`` without
|
|
635
|
+
a ``signers.yaml`` is a data error: git sources cannot verify anyone."""
|
|
636
|
+
base = os.path.dirname(os.path.abspath(args.constraints))
|
|
637
|
+
repos = args.repos or os.path.join(base, REPOS_FILE)
|
|
638
|
+
if not os.path.exists(repos):
|
|
639
|
+
if args.repos:
|
|
640
|
+
raise FileNotFoundError(2, "No such file or directory", repos)
|
|
641
|
+
return None
|
|
642
|
+
signers = args.signers or os.path.join(base, SIGNERS_FILE)
|
|
643
|
+
if not os.path.exists(signers):
|
|
644
|
+
raise DataError(f"{repos} is configured but there is no {signers}: git sources need "
|
|
645
|
+
"a signers file to verify commit signatures")
|
|
646
|
+
warnings: list[str] = []
|
|
647
|
+
fetcher = GitSourceFetcher(
|
|
648
|
+
load_repos(repos, warnings=warnings, **load),
|
|
649
|
+
load_signers(signers, warnings=warnings, **load),
|
|
650
|
+
max_source_age=args.max_source_age * 3600 if args.max_source_age is not None else None,
|
|
651
|
+
)
|
|
652
|
+
fetcher.warnings[:0] = warnings
|
|
653
|
+
return fetcher
|
|
654
|
+
|
|
655
|
+
|
|
594
656
|
def _default_sources(args: argparse.Namespace) -> str | None:
|
|
595
657
|
"""``--sources`` as given; ``''`` disables; ``None`` means the
|
|
596
658
|
``sources`` directory next to the constraints file, when it exists."""
|
|
@@ -824,11 +886,12 @@ def _open_ledger(
|
|
|
824
886
|
return ledger
|
|
825
887
|
|
|
826
888
|
|
|
827
|
-
def
|
|
889
|
+
def _load_policy(args: argparse.Namespace):
|
|
890
|
+
"""Resolves the config, verifies and loads the authority map and the
|
|
891
|
+
constraint store (with file and git source verification). Returns
|
|
892
|
+
``(store, authority_map, load, fetcher)``; shared by ``check`` and
|
|
893
|
+
``sources``."""
|
|
828
894
|
_resolve_config_paths(args)
|
|
829
|
-
now = _parse_now(args.now)
|
|
830
|
-
pretty = bool(args.pretty)
|
|
831
|
-
|
|
832
895
|
if not os.path.exists(args.constraints):
|
|
833
896
|
raise FileNotFoundError(2, "No such file or directory", args.constraints)
|
|
834
897
|
key_warnings: list[str] = []
|
|
@@ -840,6 +903,12 @@ def _evaluate(intents: list[InfrastructureIntent], args: argparse.Namespace) ->
|
|
|
840
903
|
)
|
|
841
904
|
sources = _default_sources(args)
|
|
842
905
|
fetcher = FileSourceFetcher(sources, **load) if sources else None
|
|
906
|
+
git_fetcher = _git_source_fetcher(args, load)
|
|
907
|
+
if fetcher is not None or git_fetcher is not None:
|
|
908
|
+
# always dispatch, so a git: citation is never handed to the file
|
|
909
|
+
# fetcher and is rejected as unknown-repo when git is not configured
|
|
910
|
+
# (with no fetcher at all, ConstraintStore.load rejects it itself)
|
|
911
|
+
fetcher = DispatchingSourceFetcher(fetcher, git_fetcher)
|
|
843
912
|
store = _load_or_data_error(
|
|
844
913
|
lambda: ConstraintStore.load(
|
|
845
914
|
args.constraints, authority_map=authority_map, source_fetcher=fetcher, **load
|
|
@@ -848,6 +917,13 @@ def _evaluate(intents: list[InfrastructureIntent], args: argparse.Namespace) ->
|
|
|
848
917
|
"constraints",
|
|
849
918
|
)
|
|
850
919
|
store.warnings[:0] = key_warnings
|
|
920
|
+
return store, authority_map, load, fetcher
|
|
921
|
+
|
|
922
|
+
|
|
923
|
+
def _evaluate(intents: list[InfrastructureIntent], args: argparse.Namespace) -> int:
|
|
924
|
+
now = _parse_now(args.now)
|
|
925
|
+
pretty = bool(args.pretty)
|
|
926
|
+
store, authority_map, load, _fetcher = _load_policy(args)
|
|
851
927
|
plan_store = None
|
|
852
928
|
if args.plan_constraints:
|
|
853
929
|
plan_store = _load_or_data_error(
|
|
@@ -1016,9 +1092,59 @@ def _run_init(args: argparse.Namespace) -> int:
|
|
|
1016
1092
|
f" 3. Replace the *.example.yaml files in {args.dir} with your own constraints.yaml / "
|
|
1017
1093
|
"authority.yaml / ... (aegis prefers a real file over the .example one when both exist)"
|
|
1018
1094
|
)
|
|
1095
|
+
print(
|
|
1096
|
+
" 4. Optional, rules backed by signed Git commits: copy repos.example.yaml and "
|
|
1097
|
+
"signers.example.yaml to repos.yaml / signers.yaml (see docs/configuration.md), then "
|
|
1098
|
+
"check them with: aegis sources --pretty"
|
|
1099
|
+
)
|
|
1019
1100
|
return 0
|
|
1020
1101
|
|
|
1021
1102
|
|
|
1103
|
+
def _transport(source_ref: str, fetcher) -> str:
|
|
1104
|
+
if source_ref.startswith(GIT_PREFIX):
|
|
1105
|
+
return "git"
|
|
1106
|
+
has_file = fetcher is not None and getattr(fetcher, "file_fetcher", fetcher) is not None
|
|
1107
|
+
return "file" if has_file else "unchecked"
|
|
1108
|
+
|
|
1109
|
+
|
|
1110
|
+
def _run_sources(args: argparse.Namespace) -> int:
|
|
1111
|
+
"""``aegis sources``: one line per constraint -- loaded (with the
|
|
1112
|
+
principal its source verified) or quarantined (with the reason)."""
|
|
1113
|
+
store, _authority, _load, fetcher = _load_policy(args)
|
|
1114
|
+
rows = []
|
|
1115
|
+
for c in store.constraints.values():
|
|
1116
|
+
rows.append({"id": c.id, "status": "ok", "reason": None, "principal": c.principal,
|
|
1117
|
+
"transport": _transport(c.source_ref, fetcher),
|
|
1118
|
+
"source_ref": c.source_ref})
|
|
1119
|
+
# Walk the quarantine log entry by entry, in order. Entries that parsed
|
|
1120
|
+
# into a Constraint were appended to quarantined_constraints in the same
|
|
1121
|
+
# order; "invalid: ..." entries (bad shape, duplicate id) never parsed.
|
|
1122
|
+
# Matching by id instead would drop a duplicate-id entry whose id is
|
|
1123
|
+
# also a loaded rule, and confuse two quarantined entries sharing an id.
|
|
1124
|
+
parsed = iter(store.quarantined_constraints)
|
|
1125
|
+
for q in store.health.quarantined:
|
|
1126
|
+
c = None if q["reason"].startswith("invalid: ") else next(parsed, None)
|
|
1127
|
+
rows.append({"id": q["id"], "status": "quarantined", "reason": q["reason"],
|
|
1128
|
+
"principal": None,
|
|
1129
|
+
"transport": _transport(c.source_ref, fetcher) if c else None,
|
|
1130
|
+
"source_ref": c.source_ref if c else None})
|
|
1131
|
+
rows.sort(key=lambda r: (r["status"] != "quarantined", r["id"])) # stable
|
|
1132
|
+
if args.pretty:
|
|
1133
|
+
for r in rows:
|
|
1134
|
+
label = "OK" if r["status"] == "ok" else f"QUARANTINED ({r['reason']})"
|
|
1135
|
+
print(f"{label:<44} {r['id']:<40} {r['principal'] or '-':<12} "
|
|
1136
|
+
f"{r['transport'] or '-':<9} {r['source_ref'] or ''}")
|
|
1137
|
+
h = store.health
|
|
1138
|
+
print(f"STORE: loaded={h.loaded} quarantined={len(h.quarantined)} "
|
|
1139
|
+
f"principals={h.principals}")
|
|
1140
|
+
for w in h.warnings:
|
|
1141
|
+
print(f" warning: {w}")
|
|
1142
|
+
else:
|
|
1143
|
+
for r in rows:
|
|
1144
|
+
print(json.dumps(r))
|
|
1145
|
+
return 1 if store.health.quarantined else 0
|
|
1146
|
+
|
|
1147
|
+
|
|
1022
1148
|
def _run_keygen(args: argparse.Namespace) -> int:
|
|
1023
1149
|
path = config_module.generate_signing_key(args.out)
|
|
1024
1150
|
print(f"aegis: wrote signing key to {path}")
|
|
@@ -1035,6 +1161,8 @@ def _run(argv: list[str]) -> int:
|
|
|
1035
1161
|
return _run_init(args)
|
|
1036
1162
|
if args.command == "keygen":
|
|
1037
1163
|
return _run_keygen(args)
|
|
1164
|
+
if args.command == "sources":
|
|
1165
|
+
return _run_sources(args)
|
|
1038
1166
|
if args.command == "hook":
|
|
1039
1167
|
return hook_module.main_hook(
|
|
1040
1168
|
args.agent,
|