network-ai 3.3.0 → 3.3.2
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.
- package/LICENSE +21 -21
- package/QUICKSTART.md +260 -260
- package/README.md +783 -783
- package/SKILL.md +578 -578
- package/dist/lib/locked-blackboard.js +48 -48
- package/dist/lib/swarm-utils.js +24 -24
- package/package.json +85 -85
- package/scripts/blackboard.py +852 -852
- package/scripts/check_permission.py +699 -699
- package/scripts/revoke_token.py +243 -243
- package/scripts/swarm_guard.py +1136 -1136
- package/scripts/validate_token.py +97 -97
- package/types/agent-adapter.d.ts +244 -244
- package/types/openclaw-core.d.ts +52 -52
|
@@ -1,699 +1,699 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
AuthGuardian Permission Checker
|
|
4
|
-
|
|
5
|
-
Evaluates permission requests for accessing sensitive resources
|
|
6
|
-
(DATABASE, PAYMENTS, EMAIL, FILE_EXPORT).
|
|
7
|
-
|
|
8
|
-
Usage:
|
|
9
|
-
python check_permission.py --agent AGENT_ID --resource RESOURCE_TYPE \
|
|
10
|
-
--justification "REASON" [--scope SCOPE]
|
|
11
|
-
python check_permission.py --active-grants [--agent AGENT_ID] [--json]
|
|
12
|
-
python check_permission.py --audit-summary [--last N] [--json]
|
|
13
|
-
|
|
14
|
-
Examples:
|
|
15
|
-
python check_permission.py --agent data_analyst --resource DATABASE \
|
|
16
|
-
--justification "Need customer order history for sales report" \
|
|
17
|
-
--scope "read:orders"
|
|
18
|
-
|
|
19
|
-
python check_permission.py --active-grants
|
|
20
|
-
python check_permission.py --active-grants --agent data_analyst --json
|
|
21
|
-
python check_permission.py --audit-summary --last 20 --json
|
|
22
|
-
"""
|
|
23
|
-
|
|
24
|
-
import argparse
|
|
25
|
-
import json
|
|
26
|
-
import re
|
|
27
|
-
import sys
|
|
28
|
-
import uuid
|
|
29
|
-
from datetime import datetime, timedelta, timezone
|
|
30
|
-
from pathlib import Path
|
|
31
|
-
from typing import Any, Optional
|
|
32
|
-
|
|
33
|
-
# Configuration
|
|
34
|
-
GRANT_TOKEN_TTL_MINUTES = 5
|
|
35
|
-
GRANTS_FILE = Path(__file__).parent.parent / "data" / "active_grants.json"
|
|
36
|
-
AUDIT_LOG = Path(__file__).parent.parent / "data" / "audit_log.jsonl"
|
|
37
|
-
|
|
38
|
-
# Default trust levels for known agents
|
|
39
|
-
DEFAULT_TRUST_LEVELS = {
|
|
40
|
-
"orchestrator": 0.9,
|
|
41
|
-
"data_analyst": 0.8,
|
|
42
|
-
"strategy_advisor": 0.7,
|
|
43
|
-
"risk_assessor": 0.85,
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
# Base risk scores for resource types
|
|
47
|
-
BASE_RISKS = {
|
|
48
|
-
"DATABASE": 0.5, # Internal database access
|
|
49
|
-
"PAYMENTS": 0.7, # Payment/financial systems
|
|
50
|
-
"EMAIL": 0.4, # Email sending capability
|
|
51
|
-
"FILE_EXPORT": 0.6, # Exporting data to files
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
# Default restrictions by resource type
|
|
55
|
-
RESTRICTIONS = {
|
|
56
|
-
"DATABASE": ["read_only", "max_records:100"],
|
|
57
|
-
"PAYMENTS": ["read_only", "no_pii_fields", "audit_required"],
|
|
58
|
-
"EMAIL": ["rate_limit:10_per_minute"],
|
|
59
|
-
"FILE_EXPORT": ["anonymize_pii", "local_only"],
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def ensure_data_dir():
|
|
64
|
-
"""Ensure data directory exists."""
|
|
65
|
-
data_dir = Path(__file__).parent.parent / "data"
|
|
66
|
-
data_dir.mkdir(exist_ok=True)
|
|
67
|
-
return data_dir
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
def detect_injection(justification: str) -> bool:
|
|
71
|
-
"""
|
|
72
|
-
Detect prompt-injection and manipulation patterns in justifications.
|
|
73
|
-
|
|
74
|
-
Returns True if the justification looks like a prompt-injection attempt.
|
|
75
|
-
"""
|
|
76
|
-
injection_patterns = [
|
|
77
|
-
r'ignore\s+(previous|above|prior|all)',
|
|
78
|
-
r'override\s+(policy|restriction|rule|permission|security)',
|
|
79
|
-
r'system\s*prompt',
|
|
80
|
-
r'you\s+are\s+(now|a)',
|
|
81
|
-
r'act\s+as\s+(if|a|an)',
|
|
82
|
-
r'pretend\s+(to|that|you)',
|
|
83
|
-
r'bypass\s+(security|check|restriction|auth)',
|
|
84
|
-
r'grant\s+(me|access|permission)\s+(anyway|regardless|now)',
|
|
85
|
-
r'disregard\s+(policy|rule|restriction|previous)',
|
|
86
|
-
r'admin\s+(mode|access|override)',
|
|
87
|
-
r'sudo\b',
|
|
88
|
-
r'jailbreak',
|
|
89
|
-
r'do\s+not\s+(check|verify|validate|restrict)',
|
|
90
|
-
r'skip\s+(validation|verification|check)',
|
|
91
|
-
r'trust\s+level\s*[:=]',
|
|
92
|
-
r'score\s*[:=]+\s*[\d.]',
|
|
93
|
-
]
|
|
94
|
-
text = justification.lower()
|
|
95
|
-
for pattern in injection_patterns:
|
|
96
|
-
if re.search(pattern, text):
|
|
97
|
-
return True
|
|
98
|
-
return False
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
def score_justification(justification: str) -> float:
|
|
102
|
-
"""
|
|
103
|
-
Score the quality of a justification with hardened validation.
|
|
104
|
-
|
|
105
|
-
Defenses against prompt injection and keyword stuffing:
|
|
106
|
-
- Injection pattern detection (immediate reject)
|
|
107
|
-
- Maximum length cap (prevents obfuscation in long text)
|
|
108
|
-
- Keyword-stuffing detection (penalises unnatural keyword density)
|
|
109
|
-
- Unique-word ratio check (catches copy-paste padding)
|
|
110
|
-
- Structural coherence (requires natural sentence structure)
|
|
111
|
-
|
|
112
|
-
Criteria (after safety checks):
|
|
113
|
-
- Length (more detail = better, but capped)
|
|
114
|
-
- Contains task-related keywords (capped contribution)
|
|
115
|
-
- Contains specificity keywords (capped contribution)
|
|
116
|
-
- No test/debug keywords
|
|
117
|
-
- Structural coherence bonus
|
|
118
|
-
"""
|
|
119
|
-
# ----- Hard reject: injection patterns -----
|
|
120
|
-
if detect_injection(justification):
|
|
121
|
-
return 0.0
|
|
122
|
-
|
|
123
|
-
# ----- Hard reject: empty or whitespace-only -----
|
|
124
|
-
stripped = justification.strip()
|
|
125
|
-
if not stripped:
|
|
126
|
-
return 0.0
|
|
127
|
-
|
|
128
|
-
# ----- Hard cap: excessively long justifications are suspicious -----
|
|
129
|
-
MAX_JUSTIFICATION_LENGTH = 500
|
|
130
|
-
if len(stripped) > MAX_JUSTIFICATION_LENGTH:
|
|
131
|
-
return 0.1 # Suspiciously long — allow re-submission with concise text
|
|
132
|
-
|
|
133
|
-
words = stripped.split()
|
|
134
|
-
word_count = len(words)
|
|
135
|
-
|
|
136
|
-
# ----- Hard reject: too few words to be meaningful -----
|
|
137
|
-
if word_count < 3:
|
|
138
|
-
return 0.1
|
|
139
|
-
|
|
140
|
-
# ----- Repetition / padding detection -----
|
|
141
|
-
unique_words = set(w.lower() for w in words)
|
|
142
|
-
unique_ratio = len(unique_words) / word_count # word_count >= 3 guaranteed above
|
|
143
|
-
if unique_ratio < 0.4:
|
|
144
|
-
return 0.1 # More than 60% repeated words — likely padding
|
|
145
|
-
|
|
146
|
-
# ----- Keyword-stuffing detection -----
|
|
147
|
-
task_keywords = re.findall(
|
|
148
|
-
r'\b(task|purpose|need|require|generate|analyze|create|process)\b',
|
|
149
|
-
stripped, re.IGNORECASE,
|
|
150
|
-
)
|
|
151
|
-
specificity_keywords = re.findall(
|
|
152
|
-
r'\b(specific|particular|exact|quarterly|annual|report|summary)\b',
|
|
153
|
-
stripped, re.IGNORECASE,
|
|
154
|
-
)
|
|
155
|
-
total_matched = len(task_keywords) + len(specificity_keywords)
|
|
156
|
-
keyword_density = total_matched / word_count # word_count >= 3 guaranteed above
|
|
157
|
-
if keyword_density > 0.5:
|
|
158
|
-
return 0.1 # More than half the words are scoring keywords — stuffing
|
|
159
|
-
|
|
160
|
-
# ----- Scoring (defensive caps per category) -----
|
|
161
|
-
score = 0.0
|
|
162
|
-
|
|
163
|
-
# Length contribution (max 0.25)
|
|
164
|
-
if len(stripped) > 20:
|
|
165
|
-
score += 0.15
|
|
166
|
-
if len(stripped) > 50:
|
|
167
|
-
score += 0.10
|
|
168
|
-
|
|
169
|
-
# Task keyword presence (max 0.20, but only first match counts)
|
|
170
|
-
if task_keywords:
|
|
171
|
-
score += 0.20
|
|
172
|
-
|
|
173
|
-
# Specificity keyword presence (max 0.20, but only first match counts)
|
|
174
|
-
if specificity_keywords:
|
|
175
|
-
score += 0.20
|
|
176
|
-
|
|
177
|
-
# No test/debug markers (max 0.15)
|
|
178
|
-
if not re.search(r'\b(test|debug|try|experiment)\b', stripped, re.IGNORECASE):
|
|
179
|
-
score += 0.15
|
|
180
|
-
|
|
181
|
-
# Structural coherence: sentence-like structure (max 0.20)
|
|
182
|
-
# Must contain at least one verb-like pattern and read like prose
|
|
183
|
-
has_verb = bool(re.search(
|
|
184
|
-
r'\b(is|are|was|were|need|needs|require|requires|must|should|will|'
|
|
185
|
-
r'generate|generating|analyze|analyzing|create|creating|process|processing|'
|
|
186
|
-
r'prepare|preparing|compile|compiling|review|reviewing|access|accessing|'
|
|
187
|
-
r'retrieve|retrieving|export|exporting|send|sending|run|running)\b',
|
|
188
|
-
stripped, re.IGNORECASE,
|
|
189
|
-
))
|
|
190
|
-
has_noun_object = bool(re.search(
|
|
191
|
-
r'\b(data|report|records|invoices?|orders?|customers?|accounts?|'
|
|
192
|
-
r'transactions?|files?|emails?|results?|metrics?|statistics?|'
|
|
193
|
-
r'analysis|documents?|exports?|payments?|entries|logs?|summaries)\b',
|
|
194
|
-
stripped, re.IGNORECASE,
|
|
195
|
-
))
|
|
196
|
-
if has_verb and has_noun_object:
|
|
197
|
-
score += 0.20
|
|
198
|
-
|
|
199
|
-
return min(score, 1.0)
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
def assess_risk(resource_type: str, scope: Optional[str] = None) -> float:
|
|
203
|
-
"""
|
|
204
|
-
Assess the risk level of a permission request.
|
|
205
|
-
|
|
206
|
-
Factors:
|
|
207
|
-
- Base risk of resource type
|
|
208
|
-
- Scope breadth (broad scopes = higher risk)
|
|
209
|
-
- Write operations (higher risk)
|
|
210
|
-
"""
|
|
211
|
-
risk = BASE_RISKS.get(resource_type, 0.5)
|
|
212
|
-
|
|
213
|
-
# Broad scopes increase risk
|
|
214
|
-
if not scope or scope in ("*", "all"):
|
|
215
|
-
risk += 0.2
|
|
216
|
-
|
|
217
|
-
# Write operations increase risk
|
|
218
|
-
if scope and re.search(r'\b(write|delete|update|modify|create)\b', scope, re.IGNORECASE):
|
|
219
|
-
risk += 0.2
|
|
220
|
-
|
|
221
|
-
return min(risk, 1.0)
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
def generate_grant_token() -> str:
|
|
225
|
-
"""Generate a unique grant token."""
|
|
226
|
-
return f"grant_{uuid.uuid4().hex}"
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
def log_audit(action: str, details: dict[str, Any]) -> None:
|
|
230
|
-
"""Append entry to audit log."""
|
|
231
|
-
ensure_data_dir()
|
|
232
|
-
entry: dict[str, Any] = {
|
|
233
|
-
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
234
|
-
"action": action,
|
|
235
|
-
"details": details
|
|
236
|
-
}
|
|
237
|
-
with open(AUDIT_LOG, "a") as f:
|
|
238
|
-
f.write(json.dumps(entry) + "\n")
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
def save_grant(grant: dict[str, Any]) -> None:
|
|
242
|
-
"""Save grant to persistent storage."""
|
|
243
|
-
ensure_data_dir()
|
|
244
|
-
grants = {}
|
|
245
|
-
if GRANTS_FILE.exists():
|
|
246
|
-
try:
|
|
247
|
-
grants = json.loads(GRANTS_FILE.read_text())
|
|
248
|
-
except json.JSONDecodeError:
|
|
249
|
-
grants = {}
|
|
250
|
-
|
|
251
|
-
grants[grant["token"]] = grant
|
|
252
|
-
GRANTS_FILE.write_text(json.dumps(grants, indent=2))
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
def evaluate_permission(agent_id: str, resource_type: str,
|
|
256
|
-
justification: str, scope: Optional[str] = None) -> dict[str, Any]:
|
|
257
|
-
"""
|
|
258
|
-
Evaluate a permission request using weighted scoring.
|
|
259
|
-
|
|
260
|
-
Weights:
|
|
261
|
-
- Justification Quality: 40%
|
|
262
|
-
- Agent Trust Level: 30%
|
|
263
|
-
- Risk Assessment: 30%
|
|
264
|
-
"""
|
|
265
|
-
# Log the request
|
|
266
|
-
log_audit("permission_request", {
|
|
267
|
-
"agent_id": agent_id,
|
|
268
|
-
"resource_type": resource_type,
|
|
269
|
-
"justification": justification,
|
|
270
|
-
"scope": scope
|
|
271
|
-
})
|
|
272
|
-
|
|
273
|
-
# 1. Justification Quality (40% weight)
|
|
274
|
-
justification_score = score_justification(justification)
|
|
275
|
-
if justification_score < 0.3:
|
|
276
|
-
return {
|
|
277
|
-
"granted": False,
|
|
278
|
-
"reason": "Justification is insufficient. Please provide specific task context.",
|
|
279
|
-
"scores": {
|
|
280
|
-
"justification": justification_score,
|
|
281
|
-
"trust": None,
|
|
282
|
-
"risk": None
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
# 2. Agent Trust Level (30% weight)
|
|
287
|
-
trust_level = DEFAULT_TRUST_LEVELS.get(agent_id, 0.5)
|
|
288
|
-
if trust_level < 0.4:
|
|
289
|
-
return {
|
|
290
|
-
"granted": False,
|
|
291
|
-
"reason": "Agent trust level is below threshold. Escalate to human operator.",
|
|
292
|
-
"scores": {
|
|
293
|
-
"justification": justification_score,
|
|
294
|
-
"trust": trust_level,
|
|
295
|
-
"risk": None
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
# 3. Risk Assessment (30% weight)
|
|
300
|
-
risk_score = assess_risk(resource_type, scope)
|
|
301
|
-
if risk_score > 0.8:
|
|
302
|
-
return {
|
|
303
|
-
"granted": False,
|
|
304
|
-
"reason": "Risk assessment exceeds acceptable threshold. Narrow the requested scope.",
|
|
305
|
-
"scores": {
|
|
306
|
-
"justification": justification_score,
|
|
307
|
-
"trust": trust_level,
|
|
308
|
-
"risk": risk_score
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
# Calculate weighted approval score
|
|
313
|
-
weighted_score = (
|
|
314
|
-
justification_score * 0.4 +
|
|
315
|
-
trust_level * 0.3 +
|
|
316
|
-
(1 - risk_score) * 0.3
|
|
317
|
-
)
|
|
318
|
-
|
|
319
|
-
if weighted_score < 0.5:
|
|
320
|
-
return {
|
|
321
|
-
"granted": False,
|
|
322
|
-
"reason": f"Combined evaluation score ({weighted_score:.2f}) below threshold (0.5).",
|
|
323
|
-
"scores": {
|
|
324
|
-
"justification": justification_score,
|
|
325
|
-
"trust": trust_level,
|
|
326
|
-
"risk": risk_score,
|
|
327
|
-
"weighted": weighted_score
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
# Generate grant
|
|
332
|
-
token = generate_grant_token()
|
|
333
|
-
expires_at = (datetime.now(timezone.utc) + timedelta(minutes=GRANT_TOKEN_TTL_MINUTES)).isoformat()
|
|
334
|
-
restrictions = RESTRICTIONS.get(resource_type, [])
|
|
335
|
-
|
|
336
|
-
grant: dict[str, Any] = {
|
|
337
|
-
"token": token,
|
|
338
|
-
"agent_id": agent_id,
|
|
339
|
-
"resource_type": resource_type,
|
|
340
|
-
"scope": scope,
|
|
341
|
-
"expires_at": expires_at,
|
|
342
|
-
"restrictions": restrictions,
|
|
343
|
-
"granted_at": datetime.now(timezone.utc).isoformat()
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
# Save grant and log
|
|
347
|
-
save_grant(grant)
|
|
348
|
-
log_audit("permission_granted", grant)
|
|
349
|
-
|
|
350
|
-
return {
|
|
351
|
-
"granted": True,
|
|
352
|
-
"token": token,
|
|
353
|
-
"expires_at": expires_at,
|
|
354
|
-
"restrictions": restrictions,
|
|
355
|
-
"scores": {
|
|
356
|
-
"justification": justification_score,
|
|
357
|
-
"trust": trust_level,
|
|
358
|
-
"risk": risk_score,
|
|
359
|
-
"weighted": weighted_score
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
def list_active_grants(agent_filter: Optional[str] = None, as_json: bool = False) -> int:
|
|
365
|
-
"""
|
|
366
|
-
Show which agents currently hold access to which APIs with expiry times.
|
|
367
|
-
|
|
368
|
-
Reads data/active_grants.json, filters out expired grants,
|
|
369
|
-
and displays remaining grants with TTL.
|
|
370
|
-
"""
|
|
371
|
-
if not GRANTS_FILE.exists():
|
|
372
|
-
if as_json:
|
|
373
|
-
print(json.dumps({"grants": [], "total": 0, "expired_cleaned": 0}))
|
|
374
|
-
else:
|
|
375
|
-
print("No active grants. (No grants file found.)")
|
|
376
|
-
return 0
|
|
377
|
-
|
|
378
|
-
try:
|
|
379
|
-
grants = json.loads(GRANTS_FILE.read_text())
|
|
380
|
-
except (json.JSONDecodeError, OSError):
|
|
381
|
-
if as_json:
|
|
382
|
-
print(json.dumps({"error": "Could not read grants file"}))
|
|
383
|
-
else:
|
|
384
|
-
print("Error: Could not read grants file.")
|
|
385
|
-
return 1
|
|
386
|
-
|
|
387
|
-
now = datetime.now(timezone.utc)
|
|
388
|
-
active: list[dict[str, Any]] = []
|
|
389
|
-
expired_count = 0
|
|
390
|
-
|
|
391
|
-
for token, grant in grants.items():
|
|
392
|
-
try:
|
|
393
|
-
expires_at = datetime.fromisoformat(grant["expires_at"])
|
|
394
|
-
except (KeyError, ValueError):
|
|
395
|
-
expired_count += 1
|
|
396
|
-
continue
|
|
397
|
-
|
|
398
|
-
if expires_at <= now:
|
|
399
|
-
expired_count += 1
|
|
400
|
-
continue
|
|
401
|
-
|
|
402
|
-
if agent_filter and grant.get("agent_id") != agent_filter:
|
|
403
|
-
continue
|
|
404
|
-
|
|
405
|
-
remaining = expires_at - now
|
|
406
|
-
minutes_left = remaining.total_seconds() / 60
|
|
407
|
-
|
|
408
|
-
active.append({
|
|
409
|
-
"token": token[:16] + "..." if len(token) > 16 else token,
|
|
410
|
-
"token_full": token,
|
|
411
|
-
"agent_id": grant.get("agent_id", "unknown"),
|
|
412
|
-
"resource_type": grant.get("resource_type", "unknown"),
|
|
413
|
-
"scope": grant.get("scope"),
|
|
414
|
-
"granted_at": grant.get("granted_at", "unknown"),
|
|
415
|
-
"expires_at": grant["expires_at"],
|
|
416
|
-
"minutes_remaining": round(minutes_left, 1),
|
|
417
|
-
"restrictions": grant.get("restrictions", []),
|
|
418
|
-
})
|
|
419
|
-
|
|
420
|
-
# Sort by expiry (soonest first)
|
|
421
|
-
active.sort(key=lambda g: g["expires_at"])
|
|
422
|
-
|
|
423
|
-
if as_json:
|
|
424
|
-
# In JSON mode, include full tokens
|
|
425
|
-
output: dict[str, Any] = {
|
|
426
|
-
"grants": active,
|
|
427
|
-
"total": len(active),
|
|
428
|
-
"expired_cleaned": expired_count,
|
|
429
|
-
}
|
|
430
|
-
print(json.dumps(output, indent=2))
|
|
431
|
-
else:
|
|
432
|
-
if not active:
|
|
433
|
-
filter_msg = f" for agent '{agent_filter}'" if agent_filter else ""
|
|
434
|
-
print(f"No active grants{filter_msg}. ({expired_count} expired.)")
|
|
435
|
-
else:
|
|
436
|
-
filter_msg = f" (agent: {agent_filter})" if agent_filter else ""
|
|
437
|
-
print(f"Active Grants{filter_msg}:")
|
|
438
|
-
print(f"{'='*70}")
|
|
439
|
-
for g in active:
|
|
440
|
-
print(f" Agent: {g['agent_id']}")
|
|
441
|
-
print(f" Resource: {g['resource_type']}")
|
|
442
|
-
if g["scope"]:
|
|
443
|
-
print(f" Scope: {g['scope']}")
|
|
444
|
-
print(f" Token: {g['token']}")
|
|
445
|
-
print(f" Granted: {g['granted_at']}")
|
|
446
|
-
print(f" Expires: {g['expires_at']}")
|
|
447
|
-
print(f" Remaining: {g['minutes_remaining']} min")
|
|
448
|
-
if g["restrictions"]:
|
|
449
|
-
print(f" Restrictions: {', '.join(g['restrictions'])}")
|
|
450
|
-
print(f" {'-'*66}")
|
|
451
|
-
print(f"\nTotal: {len(active)} active, {expired_count} expired")
|
|
452
|
-
|
|
453
|
-
return 0
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
def audit_summary(last_n: int = 20, as_json: bool = False) -> int:
|
|
457
|
-
"""
|
|
458
|
-
Summarize recent permission requests, grants, and denials.
|
|
459
|
-
|
|
460
|
-
Parses data/audit_log.jsonl and produces per-agent and per-resource
|
|
461
|
-
breakdowns plus recent activity.
|
|
462
|
-
"""
|
|
463
|
-
if not AUDIT_LOG.exists():
|
|
464
|
-
if as_json:
|
|
465
|
-
print(json.dumps({"entries": 0, "summary": {}, "recent": []}))
|
|
466
|
-
else:
|
|
467
|
-
print("No audit log found. (No permission requests recorded yet.)")
|
|
468
|
-
return 0
|
|
469
|
-
|
|
470
|
-
entries: list[dict[str, Any]] = []
|
|
471
|
-
try:
|
|
472
|
-
with open(AUDIT_LOG, "r") as f:
|
|
473
|
-
for line in f:
|
|
474
|
-
line = line.strip()
|
|
475
|
-
if line:
|
|
476
|
-
try:
|
|
477
|
-
entries.append(json.loads(line))
|
|
478
|
-
except json.JSONDecodeError:
|
|
479
|
-
continue
|
|
480
|
-
except OSError:
|
|
481
|
-
if as_json:
|
|
482
|
-
print(json.dumps({"error": "Could not read audit log"}))
|
|
483
|
-
else:
|
|
484
|
-
print("Error: Could not read audit log.")
|
|
485
|
-
return 1
|
|
486
|
-
|
|
487
|
-
if not entries:
|
|
488
|
-
if as_json:
|
|
489
|
-
print(json.dumps({"entries": 0, "summary": {}, "recent": []}))
|
|
490
|
-
else:
|
|
491
|
-
print("Audit log is empty.")
|
|
492
|
-
return 0
|
|
493
|
-
|
|
494
|
-
# Aggregate stats
|
|
495
|
-
total_requests = 0
|
|
496
|
-
total_grants = 0
|
|
497
|
-
by_agent: dict[str, dict[str, int]] = {}
|
|
498
|
-
by_resource: dict[str, dict[str, int]] = {}
|
|
499
|
-
|
|
500
|
-
for entry in entries:
|
|
501
|
-
action = entry.get("action", "")
|
|
502
|
-
details = entry.get("details", {})
|
|
503
|
-
agent_id = details.get("agent_id", "unknown")
|
|
504
|
-
resource_type = details.get("resource_type", "unknown")
|
|
505
|
-
|
|
506
|
-
if action == "permission_request":
|
|
507
|
-
total_requests += 1
|
|
508
|
-
by_agent.setdefault(agent_id, {"requests": 0, "grants": 0})
|
|
509
|
-
by_agent[agent_id]["requests"] += 1
|
|
510
|
-
by_resource.setdefault(resource_type, {"requests": 0, "grants": 0})
|
|
511
|
-
by_resource[resource_type]["requests"] += 1
|
|
512
|
-
elif action == "permission_granted":
|
|
513
|
-
total_grants += 1
|
|
514
|
-
by_agent.setdefault(agent_id, {"requests": 0, "grants": 0})
|
|
515
|
-
by_agent[agent_id]["grants"] += 1
|
|
516
|
-
by_resource.setdefault(resource_type, {"requests": 0, "grants": 0})
|
|
517
|
-
by_resource[resource_type]["grants"] += 1
|
|
518
|
-
|
|
519
|
-
total_denials = total_requests - total_grants
|
|
520
|
-
|
|
521
|
-
# Recent entries (last N)
|
|
522
|
-
recent = entries[-last_n:]
|
|
523
|
-
|
|
524
|
-
# Time range
|
|
525
|
-
first_ts = entries[0].get("timestamp", "unknown")
|
|
526
|
-
last_ts = entries[-1].get("timestamp", "unknown")
|
|
527
|
-
|
|
528
|
-
if as_json:
|
|
529
|
-
output: dict[str, Any] = {
|
|
530
|
-
"total_entries": len(entries),
|
|
531
|
-
"total_requests": total_requests,
|
|
532
|
-
"total_grants": total_grants,
|
|
533
|
-
"total_denials": total_denials,
|
|
534
|
-
"time_range": {"first": first_ts, "last": last_ts},
|
|
535
|
-
"by_agent": by_agent,
|
|
536
|
-
"by_resource": by_resource,
|
|
537
|
-
"recent": recent[-last_n:],
|
|
538
|
-
}
|
|
539
|
-
print(json.dumps(output, indent=2))
|
|
540
|
-
else:
|
|
541
|
-
print("Audit Summary")
|
|
542
|
-
print(f"{'='*70}")
|
|
543
|
-
print(f" Log entries: {len(entries)}")
|
|
544
|
-
print(f" Time range: {first_ts}")
|
|
545
|
-
print(f" {last_ts}")
|
|
546
|
-
print(f"")
|
|
547
|
-
print(f" Requests: {total_requests}")
|
|
548
|
-
print(f" Grants: {total_grants}")
|
|
549
|
-
print(f" Denials: {total_denials}")
|
|
550
|
-
grant_rate = (total_grants / total_requests * 100) if total_requests > 0 else 0
|
|
551
|
-
print(f" Grant Rate: {grant_rate:.0f}%")
|
|
552
|
-
|
|
553
|
-
if by_agent:
|
|
554
|
-
print(f"\n By Agent:")
|
|
555
|
-
print(f" {'-'*50}")
|
|
556
|
-
print(f" {'Agent':<20} {'Requests':>10} {'Grants':>10} {'Denials':>10}")
|
|
557
|
-
print(f" {'-'*50}")
|
|
558
|
-
for agent_id, stats in sorted(by_agent.items()):
|
|
559
|
-
denials = stats["requests"] - stats["grants"]
|
|
560
|
-
print(f" {agent_id:<20} {stats['requests']:>10} {stats['grants']:>10} {denials:>10}")
|
|
561
|
-
|
|
562
|
-
if by_resource:
|
|
563
|
-
print(f"\n By Resource:")
|
|
564
|
-
print(f" {'-'*50}")
|
|
565
|
-
print(f" {'Resource':<20} {'Requests':>10} {'Grants':>10} {'Denials':>10}")
|
|
566
|
-
print(f" {'-'*50}")
|
|
567
|
-
for resource_type, stats in sorted(by_resource.items()):
|
|
568
|
-
denials = stats["requests"] - stats["grants"]
|
|
569
|
-
print(f" {resource_type:<20} {stats['requests']:>10} {stats['grants']:>10} {denials:>10}")
|
|
570
|
-
|
|
571
|
-
print(f"\n Recent Activity (last {min(last_n, len(recent))}):")
|
|
572
|
-
print(f" {'-'*66}")
|
|
573
|
-
for entry in recent:
|
|
574
|
-
ts = entry.get("timestamp", "?")[:19]
|
|
575
|
-
action = entry.get("action", "?")
|
|
576
|
-
details = entry.get("details", {})
|
|
577
|
-
agent_id = details.get("agent_id", "?")
|
|
578
|
-
resource_type = details.get("resource_type", "?")
|
|
579
|
-
symbol = "GRANT" if action == "permission_granted" else "REQ" if action == "permission_request" else action.upper()
|
|
580
|
-
print(f" {ts} [{symbol:>5}] {agent_id} -> {resource_type}")
|
|
581
|
-
|
|
582
|
-
return 0
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
def main():
|
|
586
|
-
parser = argparse.ArgumentParser(
|
|
587
|
-
description="AuthGuardian Permission Checker",
|
|
588
|
-
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
589
|
-
epilog="""
|
|
590
|
-
Examples:
|
|
591
|
-
Check permission:
|
|
592
|
-
%(prog)s --agent data_analyst --resource DATABASE \\
|
|
593
|
-
--justification "Need Q4 invoice data for quarterly report"
|
|
594
|
-
|
|
595
|
-
List active grants:
|
|
596
|
-
%(prog)s --active-grants
|
|
597
|
-
%(prog)s --active-grants --agent data_analyst --json
|
|
598
|
-
|
|
599
|
-
View audit summary:
|
|
600
|
-
%(prog)s --audit-summary
|
|
601
|
-
%(prog)s --audit-summary --last 50 --json
|
|
602
|
-
"""
|
|
603
|
-
)
|
|
604
|
-
|
|
605
|
-
# Action flags
|
|
606
|
-
parser.add_argument(
|
|
607
|
-
"--active-grants",
|
|
608
|
-
action="store_true",
|
|
609
|
-
help="List all active (non-expired) permission grants"
|
|
610
|
-
)
|
|
611
|
-
parser.add_argument(
|
|
612
|
-
"--audit-summary",
|
|
613
|
-
action="store_true",
|
|
614
|
-
help="Show audit log summary with per-agent and per-resource breakdowns"
|
|
615
|
-
)
|
|
616
|
-
parser.add_argument(
|
|
617
|
-
"--last",
|
|
618
|
-
type=int,
|
|
619
|
-
default=20,
|
|
620
|
-
help="Number of recent audit entries to show (default: 20)"
|
|
621
|
-
)
|
|
622
|
-
|
|
623
|
-
# Permission check args (required only for check mode)
|
|
624
|
-
parser.add_argument(
|
|
625
|
-
"--agent", "-a",
|
|
626
|
-
help="Agent ID requesting permission (required for check; optional filter for --active-grants)"
|
|
627
|
-
)
|
|
628
|
-
parser.add_argument(
|
|
629
|
-
"--resource", "-r",
|
|
630
|
-
choices=["DATABASE", "PAYMENTS", "EMAIL", "FILE_EXPORT"],
|
|
631
|
-
help="Resource type to access"
|
|
632
|
-
)
|
|
633
|
-
parser.add_argument(
|
|
634
|
-
"--justification", "-j",
|
|
635
|
-
help="Business justification for the request"
|
|
636
|
-
)
|
|
637
|
-
parser.add_argument(
|
|
638
|
-
"--scope", "-s",
|
|
639
|
-
help="Specific scope of access (e.g., 'read:invoices')"
|
|
640
|
-
)
|
|
641
|
-
parser.add_argument(
|
|
642
|
-
"--json",
|
|
643
|
-
action="store_true",
|
|
644
|
-
help="Output result as JSON"
|
|
645
|
-
)
|
|
646
|
-
|
|
647
|
-
args = parser.parse_args()
|
|
648
|
-
|
|
649
|
-
# --- Action: --active-grants ---
|
|
650
|
-
if args.active_grants:
|
|
651
|
-
sys.exit(list_active_grants(agent_filter=args.agent, as_json=args.json))
|
|
652
|
-
|
|
653
|
-
# --- Action: --audit-summary ---
|
|
654
|
-
if args.audit_summary:
|
|
655
|
-
sys.exit(audit_summary(last_n=args.last, as_json=args.json))
|
|
656
|
-
|
|
657
|
-
# --- Default action: permission check ---
|
|
658
|
-
if not args.agent:
|
|
659
|
-
parser.error("--agent is required for permission checks")
|
|
660
|
-
if not args.resource:
|
|
661
|
-
parser.error("--resource is required for permission checks")
|
|
662
|
-
if not args.justification:
|
|
663
|
-
parser.error("--justification is required for permission checks")
|
|
664
|
-
|
|
665
|
-
result = evaluate_permission(
|
|
666
|
-
agent_id=args.agent,
|
|
667
|
-
resource_type=args.resource,
|
|
668
|
-
justification=args.justification,
|
|
669
|
-
scope=args.scope
|
|
670
|
-
)
|
|
671
|
-
|
|
672
|
-
if args.json:
|
|
673
|
-
print(json.dumps(result, indent=2))
|
|
674
|
-
else:
|
|
675
|
-
if result["granted"]:
|
|
676
|
-
print("GRANTED")
|
|
677
|
-
print(f"Token: {result['token']}")
|
|
678
|
-
print(f"Expires: {result['expires_at']}")
|
|
679
|
-
print(f"Restrictions: {', '.join(result['restrictions'])}")
|
|
680
|
-
else:
|
|
681
|
-
print("DENIED")
|
|
682
|
-
print(f"Reason: {result['reason']}")
|
|
683
|
-
|
|
684
|
-
print("\nEvaluation Scores:")
|
|
685
|
-
scores = result["scores"]
|
|
686
|
-
if scores.get("justification") is not None:
|
|
687
|
-
print(f" Justification: {scores['justification']:.2f}")
|
|
688
|
-
if scores.get("trust") is not None:
|
|
689
|
-
print(f" Trust Level: {scores['trust']:.2f}")
|
|
690
|
-
if scores.get("risk") is not None:
|
|
691
|
-
print(f" Risk Score: {scores['risk']:.2f}")
|
|
692
|
-
if scores.get("weighted") is not None:
|
|
693
|
-
print(f" Weighted: {scores['weighted']:.2f}")
|
|
694
|
-
|
|
695
|
-
sys.exit(0 if result["granted"] else 1)
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
if __name__ == "__main__":
|
|
699
|
-
main()
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
AuthGuardian Permission Checker
|
|
4
|
+
|
|
5
|
+
Evaluates permission requests for accessing sensitive resources
|
|
6
|
+
(DATABASE, PAYMENTS, EMAIL, FILE_EXPORT).
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
python check_permission.py --agent AGENT_ID --resource RESOURCE_TYPE \
|
|
10
|
+
--justification "REASON" [--scope SCOPE]
|
|
11
|
+
python check_permission.py --active-grants [--agent AGENT_ID] [--json]
|
|
12
|
+
python check_permission.py --audit-summary [--last N] [--json]
|
|
13
|
+
|
|
14
|
+
Examples:
|
|
15
|
+
python check_permission.py --agent data_analyst --resource DATABASE \
|
|
16
|
+
--justification "Need customer order history for sales report" \
|
|
17
|
+
--scope "read:orders"
|
|
18
|
+
|
|
19
|
+
python check_permission.py --active-grants
|
|
20
|
+
python check_permission.py --active-grants --agent data_analyst --json
|
|
21
|
+
python check_permission.py --audit-summary --last 20 --json
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import argparse
|
|
25
|
+
import json
|
|
26
|
+
import re
|
|
27
|
+
import sys
|
|
28
|
+
import uuid
|
|
29
|
+
from datetime import datetime, timedelta, timezone
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
from typing import Any, Optional
|
|
32
|
+
|
|
33
|
+
# Configuration
|
|
34
|
+
GRANT_TOKEN_TTL_MINUTES = 5
|
|
35
|
+
GRANTS_FILE = Path(__file__).parent.parent / "data" / "active_grants.json"
|
|
36
|
+
AUDIT_LOG = Path(__file__).parent.parent / "data" / "audit_log.jsonl"
|
|
37
|
+
|
|
38
|
+
# Default trust levels for known agents
|
|
39
|
+
DEFAULT_TRUST_LEVELS = {
|
|
40
|
+
"orchestrator": 0.9,
|
|
41
|
+
"data_analyst": 0.8,
|
|
42
|
+
"strategy_advisor": 0.7,
|
|
43
|
+
"risk_assessor": 0.85,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# Base risk scores for resource types
|
|
47
|
+
BASE_RISKS = {
|
|
48
|
+
"DATABASE": 0.5, # Internal database access
|
|
49
|
+
"PAYMENTS": 0.7, # Payment/financial systems
|
|
50
|
+
"EMAIL": 0.4, # Email sending capability
|
|
51
|
+
"FILE_EXPORT": 0.6, # Exporting data to files
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# Default restrictions by resource type
|
|
55
|
+
RESTRICTIONS = {
|
|
56
|
+
"DATABASE": ["read_only", "max_records:100"],
|
|
57
|
+
"PAYMENTS": ["read_only", "no_pii_fields", "audit_required"],
|
|
58
|
+
"EMAIL": ["rate_limit:10_per_minute"],
|
|
59
|
+
"FILE_EXPORT": ["anonymize_pii", "local_only"],
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def ensure_data_dir():
|
|
64
|
+
"""Ensure data directory exists."""
|
|
65
|
+
data_dir = Path(__file__).parent.parent / "data"
|
|
66
|
+
data_dir.mkdir(exist_ok=True)
|
|
67
|
+
return data_dir
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def detect_injection(justification: str) -> bool:
|
|
71
|
+
"""
|
|
72
|
+
Detect prompt-injection and manipulation patterns in justifications.
|
|
73
|
+
|
|
74
|
+
Returns True if the justification looks like a prompt-injection attempt.
|
|
75
|
+
"""
|
|
76
|
+
injection_patterns = [
|
|
77
|
+
r'ignore\s+(previous|above|prior|all)',
|
|
78
|
+
r'override\s+(policy|restriction|rule|permission|security)',
|
|
79
|
+
r'system\s*prompt',
|
|
80
|
+
r'you\s+are\s+(now|a)',
|
|
81
|
+
r'act\s+as\s+(if|a|an)',
|
|
82
|
+
r'pretend\s+(to|that|you)',
|
|
83
|
+
r'bypass\s+(security|check|restriction|auth)',
|
|
84
|
+
r'grant\s+(me|access|permission)\s+(anyway|regardless|now)',
|
|
85
|
+
r'disregard\s+(policy|rule|restriction|previous)',
|
|
86
|
+
r'admin\s+(mode|access|override)',
|
|
87
|
+
r'sudo\b',
|
|
88
|
+
r'jailbreak',
|
|
89
|
+
r'do\s+not\s+(check|verify|validate|restrict)',
|
|
90
|
+
r'skip\s+(validation|verification|check)',
|
|
91
|
+
r'trust\s+level\s*[:=]',
|
|
92
|
+
r'score\s*[:=]+\s*[\d.]',
|
|
93
|
+
]
|
|
94
|
+
text = justification.lower()
|
|
95
|
+
for pattern in injection_patterns:
|
|
96
|
+
if re.search(pattern, text):
|
|
97
|
+
return True
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def score_justification(justification: str) -> float:
|
|
102
|
+
"""
|
|
103
|
+
Score the quality of a justification with hardened validation.
|
|
104
|
+
|
|
105
|
+
Defenses against prompt injection and keyword stuffing:
|
|
106
|
+
- Injection pattern detection (immediate reject)
|
|
107
|
+
- Maximum length cap (prevents obfuscation in long text)
|
|
108
|
+
- Keyword-stuffing detection (penalises unnatural keyword density)
|
|
109
|
+
- Unique-word ratio check (catches copy-paste padding)
|
|
110
|
+
- Structural coherence (requires natural sentence structure)
|
|
111
|
+
|
|
112
|
+
Criteria (after safety checks):
|
|
113
|
+
- Length (more detail = better, but capped)
|
|
114
|
+
- Contains task-related keywords (capped contribution)
|
|
115
|
+
- Contains specificity keywords (capped contribution)
|
|
116
|
+
- No test/debug keywords
|
|
117
|
+
- Structural coherence bonus
|
|
118
|
+
"""
|
|
119
|
+
# ----- Hard reject: injection patterns -----
|
|
120
|
+
if detect_injection(justification):
|
|
121
|
+
return 0.0
|
|
122
|
+
|
|
123
|
+
# ----- Hard reject: empty or whitespace-only -----
|
|
124
|
+
stripped = justification.strip()
|
|
125
|
+
if not stripped:
|
|
126
|
+
return 0.0
|
|
127
|
+
|
|
128
|
+
# ----- Hard cap: excessively long justifications are suspicious -----
|
|
129
|
+
MAX_JUSTIFICATION_LENGTH = 500
|
|
130
|
+
if len(stripped) > MAX_JUSTIFICATION_LENGTH:
|
|
131
|
+
return 0.1 # Suspiciously long — allow re-submission with concise text
|
|
132
|
+
|
|
133
|
+
words = stripped.split()
|
|
134
|
+
word_count = len(words)
|
|
135
|
+
|
|
136
|
+
# ----- Hard reject: too few words to be meaningful -----
|
|
137
|
+
if word_count < 3:
|
|
138
|
+
return 0.1
|
|
139
|
+
|
|
140
|
+
# ----- Repetition / padding detection -----
|
|
141
|
+
unique_words = set(w.lower() for w in words)
|
|
142
|
+
unique_ratio = len(unique_words) / word_count # word_count >= 3 guaranteed above
|
|
143
|
+
if unique_ratio < 0.4:
|
|
144
|
+
return 0.1 # More than 60% repeated words — likely padding
|
|
145
|
+
|
|
146
|
+
# ----- Keyword-stuffing detection -----
|
|
147
|
+
task_keywords = re.findall(
|
|
148
|
+
r'\b(task|purpose|need|require|generate|analyze|create|process)\b',
|
|
149
|
+
stripped, re.IGNORECASE,
|
|
150
|
+
)
|
|
151
|
+
specificity_keywords = re.findall(
|
|
152
|
+
r'\b(specific|particular|exact|quarterly|annual|report|summary)\b',
|
|
153
|
+
stripped, re.IGNORECASE,
|
|
154
|
+
)
|
|
155
|
+
total_matched = len(task_keywords) + len(specificity_keywords)
|
|
156
|
+
keyword_density = total_matched / word_count # word_count >= 3 guaranteed above
|
|
157
|
+
if keyword_density > 0.5:
|
|
158
|
+
return 0.1 # More than half the words are scoring keywords — stuffing
|
|
159
|
+
|
|
160
|
+
# ----- Scoring (defensive caps per category) -----
|
|
161
|
+
score = 0.0
|
|
162
|
+
|
|
163
|
+
# Length contribution (max 0.25)
|
|
164
|
+
if len(stripped) > 20:
|
|
165
|
+
score += 0.15
|
|
166
|
+
if len(stripped) > 50:
|
|
167
|
+
score += 0.10
|
|
168
|
+
|
|
169
|
+
# Task keyword presence (max 0.20, but only first match counts)
|
|
170
|
+
if task_keywords:
|
|
171
|
+
score += 0.20
|
|
172
|
+
|
|
173
|
+
# Specificity keyword presence (max 0.20, but only first match counts)
|
|
174
|
+
if specificity_keywords:
|
|
175
|
+
score += 0.20
|
|
176
|
+
|
|
177
|
+
# No test/debug markers (max 0.15)
|
|
178
|
+
if not re.search(r'\b(test|debug|try|experiment)\b', stripped, re.IGNORECASE):
|
|
179
|
+
score += 0.15
|
|
180
|
+
|
|
181
|
+
# Structural coherence: sentence-like structure (max 0.20)
|
|
182
|
+
# Must contain at least one verb-like pattern and read like prose
|
|
183
|
+
has_verb = bool(re.search(
|
|
184
|
+
r'\b(is|are|was|were|need|needs|require|requires|must|should|will|'
|
|
185
|
+
r'generate|generating|analyze|analyzing|create|creating|process|processing|'
|
|
186
|
+
r'prepare|preparing|compile|compiling|review|reviewing|access|accessing|'
|
|
187
|
+
r'retrieve|retrieving|export|exporting|send|sending|run|running)\b',
|
|
188
|
+
stripped, re.IGNORECASE,
|
|
189
|
+
))
|
|
190
|
+
has_noun_object = bool(re.search(
|
|
191
|
+
r'\b(data|report|records|invoices?|orders?|customers?|accounts?|'
|
|
192
|
+
r'transactions?|files?|emails?|results?|metrics?|statistics?|'
|
|
193
|
+
r'analysis|documents?|exports?|payments?|entries|logs?|summaries)\b',
|
|
194
|
+
stripped, re.IGNORECASE,
|
|
195
|
+
))
|
|
196
|
+
if has_verb and has_noun_object:
|
|
197
|
+
score += 0.20
|
|
198
|
+
|
|
199
|
+
return min(score, 1.0)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def assess_risk(resource_type: str, scope: Optional[str] = None) -> float:
|
|
203
|
+
"""
|
|
204
|
+
Assess the risk level of a permission request.
|
|
205
|
+
|
|
206
|
+
Factors:
|
|
207
|
+
- Base risk of resource type
|
|
208
|
+
- Scope breadth (broad scopes = higher risk)
|
|
209
|
+
- Write operations (higher risk)
|
|
210
|
+
"""
|
|
211
|
+
risk = BASE_RISKS.get(resource_type, 0.5)
|
|
212
|
+
|
|
213
|
+
# Broad scopes increase risk
|
|
214
|
+
if not scope or scope in ("*", "all"):
|
|
215
|
+
risk += 0.2
|
|
216
|
+
|
|
217
|
+
# Write operations increase risk
|
|
218
|
+
if scope and re.search(r'\b(write|delete|update|modify|create)\b', scope, re.IGNORECASE):
|
|
219
|
+
risk += 0.2
|
|
220
|
+
|
|
221
|
+
return min(risk, 1.0)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def generate_grant_token() -> str:
|
|
225
|
+
"""Generate a unique grant token."""
|
|
226
|
+
return f"grant_{uuid.uuid4().hex}"
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def log_audit(action: str, details: dict[str, Any]) -> None:
|
|
230
|
+
"""Append entry to audit log."""
|
|
231
|
+
ensure_data_dir()
|
|
232
|
+
entry: dict[str, Any] = {
|
|
233
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
234
|
+
"action": action,
|
|
235
|
+
"details": details
|
|
236
|
+
}
|
|
237
|
+
with open(AUDIT_LOG, "a") as f:
|
|
238
|
+
f.write(json.dumps(entry) + "\n")
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def save_grant(grant: dict[str, Any]) -> None:
|
|
242
|
+
"""Save grant to persistent storage."""
|
|
243
|
+
ensure_data_dir()
|
|
244
|
+
grants = {}
|
|
245
|
+
if GRANTS_FILE.exists():
|
|
246
|
+
try:
|
|
247
|
+
grants = json.loads(GRANTS_FILE.read_text())
|
|
248
|
+
except json.JSONDecodeError:
|
|
249
|
+
grants = {}
|
|
250
|
+
|
|
251
|
+
grants[grant["token"]] = grant
|
|
252
|
+
GRANTS_FILE.write_text(json.dumps(grants, indent=2))
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def evaluate_permission(agent_id: str, resource_type: str,
|
|
256
|
+
justification: str, scope: Optional[str] = None) -> dict[str, Any]:
|
|
257
|
+
"""
|
|
258
|
+
Evaluate a permission request using weighted scoring.
|
|
259
|
+
|
|
260
|
+
Weights:
|
|
261
|
+
- Justification Quality: 40%
|
|
262
|
+
- Agent Trust Level: 30%
|
|
263
|
+
- Risk Assessment: 30%
|
|
264
|
+
"""
|
|
265
|
+
# Log the request
|
|
266
|
+
log_audit("permission_request", {
|
|
267
|
+
"agent_id": agent_id,
|
|
268
|
+
"resource_type": resource_type,
|
|
269
|
+
"justification": justification,
|
|
270
|
+
"scope": scope
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
# 1. Justification Quality (40% weight)
|
|
274
|
+
justification_score = score_justification(justification)
|
|
275
|
+
if justification_score < 0.3:
|
|
276
|
+
return {
|
|
277
|
+
"granted": False,
|
|
278
|
+
"reason": "Justification is insufficient. Please provide specific task context.",
|
|
279
|
+
"scores": {
|
|
280
|
+
"justification": justification_score,
|
|
281
|
+
"trust": None,
|
|
282
|
+
"risk": None
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
# 2. Agent Trust Level (30% weight)
|
|
287
|
+
trust_level = DEFAULT_TRUST_LEVELS.get(agent_id, 0.5)
|
|
288
|
+
if trust_level < 0.4:
|
|
289
|
+
return {
|
|
290
|
+
"granted": False,
|
|
291
|
+
"reason": "Agent trust level is below threshold. Escalate to human operator.",
|
|
292
|
+
"scores": {
|
|
293
|
+
"justification": justification_score,
|
|
294
|
+
"trust": trust_level,
|
|
295
|
+
"risk": None
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
# 3. Risk Assessment (30% weight)
|
|
300
|
+
risk_score = assess_risk(resource_type, scope)
|
|
301
|
+
if risk_score > 0.8:
|
|
302
|
+
return {
|
|
303
|
+
"granted": False,
|
|
304
|
+
"reason": "Risk assessment exceeds acceptable threshold. Narrow the requested scope.",
|
|
305
|
+
"scores": {
|
|
306
|
+
"justification": justification_score,
|
|
307
|
+
"trust": trust_level,
|
|
308
|
+
"risk": risk_score
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
# Calculate weighted approval score
|
|
313
|
+
weighted_score = (
|
|
314
|
+
justification_score * 0.4 +
|
|
315
|
+
trust_level * 0.3 +
|
|
316
|
+
(1 - risk_score) * 0.3
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
if weighted_score < 0.5:
|
|
320
|
+
return {
|
|
321
|
+
"granted": False,
|
|
322
|
+
"reason": f"Combined evaluation score ({weighted_score:.2f}) below threshold (0.5).",
|
|
323
|
+
"scores": {
|
|
324
|
+
"justification": justification_score,
|
|
325
|
+
"trust": trust_level,
|
|
326
|
+
"risk": risk_score,
|
|
327
|
+
"weighted": weighted_score
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
# Generate grant
|
|
332
|
+
token = generate_grant_token()
|
|
333
|
+
expires_at = (datetime.now(timezone.utc) + timedelta(minutes=GRANT_TOKEN_TTL_MINUTES)).isoformat()
|
|
334
|
+
restrictions = RESTRICTIONS.get(resource_type, [])
|
|
335
|
+
|
|
336
|
+
grant: dict[str, Any] = {
|
|
337
|
+
"token": token,
|
|
338
|
+
"agent_id": agent_id,
|
|
339
|
+
"resource_type": resource_type,
|
|
340
|
+
"scope": scope,
|
|
341
|
+
"expires_at": expires_at,
|
|
342
|
+
"restrictions": restrictions,
|
|
343
|
+
"granted_at": datetime.now(timezone.utc).isoformat()
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
# Save grant and log
|
|
347
|
+
save_grant(grant)
|
|
348
|
+
log_audit("permission_granted", grant)
|
|
349
|
+
|
|
350
|
+
return {
|
|
351
|
+
"granted": True,
|
|
352
|
+
"token": token,
|
|
353
|
+
"expires_at": expires_at,
|
|
354
|
+
"restrictions": restrictions,
|
|
355
|
+
"scores": {
|
|
356
|
+
"justification": justification_score,
|
|
357
|
+
"trust": trust_level,
|
|
358
|
+
"risk": risk_score,
|
|
359
|
+
"weighted": weighted_score
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def list_active_grants(agent_filter: Optional[str] = None, as_json: bool = False) -> int:
|
|
365
|
+
"""
|
|
366
|
+
Show which agents currently hold access to which APIs with expiry times.
|
|
367
|
+
|
|
368
|
+
Reads data/active_grants.json, filters out expired grants,
|
|
369
|
+
and displays remaining grants with TTL.
|
|
370
|
+
"""
|
|
371
|
+
if not GRANTS_FILE.exists():
|
|
372
|
+
if as_json:
|
|
373
|
+
print(json.dumps({"grants": [], "total": 0, "expired_cleaned": 0}))
|
|
374
|
+
else:
|
|
375
|
+
print("No active grants. (No grants file found.)")
|
|
376
|
+
return 0
|
|
377
|
+
|
|
378
|
+
try:
|
|
379
|
+
grants = json.loads(GRANTS_FILE.read_text())
|
|
380
|
+
except (json.JSONDecodeError, OSError):
|
|
381
|
+
if as_json:
|
|
382
|
+
print(json.dumps({"error": "Could not read grants file"}))
|
|
383
|
+
else:
|
|
384
|
+
print("Error: Could not read grants file.")
|
|
385
|
+
return 1
|
|
386
|
+
|
|
387
|
+
now = datetime.now(timezone.utc)
|
|
388
|
+
active: list[dict[str, Any]] = []
|
|
389
|
+
expired_count = 0
|
|
390
|
+
|
|
391
|
+
for token, grant in grants.items():
|
|
392
|
+
try:
|
|
393
|
+
expires_at = datetime.fromisoformat(grant["expires_at"])
|
|
394
|
+
except (KeyError, ValueError):
|
|
395
|
+
expired_count += 1
|
|
396
|
+
continue
|
|
397
|
+
|
|
398
|
+
if expires_at <= now:
|
|
399
|
+
expired_count += 1
|
|
400
|
+
continue
|
|
401
|
+
|
|
402
|
+
if agent_filter and grant.get("agent_id") != agent_filter:
|
|
403
|
+
continue
|
|
404
|
+
|
|
405
|
+
remaining = expires_at - now
|
|
406
|
+
minutes_left = remaining.total_seconds() / 60
|
|
407
|
+
|
|
408
|
+
active.append({
|
|
409
|
+
"token": token[:16] + "..." if len(token) > 16 else token,
|
|
410
|
+
"token_full": token,
|
|
411
|
+
"agent_id": grant.get("agent_id", "unknown"),
|
|
412
|
+
"resource_type": grant.get("resource_type", "unknown"),
|
|
413
|
+
"scope": grant.get("scope"),
|
|
414
|
+
"granted_at": grant.get("granted_at", "unknown"),
|
|
415
|
+
"expires_at": grant["expires_at"],
|
|
416
|
+
"minutes_remaining": round(minutes_left, 1),
|
|
417
|
+
"restrictions": grant.get("restrictions", []),
|
|
418
|
+
})
|
|
419
|
+
|
|
420
|
+
# Sort by expiry (soonest first)
|
|
421
|
+
active.sort(key=lambda g: g["expires_at"])
|
|
422
|
+
|
|
423
|
+
if as_json:
|
|
424
|
+
# In JSON mode, include full tokens
|
|
425
|
+
output: dict[str, Any] = {
|
|
426
|
+
"grants": active,
|
|
427
|
+
"total": len(active),
|
|
428
|
+
"expired_cleaned": expired_count,
|
|
429
|
+
}
|
|
430
|
+
print(json.dumps(output, indent=2))
|
|
431
|
+
else:
|
|
432
|
+
if not active:
|
|
433
|
+
filter_msg = f" for agent '{agent_filter}'" if agent_filter else ""
|
|
434
|
+
print(f"No active grants{filter_msg}. ({expired_count} expired.)")
|
|
435
|
+
else:
|
|
436
|
+
filter_msg = f" (agent: {agent_filter})" if agent_filter else ""
|
|
437
|
+
print(f"Active Grants{filter_msg}:")
|
|
438
|
+
print(f"{'='*70}")
|
|
439
|
+
for g in active:
|
|
440
|
+
print(f" Agent: {g['agent_id']}")
|
|
441
|
+
print(f" Resource: {g['resource_type']}")
|
|
442
|
+
if g["scope"]:
|
|
443
|
+
print(f" Scope: {g['scope']}")
|
|
444
|
+
print(f" Token: {g['token']}")
|
|
445
|
+
print(f" Granted: {g['granted_at']}")
|
|
446
|
+
print(f" Expires: {g['expires_at']}")
|
|
447
|
+
print(f" Remaining: {g['minutes_remaining']} min")
|
|
448
|
+
if g["restrictions"]:
|
|
449
|
+
print(f" Restrictions: {', '.join(g['restrictions'])}")
|
|
450
|
+
print(f" {'-'*66}")
|
|
451
|
+
print(f"\nTotal: {len(active)} active, {expired_count} expired")
|
|
452
|
+
|
|
453
|
+
return 0
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def audit_summary(last_n: int = 20, as_json: bool = False) -> int:
|
|
457
|
+
"""
|
|
458
|
+
Summarize recent permission requests, grants, and denials.
|
|
459
|
+
|
|
460
|
+
Parses data/audit_log.jsonl and produces per-agent and per-resource
|
|
461
|
+
breakdowns plus recent activity.
|
|
462
|
+
"""
|
|
463
|
+
if not AUDIT_LOG.exists():
|
|
464
|
+
if as_json:
|
|
465
|
+
print(json.dumps({"entries": 0, "summary": {}, "recent": []}))
|
|
466
|
+
else:
|
|
467
|
+
print("No audit log found. (No permission requests recorded yet.)")
|
|
468
|
+
return 0
|
|
469
|
+
|
|
470
|
+
entries: list[dict[str, Any]] = []
|
|
471
|
+
try:
|
|
472
|
+
with open(AUDIT_LOG, "r") as f:
|
|
473
|
+
for line in f:
|
|
474
|
+
line = line.strip()
|
|
475
|
+
if line:
|
|
476
|
+
try:
|
|
477
|
+
entries.append(json.loads(line))
|
|
478
|
+
except json.JSONDecodeError:
|
|
479
|
+
continue
|
|
480
|
+
except OSError:
|
|
481
|
+
if as_json:
|
|
482
|
+
print(json.dumps({"error": "Could not read audit log"}))
|
|
483
|
+
else:
|
|
484
|
+
print("Error: Could not read audit log.")
|
|
485
|
+
return 1
|
|
486
|
+
|
|
487
|
+
if not entries:
|
|
488
|
+
if as_json:
|
|
489
|
+
print(json.dumps({"entries": 0, "summary": {}, "recent": []}))
|
|
490
|
+
else:
|
|
491
|
+
print("Audit log is empty.")
|
|
492
|
+
return 0
|
|
493
|
+
|
|
494
|
+
# Aggregate stats
|
|
495
|
+
total_requests = 0
|
|
496
|
+
total_grants = 0
|
|
497
|
+
by_agent: dict[str, dict[str, int]] = {}
|
|
498
|
+
by_resource: dict[str, dict[str, int]] = {}
|
|
499
|
+
|
|
500
|
+
for entry in entries:
|
|
501
|
+
action = entry.get("action", "")
|
|
502
|
+
details = entry.get("details", {})
|
|
503
|
+
agent_id = details.get("agent_id", "unknown")
|
|
504
|
+
resource_type = details.get("resource_type", "unknown")
|
|
505
|
+
|
|
506
|
+
if action == "permission_request":
|
|
507
|
+
total_requests += 1
|
|
508
|
+
by_agent.setdefault(agent_id, {"requests": 0, "grants": 0})
|
|
509
|
+
by_agent[agent_id]["requests"] += 1
|
|
510
|
+
by_resource.setdefault(resource_type, {"requests": 0, "grants": 0})
|
|
511
|
+
by_resource[resource_type]["requests"] += 1
|
|
512
|
+
elif action == "permission_granted":
|
|
513
|
+
total_grants += 1
|
|
514
|
+
by_agent.setdefault(agent_id, {"requests": 0, "grants": 0})
|
|
515
|
+
by_agent[agent_id]["grants"] += 1
|
|
516
|
+
by_resource.setdefault(resource_type, {"requests": 0, "grants": 0})
|
|
517
|
+
by_resource[resource_type]["grants"] += 1
|
|
518
|
+
|
|
519
|
+
total_denials = total_requests - total_grants
|
|
520
|
+
|
|
521
|
+
# Recent entries (last N)
|
|
522
|
+
recent = entries[-last_n:]
|
|
523
|
+
|
|
524
|
+
# Time range
|
|
525
|
+
first_ts = entries[0].get("timestamp", "unknown")
|
|
526
|
+
last_ts = entries[-1].get("timestamp", "unknown")
|
|
527
|
+
|
|
528
|
+
if as_json:
|
|
529
|
+
output: dict[str, Any] = {
|
|
530
|
+
"total_entries": len(entries),
|
|
531
|
+
"total_requests": total_requests,
|
|
532
|
+
"total_grants": total_grants,
|
|
533
|
+
"total_denials": total_denials,
|
|
534
|
+
"time_range": {"first": first_ts, "last": last_ts},
|
|
535
|
+
"by_agent": by_agent,
|
|
536
|
+
"by_resource": by_resource,
|
|
537
|
+
"recent": recent[-last_n:],
|
|
538
|
+
}
|
|
539
|
+
print(json.dumps(output, indent=2))
|
|
540
|
+
else:
|
|
541
|
+
print("Audit Summary")
|
|
542
|
+
print(f"{'='*70}")
|
|
543
|
+
print(f" Log entries: {len(entries)}")
|
|
544
|
+
print(f" Time range: {first_ts}")
|
|
545
|
+
print(f" {last_ts}")
|
|
546
|
+
print(f"")
|
|
547
|
+
print(f" Requests: {total_requests}")
|
|
548
|
+
print(f" Grants: {total_grants}")
|
|
549
|
+
print(f" Denials: {total_denials}")
|
|
550
|
+
grant_rate = (total_grants / total_requests * 100) if total_requests > 0 else 0
|
|
551
|
+
print(f" Grant Rate: {grant_rate:.0f}%")
|
|
552
|
+
|
|
553
|
+
if by_agent:
|
|
554
|
+
print(f"\n By Agent:")
|
|
555
|
+
print(f" {'-'*50}")
|
|
556
|
+
print(f" {'Agent':<20} {'Requests':>10} {'Grants':>10} {'Denials':>10}")
|
|
557
|
+
print(f" {'-'*50}")
|
|
558
|
+
for agent_id, stats in sorted(by_agent.items()):
|
|
559
|
+
denials = stats["requests"] - stats["grants"]
|
|
560
|
+
print(f" {agent_id:<20} {stats['requests']:>10} {stats['grants']:>10} {denials:>10}")
|
|
561
|
+
|
|
562
|
+
if by_resource:
|
|
563
|
+
print(f"\n By Resource:")
|
|
564
|
+
print(f" {'-'*50}")
|
|
565
|
+
print(f" {'Resource':<20} {'Requests':>10} {'Grants':>10} {'Denials':>10}")
|
|
566
|
+
print(f" {'-'*50}")
|
|
567
|
+
for resource_type, stats in sorted(by_resource.items()):
|
|
568
|
+
denials = stats["requests"] - stats["grants"]
|
|
569
|
+
print(f" {resource_type:<20} {stats['requests']:>10} {stats['grants']:>10} {denials:>10}")
|
|
570
|
+
|
|
571
|
+
print(f"\n Recent Activity (last {min(last_n, len(recent))}):")
|
|
572
|
+
print(f" {'-'*66}")
|
|
573
|
+
for entry in recent:
|
|
574
|
+
ts = entry.get("timestamp", "?")[:19]
|
|
575
|
+
action = entry.get("action", "?")
|
|
576
|
+
details = entry.get("details", {})
|
|
577
|
+
agent_id = details.get("agent_id", "?")
|
|
578
|
+
resource_type = details.get("resource_type", "?")
|
|
579
|
+
symbol = "GRANT" if action == "permission_granted" else "REQ" if action == "permission_request" else action.upper()
|
|
580
|
+
print(f" {ts} [{symbol:>5}] {agent_id} -> {resource_type}")
|
|
581
|
+
|
|
582
|
+
return 0
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
def main():
|
|
586
|
+
parser = argparse.ArgumentParser(
|
|
587
|
+
description="AuthGuardian Permission Checker",
|
|
588
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
589
|
+
epilog="""
|
|
590
|
+
Examples:
|
|
591
|
+
Check permission:
|
|
592
|
+
%(prog)s --agent data_analyst --resource DATABASE \\
|
|
593
|
+
--justification "Need Q4 invoice data for quarterly report"
|
|
594
|
+
|
|
595
|
+
List active grants:
|
|
596
|
+
%(prog)s --active-grants
|
|
597
|
+
%(prog)s --active-grants --agent data_analyst --json
|
|
598
|
+
|
|
599
|
+
View audit summary:
|
|
600
|
+
%(prog)s --audit-summary
|
|
601
|
+
%(prog)s --audit-summary --last 50 --json
|
|
602
|
+
"""
|
|
603
|
+
)
|
|
604
|
+
|
|
605
|
+
# Action flags
|
|
606
|
+
parser.add_argument(
|
|
607
|
+
"--active-grants",
|
|
608
|
+
action="store_true",
|
|
609
|
+
help="List all active (non-expired) permission grants"
|
|
610
|
+
)
|
|
611
|
+
parser.add_argument(
|
|
612
|
+
"--audit-summary",
|
|
613
|
+
action="store_true",
|
|
614
|
+
help="Show audit log summary with per-agent and per-resource breakdowns"
|
|
615
|
+
)
|
|
616
|
+
parser.add_argument(
|
|
617
|
+
"--last",
|
|
618
|
+
type=int,
|
|
619
|
+
default=20,
|
|
620
|
+
help="Number of recent audit entries to show (default: 20)"
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
# Permission check args (required only for check mode)
|
|
624
|
+
parser.add_argument(
|
|
625
|
+
"--agent", "-a",
|
|
626
|
+
help="Agent ID requesting permission (required for check; optional filter for --active-grants)"
|
|
627
|
+
)
|
|
628
|
+
parser.add_argument(
|
|
629
|
+
"--resource", "-r",
|
|
630
|
+
choices=["DATABASE", "PAYMENTS", "EMAIL", "FILE_EXPORT"],
|
|
631
|
+
help="Resource type to access"
|
|
632
|
+
)
|
|
633
|
+
parser.add_argument(
|
|
634
|
+
"--justification", "-j",
|
|
635
|
+
help="Business justification for the request"
|
|
636
|
+
)
|
|
637
|
+
parser.add_argument(
|
|
638
|
+
"--scope", "-s",
|
|
639
|
+
help="Specific scope of access (e.g., 'read:invoices')"
|
|
640
|
+
)
|
|
641
|
+
parser.add_argument(
|
|
642
|
+
"--json",
|
|
643
|
+
action="store_true",
|
|
644
|
+
help="Output result as JSON"
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
args = parser.parse_args()
|
|
648
|
+
|
|
649
|
+
# --- Action: --active-grants ---
|
|
650
|
+
if args.active_grants:
|
|
651
|
+
sys.exit(list_active_grants(agent_filter=args.agent, as_json=args.json))
|
|
652
|
+
|
|
653
|
+
# --- Action: --audit-summary ---
|
|
654
|
+
if args.audit_summary:
|
|
655
|
+
sys.exit(audit_summary(last_n=args.last, as_json=args.json))
|
|
656
|
+
|
|
657
|
+
# --- Default action: permission check ---
|
|
658
|
+
if not args.agent:
|
|
659
|
+
parser.error("--agent is required for permission checks")
|
|
660
|
+
if not args.resource:
|
|
661
|
+
parser.error("--resource is required for permission checks")
|
|
662
|
+
if not args.justification:
|
|
663
|
+
parser.error("--justification is required for permission checks")
|
|
664
|
+
|
|
665
|
+
result = evaluate_permission(
|
|
666
|
+
agent_id=args.agent,
|
|
667
|
+
resource_type=args.resource,
|
|
668
|
+
justification=args.justification,
|
|
669
|
+
scope=args.scope
|
|
670
|
+
)
|
|
671
|
+
|
|
672
|
+
if args.json:
|
|
673
|
+
print(json.dumps(result, indent=2))
|
|
674
|
+
else:
|
|
675
|
+
if result["granted"]:
|
|
676
|
+
print("GRANTED")
|
|
677
|
+
print(f"Token: {result['token']}")
|
|
678
|
+
print(f"Expires: {result['expires_at']}")
|
|
679
|
+
print(f"Restrictions: {', '.join(result['restrictions'])}")
|
|
680
|
+
else:
|
|
681
|
+
print("DENIED")
|
|
682
|
+
print(f"Reason: {result['reason']}")
|
|
683
|
+
|
|
684
|
+
print("\nEvaluation Scores:")
|
|
685
|
+
scores = result["scores"]
|
|
686
|
+
if scores.get("justification") is not None:
|
|
687
|
+
print(f" Justification: {scores['justification']:.2f}")
|
|
688
|
+
if scores.get("trust") is not None:
|
|
689
|
+
print(f" Trust Level: {scores['trust']:.2f}")
|
|
690
|
+
if scores.get("risk") is not None:
|
|
691
|
+
print(f" Risk Score: {scores['risk']:.2f}")
|
|
692
|
+
if scores.get("weighted") is not None:
|
|
693
|
+
print(f" Weighted: {scores['weighted']:.2f}")
|
|
694
|
+
|
|
695
|
+
sys.exit(0 if result["granted"] else 1)
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
if __name__ == "__main__":
|
|
699
|
+
main()
|