devsquad 3.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- devsquad-3.6.0.dist-info/METADATA +944 -0
- devsquad-3.6.0.dist-info/RECORD +95 -0
- devsquad-3.6.0.dist-info/WHEEL +5 -0
- devsquad-3.6.0.dist-info/entry_points.txt +2 -0
- devsquad-3.6.0.dist-info/licenses/LICENSE +21 -0
- devsquad-3.6.0.dist-info/top_level.txt +2 -0
- scripts/__init__.py +0 -0
- scripts/ai_semantic_matcher.py +512 -0
- scripts/alert_manager.py +505 -0
- scripts/api/__init__.py +43 -0
- scripts/api/models.py +386 -0
- scripts/api/routes/__init__.py +20 -0
- scripts/api/routes/dispatch.py +348 -0
- scripts/api/routes/lifecycle.py +330 -0
- scripts/api/routes/metrics_gates.py +347 -0
- scripts/api_server.py +318 -0
- scripts/auth.py +451 -0
- scripts/cli/__init__.py +1 -0
- scripts/cli/cli_visual.py +642 -0
- scripts/cli.py +1094 -0
- scripts/collaboration/__init__.py +212 -0
- scripts/collaboration/_version.py +1 -0
- scripts/collaboration/agent_briefing.py +656 -0
- scripts/collaboration/ai_semantic_matcher.py +260 -0
- scripts/collaboration/anchor_checker.py +281 -0
- scripts/collaboration/anti_rationalization.py +470 -0
- scripts/collaboration/async_integration_example.py +255 -0
- scripts/collaboration/batch_scheduler.py +149 -0
- scripts/collaboration/checkpoint_manager.py +561 -0
- scripts/collaboration/ci_feedback_adapter.py +351 -0
- scripts/collaboration/code_map_generator.py +247 -0
- scripts/collaboration/concern_pack_loader.py +352 -0
- scripts/collaboration/confidence_score.py +496 -0
- scripts/collaboration/config_loader.py +188 -0
- scripts/collaboration/consensus.py +244 -0
- scripts/collaboration/context_compressor.py +533 -0
- scripts/collaboration/coordinator.py +668 -0
- scripts/collaboration/dispatcher.py +1636 -0
- scripts/collaboration/dual_layer_context.py +128 -0
- scripts/collaboration/enhanced_worker.py +539 -0
- scripts/collaboration/feature_usage_tracker.py +206 -0
- scripts/collaboration/five_axis_consensus.py +334 -0
- scripts/collaboration/input_validator.py +401 -0
- scripts/collaboration/integration_example.py +287 -0
- scripts/collaboration/intent_workflow_mapper.py +350 -0
- scripts/collaboration/language_parsers.py +269 -0
- scripts/collaboration/lifecycle_protocol.py +1446 -0
- scripts/collaboration/llm_backend.py +453 -0
- scripts/collaboration/llm_cache.py +448 -0
- scripts/collaboration/llm_cache_async.py +347 -0
- scripts/collaboration/llm_retry.py +387 -0
- scripts/collaboration/llm_retry_async.py +389 -0
- scripts/collaboration/mce_adapter.py +597 -0
- scripts/collaboration/memory_bridge.py +1607 -0
- scripts/collaboration/models.py +537 -0
- scripts/collaboration/null_providers.py +297 -0
- scripts/collaboration/operation_classifier.py +289 -0
- scripts/collaboration/output_slicer.py +225 -0
- scripts/collaboration/performance_monitor.py +462 -0
- scripts/collaboration/permission_guard.py +865 -0
- scripts/collaboration/prompt_assembler.py +756 -0
- scripts/collaboration/prompt_variant_generator.py +483 -0
- scripts/collaboration/protocols.py +267 -0
- scripts/collaboration/report_formatter.py +352 -0
- scripts/collaboration/retrospective.py +279 -0
- scripts/collaboration/role_matcher.py +92 -0
- scripts/collaboration/role_template_market.py +352 -0
- scripts/collaboration/rule_collector.py +678 -0
- scripts/collaboration/scratchpad.py +346 -0
- scripts/collaboration/skill_registry.py +151 -0
- scripts/collaboration/skillifier.py +878 -0
- scripts/collaboration/standardized_role_template.py +317 -0
- scripts/collaboration/task_completion_checker.py +237 -0
- scripts/collaboration/test_quality_guard.py +695 -0
- scripts/collaboration/unified_gate_engine.py +598 -0
- scripts/collaboration/usage_tracker.py +309 -0
- scripts/collaboration/user_friendly_error.py +176 -0
- scripts/collaboration/verification_gate.py +312 -0
- scripts/collaboration/warmup_manager.py +635 -0
- scripts/collaboration/worker.py +513 -0
- scripts/collaboration/workflow_engine.py +684 -0
- scripts/dashboard.py +1088 -0
- scripts/generate_benchmark_report.py +786 -0
- scripts/history_manager.py +604 -0
- scripts/mcp_server.py +289 -0
- skills/__init__.py +32 -0
- skills/dispatch/handler.py +52 -0
- skills/intent/handler.py +59 -0
- skills/registry.py +67 -0
- skills/retrospective/__init__.py +0 -0
- skills/retrospective/handler.py +125 -0
- skills/review/handler.py +356 -0
- skills/security/handler.py +454 -0
- skills/test/__init__.py +0 -0
- skills/test/handler.py +78 -0
|
@@ -0,0 +1,865 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
PermissionGuard - 4-Level Permission Guard System
|
|
5
|
+
|
|
6
|
+
Based on v3-phase2-permission-design.md:
|
|
7
|
+
Level DEFAULT: Dangerous ops require user confirmation
|
|
8
|
+
Level PLAN: Read-only mode, all writes denied
|
|
9
|
+
Level AUTO: AI classifier auto-judges + whitelist
|
|
10
|
+
Level BYPASS: Skip all checks (highest trust only)
|
|
11
|
+
|
|
12
|
+
Core components:
|
|
13
|
+
- ActionType: 9 operation types (FILE_READ/WRITE/DELETE/SHELL/NETWORK/GIT/ENV/PROCESS)
|
|
14
|
+
- PermissionLevel: 4 security levels (DEFAULT/PLAN/AUTO/BYPASS)
|
|
15
|
+
- DecisionOutcome: 4 verdicts (ALLOWED/DENIED/PROMPT/ESCALATED)
|
|
16
|
+
- Rule Engine: glob + prefix + regex pattern matching
|
|
17
|
+
- AI Classifier: 5-dimension risk scoring [0.0, 1.0]
|
|
18
|
+
- Audit Log: complete decision trail with filtering
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import re
|
|
22
|
+
import fnmatch
|
|
23
|
+
import uuid
|
|
24
|
+
import threading
|
|
25
|
+
import time
|
|
26
|
+
from dataclasses import dataclass, field
|
|
27
|
+
from datetime import datetime
|
|
28
|
+
from enum import Enum
|
|
29
|
+
from typing import Any, Callable, Dict, List, Optional, Set, Tuple
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class PermissionLevel(Enum):
|
|
33
|
+
DEFAULT = "default"
|
|
34
|
+
PLAN = "plan"
|
|
35
|
+
AUTO = "auto"
|
|
36
|
+
BYPASS = "bypass"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ActionType(Enum):
|
|
40
|
+
FILE_READ = "file_read"
|
|
41
|
+
FILE_CREATE = "file_create"
|
|
42
|
+
FILE_MODIFY = "file_modify"
|
|
43
|
+
FILE_DELETE = "file_delete"
|
|
44
|
+
SHELL_EXECUTE = "shell_execute"
|
|
45
|
+
NETWORK_REQUEST = "network_request"
|
|
46
|
+
GIT_OPERATION = "git_operation"
|
|
47
|
+
ENVIRONMENT = "environment"
|
|
48
|
+
PROCESS_SPAWN = "process_spawn"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class DecisionOutcome(Enum):
|
|
52
|
+
ALLOWED = "allowed"
|
|
53
|
+
DENIED = "denied"
|
|
54
|
+
PROMPT = "prompt"
|
|
55
|
+
ESCALATED = "escalated"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class ProposedAction:
|
|
60
|
+
action_type: ActionType = ActionType.FILE_READ
|
|
61
|
+
target: str = ""
|
|
62
|
+
description: str = ""
|
|
63
|
+
source_worker_id: Optional[str] = None
|
|
64
|
+
source_role_id: Optional[str] = None
|
|
65
|
+
risk_score: float = 0.0
|
|
66
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
67
|
+
timestamp: datetime = field(default_factory=datetime.now)
|
|
68
|
+
|
|
69
|
+
def to_dict(self) -> Dict:
|
|
70
|
+
return {
|
|
71
|
+
"action_type": self.action_type.value,
|
|
72
|
+
"target": self.target,
|
|
73
|
+
"description": self.description,
|
|
74
|
+
"source_worker_id": self.source_worker_id,
|
|
75
|
+
"source_role_id": self.source_role_id,
|
|
76
|
+
"risk_score": self.risk_score,
|
|
77
|
+
"metadata": self.metadata,
|
|
78
|
+
"timestamp": self.timestamp.isoformat(),
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@classmethod
|
|
82
|
+
def from_dict(cls, d: Dict) -> "ProposedAction":
|
|
83
|
+
ts = d.get("timestamp")
|
|
84
|
+
return cls(
|
|
85
|
+
action_type=ActionType(d.get("action_type", "file_read")),
|
|
86
|
+
target=d.get("target", ""),
|
|
87
|
+
description=d.get("description", ""),
|
|
88
|
+
source_worker_id=d.get("source_worker_id"),
|
|
89
|
+
source_role_id=d.get("source_role_id"),
|
|
90
|
+
risk_score=d.get("risk_score", 0.0),
|
|
91
|
+
metadata=d.get("metadata", {}),
|
|
92
|
+
timestamp=datetime.fromisoformat(ts) if ts else datetime.now(),
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass
|
|
97
|
+
class PermissionRule:
|
|
98
|
+
rule_id: str
|
|
99
|
+
action_type: ActionType
|
|
100
|
+
pattern: str
|
|
101
|
+
required_level: PermissionLevel
|
|
102
|
+
description: str = ""
|
|
103
|
+
risk_boost: float = 0.0
|
|
104
|
+
tags: List[str] = field(default_factory=list)
|
|
105
|
+
enabled: bool = True
|
|
106
|
+
|
|
107
|
+
def to_dict(self) -> Dict:
|
|
108
|
+
return {
|
|
109
|
+
"rule_id": self.rule_id,
|
|
110
|
+
"action_type": self.action_type.value,
|
|
111
|
+
"pattern": self.pattern,
|
|
112
|
+
"required_level": self.required_level.value,
|
|
113
|
+
"description": self.description,
|
|
114
|
+
"risk_boost": self.risk_boost,
|
|
115
|
+
"tags": self.tags,
|
|
116
|
+
"enabled": self.enabled,
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
@classmethod
|
|
120
|
+
def from_dict(cls, d: Dict) -> "PermissionRule":
|
|
121
|
+
return cls(
|
|
122
|
+
rule_id=d["rule_id"],
|
|
123
|
+
action_type=ActionType(d["action_type"]),
|
|
124
|
+
pattern=d["pattern"],
|
|
125
|
+
required_level=PermissionLevel(d["required_level"]),
|
|
126
|
+
description=d.get("description", ""),
|
|
127
|
+
risk_boost=d.get("risk_boost", 0.0),
|
|
128
|
+
tags=d.get("tags", []),
|
|
129
|
+
enabled=d.get("enabled", True),
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@dataclass
|
|
134
|
+
class PermissionDecision:
|
|
135
|
+
action: ProposedAction
|
|
136
|
+
outcome: DecisionOutcome
|
|
137
|
+
matched_rule: Optional[PermissionRule] = None
|
|
138
|
+
reason: str = ""
|
|
139
|
+
requires_confirmation: bool = False
|
|
140
|
+
confidence: float = 1.0
|
|
141
|
+
decided_at: datetime = field(default_factory=datetime.now)
|
|
142
|
+
decision_id: str = field(default_factory=lambda: f"pd-{uuid.uuid4().hex[:12]}")
|
|
143
|
+
|
|
144
|
+
def to_dict(self) -> Dict:
|
|
145
|
+
return {
|
|
146
|
+
"decision_id": self.decision_id,
|
|
147
|
+
"outcome": self.outcome.value,
|
|
148
|
+
"matched_rule": self.matched_rule.rule_id if self.matched_rule else None,
|
|
149
|
+
"reason": self.reason,
|
|
150
|
+
"requires_confirmation": self.requires_confirmation,
|
|
151
|
+
"confidence": self.confidence,
|
|
152
|
+
"decided_at": self.decided_at.isoformat(),
|
|
153
|
+
"action": self.action.to_dict(),
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@dataclass
|
|
158
|
+
class AuditEntry:
|
|
159
|
+
entry_id: str = field(default_factory=lambda: f"ae-{uuid.uuid4().hex[:12]}")
|
|
160
|
+
action: Optional[ProposedAction] = None
|
|
161
|
+
decision: Optional[PermissionDecision] = None
|
|
162
|
+
duration_ms: int = 0
|
|
163
|
+
guard_level: PermissionLevel = PermissionLevel.DEFAULT
|
|
164
|
+
user_response: Optional[str] = None
|
|
165
|
+
session_id: str = field(default_factory=lambda: f"sess-{uuid.uuid4().hex[:8]}")
|
|
166
|
+
timestamp: datetime = field(default_factory=datetime.now)
|
|
167
|
+
|
|
168
|
+
def to_dict(self) -> Dict:
|
|
169
|
+
return {
|
|
170
|
+
"entry_id": self.entry_id,
|
|
171
|
+
"action": self.action.to_dict() if self.action else None,
|
|
172
|
+
"decision": self.decision.to_dict() if self.decision else None,
|
|
173
|
+
"duration_ms": self.duration_ms,
|
|
174
|
+
"guard_level": self.guard_level.value,
|
|
175
|
+
"user_response": self.user_response,
|
|
176
|
+
"session_id": self.session_id,
|
|
177
|
+
"timestamp": self.timestamp.isoformat(),
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# ============================================================
|
|
182
|
+
# Default Rules (30 rules covering common scenarios)
|
|
183
|
+
# ============================================================
|
|
184
|
+
|
|
185
|
+
def _build_default_rules() -> List[PermissionRule]:
|
|
186
|
+
return [
|
|
187
|
+
# File Read (low risk)
|
|
188
|
+
PermissionRule("R001", ActionType.FILE_READ, "**/*",
|
|
189
|
+
PermissionLevel.PLAN, "Read any file", risk_boost=0.0),
|
|
190
|
+
|
|
191
|
+
# File Create (medium risk)
|
|
192
|
+
PermissionRule("R002", ActionType.FILE_CREATE, "*.py",
|
|
193
|
+
PermissionLevel.AUTO, "Create Python file", risk_boost=0.1),
|
|
194
|
+
PermissionRule("R003", ActionType.FILE_CREATE, "*.md",
|
|
195
|
+
PermissionLevel.AUTO, "Create markdown doc", risk_boost=0.05),
|
|
196
|
+
PermissionRule("R004", ActionType.FILE_CREATE, "*.json",
|
|
197
|
+
PermissionLevel.DEFAULT, "Create JSON data file", risk_boost=0.15),
|
|
198
|
+
PermissionRule("R005", ActionType.FILE_CREATE, "*",
|
|
199
|
+
PermissionLevel.DEFAULT, "Create other file type", risk_boost=0.2),
|
|
200
|
+
|
|
201
|
+
# File Modify (medium-high risk)
|
|
202
|
+
PermissionRule("R006", ActionType.FILE_MODIFY, "*.py",
|
|
203
|
+
PermissionLevel.AUTO, "Modify Python source", risk_boost=0.2),
|
|
204
|
+
PermissionRule("R007", ActionType.FILE_MODIFY, "*.md",
|
|
205
|
+
PermissionLevel.AUTO, "Modify document", risk_boost=0.1),
|
|
206
|
+
PermissionRule("R008", ActionType.FILE_MODIFY, "*.json",
|
|
207
|
+
PermissionLevel.DEFAULT, "Modify config file", risk_boost=0.25),
|
|
208
|
+
PermissionRule("R009", ActionType.FILE_MODIFY, ".env*",
|
|
209
|
+
PermissionLevel.BYPASS, "Modify env file", risk_boost=0.8),
|
|
210
|
+
PermissionRule("R010", ActionType.FILE_MODIFY, "*credentials*",
|
|
211
|
+
PermissionLevel.BYPASS, "Modify credentials", risk_boost=0.95),
|
|
212
|
+
PermissionRule("R011", ActionType.FILE_MODIFY, "*",
|
|
213
|
+
PermissionLevel.DEFAULT, "Modify other files", risk_boost=0.25),
|
|
214
|
+
|
|
215
|
+
# File Delete (high risk)
|
|
216
|
+
PermissionRule("R012", ActionType.FILE_DELETE, "__pycache__/**",
|
|
217
|
+
PermissionLevel.AUTO, "Delete Python cache", risk_boost=0.1),
|
|
218
|
+
PermissionRule("R013", ActionType.FILE_DELETE, "*.pyc",
|
|
219
|
+
PermissionLevel.AUTO, "Delete compiled cache", risk_boost=0.1),
|
|
220
|
+
PermissionRule("R014", ActionType.FILE_DELETE, ".git/**",
|
|
221
|
+
PermissionLevel.BYPASS, "Delete Git dir content", risk_boost=0.99),
|
|
222
|
+
PermissionRule("R015", ActionType.FILE_DELETE, "*",
|
|
223
|
+
PermissionLevel.BYPASS, "Delete any file", risk_boost=0.9),
|
|
224
|
+
|
|
225
|
+
# Shell Execute (very high risk)
|
|
226
|
+
PermissionRule("R016", ActionType.SHELL_EXECUTE, "cat *",
|
|
227
|
+
PermissionLevel.AUTO, "View file content", risk_boost=0.05),
|
|
228
|
+
PermissionRule("R017", ActionType.SHELL_EXECUTE, "ls *",
|
|
229
|
+
PermissionLevel.AUTO, "List directory", risk_boost=0.05),
|
|
230
|
+
PermissionRule("R018", ActionType.SHELL_EXECUTE, "git *",
|
|
231
|
+
PermissionLevel.AUTO, "Git read-only commands", risk_boost=0.1),
|
|
232
|
+
PermissionRule("R019", ActionType.SHELL_EXECUTE, "pip install *",
|
|
233
|
+
PermissionLevel.DEFAULT, "Install Python package", risk_boost=0.5),
|
|
234
|
+
PermissionRule("R020", ActionType.SHELL_EXECUTE, "rm *",
|
|
235
|
+
PermissionLevel.BYPASS, "Remove command", risk_boost=0.95),
|
|
236
|
+
PermissionRule("R021", ActionType.SHELL_EXECUTE, "sudo *",
|
|
237
|
+
PermissionLevel.BYPASS, "Privilege escalation", risk_boost=1.0),
|
|
238
|
+
PermissionRule("R022", ActionType.SHELL_EXECUTE, "*",
|
|
239
|
+
PermissionLevel.DEFAULT, "Other shell commands", risk_boost=0.6),
|
|
240
|
+
|
|
241
|
+
# Network Request (medium-high risk)
|
|
242
|
+
PermissionRule("R023", ActionType.NETWORK_REQUEST, "*pypi.org*",
|
|
243
|
+
PermissionLevel.AUTO, "PyPI download", risk_boost=0.15),
|
|
244
|
+
PermissionRule("R024", ActionType.NETWORK_REQUEST, "*github.com*",
|
|
245
|
+
PermissionLevel.DEFAULT, "GitHub API access", risk_boost=0.3),
|
|
246
|
+
PermissionRule("R025", ActionType.NETWORK_REQUEST, "*",
|
|
247
|
+
PermissionLevel.DEFAULT, "Other network requests", risk_boost=0.5),
|
|
248
|
+
|
|
249
|
+
# Git Operations
|
|
250
|
+
PermissionRule("R026", ActionType.GIT_OPERATION, "*",
|
|
251
|
+
PermissionLevel.AUTO, "Git read operations", risk_boost=0.05),
|
|
252
|
+
PermissionRule("R027", ActionType.GIT_OPERATION, "*commit*|*add*",
|
|
253
|
+
PermissionLevel.DEFAULT, "Git commit ops", risk_boost=0.3),
|
|
254
|
+
PermissionRule("R028", ActionType.GIT_OPERATION, "*push*",
|
|
255
|
+
PermissionLevel.DEFAULT, "Git push operation", risk_boost=0.4),
|
|
256
|
+
PermissionRule("R029", ActionType.GIT_OPERATION, "*reset*|*rebase*|*force*",
|
|
257
|
+
PermissionLevel.BYPASS, "Dangerous git ops", risk_boost=0.9),
|
|
258
|
+
|
|
259
|
+
# Environment
|
|
260
|
+
PermissionRule("R030", ActionType.ENVIRONMENT, "*",
|
|
261
|
+
PermissionLevel.BYPASS, "Modify environment vars", risk_boost=0.7),
|
|
262
|
+
]
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
# Sensitive keyword patterns for path traversal / injection detection
|
|
266
|
+
_SENSITIVE_PATH_PATTERNS = [
|
|
267
|
+
r"\.\./",
|
|
268
|
+
r"\.\.\\",
|
|
269
|
+
r"\x00",
|
|
270
|
+
r"/etc/passwd",
|
|
271
|
+
r"/etc/shadow",
|
|
272
|
+
]
|
|
273
|
+
|
|
274
|
+
_SENSITIVE_PATH_PATTERNS_COMPILED = [
|
|
275
|
+
re.compile(p, re.IGNORECASE) for p in _SENSITIVE_PATH_PATTERNS
|
|
276
|
+
]
|
|
277
|
+
|
|
278
|
+
_SENSITIVE_KEYWORDS = [
|
|
279
|
+
"credential", "secret", "password", "private_key", "api_key",
|
|
280
|
+
"token", ".env", ".pem", ".key", "id_rsa", ".ssh",
|
|
281
|
+
]
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
class PermissionGuard:
|
|
285
|
+
"""
|
|
286
|
+
4级权限守卫系统 - 多 Agent 协作的安全操作检查核心
|
|
287
|
+
|
|
288
|
+
安全级别(由低到高):
|
|
289
|
+
PLAN (0): 只读模式,所有写操作被拒绝
|
|
290
|
+
DEFAULT (1): 危险操作需用户确认
|
|
291
|
+
AUTO (2): AI 分类器自动判断 + 白名单放行
|
|
292
|
+
BYPASS (3): 跳过所有检查(最高信任,仅限受控环境)
|
|
293
|
+
|
|
294
|
+
核心能力:
|
|
295
|
+
- check(): 对 ProposedAction 进行权限决策(ALLOWED/DENIED/PROMPT/ESCALATED)
|
|
296
|
+
- auto_classify(): 5维风险评分模型(目标敏感性/破坏性/范围/来源可信度/上下文合理性)
|
|
297
|
+
- 规则引擎: 30条默认规则覆盖 9种操作类型,支持 glob/前缀/regex 模式匹配
|
|
298
|
+
- 审计日志: 完整的决策链路记录,支持多维度过滤查询
|
|
299
|
+
|
|
300
|
+
决策流程:
|
|
301
|
+
BYPASS → 直接放行
|
|
302
|
+
PLAN → 只读放行/写入拒绝
|
|
303
|
+
DEFAULT/AUTO → 白名单 → 规则匹配 → 风险评估 → 允许/提示/拒绝
|
|
304
|
+
|
|
305
|
+
使用示例:
|
|
306
|
+
guard = PermissionGuard(current_level=PermissionLevel.DEFAULT)
|
|
307
|
+
action = ProposedAction(
|
|
308
|
+
action_type=ActionType.FILE_CREATE,
|
|
309
|
+
target="/path/to/file.py",
|
|
310
|
+
description="创建新模块",
|
|
311
|
+
source_worker_id="arch-abc",
|
|
312
|
+
source_role_id="architect",
|
|
313
|
+
)
|
|
314
|
+
decision = guard.check(action)
|
|
315
|
+
if decision.outcome == DecisionOutcome.PROMPT:
|
|
316
|
+
print(f"需用户确认: {decision.reason}")
|
|
317
|
+
"""
|
|
318
|
+
|
|
319
|
+
LEVEL_ORDER = {
|
|
320
|
+
PermissionLevel.PLAN: 0,
|
|
321
|
+
PermissionLevel.DEFAULT: 1,
|
|
322
|
+
PermissionLevel.AUTO: 2,
|
|
323
|
+
PermissionLevel.BYPASS: 3,
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
def __init__(self,
|
|
327
|
+
current_level: PermissionLevel = PermissionLevel.DEFAULT,
|
|
328
|
+
rules: Optional[List[PermissionRule]] = None,
|
|
329
|
+
audit_log: bool = True,
|
|
330
|
+
session_id: Optional[str] = None):
|
|
331
|
+
"""
|
|
332
|
+
初始化权限守卫
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
current_level: 当前安全级别(默认 DEFAULT)
|
|
336
|
+
rules: 自定义规则列表(为空则使用 30 条内置默认规则)
|
|
337
|
+
audit_log: 是否启用审计日志记录
|
|
338
|
+
session_id: 会话标识(用于审计追踪,自动生成如未提供)
|
|
339
|
+
"""
|
|
340
|
+
self.current_level = current_level
|
|
341
|
+
self.rules: List[PermissionRule] = rules or _build_default_rules()
|
|
342
|
+
self._rule_index: Dict[str, int] = {
|
|
343
|
+
r.rule_id: i for i, r in enumerate(self.rules)
|
|
344
|
+
}
|
|
345
|
+
self._rules_by_type: Dict[ActionType, List[PermissionRule]] = {}
|
|
346
|
+
for r in self.rules:
|
|
347
|
+
self._rules_by_type.setdefault(r.action_type, []).append(r)
|
|
348
|
+
self.audit_log_enabled = audit_log
|
|
349
|
+
self._audit_log: List[AuditEntry] = []
|
|
350
|
+
self._whitelist: Set[str] = set()
|
|
351
|
+
self._lock = threading.RLock()
|
|
352
|
+
self.session_id = session_id or f"sess-{uuid.uuid4().hex[:8]}"
|
|
353
|
+
|
|
354
|
+
def check(self, action: ProposedAction) -> PermissionDecision:
|
|
355
|
+
"""
|
|
356
|
+
核心权限检查方法 - 对操作提案进行安全决策
|
|
357
|
+
|
|
358
|
+
决策流程(按优先级):
|
|
359
|
+
1. BYPASS 级别 → 直接 ALLOWED
|
|
360
|
+
2. PLAN 级别 → 只读允许 / 写入 DENIED
|
|
361
|
+
3. 白名单匹配 → 直接 ALLOWED
|
|
362
|
+
4. 规则匹配 → 结合风险评分决策:
|
|
363
|
+
- 风险 < 0.3 且级别充足 → ALLOWED
|
|
364
|
+
- 风险 0.3~0.7 或 AUTO 模式 → 调用 auto_classify() 综合判断
|
|
365
|
+
- 风险 > 0.7 → PROMPT (需用户确认)
|
|
366
|
+
5. 无匹配规则 → 低风险允许 / 高风险 PROMPT
|
|
367
|
+
|
|
368
|
+
所有决策都会记录到审计日志。
|
|
369
|
+
|
|
370
|
+
Args:
|
|
371
|
+
action: 操作提案,包含类型、目标路径、描述、来源信息等
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
PermissionDecision: 权限决策结果,包含:
|
|
375
|
+
- outcome: 最终裁决 (ALLOWED/DENIED/PROMPT/ESCALATED)
|
|
376
|
+
- reason: 决策原因说明
|
|
377
|
+
- matched_rule: 匹配到的规则(如有)
|
|
378
|
+
- requires_confirmation: 是否需要用户确认
|
|
379
|
+
- confidence: 决策置信度 [0.1, 1.0]
|
|
380
|
+
- risk_score: 计算后的风险评分 [0.0, 1.0]
|
|
381
|
+
"""
|
|
382
|
+
with self._lock:
|
|
383
|
+
start = time.perf_counter()
|
|
384
|
+
|
|
385
|
+
if self.current_level == PermissionLevel.BYPASS:
|
|
386
|
+
decision = self._make_decision(action, DecisionOutcome.ALLOWED,
|
|
387
|
+
"BYPASS模式: 跳过所有检查")
|
|
388
|
+
self._record_audit(action, decision, start)
|
|
389
|
+
return decision
|
|
390
|
+
|
|
391
|
+
if self.current_level == PermissionLevel.PLAN:
|
|
392
|
+
if action.action_type == ActionType.FILE_READ:
|
|
393
|
+
decision = self._make_decision(action, DecisionOutcome.ALLOWED,
|
|
394
|
+
"PLAN模式允许只读操作")
|
|
395
|
+
else:
|
|
396
|
+
decision = self._make_decision(action, DecisionOutcome.DENIED,
|
|
397
|
+
f"PLAN模式禁止{action.action_type.value}操作")
|
|
398
|
+
self._record_audit(action, decision, start)
|
|
399
|
+
return decision
|
|
400
|
+
|
|
401
|
+
target_str = action.target or ""
|
|
402
|
+
if target_str in self._whitelist or any(
|
|
403
|
+
fnmatch.fnmatch(target_str, p) for p in self._whitelist
|
|
404
|
+
):
|
|
405
|
+
decision = self._make_decision(action, DecisionOutcome.ALLOWED,
|
|
406
|
+
"白名单匹配,直接放行")
|
|
407
|
+
self._record_audit(action, decision, start)
|
|
408
|
+
return decision
|
|
409
|
+
|
|
410
|
+
matched_rule = self._match_rule(action)
|
|
411
|
+
base_risk = self._assess_base_risk(action)
|
|
412
|
+
|
|
413
|
+
if matched_rule:
|
|
414
|
+
rule_level_val = self.LEVEL_ORDER.get(matched_rule.required_level, 1)
|
|
415
|
+
current_level_val = self.LEVEL_ORDER.get(self.current_level, 1)
|
|
416
|
+
|
|
417
|
+
effective_risk = min(1.0, base_risk + matched_rule.risk_boost)
|
|
418
|
+
|
|
419
|
+
if current_level_val >= rule_level_val:
|
|
420
|
+
if effective_risk < 0.3:
|
|
421
|
+
outcome = DecisionOutcome.ALLOWED
|
|
422
|
+
reason = f"规则{matched_rule.rule_id}匹配, 风险低({effective_risk:.2f})"
|
|
423
|
+
elif effective_risk < 0.7 or self.current_level == PermissionLevel.AUTO:
|
|
424
|
+
auto_score = self.auto_classify(action)
|
|
425
|
+
combined = (effective_risk + auto_score) / 2
|
|
426
|
+
if combined < 0.55:
|
|
427
|
+
outcome = DecisionOutcome.ALLOWED
|
|
428
|
+
reason = f"规则{matched_rule.rule_id}匹配, 综合风险可接受({combined:.2f})"
|
|
429
|
+
else:
|
|
430
|
+
outcome = DecisionOutcome.PROMPT
|
|
431
|
+
reason = f"规则{matched_rule.rule_id}匹配, 需确认(风险{combined:.2f})"
|
|
432
|
+
else:
|
|
433
|
+
outcome = DecisionOutcome.PROMPT
|
|
434
|
+
reason = f"规则{matched_rule.rule_id}高风险, 需用户确认(风险{effective_risk:.2f})"
|
|
435
|
+
else:
|
|
436
|
+
if matched_rule.required_level == PermissionLevel.BYPASS:
|
|
437
|
+
outcome = DecisionOutcome.PROMPT
|
|
438
|
+
reason = f"BYPASS级操作需人工确认(规则{matched_rule.rule_id}, 风险{effective_risk:.2f})"
|
|
439
|
+
elif effective_risk > 0.85 and self.current_level == PermissionLevel.PLAN:
|
|
440
|
+
outcome = DecisionOutcome.DENIED
|
|
441
|
+
reason = f"当前级别不足且高风险(需{matched_rule.required_level.value}, 当前{self.current_level.value})"
|
|
442
|
+
else:
|
|
443
|
+
outcome = DecisionOutcome.PROMPT
|
|
444
|
+
reason = f"当前级别不足(需{matched_rule.required_level.value}, 当前{self.current_level.value}), 请确认"
|
|
445
|
+
|
|
446
|
+
action.risk_score = effective_risk
|
|
447
|
+
requires_conf = outcome == DecisionOutcome.PROMPT
|
|
448
|
+
decision = PermissionDecision(
|
|
449
|
+
action=action,
|
|
450
|
+
outcome=outcome,
|
|
451
|
+
matched_rule=matched_rule,
|
|
452
|
+
reason=reason,
|
|
453
|
+
requires_confirmation=requires_conf,
|
|
454
|
+
confidence=max(0.1, 1.0 - effective_risk),
|
|
455
|
+
)
|
|
456
|
+
else:
|
|
457
|
+
fallback_risk = base_risk
|
|
458
|
+
action.risk_score = fallback_risk
|
|
459
|
+
if fallback_risk < 0.3:
|
|
460
|
+
outcome = DecisionOutcome.ALLOWED
|
|
461
|
+
reason = "无匹配规则, 低风险默认允许"
|
|
462
|
+
elif self.current_level == PermissionLevel.AUTO:
|
|
463
|
+
auto_score = self.auto_classify(action)
|
|
464
|
+
if auto_score < 0.4:
|
|
465
|
+
outcome = DecisionOutcome.ALLOWED
|
|
466
|
+
reason = f"AUTO分类安全(score={auto_score:.2f})"
|
|
467
|
+
else:
|
|
468
|
+
outcome = DecisionOutcome.PROMPT
|
|
469
|
+
reason = f"AUTO分类需确认(score={auto_score:.2f})"
|
|
470
|
+
else:
|
|
471
|
+
outcome = DecisionOutcome.PROMPT
|
|
472
|
+
reason = "无匹配规则, 默认需确认"
|
|
473
|
+
|
|
474
|
+
requires_conf = outcome == DecisionOutcome.PROMPT
|
|
475
|
+
decision = PermissionDecision(
|
|
476
|
+
action=action,
|
|
477
|
+
outcome=outcome,
|
|
478
|
+
matched_rule=None,
|
|
479
|
+
reason=reason,
|
|
480
|
+
requires_confirmation=requires_conf,
|
|
481
|
+
confidence=0.7,
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
self._record_audit(action, decision, start)
|
|
485
|
+
return decision
|
|
486
|
+
|
|
487
|
+
def auto_classify(self, action: ProposedAction) -> float:
|
|
488
|
+
"""
|
|
489
|
+
AI 风险分类器 - 5维加权评分模型
|
|
490
|
+
|
|
491
|
+
对操作提案进行多维度风险评估,返回 [0.0, 1.0] 的风险分数。
|
|
492
|
+
各维度及权重:
|
|
493
|
+
- 目标敏感性 (30%): 路径是否包含敏感关键词/敏感路径模式
|
|
494
|
+
- 破坏性 (25%): 是否包含删除/覆盖/强制等破坏性关键词
|
|
495
|
+
- 作用范围 (20%): 通配符/超长目标路径
|
|
496
|
+
- 来源可信度 (15%): Worker 角色是否在已知信任列表
|
|
497
|
+
- 上下文合理性 (10%): 操作是否与任务相关、描述是否充分
|
|
498
|
+
|
|
499
|
+
Args:
|
|
500
|
+
action: 待评估的操作提案
|
|
501
|
+
|
|
502
|
+
Returns:
|
|
503
|
+
float: 风险评分 [0.0, 1.0],越高越危险
|
|
504
|
+
- < 0.3: 低风险,可自动放行
|
|
505
|
+
- 0.3~0.7: 中等风险,需综合判断
|
|
506
|
+
- > 0.7: 高风险,建议用户确认
|
|
507
|
+
"""
|
|
508
|
+
score = 0.0
|
|
509
|
+
target_lower = (action.target or "").lower()
|
|
510
|
+
|
|
511
|
+
dim_sensitivity = self._dim_target_sensitivity(target_lower)
|
|
512
|
+
dim_destructive = self._dim_destructiveness(action)
|
|
513
|
+
dim_scope = self._dim_scope(action.target or "")
|
|
514
|
+
dim_source = self._dim_source_trust(action.source_role_id)
|
|
515
|
+
dim_context = self._dim_context_reasonable(action)
|
|
516
|
+
|
|
517
|
+
weights = [0.30, 0.25, 0.20, 0.15, 0.10]
|
|
518
|
+
dims = [dim_sensitivity, dim_destructive, dim_scope, dim_source, dim_context]
|
|
519
|
+
score = sum(w * d for w, d in zip(weights, dims))
|
|
520
|
+
return max(0.0, min(1.0, score))
|
|
521
|
+
|
|
522
|
+
def _match_rule(self, action: ProposedAction) -> Optional[PermissionRule]:
|
|
523
|
+
best_match = None
|
|
524
|
+
best_strictness = -1
|
|
525
|
+
candidates = self._rules_by_type.get(action.action_type, [])
|
|
526
|
+
for rule in candidates:
|
|
527
|
+
if not rule.enabled:
|
|
528
|
+
continue
|
|
529
|
+
if self._pattern_match(rule.pattern, action.target or ""):
|
|
530
|
+
strictness = self.LEVEL_ORDER.get(rule.required_level, 0)
|
|
531
|
+
if strictness > best_strictness:
|
|
532
|
+
best_match = rule
|
|
533
|
+
best_strictness = strictness
|
|
534
|
+
return best_match
|
|
535
|
+
|
|
536
|
+
def _pattern_match(self, pattern: str, target: str) -> bool:
|
|
537
|
+
if not target:
|
|
538
|
+
return False
|
|
539
|
+
try:
|
|
540
|
+
if fnmatch.fnmatch(target, pattern):
|
|
541
|
+
return True
|
|
542
|
+
if target.startswith(pattern.rstrip("*")):
|
|
543
|
+
return True
|
|
544
|
+
if re.search(pattern, target, re.IGNORECASE):
|
|
545
|
+
return True
|
|
546
|
+
except (re.error, TypeError):
|
|
547
|
+
pass
|
|
548
|
+
if pattern.endswith("*") and target.startswith(pattern[:-1]):
|
|
549
|
+
return True
|
|
550
|
+
return False
|
|
551
|
+
|
|
552
|
+
def _assess_base_risk(self, action: ProposedAction) -> float:
|
|
553
|
+
risk = 0.0
|
|
554
|
+
target = action.target or ""
|
|
555
|
+
|
|
556
|
+
for kw in _SENSITIVE_KEYWORDS:
|
|
557
|
+
if kw.lower() in target.lower():
|
|
558
|
+
risk += 0.15
|
|
559
|
+
|
|
560
|
+
for pat in _SENSITIVE_PATH_PATTERNS_COMPILED:
|
|
561
|
+
if pat.search(target):
|
|
562
|
+
risk += 0.25
|
|
563
|
+
|
|
564
|
+
high_risk_actions = {
|
|
565
|
+
ActionType.FILE_DELETE: 0.4,
|
|
566
|
+
ActionType.SHELL_EXECUTE: 0.3,
|
|
567
|
+
ActionType.PROCESS_SPAWN: 0.35,
|
|
568
|
+
ActionType.ENVIRONMENT: 0.3,
|
|
569
|
+
}
|
|
570
|
+
risk += high_risk_actions.get(action.action_type, 0.0)
|
|
571
|
+
|
|
572
|
+
if len(target) < 3 and action.action_type != ActionType.FILE_READ:
|
|
573
|
+
risk += 0.1
|
|
574
|
+
|
|
575
|
+
return min(1.0, risk)
|
|
576
|
+
|
|
577
|
+
def _dim_target_sensitivity(self, target_lower: str) -> float:
|
|
578
|
+
score = 0.0
|
|
579
|
+
for kw in _SENSITIVE_KEYWORDS:
|
|
580
|
+
if kw in target_lower:
|
|
581
|
+
score += 0.2
|
|
582
|
+
for pat in _SENSITIVE_PATH_PATTERNS:
|
|
583
|
+
if re.search(pat, target_lower):
|
|
584
|
+
score += 0.3
|
|
585
|
+
return min(1.0, score)
|
|
586
|
+
|
|
587
|
+
def _dim_destructiveness(self, action: ProposedAction) -> float:
|
|
588
|
+
destructive_keywords = ["rm ", "rm-", "delete", "drop", "truncate", "overwrite",
|
|
589
|
+
"force", "-f", "--force", "clear", "reset", "sudo"]
|
|
590
|
+
content = ((action.target or "") + " " + (action.description or "")).lower()
|
|
591
|
+
count = sum(1 for kw in destructive_keywords if kw in content)
|
|
592
|
+
base = min(1.0, count * 0.25)
|
|
593
|
+
if action.action_type == ActionType.SHELL_EXECUTE and count > 0:
|
|
594
|
+
base = min(1.0, base + 0.2)
|
|
595
|
+
return base
|
|
596
|
+
|
|
597
|
+
def _dim_scope(self, target: str) -> float:
|
|
598
|
+
if "*" in target or "?" in target:
|
|
599
|
+
return 0.6
|
|
600
|
+
if len(target) > 200:
|
|
601
|
+
return 0.4
|
|
602
|
+
return 0.1
|
|
603
|
+
|
|
604
|
+
def _dim_source_trust(self, role_id: Optional[str]) -> float:
|
|
605
|
+
known_roles = {"architect", "product-manager", "tester",
|
|
606
|
+
"solo-coder", "ui-designer", "devops"}
|
|
607
|
+
if role_id and role_id in known_roles:
|
|
608
|
+
return 0.1
|
|
609
|
+
return 0.5
|
|
610
|
+
|
|
611
|
+
def _dim_context_reasonable(self, action: ProposedAction) -> bool:
|
|
612
|
+
if action.metadata.get("task_related"):
|
|
613
|
+
return 0.1
|
|
614
|
+
if action.description and len(action.description) > 20:
|
|
615
|
+
return 0.2
|
|
616
|
+
return 0.4
|
|
617
|
+
|
|
618
|
+
def _make_decision(self, action: ProposedAction,
|
|
619
|
+
outcome: DecisionOutcome, reason: str) -> PermissionDecision:
|
|
620
|
+
return PermissionDecision(
|
|
621
|
+
action=action,
|
|
622
|
+
outcome=outcome,
|
|
623
|
+
reason=reason,
|
|
624
|
+
confidence=1.0 if outcome != DecisionOutcome.ESCALATED else 0.5,
|
|
625
|
+
)
|
|
626
|
+
|
|
627
|
+
def _record_audit(self, action: ProposedAction,
|
|
628
|
+
decision: PermissionDecision, start_time: float):
|
|
629
|
+
if not self.audit_log_enabled:
|
|
630
|
+
return
|
|
631
|
+
duration_ms = int((time.perf_counter() - start_time) * 1000)
|
|
632
|
+
entry = AuditEntry(
|
|
633
|
+
action=action,
|
|
634
|
+
decision=decision,
|
|
635
|
+
duration_ms=duration_ms,
|
|
636
|
+
guard_level=self.current_level,
|
|
637
|
+
session_id=self.session_id,
|
|
638
|
+
)
|
|
639
|
+
self._audit_log.append(entry)
|
|
640
|
+
if len(self._audit_log) > 10000:
|
|
641
|
+
self._audit_log = self._audit_log[-5000:]
|
|
642
|
+
|
|
643
|
+
def add_rule(self, rule: PermissionRule) -> None:
|
|
644
|
+
"""
|
|
645
|
+
添加或更新权限规则
|
|
646
|
+
|
|
647
|
+
如规则 ID 已存在则更新(替换原规则),否则追加到规则列表。
|
|
648
|
+
同时维护按操作类型分组的索引以加速匹配。
|
|
649
|
+
|
|
650
|
+
Args:
|
|
651
|
+
rule: 要添加的权限规则(含 rule_id, action_type, pattern 等)
|
|
652
|
+
"""
|
|
653
|
+
with self._lock:
|
|
654
|
+
if rule.rule_id in self._rule_index:
|
|
655
|
+
idx = self._rule_index[rule.rule_id]
|
|
656
|
+
old_type = self.rules[idx].action_type
|
|
657
|
+
if old_type != rule.action_type:
|
|
658
|
+
type_list = self._rules_by_type.get(old_type, [])
|
|
659
|
+
if rule in type_list:
|
|
660
|
+
type_list.remove(rule)
|
|
661
|
+
self._rules_by_type.setdefault(rule.action_type, []).append(rule)
|
|
662
|
+
self.rules[idx] = rule
|
|
663
|
+
else:
|
|
664
|
+
self._rule_index[rule.rule_id] = len(self.rules)
|
|
665
|
+
self.rules.append(rule)
|
|
666
|
+
self._rules_by_type.setdefault(rule.action_type, []).append(rule)
|
|
667
|
+
|
|
668
|
+
def remove_rule(self, rule_id: str) -> bool:
|
|
669
|
+
"""
|
|
670
|
+
按ID移除权限规则
|
|
671
|
+
|
|
672
|
+
Args:
|
|
673
|
+
rule_id: 要移除的规则标识符
|
|
674
|
+
|
|
675
|
+
Returns:
|
|
676
|
+
bool: 是否成功移除(False 表示规则不存在)
|
|
677
|
+
"""
|
|
678
|
+
with self._lock:
|
|
679
|
+
if rule_id not in self._rule_index:
|
|
680
|
+
return False
|
|
681
|
+
idx = self._rule_index.pop(rule_id)
|
|
682
|
+
removed = self.rules.pop(idx)
|
|
683
|
+
type_list = self._rules_by_type.get(removed.action_type, [])
|
|
684
|
+
if removed in type_list:
|
|
685
|
+
type_list.remove(removed)
|
|
686
|
+
self._rule_index = {r.rule_id: i for i, r in enumerate(self.rules)}
|
|
687
|
+
return True
|
|
688
|
+
|
|
689
|
+
def set_level(self, level: PermissionLevel) -> None:
|
|
690
|
+
"""
|
|
691
|
+
动态切换安全级别
|
|
692
|
+
|
|
693
|
+
可在运行时提升/降低安全级别,无需重新创建实例。
|
|
694
|
+
典型场景:进入 Plan Mode 时切换为 PLAN 级别。
|
|
695
|
+
|
|
696
|
+
Args:
|
|
697
|
+
level: 目标安全级别
|
|
698
|
+
"""
|
|
699
|
+
self.current_level = level
|
|
700
|
+
|
|
701
|
+
def get_audit_log(self,
|
|
702
|
+
since: Optional[datetime] = None,
|
|
703
|
+
until: Optional[datetime] = None,
|
|
704
|
+
action_type: Optional[ActionType] = None,
|
|
705
|
+
outcome: Optional[DecisionOutcome] = None,
|
|
706
|
+
worker_id: Optional[str] = None,
|
|
707
|
+
limit: Optional[int] = None) -> List[AuditEntry]:
|
|
708
|
+
"""
|
|
709
|
+
查询审计日志(支持多维度过滤)
|
|
710
|
+
|
|
711
|
+
Args:
|
|
712
|
+
since: 起始时间(含)
|
|
713
|
+
until: 截止时间(含)
|
|
714
|
+
action_type: 按操作类型过滤
|
|
715
|
+
outcome: 按决策结果过滤
|
|
716
|
+
worker_id: 按 Worker ID 过滤
|
|
717
|
+
limit: 最大返回条数
|
|
718
|
+
|
|
719
|
+
Returns:
|
|
720
|
+
List[AuditEntry]: 匹配的审计条目列表
|
|
721
|
+
"""
|
|
722
|
+
results = self._audit_log
|
|
723
|
+
if since:
|
|
724
|
+
results = [e for e in results if e.timestamp >= since]
|
|
725
|
+
if until:
|
|
726
|
+
results = [e for e in results if e.timestamp <= until]
|
|
727
|
+
if action_type:
|
|
728
|
+
results = [e for e in results
|
|
729
|
+
if e.action and e.action.action_type == action_type]
|
|
730
|
+
if outcome:
|
|
731
|
+
results = [e for e in results
|
|
732
|
+
if e.decision and e.decision.outcome == outcome]
|
|
733
|
+
if worker_id:
|
|
734
|
+
results = [e for e in results
|
|
735
|
+
if e.action and e.action.source_worker_id == worker_id]
|
|
736
|
+
if limit is not None:
|
|
737
|
+
results = results[:limit]
|
|
738
|
+
return list(results)
|
|
739
|
+
|
|
740
|
+
def get_security_report(self) -> Dict[str, Any]:
|
|
741
|
+
"""
|
|
742
|
+
生成安全报告摘要
|
|
743
|
+
|
|
744
|
+
聚合所有审计日志数据,计算统计指标:
|
|
745
|
+
- 各决策结果的计数和占比
|
|
746
|
+
- 平均风险评分
|
|
747
|
+
- 最常被拒绝的操作目标 (Top 5)
|
|
748
|
+
- 当前规则数和白名单数
|
|
749
|
+
|
|
750
|
+
Returns:
|
|
751
|
+
Dict[str, Any]: 安全报告字典
|
|
752
|
+
"""
|
|
753
|
+
total = len(self._audit_log)
|
|
754
|
+
allowed = sum(1 for e in self._audit_log
|
|
755
|
+
if e.decision and e.decision.outcome == DecisionOutcome.ALLOWED)
|
|
756
|
+
denied = sum(1 for e in self._audit_log
|
|
757
|
+
if e.decision and e.decision.outcome == DecisionOutcome.DENIED)
|
|
758
|
+
prompted = sum(1 for e in self._audit_log
|
|
759
|
+
if e.decision and e.decision.outcome == DecisionOutcome.PROMPT)
|
|
760
|
+
escalated = sum(1 for e in self._audit_log
|
|
761
|
+
if e.decision and e.decision.outcome == DecisionOutcome.ESCALATED)
|
|
762
|
+
|
|
763
|
+
avg_risk = 0.0
|
|
764
|
+
if total > 0:
|
|
765
|
+
risks = [e.action.risk_score for e in self._audit_log if e.action]
|
|
766
|
+
avg_risk = sum(risks) / len(risks) if risks else 0.0
|
|
767
|
+
|
|
768
|
+
denied_targets: Dict[str, int] = {}
|
|
769
|
+
for e in self._audit_log:
|
|
770
|
+
if e.decision and e.decision.outcome == DecisionOutcome.DENIED and e.action:
|
|
771
|
+
t = e.action.target or "unknown"
|
|
772
|
+
denied_targets[t] = denied_targets.get(t, 0) + 1
|
|
773
|
+
top_denied = sorted(denied_targets.items(), key=lambda x: x[1], reverse=True)[:5]
|
|
774
|
+
|
|
775
|
+
return {
|
|
776
|
+
"total_checks": total,
|
|
777
|
+
"allowed": allowed,
|
|
778
|
+
"denied": denied,
|
|
779
|
+
"prompted": prompted,
|
|
780
|
+
"escalated": escalated,
|
|
781
|
+
"avg_risk_score": round(avg_risk, 3),
|
|
782
|
+
"top_denied_actions": top_denied,
|
|
783
|
+
"guard_level": self.current_level.value,
|
|
784
|
+
"rules_count": len(self.rules),
|
|
785
|
+
"whitelist_count": len(self._whitelist),
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
def add_whitelist(self, pattern: str) -> None:
|
|
789
|
+
"""将模式加入白名单(匹配时直接放行,跳过规则检查)"""
|
|
790
|
+
self._whitelist.add(pattern)
|
|
791
|
+
|
|
792
|
+
def remove_whitelist(self, pattern: str) -> None:
|
|
793
|
+
"""从白名单中移除指定模式"""
|
|
794
|
+
self._whitelist.discard(pattern)
|
|
795
|
+
|
|
796
|
+
def get_whitelist(self) -> Set[str]:
|
|
797
|
+
"""
|
|
798
|
+
获取当前白名单集合
|
|
799
|
+
|
|
800
|
+
Returns:
|
|
801
|
+
Set[str]: 白名单模式集合的副本
|
|
802
|
+
"""
|
|
803
|
+
return set(self._whitelist)
|
|
804
|
+
|
|
805
|
+
def export_rules(self) -> List[Dict]:
|
|
806
|
+
"""
|
|
807
|
+
导出所有启用的规则为字典列表
|
|
808
|
+
|
|
809
|
+
Returns:
|
|
810
|
+
List[Dict]: 每条规则的 to_dict() 序列化结果
|
|
811
|
+
"""
|
|
812
|
+
return [r.to_dict() for r in self.rules if r.enabled]
|
|
813
|
+
|
|
814
|
+
def import_rules(self, rules_data: List[Dict]) -> int:
|
|
815
|
+
"""
|
|
816
|
+
从字典列表批量导入规则
|
|
817
|
+
|
|
818
|
+
跳过格式无效的条目,返回成功导入的数量。
|
|
819
|
+
|
|
820
|
+
Args:
|
|
821
|
+
rules_data: 规则字典列表(每项需含 rule_id, action_type, pattern, required_level)
|
|
822
|
+
|
|
823
|
+
Returns:
|
|
824
|
+
int: 成功导入的规则数量
|
|
825
|
+
"""
|
|
826
|
+
count = 0
|
|
827
|
+
for rd in rules_data:
|
|
828
|
+
try:
|
|
829
|
+
rule = PermissionRule.from_dict(rd)
|
|
830
|
+
self.add_rule(rule)
|
|
831
|
+
count += 1
|
|
832
|
+
except (KeyError, ValueError):
|
|
833
|
+
continue
|
|
834
|
+
return count
|
|
835
|
+
|
|
836
|
+
def export_state(self) -> Dict:
|
|
837
|
+
"""
|
|
838
|
+
导出完整状态快照
|
|
839
|
+
|
|
840
|
+
包含当前级别、所有规则、白名单、会话ID和审计计数。
|
|
841
|
+
可用于持久化或跨实例迁移。
|
|
842
|
+
|
|
843
|
+
Returns:
|
|
844
|
+
Dict: 完整状态字典
|
|
845
|
+
"""
|
|
846
|
+
with self._lock:
|
|
847
|
+
return {
|
|
848
|
+
"current_level": self.current_level.value,
|
|
849
|
+
"rules": [r.to_dict() for r in self.rules],
|
|
850
|
+
"whitelist": list(self._whitelist),
|
|
851
|
+
"session_id": self.session_id,
|
|
852
|
+
"audit_count": len(self._audit_log),
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
def clear_audit_log(self) -> int:
|
|
856
|
+
"""
|
|
857
|
+
清空审计日志
|
|
858
|
+
|
|
859
|
+
Returns:
|
|
860
|
+
int: 清空前日志中的条目数
|
|
861
|
+
"""
|
|
862
|
+
with self._lock:
|
|
863
|
+
count = len(self._audit_log)
|
|
864
|
+
self._audit_log.clear()
|
|
865
|
+
return count
|