agentmetry 0.5.0__tar.gz → 0.7.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.5.0 → agentmetry-0.7.0}/.gitignore +4 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/PKG-INFO +3 -2
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/api/routes/audit.py +51 -2
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/cli/__init__.py +30 -7
- agentmetry-0.7.0/agentmetry/core/audit/detection/corpus/benign_env_example_read.jsonl +4 -0
- agentmetry-0.7.0/agentmetry/core/audit/detection/corpus/benign_fetch_piped_into_own_script.jsonl +3 -0
- agentmetry-0.7.0/agentmetry/core/audit/detection/corpus/benign_github_raw_docs_then_script.jsonl +3 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/benign_loopback_pipe_to_interpreter.jsonl +3 -3
- agentmetry-0.7.0/agentmetry/core/audit/detection/corpus/benign_mcp_text_mentions_credential_path.jsonl +3 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/corpus.yaml +42 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/disposition.py +52 -6
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/rules.py +31 -2
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/traits.py +42 -5
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/yaml_config.py +8 -2
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/ingest.py +155 -21
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/mitre.py +42 -2
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/trail_anchor.py +55 -6
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/trail_db.py +10 -3
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/bus/events.py +1 -1
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/diagnostics/doctor.py +1 -1
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/diagnostics/driver_paths.py +6 -4
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/diagnostics/mcp_schema.py +175 -10
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/version.py +1 -1
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/hooks/ingest.py +38 -6
- {agentmetry-0.5.0 → agentmetry-0.7.0}/pyproject.toml +34 -2
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_agentmetry_ingest_client.py +47 -0
- agentmetry-0.7.0/tests/test_cli_commands.py +467 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_detection_adi.py +36 -1
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_detection_disposition.py +47 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_detection_rule_identity.py +16 -4
- agentmetry-0.7.0/tests/test_ingest_roundtrip.py +163 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_mcp_audit_proxy.py +61 -7
- agentmetry-0.7.0/tests/test_mcp_schema.py +490 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_readme_claims.py +53 -0
- agentmetry-0.7.0/tests/test_sigma_pack.py +131 -0
- agentmetry-0.7.0/tools/generate_sigma_pack.py +203 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tools/mcp_audit_proxy.py +81 -7
- agentmetry-0.5.0/tests/test_mcp_schema.py +0 -233
- {agentmetry-0.5.0 → agentmetry-0.7.0}/.dockerignore +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/.env.agentmetry-demo +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/.env.example +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/Dockerfile +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/README.md +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/__init__.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/api/__init__.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/api/main.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/api/routes/__init__.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/api/websocket.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/api/ws_bridge.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/cli/__main__.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/__init__.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/__init__.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/adapters/__init__.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/adapters/agt.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/adapters/chronicle.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/adapters/cloudevents.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/adapters/ecs.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/adapters/splunk.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/alerts.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/atlas.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/canonical.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/compliance_digest.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/__init__.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/benchmark.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/attack_approval_denied_then_executed.jsonl +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/attack_arbitrary_host_stage_execute.jsonl +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/attack_autonomous_unapproved_write.jsonl +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/attack_credential_exfil.jsonl +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/attack_credential_then_cloud_api.jsonl +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/attack_destructive_delete_burst.jsonl +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/attack_discovery_then_collect.jsonl +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/attack_dotfile_then_git_push.jsonl +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/attack_encoded_command_download.jsonl +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/attack_env_credential_exfil.jsonl +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/attack_hashed_only_no_command.jsonl +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/attack_interpreter_egress.jsonl +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/attack_pr_merged_without_review.jsonl +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/attack_proc_substitution_cradle.jsonl +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/attack_remote_pipe_to_shell.jsonl +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/attack_remote_staging_then_execute.jsonl +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/attack_secret_manager_then_egress.jsonl +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/attack_session_tool_burst.jsonl +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/attack_single_command_exfil.jsonl +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/attack_single_quoted_interpreter_egress.jsonl +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/attack_ssh_directory_exfil.jsonl +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/attack_subagent_swarm.jsonl +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/attack_timestamp_collision.jsonl +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/attack_untrusted_input_then_action.jsonl +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/benign_authoring_merge_fixtures.jsonl +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/benign_autonomous_after_approval.jsonl +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/benign_build_artifact_cleanup.jsonl +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/benign_ci_artifact_download.jsonl +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/benign_commit_message_names_cloud_clis.jsonl +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/benign_database_migration.jsonl +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/benign_dependency_install_and_build.jsonl +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/benign_download_release_archive.jsonl +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/benign_fetch_data_then_run_repo_script.jsonl +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/benign_fetch_dataset_then_analyse.jsonl +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/benign_fetch_lockfile_then_install.jsonl +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/benign_git_review_and_push.jsonl +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/benign_human_driven_deletes.jsonl +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/benign_local_api_probing.jsonl +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/benign_long_but_calm_session.jsonl +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/benign_loopback_is_not_egress.jsonl +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/benign_ordinary_development.jsonl +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/benign_package_manager_after_fetch.jsonl +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/benign_reading_config_that_is_not_secret.jsonl +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/benign_remote_api_call_no_credentials.jsonl +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/benign_research_then_docs.jsonl +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/benign_reversed_order_is_not_exfil.jsonl +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/benign_secret_names_and_docs.jsonl +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/benign_test_and_fix_loop.jsonl +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/corpus/benign_writing_about_credentials.jsonl +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/engine.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/live.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/live_store.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/models.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/detection/yaml_rules.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/dlp/__init__.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/dlp/loader.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/dlp/models.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/dlp/scanner.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/dogfood.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/evidence_pack.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/external.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/hashing.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/heartbeat.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/hook_bootstrap.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/identity.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/migrate.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/policy.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/redaction.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/replay.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/run_context.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/sinks.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/spool.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/tool_policy/__init__.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/tool_policy/evaluator.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/tool_policy/loader.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/tool_policy/models.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/trail_chain.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/audit/trail_merkle.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/auth.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/bus/__init__.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/bus/audit_exporter.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/bus/bridges.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/bus/bus.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/bus/outbox.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/config.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/diagnostics/__init__.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/diagnostics/autostart.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/diagnostics/env_file.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/diagnostics/hook_coverage.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/diagnostics/mcp_inventory.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/drivers/__init__.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/drivers/host.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/drivers/permissions.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/drivers/spec.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/extensions.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/core/health.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/hooks/__init__.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/policies/detection/manifest.yaml +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/policies/dlp/manifest.yaml +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/policies/opa/agent_rules.rego +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/agentmetry/policies/tool/manifest.yaml +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/conftest.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/fixtures/agt_filesink_exfil.jsonl +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/fixtures/agt_filesink_mixed.jsonl +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/fixtures/fake_mcp_server.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_agentmetry_audit.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_agt_adapter.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_atlas_detection_enrichment.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_atlas_mapping.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_audit_sinks.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_audit_stats.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_audit_tail.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_auth.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_autostart.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_boot_sequence.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_burst_windows.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_chinese_agent_hooks.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_chinese_agent_sprint_b.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_chinese_agent_sprint_c.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_chronicle_adapter.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_cli_backup.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_cloudevents_adapter.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_compliance_digest.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_crewai_adapter.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_detection_benchmark.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_detection_default_config.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_detection_disposition_api.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_detection_duplicate_emission.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_detection_emit_durability.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_detection_engine.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_detection_evasion.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_detection_hf_incident.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_detection_quoted_command_words.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_detection_rules_v2.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_dlp_agent_env_override.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_dlp_invisible_unicode.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_dlp_markdown_exfil.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_dlp_scanner.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_doctor.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_dogfood.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_dogfood_freeze.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_driver_paths.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_ecs_threat.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_event_bus.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_evidence_pack.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_extensions.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_external_ingest.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_forwarding_shape.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_heartbeat.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_hook_bootstrap.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_hook_coverage.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_hook_enforcement_timing.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_hook_spool.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_isolation.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_launch_targets.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_live_detection.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_live_detection_store.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_mcp_inventory.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_mitre_dlp.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_opensre_adapter.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_redaction.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_replay.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_static_serving.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_subagent_lifecycle.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_tool_policy.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_tool_policy_agent_cli.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_tool_policy_git_hooks.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_trail_anchor.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_trail_chain.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_trail_concurrency.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_trail_merkle.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_unattended_agent_policy.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_version.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.0}/tests/test_yaml_detection_rules.py +0 -0
- {agentmetry-0.5.0 → agentmetry-0.7.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.7.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
|
|
@@ -35,8 +35,9 @@ Requires-Dist: uvicorn[standard]>=0.32.0
|
|
|
35
35
|
Requires-Dist: websockets>=14.0
|
|
36
36
|
Provides-Extra: dev
|
|
37
37
|
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
38
|
+
Requires-Dist: pytest-cov>=6.0; extra == 'dev'
|
|
38
39
|
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
39
|
-
Requires-Dist: ruff==0.16.
|
|
40
|
+
Requires-Dist: ruff==0.16.4; extra == 'dev'
|
|
40
41
|
Description-Content-Type: text/markdown
|
|
41
42
|
|
|
42
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,19 @@ 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
|
+
server_version: str = ""
|
|
126
|
+
list_changed: bool | None = None
|
|
78
127
|
|
|
79
128
|
|
|
80
129
|
def _parse_event_ts(event: dict[str, Any]) -> datetime | None:
|
|
@@ -7,6 +7,7 @@ Pure stdlib + httpx; never imports the FastAPI app (fast startup, no side effect
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
import argparse
|
|
10
|
+
import logging
|
|
10
11
|
import os
|
|
11
12
|
import shutil
|
|
12
13
|
import socket
|
|
@@ -30,11 +31,13 @@ _TASK_NAME = "Agentmetry Orchestrator"
|
|
|
30
31
|
# Paths bundled by backup, relative to the repo root.
|
|
31
32
|
_BACKUP_PREFIXES = ("vault/", "apps/orchestrator/data/")
|
|
32
33
|
_BACKUP_EXCLUDE_DIRS = {"logs"}
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
|
|
33
36
|
_BACKUP_EXCLUDE_SUFFIXES = {".pid"}
|
|
34
37
|
|
|
35
38
|
|
|
36
39
|
def _base_url(port: int, host: str = "127.0.0.1") -> str:
|
|
37
|
-
display = host if host != "0.0.0.0" else "127.0.0.1"
|
|
40
|
+
display = host if host != "0.0.0.0" else "127.0.0.1" # noqa: S104
|
|
38
41
|
return f"http://{display}:{port}"
|
|
39
42
|
|
|
40
43
|
|
|
@@ -65,8 +68,12 @@ def _fetch_health(port: int) -> dict | None:
|
|
|
65
68
|
resp = httpx.get(f"{_base_url(port)}/api/v1/health", timeout=10.0)
|
|
66
69
|
if resp.status_code == 200:
|
|
67
70
|
return resp.json()
|
|
68
|
-
except Exception:
|
|
69
|
-
|
|
71
|
+
except Exception as exc:
|
|
72
|
+
# Not running is the expected answer here and `None` says so. Logged at
|
|
73
|
+
# debug anyway: a probe that fails for a reason other than "nothing is
|
|
74
|
+
# listening" (TLS, a proxy, a permission error) otherwise looks identical
|
|
75
|
+
# to a stopped orchestrator, and that is an hour of somebody's afternoon.
|
|
76
|
+
logger.debug("health probe on port %s failed: %s", port, exc)
|
|
70
77
|
return None
|
|
71
78
|
|
|
72
79
|
|
|
@@ -77,7 +84,7 @@ def cmd_start(args: argparse.Namespace) -> int:
|
|
|
77
84
|
host = getattr(args, "host", "127.0.0.1")
|
|
78
85
|
if _fetch_health(args.port):
|
|
79
86
|
print(f"Already running on {_base_url(args.port, host)}")
|
|
80
|
-
if host == "0.0.0.0":
|
|
87
|
+
if host == "0.0.0.0": # noqa: S104
|
|
81
88
|
_print_lan_hint(args.port)
|
|
82
89
|
return 0
|
|
83
90
|
|
|
@@ -112,7 +119,7 @@ def cmd_start(args: argparse.Namespace) -> int:
|
|
|
112
119
|
while time.monotonic() < deadline:
|
|
113
120
|
if _fetch_health(args.port):
|
|
114
121
|
print(f"Agentmetry running on {_base_url(args.port, host)} (pid {proc.pid})")
|
|
115
|
-
if host == "0.0.0.0":
|
|
122
|
+
if host == "0.0.0.0": # noqa: S104
|
|
116
123
|
_print_lan_hint(args.port)
|
|
117
124
|
return 0
|
|
118
125
|
if proc.poll() is not None:
|
|
@@ -937,7 +944,7 @@ def cmd_verify(args: argparse.Namespace) -> int:
|
|
|
937
944
|
print(line)
|
|
938
945
|
if source == "config" and not anchor_log.is_file():
|
|
939
946
|
print(f" AGENTMETRY_ANCHOR_LOG points at {anchor_log}, which does not exist")
|
|
940
|
-
if
|
|
947
|
+
if coverage.tampering:
|
|
941
948
|
# The chain printed OK several lines ago and it was telling the
|
|
942
949
|
# truth: the file is internally consistent, because whoever
|
|
943
950
|
# rewrote it made it so. Leaving that as the last word would let
|
|
@@ -947,6 +954,17 @@ def cmd_verify(args: argparse.Namespace) -> int:
|
|
|
947
954
|
"published anchor. The file was rewritten."
|
|
948
955
|
)
|
|
949
956
|
return 1
|
|
957
|
+
if not coverage.ok:
|
|
958
|
+
# Checkpoints exist and none of them cover this file. Worth
|
|
959
|
+
# failing, because an operator who configured an anchor log
|
|
960
|
+
# believes they are anchored. Not worth calling tampering:
|
|
961
|
+
# nothing here contradicts the trail, and a trust command that
|
|
962
|
+
# says "rewritten" about a clean file gets muted.
|
|
963
|
+
print(
|
|
964
|
+
"FAILED — no checkpoint in this anchor log covers this trail. "
|
|
965
|
+
"Check AGENTMETRY_ANCHOR_LOG, or anchor this file."
|
|
966
|
+
)
|
|
967
|
+
return 1
|
|
950
968
|
return 0
|
|
951
969
|
print(f"FAILED — {result.message}")
|
|
952
970
|
if result.first_bad_line:
|
|
@@ -1055,7 +1073,12 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
1055
1073
|
# platform. Same guard as scripts/demo.py.
|
|
1056
1074
|
try:
|
|
1057
1075
|
sys.stdout.reconfigure(encoding="utf-8") # type: ignore[union-attr]
|
|
1058
|
-
except Exception: # pragma: no cover -
|
|
1076
|
+
except Exception: # noqa: S110 # pragma: no cover - host console dependent
|
|
1077
|
+
# The one swallow with nowhere to swallow into: this runs before logging
|
|
1078
|
+
# is configured, and the failure means the console cannot render UTF-8,
|
|
1079
|
+
# which is also why a log line about it would not render. The fallback is
|
|
1080
|
+
# the console's own encoding, which is what would have happened without
|
|
1081
|
+
# the call.
|
|
1059
1082
|
pass
|
|
1060
1083
|
|
|
1061
1084
|
parser = argparse.ArgumentParser(prog="agentmetry", description="Agentmetry local ops")
|
|
@@ -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.7.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.7.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]
|
|
@@ -62,6 +62,44 @@ _NOTE_REQUIRED = frozenset({"false_positive", "risk_accepted"})
|
|
|
62
62
|
#: States that mean no further action is expected.
|
|
63
63
|
CLOSED_STATUSES = frozenset({"resolved", "false_positive", "risk_accepted"})
|
|
64
64
|
|
|
65
|
+
|
|
66
|
+
def _loggable(value: object, *, limit: int = 120) -> str:
|
|
67
|
+
"""Flatten a value so it cannot forge log lines.
|
|
68
|
+
|
|
69
|
+
`rule_id`, `decided_by` and the disposition keys derived from
|
|
70
|
+
`correlation_id` all originate outside this process: a rule id can come from
|
|
71
|
+
an operator's YAML, a correlation id comes from the agent. A newline in any
|
|
72
|
+
of them lets the writer append whatever they like to the orchestrator log,
|
|
73
|
+
including lines that look like ours.
|
|
74
|
+
|
|
75
|
+
The bound is worth stating precisely. The trail is unaffected: canonical
|
|
76
|
+
events are JSON and the encoder escapes a newline inside a string, so
|
|
77
|
+
evidence was never forgeable this way. This protects the operator log, which
|
|
78
|
+
is what a human reads while deciding whether the evidence is worth opening.
|
|
79
|
+
|
|
80
|
+
The replacements are written out one call at a time on purpose. An earlier
|
|
81
|
+
version did the same job with `str.encode("unicode_escape")`, which is
|
|
82
|
+
shorter and which CodeQL cannot follow, so `py/log-injection` stayed open on
|
|
83
|
+
code that was already fixed. A sanitizer the scanner does not recognise
|
|
84
|
+
leaves you arguing with a dashboard instead of reading it.
|
|
85
|
+
|
|
86
|
+
Line breaks become visible escapes rather than disappearing, so a value that
|
|
87
|
+
contained one still looks different from one that did not.
|
|
88
|
+
"""
|
|
89
|
+
text = str(value)
|
|
90
|
+
if len(text) > limit:
|
|
91
|
+
text = text[: limit - 3] + "..."
|
|
92
|
+
# Backslash first, or the escapes introduced below get double-escaped.
|
|
93
|
+
text = text.replace("\\", "\\\\")
|
|
94
|
+
text = text.replace("\r", "\\r")
|
|
95
|
+
text = text.replace("\n", "\\n")
|
|
96
|
+
text = text.replace("\t", "\\t")
|
|
97
|
+
# Anything else non-printable (NUL, the ESC that starts an ANSI sequence)
|
|
98
|
+
# is dropped rather than escaped: it carries no meaning a reader needs and
|
|
99
|
+
# a terminal will act on some of it.
|
|
100
|
+
return "".join(ch for ch in text if ch.isprintable())
|
|
101
|
+
|
|
102
|
+
|
|
65
103
|
_MAX_NOTE_CHARS = 4000
|
|
66
104
|
_MAX_ASSIGNEE_CHARS = 128
|
|
67
105
|
|
|
@@ -234,7 +272,11 @@ class DispositionStore:
|
|
|
234
272
|
"DELETE FROM detection_dispositions WHERE detection_key = ?",
|
|
235
273
|
(stale_key,),
|
|
236
274
|
)
|
|
237
|
-
logger.info(
|
|
275
|
+
logger.info(
|
|
276
|
+
"Migrated disposition %s -> %s after rule rename",
|
|
277
|
+
_loggable(stale_key),
|
|
278
|
+
_loggable(key),
|
|
279
|
+
)
|
|
238
280
|
|
|
239
281
|
history.append({
|
|
240
282
|
"status": normalized,
|
|
@@ -535,14 +577,18 @@ async def apply_disposition(
|
|
|
535
577
|
try:
|
|
536
578
|
await sink.emit(event)
|
|
537
579
|
except Exception:
|
|
538
|
-
logger.exception("Failed to forward disposition for %s", rule_id)
|
|
580
|
+
logger.exception("Failed to forward disposition for %s", _loggable(rule_id))
|
|
539
581
|
|
|
582
|
+
# `previous` and `normalized` are validated against STATUSES before they get
|
|
583
|
+
# here, so passing them raw was safe and CodeQL kept flagging them anyway.
|
|
584
|
+
# Sanitizing a closed-set value is a no-op, and a no-op costs less than a
|
|
585
|
+
# dismissal somebody has to re-justify every time the tab is read.
|
|
540
586
|
logger.info(
|
|
541
587
|
"DISPOSITION %s %s -> %s by %s",
|
|
542
|
-
rule_id,
|
|
543
|
-
previous,
|
|
544
|
-
normalized,
|
|
545
|
-
decided_by or "operator",
|
|
588
|
+
_loggable(rule_id),
|
|
589
|
+
_loggable(previous),
|
|
590
|
+
_loggable(normalized),
|
|
591
|
+
_loggable(decided_by or "operator"),
|
|
546
592
|
)
|
|
547
593
|
return current
|
|
548
594
|
|
|
@@ -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
|
|
|
@@ -2,11 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import logging
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
from typing import Any
|
|
7
8
|
|
|
8
9
|
import yaml
|
|
9
10
|
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
10
13
|
_ORCH_ROOT = Path(__file__).resolve().parents[4]
|
|
11
14
|
_DEFAULT_MANIFEST = _ORCH_ROOT.parent.parent / "policies" / "detection" / "manifest.yaml"
|
|
12
15
|
|
|
@@ -35,8 +38,11 @@ def _manifest_path() -> Path:
|
|
|
35
38
|
custom = settings.detection_rules_path
|
|
36
39
|
if custom:
|
|
37
40
|
return Path(custom)
|
|
38
|
-
except Exception:
|
|
39
|
-
|
|
41
|
+
except Exception as exc:
|
|
42
|
+
# Falling back to the packaged manifest is correct, but doing it in
|
|
43
|
+
# silence means an operator who configured their own rules runs the
|
|
44
|
+
# default ones and is never told.
|
|
45
|
+
logger.debug("could not read detection_rules_path from settings: %s", exc)
|
|
40
46
|
return _DEFAULT_MANIFEST
|
|
41
47
|
|
|
42
48
|
|