resksecure 0.1.0__py3-none-any.whl

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.
resksecure/__init__.py ADDED
@@ -0,0 +1,57 @@
1
+ """
2
+ reskSecure - Bitmask-based LLM security firewall.
3
+
4
+ A policy-driven LogitsProcessor that restricts LLM output based on
5
+ capability bitmasks. Uses resklogits (VectorizedAhoCorasick) for
6
+ GPU-accelerated pattern matching.
7
+
8
+ Usage::
9
+
10
+ from resksecure import (
11
+ BitmaskLogitsProcessor,
12
+ PolicySet,
13
+ load_policy,
14
+ verify_tool_action,
15
+ )
16
+
17
+ policy_set = load_policy("config/policy.yaml")
18
+ processor = BitmaskLogitsProcessor(
19
+ mask=7,
20
+ model_name="mistralai/Mistral-7B-v0.1",
21
+ tokenizer=tokenizer,
22
+ policy_set=policy_set,
23
+ device="cuda",
24
+ )
25
+ outputs = model.generate(**inputs, logits_processor=[processor])
26
+ """
27
+
28
+ from .bitmask_processor import BitmaskLogitsProcessor
29
+ from .cache import TrieCache, trie_cache
30
+ from .policy_loader import (
31
+ PhraseRule,
32
+ Policy,
33
+ PolicySet,
34
+ ToolRule,
35
+ load_policy,
36
+ )
37
+ from .policy_watcher import PolicyWatcher
38
+ from .tool_guard import get_allowed_tools, verify_tool_action
39
+ from .trie_factory import build_ac_from_policy, get_or_build_ac
40
+
41
+ __version__ = "0.1.0"
42
+
43
+ __all__ = [
44
+ "BitmaskLogitsProcessor",
45
+ "TrieCache",
46
+ "trie_cache",
47
+ "PhraseRule",
48
+ "Policy",
49
+ "PolicySet",
50
+ "ToolRule",
51
+ "load_policy",
52
+ "PolicyWatcher",
53
+ "get_allowed_tools",
54
+ "verify_tool_action",
55
+ "build_ac_from_policy",
56
+ "get_or_build_ac",
57
+ ]
@@ -0,0 +1,152 @@
1
+ from __future__ import annotations
2
+ from typing import Dict, Optional
3
+
4
+ import torch
5
+ from resklogits import VectorizedAhoCorasick
6
+ from transformers import LogitsProcessor
7
+
8
+ from .policy_loader import Policy, PolicySet
9
+ from .trie_factory import get_or_build_ac
10
+
11
+
12
+ class BitmaskLogitsProcessor(LogitsProcessor):
13
+ """LogitsProcessor that applies bitmask-based policy filtering.
14
+
15
+ Uses VectorizedAhoCorasick from resklogits to detect and block
16
+ or penalize banned phrases during token generation.
17
+
18
+ Two severity modes:
19
+ - ``hard``: banned phrase tokens get logit = -inf (impossible to generate)
20
+ - ``bias``: banned phrase tokens get logit -= penalty (unlikely but not impossible)
21
+
22
+ Strict mode: forces EOS as soon as a banned pattern prefix is detected.
23
+
24
+ Usage::
25
+
26
+ processor = BitmaskLogitsProcessor(
27
+ mask=7,
28
+ model_name="mistralai/Mistral-7B-v0.1",
29
+ tokenizer=tokenizer,
30
+ policy_set=policy_set,
31
+ device="cuda",
32
+ )
33
+ outputs = model.generate(**inputs, logits_processor=[processor])
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ mask: int,
39
+ model_name: str,
40
+ tokenizer,
41
+ policy_set: PolicySet,
42
+ device: str = "cuda",
43
+ ):
44
+ super().__init__()
45
+ self.mask = mask
46
+ self.model_name = model_name
47
+ self.tokenizer = tokenizer
48
+ self.device = device
49
+ self.strict: bool = False
50
+
51
+ hard_ac, bias_ac, hard_meta, bias_meta, policy = get_or_build_ac(
52
+ mask, model_name, tokenizer, policy_set, device,
53
+ )
54
+
55
+ self.hard_ac: Optional[VectorizedAhoCorasick] = hard_ac
56
+ self.hard_modes: Dict[int, str] = hard_meta
57
+ self.bias_ac: Optional[VectorizedAhoCorasick] = bias_ac
58
+ self.bias_penalties: Dict[int, float] = {
59
+ pid: penalty for pid, penalty in bias_meta.items()
60
+ }
61
+ self.policy: Optional[Policy] = policy
62
+ self.strict = policy.strict if policy else False
63
+
64
+ self._hard_state: int = 0
65
+ self._bias_state: int = 0
66
+
67
+ def __call__(
68
+ self,
69
+ input_ids: torch.LongTensor,
70
+ scores: torch.FloatTensor,
71
+ ) -> torch.FloatTensor:
72
+ if input_ids.shape[0] != 1:
73
+ raise NotImplementedError("Batch size > 1 not supported")
74
+
75
+ if input_ids.shape[1] > 0:
76
+ last_token = int(input_ids[0, -1].item())
77
+ if self.hard_ac is not None:
78
+ self._hard_state = self.hard_ac.step(self._hard_state, last_token)
79
+ if self.bias_ac is not None:
80
+ self._bias_state = self.bias_ac.step(self._bias_state, last_token)
81
+
82
+ # Hard mode: effectively ban dangerous tokens (-1e9 ≈ -inf in fp32)
83
+ if self.hard_ac is not None and self.hard_ac.danger_mask.any():
84
+ scores[0, self.hard_ac.danger_mask] = torch.where(
85
+ scores[0, self.hard_ac.danger_mask] > -1e9,
86
+ torch.tensor(-1e9, device=scores.device),
87
+ scores[0, self.hard_ac.danger_mask],
88
+ )
89
+
90
+ # Bias mode: apply configured penalty to dangerous tokens
91
+ if self.bias_ac is not None and self.bias_ac.danger_mask.any():
92
+ penalty = self._get_bias_penalty()
93
+ if penalty != 0.0:
94
+ scores[0, self.bias_ac.danger_mask] += penalty
95
+
96
+ # Hard mode complete match: force EOS immediately
97
+ if self.hard_ac is not None and self.hard_ac.has_match(self._hard_state):
98
+ scores[0, :] = -float("inf")
99
+ eos_id = self.tokenizer.eos_token_id
100
+ if eos_id is not None:
101
+ scores[0, eos_id] = 0.0
102
+ return scores
103
+
104
+ # Strict mode: force EOS when on a path that leads to a hard match
105
+ if self.strict and self.hard_ac is not None:
106
+ if self._is_on_path_to_hard_match():
107
+ scores[0, :] = -float("inf")
108
+ eos_id = self.tokenizer.eos_token_id
109
+ if eos_id is not None:
110
+ scores[0, eos_id] = 0.0
111
+
112
+ return scores
113
+
114
+ def _get_bias_penalty(self) -> float:
115
+ """Get the bias penalty for currently matched patterns."""
116
+ if self.bias_ac is None:
117
+ return 0.0
118
+ if not self.bias_ac.has_match(self._bias_state):
119
+ return 0.0
120
+ matched = self.bias_ac.get_matched_patterns(self._bias_state)
121
+ penalty = 0.0
122
+ for pid in matched:
123
+ p = self.bias_penalties.get(pid, 0.0)
124
+ if p < penalty:
125
+ penalty = p
126
+ return penalty
127
+
128
+ def _is_on_path_to_hard_match(self) -> bool:
129
+ """Check if current AC state could reach a match in one step."""
130
+ if self.hard_ac is None:
131
+ return False
132
+ state = self._hard_state
133
+ if state == 0:
134
+ return False
135
+ for token, _child_state in self.hard_ac.trie.get(state, {}).items():
136
+ next_state = self.hard_ac.step(state, token)
137
+ if self.hard_ac.has_match(next_state):
138
+ return True
139
+ current = state
140
+ visited = set()
141
+ while current in self.hard_ac.failure and current not in visited:
142
+ visited.add(current)
143
+ current = self.hard_ac.failure[current]
144
+ for token, _child_state in self.hard_ac.trie.get(current, {}).items():
145
+ next_state = self.hard_ac.step(current, token)
146
+ if self.hard_ac.has_match(next_state):
147
+ return True
148
+ return False
149
+
150
+ def reset(self) -> None:
151
+ self._hard_state = 0
152
+ self._bias_state = 0
resksecure/cache.py ADDED
@@ -0,0 +1,53 @@
1
+ from __future__ import annotations
2
+ import time
3
+ from threading import Lock
4
+ from typing import Any, Dict, Optional, Tuple
5
+
6
+
7
+ class TrieCache:
8
+ def __init__(self, ttl: int = 300):
9
+ self._data: Dict[Tuple[int, str], Tuple[Any, float]] = {}
10
+ self._ttl = ttl
11
+ self._lock = Lock()
12
+
13
+ def get(self, key: Tuple[int, str]) -> Any:
14
+ with self._lock:
15
+ if key not in self._data:
16
+ return None
17
+ entry, timestamp = self._data[key]
18
+ if time.monotonic() - timestamp > self._ttl:
19
+ del self._data[key]
20
+ return None
21
+ return entry
22
+
23
+ def set(self, key: Tuple[int, str], value: Any) -> None:
24
+ with self._lock:
25
+ self._data[key] = (value, time.monotonic())
26
+
27
+ def invalidate(
28
+ self,
29
+ mask: Optional[int] = None,
30
+ model_name: Optional[str] = None,
31
+ ) -> None:
32
+ with self._lock:
33
+ if mask is None and model_name is None:
34
+ self._data.clear()
35
+ return
36
+ keys_to_delete = [
37
+ k for k in self._data
38
+ if (mask is None or k[0] == mask)
39
+ and (model_name is None or k[1] == model_name)
40
+ ]
41
+ for k in keys_to_delete:
42
+ del self._data[k]
43
+
44
+ def invalidate_all(self) -> None:
45
+ with self._lock:
46
+ self._data.clear()
47
+
48
+ def __len__(self) -> int:
49
+ with self._lock:
50
+ return len(self._data)
51
+
52
+
53
+ trie_cache = TrieCache()
@@ -0,0 +1,98 @@
1
+ version: "0.1.0"
2
+
3
+ policies:
4
+ - mask: 7
5
+ name: "contributor"
6
+ strict: false
7
+ default: true
8
+ rules:
9
+ - phrase: "DROP TABLE"
10
+ mode: hard
11
+ - phrase: "DELETE FROM"
12
+ mode: hard
13
+ - phrase: "ALTER TABLE"
14
+ mode: hard
15
+ - phrase: "TRUNCATE"
16
+ mode: hard
17
+ - phrase: "EXEC"
18
+ mode: hard
19
+ - phrase: "xp_cmdshell"
20
+ mode: hard
21
+ - phrase: "INTO OUTFILE"
22
+ mode: hard
23
+ - phrase: "GRANT ALL"
24
+ mode: hard
25
+ - phrase: "salaries"
26
+ mode: bias
27
+ penalty: -5.0
28
+ - phrase: "passwords"
29
+ mode: bias
30
+ penalty: -5.0
31
+ - phrase: "credit_cards"
32
+ mode: bias
33
+ penalty: -8.0
34
+ - phrase: "ssn"
35
+ mode: bias
36
+ penalty: -8.0
37
+ tools:
38
+ read_email:
39
+ required_bit: 0
40
+ trigger_phrases:
41
+ - "read_email("
42
+ - "get_emails("
43
+ send_email:
44
+ required_bit: 1
45
+ trigger_phrases:
46
+ - "send_email("
47
+ - "send_mail("
48
+ read_sql:
49
+ required_bit: 2
50
+ trigger_phrases:
51
+ - "read_sql("
52
+ - "query_database("
53
+
54
+ - mask: 15
55
+ name: "admin"
56
+ strict: false
57
+ rules:
58
+ - phrase: "xp_cmdshell"
59
+ mode: hard
60
+ tools:
61
+ read_email:
62
+ required_bit: 0
63
+ send_email:
64
+ required_bit: 1
65
+ read_sql:
66
+ required_bit: 2
67
+ write_sql:
68
+ required_bit: 3
69
+ trigger_phrases:
70
+ - "write_sql("
71
+ - "execute_sql("
72
+
73
+ - mask: 23
74
+ name: "auditor"
75
+ strict: true
76
+ rules:
77
+ - phrase: "DROP TABLE"
78
+ mode: hard
79
+ - phrase: "DELETE"
80
+ mode: hard
81
+ - phrase: "ALTER"
82
+ mode: hard
83
+ - phrase: "salaries"
84
+ mode: bias
85
+ penalty: -10.0
86
+ - phrase: "ssn"
87
+ mode: bias
88
+ penalty: -10.0
89
+ tools:
90
+ read_email:
91
+ required_bit: 0
92
+ read_sql:
93
+ required_bit: 2
94
+ create_jira:
95
+ required_bit: 4
96
+ trigger_phrases:
97
+ - "create_jira("
98
+ - "create_ticket("
@@ -0,0 +1,95 @@
1
+ from __future__ import annotations
2
+ from dataclasses import dataclass, field
3
+ from pathlib import Path
4
+ from typing import Dict, List, Optional
5
+ import yaml
6
+
7
+
8
+ @dataclass
9
+ class PhraseRule:
10
+ phrase: str
11
+ mode: str = "hard"
12
+ penalty: float = 0.0
13
+
14
+
15
+ @dataclass
16
+ class ToolRule:
17
+ name: str
18
+ required_bit: int
19
+ trigger_phrases: List[str] = field(default_factory=list)
20
+
21
+
22
+ @dataclass
23
+ class Policy:
24
+ mask: int
25
+ name: str = ""
26
+ rules: List[PhraseRule] = field(default_factory=list)
27
+ tools: Dict[str, ToolRule] = field(default_factory=dict)
28
+ strict: bool = False
29
+
30
+
31
+ @dataclass
32
+ class PolicySet:
33
+ policies: Dict[int, Policy] = field(default_factory=dict)
34
+ default_policy: Optional[Policy] = None
35
+ version: str = ""
36
+ _path: Optional[Path] = None
37
+ _mtime: float = 0.0
38
+
39
+ def get_policy_for_mask(self, mask: int) -> Optional[Policy]:
40
+ if mask in self.policies:
41
+ return self.policies[mask]
42
+ return self.default_policy
43
+
44
+
45
+ def load_policy(path: str | Path) -> PolicySet:
46
+ path = Path(path)
47
+ with open(path) as f:
48
+ data = yaml.safe_load(f)
49
+
50
+ if data is None:
51
+ return PolicySet()
52
+
53
+ version = data.get("version", "")
54
+ policies: Dict[int, Policy] = {}
55
+ default_policy: Optional[Policy] = None
56
+
57
+ for entry in data.get("policies", []):
58
+ mask = entry["mask"]
59
+ name = entry.get("name", f"policy_{mask}")
60
+ strict = entry.get("strict", False)
61
+
62
+ rules = []
63
+ for r in entry.get("rules", []):
64
+ rules.append(PhraseRule(
65
+ phrase=r["phrase"],
66
+ mode=r.get("mode", "hard"),
67
+ penalty=r.get("penalty", 0.0),
68
+ ))
69
+
70
+ tools = {}
71
+ for tool_name, config in entry.get("tools", {}).items():
72
+ tools[tool_name] = ToolRule(
73
+ name=tool_name,
74
+ required_bit=config["required_bit"],
75
+ trigger_phrases=config.get("trigger_phrases", []),
76
+ )
77
+
78
+ policy = Policy(
79
+ mask=mask,
80
+ name=name,
81
+ rules=rules,
82
+ tools=tools,
83
+ strict=strict,
84
+ )
85
+ policies[mask] = policy
86
+ if entry.get("default", False):
87
+ default_policy = policy
88
+
89
+ return PolicySet(
90
+ policies=policies,
91
+ default_policy=default_policy,
92
+ version=version,
93
+ _path=path,
94
+ _mtime=path.stat().st_mtime,
95
+ )
@@ -0,0 +1,72 @@
1
+ from __future__ import annotations
2
+ import time
3
+ from pathlib import Path
4
+ from threading import Lock, Thread
5
+ from typing import Callable, Optional
6
+
7
+ from .cache import trie_cache
8
+ from .policy_loader import PolicySet, load_policy
9
+
10
+
11
+ class PolicyWatcher:
12
+ """Watches a policy file for changes and triggers cache invalidation."""
13
+
14
+ def __init__(
15
+ self,
16
+ policy_path: str | Path,
17
+ interval: float = 5.0,
18
+ on_reload: Optional[Callable[[PolicySet], None]] = None,
19
+ auto_start: bool = True,
20
+ ):
21
+ self._path = Path(policy_path)
22
+ self._interval = interval
23
+ self._on_reload = on_reload
24
+ self._last_mtime = self._path.stat().st_mtime if self._path.exists() else 0.0
25
+ self._running = False
26
+ self._thread: Optional[Thread] = None
27
+ self._lock = Lock()
28
+ self._current_policy: Optional[PolicySet] = None
29
+
30
+ if auto_start:
31
+ self.start()
32
+
33
+ def start(self) -> None:
34
+ if self._running:
35
+ return
36
+ self._running = True
37
+ self._thread = Thread(target=self._watch, daemon=True)
38
+ self._thread.start()
39
+
40
+ def stop(self) -> None:
41
+ self._running = False
42
+ if self._thread is not None:
43
+ self._thread.join(timeout=2.0)
44
+
45
+ @property
46
+ def current_policy(self) -> Optional[PolicySet]:
47
+ with self._lock:
48
+ return self._current_policy
49
+
50
+ def _watch(self) -> None:
51
+ while self._running:
52
+ try:
53
+ if self._path.exists():
54
+ mtime = self._path.stat().st_mtime
55
+ if mtime > self._last_mtime:
56
+ self._last_mtime = mtime
57
+ policy = load_policy(self._path)
58
+ with self._lock:
59
+ self._current_policy = policy
60
+ trie_cache.invalidate_all()
61
+ if self._on_reload:
62
+ self._on_reload(policy)
63
+ except Exception:
64
+ pass
65
+ time.sleep(self._interval)
66
+
67
+ def __enter__(self) -> "PolicyWatcher":
68
+ self.start()
69
+ return self
70
+
71
+ def __exit__(self, *args) -> None:
72
+ self.stop()
@@ -0,0 +1,40 @@
1
+ from __future__ import annotations
2
+ from .policy_loader import PolicySet
3
+
4
+
5
+ def verify_tool_action(
6
+ tool_name: str,
7
+ user_mask: int,
8
+ policy_set: PolicySet,
9
+ ) -> bool:
10
+ """Check whether a tool action is allowed for the given user mask.
11
+
12
+ Looks up the tool's required bit from the policy matching *user_mask*,
13
+ then checks if that bit is set in the mask.
14
+
15
+ Returns:
16
+ True if the tool is allowed, False otherwise.
17
+ """
18
+ policy = policy_set.get_policy_for_mask(user_mask)
19
+ if policy is None:
20
+ return False
21
+ tool_rule = policy.tools.get(tool_name)
22
+ if tool_rule is None:
23
+ return False
24
+ required_bit = tool_rule.required_bit
25
+ return (user_mask & (1 << required_bit)) != 0
26
+
27
+
28
+ def get_allowed_tools(
29
+ user_mask: int,
30
+ policy_set: PolicySet,
31
+ ) -> list[str]:
32
+ """Return the list of tool names the user mask permits."""
33
+ policy = policy_set.get_policy_for_mask(user_mask)
34
+ if policy is None:
35
+ return []
36
+ return [
37
+ name
38
+ for name, rule in policy.tools.items()
39
+ if (user_mask & (1 << rule.required_bit)) != 0
40
+ ]
@@ -0,0 +1,130 @@
1
+ from __future__ import annotations
2
+ from typing import Dict, List, Optional, Tuple
3
+
4
+ from resklogits import VectorizedAhoCorasick
5
+
6
+ from .cache import trie_cache
7
+ from .policy_loader import Policy, PolicySet, PhraseRule
8
+
9
+
10
+ def _expand_phrases(phrases: List[str]) -> List[str]:
11
+ """Generate variants of each phrase (space-prefixed, capitalized)."""
12
+ expanded: List[str] = []
13
+ for s in phrases:
14
+ s = s.strip()
15
+ expanded.append(s)
16
+ if not s.startswith(" "):
17
+ expanded.append(" " + s)
18
+ capitalized = s.capitalize()
19
+ if capitalized != s:
20
+ expanded.append(capitalized)
21
+ return expanded
22
+
23
+
24
+ def build_ac_from_policy(
25
+ policy: Policy,
26
+ tokenizer,
27
+ device: str = "cuda",
28
+ ) -> Tuple[Optional[VectorizedAhoCorasick], Dict[int, str], Dict[int, float]]:
29
+ """Build a VectorizedAhoCorasick from a Policy.
30
+
31
+ Generates phrase variants (with space prefix, capitalized) to cover
32
+ different tokenization contexts.
33
+
34
+ Returns:
35
+ (automaton, pattern_modes, pattern_penalties)
36
+ - automaton: VectorizedAhoCorasick or None if no rules
37
+ - pattern_modes: pattern_index -> 'hard' | 'bias'
38
+ - pattern_penalties: pattern_index -> penalty value
39
+ """
40
+ if not policy.rules:
41
+ return None, {}, {}
42
+
43
+ all_phrases: List[str] = []
44
+ pattern_meta: List[Tuple[str, float]] = []
45
+
46
+ for rule in policy.rules:
47
+ for variant in _expand_phrases([rule.phrase]):
48
+ all_phrases.append(variant)
49
+ pattern_meta.append((rule.mode, rule.penalty))
50
+
51
+ if not all_phrases:
52
+ return None, {}, {}
53
+
54
+ ac = VectorizedAhoCorasick(
55
+ tokenizer=tokenizer,
56
+ banned_phrases=all_phrases,
57
+ device=device,
58
+ )
59
+
60
+ pattern_modes = {i: meta[0] for i, meta in enumerate(pattern_meta)}
61
+ pattern_penalties = {i: meta[1] for i, meta in enumerate(pattern_meta)}
62
+
63
+ return ac, pattern_modes, pattern_penalties
64
+
65
+
66
+ def get_or_build_ac(
67
+ mask: int,
68
+ model_name: str,
69
+ tokenizer,
70
+ policy_set: PolicySet,
71
+ device: str = "cuda",
72
+ ):
73
+ """Get cached automaton or build it from the policy.
74
+
75
+ Also derives tool trigger phrases that should be blocked for this mask.
76
+ If the user's mask does not have the required bit for a tool, that
77
+ tool's trigger phrases are added as hard-mode blocked phrases.
78
+
79
+ Returns:
80
+ (hard_ac, bias_ac, hard_meta, bias_meta, policy)
81
+ - hard_ac: VectorizedAhoCorasick or None for hard-mode phrases
82
+ - bias_ac: VectorizedAhoCorasick or None for bias-mode phrases
83
+ - hard_meta: dict pattern_index -> mode
84
+ - bias_meta: dict pattern_index -> penalty
85
+ - policy: the resolved Policy or None
86
+ """
87
+ key = (mask, model_name)
88
+ cached = trie_cache.get(key)
89
+ if cached is not None:
90
+ return cached
91
+
92
+ policy = policy_set.get_policy_for_mask(mask)
93
+ if policy is None:
94
+ return (None, None, {}, {}, policy)
95
+
96
+ # Collect explicit rules from the policy
97
+ hard_phrases = [r.phrase for r in policy.rules if r.mode == "hard"]
98
+ bias_phrases = [r.phrase for r in policy.rules if r.mode == "bias"]
99
+
100
+ # Derive tool trigger phrases for tools the user does NOT have access to.
101
+ # These are added as hard-mode phrases so the model can never generate
102
+ # a disallowed tool call at the token level.
103
+ if policy.tools:
104
+ for tool_name, tool_rule in policy.tools.items():
105
+ has_bit = (mask & (1 << tool_rule.required_bit)) != 0
106
+ if not has_bit and tool_rule.trigger_phrases:
107
+ hard_phrases.extend(tool_rule.trigger_phrases)
108
+
109
+ hard_ac, bias_ac = None, None
110
+ hard_meta, bias_meta = {}, {}
111
+
112
+ if hard_phrases:
113
+ hard_rules = [PhraseRule(phrase=p, mode="hard") for p in hard_phrases]
114
+ hard_ac, hard_meta, _ = build_ac_from_policy(
115
+ Policy(mask=policy.mask, rules=hard_rules),
116
+ tokenizer,
117
+ device,
118
+ )
119
+
120
+ if bias_phrases:
121
+ bias_rules = [PhraseRule(phrase=p, mode="bias", penalty=-5.0) for p in bias_phrases]
122
+ bias_ac, bias_meta, _ = build_ac_from_policy(
123
+ Policy(mask=policy.mask, rules=bias_rules),
124
+ tokenizer,
125
+ device,
126
+ )
127
+
128
+ result = (hard_ac, bias_ac, hard_meta, bias_meta, policy)
129
+ trie_cache.set(key, result)
130
+ return result
@@ -0,0 +1,238 @@
1
+ Metadata-Version: 2.4
2
+ Name: resksecure
3
+ Version: 0.1.0
4
+ Summary: Bitmask-based LLM security firewall with policy-driven logits filtering using GPU-accelerated Aho-Corasick pattern matching
5
+ Project-URL: Homepage, https://github.com/Resk-Security/reskSecure
6
+ Project-URL: Repository, https://github.com/Resk-Security/reskSecure
7
+ Project-URL: Documentation, https://github.com/Resk-Security/reskSecure
8
+ Author: RESK Security
9
+ License: RESK Software License
10
+ License-File: LICENSE
11
+ Keywords: aho-corasick,bitmask,dlp,filtering,firewall,gpu,llm,logits,permissions,policy,safety,security,transformers
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: License :: Other/Proprietary License
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
19
+ Classifier: Topic :: Security
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Requires-Python: >=3.13
22
+ Requires-Dist: pyyaml>=6.0
23
+ Requires-Dist: resklogits>=0.1.0
24
+ Requires-Dist: torch>=2.0.0
25
+ Requires-Dist: transformers>=4.35.0
26
+ Provides-Extra: dev
27
+ Requires-Dist: black>=23.0.0; extra == 'dev'
28
+ Requires-Dist: mypy>=1.0.0; extra == 'dev'
29
+ Requires-Dist: pytest>=7.0.0; extra == 'dev'
30
+ Requires-Dist: ruff>=0.1.0; extra == 'dev'
31
+ Description-Content-Type: text/markdown
32
+
33
+ # reskSecure
34
+
35
+ [![PyPI version](https://img.shields.io/pypi/v/resksecure)](https://pypi.org/project/resksecure/)
36
+ [![PyPI downloads](https://img.shields.io/pypi/dm/resksecure)](https://pypi.org/project/resksecure/)
37
+ [![Python versions](https://img.shields.io/pypi/pyversions/resksecure)](https://pypi.org/project/resksecure/)
38
+ [![License](https://img.shields.io/badge/license-RESK-blue)](LICENSE)
39
+
40
+ **Bitmask-based LLM security firewall.** A Python package that restricts what a language model can generate based on user permissions encoded as a bitmask. It works by intercepting the model's token predictions and blocking or penalizing disallowed phrases before they appear in the output.
41
+
42
+ Available on [PyPI](https://pypi.org/project/resksecure/).
43
+
44
+ Unlike prompt-based filters (which can be jailbroken) or post-generation content moderation (which lets forbidden content leak before detection), reskSecure acts at the logits level -- directly inside the model's generation loop. Each token must pass through the security policy before being emitted.
45
+
46
+ ---
47
+
48
+ ## How it works
49
+
50
+ ```
51
+ User request with bitmask (e.g. 7)
52
+ |
53
+ v
54
+ BitmaskLogitsProcessor intercepts each token prediction
55
+ |
56
+ v
57
+ For every candidate token, the Aho-Corasick automaton (from resklogits)
58
+ checks if selecting it would start or complete a banned phrase
59
+ |
60
+ v
61
+ Hard-mode phrases: the token's logit is set to -inf (impossible to generate)
62
+ Bias-mode phrases: the token's logit is reduced by a configurable penalty
63
+ |
64
+ v
65
+ On complete match: EOS token is forced, generation stops immediately
66
+ |
67
+ v
68
+ Output response or tool call
69
+ ```
70
+
71
+ Tool calls are also blocked at the token level. If the user's bitmask does not
72
+ contain the required bit for a tool, that tool's trigger phrases (like
73
+ `"send_email("` or `"create_ticket("`) are automatically added to the
74
+ hard-mode blocked list. The model can never generate the first token of
75
+ a disallowed tool call, regardless of prompt engineering or jailbreak
76
+ attempts.
77
+
78
+ A post-generation check (`verify_tool_action`) is also available as a
79
+ defense-in-depth layer, but the primary protection is at the logits level.
80
+
81
+ ---
82
+
83
+ ## Why not just prompt engineering?
84
+
85
+ - Prompt injections can bypass instruction-based filters
86
+ - Post-generation regex or classifier scans catch violations after they appear, but the forbidden content has already been emitted
87
+ - Logits-level filtering blocks tokens before they are sampled -- the model never "sees" the banned sequence as a completion candidate
88
+
89
+ ---
90
+
91
+ ## Features
92
+
93
+ - **Two severity modes**: `hard` blocks tokens completely (-inf logit), `bias` reduces probability by a configurable penalty
94
+ - **Strict mode**: forces end-of-sequence as soon as the generated prefix matches the start of a banned phrase, even before the full phrase is formed
95
+ - **GPU-accelerated pattern matching**: uses VectorizedAhoCorasick from the resklogits package for fast token scanning
96
+ - **Policy system**: YAML configuration associates capability bitmasks with phrase rules and tool permissions
97
+ - **Hot-reload**: PolicyWatcher detects file changes and rebuilds the automaton without restarting the server
98
+ - **Thread-safe cache**: automata are cached by (mask, model_name) with configurable TTL
99
+ - **Tool guard**: post-generation bitmask check for tool call execution
100
+ - **No JWT handling**: the package receives a raw integer bitmask; authentication and JWT decoding are handled by the calling application
101
+
102
+ ---
103
+
104
+ ## Requirements
105
+
106
+ - Python >= 3.13
107
+ - PyTorch >= 2.0.0
108
+ - transformers >= 4.35.0
109
+ - resklogits >= 0.1.0
110
+
111
+ ---
112
+
113
+ ## Installation
114
+
115
+ ```bash
116
+ pip install resksecure
117
+ ```
118
+
119
+ [![PyPI version](https://img.shields.io/pypi/v/resksecure)](https://pypi.org/project/resksecure/)
120
+ [![PyPI downloads](https://img.shields.io/pypi/dm/resksecure)](https://pypi.org/project/resksecure/)
121
+
122
+ From source:
123
+
124
+ ```bash
125
+ git clone https://github.com/Resk-Security/reskSecure.git
126
+ cd reskSecure
127
+ pip install -e .
128
+ ```
129
+
130
+ ---
131
+
132
+ ## Quick start
133
+
134
+ Define a policy file (`policy.yaml`):
135
+
136
+ ```yaml
137
+ version: "1.0"
138
+ policies:
139
+ - mask: 7
140
+ name: contributor
141
+ strict: false
142
+ default: true
143
+ rules:
144
+ - phrase: "DROP TABLE"
145
+ mode: hard
146
+ - phrase: "DELETE FROM"
147
+ mode: hard
148
+ - phrase: "salaries"
149
+ mode: bias
150
+ penalty: -5.0
151
+ tools:
152
+ read_email:
153
+ required_bit: 0
154
+ send_email:
155
+ required_bit: 1
156
+ read_sql:
157
+ required_bit: 2
158
+ ```
159
+
160
+ Use it in your generation pipeline:
161
+
162
+ ```python
163
+ from resksecure import BitmaskLogitsProcessor, load_policy, verify_tool_action
164
+
165
+ policy_set = load_policy("policy.yaml")
166
+
167
+ processor = BitmaskLogitsProcessor(
168
+ mask=7,
169
+ model_name="mistralai/Mistral-7B-v0.1",
170
+ tokenizer=tokenizer,
171
+ policy_set=policy_set,
172
+ device="cuda",
173
+ )
174
+
175
+ outputs = model.generate(**inputs, logits_processor=[processor])
176
+
177
+ # Optional: verify tool calls against the bitmask
178
+ if has_tool_call(response):
179
+ if not verify_tool_action("send_email", user_mask=7, policy_set=policy_set):
180
+ raise PermissionError("Action not authorized")
181
+ ```
182
+
183
+ ---
184
+
185
+ ## Policy reference
186
+
187
+ Policy-level fields:
188
+
189
+ | Field | Type | Description |
190
+ |-------------|---------|----------------------------------------------------------|
191
+ | `mask` | int | Capability bitmask that identifies this policy |
192
+ | `name` | string | Human-readable policy name |
193
+ | `strict` | bool | If true, stop generation at the first banned prefix |
194
+ | `default` | bool | If true, this policy is used when no exact mask matches |
195
+ | `rules` | list | List of phrase rules (see below) |
196
+ | `tools` | dict | Map of tool names to tool rule objects (see below) |
197
+
198
+ Phrase rule fields:
199
+
200
+ | Field | Type | Description |
201
+ |-----------|--------|------------------------------------------------------------|
202
+ | `phrase` | string | Text pattern to ban or penalize |
203
+ | `mode` | string | Either `hard` (block completely) or `bias` (reduce logits) |
204
+ | `penalty` | float | Logit penalty for bias mode (e.g. -5.0) |
205
+
206
+ Tool rule fields:
207
+
208
+ | Field | Type | Description |
209
+ |--------------------|----------|----------------------------------------------------------------|
210
+ | `required_bit` | int | Bit position that must be set in the user mask to use this tool |
211
+ | `trigger_phrases` | string[] | Phrases that start this tool call (blocked if bit not set) |
212
+
213
+ ---
214
+
215
+ ## Package structure
216
+
217
+ ```
218
+ reskSecure/
219
+ src/resksecure/
220
+ __init__.py Public exports, version
221
+ policy_loader.py YAML parsing, Policy/PolicySet models
222
+ trie_factory.py Builds VectorizedAhoCorasick from a Policy
223
+ bitmask_processor.py BitmaskLogitsProcessor (LogitsProcessor subclass)
224
+ tool_guard.py Post-generation tool action verification
225
+ cache.py Thread-safe TTL cache for automata
226
+ policy_watcher.py Hot-reload daemon that watches YAML mtime
227
+ config/example_policy.yaml
228
+ tests/
229
+ examples/
230
+ ```
231
+
232
+ ---
233
+
234
+ ## License
235
+
236
+ This software is licensed under the RESK Software License. Commercial use requires a separate paid license. See the [LICENSE](LICENSE) file for details.
237
+
238
+ For commercial licensing inquiries, contact: contact@resk.fr
@@ -0,0 +1,12 @@
1
+ resksecure/__init__.py,sha256=vswAYW475bnZZufYrfLsKqmccYuPvb9dUIaPvDKzUTM,1363
2
+ resksecure/bitmask_processor.py,sha256=Bawg7GeHegK-pOqrw1W5UgpcGgvgpvYBkMvc4zFtBGs,5547
3
+ resksecure/cache.py,sha256=1c00QAEr2MhVJJw3X6AjPiETMIkWfM1OqFKC_1-ZFP8,1516
4
+ resksecure/policy_loader.py,sha256=CrLnv-JsYZwEprPoguxNsWUtKuyxphCF71gNEKMT2Sk,2412
5
+ resksecure/policy_watcher.py,sha256=aaEjB7Z4eCn7p70EG9wjnvZjpW6_gLfYFFlpG7zAuIA,2211
6
+ resksecure/tool_guard.py,sha256=VtBYNscqz7RJMCBlQwfhZIWzO-8WXBY9maBs5gNJHKo,1110
7
+ resksecure/trie_factory.py,sha256=LR63Y8J-me-7HvUJOuIjfZbg_16LXZWpnozNy3GhDTU,4307
8
+ resksecure/config/example_policy.yaml,sha256=BNKee5k1_X3DFDxBvhDeWJFKKuZdzlbeOMq7itVTB5E,2040
9
+ resksecure-0.1.0.dist-info/METADATA,sha256=_kXBbk5WA9HUV61WOQO6Cf3lj6tUPcYThuod86j2Qgk,8858
10
+ resksecure-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
11
+ resksecure-0.1.0.dist-info/licenses/LICENSE,sha256=Pp2Rp2klJloZ-sEwrkv3MdSSZ7E_Ev0l94omDTWommU,4183
12
+ resksecure-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,105 @@
1
+ RESK SOFTWARE LICENSE
2
+ Version 1.0, June 2026
3
+
4
+ Copyright (c) 2026 RESK Security
5
+
6
+ TERMS AND CONDITIONS
7
+
8
+ 1. Definitions
9
+
10
+ "Software" means the source code, documentation, and any accompanying
11
+ materials in this repository.
12
+
13
+ "Licensor" means RESK Security.
14
+
15
+ "Licensee" means any individual or entity that accesses or uses the Software.
16
+
17
+ "Commercial Use" means any use of the Software where the primary purpose
18
+ is commercial advantage or monetary compensation, including but not limited
19
+ to: integrating the Software into a commercial product, offering the Software
20
+ as part of a SaaS service, using the Software for internal business operations
21
+ that generate revenue, or distributing the Software in exchange for any form
22
+ of payment.
23
+
24
+ "Royalty" means the recurring fee payable to Licensor for any Commercial Use
25
+ of the Software, as defined in a separate commercial agreement.
26
+
27
+ 2. Grant of License
28
+
29
+ Subject to the terms and conditions of this License, Licensor grants Licensee
30
+ a non-exclusive, non-transferable, non-sublicensable, limited license to:
31
+
32
+ (a) View and study the Software source code.
33
+ (b) Use the Software for non-commercial, personal, educational, or
34
+ evaluation purposes only.
35
+ (c) Modify the Software for personal, non-commercial use only.
36
+
37
+ 3. Prohibited Uses
38
+
39
+ Without the prior written consent of Licensor, Licensee may NOT:
40
+
41
+ (a) Use the Software for any Commercial Use.
42
+ (b) Distribute, sublicense, sell, lease, rent, or transfer the Software
43
+ or any derivative thereof to any third party.
44
+ (c) Incorporate the Software or any portion thereof into any product or
45
+ service offered to third parties.
46
+ (d) Remove or alter any copyright, trademark, or proprietary notice
47
+ contained in the Software.
48
+ (e) Use the Software in any manner that competes with Licensor's business.
49
+ (f) Create derivative works based on the Software for distribution or
50
+ Commercial Use.
51
+ (g) Use the Software in any environment that processes sensitive personal
52
+ data without prior written authorization from Licensor.
53
+
54
+ 4. Commercial License and Royalties
55
+
56
+ Any Commercial Use of the Software requires a separate commercial license
57
+ agreement signed by both parties. Such agreement shall specify:
58
+
59
+ (a) The scope of permitted Commercial Use.
60
+ (b) Royalty fees, which shall be calculated as a percentage of revenue or
61
+ a fixed fee per deployment, as agreed upon in writing.
62
+ (c) Duration and renewal terms.
63
+
64
+ Contact: resk-security@proton.me for commercial licensing inquiries.
65
+
66
+ 5. Intellectual Property
67
+
68
+ All right, title, and interest in and to the Software, including all
69
+ intellectual property rights therein, remain the exclusive property of
70
+ Licensor. This License does not transfer any ownership rights to Licensee.
71
+
72
+ 6. No Warranty
73
+
74
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
75
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
76
+ FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL
77
+ THE LICENSOR BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER
78
+ IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN
79
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
80
+
81
+ 7. Limitation of Liability
82
+
83
+ In no event shall Licensor be liable for any indirect, incidental, special,
84
+ consequential, or punitive damages, including but not limited to loss of
85
+ profits, data, or use, arising out of or in connection with the Software,
86
+ whether based on warranty, contract, tort, or any other legal theory, even
87
+ if Licensor has been advised of the possibility of such damages.
88
+
89
+ 8. Termination
90
+
91
+ This License terminates automatically if Licensee fails to comply with any
92
+ of its terms. Upon termination, Licensee must immediately cease all use of
93
+ the Software and destroy all copies. Licensor reserves the right to pursue
94
+ legal remedies for any unauthorized use.
95
+
96
+ 9. Governing Law
97
+
98
+ This License shall be governed by and construed in accordance with the laws
99
+ of France, without regard to its conflict of laws principles.
100
+
101
+ 10. Entire Agreement
102
+
103
+ This License constitutes the entire agreement between the parties concerning
104
+ the subject matter hereof and supersedes all prior agreements and
105
+ understandings.