destructive_command_guard 0.4.3

A Claude Code hook that blocks destructive commands before they execute
Documentation
//! `New Relic` monitoring pack - protections for destructive `New Relic` operations.
//!
//! Covers destructive CLI/API operations:
//! - `newrelic ... delete` for entities, APM applications, workloads, and synthetics
//! - `curl -X DELETE` to `api.newrelic.com`
//! - GraphQL mutations containing delete operations (best-effort)

use crate::packs::{DestructivePattern, Pack, SafePattern};
use crate::{destructive_pattern, safe_pattern};

/// Create the `New Relic` pack.
#[must_use]
pub fn create_pack() -> Pack {
    Pack {
        id: "monitoring.newrelic".to_string(),
        name: "New Relic",
        description: "Protects against destructive New Relic CLI/API operations like deleting entities \
                      or alerting resources.",
        keywords: &["newrelic", "api.newrelic.com", "graphql"],
        safe_patterns: create_safe_patterns(),
        destructive_patterns: create_destructive_patterns(),
        keyword_matcher: None,
        safe_regex_set: None,
        safe_regex_set_is_complete: false,
    }
}

fn create_safe_patterns() -> Vec<SafePattern> {
    vec![
        safe_pattern!(
            "newrelic-entity-search",
            r"\bnewrelic\b(?:\s+--?\S+(?:\s+\S+)?)*\s+entity\s+search\b"
        ),
        safe_pattern!(
            "newrelic-apm-app-get",
            r"\bnewrelic\b(?:\s+--?\S+(?:\s+\S+)?)*\s+apm\s+application\s+get\b"
        ),
        safe_pattern!(
            "newrelic-query",
            r"\bnewrelic\b(?:\s+--?\S+(?:\s+\S+)?)*\s+query\b"
        ),
        safe_pattern!(
            "newrelic-api-get",
            r"(?i)curl\s+.*(?:-X|--request)\s+GET\b.*api\.newrelic\.com"
        ),
    ]
}

fn create_destructive_patterns() -> Vec<DestructivePattern> {
    vec![
        destructive_pattern!(
            "newrelic-entity-delete",
            r"\bnewrelic\b(?:\s+--?\S+(?:\s+\S+)?)*\s+entity\s+delete\b",
            "newrelic entity delete removes a New Relic entity, impacting observability.",
            High,
            "Deleting a New Relic entity removes all associated telemetry data, relationships, \
             and alert configurations. Historical metrics for this entity will no longer be \
             accessible.\n\n\
             Safer alternatives:\n\
             - newrelic entity search: Find and verify the entity first\n\
             - Tag the entity as deprecated instead of deleting\n\
             - Export entity configuration before deletion"
        ),
        destructive_pattern!(
            "newrelic-apm-app-delete",
            r"\bnewrelic\b(?:\s+--?\S+(?:\s+\S+)?)*\s+apm\s+application\s+delete\b",
            "newrelic apm application delete removes an APM application.",
            High,
            "Deleting an APM application removes all application performance data, traces, \
             and associated alert policies. You lose visibility into application behavior \
             and historical performance trends.\n\n\
             Safer alternatives:\n\
             - newrelic apm application get <id>: Review application details first\n\
             - Disable the APM agent in your application instead\n\
             - Export application settings and dashboards as backup"
        ),
        destructive_pattern!(
            "newrelic-workload-delete",
            r"\bnewrelic\b(?:\s+--?\S+(?:\s+\S+)?)*\s+workload\s+delete\b",
            "newrelic workload delete removes a workload definition.",
            High,
            "Deleting a workload removes the logical grouping of entities and any associated \
             health status calculations. Teams using this workload for service overview will \
             lose their aggregated view.\n\n\
             Safer alternatives:\n\
             - Review workload entities and dependencies first\n\
             - Export workload definition using the API\n\
             - Modify the workload rather than deleting and recreating"
        ),
        destructive_pattern!(
            "newrelic-synthetics-delete",
            r"\bnewrelic\b(?:\s+--?\S+(?:\s+\S+)?)*\s+synthetics\s+delete\b",
            "newrelic synthetics delete removes a synthetics monitor.",
            High,
            "Deleting a synthetics monitor stops all uptime and availability checking for \
             the monitored endpoint. You will no longer receive alerts when the endpoint \
             becomes unavailable or slow.\n\n\
             Safer alternatives:\n\
             - Disable the monitor temporarily instead of deleting\n\
             - Export monitor configuration as code (Terraform/Pulumi)\n\
             - newrelic synthetics search: Verify you're deleting the correct monitor"
        ),
        destructive_pattern!(
            "newrelic-api-delete",
            r"(?i)curl\s+.*(?:-X|--request)\s+DELETE\b.*api\.newrelic\.com",
            "New Relic API DELETE calls remove monitoring/alerting resources.",
            High,
            "Direct API DELETE calls permanently remove New Relic resources without \
             confirmation. Alert policies, dashboards, and monitors are deleted immediately.\n\n\
             Safer alternatives:\n\
             - GET the resource first to verify the ID and export configuration\n\
             - Use the New Relic CLI for better feedback and confirmation\n\
             - Use Terraform/Pulumi for version-controlled resource management"
        ),
        destructive_pattern!(
            "newrelic-graphql-delete-mutation",
            r"(?i)curl\s+.*api\.newrelic\.com[^\s]*?/graphql\b.*\bmutation\b.*\bdelete\w*\b",
            "New Relic GraphQL delete mutations can remove monitoring resources.",
            High,
            "GraphQL delete mutations remove New Relic resources via the NerdGraph API. \
             This affects entities, dashboards, alert policies, and other observability \
             resources.\n\n\
             Safer alternatives:\n\
             - Run a query first to verify the GUID or resource ID\n\
             - Use the New Relic CLI or UI for deletion with better feedback\n\
             - Export the resource definition before deletion"
        ),
    ]
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::packs::test_helpers::*;

    #[test]
    fn test_pack_creation() {
        let pack = create_pack();
        assert_eq!(pack.id, "monitoring.newrelic");
        assert_eq!(pack.name, "New Relic");
        assert!(!pack.description.is_empty());
        assert!(pack.keywords.contains(&"newrelic"));

        assert_patterns_compile(&pack);
        assert_all_patterns_have_reasons(&pack);
        assert_unique_pattern_names(&pack);
    }

    #[test]
    fn allows_safe_commands() {
        let pack = create_pack();
        assert_safe_pattern_matches(&pack, "newrelic entity search --name my-service");
        assert_safe_pattern_matches(&pack, "newrelic apm application get 123");
        assert_safe_pattern_matches(&pack, "newrelic query \"SELECT count(*) FROM Transaction\"");
        assert_safe_pattern_matches(
            &pack,
            "curl -X GET https://api.newrelic.com/v2/alerts_policies.json",
        );
    }

    #[test]
    fn blocks_destructive_commands() {
        let pack = create_pack();
        assert_blocks_with_pattern(
            &pack,
            "newrelic entity delete 123",
            "newrelic-entity-delete",
        );
        assert_blocks_with_pattern(
            &pack,
            "newrelic apm application delete 123",
            "newrelic-apm-app-delete",
        );
        assert_blocks_with_pattern(
            &pack,
            "newrelic workload delete 123",
            "newrelic-workload-delete",
        );
        assert_blocks_with_pattern(
            &pack,
            "newrelic synthetics delete 123",
            "newrelic-synthetics-delete",
        );
        assert_blocks_with_pattern(
            &pack,
            "curl -X DELETE https://api.newrelic.com/v2/alerts_policies/123.json",
            "newrelic-api-delete",
        );
        assert_blocks_with_pattern(
            &pack,
            r#"curl -X POST https://api.newrelic.com/graphql -d '{"query":"mutation { deleteEntity(guid: \"abc\") }"}'"#,
            "newrelic-graphql-delete-mutation",
        );
    }
}