agentmetry 0.4.0__py3-none-any.whl

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 (131) hide show
  1. agentmetry/__init__.py +12 -0
  2. agentmetry/api/__init__.py +0 -0
  3. agentmetry/api/main.py +232 -0
  4. agentmetry/api/routes/__init__.py +0 -0
  5. agentmetry/api/routes/audit.py +396 -0
  6. agentmetry/api/websocket.py +53 -0
  7. agentmetry/api/ws_bridge.py +34 -0
  8. agentmetry/cli/__init__.py +941 -0
  9. agentmetry/cli/__main__.py +5 -0
  10. agentmetry/core/__init__.py +0 -0
  11. agentmetry/core/audit/__init__.py +1 -0
  12. agentmetry/core/audit/adapters/__init__.py +0 -0
  13. agentmetry/core/audit/adapters/agt.py +304 -0
  14. agentmetry/core/audit/adapters/cloudevents.py +159 -0
  15. agentmetry/core/audit/adapters/ecs.py +103 -0
  16. agentmetry/core/audit/adapters/splunk.py +40 -0
  17. agentmetry/core/audit/alerts.py +56 -0
  18. agentmetry/core/audit/canonical.py +150 -0
  19. agentmetry/core/audit/compliance_digest.py +299 -0
  20. agentmetry/core/audit/detection/__init__.py +9 -0
  21. agentmetry/core/audit/detection/benchmark.py +194 -0
  22. agentmetry/core/audit/detection/corpus/attack_approval_denied_then_executed.jsonl +3 -0
  23. agentmetry/core/audit/detection/corpus/attack_arbitrary_host_stage_execute.jsonl +3 -0
  24. agentmetry/core/audit/detection/corpus/attack_autonomous_unapproved_write.jsonl +3 -0
  25. agentmetry/core/audit/detection/corpus/attack_credential_exfil.jsonl +2 -0
  26. agentmetry/core/audit/detection/corpus/attack_credential_then_cloud_api.jsonl +2 -0
  27. agentmetry/core/audit/detection/corpus/attack_destructive_delete_burst.jsonl +6 -0
  28. agentmetry/core/audit/detection/corpus/attack_discovery_then_collect.jsonl +5 -0
  29. agentmetry/core/audit/detection/corpus/attack_dotfile_then_git_push.jsonl +2 -0
  30. agentmetry/core/audit/detection/corpus/attack_encoded_command_download.jsonl +1 -0
  31. agentmetry/core/audit/detection/corpus/attack_env_credential_exfil.jsonl +3 -0
  32. agentmetry/core/audit/detection/corpus/attack_hashed_only_no_command.jsonl +2 -0
  33. agentmetry/core/audit/detection/corpus/attack_interpreter_egress.jsonl +2 -0
  34. agentmetry/core/audit/detection/corpus/attack_pr_merged_without_review.jsonl +2 -0
  35. agentmetry/core/audit/detection/corpus/attack_proc_substitution_cradle.jsonl +2 -0
  36. agentmetry/core/audit/detection/corpus/attack_remote_pipe_to_shell.jsonl +1 -0
  37. agentmetry/core/audit/detection/corpus/attack_remote_staging_then_execute.jsonl +2 -0
  38. agentmetry/core/audit/detection/corpus/attack_session_tool_burst.jsonl +42 -0
  39. agentmetry/core/audit/detection/corpus/attack_single_command_exfil.jsonl +2 -0
  40. agentmetry/core/audit/detection/corpus/attack_ssh_directory_exfil.jsonl +3 -0
  41. agentmetry/core/audit/detection/corpus/attack_subagent_swarm.jsonl +6 -0
  42. agentmetry/core/audit/detection/corpus/attack_timestamp_collision.jsonl +2 -0
  43. agentmetry/core/audit/detection/corpus/attack_untrusted_input_then_action.jsonl +3 -0
  44. agentmetry/core/audit/detection/corpus/benign_authoring_merge_fixtures.jsonl +3 -0
  45. agentmetry/core/audit/detection/corpus/benign_autonomous_after_approval.jsonl +4 -0
  46. agentmetry/core/audit/detection/corpus/benign_build_artifact_cleanup.jsonl +5 -0
  47. agentmetry/core/audit/detection/corpus/benign_ci_artifact_download.jsonl +3 -0
  48. agentmetry/core/audit/detection/corpus/benign_database_migration.jsonl +5 -0
  49. agentmetry/core/audit/detection/corpus/benign_dependency_install_and_build.jsonl +5 -0
  50. agentmetry/core/audit/detection/corpus/benign_download_release_archive.jsonl +4 -0
  51. agentmetry/core/audit/detection/corpus/benign_fetch_data_then_run_repo_script.jsonl +3 -0
  52. agentmetry/core/audit/detection/corpus/benign_fetch_dataset_then_analyse.jsonl +3 -0
  53. agentmetry/core/audit/detection/corpus/benign_fetch_lockfile_then_install.jsonl +3 -0
  54. agentmetry/core/audit/detection/corpus/benign_git_review_and_push.jsonl +6 -0
  55. agentmetry/core/audit/detection/corpus/benign_human_driven_deletes.jsonl +6 -0
  56. agentmetry/core/audit/detection/corpus/benign_local_api_probing.jsonl +5 -0
  57. agentmetry/core/audit/detection/corpus/benign_long_but_calm_session.jsonl +30 -0
  58. agentmetry/core/audit/detection/corpus/benign_loopback_is_not_egress.jsonl +2 -0
  59. agentmetry/core/audit/detection/corpus/benign_loopback_pipe_to_interpreter.jsonl +3 -0
  60. agentmetry/core/audit/detection/corpus/benign_ordinary_development.jsonl +5 -0
  61. agentmetry/core/audit/detection/corpus/benign_package_manager_after_fetch.jsonl +2 -0
  62. agentmetry/core/audit/detection/corpus/benign_reading_config_that_is_not_secret.jsonl +5 -0
  63. agentmetry/core/audit/detection/corpus/benign_remote_api_call_no_credentials.jsonl +4 -0
  64. agentmetry/core/audit/detection/corpus/benign_research_then_docs.jsonl +5 -0
  65. agentmetry/core/audit/detection/corpus/benign_reversed_order_is_not_exfil.jsonl +2 -0
  66. agentmetry/core/audit/detection/corpus/benign_test_and_fix_loop.jsonl +6 -0
  67. agentmetry/core/audit/detection/corpus/benign_writing_about_credentials.jsonl +6 -0
  68. agentmetry/core/audit/detection/corpus/corpus.yaml +443 -0
  69. agentmetry/core/audit/detection/disposition.py +651 -0
  70. agentmetry/core/audit/detection/engine.py +78 -0
  71. agentmetry/core/audit/detection/live.py +127 -0
  72. agentmetry/core/audit/detection/live_store.py +355 -0
  73. agentmetry/core/audit/detection/models.py +53 -0
  74. agentmetry/core/audit/detection/rules.py +1314 -0
  75. agentmetry/core/audit/detection/traits.py +648 -0
  76. agentmetry/core/audit/detection/yaml_config.py +91 -0
  77. agentmetry/core/audit/detection/yaml_rules.py +83 -0
  78. agentmetry/core/audit/dlp/__init__.py +4 -0
  79. agentmetry/core/audit/dlp/loader.py +29 -0
  80. agentmetry/core/audit/dlp/models.py +29 -0
  81. agentmetry/core/audit/dlp/scanner.py +96 -0
  82. agentmetry/core/audit/dogfood.py +398 -0
  83. agentmetry/core/audit/evidence_pack.py +500 -0
  84. agentmetry/core/audit/external.py +213 -0
  85. agentmetry/core/audit/hashing.py +21 -0
  86. agentmetry/core/audit/hook_bootstrap.py +451 -0
  87. agentmetry/core/audit/identity.py +39 -0
  88. agentmetry/core/audit/ingest.py +242 -0
  89. agentmetry/core/audit/migrate.py +73 -0
  90. agentmetry/core/audit/mitre.py +244 -0
  91. agentmetry/core/audit/policy.py +99 -0
  92. agentmetry/core/audit/redaction.py +50 -0
  93. agentmetry/core/audit/replay.py +54 -0
  94. agentmetry/core/audit/run_context.py +129 -0
  95. agentmetry/core/audit/sinks.py +235 -0
  96. agentmetry/core/audit/spool.py +394 -0
  97. agentmetry/core/audit/tool_policy/__init__.py +4 -0
  98. agentmetry/core/audit/tool_policy/evaluator.py +198 -0
  99. agentmetry/core/audit/tool_policy/loader.py +44 -0
  100. agentmetry/core/audit/tool_policy/models.py +25 -0
  101. agentmetry/core/audit/trail_chain.py +300 -0
  102. agentmetry/core/audit/trail_db.py +491 -0
  103. agentmetry/core/audit/trail_merkle.py +332 -0
  104. agentmetry/core/auth.py +54 -0
  105. agentmetry/core/bus/__init__.py +5 -0
  106. agentmetry/core/bus/audit_exporter.py +107 -0
  107. agentmetry/core/bus/bridges.py +26 -0
  108. agentmetry/core/bus/bus.py +102 -0
  109. agentmetry/core/bus/events.py +50 -0
  110. agentmetry/core/bus/outbox.py +124 -0
  111. agentmetry/core/config.py +177 -0
  112. agentmetry/core/diagnostics/__init__.py +0 -0
  113. agentmetry/core/diagnostics/autostart.py +563 -0
  114. agentmetry/core/diagnostics/doctor.py +535 -0
  115. agentmetry/core/diagnostics/driver_paths.py +156 -0
  116. agentmetry/core/diagnostics/env_file.py +45 -0
  117. agentmetry/core/drivers/__init__.py +4 -0
  118. agentmetry/core/drivers/host.py +263 -0
  119. agentmetry/core/drivers/permissions.py +37 -0
  120. agentmetry/core/drivers/spec.py +118 -0
  121. agentmetry/core/extensions.py +107 -0
  122. agentmetry/core/health.py +26 -0
  123. agentmetry/core/version.py +13 -0
  124. agentmetry/policies/detection/manifest.yaml +43 -0
  125. agentmetry/policies/dlp/manifest.yaml +161 -0
  126. agentmetry/policies/opa/agent_rules.rego +33 -0
  127. agentmetry/policies/tool/manifest.yaml +117 -0
  128. agentmetry-0.4.0.dist-info/METADATA +86 -0
  129. agentmetry-0.4.0.dist-info/RECORD +131 -0
  130. agentmetry-0.4.0.dist-info/WHEEL +4 -0
  131. agentmetry-0.4.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,300 @@
1
+ """Hash-chained JSONL audit trail records (tamper-evident file sink).
2
+
3
+ Each file line is an envelope:
4
+
5
+ {"trail": {"v": 1, "seq": N, "prev_sha256": "...", "record_sha256": "..."}, "event": {...}}
6
+
7
+ Legacy lines (plain canonical events) remain readable; verify reports them separately.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import hashlib
13
+ import json
14
+ import logging
15
+ from dataclasses import dataclass
16
+ from pathlib import Path
17
+ from typing import Any
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+ TRAIL_VERSION = 1
22
+ GENESIS_SHA256 = hashlib.sha256(b"AGENTMETRY_TRAIL_GENESIS_v1").hexdigest()
23
+
24
+
25
+ def chain_sidecar_path(trail_path: Path) -> Path:
26
+ return trail_path.with_name(trail_path.name + ".chain")
27
+
28
+
29
+ def canonical_event_json(event: dict[str, Any]) -> str:
30
+ return json.dumps(event, separators=(",", ":"), sort_keys=True, default=str)
31
+
32
+
33
+ def compute_record_sha256(prev_sha256: str, event: dict[str, Any]) -> str:
34
+ material = f"{prev_sha256}\n{canonical_event_json(event)}".encode("utf-8")
35
+ return hashlib.sha256(material).hexdigest()
36
+
37
+
38
+ def wrap_chained_record(seq: int, prev_sha256: str, event: dict[str, Any]) -> dict[str, Any]:
39
+ record_sha256 = compute_record_sha256(prev_sha256, event)
40
+ return {
41
+ "trail": {
42
+ "v": TRAIL_VERSION,
43
+ "seq": seq,
44
+ "prev_sha256": prev_sha256,
45
+ "record_sha256": record_sha256,
46
+ },
47
+ "event": event,
48
+ }
49
+
50
+
51
+ def unwrap_trail_record(record: dict[str, Any]) -> dict[str, Any]:
52
+ """Return the canonical event from an envelope or a legacy plain record."""
53
+ if not isinstance(record, dict):
54
+ return record
55
+ trail = record.get("trail")
56
+ event = record.get("event")
57
+ if isinstance(trail, dict) and isinstance(event, dict):
58
+ return event
59
+ return record
60
+
61
+
62
+ def is_chained_record(record: dict[str, Any]) -> bool:
63
+ trail = record.get("trail") if isinstance(record, dict) else None
64
+ event = record.get("event") if isinstance(record, dict) else None
65
+ return isinstance(trail, dict) and isinstance(event, dict)
66
+
67
+
68
+ @dataclass
69
+ class ChainHead:
70
+ seq: int
71
+ last_sha256: str
72
+
73
+
74
+ def _load_sidecar(sidecar: Path) -> ChainHead | None:
75
+ if not sidecar.is_file():
76
+ return None
77
+ try:
78
+ data = json.loads(sidecar.read_text(encoding="utf-8"))
79
+ return ChainHead(
80
+ seq=int(data.get("seq", 0)),
81
+ last_sha256=str(data.get("last_sha256") or GENESIS_SHA256),
82
+ )
83
+ except (OSError, json.JSONDecodeError, TypeError, ValueError):
84
+ return None
85
+
86
+
87
+ def _save_sidecar(sidecar: Path, head: ChainHead) -> None:
88
+ sidecar.parent.mkdir(parents=True, exist_ok=True)
89
+ sidecar.write_text(
90
+ json.dumps({"seq": head.seq, "last_sha256": head.last_sha256}, indent=2) + "\n",
91
+ encoding="utf-8",
92
+ )
93
+
94
+
95
+ def _head_from_trail_file(trail_path: Path) -> ChainHead | None:
96
+ if not trail_path.is_file():
97
+ return None
98
+ try:
99
+ last_line = ""
100
+ with trail_path.open("r", encoding="utf-8", errors="replace") as fh:
101
+ for line in fh:
102
+ if line.strip():
103
+ last_line = line.strip()
104
+ if not last_line:
105
+ return None
106
+ record = json.loads(last_line)
107
+ if not is_chained_record(record):
108
+ return None
109
+ trail = record["trail"]
110
+ return ChainHead(seq=int(trail["seq"]), last_sha256=str(trail["record_sha256"]))
111
+ except (OSError, json.JSONDecodeError, KeyError, TypeError, ValueError):
112
+ return None
113
+
114
+
115
+ def load_chain_head(trail_path: Path) -> ChainHead:
116
+ sidecar = chain_sidecar_path(trail_path)
117
+ head = _load_sidecar(sidecar)
118
+ if head is not None:
119
+ return head
120
+ recovered = _head_from_trail_file(trail_path)
121
+ if recovered is not None:
122
+ return recovered
123
+ return ChainHead(seq=0, last_sha256=GENESIS_SHA256)
124
+
125
+
126
+ def append_chained_line(trail_path: Path, event: dict[str, Any]) -> ChainHead:
127
+ """Append one chained envelope line and persist the sidecar head."""
128
+ head = load_chain_head(trail_path)
129
+ next_seq = head.seq + 1
130
+ envelope = wrap_chained_record(next_seq, head.last_sha256, event)
131
+ line = json.dumps(envelope, separators=(",", ":"), default=str) + "\n"
132
+ trail_path.parent.mkdir(parents=True, exist_ok=True)
133
+ with trail_path.open("a", encoding="utf-8") as fh:
134
+ fh.write(line)
135
+ new_head = ChainHead(seq=next_seq, last_sha256=envelope["trail"]["record_sha256"])
136
+ _save_sidecar(chain_sidecar_path(trail_path), new_head)
137
+ return new_head
138
+
139
+
140
+ @dataclass
141
+ class TrailVerifyResult:
142
+ ok: bool
143
+ message: str
144
+ lines_total: int = 0
145
+ lines_chained: int = 0
146
+ lines_legacy: int = 0
147
+ first_bad_line: int | None = None
148
+ # The chain head after a successful walk. Printed by the CLI so an operator
149
+ # can record it somewhere the audited agent cannot write and compare later —
150
+ # the only defense a local file can offer against tail deletion.
151
+ head_seq: int = 0
152
+ head_sha256: str = ""
153
+
154
+
155
+ def verify_trail_file(trail_path: Path) -> TrailVerifyResult:
156
+ if not trail_path.is_file():
157
+ return TrailVerifyResult(False, f"no such file: {trail_path}")
158
+
159
+ prev = GENESIS_SHA256
160
+ expected_seq = 0
161
+ total = chained = legacy = 0
162
+ first_bad: int | None = None
163
+
164
+ try:
165
+ with trail_path.open("r", encoding="utf-8", errors="replace") as fh:
166
+ for line_no, raw in enumerate(fh, start=1):
167
+ line = raw.strip()
168
+ if not line:
169
+ continue
170
+ total += 1
171
+ try:
172
+ record = json.loads(line)
173
+ except json.JSONDecodeError:
174
+ return TrailVerifyResult(
175
+ False,
176
+ f"invalid JSON at line {line_no}",
177
+ total,
178
+ chained,
179
+ legacy,
180
+ line_no,
181
+ )
182
+ if not isinstance(record, dict):
183
+ return TrailVerifyResult(
184
+ False,
185
+ f"expected object at line {line_no}",
186
+ total,
187
+ chained,
188
+ legacy,
189
+ line_no,
190
+ )
191
+ if not is_chained_record(record):
192
+ # Legacy lines are only legitimate as a prefix from before
193
+ # chaining was enabled. An unchained line AFTER chained
194
+ # records is exactly what a forged append looks like, and
195
+ # readers would render it as a real event.
196
+ if chained > 0:
197
+ return TrailVerifyResult(
198
+ False,
199
+ f"unchained line after chained records at line {line_no} (forged append?)",
200
+ total,
201
+ chained,
202
+ legacy,
203
+ line_no,
204
+ )
205
+ legacy += 1
206
+ continue
207
+
208
+ trail = record["trail"]
209
+ event = record["event"]
210
+ seq = int(trail.get("seq", -1))
211
+ prev_sha = str(trail.get("prev_sha256", ""))
212
+ record_sha = str(trail.get("record_sha256", ""))
213
+
214
+ if seq != expected_seq + 1:
215
+ return TrailVerifyResult(
216
+ False,
217
+ f"sequence break at line {line_no}: expected seq {expected_seq + 1}, got {seq}",
218
+ total,
219
+ chained,
220
+ legacy,
221
+ line_no,
222
+ )
223
+ if prev_sha != prev:
224
+ return TrailVerifyResult(
225
+ False,
226
+ f"prev_sha256 mismatch at line {line_no}",
227
+ total,
228
+ chained,
229
+ legacy,
230
+ line_no,
231
+ )
232
+ expected = compute_record_sha256(prev_sha, event)
233
+ if record_sha != expected:
234
+ return TrailVerifyResult(
235
+ False,
236
+ f"record_sha256 mismatch at line {line_no} (tampered?)",
237
+ total,
238
+ chained,
239
+ legacy,
240
+ line_no,
241
+ )
242
+
243
+ chained += 1
244
+ expected_seq = seq
245
+ prev = record_sha
246
+ except OSError as exc:
247
+ return TrailVerifyResult(False, f"read failed: {exc}")
248
+
249
+ if chained == 0 and legacy > 0:
250
+ return TrailVerifyResult(
251
+ True,
252
+ f"legacy trail only — {legacy} unchained line(s); enable export to start chaining",
253
+ total,
254
+ chained,
255
+ legacy,
256
+ )
257
+ if chained == 0:
258
+ return TrailVerifyResult(True, "empty trail", total, chained, legacy)
259
+
260
+ # The chain itself proves in-place edits, inserts, and reordering. It cannot
261
+ # prove the file was not cut short: compare against the sidecar head, which
262
+ # records how far the writer actually got.
263
+ sidecar_head = _load_sidecar(chain_sidecar_path(trail_path))
264
+ if sidecar_head is not None and sidecar_head.seq > expected_seq:
265
+ return TrailVerifyResult(
266
+ False,
267
+ f"trail ends at seq {expected_seq} but sidecar records seq {sidecar_head.seq} (truncated?)",
268
+ total,
269
+ chained,
270
+ legacy,
271
+ )
272
+ if sidecar_head is not None and sidecar_head.seq == expected_seq and sidecar_head.last_sha256 != prev:
273
+ return TrailVerifyResult(
274
+ False,
275
+ f"sidecar head hash does not match the last chained record at seq {expected_seq}",
276
+ total,
277
+ chained,
278
+ legacy,
279
+ )
280
+
281
+ prefix = (
282
+ f"{chained} chained line(s) verified; {legacy} legacy unchained prefix line(s)"
283
+ if legacy
284
+ else f"{chained} chained line(s) verified"
285
+ )
286
+ # A missing sidecar is legitimate (verifying a copied .jsonl on another
287
+ # machine) but it means tail deletion cannot be ruled out — say so instead
288
+ # of implying a stronger guarantee than the file alone can carry.
289
+ if sidecar_head is None:
290
+ prefix += "; no sidecar head found, so tail truncation cannot be ruled out"
291
+ return TrailVerifyResult(
292
+ True,
293
+ prefix,
294
+ total,
295
+ chained,
296
+ legacy,
297
+ first_bad,
298
+ head_seq=expected_seq,
299
+ head_sha256=prev,
300
+ )