agentmetry 0.6.0__tar.gz → 0.8.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.
- {agentmetry-0.6.0 → agentmetry-0.8.0}/PKG-INFO +3 -3
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/api/routes/audit.py +55 -2
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/cli/__init__.py +92 -1
- agentmetry-0.8.0/agentmetry/core/audit/detection/corpus/benign_env_example_read.jsonl +4 -0
- agentmetry-0.8.0/agentmetry/core/audit/detection/corpus/benign_fetch_piped_into_own_script.jsonl +3 -0
- agentmetry-0.8.0/agentmetry/core/audit/detection/corpus/benign_github_raw_docs_then_script.jsonl +3 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/detection/corpus/benign_loopback_pipe_to_interpreter.jsonl +3 -3
- agentmetry-0.8.0/agentmetry/core/audit/detection/corpus/benign_mcp_text_mentions_credential_path.jsonl +3 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/detection/corpus/corpus.yaml +42 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/detection/rules.py +31 -2
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/detection/traits.py +42 -5
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/ingest.py +40 -7
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/mitre.py +42 -2
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/diagnostics/mcp_schema.py +294 -3
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/version.py +1 -1
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/policies/dlp/manifest.yaml +26 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/pyproject.toml +7 -2
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_cli_commands.py +242 -0
- agentmetry-0.8.0/tests/test_cli_env_var_names.py +90 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_detection_adi.py +36 -1
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_detection_rule_identity.py +16 -4
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_dlp_scanner.py +49 -0
- agentmetry-0.8.0/tests/test_funding_manifest.py +96 -0
- agentmetry-0.8.0/tests/test_identity_enrich.py +348 -0
- agentmetry-0.8.0/tests/test_ingest_roundtrip.py +163 -0
- agentmetry-0.8.0/tests/test_mcp_concealed_text.py +302 -0
- agentmetry-0.8.0/tests/test_mcp_meta_fingerprint.py +203 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_mcp_schema.py +26 -5
- agentmetry-0.8.0/tests/test_powershell_encoding.py +102 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_readme_claims.py +43 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_sigma_pack.py +6 -1
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tools/generate_sigma_pack.py +6 -1
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tools/mcp_audit_proxy.py +6 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/.dockerignore +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/.env.agentmetry-demo +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/.env.example +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/.gitignore +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/Dockerfile +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/README.md +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/__init__.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/api/__init__.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/api/main.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/api/routes/__init__.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/api/websocket.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/api/ws_bridge.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/cli/__main__.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/__init__.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/__init__.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/adapters/__init__.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/adapters/agt.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/adapters/chronicle.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/adapters/cloudevents.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/adapters/ecs.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/adapters/splunk.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/alerts.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/atlas.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/canonical.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/compliance_digest.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/detection/__init__.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/detection/benchmark.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/detection/corpus/attack_approval_denied_then_executed.jsonl +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/detection/corpus/attack_arbitrary_host_stage_execute.jsonl +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/detection/corpus/attack_autonomous_unapproved_write.jsonl +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/detection/corpus/attack_credential_exfil.jsonl +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/detection/corpus/attack_credential_then_cloud_api.jsonl +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/detection/corpus/attack_destructive_delete_burst.jsonl +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/detection/corpus/attack_discovery_then_collect.jsonl +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/detection/corpus/attack_dotfile_then_git_push.jsonl +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/detection/corpus/attack_encoded_command_download.jsonl +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/detection/corpus/attack_env_credential_exfil.jsonl +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/detection/corpus/attack_hashed_only_no_command.jsonl +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/detection/corpus/attack_interpreter_egress.jsonl +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/detection/corpus/attack_pr_merged_without_review.jsonl +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/detection/corpus/attack_proc_substitution_cradle.jsonl +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/detection/corpus/attack_remote_pipe_to_shell.jsonl +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/detection/corpus/attack_remote_staging_then_execute.jsonl +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/detection/corpus/attack_secret_manager_then_egress.jsonl +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/detection/corpus/attack_session_tool_burst.jsonl +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/detection/corpus/attack_single_command_exfil.jsonl +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/detection/corpus/attack_single_quoted_interpreter_egress.jsonl +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/detection/corpus/attack_ssh_directory_exfil.jsonl +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/detection/corpus/attack_subagent_swarm.jsonl +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/detection/corpus/attack_timestamp_collision.jsonl +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/detection/corpus/attack_untrusted_input_then_action.jsonl +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/detection/corpus/benign_authoring_merge_fixtures.jsonl +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/detection/corpus/benign_autonomous_after_approval.jsonl +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/detection/corpus/benign_build_artifact_cleanup.jsonl +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/detection/corpus/benign_ci_artifact_download.jsonl +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/detection/corpus/benign_commit_message_names_cloud_clis.jsonl +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/detection/corpus/benign_database_migration.jsonl +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/detection/corpus/benign_dependency_install_and_build.jsonl +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/detection/corpus/benign_download_release_archive.jsonl +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/detection/corpus/benign_fetch_data_then_run_repo_script.jsonl +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/detection/corpus/benign_fetch_dataset_then_analyse.jsonl +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/detection/corpus/benign_fetch_lockfile_then_install.jsonl +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/detection/corpus/benign_git_review_and_push.jsonl +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/detection/corpus/benign_human_driven_deletes.jsonl +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/detection/corpus/benign_local_api_probing.jsonl +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/detection/corpus/benign_long_but_calm_session.jsonl +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/detection/corpus/benign_loopback_is_not_egress.jsonl +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/detection/corpus/benign_ordinary_development.jsonl +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/detection/corpus/benign_package_manager_after_fetch.jsonl +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/detection/corpus/benign_reading_config_that_is_not_secret.jsonl +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/detection/corpus/benign_remote_api_call_no_credentials.jsonl +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/detection/corpus/benign_research_then_docs.jsonl +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/detection/corpus/benign_reversed_order_is_not_exfil.jsonl +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/detection/corpus/benign_secret_names_and_docs.jsonl +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/detection/corpus/benign_test_and_fix_loop.jsonl +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/detection/corpus/benign_writing_about_credentials.jsonl +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/detection/disposition.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/detection/engine.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/detection/live.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/detection/live_store.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/detection/models.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/detection/yaml_config.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/detection/yaml_rules.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/dlp/__init__.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/dlp/loader.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/dlp/models.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/dlp/scanner.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/dogfood.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/evidence_pack.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/external.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/hashing.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/heartbeat.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/hook_bootstrap.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/identity.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/migrate.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/policy.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/redaction.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/replay.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/run_context.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/sinks.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/spool.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/tool_policy/__init__.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/tool_policy/evaluator.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/tool_policy/loader.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/tool_policy/models.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/trail_anchor.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/trail_chain.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/trail_db.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/audit/trail_merkle.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/auth.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/bus/__init__.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/bus/audit_exporter.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/bus/bridges.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/bus/bus.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/bus/events.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/bus/outbox.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/config.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/diagnostics/__init__.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/diagnostics/autostart.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/diagnostics/doctor.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/diagnostics/driver_paths.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/diagnostics/env_file.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/diagnostics/hook_coverage.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/diagnostics/mcp_inventory.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/drivers/__init__.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/drivers/host.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/drivers/permissions.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/drivers/spec.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/extensions.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/core/health.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/hooks/__init__.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/hooks/ingest.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/policies/detection/manifest.yaml +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/policies/opa/agent_rules.rego +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/agentmetry/policies/tool/manifest.yaml +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/conftest.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/fixtures/agt_filesink_exfil.jsonl +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/fixtures/agt_filesink_mixed.jsonl +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/fixtures/fake_mcp_server.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_agentmetry_audit.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_agentmetry_ingest_client.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_agt_adapter.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_atlas_detection_enrichment.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_atlas_mapping.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_audit_sinks.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_audit_stats.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_audit_tail.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_auth.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_autostart.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_boot_sequence.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_burst_windows.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_chinese_agent_hooks.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_chinese_agent_sprint_b.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_chinese_agent_sprint_c.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_chronicle_adapter.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_cli_backup.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_cloudevents_adapter.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_compliance_digest.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_crewai_adapter.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_detection_benchmark.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_detection_default_config.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_detection_disposition.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_detection_disposition_api.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_detection_duplicate_emission.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_detection_emit_durability.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_detection_engine.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_detection_evasion.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_detection_hf_incident.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_detection_quoted_command_words.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_detection_rules_v2.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_dlp_agent_env_override.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_dlp_invisible_unicode.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_dlp_markdown_exfil.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_doctor.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_dogfood.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_dogfood_freeze.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_driver_paths.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_ecs_threat.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_event_bus.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_evidence_pack.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_extensions.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_external_ingest.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_forwarding_shape.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_heartbeat.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_hook_bootstrap.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_hook_coverage.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_hook_enforcement_timing.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_hook_spool.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_isolation.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_launch_targets.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_live_detection.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_live_detection_store.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_mcp_audit_proxy.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_mcp_inventory.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_mitre_dlp.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_opensre_adapter.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_redaction.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_replay.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_static_serving.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_subagent_lifecycle.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_tool_policy.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_tool_policy_agent_cli.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_tool_policy_git_hooks.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_trail_anchor.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_trail_chain.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_trail_concurrency.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_trail_merkle.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_unattended_agent_policy.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_version.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tests/test_yaml_detection_rules.py +0 -0
- {agentmetry-0.6.0 → agentmetry-0.8.0}/tools/vault_fs_server.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: agentmetry
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: Local-first flight recorder for AI coding agents: hash-chained audit trail, MITRE-mapped sequence detection, DLP
|
|
5
5
|
Project-URL: Homepage, https://agentmetry.ai
|
|
6
6
|
Project-URL: Source, https://github.com/blitzcrieg1/agentmetry
|
|
@@ -25,7 +25,7 @@ Requires-Python: >=3.11
|
|
|
25
25
|
Requires-Dist: aiosqlite>=0.20.0
|
|
26
26
|
Requires-Dist: fastapi>=0.115.0
|
|
27
27
|
Requires-Dist: httpx>=0.28.0
|
|
28
|
-
Requires-Dist: mcp
|
|
28
|
+
Requires-Dist: mcp<2,>=1.2
|
|
29
29
|
Requires-Dist: pydantic-settings>=2.6.0
|
|
30
30
|
Requires-Dist: pydantic>=2.9.0
|
|
31
31
|
Requires-Dist: pyyaml>=6.0
|
|
@@ -37,7 +37,7 @@ Provides-Extra: dev
|
|
|
37
37
|
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
38
38
|
Requires-Dist: pytest-cov>=6.0; extra == 'dev'
|
|
39
39
|
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
40
|
-
Requires-Dist: ruff==0.16.
|
|
40
|
+
Requires-Dist: ruff==0.16.6; extra == 'dev'
|
|
41
41
|
Description-Content-Type: text/markdown
|
|
42
42
|
|
|
43
43
|
# Agentmetry
|
|
@@ -8,7 +8,7 @@ from typing import Any, Literal
|
|
|
8
8
|
|
|
9
9
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
10
10
|
from fastapi.responses import FileResponse
|
|
11
|
-
from pydantic import BaseModel, Field
|
|
11
|
+
from pydantic import BaseModel, Field, field_validator
|
|
12
12
|
|
|
13
13
|
from agentmetry.core.auth import require_api_key
|
|
14
14
|
from agentmetry.core.audit.detection.disposition import STATUSES, get_disposition_store
|
|
@@ -42,8 +42,48 @@ class IngestToolBody(BaseModel):
|
|
|
42
42
|
mitre: dict[str, str] | None = None
|
|
43
43
|
|
|
44
44
|
|
|
45
|
+
#: Actor kinds a capture surface may assert. Anything else is coerced to
|
|
46
|
+
#: `agent`, which is the safe direction: never silently promoted to `human`
|
|
47
|
+
#: (whose approvals reset detection gates) and never to `autonomous` (which
|
|
48
|
+
#: `autonomous-unapproved-write` keys on). A surface that needs a new kind adds
|
|
49
|
+
#: it here deliberately rather than by sending a novel string.
|
|
50
|
+
_ACTOR_TYPES = frozenset({"human", "agent", "autonomous", "system"})
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class IngestInitiatorBody(BaseModel):
|
|
54
|
+
"""Who triggered this call, as the capture surface saw it.
|
|
55
|
+
|
|
56
|
+
This is not identity. `identity_fields()` stamps `host_id` and `fleet_id` on
|
|
57
|
+
the receiving orchestrator whatever a client sends, and that lockdown is
|
|
58
|
+
unchanged here: this field says what *kind* of actor acted, never which
|
|
59
|
+
machine it was. `external.py` already reads and honours it; without the
|
|
60
|
+
field declared, pydantic dropped it before that code ever saw it.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
actor_type: str = "human"
|
|
64
|
+
trigger: str = "manual"
|
|
65
|
+
operator_id: str = ""
|
|
66
|
+
|
|
67
|
+
@field_validator("actor_type")
|
|
68
|
+
@classmethod
|
|
69
|
+
def _known_actor(cls, v: str) -> str:
|
|
70
|
+
return v if v in _ACTOR_TYPES else "agent"
|
|
71
|
+
|
|
72
|
+
|
|
45
73
|
class ExternalIngestBody(BaseModel):
|
|
46
|
-
"""Adapter payload — normalized to canonical v1.
|
|
74
|
+
"""Adapter payload — normalized to canonical v1.2 on ingest.
|
|
75
|
+
|
|
76
|
+
Every field a capture surface sends must be declared here. Pydantic ignores
|
|
77
|
+
what it does not know, silently, so an undeclared field is not an error at
|
|
78
|
+
ingest: it is a feature that works in unit tests and disappears over HTTP.
|
|
79
|
+
|
|
80
|
+
That has now happened twice. `traits` and `mitre` were dropped, which made
|
|
81
|
+
hashed-only events invisible to every command rule. Then 0.6.0 shipped MCP
|
|
82
|
+
per-tool digests and the initialize handshake, and those were dropped too,
|
|
83
|
+
so the release headline did not survive the wire. `test_ingest_roundtrip.py`
|
|
84
|
+
exists so there is not a third time: it builds real proxy payloads and fails
|
|
85
|
+
if any key does not survive this model.
|
|
86
|
+
"""
|
|
47
87
|
|
|
48
88
|
source_app: str = Field(
|
|
49
89
|
...,
|
|
@@ -71,10 +111,23 @@ class ExternalIngestBody(BaseModel):
|
|
|
71
111
|
# pydantic drops it and a `log`-mode match is silently lost.
|
|
72
112
|
dlp: dict[str, Any] | None = None
|
|
73
113
|
tool_policy: dict[str, Any] | None = None
|
|
114
|
+
# Who the capture surface says triggered this. Honoured by external.py.
|
|
115
|
+
initiator: IngestInitiatorBody | None = None
|
|
74
116
|
# Compact `tools/list` fingerprint from mcp_audit_proxy. Hash only: the
|
|
75
117
|
# description never leaves the proxy process.
|
|
76
118
|
schema_fingerprint: str = ""
|
|
77
119
|
schema_tool_count: int = 0
|
|
120
|
+
# 0.6.0 MCP fields. Per-tool digests so a schema move names the tool that
|
|
121
|
+
# moved rather than only the server, and the two initialize handshake
|
|
122
|
+
# fields that separate a shipped release from a rug pull. All hashes and
|
|
123
|
+
# flags: no tool name, no description, nothing model-visible.
|
|
124
|
+
schema_tool_digests: dict[str, str] = Field(default_factory=dict)
|
|
125
|
+
# Concealed-character counts per category. Declared here because pydantic
|
|
126
|
+
# drops what it does not know, silently, which has already cost this file
|
|
127
|
+
# two shipped bugs.
|
|
128
|
+
schema_concealed: dict[str, int] = Field(default_factory=dict)
|
|
129
|
+
server_version: str = ""
|
|
130
|
+
list_changed: bool | None = None
|
|
78
131
|
|
|
79
132
|
|
|
80
133
|
def _parse_event_ts(event: dict[str, Any]) -> datetime | None:
|
|
@@ -34,6 +34,7 @@ _BACKUP_EXCLUDE_DIRS = {"logs"}
|
|
|
34
34
|
logger = logging.getLogger(__name__)
|
|
35
35
|
|
|
36
36
|
_BACKUP_EXCLUDE_SUFFIXES = {".pid"}
|
|
37
|
+
_CLOSING_DISPOSITIONS = ("resolved", "false_positive", "risk_accepted")
|
|
37
38
|
|
|
38
39
|
|
|
39
40
|
def _base_url(port: int, host: str = "127.0.0.1") -> str:
|
|
@@ -41,6 +42,15 @@ def _base_url(port: int, host: str = "127.0.0.1") -> str:
|
|
|
41
42
|
return f"http://{display}:{port}"
|
|
42
43
|
|
|
43
44
|
|
|
45
|
+
def _api_base_url(port: int) -> str:
|
|
46
|
+
return os.environ.get("AGENTMETRY_URL", "").strip().rstrip("/") or _base_url(port)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _api_headers() -> dict[str, str]:
|
|
50
|
+
key = os.environ.get("AGENTMETRY_API_KEY", "").strip()
|
|
51
|
+
return {"X-API-Key": key} if key else {}
|
|
52
|
+
|
|
53
|
+
|
|
44
54
|
def _lan_ip() -> str | None:
|
|
45
55
|
"""Best-effort local IPv4 for phone/LAN access hints."""
|
|
46
56
|
try:
|
|
@@ -219,7 +229,8 @@ def cmd_stats(args: argparse.Namespace) -> int:
|
|
|
219
229
|
return 1
|
|
220
230
|
|
|
221
231
|
if not data.get("enabled", True):
|
|
222
|
-
print("Audit export disabled
|
|
232
|
+
print("Audit export is disabled, so no events are recorded and stats are empty.")
|
|
233
|
+
print("Set AGENTMETRY_AUDIT_EXPORT_ENABLED=1 and restart.")
|
|
223
234
|
return 1
|
|
224
235
|
|
|
225
236
|
days = data.get("window_days", args.days)
|
|
@@ -241,6 +252,76 @@ def cmd_stats(args: argparse.Namespace) -> int:
|
|
|
241
252
|
return 0
|
|
242
253
|
|
|
243
254
|
|
|
255
|
+
def cmd_detections(args: argparse.Namespace) -> int:
|
|
256
|
+
try:
|
|
257
|
+
data = httpx.get(
|
|
258
|
+
f"{_base_url(args.port)}/api/v1/audit/detections/{args.correlation_id}",
|
|
259
|
+
timeout=10.0,
|
|
260
|
+
).json()
|
|
261
|
+
except Exception:
|
|
262
|
+
print("Not running - start Agentmetry first (detections reads via the API).")
|
|
263
|
+
return 1
|
|
264
|
+
if not data.get("enabled", True):
|
|
265
|
+
print("Audit export is disabled, so no sessions are recorded.")
|
|
266
|
+
print("Set AGENTMETRY_AUDIT_EXPORT_ENABLED=1 and restart.")
|
|
267
|
+
return 1
|
|
268
|
+
detections = data.get("detections") or []
|
|
269
|
+
if not detections:
|
|
270
|
+
print(f"No detections for {args.correlation_id}.")
|
|
271
|
+
return 0
|
|
272
|
+
rule_width = max(len("Rule"), *(len(str(d.get("rule_id", ""))) for d in detections))
|
|
273
|
+
severity_width = max(
|
|
274
|
+
len("Severity"), *(len(str(d.get("severity", ""))) for d in detections)
|
|
275
|
+
)
|
|
276
|
+
print(f"{'Rule':<{rule_width}} {'Severity':<{severity_width}} Summary")
|
|
277
|
+
print(f"{'-' * rule_width} {'-' * severity_width} {'-' * len('Summary')}")
|
|
278
|
+
for detection in detections:
|
|
279
|
+
print(
|
|
280
|
+
f"{str(detection.get('rule_id', '')):<{rule_width}} "
|
|
281
|
+
f"{str(detection.get('severity', '')):<{severity_width}} "
|
|
282
|
+
f"{detection.get('summary', '')}"
|
|
283
|
+
)
|
|
284
|
+
return 0
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def cmd_disposition(args: argparse.Namespace) -> int:
|
|
288
|
+
"""Close a detection from the shell, using the same API as the dashboard."""
|
|
289
|
+
note = args.note.strip()
|
|
290
|
+
if args.status in {"false_positive", "risk_accepted"} and not note:
|
|
291
|
+
print(f"--note is required for {args.status}")
|
|
292
|
+
return 1
|
|
293
|
+
|
|
294
|
+
try:
|
|
295
|
+
resp = httpx.post(
|
|
296
|
+
f"{_api_base_url(args.port)}/api/v1/audit/detections/disposition",
|
|
297
|
+
json={
|
|
298
|
+
"correlation_id": args.correlation_id,
|
|
299
|
+
"rule_id": args.rule_id,
|
|
300
|
+
"status": args.status,
|
|
301
|
+
"note": note,
|
|
302
|
+
"decided_by": args.decided_by,
|
|
303
|
+
},
|
|
304
|
+
headers=_api_headers(),
|
|
305
|
+
timeout=10.0,
|
|
306
|
+
)
|
|
307
|
+
except Exception:
|
|
308
|
+
print("Not running - start Agentmetry first (disposition writes via the API).")
|
|
309
|
+
return 1
|
|
310
|
+
|
|
311
|
+
if resp.status_code >= 400:
|
|
312
|
+
try:
|
|
313
|
+
detail = resp.json().get("detail")
|
|
314
|
+
except Exception:
|
|
315
|
+
detail = None
|
|
316
|
+
print(f"FAILED — {detail or resp.text or f'HTTP {resp.status_code}'}")
|
|
317
|
+
return 1
|
|
318
|
+
|
|
319
|
+
current = resp.json().get("disposition", {})
|
|
320
|
+
status = current.get("status", args.status)
|
|
321
|
+
print(f"Disposition set: {args.correlation_id} {args.rule_id} -> {status}")
|
|
322
|
+
return 0
|
|
323
|
+
|
|
324
|
+
|
|
244
325
|
def cmd_logs(args: argparse.Namespace) -> int:
|
|
245
326
|
log = _DATA_DIR / "logs" / "orchestrator.log"
|
|
246
327
|
if not log.exists():
|
|
@@ -1132,6 +1213,14 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
1132
1213
|
sub.add_parser("status", help="orchestrator health and audit export status")
|
|
1133
1214
|
stats = sub.add_parser("stats", help="audit trail metrics for dogfood (events, detections)")
|
|
1134
1215
|
stats.add_argument("--days", type=int, default=7)
|
|
1216
|
+
detections = sub.add_parser("detections", help="list detections for one session")
|
|
1217
|
+
detections.add_argument("correlation_id", help="correlation_id / session id")
|
|
1218
|
+
disposition = sub.add_parser("disposition", help="close an audit detection from the shell")
|
|
1219
|
+
disposition.add_argument("correlation_id", help="session/correlation id that owns the detection")
|
|
1220
|
+
disposition.add_argument("rule_id", help="detection rule id to close")
|
|
1221
|
+
disposition.add_argument("--status", required=True, choices=_CLOSING_DISPOSITIONS)
|
|
1222
|
+
disposition.add_argument("--note", default="")
|
|
1223
|
+
disposition.add_argument("--decided-by", default="")
|
|
1135
1224
|
logs = sub.add_parser("logs", help="tail the orchestrator log")
|
|
1136
1225
|
logs.add_argument("-n", "--lines", type=int, default=50)
|
|
1137
1226
|
logs.add_argument("-f", "--follow", action="store_true")
|
|
@@ -1273,6 +1362,8 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
1273
1362
|
"hook": cmd_hook,
|
|
1274
1363
|
"status": cmd_status,
|
|
1275
1364
|
"stats": cmd_stats,
|
|
1365
|
+
"detections": cmd_detections,
|
|
1366
|
+
"disposition": cmd_disposition,
|
|
1276
1367
|
"logs": cmd_logs,
|
|
1277
1368
|
"backup": cmd_backup,
|
|
1278
1369
|
"restore": cmd_restore,
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
{"action": {"outcome": "success", "reason": "", "type": "tool_called"}, "actor": {"id": "corpus", "role": "agent", "type": "agent"}, "agent": {"name": "claude", "skill_id": ""}, "correlation_id": "fp44-1", "event_id": "fp44-1-0", "host_id": "corpus-host", "initiator": {"actor_type": "agent", "operator_id": "corpus", "trigger": "autonomous"}, "schema_version": "1.2.0", "session_id": "fp44-1", "source": {"app": "claude"}, "timestamp_utc": "2026-08-26T10:00:00+00:00", "tool": {"command": "cat .env.example", "input_hash": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", "qualified": "claude.Bash", "server": "claude"}}
|
|
2
|
+
{"action": {"outcome": "success", "reason": "", "type": "tool_called"}, "actor": {"id": "corpus", "role": "agent", "type": "agent"}, "agent": {"name": "claude", "skill_id": ""}, "correlation_id": "fp44-1", "event_id": "fp44-1-1", "host_id": "corpus-host", "initiator": {"actor_type": "agent", "operator_id": "corpus", "trigger": "autonomous"}, "schema_version": "1.2.0", "session_id": "fp44-1", "source": {"app": "claude"}, "timestamp_utc": "2026-08-26T10:01:00+00:00", "tool": {"command": "cp .env.example .env.local", "input_hash": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", "qualified": "claude.Bash", "server": "claude"}}
|
|
3
|
+
{"action": {"outcome": "success", "reason": "", "type": "tool_called"}, "actor": {"id": "corpus", "role": "agent", "type": "agent"}, "agent": {"name": "claude", "skill_id": ""}, "correlation_id": "fp44-1", "event_id": "fp44-1-2", "host_id": "corpus-host", "initiator": {"actor_type": "agent", "operator_id": "corpus", "trigger": "autonomous"}, "schema_version": "1.2.0", "session_id": "fp44-1", "source": {"app": "claude"}, "timestamp_utc": "2026-08-26T10:02:00+00:00", "tool": {"command": "cat config/.env.template", "input_hash": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", "qualified": "claude.Read", "server": "claude"}}
|
|
4
|
+
{"action": {"outcome": "success", "reason": "", "type": "tool_called"}, "actor": {"id": "corpus", "role": "agent", "type": "agent"}, "agent": {"name": "claude", "skill_id": ""}, "correlation_id": "fp44-1", "event_id": "fp44-1-3", "host_id": "corpus-host", "initiator": {"actor_type": "agent", "operator_id": "corpus", "trigger": "autonomous"}, "schema_version": "1.2.0", "session_id": "fp44-1", "source": {"app": "claude"}, "timestamp_utc": "2026-08-26T10:03:00+00:00", "tool": {"command": "npm run dev", "input_hash": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", "qualified": "claude.Bash", "server": "claude"}}
|
agentmetry-0.8.0/agentmetry/core/audit/detection/corpus/benign_fetch_piped_into_own_script.jsonl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
{"action": {"outcome": "success", "reason": "", "type": "tool_called"}, "actor": {"id": "corpus", "role": "agent", "type": "agent"}, "agent": {"name": "claude", "skill_id": ""}, "correlation_id": "fp50-1", "event_id": "fp50-1-0", "host_id": "corpus-host", "initiator": {"actor_type": "agent", "operator_id": "corpus", "trigger": "autonomous"}, "schema_version": "1.2.0", "session_id": "fp50-1", "source": {"app": "claude"}, "timestamp_utc": "2026-08-26T10:00:00+00:00", "tool": {"command": "curl -s https://api.example.com/schema.json | python -c \"import sys,json;print(len(json.load(sys.stdin)))\"", "input_hash": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", "qualified": "claude.Bash", "server": "claude"}}
|
|
2
|
+
{"action": {"outcome": "success", "reason": "", "type": "tool_called"}, "actor": {"id": "corpus", "role": "agent", "type": "agent"}, "agent": {"name": "claude", "skill_id": ""}, "correlation_id": "fp50-1", "event_id": "fp50-1-1", "host_id": "corpus-host", "initiator": {"actor_type": "agent", "operator_id": "corpus", "trigger": "autonomous"}, "schema_version": "1.2.0", "session_id": "fp50-1", "source": {"app": "claude"}, "timestamp_utc": "2026-08-26T10:02:00+00:00", "tool": {"command": "curl -s https://registry.example.com/v1/index | node -e \"let d='';process.stdin.on('data',c=>d+=c)\"", "input_hash": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", "qualified": "claude.Bash", "server": "claude"}}
|
|
3
|
+
{"action": {"outcome": "success", "reason": "", "type": "tool_called"}, "actor": {"id": "corpus", "role": "agent", "type": "agent"}, "agent": {"name": "claude", "skill_id": ""}, "correlation_id": "fp50-1", "event_id": "fp50-1-2", "host_id": "corpus-host", "initiator": {"actor_type": "agent", "operator_id": "corpus", "trigger": "autonomous"}, "schema_version": "1.2.0", "session_id": "fp50-1", "source": {"app": "claude"}, "timestamp_utc": "2026-08-26T10:04:00+00:00", "tool": {"command": "pytest -q", "input_hash": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", "qualified": "claude.Bash", "server": "claude"}}
|
agentmetry-0.8.0/agentmetry/core/audit/detection/corpus/benign_github_raw_docs_then_script.jsonl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
{"action": {"outcome": "success", "reason": "", "type": "tool_called"}, "actor": {"id": "corpus", "role": "agent", "type": "agent"}, "agent": {"name": "claude", "skill_id": ""}, "correlation_id": "fp51-1", "event_id": "fp51-1-0", "host_id": "corpus-host", "initiator": {"actor_type": "agent", "operator_id": "corpus", "trigger": "autonomous"}, "schema_version": "1.2.0", "session_id": "fp51-1", "source": {"app": "claude"}, "timestamp_utc": "2026-08-26T10:00:00+00:00", "tool": {"command": "curl -s https://raw.githubusercontent.com/psf/requests/main/README.md", "input_hash": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", "qualified": "claude.Bash", "server": "claude"}}
|
|
2
|
+
{"action": {"outcome": "success", "reason": "", "type": "tool_called"}, "actor": {"id": "corpus", "role": "agent", "type": "agent"}, "agent": {"name": "claude", "skill_id": ""}, "correlation_id": "fp51-1", "event_id": "fp51-1-1", "host_id": "corpus-host", "initiator": {"actor_type": "agent", "operator_id": "corpus", "trigger": "autonomous"}, "schema_version": "1.2.0", "session_id": "fp51-1", "source": {"app": "claude"}, "timestamp_utc": "2026-08-26T10:06:00+00:00", "tool": {"command": "python -c \"import json;print(json.dumps({'ok':True}))\"", "input_hash": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", "qualified": "claude.Bash", "server": "claude"}}
|
|
3
|
+
{"action": {"outcome": "success", "reason": "", "type": "tool_called"}, "actor": {"id": "corpus", "role": "agent", "type": "agent"}, "agent": {"name": "claude", "skill_id": ""}, "correlation_id": "fp51-1", "event_id": "fp51-1-2", "host_id": "corpus-host", "initiator": {"actor_type": "agent", "operator_id": "corpus", "trigger": "autonomous"}, "schema_version": "1.2.0", "session_id": "fp51-1", "source": {"app": "claude"}, "timestamp_utc": "2026-08-26T10:09:00+00:00", "tool": {"command": "pytest -q", "input_hash": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", "qualified": "claude.Bash", "server": "claude"}}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
{"action": {"outcome": "success", "reason": "", "type": "tool_called"}, "actor": {"id": "corpus", "role": "agent", "type": "agent"}, "agent": {"name": "claude", "skill_id": ""}, "correlation_id": "bl-1", "event_id": "bl-1-0", "host_id": "corpus-host", "initiator": {"actor_type": "agent", "operator_id": "corpus", "trigger": "autonomous"}, "schema_version": "1.
|
|
2
|
-
{"action": {"outcome": "success", "reason": "", "type": "tool_called"}, "actor": {"id": "corpus", "role": "agent", "type": "agent"}, "agent": {"name": "claude", "skill_id": ""}, "correlation_id": "bl-1", "event_id": "bl-1-1", "host_id": "corpus-host", "initiator": {"actor_type": "agent", "operator_id": "corpus", "trigger": "autonomous"}, "schema_version": "1.
|
|
3
|
-
{"action": {"outcome": "success", "reason": "", "type": "tool_called"}, "actor": {"id": "corpus", "role": "agent", "type": "agent"}, "agent": {"name": "claude", "skill_id": ""}, "correlation_id": "bl-1", "event_id": "bl-1-2", "host_id": "corpus-host", "initiator": {"actor_type": "agent", "operator_id": "corpus", "trigger": "autonomous"}, "schema_version": "1.
|
|
1
|
+
{"action": {"outcome": "success", "reason": "", "type": "tool_called"}, "actor": {"id": "corpus", "role": "agent", "type": "agent"}, "agent": {"name": "claude", "skill_id": ""}, "correlation_id": "bl-1", "event_id": "bl-1-0", "host_id": "corpus-host", "initiator": {"actor_type": "agent", "operator_id": "corpus", "trigger": "autonomous"}, "schema_version": "1.2.0", "session_id": "bl-1", "source": {"app": "claude"}, "timestamp_utc": "2026-08-26T10:00:00+00:00", "tool": {"command": "curl -s http://127.0.0.1:8000/tmp/bootstrap.sh | bash", "input_hash": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", "qualified": "claude.Bash", "server": "claude", "traits": ["pipe_to_shell_local", "risky_exec"]}}
|
|
2
|
+
{"action": {"outcome": "success", "reason": "", "type": "tool_called"}, "actor": {"id": "corpus", "role": "agent", "type": "agent"}, "agent": {"name": "claude", "skill_id": ""}, "correlation_id": "bl-1", "event_id": "bl-1-1", "host_id": "corpus-host", "initiator": {"actor_type": "agent", "operator_id": "corpus", "trigger": "autonomous"}, "schema_version": "1.2.0", "session_id": "bl-1", "source": {"app": "claude"}, "timestamp_utc": "2026-08-26T10:02:00+00:00", "tool": {"command": "curl -s http://localhost:3000/setup.sh | sh", "input_hash": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", "qualified": "claude.Bash", "server": "claude", "traits": ["pipe_to_shell_local", "risky_exec"]}}
|
|
3
|
+
{"action": {"outcome": "success", "reason": "", "type": "tool_called"}, "actor": {"id": "corpus", "role": "agent", "type": "agent"}, "agent": {"name": "claude", "skill_id": ""}, "correlation_id": "bl-1", "event_id": "bl-1-2", "host_id": "corpus-host", "initiator": {"actor_type": "agent", "operator_id": "corpus", "trigger": "autonomous"}, "schema_version": "1.2.0", "session_id": "bl-1", "source": {"app": "claude"}, "timestamp_utc": "2026-08-26T10:04:00+00:00", "tool": {"command": "npm run test", "input_hash": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", "qualified": "claude.Bash", "server": "claude"}}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
{"action": {"outcome": "success", "reason": "", "type": "tool_called"}, "actor": {"id": "corpus", "role": "agent", "type": "agent"}, "agent": {"name": "claude", "skill_id": ""}, "correlation_id": "fp49-1", "event_id": "fp49-1-0", "host_id": "corpus-host", "initiator": {"actor_type": "agent", "operator_id": "corpus", "trigger": "autonomous"}, "schema_version": "1.2.0", "session_id": "fp49-1", "source": {"app": "claude"}, "timestamp_utc": "2026-08-26T10:00:00+00:00", "tool": {"arguments": {"channel": "#eng", "text": "reminder: never commit ~/.aws/credentials or ~/.ssh/id_rsa"}, "input_hash": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", "qualified": "mcp.slack.post_message", "server": "claude"}}
|
|
2
|
+
{"action": {"outcome": "success", "reason": "", "type": "tool_called"}, "actor": {"id": "corpus", "role": "agent", "type": "agent"}, "agent": {"name": "claude", "skill_id": ""}, "correlation_id": "fp49-1", "event_id": "fp49-1-1", "host_id": "corpus-host", "initiator": {"actor_type": "agent", "operator_id": "corpus", "trigger": "autonomous"}, "schema_version": "1.2.0", "session_id": "fp49-1", "source": {"app": "claude"}, "timestamp_utc": "2026-08-26T10:02:00+00:00", "tool": {"arguments": {"description": "the old key is still in ~/.aws/credentials on the build box", "title": "Rotate keys"}, "input_hash": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", "qualified": "mcp.linear.create_issue", "server": "claude"}}
|
|
3
|
+
{"action": {"outcome": "success", "reason": "", "type": "tool_called"}, "actor": {"id": "corpus", "role": "agent", "type": "agent"}, "agent": {"name": "claude", "skill_id": ""}, "correlation_id": "fp49-1", "event_id": "fp49-1-2", "host_id": "corpus-host", "initiator": {"actor_type": "agent", "operator_id": "corpus", "trigger": "autonomous"}, "schema_version": "1.2.0", "session_id": "fp49-1", "source": {"app": "claude"}, "timestamp_utc": "2026-08-26T10:04:00+00:00", "tool": {"command": "git status", "input_hash": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", "qualified": "claude.Bash", "server": "claude"}}
|
|
@@ -454,6 +454,48 @@ cases:
|
|
|
454
454
|
at low with no T1105 and no TA0011, neither of which a loopback fetch
|
|
455
455
|
earns.
|
|
456
456
|
|
|
457
|
+
- name: fetch piped into the interpreter's own script
|
|
458
|
+
session: benign_fetch_piped_into_own_script.jsonl
|
|
459
|
+
expect: []
|
|
460
|
+
benign: true
|
|
461
|
+
note: >
|
|
462
|
+
Issue #50. `curl api/x.json | python -c 'json.load(sys.stdin)'` was
|
|
463
|
+
critical. An interpreter handed its own program does not execute the pipe,
|
|
464
|
+
it reads it as data, so this is a data pipeline and not a cradle. The
|
|
465
|
+
cradle shapes have no inline script and still fire: see the two cases
|
|
466
|
+
above.
|
|
467
|
+
|
|
468
|
+
- name: read .env.example while setting a project up
|
|
469
|
+
session: benign_env_example_read.jsonl
|
|
470
|
+
expect: []
|
|
471
|
+
benign: true
|
|
472
|
+
note: >
|
|
473
|
+
Issue #44, six of seventeen week-one dogfood findings and every one a
|
|
474
|
+
critical. `.env.example` is the file you commit *instead* of a credential
|
|
475
|
+
file, and reading it is day one in any repository. A bare `.env` and
|
|
476
|
+
`.env.local` still map to credential access.
|
|
477
|
+
|
|
478
|
+
- name: fetch docs from GitHub raw, then run an unrelated script
|
|
479
|
+
session: benign_github_raw_docs_then_script.jsonl
|
|
480
|
+
expect: []
|
|
481
|
+
benign: true
|
|
482
|
+
note: >
|
|
483
|
+
Issue #51. Any staging-host fetch followed by any `python -c` was critical
|
|
484
|
+
with nothing linking the two. GitHub raw serves documentation as well as
|
|
485
|
+
payloads, and a fetch that only printed staged no artifact for a later
|
|
486
|
+
command to run. The bound form, where the file downloaded is the file
|
|
487
|
+
executed, still fires.
|
|
488
|
+
|
|
489
|
+
- name: MCP message that names a credential path
|
|
490
|
+
session: benign_mcp_text_mentions_credential_path.jsonl
|
|
491
|
+
expect: []
|
|
492
|
+
benign: true
|
|
493
|
+
note: >
|
|
494
|
+
Issue #49, the sibling of #44. Structured tool arguments have no shell
|
|
495
|
+
quoting to mask, so a path inside a free-text field read as a credential
|
|
496
|
+
access. Naming `~/.aws/credentials` in a Slack message is a mention. The
|
|
497
|
+
same path arriving in a `path` argument is still a read.
|
|
498
|
+
|
|
457
499
|
- name: remote pipe to shell is still a cradle
|
|
458
500
|
session: attack_remote_pipe_to_shell.jsonl
|
|
459
501
|
expect: [encoded-command-download]
|
|
@@ -43,6 +43,7 @@ from .traits import (
|
|
|
43
43
|
PR_MERGE_COMMAND as _PR_MERGE_COMMAND,
|
|
44
44
|
RAW_IP_URL as _RAW_IP_URL,
|
|
45
45
|
RISKY_EXEC_AFTER_STAGING as _RISKY_EXEC_AFTER_STAGING,
|
|
46
|
+
STAGES_ARTIFACT as _STAGES_ARTIFACT,
|
|
46
47
|
STAGING_FETCH as _STAGING_FETCH,
|
|
47
48
|
STAGING_HOST as _STAGING_HOST,
|
|
48
49
|
UNTRUSTED_INPUT_COMMAND as _UNTRUSTED_INPUT_COMMAND,
|
|
@@ -307,7 +308,14 @@ def _is_staging_fetch(event: dict[str, Any]) -> bool:
|
|
|
307
308
|
if not _STAGING_HOST.search(cmd):
|
|
308
309
|
return False
|
|
309
310
|
words = _command_words(event)
|
|
310
|
-
|
|
311
|
+
if not (_STAGING_FETCH.search(words) or _DOWNLOAD_EXEC.search(words)):
|
|
312
|
+
return False
|
|
313
|
+
# A fetch that only printed staged nothing, so there is no artifact for
|
|
314
|
+
# a later command to run. `curl raw.githubusercontent.com/.../README.md`
|
|
315
|
+
# followed an hour later by any `python -c` was reported as critical
|
|
316
|
+
# staging with no link between the two (issue #51). GitHub raw is where
|
|
317
|
+
# documentation lives as well as where payloads do.
|
|
318
|
+
return bool(_STAGES_ARTIFACT.search(cmd))
|
|
311
319
|
return _has_trait(event, "staging_fetch")
|
|
312
320
|
|
|
313
321
|
|
|
@@ -1265,9 +1273,30 @@ HOST_REGISTRY = [
|
|
|
1265
1273
|
#: and the alternative is calling every rule to find out what it might say.
|
|
1266
1274
|
#: `test_detection_rule_identity.py` greps this module and fails if the two
|
|
1267
1275
|
#: disagree, so it cannot drift silently.
|
|
1276
|
+
#: Rules that are in the tree and run, but are NOT part of the published set.
|
|
1277
|
+
#:
|
|
1278
|
+
#: `autonomous-unapproved-write` keys on `initiator.actor_type == "autonomous"`.
|
|
1279
|
+
#: The bus and SDK paths do produce that (cron, vault_watch, ingress, recovery),
|
|
1280
|
+
#: so the rule is correct and still registered. The IDE hook path never does:
|
|
1281
|
+
#: across roughly 32,000 events of real dogfood traffic from five agent
|
|
1282
|
+
#: surfaces, the actor is `human`, `agent` or `system` and never once
|
|
1283
|
+
#: `autonomous`. Ingest coercion deliberately cannot promote a client into it
|
|
1284
|
+
#: either, because that would let anyone fake this rule.
|
|
1285
|
+
#:
|
|
1286
|
+
#: So on every capture surface a user actually installs, it cannot fire. It was
|
|
1287
|
+
#: being counted in "fifteen detection rules", shipped in the Sigma pack at
|
|
1288
|
+
#: severity high, and named in the pitch as the flagship no-default-self-approve
|
|
1289
|
+
#: story. A published rule that cannot fire is a claim, not a detection.
|
|
1290
|
+
#:
|
|
1291
|
+
#: It stays registered so that a session which really is autonomous is still
|
|
1292
|
+
#: caught. It leaves the published set until a capture surface produces the
|
|
1293
|
+
#: signal it reads.
|
|
1294
|
+
EXPERIMENTAL_RULE_IDS: frozenset[str] = frozenset({
|
|
1295
|
+
"autonomous-unapproved-write",
|
|
1296
|
+
})
|
|
1297
|
+
|
|
1268
1298
|
BUILTIN_RULE_IDS: frozenset[str] = frozenset({
|
|
1269
1299
|
"credential-exfil",
|
|
1270
|
-
"autonomous-unapproved-write",
|
|
1271
1300
|
"discovery-then-collect",
|
|
1272
1301
|
"approval-denied-then-executed",
|
|
1273
1302
|
"encoded-command-download",
|
|
@@ -44,11 +44,22 @@ DOWNLOAD_EXEC = re.compile(
|
|
|
44
44
|
ENCODED_CMD = re.compile(r"-enc(odedcommand)?\b|frombase64string", re.IGNORECASE)
|
|
45
45
|
|
|
46
46
|
# Fetch remote content and feed it straight to an interpreter (ADI §4.2).
|
|
47
|
+
# An interpreter given its own program does not execute the pipe; it reads it as
|
|
48
|
+
# data. `curl api/x.json | python -c 'json.load(sys.stdin)'` is a data pipeline,
|
|
49
|
+
# and calling it a download cradle put a critical on ordinary scripting (issue
|
|
50
|
+
# #50). `curl x.sh | python` has no script of its own, so the download *is* the
|
|
51
|
+
# program, and that stays a cradle.
|
|
52
|
+
#
|
|
53
|
+
# Residual, stated rather than hidden: `| python -c "exec(sys.stdin.read())"`
|
|
54
|
+
# reaches for the download deliberately and slips this. INLINE_EVAL sees that
|
|
55
|
+
# shape, and a cradle written that way is no longer hiding.
|
|
56
|
+
_INLINE_SCRIPT = r"(?!\s*-\w*[ce]\b)"
|
|
57
|
+
|
|
47
58
|
PIPE_TO_SHELL = re.compile(
|
|
48
59
|
r"\b(curl|wget|iwr|invoke-webrequest|invoke-restmethod)\b[^|;&]*[|]\s*"
|
|
49
|
-
r"(sudo\s+)?\b(ba|z|k|da)?sh\b|"
|
|
60
|
+
r"(sudo\s+)?\b(ba|z|k|da)?sh\b" + _INLINE_SCRIPT + r"|"
|
|
50
61
|
r"\b(curl|wget|iwr|invoke-webrequest|invoke-restmethod)\b[^|;&]*[|]\s*"
|
|
51
|
-
r"(iex|invoke-expression|python\d?|perl|ruby|node)\b",
|
|
62
|
+
r"(iex|invoke-expression|python\d?|perl|ruby|node)\b" + _INLINE_SCRIPT,
|
|
52
63
|
re.IGNORECASE,
|
|
53
64
|
)
|
|
54
65
|
|
|
@@ -314,6 +325,18 @@ RISKY_EXEC_AFTER_STAGING = re.compile(
|
|
|
314
325
|
r"\bpowershell(?:\.exe)?\s+-(?:enc|f|file)\b",
|
|
315
326
|
re.IGNORECASE,
|
|
316
327
|
)
|
|
328
|
+
#: A fetch that leaves something behind: written to disk, or handed onward. A
|
|
329
|
+
#: fetch that only printed staged nothing, so there is no artifact for a later
|
|
330
|
+
#: command to run (issue #51).
|
|
331
|
+
STAGES_ARTIFACT = re.compile(
|
|
332
|
+
r"\s-[a-zA-Z]*[oO]|" # curl -o / -O, wget -O
|
|
333
|
+
r"\s--output(?:-document)?|"
|
|
334
|
+
r"\s>\s*\S|" # shell redirect to a file
|
|
335
|
+
r"\s-[a-zA-Z]*P|" # wget -P <dir>
|
|
336
|
+
r"[|]", # piped onward, including into an interpreter
|
|
337
|
+
re.IGNORECASE,
|
|
338
|
+
)
|
|
339
|
+
|
|
317
340
|
BENIGN_AFTER_STAGING = re.compile(
|
|
318
341
|
r"\b(npm|yarn|pnpm|pip|pip3|cargo|go)\s+(?:install|run|build)\b",
|
|
319
342
|
re.IGNORECASE,
|
|
@@ -359,13 +382,27 @@ CREDENTIAL_PATH = re.compile(
|
|
|
359
382
|
# directly after something that reads a file. A bare mention in prose does not
|
|
360
383
|
# qualify, which costs nothing: nobody reads a credential file without naming a
|
|
361
384
|
# path or a verb.
|
|
385
|
+
# `.env.example` is not a credential file. It is the file you commit *instead*
|
|
386
|
+
# of one, and reading it is what every developer does on their first day in a
|
|
387
|
+
# repository. It was six of seventeen week-one dogfood findings, every one a
|
|
388
|
+
# critical, which is the exact rate at which people stop reading criticals
|
|
389
|
+
# (issue #44).
|
|
390
|
+
#
|
|
391
|
+
# The lookahead sits immediately after `\.env` rather than at the end, because
|
|
392
|
+
# at the end it fails and the engine simply matches the shorter `.env` prefix,
|
|
393
|
+
# leaving the placeholder tagged anyway.
|
|
394
|
+
#
|
|
395
|
+
# Placeholders only. `.env.local`, `.env.production` and a bare `.env` still
|
|
396
|
+
# match, because those hold real values.
|
|
397
|
+
_ENV_PLACEHOLDER = r"(?!\.(?:example|sample|template|dist|defaults|placeholder)\b)"
|
|
398
|
+
|
|
362
399
|
ENV_FILE = re.compile(
|
|
363
|
-
r"[\w.~$-]*[/\\]\.env(?:\.[A-Za-z0-9_-]+)?\b|"
|
|
400
|
+
r"[\w.~$-]*[/\\]\.env" + _ENV_PLACEHOLDER + r"(?:\.[A-Za-z0-9_-]+)?\b|"
|
|
364
401
|
r"\b(?:cat|bat|less|more|head|tail|type|source|export|dotenv|load_dotenv|"
|
|
365
402
|
r"get-content|gc|cp|mv|scp|rsync|base64|xxd|od|strings|"
|
|
366
403
|
r"grep|rg|ag|awk|sed|nano|vim|vi|emacs|code|open|start)"
|
|
367
|
-
r"\s+(?:-[-\w]+\s+)*\.env(?:\.[A-Za-z0-9_-]+)?\b|"
|
|
368
|
-
r"^\s*\.env(?:\.[A-Za-z0-9_-]+)?\b",
|
|
404
|
+
r"\s+(?:-[-\w]+\s+)*\.env" + _ENV_PLACEHOLDER + r"(?:\.[A-Za-z0-9_-]+)?\b|"
|
|
405
|
+
r"^\s*\.env" + _ENV_PLACEHOLDER + r"(?:\.[A-Za-z0-9_-]+)?\b",
|
|
369
406
|
re.IGNORECASE | re.MULTILINE,
|
|
370
407
|
)
|
|
371
408
|
|
|
@@ -187,6 +187,7 @@ class _SchemaFields(NamedTuple):
|
|
|
187
187
|
server_version: str
|
|
188
188
|
list_changed: bool | None
|
|
189
189
|
tool_digests: dict[str, str]
|
|
190
|
+
concealed: dict[str, int]
|
|
190
191
|
|
|
191
192
|
|
|
192
193
|
def _schema_payload_fields(payload: dict[str, Any]) -> _SchemaFields:
|
|
@@ -202,6 +203,12 @@ def _schema_payload_fields(payload: dict[str, Any]) -> _SchemaFields:
|
|
|
202
203
|
list_changed = payload.get("list_changed")
|
|
203
204
|
if list_changed is not None and not isinstance(list_changed, bool):
|
|
204
205
|
list_changed = None
|
|
206
|
+
raw_concealed = payload.get("schema_concealed")
|
|
207
|
+
concealed = (
|
|
208
|
+
{str(k): int(v) for k, v in raw_concealed.items() if isinstance(v, int)}
|
|
209
|
+
if isinstance(raw_concealed, dict)
|
|
210
|
+
else {}
|
|
211
|
+
)
|
|
205
212
|
raw_digests = payload.get("schema_tool_digests")
|
|
206
213
|
tool_digests = (
|
|
207
214
|
{str(k): str(v) for k, v in raw_digests.items() if isinstance(v, str)}
|
|
@@ -209,7 +216,8 @@ def _schema_payload_fields(payload: dict[str, Any]) -> _SchemaFields:
|
|
|
209
216
|
else {}
|
|
210
217
|
)
|
|
211
218
|
return _SchemaFields(
|
|
212
|
-
server, fingerprint, tool_count, source, server_version, list_changed,
|
|
219
|
+
server, fingerprint, tool_count, source, server_version, list_changed,
|
|
220
|
+
tool_digests, concealed,
|
|
213
221
|
)
|
|
214
222
|
|
|
215
223
|
|
|
@@ -232,22 +240,47 @@ def build_schema_canonical(
|
|
|
232
240
|
from agentmetry.core.audit.canonical import SCHEMA_VERSION
|
|
233
241
|
from agentmetry.core.audit.identity import identity_fields
|
|
234
242
|
from agentmetry.core.audit.atlas import RUG_PULL
|
|
235
|
-
from agentmetry.core.diagnostics.mcp_schema import server_id
|
|
243
|
+
from agentmetry.core.diagnostics.mcp_schema import FORMATTING_CATEGORY, server_id
|
|
236
244
|
|
|
237
245
|
fields = _schema_payload_fields(payload)
|
|
238
246
|
server, fingerprint, tool_count = fields.server, fields.fingerprint, fields.tool_count
|
|
239
247
|
server_version, list_changed = fields.server_version, fields.list_changed
|
|
240
248
|
outcome = "changed" if status == "changed" else "success"
|
|
241
|
-
|
|
242
|
-
"MCP tool schema changed; config may be unchanged (rug-pull candidate)"
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
249
|
+
_REASONS = {
|
|
250
|
+
"changed": "MCP tool schema changed; config may be unchanged (rug-pull candidate)",
|
|
251
|
+
# Says what it is and what it is not. A re-baseline adopts whatever the
|
|
252
|
+
# server serves today without comparing it to anything, so if the server
|
|
253
|
+
# was already poisoned before our hashing changed, this event is the
|
|
254
|
+
# moment that state became the trusted one. `new` would have hidden that
|
|
255
|
+
# behind a word that also means "nothing was ever wrong here" (#146).
|
|
256
|
+
"rebaselined": (
|
|
257
|
+
"MCP tool schema re-baselined after a fingerprint change; "
|
|
258
|
+
"trust-on-first-use, not compared against the previous baseline"
|
|
259
|
+
),
|
|
260
|
+
}
|
|
261
|
+
reason = _REASONS.get(status, "MCP tool schema observed")
|
|
262
|
+
_formatting = {k: v for k, v in fields.concealed.items() if k == FORMATTING_CATEGORY}
|
|
263
|
+
_concealment = {k: v for k, v in fields.concealed.items() if k != FORMATTING_CATEGORY}
|
|
246
264
|
mcp_schema: dict[str, Any] = {
|
|
247
265
|
"server_id": server_id(server) if server else "",
|
|
248
266
|
"fingerprint": fingerprint,
|
|
249
267
|
"tool_count": tool_count,
|
|
250
268
|
"status": status,
|
|
269
|
+
# A baseline nobody has verified against a predecessor. Structured
|
|
270
|
+
# rather than left in the reason string so a SIEM can count them
|
|
271
|
+
# instead of matching prose.
|
|
272
|
+
**({"unverified_baseline": True} if status == "rebaselined" else {}),
|
|
273
|
+
# Concealed characters in strings the model reads. Counts per category,
|
|
274
|
+
# never the text. Attached on every status including `new`, because
|
|
275
|
+
# unlike everything else here it does not need a baseline: this is the
|
|
276
|
+
# one poisoning visible on a first sighting.
|
|
277
|
+
**({"concealed": _concealment} if _concealment else {}),
|
|
278
|
+
# Typography, kept apart from the evidence. A soft hyphen or a
|
|
279
|
+
# zero-width space arrives in descriptions imported from formatted
|
|
280
|
+
# documentation, and no neighbouring character can clear it the way one
|
|
281
|
+
# clears a ZWJ. Reporting it beside real concealment would make an
|
|
282
|
+
# operator who reads one finding distrust the next.
|
|
283
|
+
**({"formatting": _formatting} if _formatting else {}),
|
|
251
284
|
# Only a schema that MOVED is the technique. `new` is the first
|
|
252
285
|
# sight of a server and `same` is a quiet reconnect; tagging either
|
|
253
286
|
# as a rug pull would put a Defense Evasion label on installing a
|
|
@@ -160,6 +160,33 @@ def _reaches_remote_host(text: str) -> bool:
|
|
|
160
160
|
return any(not _LOOPBACK.match(host) for host in hosts)
|
|
161
161
|
|
|
162
162
|
|
|
163
|
+
#: Argument names that carry free text a human or a model wrote. A credential
|
|
164
|
+
#: path inside one of these is something being talked about.
|
|
165
|
+
_PROSE_KEYS = frozenset(
|
|
166
|
+
{
|
|
167
|
+
"text", "content", "body", "message", "prompt", "query", "description",
|
|
168
|
+
"summary", "comment", "note", "title", "instruction", "instructions",
|
|
169
|
+
"question", "answer", "input", "output", "markdown", "html",
|
|
170
|
+
}
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _path_evidence_text(evidence: Any, fallback: str) -> str:
|
|
175
|
+
"""Evidence with prose arguments removed, for path-based rules only.
|
|
176
|
+
|
|
177
|
+
Returns everything except the free-text values, so `{"path": "~/.ssh/id_rsa"}`
|
|
178
|
+
still reads as a credential access and `{"text": "look in ~/.ssh/id_rsa"}`
|
|
179
|
+
does not. Non-dict evidence is unchanged: there are no argument names to
|
|
180
|
+
reason about, and guessing would be worse than not trying.
|
|
181
|
+
"""
|
|
182
|
+
if not isinstance(evidence, dict):
|
|
183
|
+
return fallback
|
|
184
|
+
kept = {k: v for k, v in evidence.items() if str(k).lower() not in _PROSE_KEYS}
|
|
185
|
+
if not kept:
|
|
186
|
+
return ""
|
|
187
|
+
return _evidence_text(kept)
|
|
188
|
+
|
|
189
|
+
|
|
163
190
|
def _shell_text(evidence: Any) -> str | None:
|
|
164
191
|
"""The shell command inside `evidence`, or None if this is not shell text.
|
|
165
192
|
|
|
@@ -210,8 +237,21 @@ def get_mitre_mapping(
|
|
|
210
237
|
# text somebody is writing, not a file somebody is reading. Structured
|
|
211
238
|
# evidence is not masked at all -- see _shell_text.
|
|
212
239
|
shell = _shell_text(evidence)
|
|
213
|
-
|
|
214
|
-
|
|
240
|
+
if shell:
|
|
241
|
+
literal = mask_literals(shell, include_double=False).lower()
|
|
242
|
+
written = mask_literals(shell).lower()
|
|
243
|
+
else:
|
|
244
|
+
# Structured args, so there is no shell quoting to mask. Path rules
|
|
245
|
+
# then run against whichever values could plausibly *be* a path.
|
|
246
|
+
#
|
|
247
|
+
# An MCP tool called with `{"text": "check ~/.aws/credentials"}` is
|
|
248
|
+
# a message that names a file, not a read of one, and it was being
|
|
249
|
+
# mapped to T1552 (issue #49). Prose lives in `text`, `content`,
|
|
250
|
+
# `prompt` and their siblings; a path the tool will open arrives in
|
|
251
|
+
# `path`, `file`, `target`. Same defect class as #44: a mention
|
|
252
|
+
# treated as a read.
|
|
253
|
+
literal = _path_evidence_text(evidence, text)
|
|
254
|
+
written = text
|
|
215
255
|
if PRIVATE_KEY_PATH.search(literal):
|
|
216
256
|
return _PRIVATE_KEY
|
|
217
257
|
if (
|