Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,35 @@ separately (currently `1.1.0`) and changes additively.

## [Unreleased]

### Added
- **Chinese agent capture (Sprint C).** Kimi `stream-json` print-mode ingest
(`python scripts/agentmetry_ingest.py kimi stream-json`), `session-tool-burst`
and `host-subagent-swarm-burst` detection rules, DashScope-specific DLP,
host-level live detection aggregation, Trae MCP-proxy adapter stub, and
`scripts/install_chinese_hooks.ps1` (all Chinese CLIs in one script).
- **Chinese agent capture (Sprint B).** Qoder and CodeBuddy hook adapters,
`subagent-swarm-burst` detection rule, Chinese provider DLP (Tencent SecretId,
API key assignments, env overrides), CN cloud CLI coverage in
`credential-read-then-cloud-api`, extended tool policy for kimi/qwen/deepseek.
- **Chinese agent capture (Sprint A).** Qwen Code and Kimi Code Tier B hooks:
`map_qwen_hook`, `map_kimi_hook`, `install_qwen_hooks.ps1`, `install_kimi_hooks.ps1`,
dashboard badges, and [`docs/integrations/chinese-agents.md`](docs/integrations/chinese-agents.md).
- **Detection rules (HF July 2026 patterns).** Three sequence rules for agentic
intrusion patterns seen in governed coding-agent sessions:
`credential-read-then-cloud-api`, `dotfile-read-then-git-push`, and
`remote-staging-then-execute`.
- **DLP:** Hugging Face access token pattern (`hf_…`).
- **Docs:** [`docs/compliance/local-llm-forensics.md`](docs/compliance/local-llm-forensics.md)
— forensic playbook for analyzing JSONL trails with a self-hosted model when
commercial APIs block incident payloads (the "asymmetry problem" from Hugging
Face's July 2026 disclosure).

### Changed
- **External ingest.** `qwen`, `kimi`, `crewai`, and `opensre` are first-class
`source_app` values (no longer rewritten to `cursor` in canonical events).
- **MITRE credential paths:** `.docker/config.json` and `.config/gcloud` upgrade
file reads to T1552 (Credentials In Files).

## [0.2.0] - 2026-07-19

First tagged public-alpha release. A local-first flight recorder and mini-SIEM
Expand Down
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ No server, no API key, no config. Clone and run:
git clone https://github.com/blitzcrieg1/agentmetry.git && cd agentmetry
pip install -e apps/orchestrator
python scripts/demo.py
python scripts/demo.py --scenario hf # HF July 2026 agentic intrusion patterns
```

No single one of those events is an alert. The sequence is. That is the whole
Expand Down Expand Up @@ -257,7 +258,7 @@ Two scope limits worth stating plainly:
```mermaid
flowchart TB
subgraph Capture["Capture Layer (Tier A + B)"]
HOOKS["IDE Lifecycle Hooks<br/>Claude · Cursor · Codex · Antigravity"]
HOOKS["IDE Lifecycle Hooks<br/>Claude · Cursor · Codex · Antigravity · Qwen · Kimi"]
PROXY["MCP Audit Proxy<br/>mcp_audit_proxy.py"]
end

Expand Down Expand Up @@ -393,7 +394,7 @@ Agentmetry records agents you wire in — **IDE hooks** or the **MCP proxy**. It

| Category | Supported today | Roadmap |
| -------- | --------------- | ------- |
| **IDE / Agent hosts** | Claude · Cursor · Codex · Antigravity | Windsurf · VS Code Copilot |
| **IDE / Agent hosts** | Claude · Cursor · Codex · Antigravity · [Qwen · Kimi · Qoder · CodeBuddy](docs/integrations/chinese-agents.md) | Windsurf · VS Code Copilot |
| **Agent frameworks** | [CrewAI](adapters/crewai/) · [OpenSRE](adapters/opensre/) | LangChain · AutoGen |
| **MCP transport** | Stdio audit proxy (wrap any MCP server command) | SSE / streamable HTTP proxy |
| **Observability / SIEM** | Loki · Grafana · Elastic ECS · Splunk HEC · generic webhook | Datadog · New Relic |
Expand Down Expand Up @@ -460,6 +461,10 @@ sequenceDiagram
| Rule ID | Severity | Pattern |
| ------- | -------- | ------- |
| `credential-exfil` | critical | Credential access (T1552) → network egress (TA0011) |
| `credential-read-then-cloud-api` | critical | Credential access (T1552) → kubectl / aws / gcloud / az / HF CLI |
| `dotfile-read-then-git-push` | critical | Credential access (T1552) → `git push` or `gh repo create` |
| `remote-staging-then-execute` | critical | Fetch from public staging host (gist, HF raw, GitHub raw) → execute in a later step |
| `subagent-swarm-burst` | high | ≥5 subagent starts in one session (Kimi AgentSwarm, Qwen Agent Teams) |
| `approval-denied-then-executed` | critical | Human denied a gated tool → same tool executed successfully later |
| `encoded-command-download` | critical | Remote code fetched and executed: a raw-IP download, or a fetch piped into an interpreter (`curl … \| bash`). T1105, plus T1027 when base64-encoded |
| `pr-merged-without-review` | critical | A pull request merged with no preceding read of its diff (T1195.002) |
Expand Down
4 changes: 3 additions & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ item matters to you.
- **Correlated detection** — sequence-rule engine: `credential-exfil`,
`approval-denied-then-executed`, `encoded-command-download`,
`pr-merged-without-review`, `untrusted-input-then-risky-action`,
`credential-read-then-cloud-api`, `dotfile-read-then-git-push`,
`remote-staging-then-execute`,
`destructive-delete-burst`, `autonomous-unapproved-write`,
`discovery-then-collect`, and opt-in `off-hours-activity`. Includes Agent Data
Injection chains ([arXiv:2607.05120](https://arxiv.org/abs/2607.05120)).
Expand Down Expand Up @@ -72,7 +74,7 @@ item matters to you.
| P0 | ~~**Tool allow/deny policy YAML**~~ | `policies/tool/manifest.yaml` | ✓ Hook enforcement + tests |
| P1 | ~~**Dogfood metrics in CLI/dashboard**~~ | `agentmetry stats --days 7` + Analytics tab | ✓ Weekly events/detections/denials |
| P1 | **Marketing site polish** | `ai-audit-watch` | Distinct detection screenshot (today it duplicates the hero); no “AI Audit Watch” residue. Canonical URL shipped; agentmetry.ai is the only domain |
| P1 | **More detection rules** | `core/audit/detection/rules.py` | Community asks: dotfile read → remote push; consecutive writes outside project root (`~/.ssh`, `.env`). Also rapid-fire denials, package-install tampering (~20 lines + tests each) |
| P1 | **More detection rules** | `core/audit/detection/rules.py` | ~~Dotfile read → remote push~~ ✓ (`dotfile-read-then-git-push`). Remaining: consecutive writes outside project root; rapid-fire denials; package-install tampering |

---

Expand Down
19 changes: 19 additions & 0 deletions adapters/qoder/settings.agentmetry.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"hooks": {
"SessionStart": [
{ "hooks": [ { "type": "command", "command": "python scripts/agentmetry_ingest.py qoder hook SessionStart", "timeout": 10 } ] }
],
"PreToolUse": [
{ "hooks": [ { "type": "command", "command": "python scripts/agentmetry_ingest.py qoder hook PreToolUse", "timeout": 10 } ] }
],
"PostToolUse": [
{ "hooks": [ { "type": "command", "command": "python scripts/agentmetry_ingest.py qoder hook PostToolUse", "timeout": 10 } ] }
],
"SubagentStart": [
{ "hooks": [ { "type": "command", "command": "python scripts/agentmetry_ingest.py qoder hook SubagentStart", "timeout": 10 } ] }
],
"Stop": [
{ "hooks": [ { "type": "command", "command": "python scripts/agentmetry_ingest.py qoder hook Stop", "timeout": 10 } ] }
]
}
}
22 changes: 22 additions & 0 deletions adapters/qwen/settings.agentmetry.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"hooks": {
"SessionStart": [
{ "hooks": [ { "type": "command", "command": "python scripts/agentmetry_ingest.py qwen hook SessionStart", "timeout": 10 } ] }
],
"SessionEnd": [
{ "hooks": [ { "type": "command", "command": "python scripts/agentmetry_ingest.py qwen hook SessionEnd", "timeout": 10 } ] }
],
"PreToolUse": [
{ "hooks": [ { "type": "command", "command": "python scripts/agentmetry_ingest.py qwen hook PreToolUse", "timeout": 10 } ] }
],
"PostToolUse": [
{ "hooks": [ { "type": "command", "command": "python scripts/agentmetry_ingest.py qwen hook PostToolUse", "timeout": 10 } ] }
],
"PostToolUseFailure": [
{ "hooks": [ { "type": "command", "command": "python scripts/agentmetry_ingest.py qwen hook PostToolUseFailure", "timeout": 10 } ] }
],
"Stop": [
{ "hooks": [ { "type": "command", "command": "python scripts/agentmetry_ingest.py qwen hook Stop", "timeout": 10 } ] }
]
}
}
22 changes: 22 additions & 0 deletions adapters/trae/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Trae (ByteDance) — partial Tier B coverage

[Trae](https://www.trae.ai/) does **not** ship lifecycle hooks yet
([trae-agent #397](https://github.com/bytedance/trae-agent/issues/397)).

## Today

Wrap MCP servers with `tools/mcp_audit_proxy.py` and set:

```powershell
$env:AGENTMETRY_SOURCE_APP="trae"
python tools/mcp_audit_proxy.py --server "your-mcp-command"
```

Events arrive as `source.app: trae` via the MCP proxy adapter.

## When ByteDance ships hooks

Follow the Qwen/Kimi pattern in `core/audit/hook_bootstrap.py`:
Claude-family JSON on stdin → `scripts/agentmetry_ingest.py trae hook <Event>`.

Track: [docs/integrations/chinese-agents.md](../../docs/integrations/chinese-agents.md)
35 changes: 35 additions & 0 deletions apps/dashboard/lib/audit-source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ export const SOURCE_LABELS: Record<string, string> = {
codex: "Codex",
antigravity: "Antigravity",
mcp_proxy: "MCP",
qwen: "Qwen Code",
kimi: "Kimi Code",
qoder: "Qoder",
codebuddy: "CodeBuddy",
trae: "Trae",
crewai: "CrewAI",
opensre: "OpenSRE",
};

/** Light + dark Tailwind classes for source badges in the event feed. */
Expand All @@ -30,6 +37,20 @@ const SOURCE_BADGE_CLASS: Record<string, string> = {
"bg-fuchsia-100 text-fuchsia-900 ring-1 ring-fuchsia-500/25 dark:bg-fuchsia-950/50 dark:text-fuchsia-200",
mcp_proxy:
"bg-orange-100 text-orange-900 ring-1 ring-orange-500/25 dark:bg-orange-950/50 dark:text-orange-200",
qwen:
"bg-rose-100 text-rose-900 ring-1 ring-rose-500/25 dark:bg-rose-950/50 dark:text-rose-200",
kimi:
"bg-indigo-100 text-indigo-900 ring-1 ring-indigo-500/25 dark:bg-indigo-950/50 dark:text-indigo-200",
qoder:
"bg-orange-100 text-orange-950 ring-1 ring-orange-500/25 dark:bg-orange-950/50 dark:text-orange-200",
codebuddy:
"bg-sky-100 text-sky-900 ring-1 ring-sky-500/25 dark:bg-sky-950/50 dark:text-sky-200",
trae:
"bg-red-100 text-red-900 ring-1 ring-red-500/25 dark:bg-red-950/50 dark:text-red-200",
crewai:
"bg-lime-100 text-lime-900 ring-1 ring-lime-500/25 dark:bg-lime-950/50 dark:text-lime-200",
opensre:
"bg-teal-100 text-teal-900 ring-1 ring-teal-500/25 dark:bg-teal-950/50 dark:text-teal-200",
};

const SOURCE_DOT_CLASS: Record<string, string> = {
Expand All @@ -39,6 +60,13 @@ const SOURCE_DOT_CLASS: Record<string, string> = {
codex: "bg-cyan-400",
antigravity: "bg-fuchsia-400",
mcp_proxy: "bg-orange-400",
qwen: "bg-rose-400",
kimi: "bg-indigo-400",
qoder: "bg-orange-400",
codebuddy: "bg-sky-400",
trae: "bg-red-400",
crewai: "bg-lime-400",
opensre: "bg-teal-400",
};

/** Hex fills for Recharts source breakdown. */
Expand All @@ -49,6 +77,13 @@ export const SOURCE_CHART_COLOR: Record<string, string> = {
codex: "#22d3ee",
antigravity: "#e879f9",
mcp_proxy: "#fb923c",
qwen: "#fb7185",
kimi: "#818cf8",
qoder: "#fb923c",
codebuddy: "#38bdf8",
trae: "#f87171",
crewai: "#a3e635",
opensre: "#2dd4bf",
};

const DEFAULT_BADGE_CLASS =
Expand Down
5 changes: 4 additions & 1 deletion apps/orchestrator/api/routes/audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@ class IngestToolBody(BaseModel):
class ExternalIngestBody(BaseModel):
"""Adapter payload — normalized to canonical v1.1 on ingest."""

source_app: str = Field(..., description="cursor | claude | antigravity | codex | mcp_proxy")
source_app: str = Field(
...,
description="cursor | claude | antigravity | codex | mcp_proxy | qwen | kimi | qoder | codebuddy | trae | crewai | opensre",
)
event_type: str = Field(
...,
description="session_start | session_end | tool_called | tool_denied | tool_failed | approval_request | approval_response",
Expand Down
12 changes: 11 additions & 1 deletion apps/orchestrator/core/audit/detection/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from typing import Any

from .models import SEVERITY_RANK, Detection
from .rules import REGISTRY
from .rules import HOST_REGISTRY, REGISTRY

_EPOCH = datetime.min.replace(tzinfo=timezone.utc)

Expand Down Expand Up @@ -44,3 +44,13 @@ def run_detections(events: list[dict[str, Any]]) -> list[Detection]:
detections.extend(rule(ordered))
detections.sort(key=lambda d: (SEVERITY_RANK.get(d.severity, 99), d.first_seen_utc))
return detections


def run_host_detections(events: list[dict[str, Any]]) -> list[Detection]:
"""Run host-scoped rules over events aggregated by host_id."""
ordered = _sorted(events)
detections: list[Detection] = []
for rule in HOST_REGISTRY:
detections.extend(rule(ordered))
detections.sort(key=lambda d: (SEVERITY_RANK.get(d.severity, 99), d.first_seen_utc))
return detections
28 changes: 27 additions & 1 deletion apps/orchestrator/core/audit/detection/live.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
import uuid
from typing import Any

from .engine import run_detections
from .engine import run_detections, run_host_detections
from .live_store import get_live_store
from .models import Detection

Expand Down Expand Up @@ -61,6 +61,32 @@ def observe(canonical: dict[str, Any]) -> list[Detection]:
return fresh


def observe_host(canonical: dict[str, Any]) -> list[Detection]:
"""Record an event in the host-level window and return new host-scoped detections."""
host_id = str(canonical.get("host_id") or "")
if not host_id:
return []

store = get_live_store()
events = store.append_host_event(host_id, canonical)

fresh: list[Detection] = []
seen_in_batch: set[str] = set()
for detection in run_host_detections(events):
if detection.rule_id in seen_in_batch or store.is_host_emitted(host_id, detection.rule_id):
continue
seen_in_batch.add(detection.rule_id)
fresh.append(detection)
return fresh


def mark_host_detection_emitted(host_id: str, rule_id: str, emitted_at: str = "") -> None:
"""Checkpoint a host-level detection as emitted."""
if not host_id or not rule_id:
return
get_live_store().mark_host_emitted(host_id, rule_id, emitted_at=emitted_at)


def mark_detection_emitted(correlation_id: str, rule_id: str, emitted_at: str = "") -> None:
"""Checkpoint a detection as emitted for its session.

Expand Down
Loading
Loading