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,124 @@
1
+ """Durable event outbox — SQLite WAL log of every bus event (tokens excluded).
2
+
3
+ Gives the dashboard replay-on-reconnect (GET /api/v1/events?since=N) and makes
4
+ the event stream a queryable audit source instead of fire-and-forget frames.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import sqlite3
11
+ from pathlib import Path
12
+ from threading import Lock
13
+ from typing import Any
14
+
15
+ from agentmetry.core.bus.events import Event
16
+
17
+ _DATA_DIR = Path(__file__).resolve().parents[3] / "data"
18
+
19
+
20
+ class EventOutbox:
21
+ def __init__(self, db_path: str | Path | None = None):
22
+ path = Path(db_path) if db_path else _DATA_DIR / "events.db"
23
+ path.parent.mkdir(parents=True, exist_ok=True)
24
+ self._conn = sqlite3.connect(str(path), check_same_thread=False)
25
+ self._lock = Lock()
26
+ with self._lock, self._conn:
27
+ self._conn.execute("PRAGMA journal_mode=WAL")
28
+ self._conn.execute(
29
+ """
30
+ CREATE TABLE IF NOT EXISTS events (
31
+ seq INTEGER PRIMARY KEY,
32
+ ts TEXT NOT NULL,
33
+ topic TEXT NOT NULL,
34
+ session_id TEXT NOT NULL DEFAULT '',
35
+ thread_id TEXT NOT NULL DEFAULT '',
36
+ payload TEXT NOT NULL
37
+ )
38
+ """
39
+ )
40
+
41
+ def append(self, event: Event) -> None:
42
+ with self._lock, self._conn:
43
+ self._conn.execute(
44
+ "INSERT OR REPLACE INTO events (seq, ts, topic, session_id, thread_id, payload) "
45
+ "VALUES (?, ?, ?, ?, ?, ?)",
46
+ (
47
+ event.seq,
48
+ event.ts,
49
+ event.topic,
50
+ event.session_id,
51
+ event.thread_id,
52
+ json.dumps(event.payload, default=str),
53
+ ),
54
+ )
55
+
56
+ def max_seq(self) -> int:
57
+ with self._lock:
58
+ row = self._conn.execute("SELECT MAX(seq) FROM events").fetchone()
59
+ return int(row[0]) if row and row[0] is not None else 0
60
+
61
+ def read_since(self, seq: int, limit: int = 500) -> list[dict[str, Any]]:
62
+ with self._lock:
63
+ rows = self._conn.execute(
64
+ "SELECT seq, ts, topic, session_id, thread_id, payload FROM events "
65
+ "WHERE seq > ? ORDER BY seq LIMIT ?",
66
+ (seq, limit),
67
+ ).fetchall()
68
+ return self._rows_to_dicts(rows)
69
+
70
+ def read_between(
71
+ self,
72
+ start_ts: str,
73
+ end_ts: str,
74
+ *,
75
+ limit: int = 100_000,
76
+ ) -> list[dict[str, Any]]:
77
+ """Return events with ts in [start_ts, end_ts] (ISO-8601 strings, inclusive)."""
78
+ with self._lock:
79
+ rows = self._conn.execute(
80
+ "SELECT seq, ts, topic, session_id, thread_id, payload FROM events "
81
+ "WHERE ts >= ? AND ts <= ? ORDER BY seq LIMIT ?",
82
+ (start_ts, end_ts, limit),
83
+ ).fetchall()
84
+ return self._rows_to_dicts(rows)
85
+
86
+ def read_by_thread_id(self, thread_id: str, *, limit: int = 10_000) -> list[dict[str, Any]]:
87
+ """Return events for one LangGraph thread / correlation id, ordered by seq."""
88
+ with self._lock:
89
+ rows = self._conn.execute(
90
+ "SELECT seq, ts, topic, session_id, thread_id, payload FROM events "
91
+ "WHERE thread_id = ? ORDER BY seq LIMIT ?",
92
+ (thread_id, limit),
93
+ ).fetchall()
94
+ return self._rows_to_dicts(rows)
95
+
96
+ @staticmethod
97
+ def _rows_to_dicts(rows) -> list[dict[str, Any]]:
98
+ out = []
99
+ for r in rows:
100
+ try:
101
+ payload = json.loads(r[5])
102
+ except json.JSONDecodeError:
103
+ payload = {}
104
+ out.append(
105
+ {
106
+ "seq": r[0],
107
+ "ts": r[1],
108
+ "topic": r[2],
109
+ "session_id": r[3],
110
+ "thread_id": r[4],
111
+ "payload": payload,
112
+ }
113
+ )
114
+ return out
115
+
116
+
117
+ _outbox: EventOutbox | None = None
118
+
119
+
120
+ def get_outbox() -> EventOutbox:
121
+ global _outbox
122
+ if _outbox is None:
123
+ _outbox = EventOutbox()
124
+ return _outbox
@@ -0,0 +1,177 @@
1
+ from pathlib import Path
2
+
3
+ from pydantic import AliasChoices, Field
4
+ from pydantic_settings import BaseSettings, SettingsConfigDict
5
+
6
+ _ORCHESTRATOR_ROOT = Path(__file__).resolve().parents[2]
7
+ _PACKAGE_ROOT = Path(__file__).resolve().parents[1]
8
+
9
+
10
+ def _policy(*parts: str) -> Path:
11
+ """A policy manifest shipped inside the package.
12
+
13
+ These are default configuration, not test data: without them DLP, tool
14
+ policy and the YAML detection rules are all inert, and a `pip install` used
15
+ to leave `doctor` opening with three FAILs and secret scanning silently off.
16
+
17
+ They live in the package rather than at the repo root because `python -m
18
+ build` builds the wheel from the sdist, and a force-include reaching outside
19
+ the project directory does not survive that trip. Config the package needs
20
+ to run belongs with the package.
21
+ """
22
+ return _PACKAGE_ROOT.joinpath("policies", *parts)
23
+
24
+
25
+ class Settings(BaseSettings):
26
+ """Runtime configuration for the Agentmetry SIEM flight recorder."""
27
+
28
+ model_config = SettingsConfigDict(
29
+ env_prefix="AGENTMETRY_",
30
+ env_file=_ORCHESTRATOR_ROOT / ".env",
31
+ env_file_encoding="utf-8",
32
+ extra="ignore",
33
+ )
34
+
35
+ # ------------------------------------------------------------------ SIEM
36
+ # Operator identity + optional API key protecting ingest/tail/export.
37
+ operator_id: str = Field(
38
+ default="",
39
+ validation_alias=AliasChoices(
40
+ "OPERATOR_ID",
41
+ "AGENTMETRY_OPERATOR_ID",
42
+ ),
43
+ )
44
+ fleet_id: str = Field(
45
+ default="",
46
+ validation_alias=AliasChoices("AGENTMETRY_FLEET_ID"),
47
+ )
48
+ api_key: str = Field(
49
+ default="",
50
+ validation_alias=AliasChoices("AGENTMETRY_API_KEY"),
51
+ )
52
+
53
+ # Canonical JSONL trail + query index (system of record for the hook path).
54
+ audit_export_enabled: bool = Field(
55
+ default=True,
56
+ validation_alias=AliasChoices(
57
+ "AGENTMETRY_AUDIT_EXPORT_ENABLED",
58
+ ),
59
+ )
60
+ audit_export_path: Path = _ORCHESTRATOR_ROOT / "data" / "audit-forward.jsonl"
61
+ audit_db_path: Path = _ORCHESTRATOR_ROOT / "data" / "audit.db"
62
+ detection_live_db_path: Path = _ORCHESTRATOR_ROOT / "data" / "detection_live.db"
63
+ # Triage state. An index over the `detection_disposition` events in the
64
+ # trail, which remain the record — see core/audit/detection/disposition.py.
65
+ detection_disposition_db_path: Path = (
66
+ _ORCHESTRATOR_ROOT / "data" / "detection_disposition.db"
67
+ )
68
+ audit_ingest_enabled: bool = True
69
+ audit_ingest_url: str = "http://127.0.0.1:8000"
70
+
71
+ # Forward sinks (all optional; file is the default and never a cloud ledger).
72
+ audit_sink: str = Field(
73
+ default="file",
74
+ validation_alias=AliasChoices("AGENTMETRY_AUDIT_SINK"),
75
+ )
76
+ audit_webhook_url: str = Field(
77
+ default="",
78
+ validation_alias=AliasChoices(
79
+ "AGENTMETRY_AUDIT_WEBHOOK_URL",
80
+ ),
81
+ )
82
+ audit_webhook_timeout_seconds: float = 5.0
83
+ # `canonical` (default) or `cloudevents`. CloudEvents v1.0 structured mode
84
+ # is what brokers consume; the canonical record still travels whole inside
85
+ # `data`. Default unchanged so an existing webhook keeps receiving the shape
86
+ # it was wired up for.
87
+ audit_webhook_format: str = Field(
88
+ default="canonical",
89
+ validation_alias=AliasChoices("AGENTMETRY_AUDIT_WEBHOOK_FORMAT"),
90
+ )
91
+ audit_elastic_url: str = Field(
92
+ default="",
93
+ validation_alias=AliasChoices(
94
+ "AGENTMETRY_AUDIT_ELASTIC_URL",
95
+ ),
96
+ )
97
+ audit_elastic_index: str = "logs-agentmetry"
98
+ audit_elastic_api_key: str = Field(
99
+ default="",
100
+ validation_alias=AliasChoices(
101
+ "ELASTIC_API_KEY",
102
+ "AGENTMETRY_ELASTIC_API_KEY",
103
+ ),
104
+ )
105
+ audit_elastic_verify_tls: bool = True
106
+ audit_splunk_hec_url: str = Field(
107
+ default="",
108
+ validation_alias=AliasChoices(
109
+ "AGENTMETRY_AUDIT_SPLUNK_HEC_URL",
110
+ ),
111
+ )
112
+ audit_splunk_hec_token: str = Field(
113
+ default="",
114
+ validation_alias=AliasChoices(
115
+ "SPLUNK_HEC_TOKEN",
116
+ "AGENTMETRY_SPLUNK_HEC_TOKEN",
117
+ ),
118
+ )
119
+ audit_splunk_index: str = "main"
120
+ audit_splunk_sourcetype: str = "agentmetry:json"
121
+ audit_splunk_verify_tls: bool = True
122
+ audit_alert_webhook_url: str = Field(
123
+ default="",
124
+ validation_alias=AliasChoices(
125
+ "AGENTMETRY_AUDIT_ALERT_WEBHOOK_URL",
126
+ ),
127
+ )
128
+
129
+ # Semantic DLP — regex scan of tool arguments at the hook boundary.
130
+ dlp_mode: str = Field(
131
+ default="log",
132
+ validation_alias=AliasChoices("AGENTMETRY_DLP_MODE"),
133
+ )
134
+ dlp_rules_path: Path = _policy("dlp", "manifest.yaml")
135
+ dlp_pii: bool = True
136
+
137
+ # Tool allow/deny policy — enforced at the hook boundary (like DLP block mode).
138
+ tool_policy_mode: str = Field(
139
+ default="log",
140
+ validation_alias=AliasChoices("AGENTMETRY_TOOL_POLICY_MODE"),
141
+ )
142
+ tool_policy_path: Path = (
143
+ _policy("tool", "manifest.yaml")
144
+ )
145
+
146
+ # Post-ingest policy checks (core/audit/policy.py). Off by default: the
147
+ # built-in ruleset is a hardcoded starting point, and it annotates only, it
148
+ # cannot block. Real prevention lives in the hook (DLP block mode).
149
+ policy_enabled: bool = Field(
150
+ default=False,
151
+ validation_alias=AliasChoices("AGENTMETRY_POLICY_ENABLED"),
152
+ )
153
+ # off-hours-activity detection. Off by default: "unusual hours" is only a
154
+ # signal once an operator says which hours are usual, and scheduled jobs
155
+ # legitimately run at night.
156
+ detect_off_hours: bool = Field(
157
+ default=False,
158
+ validation_alias=AliasChoices("AGENTMETRY_DETECT_OFF_HOURS"),
159
+ )
160
+ business_hours: str = Field(
161
+ default="09-18", # local start-end, 24h
162
+ validation_alias=AliasChoices("AGENTMETRY_BUSINESS_HOURS"),
163
+ )
164
+ business_tz: str = Field(
165
+ default="UTC", # IANA name, e.g. Europe/Athens
166
+ validation_alias=AliasChoices("AGENTMETRY_BUSINESS_TZ"),
167
+ )
168
+
169
+ detection_rules_path: Path = (
170
+ _policy("detection", "manifest.yaml")
171
+ )
172
+
173
+ # Demo MCP vault — doctor, drivers.json, vault_fs server (not a skill runtime).
174
+ vault_path: Path = Path(__file__).resolve().parents[4] / "vault"
175
+
176
+
177
+ settings = Settings()
File without changes