@highflame/policy 2.2.41 → 2.2.42

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.
@@ -779,6 +779,306 @@
779
779
  }
780
780
  ]
781
781
  },
782
+ {
783
+ "name": "list",
784
+ "description": "Enumerate a server's tools, prompts, or resources — threat focus: tool poisoning and rug pull in the returned descriptions",
785
+ "context_attributes": [
786
+ {
787
+ "key": "role",
788
+ "type": "string",
789
+ "required": false,
790
+ "description": "Caller's RBAC role projected from the principal's token (AARM R6 / CAP-IDN-011)."
791
+ },
792
+ {
793
+ "key": "privilege_scope",
794
+ "type": "array",
795
+ "required": false,
796
+ "description": "Privilege-scope strings granted to the caller, projected from the token (AARM R6 / CAP-IDN-011)."
797
+ },
798
+ {
799
+ "key": "content",
800
+ "type": "string",
801
+ "required": false,
802
+ "description": "The enumerated item's description — the text scanned for poisoning. Absent when the item declares none."
803
+ },
804
+ {
805
+ "key": "mcp_server",
806
+ "type": "string",
807
+ "required": false,
808
+ "description": "MCP server being enumerated"
809
+ },
810
+ {
811
+ "key": "tool_name",
812
+ "type": "string",
813
+ "required": false,
814
+ "description": "Name of the enumerated item being evaluated"
815
+ },
816
+ {
817
+ "key": "tool_risk_score",
818
+ "type": "number",
819
+ "required": false,
820
+ "description": "Tool risk score"
821
+ },
822
+ {
823
+ "key": "tool_category",
824
+ "type": "string",
825
+ "required": false,
826
+ "description": "\"safe\", \"sensitive\", \"dangerous\""
827
+ },
828
+ {
829
+ "key": "tool_is_sensitive",
830
+ "type": "boolean",
831
+ "required": false,
832
+ "description": "Tool is sensitive"
833
+ },
834
+ {
835
+ "key": "tool_is_builtin",
836
+ "type": "boolean",
837
+ "required": false,
838
+ "description": "Tool is builtin"
839
+ },
840
+ {
841
+ "key": "tool_operation_classes",
842
+ "type": "array",
843
+ "required": false,
844
+ "description": "Tool operation classes"
845
+ },
846
+ {
847
+ "key": "threat_count",
848
+ "type": "number",
849
+ "required": false,
850
+ "description": "Total threats detected"
851
+ },
852
+ {
853
+ "key": "highest_severity",
854
+ "type": "string",
855
+ "required": false,
856
+ "description": "Highest threat severity"
857
+ },
858
+ {
859
+ "key": "threat_categories",
860
+ "type": "array",
861
+ "required": false,
862
+ "description": "Threat category names"
863
+ },
864
+ {
865
+ "key": "detected_threats",
866
+ "type": "array",
867
+ "required": false,
868
+ "description": "Detection rule names that matched"
869
+ },
870
+ {
871
+ "key": "max_threat_severity",
872
+ "type": "number",
873
+ "required": false,
874
+ "description": "Numeric severity (0=none, 4=critical)"
875
+ },
876
+ {
877
+ "key": "tool_poisoning_score",
878
+ "type": "number",
879
+ "required": false,
880
+ "description": "Hidden instructions in a tool description"
881
+ },
882
+ {
883
+ "key": "tool_poisoning_detected",
884
+ "type": "boolean",
885
+ "required": false,
886
+ "description": "Hidden instructions found in the description"
887
+ },
888
+ {
889
+ "key": "rug_pull_score",
890
+ "type": "number",
891
+ "required": false,
892
+ "description": "Description drift after trust establishment"
893
+ },
894
+ {
895
+ "key": "rug_pull_detected",
896
+ "type": "boolean",
897
+ "required": false,
898
+ "description": "Description drifted after trust was established"
899
+ },
900
+ {
901
+ "key": "indirect_injection_score",
902
+ "type": "number",
903
+ "required": false,
904
+ "description": "Indirect injection score"
905
+ },
906
+ {
907
+ "key": "injection_score",
908
+ "type": "number",
909
+ "required": false,
910
+ "description": "Prompt injection classifier confidence (0-100)"
911
+ },
912
+ {
913
+ "key": "jailbreak_score",
914
+ "type": "number",
915
+ "required": false,
916
+ "description": "Jailbreak classifier confidence (0-100)"
917
+ },
918
+ {
919
+ "key": "secrets_detected",
920
+ "type": "boolean",
921
+ "required": false,
922
+ "description": "Whether secrets were detected in the description"
923
+ },
924
+ {
925
+ "key": "secret_types",
926
+ "type": "array",
927
+ "required": false,
928
+ "description": "Secret types detected"
929
+ },
930
+ {
931
+ "key": "secret_count",
932
+ "type": "number",
933
+ "required": false,
934
+ "description": "Number of secrets detected"
935
+ },
936
+ {
937
+ "key": "pii_detected",
938
+ "type": "boolean",
939
+ "required": false,
940
+ "description": "Whether PII was detected"
941
+ },
942
+ {
943
+ "key": "pii_types",
944
+ "type": "array",
945
+ "required": false,
946
+ "description": "PII types detected"
947
+ },
948
+ {
949
+ "key": "pii_count",
950
+ "type": "number",
951
+ "required": false,
952
+ "description": "Number of PII entities"
953
+ },
954
+ {
955
+ "key": "pii_score",
956
+ "type": "number",
957
+ "required": false,
958
+ "description": "PII confidence (0-100)"
959
+ },
960
+ {
961
+ "key": "mcp_server_verified",
962
+ "type": "boolean",
963
+ "required": false,
964
+ "description": "Whether the server is from a verified registry"
965
+ },
966
+ {
967
+ "key": "mcp_config_risk",
968
+ "type": "boolean",
969
+ "required": false,
970
+ "description": "Server config flagged risky"
971
+ },
972
+ {
973
+ "key": "mcp_risk_score",
974
+ "type": "number",
975
+ "required": false,
976
+ "description": "Server risk score (0-100)"
977
+ },
978
+ {
979
+ "key": "session_threat_turns",
980
+ "type": "number",
981
+ "required": false,
982
+ "description": "Session-aggregated threat turns across prior turns"
983
+ },
984
+ {
985
+ "key": "session_cumulative_risk_score",
986
+ "type": "number",
987
+ "required": false,
988
+ "description": "Session-aggregated cumulative risk score across prior turns"
989
+ },
990
+ {
991
+ "key": "session_injection_detected",
992
+ "type": "boolean",
993
+ "required": false,
994
+ "description": "Session-aggregated injection detected across prior turns"
995
+ },
996
+ {
997
+ "key": "session_command_injection",
998
+ "type": "boolean",
999
+ "required": false,
1000
+ "description": "Session-aggregated command injection across prior turns"
1001
+ },
1002
+ {
1003
+ "key": "session_pii_detected",
1004
+ "type": "boolean",
1005
+ "required": false,
1006
+ "description": "Session-aggregated pii detected across prior turns"
1007
+ },
1008
+ {
1009
+ "key": "session_pii_types",
1010
+ "type": "array",
1011
+ "required": false,
1012
+ "description": "Session-aggregated pii types across prior turns"
1013
+ },
1014
+ {
1015
+ "key": "session_secrets_detected",
1016
+ "type": "boolean",
1017
+ "required": false,
1018
+ "description": "Session-aggregated secrets detected across prior turns"
1019
+ },
1020
+ {
1021
+ "key": "session_secret_types",
1022
+ "type": "array",
1023
+ "required": false,
1024
+ "description": "Session-aggregated secret types across prior turns"
1025
+ },
1026
+ {
1027
+ "key": "session_max_injection_score",
1028
+ "type": "number",
1029
+ "required": false,
1030
+ "description": "Session-aggregated max injection score across prior turns"
1031
+ },
1032
+ {
1033
+ "key": "session_max_jailbreak_score",
1034
+ "type": "number",
1035
+ "required": false,
1036
+ "description": "Session-aggregated max jailbreak score across prior turns"
1037
+ },
1038
+ {
1039
+ "key": "session_max_command_injection_score",
1040
+ "type": "number",
1041
+ "required": false,
1042
+ "description": "Session-aggregated max command injection score across prior turns"
1043
+ },
1044
+ {
1045
+ "key": "session_max_pii_score",
1046
+ "type": "number",
1047
+ "required": false,
1048
+ "description": "Session-aggregated max pii score across prior turns"
1049
+ },
1050
+ {
1051
+ "key": "session_max_secret_score",
1052
+ "type": "number",
1053
+ "required": false,
1054
+ "description": "Session-aggregated max secret score across prior turns"
1055
+ },
1056
+ {
1057
+ "key": "rpm_remaining_pct",
1058
+ "type": "number",
1059
+ "required": false,
1060
+ "description": "Requests-per-minute headroom remaining"
1061
+ },
1062
+ {
1063
+ "key": "rpm_exceeded",
1064
+ "type": "boolean",
1065
+ "required": false,
1066
+ "description": "RPM limit exceeded"
1067
+ },
1068
+ {
1069
+ "key": "tpm_remaining_pct",
1070
+ "type": "number",
1071
+ "required": false,
1072
+ "description": "Tokens-per-minute headroom remaining"
1073
+ },
1074
+ {
1075
+ "key": "tpm_exceeded",
1076
+ "type": "boolean",
1077
+ "required": false,
1078
+ "description": "TPM limit exceeded"
1079
+ }
1080
+ ]
1081
+ },
782
1082
  {
783
1083
  "name": "process_prompt",
784
1084
  "description": "Process a prompt (MCP or LLM chat completion) — threat focus: injection, jailbreak, secrets, PII, content safety",
@@ -74,7 +74,7 @@ entity FilePath in [Project];
74
74
  // ACTIONS
75
75
  // =============================================================================
76
76
 
77
- // Call an MCP tool
77
+ // Call an MCP tool (tools/call). Enumeration is the `list` action.
78
78
  // Threat focus: command injection, tool poisoning, rug pull, secrets, PII
79
79
  action call_tool appliesTo {
80
80
  principal: [User, MCP_Client],
@@ -274,6 +274,108 @@ action connect_server appliesTo {
274
274
  },
275
275
  };
276
276
 
277
+ // Enumerate a server's tools, prompts, or resources
278
+ // (tools/list, prompts/list, resources/list)
279
+ //
280
+ // Threat focus: tool poisoning and rug pull in the DESCRIPTIONS returned by
281
+ // enumeration — the text a host feeds the model when it assembles the
282
+ // session's tool schema, and the vector a rug-pull exploits between scans.
283
+ //
284
+ // Distinct from the actions it enumerates, and deliberately NOT default-locked
285
+ // (highflame-shield#515 / INV-AUTHZ-003). Listing is host plumbing: the model
286
+ // never decides to enumerate, the host does it to build the session. Folding
287
+ // it into call_tool / process_prompt / read_file made enumeration inherit
288
+ // their lock, and because a list names no tool, no conditioned grant
289
+ // (`context has tool_name && ...`) can ever match it — so an agent holding a
290
+ // correct grant for every tool it uses was still denied the enumeration step,
291
+ // and without tools/list it cannot call anything at all. The only grant that
292
+ // could match was an unconditioned one, which would have lifted the lock for
293
+ // every call as well. There is no way to express "may list, may not call"
294
+ // while a list and a call are the same action. See highflame-firehog#589.
295
+ //
296
+ // Authorization over WHAT may be enumerated is expressed through the list
297
+ // CONTENTS (the grant-derived list filter), not by denying the request.
298
+ action list appliesTo {
299
+ principal: [User, MCP_Client],
300
+ resource: [Server],
301
+ context: {
302
+ // Identity (AARM R6 / CAP-IDN-011) — projected from the principal's token; optional.
303
+ role?: String,
304
+ privilege_scope?: Set<String>,
305
+
306
+ // --- Content ---
307
+ // The enumerated item's description. Optional: a tool may declare none,
308
+ // and a policy must still evaluate when it does not.
309
+ content?: String,
310
+ mcp_server?: String,
311
+
312
+ // --- Enumerated item identity ---
313
+ // The list is scanned one item at a time, so the item being evaluated
314
+ // rides here while the resource stays the server being enumerated.
315
+ tool_name?: String,
316
+ tool_risk_score?: Long,
317
+ tool_category?: String, // "safe", "sensitive", "dangerous"
318
+ tool_is_sensitive?: Bool,
319
+ tool_is_builtin?: Bool,
320
+ tool_operation_classes?: Set<String>,
321
+
322
+ // --- Threat Detection ---
323
+ threat_count?: Long,
324
+ highest_severity?: String,
325
+ threat_categories?: Set<String>,
326
+ detected_threats?: Set<String>,
327
+ max_threat_severity?: Long,
328
+
329
+ // --- Agent Security (0-100) — the reason this action scans at all ---
330
+ tool_poisoning_score?: Long, // Hidden instructions in a tool description
331
+ tool_poisoning_detected?: Bool,
332
+ rug_pull_score?: Long, // Description drift after trust establishment
333
+ rug_pull_detected?: Bool,
334
+ indirect_injection_score?: Long,
335
+
336
+ // --- ML Detector Confidence Scores (0-100) ---
337
+ injection_score?: Long,
338
+ jailbreak_score?: Long,
339
+
340
+ // --- Secrets ---
341
+ secrets_detected?: Bool,
342
+ secret_types?: Set<String>,
343
+ secret_count?: Long,
344
+
345
+ // --- PII Detection ---
346
+ pii_detected?: Bool,
347
+ pii_types?: Set<String>,
348
+ pii_count?: Long,
349
+ pii_score?: Long,
350
+
351
+ // --- MCP Trust & Config Risk ---
352
+ mcp_server_verified?: Bool,
353
+ mcp_config_risk?: Bool,
354
+ mcp_risk_score?: Long,
355
+
356
+ // --- Session-Aware Threat Aggregation ---
357
+ session_threat_turns?: Long,
358
+ session_cumulative_risk_score?: Long,
359
+ session_injection_detected?: Bool,
360
+ session_command_injection?: Bool,
361
+ session_pii_detected?: Bool,
362
+ session_pii_types?: Set<String>,
363
+ session_secrets_detected?: Bool,
364
+ session_secret_types?: Set<String>,
365
+ session_max_injection_score?: Long,
366
+ session_max_jailbreak_score?: Long,
367
+ session_max_command_injection_score?: Long,
368
+ session_max_pii_score?: Long,
369
+ session_max_secret_score?: Long,
370
+
371
+ // Rate Limiting — gateway-metered, Shield-decided (ADR 0014)
372
+ rpm_remaining_pct?: Long,
373
+ rpm_exceeded?: Bool,
374
+ tpm_remaining_pct?: Long,
375
+ tpm_exceeded?: Bool,
376
+ },
377
+ };
378
+
277
379
  // Shared content-inspection context.
278
380
  //
279
381
  // Prompts and responses are inspected by the same detector pipeline and so
@@ -361,7 +463,8 @@ type ContentInspectionContext = {
361
463
  tpm_exceeded?: Bool,
362
464
  };
363
465
 
364
- // Process a prompt (MCP prompts/get or LLM chat completions)
466
+ // Process a prompt (MCP prompts/get or LLM chat completions).
467
+ // Enumeration (prompts/list) is the `list` action.
365
468
  // Threat focus: injection, jailbreak, secrets, PII, content safety
366
469
  action process_prompt appliesTo {
367
470
  principal: [User, MCP_Client],
@@ -384,7 +487,8 @@ action process_response appliesTo {
384
487
  context: ContentInspectionContext,
385
488
  };
386
489
 
387
- // Read an MCP resource (resources/read, resources/list)
490
+ // Read an MCP resource (resources/read). Enumeration (resources/list)
491
+ // is the `list` action.
388
492
  // Threat focus: secrets exposure, PII exposure, sensitive paths
389
493
  action read_file appliesTo {
390
494
  principal: [User, MCP_Client],
@@ -0,0 +1,35 @@
1
+ // =============================================================================
2
+ // Output Protection — Credentials
3
+ // =============================================================================
4
+ // Split from data_leakage.cedar: secrets are category `data-protection`, PII is
5
+ // `privacy`, and a template wrapper may only carry rules of its own category
6
+ // (HFP-LINT-TMPL-006). Relabelling one to fit the other would put a wrong
7
+ // category on the wire, where it drives signals[] and the severity rollup.
8
+ //
9
+ // Same scoping rule as the rest of this profile — process_response ALONE, so a
10
+ // tenant can police what the model says without touching what users send.
11
+ //
12
+ // Context keys consumed:
13
+ // - secrets_detected: Bool
14
+ //
15
+ // Compliance:
16
+ // - OWASP LLM02 (Sensitive Information Disclosure)
17
+ //
18
+ // Category: data-protection
19
+ // Namespace: Guardrails
20
+ // =============================================================================
21
+
22
+ @id("data-protection.output-block-secrets")
23
+ @name("Block secrets in model responses")
24
+ @description("Blocks process_response when the model's own output contains credentials, API keys or tokens — the canonical way a leaked secret reaches a caller.")
25
+ @severity("critical")
26
+ @tags("category:data-protection,threat:secrets,surface:process-response,detection:rule,owasp:llm02")
27
+ @reject_message("Response blocked: the model's output contained credentials.")
28
+ forbid (
29
+ principal,
30
+ action == Guardrails::Action::"process_response",
31
+ resource
32
+ )
33
+ when {
34
+ context has secrets_detected && context.secrets_detected == true
35
+ };
@@ -0,0 +1,63 @@
1
+ // =============================================================================
2
+ // Output Protection — Data Leakage
3
+ // =============================================================================
4
+ // Guards what the MODEL SAYS BACK, not what the user sends in.
5
+ //
6
+ // Every other guardrails template binds process_response alongside
7
+ // process_prompt and the tool/file actions, which is right for a threat that
8
+ // is the same in both directions. Data leakage is not that: the risk is
9
+ // specific to egress, and a tenant who wants to police only their outputs had
10
+ // no template to enable. That gap is why one deployed tenant hand-authored a
11
+ // rule named "Block PII in outputs" and bound it to process_prompt — the only
12
+ // action that covered responses before the direction split (ADR 0031).
13
+ //
14
+ // These rules bind process_response ALONE. That is the point: ADR 0031 created
15
+ // the distinct trigger precisely so a policy could be scoped to one direction
16
+ // from its head, and this profile is what that is for. Enabling it cannot
17
+ // change how prompts are treated.
18
+ //
19
+ // Context keys consumed:
20
+ // - pii_detected: Bool
21
+ // - pii_count: Long
22
+ //
23
+ // Compliance:
24
+ // - OWASP LLM02 (Sensitive Information Disclosure), OWASP LLM06
25
+ //
26
+ // Category: privacy
27
+ // Namespace: Guardrails
28
+ // =============================================================================
29
+
30
+ @id("privacy.output-block-pii")
31
+ @name("Block PII in model responses")
32
+ @description("Blocks process_response when the model's own output contains PII. Scoped to the response direction only — prompts are unaffected.")
33
+ @severity("high")
34
+ @tags("category:privacy,threat:data-leak,surface:process-response,detection:rule,owasp:llm02")
35
+ @reject_message("Response blocked: the model's output contained personal data.")
36
+ forbid (
37
+ principal,
38
+ action == Guardrails::Action::"process_response",
39
+ resource
40
+ )
41
+ when {
42
+ context has pii_detected && context.pii_detected == true
43
+ };
44
+
45
+ // Bulk disclosure is a separate finding from a single incidental match: a
46
+ // response carrying many distinct PII items is an exfiltration shape rather
47
+ // than a mention, and is worth its own severity and message even though the
48
+ // rule above already blocks it. Kept as a distinct rule so a tenant running
49
+ // the first in monitor mode can still enforce on bulk.
50
+ @id("privacy.output-block-bulk-pii")
51
+ @name("Block bulk PII disclosure in model responses")
52
+ @description("Blocks process_response when the output carries five or more PII items — a disclosure shape rather than an incidental mention.")
53
+ @severity("critical")
54
+ @tags("category:privacy,threat:data-leak,surface:process-response,detection:rule,owasp:llm06")
55
+ @reject_message("Response blocked: the model's output contained bulk personal data.")
56
+ forbid (
57
+ principal,
58
+ action == Guardrails::Action::"process_response",
59
+ resource
60
+ )
61
+ when {
62
+ context has pii_count && context.pii_count >= 5
63
+ };
@@ -0,0 +1,58 @@
1
+ // =============================================================================
2
+ // Output Protection — Harmful Content
3
+ // =============================================================================
4
+ // The companion to data_leakage.cedar: what the model says back, judged for
5
+ // safety rather than for disclosure.
6
+ //
7
+ // Same scoping rule and same reason — process_response ALONE (ADR 0031). A
8
+ // tenant enabling this is saying "hold my model's answers to a standard",
9
+ // which is a different decision from "police what users send me", and the two
10
+ // should be separately enableable.
11
+ //
12
+ // Thresholds match the org-wide toxicity defaults rather than tightening them.
13
+ // An output profile that silently enforced stricter limits than the tenant's
14
+ // existing prompt rules would surprise anyone comparing the two.
15
+ //
16
+ // Context keys consumed:
17
+ // - violence_score: Long (0-100)
18
+ // - hate_speech_score: Long (0-100)
19
+ // - sexual_score: Long (0-100)
20
+ // - injection_score: Long (0-100)
21
+ //
22
+ // Compliance:
23
+ // - OWASP LLM05 (Improper Output Handling), OWASP LLM01
24
+ //
25
+ // Category: trust-safety
26
+ // Namespace: Guardrails
27
+ // =============================================================================
28
+
29
+ @id("trust-safety.output-block-toxic")
30
+ @name("Block toxic model responses")
31
+ @description("Blocks process_response when the model's own output scores high for violence or hate speech.")
32
+ @severity("high")
33
+ @tags("category:trust-safety,threat:harmful,surface:process-response,detection:ml,owasp:llm05")
34
+ @reject_message("Response blocked: the model's output was flagged as harmful.")
35
+ forbid (
36
+ principal,
37
+ action == Guardrails::Action::"process_response",
38
+ resource
39
+ )
40
+ when {
41
+ context has violence_score && context.violence_score >= 70 ||
42
+ context has hate_speech_score && context.hate_speech_score >= 70
43
+ };
44
+
45
+ @id("trust-safety.output-block-sexual")
46
+ @name("Block sexual content in model responses")
47
+ @description("Blocks process_response when the model's own output scores high for sexual content.")
48
+ @severity("critical")
49
+ @tags("category:trust-safety,threat:sexual,surface:process-response,detection:ml,owasp:llm05")
50
+ @reject_message("Response blocked: the model's output was flagged as explicit.")
51
+ forbid (
52
+ principal,
53
+ action == Guardrails::Action::"process_response",
54
+ resource
55
+ )
56
+ when {
57
+ context has sexual_score && context.sexual_score >= 91
58
+ };
@@ -0,0 +1,39 @@
1
+ // =============================================================================
2
+ // Output Protection — Injection Carried Back
3
+ // =============================================================================
4
+ // Split from harmful_content.cedar because its category is `security`, not
5
+ // `trust-safety`, and a template wrapper may only carry rules of its own
6
+ // category (HFP-LINT-TMPL-006). The split is the honest outcome: this is a
7
+ // different threat with a different owner, not a toxicity variant.
8
+ //
9
+ // Same scoping rule as the rest of this profile — process_response ALONE.
10
+ //
11
+ // Context keys consumed:
12
+ // - injection_score: Long (0-100)
13
+ //
14
+ // Compliance:
15
+ // - OWASP LLM05 (Improper Output Handling)
16
+ //
17
+ // Category: security
18
+ // Namespace: Guardrails
19
+ // =============================================================================
20
+
21
+ // Injection scored on an OUTPUT is a different finding from injection scored on
22
+ // a prompt. On the way in it is a user attacking the model; on the way out it
23
+ // is content the model is handing to the caller — a downstream agent, a
24
+ // renderer, or another tool — which is OWASP LLM05's improper output handling.
25
+ // Worth its own rule so the two can be tuned and reasoned about separately.
26
+ @id("security.output-block-injection-carried-back")
27
+ @name("Block injection payloads carried back in model responses")
28
+ @description("Blocks process_response when the output itself scores as an injection payload — content the caller may execute or forward.")
29
+ @severity("high")
30
+ @tags("category:security,threat:injection,surface:process-response,detection:ml,owasp:llm05")
31
+ @reject_message("Response blocked: the model's output contained an injection payload.")
32
+ forbid (
33
+ principal,
34
+ action == Guardrails::Action::"process_response",
35
+ resource
36
+ )
37
+ when {
38
+ context has injection_score && context.injection_score >= 80
39
+ };