iris-security-gemini 0.1.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.
- iris_security_gemini-0.1.0/PKG-INFO +50 -0
- iris_security_gemini-0.1.0/README.md +31 -0
- iris_security_gemini-0.1.0/iris_gemini/__init__.py +31 -0
- iris_security_gemini-0.1.0/iris_gemini/client.py +304 -0
- iris_security_gemini-0.1.0/iris_gemini/guardrails.py +114 -0
- iris_security_gemini-0.1.0/iris_gemini/init.py +5 -0
- iris_security_gemini-0.1.0/iris_security_gemini.egg-info/PKG-INFO +50 -0
- iris_security_gemini-0.1.0/iris_security_gemini.egg-info/SOURCES.txt +12 -0
- iris_security_gemini-0.1.0/iris_security_gemini.egg-info/dependency_links.txt +1 -0
- iris_security_gemini-0.1.0/iris_security_gemini.egg-info/requires.txt +10 -0
- iris_security_gemini-0.1.0/iris_security_gemini.egg-info/top_level.txt +1 -0
- iris_security_gemini-0.1.0/pyproject.toml +39 -0
- iris_security_gemini-0.1.0/setup.cfg +4 -0
- iris_security_gemini-0.1.0/tests/test_gemini_integration.py +213 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: iris-security-gemini
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: IRIS governance for Google Gemini via google-genai
|
|
5
|
+
Author-email: IRIS Platform <sdk@iris.ai>
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/gimartinb/iris-sdk
|
|
8
|
+
Project-URL: Repository, https://github.com/gimartinb/iris-sdk
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: iris-security-core>=0.1.0
|
|
12
|
+
Requires-Dist: iris-security-sdk>=0.1.0
|
|
13
|
+
Provides-Extra: google
|
|
14
|
+
Requires-Dist: google-genai>=0.3; extra == "google"
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
17
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
18
|
+
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
19
|
+
|
|
20
|
+
# iris-gemini
|
|
21
|
+
|
|
22
|
+
Drop-in IRIS governance for the [Google GenAI Python SDK](https://github.com/googleapis/python-genai).
|
|
23
|
+
|
|
24
|
+
Replace one line:
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
# client = google.genai.Client()
|
|
28
|
+
client = IrisGemini(passport=passport)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Every `client.models.generate_content()` and `generate_content_stream()` call is evaluated against Cedar policy, recorded in the Evidence Vault, and enforced per `IRIS_ENV` (warn in dev, block in production).
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install iris-security-gemini
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Quickstart
|
|
40
|
+
|
|
41
|
+
See [examples/governed_gemini.py](examples/governed_gemini.py).
|
|
42
|
+
|
|
43
|
+
## Environment
|
|
44
|
+
|
|
45
|
+
| `IRIS_ENV` | Behavior |
|
|
46
|
+
|---------------|---------------------------------------------|
|
|
47
|
+
| `dev` | Fail open - warnings to stderr, never block |
|
|
48
|
+
| `production` | Fail closed - `IrisViolationError` on deny |
|
|
49
|
+
|
|
50
|
+
Defaults to `dev` when unset.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# iris-gemini
|
|
2
|
+
|
|
3
|
+
Drop-in IRIS governance for the [Google GenAI Python SDK](https://github.com/googleapis/python-genai).
|
|
4
|
+
|
|
5
|
+
Replace one line:
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
# client = google.genai.Client()
|
|
9
|
+
client = IrisGemini(passport=passport)
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Every `client.models.generate_content()` and `generate_content_stream()` call is evaluated against Cedar policy, recorded in the Evidence Vault, and enforced per `IRIS_ENV` (warn in dev, block in production).
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install iris-security-gemini
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Quickstart
|
|
21
|
+
|
|
22
|
+
See [examples/governed_gemini.py](examples/governed_gemini.py).
|
|
23
|
+
|
|
24
|
+
## Environment
|
|
25
|
+
|
|
26
|
+
| `IRIS_ENV` | Behavior |
|
|
27
|
+
|---------------|---------------------------------------------|
|
|
28
|
+
| `dev` | Fail open - warnings to stderr, never block |
|
|
29
|
+
| `production` | Fail closed - `IrisViolationError` on deny |
|
|
30
|
+
|
|
31
|
+
Defaults to `dev` when unset.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""
|
|
2
|
+
IRIS Gemini integration - one-line drop-in for google.genai.Client().
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from iris import IrisViolationError
|
|
8
|
+
from iris_core.models.passport import (
|
|
9
|
+
AgentPassport,
|
|
10
|
+
ComplianceTag,
|
|
11
|
+
DataClassification,
|
|
12
|
+
Environment,
|
|
13
|
+
)
|
|
14
|
+
from iris_core.models.policy import Violation
|
|
15
|
+
|
|
16
|
+
from iris_gemini.client import IrisGemini, IrisGeminiAsync
|
|
17
|
+
from iris_gemini.guardrails import scan_gemini_content
|
|
18
|
+
|
|
19
|
+
__version__ = "0.1.0"
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"IrisGemini",
|
|
23
|
+
"IrisGeminiAsync",
|
|
24
|
+
"IrisViolationError",
|
|
25
|
+
"AgentPassport",
|
|
26
|
+
"ComplianceTag",
|
|
27
|
+
"DataClassification",
|
|
28
|
+
"Environment",
|
|
29
|
+
"Violation",
|
|
30
|
+
"scan_gemini_content",
|
|
31
|
+
]
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
"""Drop-in Google GenAI client wrapper with IRIS governance."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
import threading
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Optional
|
|
12
|
+
|
|
13
|
+
from iris import IrisViolationError
|
|
14
|
+
from iris_core.dlp import DLPScanner
|
|
15
|
+
from iris_core.dlp.enforcement import (
|
|
16
|
+
enforce_prompt_dlp,
|
|
17
|
+
extract_gemini_response_text,
|
|
18
|
+
handle_response_dlp,
|
|
19
|
+
)
|
|
20
|
+
from iris_core.engine.cedar import CedarEngine, EvaluationContext
|
|
21
|
+
from iris_core.rbac.context import UserContext
|
|
22
|
+
from iris_core.evidence.vault import EvidenceVault
|
|
23
|
+
from iris_core.models.passport import AgentPassport, Environment
|
|
24
|
+
from iris_core.models.policy import PolicyResult, Severity
|
|
25
|
+
|
|
26
|
+
from iris_gemini.guardrails import _extract_text, scan_gemini_content
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger("iris.gemini")
|
|
29
|
+
_VAULT_LOCK = threading.Lock()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _lazy_genai():
|
|
33
|
+
try:
|
|
34
|
+
return importlib.import_module("google.genai")
|
|
35
|
+
except ModuleNotFoundError as exc:
|
|
36
|
+
raise ImportError(
|
|
37
|
+
"google-genai is required for IrisGemini. Install with: pip install google-genai"
|
|
38
|
+
) from exc
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _current_environment() -> Environment:
|
|
42
|
+
return Environment(os.environ.get("IRIS_ENV", "dev"))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _load_passport_policy(engine: CedarEngine, passport: AgentPassport) -> None:
|
|
46
|
+
if not passport.policy_ref:
|
|
47
|
+
return
|
|
48
|
+
policy_path = Path(passport.policy_ref)
|
|
49
|
+
if not policy_path.is_absolute():
|
|
50
|
+
policy_path = Path.cwd() / policy_path
|
|
51
|
+
if policy_path.exists():
|
|
52
|
+
engine.load_policy_file(passport.agent_id, policy_path)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _has_policy_loaded(engine: CedarEngine, passport: AgentPassport) -> bool:
|
|
56
|
+
return bool(engine._policy_cache.get(passport.agent_id))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _apply_no_policy_gate(
|
|
60
|
+
engine: CedarEngine,
|
|
61
|
+
passport: AgentPassport,
|
|
62
|
+
env: Environment,
|
|
63
|
+
result: PolicyResult,
|
|
64
|
+
) -> PolicyResult:
|
|
65
|
+
if _has_policy_loaded(engine, passport):
|
|
66
|
+
return result
|
|
67
|
+
if env in (Environment.DEV, Environment.TEST) and result.decision == "DENY":
|
|
68
|
+
return PolicyResult(
|
|
69
|
+
decision="PERMIT_WITH_WARNINGS",
|
|
70
|
+
violations=result.violations,
|
|
71
|
+
agent_id=result.agent_id,
|
|
72
|
+
action=result.action,
|
|
73
|
+
resource=result.resource,
|
|
74
|
+
environment=result.environment,
|
|
75
|
+
)
|
|
76
|
+
return result
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _merge_content_violations(
|
|
80
|
+
result: PolicyResult, env: Environment, content_violations: list
|
|
81
|
+
) -> PolicyResult:
|
|
82
|
+
if not content_violations:
|
|
83
|
+
return result
|
|
84
|
+
violations = list(result.violations) + list(content_violations)
|
|
85
|
+
critical = [v for v in violations if v.severity == Severity.CRITICAL]
|
|
86
|
+
high = [v for v in violations if v.severity in (Severity.HIGH, Severity.CRITICAL)]
|
|
87
|
+
if critical:
|
|
88
|
+
decision = "DENY"
|
|
89
|
+
elif high and result.decision == "PERMIT":
|
|
90
|
+
decision = "PERMIT_WITH_WARNINGS" if env in (Environment.DEV, Environment.TEST) else "DENY"
|
|
91
|
+
elif violations and result.decision == "PERMIT":
|
|
92
|
+
decision = "PERMIT_WITH_WARNINGS"
|
|
93
|
+
else:
|
|
94
|
+
decision = result.decision
|
|
95
|
+
return PolicyResult(
|
|
96
|
+
decision=decision,
|
|
97
|
+
violations=violations,
|
|
98
|
+
agent_id=result.agent_id,
|
|
99
|
+
action=result.action,
|
|
100
|
+
resource=result.resource,
|
|
101
|
+
environment=result.environment,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _enforce_result(result: PolicyResult, env: Environment) -> None:
|
|
106
|
+
if result.decision == "DENY":
|
|
107
|
+
if env in (Environment.DEV, Environment.TEST):
|
|
108
|
+
for violation in result.violations:
|
|
109
|
+
msg = (
|
|
110
|
+
f"[IRIS WARNING] {violation.message} "
|
|
111
|
+
f"Remediation: {violation.remediation}"
|
|
112
|
+
)
|
|
113
|
+
logger.warning(msg)
|
|
114
|
+
print(msg, file=sys.stderr)
|
|
115
|
+
return
|
|
116
|
+
raise IrisViolationError(result)
|
|
117
|
+
if result.decision == "PERMIT_WITH_WARNINGS":
|
|
118
|
+
for violation in result.violations:
|
|
119
|
+
msg = f"[IRIS WARNING] {violation.message} Remediation: {violation.remediation}"
|
|
120
|
+
logger.warning(msg)
|
|
121
|
+
print(msg, file=sys.stderr)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class _IrisGeminiBase:
|
|
125
|
+
_passport: AgentPassport
|
|
126
|
+
_engine: CedarEngine
|
|
127
|
+
_vault: EvidenceVault
|
|
128
|
+
_dlp: DLPScanner
|
|
129
|
+
_user_email: Optional[str] = None
|
|
130
|
+
_user_role: Optional[str] = None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class IrisModelsResource:
|
|
134
|
+
"""Governed wrapper around google.genai client.models."""
|
|
135
|
+
|
|
136
|
+
def __init__(self, parent: _IrisGeminiBase, models_resource: Any):
|
|
137
|
+
self._parent = parent
|
|
138
|
+
self._models = models_resource
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def _passport(self) -> AgentPassport:
|
|
142
|
+
return self._parent._passport
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def _engine(self) -> CedarEngine:
|
|
146
|
+
return self._parent._engine
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def _vault(self) -> EvidenceVault:
|
|
150
|
+
return self._parent._vault
|
|
151
|
+
|
|
152
|
+
def _govern(self, model: Optional[str], contents: Any, kwargs: dict) -> None:
|
|
153
|
+
env = _current_environment()
|
|
154
|
+
model_name = model or kwargs.get("model") or "unknown-model"
|
|
155
|
+
request_contents = contents if contents is not None else kwargs.get("contents")
|
|
156
|
+
prompt_text = "\n".join(_extract_text(request_contents))
|
|
157
|
+
dlp_result = enforce_prompt_dlp(
|
|
158
|
+
self._parent._dlp,
|
|
159
|
+
self._vault,
|
|
160
|
+
self._passport,
|
|
161
|
+
env,
|
|
162
|
+
prompt_text,
|
|
163
|
+
resource=f"gemini-api/{model_name}",
|
|
164
|
+
)
|
|
165
|
+
content_violations = scan_gemini_content(request_contents, self._passport)
|
|
166
|
+
user_ctx = UserContext.from_params(self._parent._user_email, self._parent._user_role)
|
|
167
|
+
ctx = EvaluationContext(
|
|
168
|
+
agent_id=self._passport.agent_id,
|
|
169
|
+
action="call",
|
|
170
|
+
resource=f"gemini-api/{model_name}",
|
|
171
|
+
resource_type="api",
|
|
172
|
+
environment=env,
|
|
173
|
+
data_classification=self._passport.data_classification.value,
|
|
174
|
+
dlp_prompt_findings=dlp_result.findings,
|
|
175
|
+
additional={
|
|
176
|
+
"model": model_name,
|
|
177
|
+
"content_violation_count": len(content_violations),
|
|
178
|
+
},
|
|
179
|
+
**user_ctx.evaluation_fields(),
|
|
180
|
+
)
|
|
181
|
+
result = self._engine.evaluate(self._passport, ctx)
|
|
182
|
+
result = _apply_no_policy_gate(self._engine, self._passport, env, result)
|
|
183
|
+
result = _merge_content_violations(result, env, content_violations)
|
|
184
|
+
with _VAULT_LOCK:
|
|
185
|
+
self._vault.record(ctx, result)
|
|
186
|
+
_enforce_result(result, env)
|
|
187
|
+
|
|
188
|
+
def _scan_response(self, response: Any) -> Any:
|
|
189
|
+
env = _current_environment()
|
|
190
|
+
response_text = extract_gemini_response_text(response)
|
|
191
|
+
blocked, _ = handle_response_dlp(
|
|
192
|
+
self._parent._dlp,
|
|
193
|
+
self._vault,
|
|
194
|
+
self._passport,
|
|
195
|
+
env,
|
|
196
|
+
response_text,
|
|
197
|
+
response,
|
|
198
|
+
resource="gemini-api",
|
|
199
|
+
)
|
|
200
|
+
return blocked
|
|
201
|
+
|
|
202
|
+
def generate_content(self, model: Any = None, contents: Any = None, **kwargs: Any) -> Any:
|
|
203
|
+
self._govern(model, contents, kwargs)
|
|
204
|
+
if model is not None:
|
|
205
|
+
kwargs["model"] = model
|
|
206
|
+
if contents is not None:
|
|
207
|
+
kwargs["contents"] = contents
|
|
208
|
+
response = self._models.generate_content(**kwargs)
|
|
209
|
+
return self._scan_response(response)
|
|
210
|
+
|
|
211
|
+
def generate_content_stream(
|
|
212
|
+
self, model: Any = None, contents: Any = None, **kwargs: Any
|
|
213
|
+
) -> Any:
|
|
214
|
+
self._govern(model, contents, kwargs)
|
|
215
|
+
if model is not None:
|
|
216
|
+
kwargs["model"] = model
|
|
217
|
+
if contents is not None:
|
|
218
|
+
kwargs["contents"] = contents
|
|
219
|
+
return self._models.generate_content_stream(**kwargs)
|
|
220
|
+
|
|
221
|
+
async def generate_content_async(
|
|
222
|
+
self, model: Any = None, contents: Any = None, **kwargs: Any
|
|
223
|
+
) -> Any:
|
|
224
|
+
self._govern(model, contents, kwargs)
|
|
225
|
+
if model is not None:
|
|
226
|
+
kwargs["model"] = model
|
|
227
|
+
if contents is not None:
|
|
228
|
+
kwargs["contents"] = contents
|
|
229
|
+
response = await self._models.generate_content_async(**kwargs)
|
|
230
|
+
return self._scan_response(response)
|
|
231
|
+
|
|
232
|
+
async def generate_content_stream_async(
|
|
233
|
+
self, model: Any = None, contents: Any = None, **kwargs: Any
|
|
234
|
+
) -> Any:
|
|
235
|
+
self._govern(model, contents, kwargs)
|
|
236
|
+
if model is not None:
|
|
237
|
+
kwargs["model"] = model
|
|
238
|
+
if contents is not None:
|
|
239
|
+
kwargs["contents"] = contents
|
|
240
|
+
return await self._models.generate_content_stream_async(**kwargs)
|
|
241
|
+
|
|
242
|
+
def __getattr__(self, name: str) -> Any:
|
|
243
|
+
return getattr(self._models, name)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
class IrisGemini(_IrisGeminiBase):
|
|
247
|
+
"""Drop-in replacement for google.genai.Client()."""
|
|
248
|
+
|
|
249
|
+
def __init__(
|
|
250
|
+
self,
|
|
251
|
+
passport: AgentPassport,
|
|
252
|
+
user_email: Optional[str] = None,
|
|
253
|
+
user_role: Optional[str] = None,
|
|
254
|
+
**genai_kwargs: Any,
|
|
255
|
+
):
|
|
256
|
+
from iris_core.dev_trust import print_dev_trust_message
|
|
257
|
+
|
|
258
|
+
print_dev_trust_message()
|
|
259
|
+
genai = _lazy_genai()
|
|
260
|
+
self._passport = passport
|
|
261
|
+
self._user_email = user_email
|
|
262
|
+
self._user_role = user_role
|
|
263
|
+
self._engine = CedarEngine()
|
|
264
|
+
self._vault = EvidenceVault(agent_id=passport.agent_id)
|
|
265
|
+
self._dlp = DLPScanner(passport)
|
|
266
|
+
_load_passport_policy(self._engine, passport)
|
|
267
|
+
self._client = genai.Client(**genai_kwargs)
|
|
268
|
+
self._models_resource = IrisModelsResource(self, self._client.models)
|
|
269
|
+
|
|
270
|
+
@property
|
|
271
|
+
def models(self) -> IrisModelsResource:
|
|
272
|
+
return self._models_resource
|
|
273
|
+
|
|
274
|
+
def __getattr__(self, name: str) -> Any:
|
|
275
|
+
return getattr(self._client, name)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
class IrisGeminiAsync(_IrisGeminiBase):
|
|
279
|
+
"""Async drop-in wrapper for google.genai.Client async methods."""
|
|
280
|
+
|
|
281
|
+
def __init__(
|
|
282
|
+
self,
|
|
283
|
+
passport: AgentPassport,
|
|
284
|
+
user_email: Optional[str] = None,
|
|
285
|
+
user_role: Optional[str] = None,
|
|
286
|
+
**genai_kwargs: Any,
|
|
287
|
+
):
|
|
288
|
+
genai = _lazy_genai()
|
|
289
|
+
self._passport = passport
|
|
290
|
+
self._user_email = user_email
|
|
291
|
+
self._user_role = user_role
|
|
292
|
+
self._engine = CedarEngine()
|
|
293
|
+
self._vault = EvidenceVault(agent_id=passport.agent_id)
|
|
294
|
+
self._dlp = DLPScanner(passport)
|
|
295
|
+
_load_passport_policy(self._engine, passport)
|
|
296
|
+
self._client = genai.Client(**genai_kwargs)
|
|
297
|
+
self._models_resource = IrisModelsResource(self, self._client.models)
|
|
298
|
+
|
|
299
|
+
@property
|
|
300
|
+
def models(self) -> IrisModelsResource:
|
|
301
|
+
return self._models_resource
|
|
302
|
+
|
|
303
|
+
def __getattr__(self, name: str) -> Any:
|
|
304
|
+
return getattr(self._client, name)
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Guardrail scanning for Gemini contents payloads."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import Any, List
|
|
7
|
+
|
|
8
|
+
from iris_core.models.passport import AgentPassport
|
|
9
|
+
from iris_core.models.policy import Severity, Violation
|
|
10
|
+
|
|
11
|
+
_SSN = re.compile(r"\b\d{3}-\d{2}-\d{4}\b")
|
|
12
|
+
_CREDIT_CARD = re.compile(r"\b(?:\d{4}[-\s]?){3}\d{4}\b")
|
|
13
|
+
_DOB = re.compile(
|
|
14
|
+
r"\b(?:0[1-9]|1[0-2])[/-](?:0[1-9]|[12]\d|3[01])[/-](?:19|20)\d{2}\b"
|
|
15
|
+
)
|
|
16
|
+
_CROSS_REGION = re.compile(r"cn-north|china|beijing", re.IGNORECASE)
|
|
17
|
+
_HIGH_RISK_DOMAIN = re.compile(r"\b(loan|diagnosis|hiring)\b", re.IGNORECASE)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _extract_text(contents: Any) -> List[str]:
|
|
21
|
+
if contents is None:
|
|
22
|
+
return []
|
|
23
|
+
if isinstance(contents, str):
|
|
24
|
+
return [contents]
|
|
25
|
+
if isinstance(contents, dict):
|
|
26
|
+
values: List[str] = []
|
|
27
|
+
for key in ("text", "content"):
|
|
28
|
+
value = contents.get(key)
|
|
29
|
+
if isinstance(value, str):
|
|
30
|
+
values.append(value)
|
|
31
|
+
elif value is not None:
|
|
32
|
+
values.extend(_extract_text(value))
|
|
33
|
+
if not values:
|
|
34
|
+
for value in contents.values():
|
|
35
|
+
values.extend(_extract_text(value))
|
|
36
|
+
return values
|
|
37
|
+
if isinstance(contents, (list, tuple)):
|
|
38
|
+
values: List[str] = []
|
|
39
|
+
for item in contents:
|
|
40
|
+
values.extend(_extract_text(item))
|
|
41
|
+
return values
|
|
42
|
+
|
|
43
|
+
text = getattr(contents, "text", None)
|
|
44
|
+
if isinstance(text, str):
|
|
45
|
+
return [text]
|
|
46
|
+
content = getattr(contents, "content", None)
|
|
47
|
+
if content is not None:
|
|
48
|
+
return _extract_text(content)
|
|
49
|
+
return []
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def scan_gemini_content(contents: Any, passport: AgentPassport) -> List[Violation]:
|
|
53
|
+
"""Scan Gemini request contents and return policy violations."""
|
|
54
|
+
prompt = "\n".join(_extract_text(contents))
|
|
55
|
+
if not prompt:
|
|
56
|
+
return []
|
|
57
|
+
|
|
58
|
+
violations: List[Violation] = []
|
|
59
|
+
|
|
60
|
+
if _SSN.search(prompt) or _CREDIT_CARD.search(prompt) or _DOB.search(prompt):
|
|
61
|
+
violations.append(
|
|
62
|
+
Violation(
|
|
63
|
+
rule_id="IRIS-DATA-001",
|
|
64
|
+
severity=Severity.HIGH,
|
|
65
|
+
message=(
|
|
66
|
+
f"Gemini prompt for agent '{passport.name}' may contain PII "
|
|
67
|
+
"(SSN, payment card, or date-of-birth pattern)."
|
|
68
|
+
),
|
|
69
|
+
compliance_refs=[
|
|
70
|
+
"colorado-ai-act:impact-assessment",
|
|
71
|
+
"gdpr:data-minimization",
|
|
72
|
+
],
|
|
73
|
+
remediation=(
|
|
74
|
+
"Remove sensitive identifiers from contents or update the "
|
|
75
|
+
"passport data_classification before this call."
|
|
76
|
+
),
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
if _CROSS_REGION.search(prompt):
|
|
81
|
+
violations.append(
|
|
82
|
+
Violation(
|
|
83
|
+
rule_id="IRIS-XR-001",
|
|
84
|
+
severity=Severity.CRITICAL,
|
|
85
|
+
message=(
|
|
86
|
+
f"Gemini prompt for agent '{passport.name}' references "
|
|
87
|
+
"restricted cross-region geography (China / cn-north)."
|
|
88
|
+
),
|
|
89
|
+
compliance_refs=["china-pipl:cross-border-transfer"],
|
|
90
|
+
remediation=(
|
|
91
|
+
"Remove cross-region references from contents or document an "
|
|
92
|
+
"approved exception with security."
|
|
93
|
+
),
|
|
94
|
+
)
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
if _HIGH_RISK_DOMAIN.search(prompt):
|
|
98
|
+
violations.append(
|
|
99
|
+
Violation(
|
|
100
|
+
rule_id="CO-004",
|
|
101
|
+
severity=Severity.HIGH,
|
|
102
|
+
message=(
|
|
103
|
+
f"Gemini prompt for agent '{passport.name}' references a "
|
|
104
|
+
"high-risk consequential domain (loan, diagnosis, or hiring)."
|
|
105
|
+
),
|
|
106
|
+
compliance_refs=["colorado-ai-act:sb-24-205:consumer-opt-out"],
|
|
107
|
+
remediation=(
|
|
108
|
+
"Set consent evidence in policy context for consequential "
|
|
109
|
+
"processing, or run an IRIS compliance assessment."
|
|
110
|
+
),
|
|
111
|
+
)
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
return violations
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: iris-security-gemini
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: IRIS governance for Google Gemini via google-genai
|
|
5
|
+
Author-email: IRIS Platform <sdk@iris.ai>
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/gimartinb/iris-sdk
|
|
8
|
+
Project-URL: Repository, https://github.com/gimartinb/iris-sdk
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: iris-security-core>=0.1.0
|
|
12
|
+
Requires-Dist: iris-security-sdk>=0.1.0
|
|
13
|
+
Provides-Extra: google
|
|
14
|
+
Requires-Dist: google-genai>=0.3; extra == "google"
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
17
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
18
|
+
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
19
|
+
|
|
20
|
+
# iris-gemini
|
|
21
|
+
|
|
22
|
+
Drop-in IRIS governance for the [Google GenAI Python SDK](https://github.com/googleapis/python-genai).
|
|
23
|
+
|
|
24
|
+
Replace one line:
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
# client = google.genai.Client()
|
|
28
|
+
client = IrisGemini(passport=passport)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Every `client.models.generate_content()` and `generate_content_stream()` call is evaluated against Cedar policy, recorded in the Evidence Vault, and enforced per `IRIS_ENV` (warn in dev, block in production).
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install iris-security-gemini
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Quickstart
|
|
40
|
+
|
|
41
|
+
See [examples/governed_gemini.py](examples/governed_gemini.py).
|
|
42
|
+
|
|
43
|
+
## Environment
|
|
44
|
+
|
|
45
|
+
| `IRIS_ENV` | Behavior |
|
|
46
|
+
|---------------|---------------------------------------------|
|
|
47
|
+
| `dev` | Fail open - warnings to stderr, never block |
|
|
48
|
+
| `production` | Fail closed - `IrisViolationError` on deny |
|
|
49
|
+
|
|
50
|
+
Defaults to `dev` when unset.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
iris_gemini/__init__.py
|
|
4
|
+
iris_gemini/client.py
|
|
5
|
+
iris_gemini/guardrails.py
|
|
6
|
+
iris_gemini/init.py
|
|
7
|
+
iris_security_gemini.egg-info/PKG-INFO
|
|
8
|
+
iris_security_gemini.egg-info/SOURCES.txt
|
|
9
|
+
iris_security_gemini.egg-info/dependency_links.txt
|
|
10
|
+
iris_security_gemini.egg-info/requires.txt
|
|
11
|
+
iris_security_gemini.egg-info/top_level.txt
|
|
12
|
+
tests/test_gemini_integration.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
iris_gemini
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "iris-security-gemini"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "IRIS governance for Google Gemini via google-genai"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "Apache-2.0" }
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [{ name = "IRIS Platform", email = "sdk@iris.ai" }]
|
|
13
|
+
dependencies = [
|
|
14
|
+
"iris-security-core>=0.1.0",
|
|
15
|
+
"iris-security-sdk>=0.1.0",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.optional-dependencies]
|
|
19
|
+
google = ["google-genai>=0.3"]
|
|
20
|
+
dev = [
|
|
21
|
+
"pytest>=8.0",
|
|
22
|
+
"pytest-asyncio>=0.23",
|
|
23
|
+
"ruff>=0.4",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://github.com/gimartinb/iris-sdk"
|
|
28
|
+
Repository = "https://github.com/gimartinb/iris-sdk"
|
|
29
|
+
|
|
30
|
+
[tool.setuptools]
|
|
31
|
+
packages = ["iris_gemini"]
|
|
32
|
+
|
|
33
|
+
[tool.ruff]
|
|
34
|
+
line-length = 100
|
|
35
|
+
target-version = "py310"
|
|
36
|
+
|
|
37
|
+
[tool.pytest.ini_options]
|
|
38
|
+
testpaths = ["tests"]
|
|
39
|
+
asyncio_mode = "auto"
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""Integration tests for iris-gemini - mocked google-genai, no network calls."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import types
|
|
6
|
+
from unittest.mock import AsyncMock, MagicMock, patch
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from iris import IrisViolationError
|
|
11
|
+
from iris_core.engine.cedar import CedarEngine
|
|
12
|
+
from iris_core.evidence.vault import EvidenceVault
|
|
13
|
+
from iris_core.models.passport import AgentPassport, ComplianceTag, DataClassification, Environment
|
|
14
|
+
from iris_gemini import IrisGemini
|
|
15
|
+
from iris_gemini.guardrails import scan_gemini_content
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@pytest.fixture
|
|
19
|
+
def compliant_passport():
|
|
20
|
+
return AgentPassport(
|
|
21
|
+
name="gemini-agent",
|
|
22
|
+
owner="team@company.com",
|
|
23
|
+
data_classification=DataClassification.INTERNAL,
|
|
24
|
+
compliance_tags=[ComplianceTag.COLORADO_AI_ACT],
|
|
25
|
+
environments=[Environment.DEV, Environment.PRODUCTION],
|
|
26
|
+
is_high_risk_ai=True,
|
|
27
|
+
evidence_vault_id="vault-abc",
|
|
28
|
+
intent_ref="governance/agents/gemini-agent/policy-intent.md",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@pytest.fixture
|
|
33
|
+
def high_risk_incomplete_passport():
|
|
34
|
+
return AgentPassport(
|
|
35
|
+
name="loan-agent",
|
|
36
|
+
owner="gmoney@gmail.com",
|
|
37
|
+
compliance_tags=[ComplianceTag.COLORADO_AI_ACT],
|
|
38
|
+
environments=[Environment.DEV, Environment.PRODUCTION],
|
|
39
|
+
is_high_risk_ai=True,
|
|
40
|
+
evidence_vault_id=None,
|
|
41
|
+
intent_ref=None,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _mock_google_genai_module():
|
|
46
|
+
genai_module = MagicMock()
|
|
47
|
+
response = MagicMock()
|
|
48
|
+
response.text = "Hello from Gemini"
|
|
49
|
+
models = MagicMock()
|
|
50
|
+
models.generate_content.return_value = response
|
|
51
|
+
models.generate_content_stream.return_value = iter([response])
|
|
52
|
+
models.generate_content_async = AsyncMock(return_value=response)
|
|
53
|
+
models.generate_content_stream_async = AsyncMock(return_value=response)
|
|
54
|
+
|
|
55
|
+
client = MagicMock()
|
|
56
|
+
client.models = models
|
|
57
|
+
client.api_key = "test-key"
|
|
58
|
+
client.endpoint = "https://generativelanguage.googleapis.com"
|
|
59
|
+
genai_module.Client.return_value = client
|
|
60
|
+
|
|
61
|
+
google_module = types.ModuleType("google")
|
|
62
|
+
google_module.genai = genai_module
|
|
63
|
+
return google_module, genai_module, client
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class Part:
|
|
67
|
+
def __init__(self, text):
|
|
68
|
+
self.text = text
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class TestIrisGeminiClient:
|
|
72
|
+
def test_client_permits_allowed_call(self, compliant_passport, tmp_path, monkeypatch):
|
|
73
|
+
monkeypatch.setenv("IRIS_ENV", "dev")
|
|
74
|
+
google_module, genai_module, mock_client = _mock_google_genai_module()
|
|
75
|
+
engine = CedarEngine()
|
|
76
|
+
engine.load_policy(compliant_passport.agent_id, "permit(principal, action, resource);")
|
|
77
|
+
vault = EvidenceVault(agent_id=compliant_passport.agent_id, vault_dir=tmp_path)
|
|
78
|
+
|
|
79
|
+
with patch.dict("sys.modules", {"google": google_module, "google.genai": genai_module}):
|
|
80
|
+
client = IrisGemini(passport=compliant_passport)
|
|
81
|
+
client._engine = engine
|
|
82
|
+
client._vault = vault
|
|
83
|
+
result = client.models.generate_content(
|
|
84
|
+
model="gemini-2.0-flash",
|
|
85
|
+
contents="Help this customer.",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
assert result.text == "Hello from Gemini"
|
|
89
|
+
mock_client.models.generate_content.assert_called_once()
|
|
90
|
+
events = vault.get_events()
|
|
91
|
+
assert len(events) == 1
|
|
92
|
+
assert events[0]["decision"] in ("PERMIT", "PERMIT_WITH_WARNINGS")
|
|
93
|
+
assert events[0]["resource"] == "gemini-api/gemini-2.0-flash"
|
|
94
|
+
|
|
95
|
+
def test_client_blocks_in_production(
|
|
96
|
+
self, high_risk_incomplete_passport, tmp_path, monkeypatch
|
|
97
|
+
):
|
|
98
|
+
monkeypatch.setenv("IRIS_ENV", "production")
|
|
99
|
+
google_module, genai_module, mock_client = _mock_google_genai_module()
|
|
100
|
+
engine = CedarEngine()
|
|
101
|
+
engine.load_policy(
|
|
102
|
+
high_risk_incomplete_passport.agent_id,
|
|
103
|
+
"permit(principal, action, resource);",
|
|
104
|
+
)
|
|
105
|
+
vault = EvidenceVault(
|
|
106
|
+
agent_id=high_risk_incomplete_passport.agent_id,
|
|
107
|
+
vault_dir=tmp_path,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
with patch.dict("sys.modules", {"google": google_module, "google.genai": genai_module}):
|
|
111
|
+
client = IrisGemini(passport=high_risk_incomplete_passport)
|
|
112
|
+
client._engine = engine
|
|
113
|
+
client._vault = vault
|
|
114
|
+
with pytest.raises(IrisViolationError):
|
|
115
|
+
client.models.generate_content(
|
|
116
|
+
model="gemini-2.0-flash",
|
|
117
|
+
contents="Approve this loan application for decisioning.",
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
mock_client.models.generate_content.assert_not_called()
|
|
121
|
+
|
|
122
|
+
def test_client_warns_in_dev(self, high_risk_incomplete_passport, tmp_path, monkeypatch, capsys):
|
|
123
|
+
monkeypatch.setenv("IRIS_ENV", "dev")
|
|
124
|
+
google_module, genai_module, mock_client = _mock_google_genai_module()
|
|
125
|
+
engine = CedarEngine()
|
|
126
|
+
engine.load_policy(
|
|
127
|
+
high_risk_incomplete_passport.agent_id,
|
|
128
|
+
"permit(principal, action, resource);",
|
|
129
|
+
)
|
|
130
|
+
vault = EvidenceVault(
|
|
131
|
+
agent_id=high_risk_incomplete_passport.agent_id,
|
|
132
|
+
vault_dir=tmp_path,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
with patch.dict("sys.modules", {"google": google_module, "google.genai": genai_module}):
|
|
136
|
+
client = IrisGemini(passport=high_risk_incomplete_passport)
|
|
137
|
+
client._engine = engine
|
|
138
|
+
client._vault = vault
|
|
139
|
+
client.models.generate_content(
|
|
140
|
+
model="gemini-2.0-flash",
|
|
141
|
+
contents="Approve this loan application for decisioning.",
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
mock_client.models.generate_content.assert_called_once()
|
|
145
|
+
captured = capsys.readouterr()
|
|
146
|
+
assert "[IRIS WARNING]" in captured.err
|
|
147
|
+
|
|
148
|
+
def test_streaming_intercept(self, compliant_passport, tmp_path, monkeypatch):
|
|
149
|
+
monkeypatch.setenv("IRIS_ENV", "dev")
|
|
150
|
+
google_module, genai_module, mock_client = _mock_google_genai_module()
|
|
151
|
+
engine = CedarEngine()
|
|
152
|
+
engine.load_policy(compliant_passport.agent_id, "permit(principal, action, resource);")
|
|
153
|
+
vault = EvidenceVault(agent_id=compliant_passport.agent_id, vault_dir=tmp_path)
|
|
154
|
+
|
|
155
|
+
with patch.dict("sys.modules", {"google": google_module, "google.genai": genai_module}):
|
|
156
|
+
client = IrisGemini(passport=compliant_passport)
|
|
157
|
+
client._engine = engine
|
|
158
|
+
client._vault = vault
|
|
159
|
+
stream = client.models.generate_content_stream(
|
|
160
|
+
model="gemini-2.0-flash",
|
|
161
|
+
contents="Stream a short response.",
|
|
162
|
+
)
|
|
163
|
+
list(stream)
|
|
164
|
+
|
|
165
|
+
mock_client.models.generate_content_stream.assert_called_once()
|
|
166
|
+
assert len(vault.get_events()) == 1
|
|
167
|
+
|
|
168
|
+
def test_evidence_vault_records_call(self, compliant_passport, tmp_path, monkeypatch):
|
|
169
|
+
monkeypatch.setenv("IRIS_ENV", "dev")
|
|
170
|
+
google_module, genai_module, _ = _mock_google_genai_module()
|
|
171
|
+
engine = CedarEngine()
|
|
172
|
+
engine.load_policy(compliant_passport.agent_id, "permit(principal, action, resource);")
|
|
173
|
+
vault = EvidenceVault(agent_id=compliant_passport.agent_id, vault_dir=tmp_path)
|
|
174
|
+
|
|
175
|
+
with patch.dict("sys.modules", {"google": google_module, "google.genai": genai_module}):
|
|
176
|
+
client = IrisGemini(passport=compliant_passport)
|
|
177
|
+
client._engine = engine
|
|
178
|
+
client._vault = vault
|
|
179
|
+
client.models.generate_content(model="gemini-2.0-flash", contents="First call")
|
|
180
|
+
|
|
181
|
+
events = vault.get_events()
|
|
182
|
+
assert len(events) == 1
|
|
183
|
+
assert events[0]["action"] == "call"
|
|
184
|
+
assert events[0]["resource"] == "gemini-api/gemini-2.0-flash"
|
|
185
|
+
|
|
186
|
+
def test_drop_in_replacement_compatibility(self, compliant_passport, monkeypatch):
|
|
187
|
+
monkeypatch.setenv("IRIS_ENV", "dev")
|
|
188
|
+
google_module, genai_module, mock_client = _mock_google_genai_module()
|
|
189
|
+
|
|
190
|
+
with patch.dict("sys.modules", {"google": google_module, "google.genai": genai_module}):
|
|
191
|
+
client = IrisGemini(passport=compliant_passport, api_key="test-key")
|
|
192
|
+
|
|
193
|
+
assert client.api_key == "test-key"
|
|
194
|
+
assert client.endpoint == "https://generativelanguage.googleapis.com"
|
|
195
|
+
genai_module.Client.assert_called_once_with(api_key="test-key")
|
|
196
|
+
assert client.models is not None
|
|
197
|
+
assert mock_client.models is not None
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class TestGeminiGuardrails:
|
|
201
|
+
def test_pii_detection_in_contents(self, compliant_passport):
|
|
202
|
+
violations = scan_gemini_content(
|
|
203
|
+
[Part("Customer SSN is 123-45-6789 for verification.")],
|
|
204
|
+
compliant_passport,
|
|
205
|
+
)
|
|
206
|
+
assert any(v.rule_id == "IRIS-DATA-001" for v in violations)
|
|
207
|
+
|
|
208
|
+
def test_cross_region_detection(self, compliant_passport):
|
|
209
|
+
violations = scan_gemini_content(
|
|
210
|
+
["Send this data to beijing data center."],
|
|
211
|
+
compliant_passport,
|
|
212
|
+
)
|
|
213
|
+
assert any(v.rule_id == "IRIS-XR-001" for v in violations)
|