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.
- agentmetry/__init__.py +12 -0
- agentmetry/api/__init__.py +0 -0
- agentmetry/api/main.py +232 -0
- agentmetry/api/routes/__init__.py +0 -0
- agentmetry/api/routes/audit.py +396 -0
- agentmetry/api/websocket.py +53 -0
- agentmetry/api/ws_bridge.py +34 -0
- agentmetry/cli/__init__.py +941 -0
- agentmetry/cli/__main__.py +5 -0
- agentmetry/core/__init__.py +0 -0
- agentmetry/core/audit/__init__.py +1 -0
- agentmetry/core/audit/adapters/__init__.py +0 -0
- agentmetry/core/audit/adapters/agt.py +304 -0
- agentmetry/core/audit/adapters/cloudevents.py +159 -0
- agentmetry/core/audit/adapters/ecs.py +103 -0
- agentmetry/core/audit/adapters/splunk.py +40 -0
- agentmetry/core/audit/alerts.py +56 -0
- agentmetry/core/audit/canonical.py +150 -0
- agentmetry/core/audit/compliance_digest.py +299 -0
- agentmetry/core/audit/detection/__init__.py +9 -0
- agentmetry/core/audit/detection/benchmark.py +194 -0
- agentmetry/core/audit/detection/corpus/attack_approval_denied_then_executed.jsonl +3 -0
- agentmetry/core/audit/detection/corpus/attack_arbitrary_host_stage_execute.jsonl +3 -0
- agentmetry/core/audit/detection/corpus/attack_autonomous_unapproved_write.jsonl +3 -0
- agentmetry/core/audit/detection/corpus/attack_credential_exfil.jsonl +2 -0
- agentmetry/core/audit/detection/corpus/attack_credential_then_cloud_api.jsonl +2 -0
- agentmetry/core/audit/detection/corpus/attack_destructive_delete_burst.jsonl +6 -0
- agentmetry/core/audit/detection/corpus/attack_discovery_then_collect.jsonl +5 -0
- agentmetry/core/audit/detection/corpus/attack_dotfile_then_git_push.jsonl +2 -0
- agentmetry/core/audit/detection/corpus/attack_encoded_command_download.jsonl +1 -0
- agentmetry/core/audit/detection/corpus/attack_env_credential_exfil.jsonl +3 -0
- agentmetry/core/audit/detection/corpus/attack_hashed_only_no_command.jsonl +2 -0
- agentmetry/core/audit/detection/corpus/attack_interpreter_egress.jsonl +2 -0
- agentmetry/core/audit/detection/corpus/attack_pr_merged_without_review.jsonl +2 -0
- agentmetry/core/audit/detection/corpus/attack_proc_substitution_cradle.jsonl +2 -0
- agentmetry/core/audit/detection/corpus/attack_remote_pipe_to_shell.jsonl +1 -0
- agentmetry/core/audit/detection/corpus/attack_remote_staging_then_execute.jsonl +2 -0
- agentmetry/core/audit/detection/corpus/attack_session_tool_burst.jsonl +42 -0
- agentmetry/core/audit/detection/corpus/attack_single_command_exfil.jsonl +2 -0
- agentmetry/core/audit/detection/corpus/attack_ssh_directory_exfil.jsonl +3 -0
- agentmetry/core/audit/detection/corpus/attack_subagent_swarm.jsonl +6 -0
- agentmetry/core/audit/detection/corpus/attack_timestamp_collision.jsonl +2 -0
- agentmetry/core/audit/detection/corpus/attack_untrusted_input_then_action.jsonl +3 -0
- agentmetry/core/audit/detection/corpus/benign_authoring_merge_fixtures.jsonl +3 -0
- agentmetry/core/audit/detection/corpus/benign_autonomous_after_approval.jsonl +4 -0
- agentmetry/core/audit/detection/corpus/benign_build_artifact_cleanup.jsonl +5 -0
- agentmetry/core/audit/detection/corpus/benign_ci_artifact_download.jsonl +3 -0
- agentmetry/core/audit/detection/corpus/benign_database_migration.jsonl +5 -0
- agentmetry/core/audit/detection/corpus/benign_dependency_install_and_build.jsonl +5 -0
- agentmetry/core/audit/detection/corpus/benign_download_release_archive.jsonl +4 -0
- agentmetry/core/audit/detection/corpus/benign_fetch_data_then_run_repo_script.jsonl +3 -0
- agentmetry/core/audit/detection/corpus/benign_fetch_dataset_then_analyse.jsonl +3 -0
- agentmetry/core/audit/detection/corpus/benign_fetch_lockfile_then_install.jsonl +3 -0
- agentmetry/core/audit/detection/corpus/benign_git_review_and_push.jsonl +6 -0
- agentmetry/core/audit/detection/corpus/benign_human_driven_deletes.jsonl +6 -0
- agentmetry/core/audit/detection/corpus/benign_local_api_probing.jsonl +5 -0
- agentmetry/core/audit/detection/corpus/benign_long_but_calm_session.jsonl +30 -0
- agentmetry/core/audit/detection/corpus/benign_loopback_is_not_egress.jsonl +2 -0
- agentmetry/core/audit/detection/corpus/benign_loopback_pipe_to_interpreter.jsonl +3 -0
- agentmetry/core/audit/detection/corpus/benign_ordinary_development.jsonl +5 -0
- agentmetry/core/audit/detection/corpus/benign_package_manager_after_fetch.jsonl +2 -0
- agentmetry/core/audit/detection/corpus/benign_reading_config_that_is_not_secret.jsonl +5 -0
- agentmetry/core/audit/detection/corpus/benign_remote_api_call_no_credentials.jsonl +4 -0
- agentmetry/core/audit/detection/corpus/benign_research_then_docs.jsonl +5 -0
- agentmetry/core/audit/detection/corpus/benign_reversed_order_is_not_exfil.jsonl +2 -0
- agentmetry/core/audit/detection/corpus/benign_test_and_fix_loop.jsonl +6 -0
- agentmetry/core/audit/detection/corpus/benign_writing_about_credentials.jsonl +6 -0
- agentmetry/core/audit/detection/corpus/corpus.yaml +443 -0
- agentmetry/core/audit/detection/disposition.py +651 -0
- agentmetry/core/audit/detection/engine.py +78 -0
- agentmetry/core/audit/detection/live.py +127 -0
- agentmetry/core/audit/detection/live_store.py +355 -0
- agentmetry/core/audit/detection/models.py +53 -0
- agentmetry/core/audit/detection/rules.py +1314 -0
- agentmetry/core/audit/detection/traits.py +648 -0
- agentmetry/core/audit/detection/yaml_config.py +91 -0
- agentmetry/core/audit/detection/yaml_rules.py +83 -0
- agentmetry/core/audit/dlp/__init__.py +4 -0
- agentmetry/core/audit/dlp/loader.py +29 -0
- agentmetry/core/audit/dlp/models.py +29 -0
- agentmetry/core/audit/dlp/scanner.py +96 -0
- agentmetry/core/audit/dogfood.py +398 -0
- agentmetry/core/audit/evidence_pack.py +500 -0
- agentmetry/core/audit/external.py +213 -0
- agentmetry/core/audit/hashing.py +21 -0
- agentmetry/core/audit/hook_bootstrap.py +451 -0
- agentmetry/core/audit/identity.py +39 -0
- agentmetry/core/audit/ingest.py +242 -0
- agentmetry/core/audit/migrate.py +73 -0
- agentmetry/core/audit/mitre.py +244 -0
- agentmetry/core/audit/policy.py +99 -0
- agentmetry/core/audit/redaction.py +50 -0
- agentmetry/core/audit/replay.py +54 -0
- agentmetry/core/audit/run_context.py +129 -0
- agentmetry/core/audit/sinks.py +235 -0
- agentmetry/core/audit/spool.py +394 -0
- agentmetry/core/audit/tool_policy/__init__.py +4 -0
- agentmetry/core/audit/tool_policy/evaluator.py +198 -0
- agentmetry/core/audit/tool_policy/loader.py +44 -0
- agentmetry/core/audit/tool_policy/models.py +25 -0
- agentmetry/core/audit/trail_chain.py +300 -0
- agentmetry/core/audit/trail_db.py +491 -0
- agentmetry/core/audit/trail_merkle.py +332 -0
- agentmetry/core/auth.py +54 -0
- agentmetry/core/bus/__init__.py +5 -0
- agentmetry/core/bus/audit_exporter.py +107 -0
- agentmetry/core/bus/bridges.py +26 -0
- agentmetry/core/bus/bus.py +102 -0
- agentmetry/core/bus/events.py +50 -0
- agentmetry/core/bus/outbox.py +124 -0
- agentmetry/core/config.py +177 -0
- agentmetry/core/diagnostics/__init__.py +0 -0
- agentmetry/core/diagnostics/autostart.py +563 -0
- agentmetry/core/diagnostics/doctor.py +535 -0
- agentmetry/core/diagnostics/driver_paths.py +156 -0
- agentmetry/core/diagnostics/env_file.py +45 -0
- agentmetry/core/drivers/__init__.py +4 -0
- agentmetry/core/drivers/host.py +263 -0
- agentmetry/core/drivers/permissions.py +37 -0
- agentmetry/core/drivers/spec.py +118 -0
- agentmetry/core/extensions.py +107 -0
- agentmetry/core/health.py +26 -0
- agentmetry/core/version.py +13 -0
- agentmetry/policies/detection/manifest.yaml +43 -0
- agentmetry/policies/dlp/manifest.yaml +161 -0
- agentmetry/policies/opa/agent_rules.rego +33 -0
- agentmetry/policies/tool/manifest.yaml +117 -0
- agentmetry-0.4.0.dist-info/METADATA +86 -0
- agentmetry-0.4.0.dist-info/RECORD +131 -0
- agentmetry-0.4.0.dist-info/WHEEL +4 -0
- agentmetry-0.4.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
"""Four green weeks: is the recorder actually being used, and answered?
|
|
2
|
+
|
|
3
|
+
The beta gate is four consecutive green dogfood weeks. It has never started, and
|
|
4
|
+
the reason is worth naming: there was no way to answer "was this week green?"
|
|
5
|
+
short of a twenty-minute manual checklist, so it never got asked.
|
|
6
|
+
|
|
7
|
+
A week is green on four counts, and each one exists because its absence is a
|
|
8
|
+
failure that looks like success:
|
|
9
|
+
|
|
10
|
+
* **Capture continuity.** Silence is indistinguishable from a quiet week. A
|
|
11
|
+
recorder that was switched off produces exactly the same empty trail as a
|
|
12
|
+
developer on holiday, and only the calendar knows which. Active days are
|
|
13
|
+
counted so a gap has to be explained rather than assumed benign.
|
|
14
|
+
* **Chain integrity.** A trail that no longer verifies is not evidence.
|
|
15
|
+
* **Triage.** Detections raised and never answered mean the loop is running open.
|
|
16
|
+
This is the count that separates a product being *used* from one merely being
|
|
17
|
+
*installed*.
|
|
18
|
+
* **No spool backlog.** A spool that keeps growing means the orchestrator is not
|
|
19
|
+
coming up, and the operator is looking at a healthy trail that is quietly
|
|
20
|
+
missing sessions.
|
|
21
|
+
|
|
22
|
+
The verdict is deliberately harsh about capture gaps and deliberately lenient
|
|
23
|
+
about volume. A slow week is fine. A week the recorder missed is not.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import hashlib
|
|
29
|
+
import json
|
|
30
|
+
from dataclasses import dataclass, field
|
|
31
|
+
from datetime import date, datetime, timedelta, timezone
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
from typing import Any
|
|
34
|
+
|
|
35
|
+
#: A working week, not seven days. Nobody should fail the gate for resting.
|
|
36
|
+
MIN_ACTIVE_DAYS = 3
|
|
37
|
+
|
|
38
|
+
#: Severities that must not be left unanswered.
|
|
39
|
+
MUST_TRIAGE = frozenset({"critical", "high"})
|
|
40
|
+
|
|
41
|
+
MARKER_NAME = "dogfood.json"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def marker_path() -> Path:
|
|
45
|
+
from agentmetry.core.config import settings
|
|
46
|
+
|
|
47
|
+
return Path(settings.audit_db_path).parent / MARKER_NAME
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def read_marker() -> dict[str, Any] | None:
|
|
51
|
+
path = marker_path()
|
|
52
|
+
if not path.is_file():
|
|
53
|
+
return None
|
|
54
|
+
try:
|
|
55
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
56
|
+
except (json.JSONDecodeError, OSError):
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# Files whose contents define what a detection means. A change to any of them
|
|
61
|
+
# changes what the gate is measuring, which is why the fingerprint covers whole
|
|
62
|
+
# sources rather than just the rule id list: today's two fixes changed severities
|
|
63
|
+
# and trait classification without adding or removing a single rule.
|
|
64
|
+
_RULESET_SOURCES = (
|
|
65
|
+
"core/audit/detection/rules.py",
|
|
66
|
+
"core/audit/detection/traits.py",
|
|
67
|
+
"core/audit/detection/engine.py",
|
|
68
|
+
"core/audit/mitre.py",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _package_root() -> Path:
|
|
73
|
+
"""The `agentmetry` package directory. Fingerprinted paths are relative to it."""
|
|
74
|
+
return Path(__file__).resolve().parents[2]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def ruleset_fingerprint() -> str:
|
|
78
|
+
"""A hash of everything that decides whether a detection fires, and how hard.
|
|
79
|
+
|
|
80
|
+
The gate exists to produce a number worth quoting. Four green weeks measured
|
|
81
|
+
against four different rulesets is not that number, and nothing was stopping
|
|
82
|
+
it: three detection changes shipped into week one on the day the clock
|
|
83
|
+
started, each one an improvement, and collectively they made the week
|
|
84
|
+
meaningless.
|
|
85
|
+
|
|
86
|
+
A promise to freeze the rules is the same kind of promise as an install
|
|
87
|
+
command nobody runs. This makes the freeze checkable.
|
|
88
|
+
|
|
89
|
+
Whole-file hashing is deliberately blunt. It flags a comment-only edit as a
|
|
90
|
+
change, which is a small annoyance, and it cannot miss a real one, which is
|
|
91
|
+
the point. Being told to look is cheap; a silently invalidated month is not.
|
|
92
|
+
"""
|
|
93
|
+
digest = hashlib.sha256()
|
|
94
|
+
root = _package_root()
|
|
95
|
+
for rel in _RULESET_SOURCES:
|
|
96
|
+
path = root / rel
|
|
97
|
+
digest.update(rel.encode("utf-8"))
|
|
98
|
+
try:
|
|
99
|
+
digest.update(path.read_bytes())
|
|
100
|
+
except OSError:
|
|
101
|
+
digest.update(b"<unreadable>")
|
|
102
|
+
|
|
103
|
+
# The YAML rules are ruleset too, and they are the half an operator is most
|
|
104
|
+
# likely to edit without thinking of it as changing the product.
|
|
105
|
+
try:
|
|
106
|
+
from agentmetry.core.config import settings
|
|
107
|
+
|
|
108
|
+
manifest = Path(settings.detection_rules_path)
|
|
109
|
+
digest.update(b"detection-manifest")
|
|
110
|
+
digest.update(manifest.read_bytes() if manifest.is_file() else b"<absent>")
|
|
111
|
+
except Exception:
|
|
112
|
+
digest.update(b"<manifest-unavailable>")
|
|
113
|
+
|
|
114
|
+
return digest.hexdigest()
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def start_clock(when: date | None = None, *, operator: str = "") -> dict[str, Any]:
|
|
118
|
+
"""Record the start of the dogfood period.
|
|
119
|
+
|
|
120
|
+
Deliberately an explicit act rather than inferred from the first event. The
|
|
121
|
+
gate is a commitment to watch for four weeks, and a commitment nobody made
|
|
122
|
+
on a particular day is one nobody is keeping.
|
|
123
|
+
|
|
124
|
+
The ruleset fingerprint is recorded here so the commitment includes what was
|
|
125
|
+
being measured, not just when the measuring began.
|
|
126
|
+
"""
|
|
127
|
+
marker = {
|
|
128
|
+
"started_utc": (when or datetime.now(timezone.utc).date()).isoformat(),
|
|
129
|
+
"operator": operator,
|
|
130
|
+
"ruleset_fingerprint": ruleset_fingerprint(),
|
|
131
|
+
"note": "Four consecutive green weeks. See core/audit/dogfood.py for what green means.",
|
|
132
|
+
}
|
|
133
|
+
path = marker_path()
|
|
134
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
135
|
+
path.write_text(json.dumps(marker, indent=2) + "\n", encoding="utf-8")
|
|
136
|
+
return marker
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@dataclass
|
|
140
|
+
class Week:
|
|
141
|
+
index: int
|
|
142
|
+
start: date
|
|
143
|
+
end: date
|
|
144
|
+
events: int = 0
|
|
145
|
+
active_days: int = 0
|
|
146
|
+
sessions: int = 0
|
|
147
|
+
detections: int = 0
|
|
148
|
+
untriaged: int = 0
|
|
149
|
+
reasons: list[str] = field(default_factory=list)
|
|
150
|
+
complete: bool = True
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def green(self) -> bool:
|
|
154
|
+
return not self.reasons
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def verdict(self) -> str:
|
|
158
|
+
if not self.complete:
|
|
159
|
+
return "IN PROGRESS"
|
|
160
|
+
return "GREEN" if self.green else "RED"
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@dataclass
|
|
164
|
+
class DogfoodReport:
|
|
165
|
+
started: date | None
|
|
166
|
+
weeks: list[Week] = field(default_factory=list)
|
|
167
|
+
chain_ok: bool = True
|
|
168
|
+
chain_message: str = ""
|
|
169
|
+
spooled: int = 0
|
|
170
|
+
# False when the detection rules have changed since the clock started, which
|
|
171
|
+
# means the weeks measured different products and cannot be added together.
|
|
172
|
+
ruleset_frozen: bool = True
|
|
173
|
+
|
|
174
|
+
@property
|
|
175
|
+
def consecutive_green(self) -> int:
|
|
176
|
+
"""Green weeks in an unbroken run ending at the most recent complete week."""
|
|
177
|
+
run = 0
|
|
178
|
+
for week in self.weeks:
|
|
179
|
+
if not week.complete:
|
|
180
|
+
continue
|
|
181
|
+
if week.green:
|
|
182
|
+
run += 1
|
|
183
|
+
else:
|
|
184
|
+
run = 0
|
|
185
|
+
return run
|
|
186
|
+
|
|
187
|
+
@property
|
|
188
|
+
def passed(self) -> bool:
|
|
189
|
+
# A changed ruleset does not turn individual weeks red, because the
|
|
190
|
+
# operator's behaviour that week was real and worth recording. It does
|
|
191
|
+
# stop the run adding up to a pass, because four weeks measured against
|
|
192
|
+
# four different rulesets is not four weeks of evidence about one
|
|
193
|
+
# product.
|
|
194
|
+
return self.consecutive_green >= 4 and self.ruleset_frozen
|
|
195
|
+
|
|
196
|
+
def as_dict(self) -> dict[str, Any]:
|
|
197
|
+
return {
|
|
198
|
+
"started": self.started.isoformat() if self.started else None,
|
|
199
|
+
"consecutive_green": self.consecutive_green,
|
|
200
|
+
"required": 4,
|
|
201
|
+
"passed": self.passed,
|
|
202
|
+
"chain_ok": self.chain_ok,
|
|
203
|
+
"chain_message": self.chain_message,
|
|
204
|
+
"spooled": self.spooled,
|
|
205
|
+
"ruleset_frozen": self.ruleset_frozen,
|
|
206
|
+
"weeks": [
|
|
207
|
+
{
|
|
208
|
+
"index": w.index,
|
|
209
|
+
"start": w.start.isoformat(),
|
|
210
|
+
"end": w.end.isoformat(),
|
|
211
|
+
"events": w.events,
|
|
212
|
+
"active_days": w.active_days,
|
|
213
|
+
"sessions": w.sessions,
|
|
214
|
+
"detections": w.detections,
|
|
215
|
+
"untriaged": w.untriaged,
|
|
216
|
+
"complete": w.complete,
|
|
217
|
+
"verdict": w.verdict,
|
|
218
|
+
"reasons": w.reasons,
|
|
219
|
+
}
|
|
220
|
+
for w in self.weeks
|
|
221
|
+
],
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _week_bounds(start: date, index: int) -> tuple[date, date]:
|
|
226
|
+
begin = start + timedelta(days=7 * index)
|
|
227
|
+
return begin, begin + timedelta(days=6)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def assess(
|
|
231
|
+
*,
|
|
232
|
+
trail_db: Any | None = None,
|
|
233
|
+
started: date | None = None,
|
|
234
|
+
today: date | None = None,
|
|
235
|
+
) -> DogfoodReport:
|
|
236
|
+
"""Score every week since the clock started."""
|
|
237
|
+
from agentmetry.core.audit.detection.disposition import get_disposition_store
|
|
238
|
+
|
|
239
|
+
if trail_db is None:
|
|
240
|
+
from agentmetry.core.audit.trail_db import get_trail_db
|
|
241
|
+
|
|
242
|
+
trail_db = get_trail_db()
|
|
243
|
+
|
|
244
|
+
marker = read_marker()
|
|
245
|
+
if started is None and marker:
|
|
246
|
+
started = date.fromisoformat(marker["started_utc"])
|
|
247
|
+
|
|
248
|
+
report = DogfoodReport(started=started)
|
|
249
|
+
# A marker written before fingerprints existed has nothing to compare, and
|
|
250
|
+
# assuming it drifted would be inventing a failure. Absence is not evidence.
|
|
251
|
+
recorded = (marker or {}).get("ruleset_fingerprint")
|
|
252
|
+
if recorded:
|
|
253
|
+
report.ruleset_frozen = recorded == ruleset_fingerprint()
|
|
254
|
+
_attach_health(report)
|
|
255
|
+
if started is None:
|
|
256
|
+
return report
|
|
257
|
+
|
|
258
|
+
now = today or datetime.now(timezone.utc).date()
|
|
259
|
+
events = trail_db.read_between(
|
|
260
|
+
datetime.combine(started, datetime.min.time(), timezone.utc).isoformat(),
|
|
261
|
+
datetime.combine(now + timedelta(days=1), datetime.min.time(), timezone.utc).isoformat(),
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
store = get_disposition_store()
|
|
265
|
+
total_weeks = max(1, ((now - started).days // 7) + 1)
|
|
266
|
+
|
|
267
|
+
for index in range(total_weeks):
|
|
268
|
+
begin, end = _week_bounds(started, index)
|
|
269
|
+
week = Week(index=index + 1, start=begin, end=end, complete=end < now)
|
|
270
|
+
|
|
271
|
+
days: set[str] = set()
|
|
272
|
+
sessions: set[str] = set()
|
|
273
|
+
for event in events:
|
|
274
|
+
stamp = str(event.get("timestamp_utc") or "")[:10]
|
|
275
|
+
if not stamp or not (begin.isoformat() <= stamp <= end.isoformat()):
|
|
276
|
+
continue
|
|
277
|
+
week.events += 1
|
|
278
|
+
days.add(stamp)
|
|
279
|
+
if event.get("correlation_id"):
|
|
280
|
+
sessions.add(str(event["correlation_id"]))
|
|
281
|
+
|
|
282
|
+
action = event.get("action") or {}
|
|
283
|
+
if action.get("type") != "detection":
|
|
284
|
+
continue
|
|
285
|
+
detection = event.get("detection") or {}
|
|
286
|
+
week.detections += 1
|
|
287
|
+
if str(action.get("outcome") or "").lower() not in MUST_TRIAGE:
|
|
288
|
+
continue
|
|
289
|
+
current = store.get(
|
|
290
|
+
str(event.get("correlation_id") or ""),
|
|
291
|
+
str(detection.get("rule_id") or ""),
|
|
292
|
+
)
|
|
293
|
+
if current is None or current["status"] == "new":
|
|
294
|
+
week.untriaged += 1
|
|
295
|
+
|
|
296
|
+
week.active_days = len(days)
|
|
297
|
+
week.sessions = len(sessions)
|
|
298
|
+
|
|
299
|
+
if week.complete:
|
|
300
|
+
if week.active_days < MIN_ACTIVE_DAYS:
|
|
301
|
+
week.reasons.append(
|
|
302
|
+
f"only {week.active_days} active day(s); the recorder was not "
|
|
303
|
+
"running, or was not being used"
|
|
304
|
+
)
|
|
305
|
+
if week.untriaged:
|
|
306
|
+
week.reasons.append(
|
|
307
|
+
f"{week.untriaged} critical/high detection(s) never dispositioned"
|
|
308
|
+
)
|
|
309
|
+
report.weeks.append(week)
|
|
310
|
+
|
|
311
|
+
if not report.chain_ok:
|
|
312
|
+
for week in report.weeks:
|
|
313
|
+
week.reasons.append("trail chain does not verify")
|
|
314
|
+
if report.spooled:
|
|
315
|
+
for week in report.weeks[-1:]:
|
|
316
|
+
week.reasons.append(
|
|
317
|
+
f"{report.spooled} event(s) stuck in the hook spool; the "
|
|
318
|
+
"orchestrator is not draining them"
|
|
319
|
+
)
|
|
320
|
+
return report
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _attach_health(report: DogfoodReport) -> None:
|
|
324
|
+
from agentmetry.core.config import settings
|
|
325
|
+
|
|
326
|
+
trail = Path(settings.audit_export_path)
|
|
327
|
+
if trail.is_file():
|
|
328
|
+
from agentmetry.core.audit.trail_chain import verify_trail_file
|
|
329
|
+
|
|
330
|
+
result = verify_trail_file(trail)
|
|
331
|
+
report.chain_ok = result.ok
|
|
332
|
+
report.chain_message = result.message
|
|
333
|
+
else:
|
|
334
|
+
# No trail file is not a broken chain. On day one there is nothing to
|
|
335
|
+
# verify, and failing the gate for that would make the first week
|
|
336
|
+
# unpassable by construction. A week with no events is already caught by
|
|
337
|
+
# the active-days check, which is the honest place for it.
|
|
338
|
+
report.chain_ok = True
|
|
339
|
+
report.chain_message = "no trail file yet"
|
|
340
|
+
|
|
341
|
+
try:
|
|
342
|
+
from agentmetry.core.audit.spool import read_spool, spool_path
|
|
343
|
+
|
|
344
|
+
path = spool_path()
|
|
345
|
+
if path.is_file():
|
|
346
|
+
payloads, _dropped = read_spool(path)
|
|
347
|
+
report.spooled = len(payloads)
|
|
348
|
+
except Exception:
|
|
349
|
+
report.spooled = 0
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def render(report: DogfoodReport) -> str:
|
|
353
|
+
if report.started is None:
|
|
354
|
+
return (
|
|
355
|
+
"Dogfood clock has not started.\n\n"
|
|
356
|
+
" Start it with: agentmetry dogfood --start\n\n"
|
|
357
|
+
" Four consecutive green weeks is the beta gate. A week is green when\n"
|
|
358
|
+
" the recorder ran on at least three days, the trail chain verifies,\n"
|
|
359
|
+
" every critical or high detection was dispositioned, and no events\n"
|
|
360
|
+
" are stuck in the hook spool.\n"
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
lines = [
|
|
364
|
+
f"Dogfood clock started {report.started.isoformat()}",
|
|
365
|
+
"",
|
|
366
|
+
f" {'Week':<6}{'Dates':<26}{'Days':>5}{'Events':>9}{'Sessions':>10}"
|
|
367
|
+
f"{'Detect':>8}{'Untriaged':>11} Verdict",
|
|
368
|
+
]
|
|
369
|
+
for week in report.weeks:
|
|
370
|
+
lines.append(
|
|
371
|
+
f" {week.index:<6}"
|
|
372
|
+
f"{week.start.isoformat()} to {week.end.isoformat():<7}"
|
|
373
|
+
f"{week.active_days:>5}{week.events:>9}{week.sessions:>10}"
|
|
374
|
+
f"{week.detections:>8}{week.untriaged:>11} {week.verdict}"
|
|
375
|
+
)
|
|
376
|
+
for reason in week.reasons:
|
|
377
|
+
lines.append(f" - {reason}")
|
|
378
|
+
|
|
379
|
+
lines += ["", f" Consecutive green weeks: {report.consecutive_green} of 4"]
|
|
380
|
+
if report.passed:
|
|
381
|
+
lines.append(" Gate PASSED.")
|
|
382
|
+
else:
|
|
383
|
+
remaining = 4 - report.consecutive_green
|
|
384
|
+
lines.append(f" {remaining} more green week(s) needed.")
|
|
385
|
+
|
|
386
|
+
if not report.ruleset_frozen:
|
|
387
|
+
lines += [
|
|
388
|
+
"",
|
|
389
|
+
" RULESET CHANGED since the clock started, so these weeks did not all",
|
|
390
|
+
" measure the same product and the run cannot pass. Land rule changes",
|
|
391
|
+
" and restart the clock: agentmetry dogfood --start --restart",
|
|
392
|
+
]
|
|
393
|
+
|
|
394
|
+
if not report.chain_ok:
|
|
395
|
+
lines.append(f" Trail chain: BROKEN - {report.chain_message}")
|
|
396
|
+
if report.spooled:
|
|
397
|
+
lines.append(f" Spool backlog: {report.spooled} event(s) pending replay")
|
|
398
|
+
return "\n".join(lines) + "\n"
|