Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

alert_heartbeat_and_login_rate

A top-level && chain combining a formerly with an exists-guarded count (the login-rate threshold). Permit an Alert only if a Heartbeat for this server fired within the last hour and more than two Logins to this server occurred.

when temporal {
    formerly within 1h Drupe::Action::"Heartbeat"::request{ input.server: context.input.server }
    && exists (n: Long). (
        (count for (t: Timepoint). where (
            Drupe::Action::"Login"::request{ input.user: _, input.server: context.input.server } && tp(t)
        )) == n && n > 2
    )
};

Schema is lifted from the temporal_only corpus case 0059_count_threshold (it declares Heartbeat/Login/Alert with a server input); the trace is lifted from that case’s trace_1.log. The default event schema (request/response) is used.

What the trace shows

The trace fires a Heartbeat for s1, three Logins (alice, bob, carol) to s1, and two Alerts. Every timepoint replays to DENY:

  • The formerly ... Heartbeat conjunct is satisfied at the two Alert timepoints (a heartbeat fired within the window), so on its own it would allow.
  • But the count conjunct is never satisfied. Its body Login && tp(t) is not wrapped in a past-temporal operator, so tp(t) pins the count to the current timepoint (the Alert), where no Login fires. The count is therefore 0 at every Alert, so n > 2 is false.
  • Because the two conjuncts are joined by &&, the rule denies everywhere.

This is the intended semantics of the guide fragment as written: a bare count ... where (P && tp(t)) counts occurrences at the verdict timepoint, not across history. To count historical logins you would wrap the body in a formerly within 1h (...) (compare corpus 0179_agg_with_once_counts_history).

Referenced by guide/04-temporal-expressions.md.

Policy

// A top-level && chain combining a `formerly` with an exists-guarded count.
// Permit an Alert only if a Heartbeat for this server fired within the last
// hour AND more than two Logins to this server occurred.
@id("alert_heartbeat_and_login_rate")
permit (
    principal,
    action == Drupe::Action::"Alert",
    resource
)
when temporal {
    formerly within 1h Drupe::Action::"Heartbeat"::request{ input.server: context.input.server }
    && exists (n: Long). (
        (count for (t: Timepoint). where (
            Drupe::Action::"Login"::request{ input.user: _, input.server: context.input.server } && tp(t)
        )) == n && n > 2
    )
};

Schema

namespace Drupe {
  type AlertInput = {
    level: Long,
    server: String
  };

  type AlertOutput = {  };

  type ContentFilterFinding = {
    severityScore: decimal
  };

  type HeartbeatInput = {
    server: String
  };

  type HeartbeatOutput = {  };

  type LoginInput = {
    server: String,
    user: String
  };

  type LoginOutput = {  };

  type PromptAttackFinding = {
    severityScore: decimal
  };

  type ReadInput = {
    document: String,
    user: String
  };

  type ReadOutput = {  };

  type SensitiveInfoFinding = {
    confidenceScore: decimal
  };

  type SystemContext = {
    now: datetime
  };

  type TransferInput = {
    amount: Long,
    user: String
  };

  type TransferOutput = {  };

  entity Gateway;

  entity IamEntity = {
    id: String
  };

  entity OAuthUser = {
    id: String
  } tags String;

  entity UnauthenticatedUser;

  action "Alert" in [Action::"CallTool"] appliesTo {
    principal: [IamEntity, OAuthUser, UnauthenticatedUser],
    resource: [Gateway],
    context: {
      input: AlertInput,
      output?: AlertOutput,
      system: SystemContext
    }
  };

  action "CallTool" in [Action::"Mcp"] appliesTo {
    principal: [OAuthUser, IamEntity, UnauthenticatedUser],
    resource: [Gateway],
    context: {
      system: SystemContext
    }
  };

  action "Heartbeat" in [Action::"CallTool"] appliesTo {
    principal: [IamEntity, OAuthUser, UnauthenticatedUser],
    resource: [Gateway],
    context: {
      input: HeartbeatInput,
      output?: HeartbeatOutput,
      system: SystemContext
    }
  };

  action "Http" appliesTo {
    principal: [OAuthUser, IamEntity, UnauthenticatedUser],
    resource: [Gateway],
    context: {
      system: SystemContext
    }
  };

  action "InvokeAgent" in [Action::"Http"] appliesTo {
    principal: [OAuthUser, IamEntity, UnauthenticatedUser],
    resource: [Gateway],
    context: {
      input?: {      },
      system: SystemContext
    }
  };

  action "InvokeLLM" in [Action::"Http"] appliesTo {
    principal: [OAuthUser, IamEntity, UnauthenticatedUser],
    resource: [Gateway],
    context: {
      input?: {      },
      system: SystemContext
    }
  };

  action "Login" in [Action::"CallTool"] appliesTo {
    principal: [IamEntity, OAuthUser, UnauthenticatedUser],
    resource: [Gateway],
    context: {
      input: LoginInput,
      output?: LoginOutput,
      system: SystemContext
    }
  };

  action "Mcp" appliesTo {
    principal: [OAuthUser, IamEntity, UnauthenticatedUser],
    resource: [Gateway],
    context: {
      system: SystemContext
    }
  };

  action "Read" in [Action::"CallTool"] appliesTo {
    principal: [IamEntity, OAuthUser, UnauthenticatedUser],
    resource: [Gateway],
    context: {
      input: ReadInput,
      output?: ReadOutput,
      system: SystemContext
    }
  };

  action "Transfer" in [Action::"CallTool"] appliesTo {
    principal: [IamEntity, OAuthUser, UnauthenticatedUser],
    resource: [Gateway],
    context: {
      input: TransferInput,
      output?: TransferOutput,
      system: SystemContext
    }
  };

  action "UnknownTool" in [Action::"CallTool"] appliesTo {
    principal: [OAuthUser, IamEntity, UnauthenticatedUser],
    resource: [Gateway],
    context: {
      system: SystemContext
    }
  };
}

Trace

@0 scope(principal: Drupe::OAuthUser::"s1", resource: Drupe::Gateway::"gw1") request_context(input: { server: "s1" }) Drupe::Action::"Heartbeat"::request(input: { server: "s1" }, callerPrincipal: Drupe::OAuthUser::"s1", callerResource: Drupe::Gateway::"gw1", requestId: "u1")
@1 scope(principal: Drupe::OAuthUser::"s1", resource: Drupe::Gateway::"gw1") Drupe::Action::"Heartbeat"::response(input: { server: "s1" }, callerPrincipal: Drupe::OAuthUser::"s1", callerResource: Drupe::Gateway::"gw1", requestId: "u1")
@2 scope(principal: Drupe::OAuthUser::"alice", resource: Drupe::Gateway::"gw1") request_context(input: { server: "s1", user: "alice" }) Drupe::Action::"Login"::request(input: { server: "s1", user: "alice" }, callerPrincipal: Drupe::OAuthUser::"alice", callerResource: Drupe::Gateway::"gw1", requestId: "u2")
@3 scope(principal: Drupe::OAuthUser::"alice", resource: Drupe::Gateway::"gw1") Drupe::Action::"Login"::response(input: { server: "s1", user: "alice" }, callerPrincipal: Drupe::OAuthUser::"alice", callerResource: Drupe::Gateway::"gw1", requestId: "u2")
@4 scope(principal: Drupe::OAuthUser::"bob", resource: Drupe::Gateway::"gw1") request_context(input: { server: "s1", user: "bob" }) Drupe::Action::"Login"::request(input: { server: "s1", user: "bob" }, callerPrincipal: Drupe::OAuthUser::"bob", callerResource: Drupe::Gateway::"gw1", requestId: "u3")
@5 scope(principal: Drupe::OAuthUser::"bob", resource: Drupe::Gateway::"gw1") Drupe::Action::"Login"::response(input: { server: "s1", user: "bob" }, callerPrincipal: Drupe::OAuthUser::"bob", callerResource: Drupe::Gateway::"gw1", requestId: "u3")
@10 scope(principal: Drupe::OAuthUser::"s1", resource: Drupe::Gateway::"gw1") request_context(input: { level: 1, server: "s1" }) Drupe::Action::"Alert"::request(input: { level: 1, server: "s1" }, callerPrincipal: Drupe::OAuthUser::"s1", callerResource: Drupe::Gateway::"gw1", requestId: "u4")
@11 scope(principal: Drupe::OAuthUser::"s1", resource: Drupe::Gateway::"gw1") Drupe::Action::"Alert"::response(input: { level: 1, server: "s1" }, callerPrincipal: Drupe::OAuthUser::"s1", callerResource: Drupe::Gateway::"gw1", requestId: "u4")
@20 scope(principal: Drupe::OAuthUser::"carol", resource: Drupe::Gateway::"gw1") request_context(input: { server: "s1", user: "carol" }) Drupe::Action::"Login"::request(input: { server: "s1", user: "carol" }, callerPrincipal: Drupe::OAuthUser::"carol", callerResource: Drupe::Gateway::"gw1", requestId: "u5")
@21 scope(principal: Drupe::OAuthUser::"carol", resource: Drupe::Gateway::"gw1") Drupe::Action::"Login"::response(input: { server: "s1", user: "carol" }, callerPrincipal: Drupe::OAuthUser::"carol", callerResource: Drupe::Gateway::"gw1", requestId: "u5")
@30 scope(principal: Drupe::OAuthUser::"s1", resource: Drupe::Gateway::"gw1") request_context(input: { level: 2, server: "s1" }) Drupe::Action::"Alert"::request(input: { level: 2, server: "s1" }, callerPrincipal: Drupe::OAuthUser::"s1", callerResource: Drupe::Gateway::"gw1", requestId: "u6")
@31 scope(principal: Drupe::OAuthUser::"s1", resource: Drupe::Gateway::"gw1") Drupe::Action::"Alert"::response(input: { level: 2, server: "s1" }, callerPrincipal: Drupe::OAuthUser::"s1", callerResource: Drupe::Gateway::"gw1", requestId: "u6")

Expected Output

@0 (time point 0): DENY
@2 (time point 1): DENY
@4 (time point 2): DENY
@10 (time point 3): DENY
@20 (time point 4): DENY
@30 (time point 5): DENY