iris-security-sdk 0.2.12__tar.gz → 0.2.16__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_sdk-0.2.12 → iris_security_sdk-0.2.16}/PKG-INFO +1 -1
- {iris_security_sdk-0.2.12 → iris_security_sdk-0.2.16}/iris/__init__.py +9 -1
- {iris_security_sdk-0.2.12 → iris_security_sdk-0.2.16}/iris/_telemetry.py +80 -2
- iris_security_sdk-0.2.16/iris/scan.py +227 -0
- iris_security_sdk-0.2.16/iris_sdk/__init__.py +1 -0
- iris_security_sdk-0.2.16/iris_sdk/scan.py +17 -0
- {iris_security_sdk-0.2.12 → iris_security_sdk-0.2.16}/iris_security_sdk.egg-info/PKG-INFO +1 -1
- {iris_security_sdk-0.2.12 → iris_security_sdk-0.2.16}/iris_security_sdk.egg-info/SOURCES.txt +4 -0
- {iris_security_sdk-0.2.12 → iris_security_sdk-0.2.16}/iris_security_sdk.egg-info/top_level.txt +1 -0
- {iris_security_sdk-0.2.12 → iris_security_sdk-0.2.16}/pyproject.toml +1 -1
- iris_security_sdk-0.2.16/tests/test_scan.py +51 -0
- iris_security_sdk-0.2.16/tests/test_telemetry.py +105 -0
- iris_security_sdk-0.2.12/tests/test_telemetry.py +0 -46
- {iris_security_sdk-0.2.12 → iris_security_sdk-0.2.16}/README.md +0 -0
- {iris_security_sdk-0.2.12 → iris_security_sdk-0.2.16}/iris/_telemetry_config.py +0 -0
- {iris_security_sdk-0.2.12 → iris_security_sdk-0.2.16}/iris/hitl.py +0 -0
- {iris_security_sdk-0.2.12 → iris_security_sdk-0.2.16}/iris_security_sdk.egg-info/dependency_links.txt +0 -0
- {iris_security_sdk-0.2.12 → iris_security_sdk-0.2.16}/iris_security_sdk.egg-info/requires.txt +0 -0
- {iris_security_sdk-0.2.12 → iris_security_sdk-0.2.16}/setup.cfg +0 -0
|
@@ -47,7 +47,11 @@ from iris_core.evidence.vault import EvidenceVault
|
|
|
47
47
|
from iris_core.cost.tracker import CostSummary, CostTracker, CostEntry
|
|
48
48
|
from iris_core.cost.pricing import PricingRegistry
|
|
49
49
|
|
|
50
|
-
__version__ = "0.2.
|
|
50
|
+
__version__ = "0.2.16"
|
|
51
|
+
|
|
52
|
+
AARM_CONFORMANCE = "Core" # R1-R6 satisfied
|
|
53
|
+
AARM_VERSION = "1.0"
|
|
54
|
+
AIUC1_ALIGNMENT = "Q1-2026"
|
|
51
55
|
__all__ = [
|
|
52
56
|
# Main classes
|
|
53
57
|
"IrisAgent",
|
|
@@ -74,6 +78,10 @@ __all__ = [
|
|
|
74
78
|
"CostSummary",
|
|
75
79
|
"CostEntry",
|
|
76
80
|
"PricingRegistry",
|
|
81
|
+
"AARM_CONFORMANCE",
|
|
82
|
+
"AARM_VERSION",
|
|
83
|
+
"AIUC1_ALIGNMENT",
|
|
84
|
+
"__version__",
|
|
77
85
|
"IrisViolationError",
|
|
78
86
|
"IrisCrisisDetectedError",
|
|
79
87
|
"IrisHITLRequiredError",
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
"""First-run telemetry for IRIS SDK installs. Opt-out via IRIS_TELEMETRY_OPT_OUT=1.
|
|
2
2
|
|
|
3
3
|
Each event includes an ISO 8601 UTC timestamp for server-side DoD/WoW/MoM aggregation.
|
|
4
|
+
Daily CLI usage is aggregated locally and sent once per UTC day (previous day's totals).
|
|
4
5
|
"""
|
|
5
6
|
|
|
6
7
|
from __future__ import annotations
|
|
7
8
|
|
|
9
|
+
import json
|
|
8
10
|
import os
|
|
9
11
|
import shutil
|
|
10
12
|
import sys
|
|
@@ -13,6 +15,7 @@ import uuid
|
|
|
13
15
|
from datetime import datetime, timezone
|
|
14
16
|
from importlib.metadata import PackageNotFoundError, version
|
|
15
17
|
from pathlib import Path
|
|
18
|
+
from typing import Any
|
|
16
19
|
|
|
17
20
|
from iris._telemetry_config import TELEMETRY_ENABLED, TELEMETRY_ENDPOINT
|
|
18
21
|
|
|
@@ -20,6 +23,8 @@ _IRIS_DIR = Path.home() / ".iris"
|
|
|
20
23
|
_INSTALL_ID_FILE = _IRIS_DIR / "install_id"
|
|
21
24
|
_FIRST_RUN_SENTINEL = _IRIS_DIR / ".telemetry_sent"
|
|
22
25
|
_FIRST_POLICY_SENTINEL = _IRIS_DIR / ".first_policy_sent"
|
|
26
|
+
_RUN_STATS_FILE = _IRIS_DIR / "run_stats.json"
|
|
27
|
+
_SKIPPED_COMMANDS = frozenset({"ping"})
|
|
23
28
|
|
|
24
29
|
|
|
25
30
|
def detect_country() -> str:
|
|
@@ -127,6 +132,10 @@ def _get_iris_version() -> str:
|
|
|
127
132
|
return "unknown"
|
|
128
133
|
|
|
129
134
|
|
|
135
|
+
def _utc_today() -> str:
|
|
136
|
+
return datetime.now(timezone.utc).date().isoformat()
|
|
137
|
+
|
|
138
|
+
|
|
130
139
|
def _build_payload(event: str) -> dict[str, str]:
|
|
131
140
|
return {
|
|
132
141
|
"event": event,
|
|
@@ -142,15 +151,56 @@ def _build_payload(event: str) -> dict[str, str]:
|
|
|
142
151
|
}
|
|
143
152
|
|
|
144
153
|
|
|
145
|
-
def
|
|
154
|
+
def _build_daily_payload(stats: dict[str, Any]) -> dict[str, str]:
|
|
155
|
+
payload = _build_payload("daily_usage")
|
|
156
|
+
payload["usage_date"] = str(stats["date"])
|
|
157
|
+
payload["run_count"] = str(stats["run_count"])
|
|
158
|
+
payload["commands"] = json.dumps(stats.get("commands", {}), sort_keys=True)
|
|
159
|
+
return payload
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _empty_day_stats(day: str) -> dict[str, Any]:
|
|
163
|
+
return {"date": day, "run_count": 0, "commands": {}}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _load_run_stats() -> dict[str, Any]:
|
|
167
|
+
if not _RUN_STATS_FILE.exists():
|
|
168
|
+
return _empty_day_stats(_utc_today())
|
|
169
|
+
try:
|
|
170
|
+
data = json.loads(_RUN_STATS_FILE.read_text(encoding="utf-8"))
|
|
171
|
+
if not isinstance(data, dict):
|
|
172
|
+
return _empty_day_stats(_utc_today())
|
|
173
|
+
data.setdefault("commands", {})
|
|
174
|
+
return data
|
|
175
|
+
except (OSError, json.JSONDecodeError):
|
|
176
|
+
return _empty_day_stats(_utc_today())
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _save_run_stats(stats: dict[str, Any]) -> None:
|
|
180
|
+
_RUN_STATS_FILE.write_text(json.dumps(stats, indent=2, sort_keys=True), encoding="utf-8")
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _send_payload(payload: dict[str, str]) -> None:
|
|
146
184
|
try:
|
|
147
185
|
import httpx
|
|
148
186
|
|
|
149
|
-
httpx.post(TELEMETRY_ENDPOINT, json=
|
|
187
|
+
httpx.post(TELEMETRY_ENDPOINT, json=payload, timeout=2.0)
|
|
150
188
|
except Exception:
|
|
151
189
|
pass
|
|
152
190
|
|
|
153
191
|
|
|
192
|
+
def _send_event(event: str) -> None:
|
|
193
|
+
_send_payload(_build_payload(event))
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _flush_daily_usage(stats: dict[str, Any]) -> None:
|
|
197
|
+
if int(stats.get("run_count", 0)) <= 0:
|
|
198
|
+
return
|
|
199
|
+
payload = _build_daily_payload(stats)
|
|
200
|
+
thread = threading.Thread(target=_send_payload, args=(payload,), daemon=True)
|
|
201
|
+
thread.start()
|
|
202
|
+
|
|
203
|
+
|
|
154
204
|
def _maybe_fire_once(event: str, sentinel: Path) -> None:
|
|
155
205
|
if not telemetry_enabled():
|
|
156
206
|
return
|
|
@@ -178,6 +228,34 @@ def maybe_fire_first_policy_run() -> None:
|
|
|
178
228
|
_maybe_fire_once("first_policy_run", _FIRST_POLICY_SENTINEL)
|
|
179
229
|
|
|
180
230
|
|
|
231
|
+
def maybe_record_cli_usage(command: str) -> None:
|
|
232
|
+
"""Increment local run counters; send one daily_usage event per UTC day on rollover."""
|
|
233
|
+
if not telemetry_enabled() or not command or command in _SKIPPED_COMMANDS:
|
|
234
|
+
return
|
|
235
|
+
|
|
236
|
+
today = _utc_today()
|
|
237
|
+
try:
|
|
238
|
+
_IRIS_DIR.mkdir(parents=True, exist_ok=True)
|
|
239
|
+
stats = _load_run_stats()
|
|
240
|
+
except OSError:
|
|
241
|
+
return
|
|
242
|
+
|
|
243
|
+
stored_date = stats.get("date")
|
|
244
|
+
if stored_date != today:
|
|
245
|
+
if stored_date and int(stats.get("run_count", 0)) > 0:
|
|
246
|
+
_flush_daily_usage(stats)
|
|
247
|
+
stats = _empty_day_stats(today)
|
|
248
|
+
|
|
249
|
+
commands = stats.setdefault("commands", {})
|
|
250
|
+
commands[command] = int(commands.get(command, 0)) + 1
|
|
251
|
+
stats["run_count"] = int(stats.get("run_count", 0)) + 1
|
|
252
|
+
|
|
253
|
+
try:
|
|
254
|
+
_save_run_stats(stats)
|
|
255
|
+
except OSError:
|
|
256
|
+
pass
|
|
257
|
+
|
|
258
|
+
|
|
181
259
|
def send_ping() -> None:
|
|
182
260
|
"""Fire a test telemetry ping event (internal verification)."""
|
|
183
261
|
if not telemetry_enabled():
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""Offline workload detection for compliance intelligence scans."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
_PROVIDER_IMPORTS = {
|
|
11
|
+
"openai": "openai",
|
|
12
|
+
"anthropic": "anthropic",
|
|
13
|
+
"google": "google",
|
|
14
|
+
"generativeai": "google",
|
|
15
|
+
"vertexai": "google",
|
|
16
|
+
"azure": "azure",
|
|
17
|
+
"bedrock": "aws",
|
|
18
|
+
"boto3": "aws",
|
|
19
|
+
"cohere": "cohere",
|
|
20
|
+
"mistralai": "mistral",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
_FRAMEWORK_IMPORTS = {
|
|
24
|
+
"langchain": "langchain",
|
|
25
|
+
"crewai": "crewai",
|
|
26
|
+
"llama_index": "llama_index",
|
|
27
|
+
"semantic_kernel": "semantic_kernel",
|
|
28
|
+
"autogen": "autogen",
|
|
29
|
+
"haystack": "haystack",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
_MODEL_PATTERNS = [
|
|
33
|
+
re.compile(r"gpt-4[a-z0-9.-]*", re.I),
|
|
34
|
+
re.compile(r"gpt-3\.5[a-z0-9.-]*", re.I),
|
|
35
|
+
re.compile(r"claude-[a-z0-9.-]+", re.I),
|
|
36
|
+
re.compile(r"gemini-[a-z0-9.-]+", re.I),
|
|
37
|
+
re.compile(r"text-embedding-[a-z0-9.-]+", re.I),
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
_MODEL_PROVIDER_PREFIXES: list[tuple[re.Pattern[str], str]] = [
|
|
41
|
+
(re.compile(r"^gpt-", re.I), "openai"),
|
|
42
|
+
(re.compile(r"^o[134]-", re.I), "openai"),
|
|
43
|
+
(re.compile(r"^text-embedding-", re.I), "openai"),
|
|
44
|
+
(re.compile(r"^claude-", re.I), "anthropic"),
|
|
45
|
+
(re.compile(r"^gemini-", re.I), "google"),
|
|
46
|
+
(re.compile(r"^gemini/", re.I), "google"),
|
|
47
|
+
(re.compile(r"^text-bison", re.I), "google"),
|
|
48
|
+
(re.compile(r"^command", re.I), "cohere"),
|
|
49
|
+
(re.compile(r"^mistral", re.I), "mistral"),
|
|
50
|
+
(re.compile(r"^llama", re.I), "meta"),
|
|
51
|
+
(re.compile(r"^amazon\.", re.I), "aws"),
|
|
52
|
+
(re.compile(r"^anthropic\.", re.I), "anthropic"),
|
|
53
|
+
(re.compile(r"^azure/", re.I), "azure"),
|
|
54
|
+
(re.compile(r"^vertex_ai/", re.I), "google"),
|
|
55
|
+
(re.compile(r"^openai/", re.I), "openai"),
|
|
56
|
+
(re.compile(r"^bedrock/", re.I), "aws"),
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
_DATA_CATEGORY_PATTERNS = {
|
|
60
|
+
"pii": re.compile(
|
|
61
|
+
r"\b(ssn|social_security|date_of_birth|dob|passport|email_address|phone_number|"
|
|
62
|
+
r"first_name|last_name|address_line|national_id)\b",
|
|
63
|
+
re.I,
|
|
64
|
+
),
|
|
65
|
+
"phi": re.compile(
|
|
66
|
+
r"\b(patient_id|medical_record|diagnosis|icd_?10|hipaa|health_record|"
|
|
67
|
+
r"prescription|mrn|protected_health)\b",
|
|
68
|
+
re.I,
|
|
69
|
+
),
|
|
70
|
+
"financial": re.compile(
|
|
71
|
+
r"\b(credit_card|card_number|bank_account|routing_number|iban|pci|"
|
|
72
|
+
r"account_balance|transaction_amount)\b",
|
|
73
|
+
re.I,
|
|
74
|
+
),
|
|
75
|
+
"biometric": re.compile(
|
|
76
|
+
r"\b(fingerprint|face_id|retina_scan|voice_print|biometric)\b",
|
|
77
|
+
re.I,
|
|
78
|
+
),
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
_REQUIREMENTS_FILES = (
|
|
82
|
+
"requirements.txt",
|
|
83
|
+
"requirements-dev.txt",
|
|
84
|
+
"pyproject.toml",
|
|
85
|
+
"Pipfile",
|
|
86
|
+
"setup.py",
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _iter_source_files(root: Path) -> list[Path]:
|
|
91
|
+
files: list[Path] = []
|
|
92
|
+
skip = {".git", ".venv", "venv", "node_modules", "__pycache__", ".iris", "dist", "build"}
|
|
93
|
+
for path in root.rglob("*"):
|
|
94
|
+
if any(part in skip for part in path.parts):
|
|
95
|
+
continue
|
|
96
|
+
if path.suffix in {".py", ".pyi", ".ts", ".tsx", ".js", ".jsx", ".json", ".yaml", ".yml"}:
|
|
97
|
+
if path.stat().st_size > 512_000:
|
|
98
|
+
continue
|
|
99
|
+
files.append(path)
|
|
100
|
+
return files
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _scan_imports(text: str) -> tuple[set[str], set[str]]:
|
|
104
|
+
providers: set[str] = set()
|
|
105
|
+
frameworks: set[str] = set()
|
|
106
|
+
for line in text.splitlines():
|
|
107
|
+
stripped = line.strip()
|
|
108
|
+
if stripped.startswith("import ") or stripped.startswith("from "):
|
|
109
|
+
lowered = stripped.lower()
|
|
110
|
+
for needle, provider in _PROVIDER_IMPORTS.items():
|
|
111
|
+
if needle in lowered:
|
|
112
|
+
providers.add(provider)
|
|
113
|
+
for needle, fw in _FRAMEWORK_IMPORTS.items():
|
|
114
|
+
if needle in lowered:
|
|
115
|
+
frameworks.add(fw)
|
|
116
|
+
return providers, frameworks
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _scan_models(text: str) -> set[str]:
|
|
120
|
+
models: set[str] = set()
|
|
121
|
+
for pattern in _MODEL_PATTERNS:
|
|
122
|
+
for match in pattern.findall(text):
|
|
123
|
+
models.add(match.lower())
|
|
124
|
+
return models
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _scan_data_categories(text: str) -> set[str]:
|
|
128
|
+
categories: set[str] = set()
|
|
129
|
+
for category, pattern in _DATA_CATEGORY_PATTERNS.items():
|
|
130
|
+
if pattern.search(text):
|
|
131
|
+
categories.add(category)
|
|
132
|
+
return categories
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def infer_provider_from_model(model: str) -> str | None:
|
|
136
|
+
"""Infer provider slug from a model identifier (shared by observability adapters)."""
|
|
137
|
+
if not model:
|
|
138
|
+
return None
|
|
139
|
+
normalized = model.strip()
|
|
140
|
+
for pattern, provider in _MODEL_PROVIDER_PREFIXES:
|
|
141
|
+
if pattern.search(normalized):
|
|
142
|
+
return provider
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def infer_providers_from_models(models: list[str]) -> list[str]:
|
|
147
|
+
"""Return sorted unique providers inferred from model names."""
|
|
148
|
+
providers: set[str] = set()
|
|
149
|
+
for model in models:
|
|
150
|
+
provider = infer_provider_from_model(model)
|
|
151
|
+
if provider:
|
|
152
|
+
providers.add(provider)
|
|
153
|
+
return sorted(providers)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def scan_data_categories_from_text(text: str) -> list[str]:
|
|
157
|
+
"""Scan arbitrary metadata/tags text for sensitive data category hints."""
|
|
158
|
+
return sorted(_scan_data_categories(text))
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _scan_requirements(root: Path) -> tuple[set[str], set[str]]:
|
|
162
|
+
providers: set[str] = set()
|
|
163
|
+
frameworks: set[str] = set()
|
|
164
|
+
for name in _REQUIREMENTS_FILES:
|
|
165
|
+
req_path = root / name
|
|
166
|
+
if not req_path.exists():
|
|
167
|
+
continue
|
|
168
|
+
text = req_path.read_text(encoding="utf-8", errors="ignore").lower()
|
|
169
|
+
for needle, provider in _PROVIDER_IMPORTS.items():
|
|
170
|
+
if needle in text:
|
|
171
|
+
providers.add(provider)
|
|
172
|
+
for needle, fw in _FRAMEWORK_IMPORTS.items():
|
|
173
|
+
if needle in text:
|
|
174
|
+
frameworks.add(fw)
|
|
175
|
+
return providers, frameworks
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def detect_workload(path: str = ".") -> dict[str, Any]:
|
|
179
|
+
"""Static offline detection of workload profile attributes."""
|
|
180
|
+
root = Path(path).resolve()
|
|
181
|
+
providers: set[str] = set()
|
|
182
|
+
frameworks: set[str] = set()
|
|
183
|
+
models: set[str] = set()
|
|
184
|
+
data_categories: set[str] = set()
|
|
185
|
+
|
|
186
|
+
req_providers, req_frameworks = _scan_requirements(root)
|
|
187
|
+
providers |= req_providers
|
|
188
|
+
frameworks |= req_frameworks
|
|
189
|
+
|
|
190
|
+
for file_path in _iter_source_files(root):
|
|
191
|
+
try:
|
|
192
|
+
text = file_path.read_text(encoding="utf-8", errors="ignore")
|
|
193
|
+
except OSError:
|
|
194
|
+
continue
|
|
195
|
+
p, f = _scan_imports(text)
|
|
196
|
+
providers |= p
|
|
197
|
+
frameworks |= f
|
|
198
|
+
models |= _scan_models(text)
|
|
199
|
+
data_categories |= _scan_data_categories(text)
|
|
200
|
+
|
|
201
|
+
agent_hints = sum(1 for _ in root.rglob("passport.yaml"))
|
|
202
|
+
autonomy = "assistive"
|
|
203
|
+
if agent_hints >= 3:
|
|
204
|
+
autonomy = "supervised"
|
|
205
|
+
if agent_hints >= 6:
|
|
206
|
+
autonomy = "autonomous"
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
"source": "sdk_scan",
|
|
210
|
+
"models": sorted(models),
|
|
211
|
+
"providers": sorted(providers),
|
|
212
|
+
"frameworks": sorted(frameworks),
|
|
213
|
+
"data_categories": sorted(data_categories),
|
|
214
|
+
"deployment_regions": ["us"],
|
|
215
|
+
"agent_count": max(agent_hints, 1 if models or providers else 0),
|
|
216
|
+
"autonomy_level": autonomy,
|
|
217
|
+
"customer_facing": any(
|
|
218
|
+
part in {"pages", "app", "frontend", "ui", "web"} for part in root.parts
|
|
219
|
+
),
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def profile_payload_hash(profile: dict[str, Any]) -> str:
|
|
224
|
+
import hashlib
|
|
225
|
+
|
|
226
|
+
canonical = json.dumps(profile, sort_keys=True, separators=(",", ":"))
|
|
227
|
+
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""iris_sdk namespace package for scan helpers."""
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Re-export scan helpers for iris_sdk import path."""
|
|
2
|
+
|
|
3
|
+
from iris.scan import (
|
|
4
|
+
detect_workload,
|
|
5
|
+
infer_provider_from_model,
|
|
6
|
+
infer_providers_from_models,
|
|
7
|
+
profile_payload_hash,
|
|
8
|
+
scan_data_categories_from_text,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"detect_workload",
|
|
13
|
+
"infer_provider_from_model",
|
|
14
|
+
"infer_providers_from_models",
|
|
15
|
+
"profile_payload_hash",
|
|
16
|
+
"scan_data_categories_from_text",
|
|
17
|
+
]
|
{iris_security_sdk-0.2.12 → iris_security_sdk-0.2.16}/iris_security_sdk.egg-info/SOURCES.txt
RENAMED
|
@@ -4,9 +4,13 @@ iris/__init__.py
|
|
|
4
4
|
iris/_telemetry.py
|
|
5
5
|
iris/_telemetry_config.py
|
|
6
6
|
iris/hitl.py
|
|
7
|
+
iris/scan.py
|
|
8
|
+
iris_sdk/__init__.py
|
|
9
|
+
iris_sdk/scan.py
|
|
7
10
|
iris_security_sdk.egg-info/PKG-INFO
|
|
8
11
|
iris_security_sdk.egg-info/SOURCES.txt
|
|
9
12
|
iris_security_sdk.egg-info/dependency_links.txt
|
|
10
13
|
iris_security_sdk.egg-info/requires.txt
|
|
11
14
|
iris_security_sdk.egg-info/top_level.txt
|
|
15
|
+
tests/test_scan.py
|
|
12
16
|
tests/test_telemetry.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "iris-security-sdk"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.16"
|
|
8
8
|
description = "IRIS — AI Agent Governance SDK. Govern AI agents locally."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [{ name = "Gilbert Martin", email = "gilbert@iris-security.io" }]
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Copyright 2024-2025 Gilbert Martin / IRIS Security, Inc.
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from unittest.mock import patch
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from iris.scan import detect_workload
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@pytest.fixture
|
|
14
|
+
def sample_project(tmp_path: Path) -> Path:
|
|
15
|
+
(tmp_path / "requirements.txt").write_text(
|
|
16
|
+
"openai>=1.0\nlangchain>=0.2\n",
|
|
17
|
+
encoding="utf-8",
|
|
18
|
+
)
|
|
19
|
+
app = tmp_path / "app.py"
|
|
20
|
+
app.write_text(
|
|
21
|
+
"""
|
|
22
|
+
import openai
|
|
23
|
+
from langchain_openai import ChatOpenAI
|
|
24
|
+
|
|
25
|
+
model = ChatOpenAI(model="gpt-4o")
|
|
26
|
+
client = openai.OpenAI()
|
|
27
|
+
|
|
28
|
+
class UserSchema:
|
|
29
|
+
ssn = "social_security"
|
|
30
|
+
email_address = "test@example.com"
|
|
31
|
+
""",
|
|
32
|
+
encoding="utf-8",
|
|
33
|
+
)
|
|
34
|
+
return tmp_path
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_detect_workload_finds_openai_and_langchain(sample_project: Path):
|
|
38
|
+
profile = detect_workload(str(sample_project))
|
|
39
|
+
assert "openai" in profile["providers"]
|
|
40
|
+
assert "langchain" in profile["frameworks"]
|
|
41
|
+
assert any("gpt" in m for m in profile["models"])
|
|
42
|
+
assert "pii" in profile["data_categories"]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_detect_workload_offline_no_network(sample_project: Path):
|
|
46
|
+
def fail_socket(*args, **kwargs):
|
|
47
|
+
raise AssertionError("Network call attempted during offline scan")
|
|
48
|
+
|
|
49
|
+
with patch("socket.socket", side_effect=fail_socket):
|
|
50
|
+
profile = detect_workload(str(sample_project))
|
|
51
|
+
assert profile["source"] == "sdk_scan"
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Tests for IRIS telemetry opt-out, internal/CI exclusion, and daily usage."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from unittest.mock import patch
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
from iris import _telemetry
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.fixture(autouse=True)
|
|
16
|
+
def _reset_env(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
17
|
+
monkeypatch.delenv("IRIS_TELEMETRY_OPT_OUT", raising=False)
|
|
18
|
+
monkeypatch.delenv("IRIS_TELEMETRY_INTERNAL", raising=False)
|
|
19
|
+
monkeypatch.delenv("CI", raising=False)
|
|
20
|
+
monkeypatch.delenv("GITHUB_ACTIONS", raising=False)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@pytest.fixture
|
|
24
|
+
def iris_home(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
|
|
25
|
+
iris_dir = tmp_path / ".iris"
|
|
26
|
+
iris_dir.mkdir()
|
|
27
|
+
monkeypatch.setattr(_telemetry, "_IRIS_DIR", iris_dir)
|
|
28
|
+
monkeypatch.setattr(_telemetry, "_INSTALL_ID_FILE", iris_dir / "install_id")
|
|
29
|
+
monkeypatch.setattr(_telemetry, "_FIRST_RUN_SENTINEL", iris_dir / ".telemetry_sent")
|
|
30
|
+
monkeypatch.setattr(_telemetry, "_FIRST_POLICY_SENTINEL", iris_dir / ".first_policy_sent")
|
|
31
|
+
monkeypatch.setattr(_telemetry, "_RUN_STATS_FILE", iris_dir / "run_stats.json")
|
|
32
|
+
return iris_dir
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_telemetry_enabled_by_default() -> None:
|
|
36
|
+
with patch.object(_telemetry, "TELEMETRY_ENABLED", True):
|
|
37
|
+
assert _telemetry.telemetry_enabled() is True
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_telemetry_opt_out() -> None:
|
|
41
|
+
with patch.dict(os.environ, {"IRIS_TELEMETRY_OPT_OUT": "1"}):
|
|
42
|
+
assert _telemetry.telemetry_enabled() is False
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_telemetry_skips_ci() -> None:
|
|
46
|
+
with patch.object(_telemetry, "TELEMETRY_ENABLED", True):
|
|
47
|
+
with patch.dict(os.environ, {"GITHUB_ACTIONS": "true"}):
|
|
48
|
+
assert _telemetry.telemetry_enabled() is False
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_telemetry_skips_internal_flag() -> None:
|
|
52
|
+
with patch.object(_telemetry, "TELEMETRY_ENABLED", True):
|
|
53
|
+
with patch.dict(os.environ, {"IRIS_TELEMETRY_INTERNAL": "1"}):
|
|
54
|
+
assert _telemetry.telemetry_enabled() is False
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_telemetry_skips_iris_sdk_path() -> None:
|
|
58
|
+
with patch.object(_telemetry, "TELEMETRY_ENABLED", True):
|
|
59
|
+
with patch.object(_telemetry.shutil, "which", return_value="/Users/dev/iris-sdk/.venv/bin/iris"):
|
|
60
|
+
assert _telemetry.telemetry_enabled() is False
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_maybe_record_cli_usage_increments_local_stats(iris_home: Path) -> None:
|
|
64
|
+
with patch.object(_telemetry, "TELEMETRY_ENABLED", True):
|
|
65
|
+
_telemetry.maybe_record_cli_usage("scan")
|
|
66
|
+
_telemetry.maybe_record_cli_usage("scan")
|
|
67
|
+
_telemetry.maybe_record_cli_usage("compile")
|
|
68
|
+
|
|
69
|
+
stats = json.loads((iris_home / "run_stats.json").read_text())
|
|
70
|
+
assert stats["run_count"] == 3
|
|
71
|
+
assert stats["commands"] == {"scan": 2, "compile": 1}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_maybe_record_cli_usage_skips_ping(iris_home: Path) -> None:
|
|
75
|
+
with patch.object(_telemetry, "TELEMETRY_ENABLED", True):
|
|
76
|
+
_telemetry.maybe_record_cli_usage("ping")
|
|
77
|
+
assert not (iris_home / "run_stats.json").exists()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_day_rollover_flushes_previous_day(iris_home: Path) -> None:
|
|
81
|
+
sent: list[dict[str, str]] = []
|
|
82
|
+
|
|
83
|
+
def capture(payload: dict[str, str]) -> None:
|
|
84
|
+
sent.append(payload)
|
|
85
|
+
|
|
86
|
+
(iris_home / "run_stats.json").write_text(
|
|
87
|
+
json.dumps({"date": "2026-06-14", "run_count": 5, "commands": {"scan": 5}}),
|
|
88
|
+
encoding="utf-8",
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
with patch.object(_telemetry, "TELEMETRY_ENABLED", True):
|
|
92
|
+
with patch.object(_telemetry, "_utc_today", return_value="2026-06-15"):
|
|
93
|
+
with patch.object(_telemetry, "_send_payload", side_effect=capture):
|
|
94
|
+
_telemetry.maybe_record_cli_usage("scan")
|
|
95
|
+
|
|
96
|
+
assert len(sent) == 1
|
|
97
|
+
assert sent[0]["event"] == "daily_usage"
|
|
98
|
+
assert sent[0]["usage_date"] == "2026-06-14"
|
|
99
|
+
assert sent[0]["run_count"] == "5"
|
|
100
|
+
assert json.loads(sent[0]["commands"]) == {"scan": 5}
|
|
101
|
+
|
|
102
|
+
stats = json.loads((iris_home / "run_stats.json").read_text())
|
|
103
|
+
assert stats["date"] == "2026-06-15"
|
|
104
|
+
assert stats["run_count"] == 1
|
|
105
|
+
assert stats["commands"] == {"scan": 1}
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
"""Tests for IRIS telemetry opt-out and internal/CI exclusion."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import os
|
|
6
|
-
from unittest.mock import patch
|
|
7
|
-
|
|
8
|
-
import pytest
|
|
9
|
-
|
|
10
|
-
from iris import _telemetry
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
@pytest.fixture(autouse=True)
|
|
14
|
-
def _reset_env(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
15
|
-
monkeypatch.delenv("IRIS_TELEMETRY_OPT_OUT", raising=False)
|
|
16
|
-
monkeypatch.delenv("IRIS_TELEMETRY_INTERNAL", raising=False)
|
|
17
|
-
monkeypatch.delenv("CI", raising=False)
|
|
18
|
-
monkeypatch.delenv("GITHUB_ACTIONS", raising=False)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def test_telemetry_enabled_by_default() -> None:
|
|
22
|
-
with patch.object(_telemetry, "TELEMETRY_ENABLED", True):
|
|
23
|
-
assert _telemetry.telemetry_enabled() is True
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def test_telemetry_opt_out() -> None:
|
|
27
|
-
with patch.dict(os.environ, {"IRIS_TELEMETRY_OPT_OUT": "1"}):
|
|
28
|
-
assert _telemetry.telemetry_enabled() is False
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def test_telemetry_skips_ci() -> None:
|
|
32
|
-
with patch.object(_telemetry, "TELEMETRY_ENABLED", True):
|
|
33
|
-
with patch.dict(os.environ, {"GITHUB_ACTIONS": "true"}):
|
|
34
|
-
assert _telemetry.telemetry_enabled() is False
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def test_telemetry_skips_internal_flag() -> None:
|
|
38
|
-
with patch.object(_telemetry, "TELEMETRY_ENABLED", True):
|
|
39
|
-
with patch.dict(os.environ, {"IRIS_TELEMETRY_INTERNAL": "1"}):
|
|
40
|
-
assert _telemetry.telemetry_enabled() is False
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def test_telemetry_skips_iris_sdk_path() -> None:
|
|
44
|
-
with patch.object(_telemetry, "TELEMETRY_ENABLED", True):
|
|
45
|
-
with patch.object(_telemetry.shutil, "which", return_value="/Users/dev/iris-sdk/.venv/bin/iris"):
|
|
46
|
-
assert _telemetry.telemetry_enabled() is False
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{iris_security_sdk-0.2.12 → iris_security_sdk-0.2.16}/iris_security_sdk.egg-info/requires.txt
RENAMED
|
File without changes
|
|
File without changes
|